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"
|
||||
"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
|
||||
|
|
|
@ -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