diff --git a/wox.core/updater/updater.go b/wox.core/updater/updater.go index 19686bda..b5b45fa8 100644 --- a/wox.core/updater/updater.go +++ b/wox.core/updater/updater.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "time" "wox/setting" @@ -62,6 +61,12 @@ type UpdateInfo struct { 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 func StartAutoUpdateChecker(ctx context.Context) { util.Go(ctx, "auto-update-checker", func() { @@ -279,461 +284,19 @@ func ApplyUpdate(ctx context.Context) error { if currentUpdateInfo.Status != UpdateStatusReady || currentUpdateInfo.DownloadedPath == "" { return errors.New("no update ready to apply") } - filePath := 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) - } - } + newPath := currentUpdateInfo.DownloadedPath // Get the current executable path - execPath, err := os.Executable() + oldPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get current executable path: %w", err) } - // On Windows, we can't replace the running executable directly - // 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, - ) + pid := os.Getegid() - batchPath := filepath.Join(filepath.Dir(filePath), "update.bat") - if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil { - return fmt.Errorf("failed to create update batch file: %w", err) - } + util.GetLogger().Info(ctx, fmt.Sprintf("Applying update from %s to %s, pid: %d", oldPath, newPath, pid)) - // Execute the batch file - 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 + return applyUpdaterInstance.ApplyUpdate(ctx, pid, oldPath, newPath) } // calculateFileChecksum calculates the MD5 checksum of a file diff --git a/wox.core/updater/updater_darwin.go b/wox.core/updater/updater_darwin.go new file mode 100644 index 00000000..a509837f --- /dev/null +++ b/wox.core/updater/updater_darwin.go @@ -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) +} diff --git a/wox.core/updater/updater_linux.go b/wox.core/updater/updater_linux.go new file mode 100644 index 00000000..bdd3f71e --- /dev/null +++ b/wox.core/updater/updater_linux.go @@ -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) +} diff --git a/wox.core/updater/updater_windows.go b/wox.core/updater/updater_windows.go new file mode 100644 index 00000000..2b1ea1a9 --- /dev/null +++ b/wox.core/updater/updater_windows.go @@ -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) +}