mirror of https://github.com/Wox-launcher/Wox
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:
parent
8e5adf7c5c
commit
b0a1405d12
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue