feat(updater): implement platform-specific update mechanisms for Windows, macOS, and Linux

* Added `MacOSUpdater` to handle DMG extraction and application updates on macOS.
* Introduced `LinuxUpdater` for managing application updates on Linux systems.
* Implemented `WindowsUpdater` for executing batch scripts to replace the application on Windows.
* Each updater utilizes a shell script or batch file to manage the update process seamlessly.
This commit is contained in:
qianlifeng 2025-05-27 11:04:12 +08:00
parent 8e5adf7c5c
commit b0a1405d12
No known key found for this signature in database
4 changed files with 356 additions and 448 deletions

View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"time" "time"
"wox/setting" "wox/setting"
@ -62,6 +61,12 @@ type UpdateInfo struct {
HasUpdate bool // Whether there is an update available HasUpdate bool // Whether there is an update available
} }
type applyUpdater interface {
ApplyUpdate(ctx context.Context, pid int, oldPath, newPath string) error
}
var applyUpdaterInstance applyUpdater
// StartAutoUpdateChecker starts a background task that periodically checks for updates // StartAutoUpdateChecker starts a background task that periodically checks for updates
func StartAutoUpdateChecker(ctx context.Context) { func StartAutoUpdateChecker(ctx context.Context) {
util.Go(ctx, "auto-update-checker", func() { util.Go(ctx, "auto-update-checker", func() {
@ -279,461 +284,19 @@ func ApplyUpdate(ctx context.Context) error {
if currentUpdateInfo.Status != UpdateStatusReady || currentUpdateInfo.DownloadedPath == "" { if currentUpdateInfo.Status != UpdateStatusReady || currentUpdateInfo.DownloadedPath == "" {
return errors.New("no update ready to apply") return errors.New("no update ready to apply")
} }
filePath := currentUpdateInfo.DownloadedPath newPath := currentUpdateInfo.DownloadedPath
// Make the file executable (for Unix systems)
if !util.IsWindows() {
if err := os.Chmod(filePath, 0755); err != nil {
return fmt.Errorf("failed to make update executable: %w", err)
}
}
// Get the current executable path // Get the current executable path
execPath, err := os.Executable() oldPath, err := os.Executable()
if err != nil { if err != nil {
return fmt.Errorf("failed to get current executable path: %w", err) return fmt.Errorf("failed to get current executable path: %w", err)
} }
// On Windows, we can't replace the running executable directly pid := os.Getegid()
// So we need to use a batch file or similar approach to replace it after the app exits
if util.IsWindows() {
// Create a batch file to replace the executable after the app exits
batchContent := fmt.Sprintf(
"@echo off\n"+
":loop\n"+
"tasklist | find /i \"wox.exe\" >nul 2>&1\n"+
"if errorlevel 1 (\n"+
" move /y \"%s\" \"%s\"\n"+
" start \"\" \"%s\"\n"+
" del %%0\n"+
") else (\n"+
" timeout /t 1 /nobreak >nul\n"+
" goto loop\n"+
")\n",
filePath, execPath, execPath,
)
batchPath := filepath.Join(filepath.Dir(filePath), "update.bat") util.GetLogger().Info(ctx, fmt.Sprintf("Applying update from %s to %s, pid: %d", oldPath, newPath, pid))
if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil {
return fmt.Errorf("failed to create update batch file: %w", err)
}
// Execute the batch file return applyUpdaterInstance.ApplyUpdate(ctx, pid, oldPath, newPath)
util.GetLogger().Info(ctx, "starting update process")
cmd := exec.Command("cmd", "/c", "start", "", batchPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
os.Exit(0)
} else if util.IsMacOS() {
// On macOS, we need to mount the DMG file, copy the app to Applications, and then restart
// Create a shell script to handle the DMG installation after the app exits
// Get log directory for update logs
logDir := util.GetLocation().GetLogDirectory()
updateLogFile := filepath.Join(logDir, "update.log")
shellContent := fmt.Sprintf(
`#!/bin/bash
# Log file setup
LOG_FILE="%s"
LOG_TIMESTAMP=$(date "+%%Y-%%m-%%d %%H:%%M:%%S")
# Log function
log() {
echo "$LOG_TIMESTAMP $1" >> "$LOG_FILE"
echo "$1"
}
log "Update process started for version %s"
# Wait for the current app to exit
log "Waiting for current application to exit..."
log "Looking for Wox application process"
WAIT_COUNT=0
# Get the actual executable name and path
EXEC_NAME=$(basename %s)
EXEC_PATH=%s
log "Executable name: $EXEC_NAME"
log "Executable path: $EXEC_PATH"
# Function to check if the real Wox application is running
is_wox_running() {
# More specific search to avoid matching update scripts and tail commands
# Look for the actual executable in Applications folder or the original path
if pgrep -f "/Applications/Wox.app/Contents/MacOS/wox" > /dev/null 2>&1; then
return 0 # Found
fi
if [ -f "$EXEC_PATH" ] && pgrep -f "$EXEC_PATH" > /dev/null 2>&1; then
return 0 # Found
fi
# Check for processes with Wox in the name that aren't update scripts or tail
POSSIBLE_WOX=$(ps aux | grep -i "wox" | grep -v "update.sh" | grep -v "tail" | grep -v "grep")
if [ -n "$POSSIBLE_WOX" ]; then
log "Found possible Wox processes:"
echo "$POSSIBLE_WOX" >> "$LOG_FILE"
return 0 # Found
fi
return 1 # Not found
}
while is_wox_running; do
# Log every 5 seconds to show progress
if [ $((WAIT_COUNT %% 5)) -eq 0 ]; then
log "Still waiting for Wox application to exit after ${WAIT_COUNT}s"
log "Current processes with 'wox' in name (excluding update scripts and grep):"
ps aux | grep -i "wox" | grep -v "update.sh" | grep -v "tail" | grep -v "grep" >> "$LOG_FILE"
fi
# After 30 seconds, provide more detailed information
if [ $WAIT_COUNT -eq 30 ]; then
log "Waiting for a long time (30s). Detailed process information:"
ps aux | grep -i "wox" | grep -v "grep" >> "$LOG_FILE"
log "All processes for current user:"
ps -u $(whoami) >> "$LOG_FILE"
fi
# After 60 seconds, force continue
if [ $WAIT_COUNT -eq 60 ]; then
log "WARNING: Waited for 60 seconds. Forcing continue."
log "The update will proceed but may not complete properly if Wox is still running."
break
fi
sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
done
log "Wox application has exited or timeout reached after ${WAIT_COUNT}s"
# Check if DMG file exists
log "Checking if DMG file exists: %s"
if [ ! -f "%s" ]; then
log "ERROR: DMG file does not exist"
log "Current directory: $(pwd)"
log "Directory listing:"
ls -la "$(dirname "%s")" >> "$LOG_FILE"
exit 1
fi
log "DMG file exists and has size: $(du -h "%s" | cut -f1)"
log "DMG file details:"
file "%s" >> "$LOG_FILE"
# Mount the DMG file
log "Mounting DMG file: %s"
log "Using hdiutil attach command"
MOUNT_OUTPUT=$(hdiutil attach -nobrowse -verbose "%s" 2>&1)
log "Full mount output:"
echo "$MOUNT_OUTPUT" >> "$LOG_FILE"
# Try to parse the mount point
VOLUME=$(echo "$MOUNT_OUTPUT" | tail -n1 | awk '{print $NF}')
log "Parsed volume path: $VOLUME"
# Verify if volume path is valid
if [ -z "$VOLUME" ]; then
log "ERROR: Failed to parse volume path from mount output"
log "Trying alternative parsing method"
VOLUME=$(echo "$MOUNT_OUTPUT" | grep "mounted at" | sed 's/.*mounted at //g' | tr -d '\n')
log "Alternative parsed volume path: $VOLUME"
fi
if [ -z "$VOLUME" ]; then
log "ERROR: All parsing methods failed to identify mount point"
log "Listing all volumes to check if DMG was mounted:"
ls -la /Volumes/ >> "$LOG_FILE"
log "Checking for Wox-related volumes:"
find /Volumes -name "*Wox*" -o -name "*wox*" >> "$LOG_FILE"
exit 1
fi
# Verify the volume exists
if [ ! -d "$VOLUME" ]; then
log "ERROR: Parsed volume path does not exist: $VOLUME"
log "Listing all volumes:"
ls -la /Volumes/ >> "$LOG_FILE"
exit 1
fi
log "DMG successfully mounted at: $VOLUME"
log "Volume contents:"
ls -la "$VOLUME" >> "$LOG_FILE"
# Find the .app in the mounted volume
log "Searching for .app in mounted volume: $VOLUME"
log "Listing contents of mounted volume:"
ls -la "$VOLUME" >> "$LOG_FILE"
# Check if we're looking at the right volume
log "Checking all available volumes:"
ls -la /Volumes/ >> "$LOG_FILE"
# Look for Wox-related volumes specifically
log "Looking for Wox-related volumes:"
find /Volumes -name "*Wox*" -o -name "*wox*" -o -name "*Installer*" >> "$LOG_FILE"
# Try different search methods on the parsed volume
log "Trying find command with maxdepth 1 on $VOLUME"
APP_PATH=$(find "$VOLUME" -name "*.app" -maxdepth 1)
if [ -z "$APP_PATH" ]; then
log "No app found with maxdepth 1, trying maxdepth 2"
APP_PATH=$(find "$VOLUME" -name "*.app" -maxdepth 2)
fi
if [ -z "$APP_PATH" ]; then
log "Still no app found, trying ls command"
APP_PATH=$(ls -d "$VOLUME"/*.app 2>/dev/null)
fi
# If still not found, try searching in all Wox-related volumes
if [ -z "$APP_PATH" ]; then
log "No app found in primary volume, checking all Wox-related volumes"
for VOL in $(find /Volumes -name "*Wox*" -o -name "*wox*" -o -name "*Installer*" -type d); do
log "Checking volume: $VOL"
ls -la "$VOL" >> "$LOG_FILE"
FOUND_APP=$(find "$VOL" -name "*.app" -maxdepth 2)
if [ -n "$FOUND_APP" ]; then
APP_PATH="$FOUND_APP"
VOLUME="$VOL"
log "Found app in alternative volume: $VOL"
break
fi
done
fi
# If still not found, try a more aggressive search
if [ -z "$APP_PATH" ]; then
log "Still no app found, trying more aggressive search in all volumes"
for VOL in /Volumes/*; do
if [ -d "$VOL" ]; then
log "Checking volume: $VOL"
FOUND_APP=$(find "$VOL" -name "*.app" -maxdepth 2 2>/dev/null)
if [ -n "$FOUND_APP" ]; then
APP_PATH="$FOUND_APP"
VOLUME="$VOL"
log "Found app in volume: $VOL"
break
fi
fi
done
fi
# Last resort: check if Wox.app is already in Applications
if [ -z "$APP_PATH" ] && [ -d "/Applications/Wox.app" ]; then
log "No app found in DMG, but Wox.app exists in Applications"
log "Using existing Wox.app as the target"
APP_PATH="/Applications/Wox.app"
# Skip the copy step later by setting a flag
SKIP_COPY=1
elif [ -z "$APP_PATH" ]; then
log "ERROR: No .app found in DMG or any volumes using multiple methods"
log "Volume contents (recursive):"
find "$VOLUME" -type d -maxdepth 3 >> "$LOG_FILE"
log "All volumes contents:"
ls -la /Volumes/ >> "$LOG_FILE"
log "Detaching volume"
hdiutil detach "$VOLUME" -force
exit 1
fi
log "Found app at: $APP_PATH"
# Copy the app to Applications folder
APP_NAME=$(basename "$APP_PATH")
log "Application name: $APP_NAME"
# Check if we should skip the copy step (set earlier if app is already in Applications)
if [ "${SKIP_COPY:-0}" -eq 1 ]; then
log "Skipping copy step as application is already in Applications folder"
else
log "Checking if application already exists in Applications folder"
if [ -d "/Applications/$APP_NAME" ]; then
log "Existing application found, removing: /Applications/$APP_NAME"
rm -rf "/Applications/$APP_NAME"
if [ $? -ne 0 ]; then
log "ERROR: Failed to remove existing application"
log "Permissions for /Applications:"
ls -la /Applications/ >> "$LOG_FILE"
log "Trying with sudo (may prompt for password)"
sudo rm -rf "/Applications/$APP_NAME"
if [ $? -ne 0 ]; then
log "ERROR: Failed to remove existing application even with sudo"
if [ -n "$VOLUME" ] && [ "$VOLUME" != "/" ]; then
log "Detaching volume"
hdiutil detach "$VOLUME" -force
fi
exit 1
fi
log "Successfully removed existing application with sudo"
else
log "Successfully removed existing application"
fi
else
log "No existing application found in Applications folder"
fi
log "Copying $APP_PATH to /Applications/"
log "Using cp -R command"
CP_OUTPUT=$(cp -Rv "$APP_PATH" /Applications/ 2>&1)
CP_STATUS=$?
log "Copy command output:"
echo "$CP_OUTPUT" >> "$LOG_FILE"
if [ $CP_STATUS -ne 0 ]; then
log "ERROR: Failed to copy application to /Applications/ (status: $CP_STATUS)"
log "Checking permissions:"
ls -la "$APP_PATH" >> "$LOG_FILE"
ls -la /Applications/ >> "$LOG_FILE"
log "Trying with sudo (may prompt for password)"
sudo cp -R "$APP_PATH" /Applications/
if [ $? -ne 0 ]; then
log "ERROR: Failed to copy application even with sudo"
if [ -n "$VOLUME" ] && [ "$VOLUME" != "/" ]; then
log "Detaching volume"
hdiutil detach "$VOLUME" -force
fi
exit 1
fi
log "Successfully copied application with sudo"
else
log "Application copied successfully"
fi
fi
log "Verifying application exists in Applications folder"
if [ ! -d "/Applications/$APP_NAME" ]; then
log "ERROR: Application not found in /Applications"
log "Applications directory contents:"
ls -la /Applications/ >> "$LOG_FILE"
if [ -n "$VOLUME" ] && [ "$VOLUME" != "/" ]; then
log "Detaching volume"
hdiutil detach "$VOLUME" -force
fi
exit 1
fi
log "Application verified in /Applications folder"
# Detach the DMG if it was mounted
if [ -n "$VOLUME" ] && [ "$VOLUME" != "/" ] && [ -d "$VOLUME" ]; then
log "Detaching DMG volume: $VOLUME"
DETACH_OUTPUT=$(hdiutil detach "$VOLUME" -force 2>&1)
DETACH_STATUS=$?
log "Detach output:"
echo "$DETACH_OUTPUT" >> "$LOG_FILE"
if [ $DETACH_STATUS -ne 0 ]; then
log "WARNING: Failed to detach volume (status: $DETACH_STATUS)"
log "This is not critical, continuing with update"
else
log "Successfully detached volume"
fi
else
log "No volume to detach or volume is not valid"
fi
# Open the new app
log "Opening new application: /Applications/$APP_NAME"
log "Using open command"
OPEN_OUTPUT=$(open "/Applications/$APP_NAME" 2>&1)
OPEN_STATUS=$?
log "Open command output:"
echo "$OPEN_OUTPUT" >> "$LOG_FILE"
if [ $OPEN_STATUS -ne 0 ]; then
log "ERROR: Failed to open new application (status: $OPEN_STATUS)"
log "Checking application bundle:"
ls -la "/Applications/$APP_NAME" >> "$LOG_FILE"
log "Checking application executable:"
ls -la "/Applications/$APP_NAME/Contents/MacOS/" >> "$LOG_FILE"
log "Trying alternative open method"
OPEN_OUTPUT=$(open -a "/Applications/$APP_NAME" 2>&1)
if [ $? -ne 0 ]; then
log "ERROR: Alternative open method also failed"
log "Application may need to be opened manually"
# Don't exit with error as the update itself was successful
else
log "Alternative open method succeeded"
fi
else
log "New application opened successfully"
fi
# Check if application is running
sleep 2
log "Checking if application is running"
PS_OUTPUT=$(ps aux | grep -i "$APP_NAME" | grep -v grep)
log "Process check output:"
echo "$PS_OUTPUT" >> "$LOG_FILE"
if [ -z "$PS_OUTPUT" ]; then
log "WARNING: Application does not appear to be running"
log "User may need to open the application manually"
else
log "Application appears to be running"
fi
# Clean up
log "Cleaning up update script"
log "Update process completed successfully"
rm -f "$0"
`,
updateLogFile, currentUpdateInfo.LatestVersion, execPath, execPath, filePath, filePath, filePath, filePath, filePath, filePath, filePath,
)
shellPath := filepath.Join(filepath.Dir(filePath), "update.sh")
if err := os.WriteFile(shellPath, []byte(shellContent), 0755); err != nil {
return fmt.Errorf("failed to create update shell script: %w", err)
}
// Execute the shell script
util.GetLogger().Info(ctx, "starting update process")
cmd := exec.Command("bash", shellPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
os.Exit(0)
} else {
// On Linux, we can replace the executable and restart
// Create a shell script to replace the executable after the app exits
shellContent := fmt.Sprintf(
"#!/bin/bash\n"+
"while pgrep -f $(basename %s) > /dev/null; do\n"+
" sleep 1\n"+
"done\n"+
"cp %s %s\n"+
"chmod +x %s\n"+
"%s &\n"+
"rm $0\n",
execPath, filePath, execPath, execPath, execPath,
)
shellPath := filepath.Join(filepath.Dir(filePath), "update.sh")
if err := os.WriteFile(shellPath, []byte(shellContent), 0755); err != nil {
return fmt.Errorf("failed to create update shell script: %w", err)
}
// Execute the shell script
util.GetLogger().Info(ctx, "starting update process")
cmd := exec.Command("bash", shellPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
os.Exit(0)
}
return nil
} }
// calculateFileChecksum calculates the MD5 checksum of a file // calculateFileChecksum calculates the MD5 checksum of a file

View File

@ -0,0 +1,239 @@
package updater
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"wox/util"
)
func init() {
applyUpdaterInstance = &MacOSUpdater{}
}
type MacOSUpdater struct{}
// extractAppFromDMG extracts the app from a DMG file to a temporary directory
// Returns the path to the extracted app
func extractAppFromDMG(ctx context.Context, dmgPath string) (string, error) {
util.GetLogger().Info(ctx, fmt.Sprintf("Extracting app from DMG file: %s", dmgPath))
// Check if DMG file exists
if _, err := os.Stat(dmgPath); err != nil {
return "", fmt.Errorf("DMG file does not exist: %w", err)
}
// Create a temporary directory to store the extracted app
tempDir, err := os.MkdirTemp("", "wox_update_*")
if err != nil {
return "", fmt.Errorf("failed to create temporary directory: %w", err)
}
// Mount the DMG file
cmd := exec.Command("hdiutil", "attach", "-nobrowse", "-mountpoint", tempDir, dmgPath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to mount DMG: %s, stderr: %s", err, stderr.String())
}
// Find the app in the mounted DMG
var appPath string
err = filepath.WalkDir(tempDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && strings.HasSuffix(path, ".app") {
appPath = path
return filepath.SkipAll
}
return nil
})
if err != nil {
exec.Command("hdiutil", "detach", tempDir, "-force").Run()
os.RemoveAll(tempDir)
return "", fmt.Errorf("error searching for app: %w", err)
}
if appPath == "" {
exec.Command("hdiutil", "detach", tempDir, "-force").Run()
os.RemoveAll(tempDir)
return "", errors.New("no .app found in DMG")
}
util.GetLogger().Info(ctx, fmt.Sprintf("Found app at: %s", appPath))
// Create a new temporary directory for the extracted app
extractDir, err := os.MkdirTemp("", "wox_app_*")
if err != nil {
exec.Command("hdiutil", "detach", tempDir, "-force").Run()
os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to create extraction directory: %w", err)
}
// Copy the app to the extraction directory
appName := filepath.Base(appPath)
extractedAppPath := filepath.Join(extractDir, appName)
util.GetLogger().Info(ctx, fmt.Sprintf("Copying app to temporary directory: %s", extractedAppPath))
cmd = exec.Command("cp", "-R", appPath, extractDir)
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
exec.Command("hdiutil", "detach", tempDir, "-force").Run()
os.RemoveAll(tempDir)
os.RemoveAll(extractDir)
return "", fmt.Errorf("failed to copy app: %s, stderr: %s", err, stderr.String())
}
// Unmount the DMG
if err := exec.Command("hdiutil", "detach", tempDir, "-force").Run(); err != nil {
util.GetLogger().Info(ctx, fmt.Sprintf("Warning: failed to unmount DMG: %s", err))
}
// Clean up the mount point
os.RemoveAll(tempDir)
util.GetLogger().Info(ctx, fmt.Sprintf("App extracted successfully to: %s", extractedAppPath))
return extractedAppPath, nil
}
func (u *MacOSUpdater) ApplyUpdate(ctx context.Context, pid int, oldPath, newPath string) error {
updateLogFile := filepath.Join(util.GetLocation().GetLogDirectory(), "update.log")
util.GetLogger().Info(ctx, fmt.Sprintf("Processing DMG file: %s", newPath))
extractedAppPath, err := extractAppFromDMG(ctx, newPath)
if err != nil {
return fmt.Errorf("failed to extract app from DMG: %w", err)
}
util.GetLogger().Info(ctx, fmt.Sprintf("App extracted to: %s", extractedAppPath))
// Create a shell script that will wait for the app to exit and then copy the extracted app
shellContent := fmt.Sprintf(
`#!/bin/bash
# Log file setup
LOG_FILE="%s"
LOG_TIMESTAMP=$(date "+%%Y-%%m-%%d %%H:%%M:%%S")
# Log function
log() {
echo "$LOG_TIMESTAMP $1" >> "$LOG_FILE"
echo "$1"
}
log "Update process started for version %s"
log "Extracted app path: %s"
# Wait for the current app to exit
log "Waiting for application with PID %d to exit..."
WAIT_COUNT=0
# Simple function to check if the PID is still running
is_process_running() {
ps -p %d > /dev/null 2>&1
return $?
}
# Wait for the process to exit
while is_process_running; do
# Log every 5 seconds to show progress
if [ $((WAIT_COUNT %% 5)) -eq 0 ]; then
log "Still waiting for application to exit after ${WAIT_COUNT}s"
fi
# After 30 seconds, force continue
if [ $WAIT_COUNT -eq 30 ]; then
log "WARNING: Waited for 30 seconds. Forcing continue."
break
fi
sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
done
log "Application has exited or timeout reached after ${WAIT_COUNT}s"
# Now that the app has exited, copy the extracted app to Applications
APP_PATH="%s"
APP_NAME=$(basename "$APP_PATH")
log "Copying $APP_NAME to /Applications/"
# Remove existing app if it exists
if [ -d "/Applications/$APP_NAME" ]; then
log "Removing existing app: /Applications/$APP_NAME"
rm -rf "/Applications/$APP_NAME"
if [ $? -ne 0 ]; then
log "Failed to remove existing app, trying with sudo"
sudo rm -rf "/Applications/$APP_NAME"
if [ $? -ne 0 ]; then
log "ERROR: Failed to remove existing app even with sudo"
exit 1
fi
fi
fi
# Copy the app
log "Copying app to Applications folder"
cp -R "$APP_PATH" "/Applications/"
if [ $? -ne 0 ]; then
log "Failed to copy app, trying with sudo"
sudo cp -R "$APP_PATH" "/Applications/"
if [ $? -ne 0 ]; then
log "ERROR: Failed to copy app to Applications"
exit 1
fi
fi
# Verify the app was copied
if [ ! -d "/Applications/$APP_NAME" ]; then
log "ERROR: App was not copied to Applications"
exit 1
fi
log "App copied successfully to Applications folder"
# Clean up the temporary directory
log "Cleaning up temporary directory"
rm -rf "$(dirname "$APP_PATH")"
# Open the new app
log "Opening new application: /Applications/$APP_NAME"
open "/Applications/$APP_NAME" || open -a "/Applications/$APP_NAME"
# Clean up
log "Cleaning up update script"
log "Update process completed successfully"
rm -f "$0"
`,
updateLogFile, currentUpdateInfo.LatestVersion, extractedAppPath, pid, pid, extractedAppPath,
)
// Write the shell script
shellPath := filepath.Join(filepath.Dir(newPath), "update.sh")
if err := os.WriteFile(shellPath, []byte(shellContent), 0755); err != nil {
return fmt.Errorf("failed to create update shell script: %w", err)
}
// Execute the shell script
util.GetLogger().Info(ctx, "starting update process")
cmd := exec.Command("bash", shellPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
os.Exit(0)
return nil // This line will never be reached due to os.Exit(0)
}

View File

@ -0,0 +1,54 @@
package updater
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"wox/util"
)
func init() {
applyUpdaterInstance = &LinuxUpdater{}
}
type LinuxUpdater struct{}
func (u *LinuxUpdater) ApplyUpdate(ctx context.Context, pid int, oldPath, newPath string) error {
// Create a shell script to replace the executable after the app exits
shellContent := fmt.Sprintf(
"#!/bin/bash\n"+
"# Wait for the application to exit\n"+
"while ps -p %d > /dev/null 2>&1 || pgrep -f $(basename %s) > /dev/null 2>&1; do\n"+
" sleep 1\n"+
"done\n"+
"# Replace the executable\n"+
"cp %s %s\n"+
"chmod +x %s\n"+
"# Start the new version\n"+
"%s &\n"+
"# Clean up\n"+
"rm $0\n",
pid, oldPath, newPath, oldPath, oldPath, oldPath,
)
// Write the shell script
shellPath := filepath.Join(filepath.Dir(newPath), "update.sh")
if err := os.WriteFile(shellPath, []byte(shellContent), 0755); err != nil {
return fmt.Errorf("failed to create update shell script: %w", err)
}
// Execute the shell script
util.GetLogger().Info(ctx, "starting Linux update process")
cmd := exec.Command("bash", shellPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
util.GetLogger().Info(ctx, "exiting application for update to proceed")
os.Exit(0)
return nil // This line will never be reached due to os.Exit(0)
}

View File

@ -0,0 +1,52 @@
package updater
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"wox/util"
)
func init() {
applyUpdaterInstance = &WindowsUpdater{}
}
type WindowsUpdater struct{}
func (u *WindowsUpdater) ApplyUpdate(ctx context.Context, pid int, oldPath, newPath string) error {
batchContent := fmt.Sprintf(
"@echo off\n"+
":loop\n"+
"tasklist | find /i \"wox.exe\" >nul 2>&1\n"+
"if errorlevel 1 (\n"+
" move /y \"%s\" \"%s\"\n"+
" start \"\" \"%s\"\n"+
" del %%0\n"+
") else (\n"+
" timeout /t 1 /nobreak >nul\n"+
" goto loop\n"+
")\n",
newPath, oldPath, oldPath,
)
// Write the batch file
batchPath := filepath.Join(filepath.Dir(newPath), "update.bat")
if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil {
return fmt.Errorf("failed to create update batch file: %w", err)
}
// Execute the batch file
util.GetLogger().Info(ctx, "starting Windows update process")
cmd := exec.Command("cmd", "/c", "start", "", batchPath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start update process: %w", err)
}
// Exit the application
util.GetLogger().Info(ctx, "exiting application for update to proceed")
os.Exit(0)
return nil // This line will never be reached due to os.Exit(0)
}