diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index 48d53609..15d11c5e 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -307,16 +307,28 @@ func replaceBinary(ctx context.Context, logger logger.Logger, assetPath, version return fmt.Errorf("failed to extract binary") } - if err := checkWritePermission(currentExe); err != nil { - return fmt.Errorf("insufficient permissions to update binary: %w", err) - } - info, err := os.Stat(currentExe) if err != nil { return fmt.Errorf("failed to get file info: %w", err) } fileMode := info.Mode() + if err := checkWritePermission(currentExe); err != nil { + if strings.Contains(err.Error(), "binary is currently running") { + var updateErr error + updateAction := func() { + updateErr = updateRunningBinary(currentExe, binaryPath, fileMode) + } + tui.ShowSpinner("Setting up background update...", updateAction) + if updateErr != nil { + return fmt.Errorf("failed to set up background update: %w", updateErr) + } + tui.ShowSuccess("Successfully scheduled update to %s. The update will complete when this process exits.", version) + return nil + } + return fmt.Errorf("insufficient permissions to update binary: %w", err) + } + var replaceErr error replaceAction := func() { if err := copyFile(binaryPath, currentExe); err != nil { @@ -369,6 +381,9 @@ func checkWritePermission(filePath string) error { file, err := os.OpenFile(filePath, os.O_WRONLY, info.Mode()) if err != nil { + if strings.Contains(err.Error(), "text file busy") { + return fmt.Errorf("binary is currently running: %w", err) + } return err } file.Close() @@ -376,6 +391,100 @@ func checkWritePermission(filePath string) error { return nil } +// cleanupUpdateFiles removes any leftover temporary files from previous update attempts +func cleanupUpdateFiles(dir string) { + tmpFiles := []string{ + filepath.Join(dir, ".agentuity.new"), + filepath.Join(dir, ".agentuity.old"), + filepath.Join(dir, ".agentuity_updater.sh"), + filepath.Join(dir, ".agentuity_updater.ps1"), + } + + for _, file := range tmpFiles { + _ = os.Remove(file) + } +} + +func updateRunningBinary(currentExe, newBinary string, fileMode os.FileMode) error { + dir := filepath.Dir(currentExe) + tmpBinary := filepath.Join(dir, ".agentuity.new") + oldBinary := filepath.Join(dir, ".agentuity.old") + + cleanupUpdateFiles(dir) + + if err := copyFile(newBinary, tmpBinary); err != nil { + return fmt.Errorf("failed to copy new binary to temp location: %w", err) + } + + if err := os.Chmod(tmpBinary, fileMode); err != nil { + return fmt.Errorf("failed to set permissions on new binary: %w", err) + } + + if runtime.GOOS == "windows" { + script := fmt.Sprintf(` +$currentExe = "%s" +$oldBinary = "%s" +$tmpBinary = "%s" +$updateScript = $MyInvocation.MyCommand.Path + +# Wait for the process to exit (give it a moment) +Start-Sleep -Seconds 1 + +# Perform the update +try { + # Move current binary to old + if (Test-Path $currentExe) { + Move-Item -Path $currentExe -Destination $oldBinary -Force + } + + # Move new binary to current location + Move-Item -Path $tmpBinary -Destination $currentExe -Force + + # Clean up old binary + if (Test-Path $oldBinary) { + Remove-Item -Path $oldBinary -Force + } + + # Clean up this script + Remove-Item -Path $updateScript -Force +} catch { + # If something goes wrong, try to restore the old binary + if (Test-Path $oldBinary) { + Move-Item -Path $oldBinary -Destination $currentExe -Force + } +} +`, currentExe, oldBinary, tmpBinary) + + updateScript := filepath.Join(dir, ".agentuity_updater.ps1") + if err := os.WriteFile(updateScript, []byte(script), 0644); err != nil { + return fmt.Errorf("failed to create update script: %w", err) + } + + cmd := exec.Command("powershell", "-WindowStyle", "Hidden", "-Command", + fmt.Sprintf("Start-Process powershell -ArgumentList '-WindowStyle Hidden -ExecutionPolicy Bypass -File \"%s\"' -WindowStyle Hidden", updateScript)) + return cmd.Start() + } else { + script := fmt.Sprintf(`#!/bin/sh +# Wait for the process to exit +sleep 1 +# Perform the update +mv "%s" "%s" +mv "%s" "%s" +rm "%s" +# Clean up this script +rm -- "$0" +`, currentExe, oldBinary, tmpBinary, currentExe, oldBinary) + + updateScript := filepath.Join(dir, ".agentuity_updater.sh") + if err := os.WriteFile(updateScript, []byte(script), 0755); err != nil { + return fmt.Errorf("failed to create update script: %w", err) + } + + cmd := exec.Command("sh", "-c", fmt.Sprintf("nohup %s > /dev/null 2>&1 &", updateScript)) + return cmd.Start() + } +} + func extractBinary(ctx context.Context, logger logger.Logger, assetPath, extractDir string) string { var extractErr error var binaryPath string