Files
MyGoNavi/internal/app/update_cleanup.go

364 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"io"
"os"
"path/filepath"
stdRuntime "runtime"
"strconv"
"strings"
"GoNavi-Wails/internal/logger"
)
func init() {
updateLaunchInstallScript = launchUpdateScriptWithCleanup
}
func launchUpdateScriptWithCleanup(staged *stagedUpdate) error {
exePath, err := os.Executable()
if err != nil {
return err
}
exePath, _ = filepath.EvalSymlinks(exePath)
pid := os.Getpid()
if stdRuntime.GOOS == "windows" {
return launchWindowsUpdateWithCleanup(staged, exePath, pid)
}
return launchUpdateScript(staged)
}
func launchWindowsUpdateWithCleanup(staged *stagedUpdate, targetExe string, pid int) error {
if staged == nil {
return localizedUpdateError{key: "app.update.backend.message.no_downloaded_package"}
}
if err := os.MkdirAll(staged.StagedDir, 0o755); err != nil {
return err
}
currentTargetExe := strings.TrimSpace(targetExe)
originalSourceDir := strings.TrimSpace(filepath.Dir(staged.FilePath))
preparedSource, err := prepareWindowsStagedUpdateAsset(staged.FilePath, staged.StagedDir)
if err != nil {
return err
}
staged.FilePath = preparedSource
staged.InstallLogPath = buildUpdateInstallLogPath(staged.StagedDir)
finalTargetExe := resolveWindowsUpdateFinalTargetPath(currentTargetExe, staged.FilePath)
cleanupWindowsUpdateArtifacts([]string{
originalSourceDir,
strings.TrimSpace(filepath.Dir(currentTargetExe)),
strings.TrimSpace(filepath.Dir(finalTargetExe)),
}, map[string]struct{}{
cleanComparablePath(currentTargetExe): {},
cleanComparablePath(finalTargetExe): {},
cleanComparablePath(staged.FilePath): {},
cleanComparablePath(staged.StagedDir): {},
})
scriptPath := filepath.Join(staged.StagedDir, "update.cmd")
content := buildWindowsScriptWithCurrentTargetCleanup(staged.FilePath, finalTargetExe, currentTargetExe, staged.StagedDir, staged.InstallLogPath, pid)
if err := os.WriteFile(scriptPath, []byte(content), 0o644); err != nil {
return err
}
logger.Infof("启动 Windows 更新脚本current=%s target=%s script=%s log=%s", currentTargetExe, finalTargetExe, scriptPath, staged.InstallLogPath)
cmd := buildWindowsLaunchCommand(scriptPath)
if err := cmd.Start(); err != nil {
return err
}
if cmd.Process != nil {
if err := cmd.Process.Release(); err != nil {
logger.Warnf("释放 Windows 更新脚本进程句柄失败:%v", err)
}
}
return nil
}
func resolveWindowsUpdateFinalTargetPath(currentTarget string, sourcePath string) string {
currentTarget = strings.TrimSpace(currentTarget)
if currentTarget == "" {
return currentTarget
}
currentName := filepath.Base(currentTarget)
sourceName := filepath.Base(strings.TrimSpace(sourcePath))
if isVersionedWindowsUpdatePackageName(currentName) && isVersionedWindowsUpdatePackageName(sourceName) {
return filepath.Join(filepath.Dir(currentTarget), sourceName)
}
return currentTarget
}
func isVersionedWindowsUpdatePackageName(name string) bool {
trimmed := strings.TrimSpace(name)
lower := strings.ToLower(trimmed)
return strings.HasPrefix(trimmed, "GoNavi-") &&
strings.Contains(trimmed, "-Windows-") &&
strings.HasSuffix(lower, ".exe")
}
func prepareWindowsStagedUpdateAsset(sourcePath string, stagedDir string) (string, error) {
sourcePath = strings.TrimSpace(sourcePath)
stagedDir = strings.TrimSpace(stagedDir)
if sourcePath == "" || stagedDir == "" {
return sourcePath, nil
}
if isUpdateAssetPathInsideStagedDir(sourcePath, stagedDir) {
return sourcePath, nil
}
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
return "", err
}
targetPath := filepath.Join(stagedDir, filepath.Base(sourcePath))
if cleanComparablePath(sourcePath) == cleanComparablePath(targetPath) {
return sourcePath, nil
}
_ = os.Remove(targetPath)
if err := os.Rename(sourcePath, targetPath); err == nil {
return targetPath, nil
}
if err := copyFileForWindowsUpdate(sourcePath, targetPath); err != nil {
return "", err
}
_ = os.Remove(sourcePath)
return targetPath, nil
}
func copyFileForWindowsUpdate(sourcePath string, targetPath string) error {
in, err := os.Open(sourcePath)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
func cleanupWindowsUpdateArtifacts(dirs []string, keep map[string]struct{}) {
seen := map[string]struct{}{}
for _, dir := range dirs {
dir = strings.TrimSpace(dir)
if dir == "" || dir == "." {
continue
}
cleanDir := cleanComparablePath(dir)
if cleanDir == "" {
continue
}
if _, ok := seen[cleanDir]; ok {
continue
}
seen[cleanDir] = struct{}{}
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
path := filepath.Join(dir, entry.Name())
cleanPath := cleanComparablePath(path)
if cleanPath == "" {
continue
}
if _, ok := keep[cleanPath]; ok {
continue
}
if shouldRemoveWindowsUpdateArtifact(entry.Name(), entry.IsDir()) {
if entry.IsDir() {
_ = os.RemoveAll(path)
} else {
_ = os.Remove(path)
}
}
}
}
}
func shouldRemoveWindowsUpdateArtifact(name string, isDir bool) bool {
trimmed := strings.TrimSpace(name)
lower := strings.ToLower(trimmed)
if trimmed == "" {
return false
}
if isDir {
return strings.HasPrefix(lower, ".gonavi-update-")
}
if strings.HasPrefix(lower, "gonavi-update-") && strings.HasSuffix(lower, ".log") {
return true
}
if !strings.HasPrefix(trimmed, "GoNavi-") {
return false
}
if !strings.Contains(trimmed, "-Windows-") {
return false
}
return strings.HasSuffix(lower, ".exe") || strings.HasSuffix(lower, ".zip")
}
func cleanComparablePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
cleaned := filepath.Clean(path)
if cleaned == "." {
return ""
}
if stdRuntime.GOOS == "windows" {
return strings.ToLower(cleaned)
}
return cleaned
}
func buildWindowsScriptWithCleanup(source, target, stagedDir, logPath string, pid int) string {
return buildWindowsScriptWithCurrentTargetCleanup(source, target, target, stagedDir, logPath, pid)
}
func buildWindowsScriptWithCurrentTargetCleanup(source, target, currentTarget, stagedDir, logPath string, pid int) string {
script := `@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "SOURCE=__GONAVI_UPDATE_SOURCE__"
set "TARGET=__GONAVI_UPDATE_TARGET__"
set "CURRENT_TARGET=__GONAVI_CURRENT_TARGET__"
set "TARGET_OLD=%TARGET%.old"
set "STAGED=__GONAVI_UPDATE_STAGED__"
set "LOG_FILE=__GONAVI_UPDATE_LOG__"
set PID=__GONAVI_UPDATE_PID__
set /a WAIT_PID_SECONDS=0
call :log updater started
if not exist "%SOURCE%" (
call :log source file not found: %SOURCE%
exit /b 1
)
for %%I in ("%TARGET%") do set "TARGET_NAME=%%~nxI"
for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"
for %%I in ("%SOURCE%") do set "SOURCE_EXT=%%~xI"
set "SOURCE_EXE="
if /I "%SOURCE_EXT%"==".zip" (
set "EXTRACT_DIR=%STAGED%\_extract"
if exist "%EXTRACT_DIR%" (
rmdir /S /Q "%EXTRACT_DIR%" >> "%LOG_FILE%" 2>&1
)
mkdir "%EXTRACT_DIR%" >> "%LOG_FILE%" 2>&1
powershell -NoProfile -ExecutionPolicy Bypass -Command "$src=$env:SOURCE; $dst=$env:EXTRACT_DIR; Expand-Archive -LiteralPath $src -DestinationPath $dst -Force" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL! NEQ 0 (
call :log expand zip failed: %SOURCE%
exit /b 1
)
if exist "%EXTRACT_DIR%\%TARGET_NAME%" (
set "SOURCE_EXE=%EXTRACT_DIR%\%TARGET_NAME%"
) else (
for /R "%EXTRACT_DIR%" %%F in (*.exe) do (
if not defined SOURCE_EXE (
set "SOURCE_EXE=%%~fF"
)
)
)
if not defined SOURCE_EXE (
call :log no executable found in portable zip: %SOURCE%
exit /b 1
)
) else (
set "SOURCE_EXE=%SOURCE%"
)
:waitloop
tasklist /FI "PID eq %PID%" | find "%PID%" >nul
if %ERRORLEVEL%==0 (
if !WAIT_PID_SECONDS! GEQ 90 (
call :log host process still running after !WAIT_PID_SECONDS! seconds, aborting update
exit /b 1
)
timeout /t 1 /nobreak >nul
set /a WAIT_PID_SECONDS+=1
goto waitloop
)
call :log host process exited
timeout /t 3 /nobreak >nul
call :log cooldown finished, starting file replace
:replace_binary
if /I "%SOURCE_EXE%"=="%TARGET%" (
call :log downloaded executable already at target path, skip replace
goto move_done
)
set /a RETRY=0
:move_retry
call :log attempt !RETRY!: trying rename-then-copy strategy
move /Y "%TARGET%" "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL!==0 (
copy /Y "%SOURCE_EXE%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL!==0 (
del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
goto move_done
)
call :log copy after rename failed, restoring old file
move /Y "%TARGET_OLD%" "%TARGET%" >> "%LOG_FILE%" 2>&1
)
call :log rename strategy failed, trying direct move
move /Y "%SOURCE_EXE%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if %ERRORLEVEL%==0 goto move_done
copy /Y "%SOURCE_EXE%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if %ERRORLEVEL%==0 goto move_done
set /a RETRY+=1
if !RETRY! LSS 15 (
set /a WAIT=1
if !RETRY! GEQ 3 set /a WAIT=2
if !RETRY! GEQ 6 set /a WAIT=3
if !RETRY! GEQ 9 set /a WAIT=5
call :log waiting !WAIT! seconds before retry
timeout /t !WAIT! /nobreak >nul
goto move_retry
)
call :log replace failed after retries (portable mode, no elevation): check directory write permission or file lock
exit /b 1
:move_done
del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
if /I not "%CURRENT_TARGET%"=="%TARGET%" (
if exist "%CURRENT_TARGET%" del /F /Q "%CURRENT_TARGET%" >> "%LOG_FILE%" 2>&1
)
if exist "%SOURCE%" del /F /Q "%SOURCE%" >> "%LOG_FILE%" 2>&1
start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1
if %ERRORLEVEL% NEQ 0 (
call :log cmd start failed, trying powershell Start-Process
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1
if !ERRORLEVEL! NEQ 0 (
call :log relaunch failed
exit /b 1
)
)
call :log update finished
start "" /MIN cmd.exe /D /C "timeout /t 2 /nobreak >nul & del /F /Q ""%LOG_FILE%"" >nul 2>&1 & rmdir /S /Q ""%STAGED%"" >nul 2>&1"
exit /b 0
:log
echo [%date% %time%] %*>>"%LOG_FILE%"
exit /b 0
`
return strings.NewReplacer(
"__GONAVI_UPDATE_SOURCE__", source,
"__GONAVI_UPDATE_TARGET__", target,
"__GONAVI_CURRENT_TARGET__", currentTarget,
"__GONAVI_UPDATE_STAGED__", stagedDir,
"__GONAVI_UPDATE_LOG__", logPath,
"__GONAVI_UPDATE_PID__", strconv.Itoa(pid),
).Replace(strings.ReplaceAll(script, "\n", "\r\n"))
}