🐛 fix(update): 修复 dev→latest 安装后丢包且仍停在旧版的问题

- 安装成功且 relaunch 发出后再删除下载包,失败保留 latest 包便于手动安装
- macOS open 使用路径形式,避免 -a 传完整路径导致重启失败
- 明确覆盖「当前运行的 .app」(桌面 dev 包),避免误以为只更新了 Applications
- Windows 同样先启动目标再删 SOURCE,防止重启失败时只剩旧包
This commit is contained in:
Syngnat
2026-07-09 13:45:17 +08:00
parent cdfbdd5dd5
commit 097826508a
3 changed files with 70 additions and 20 deletions

View File

@@ -1765,16 +1765,25 @@ exit /b 1
:move_done
del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1
if exist "%SOURCE%" del /F /Q "%SOURCE%" >> "%LOG_FILE%" 2>&1
rem 先重启,成功后再删安装包,避免失败时 latest 包被删、用户只剩旧 dev 可运行
call :log launching target: %TARGET%
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
call :log relaunch failed - package kept for manual install: %SOURCE%
exit /b 1
)
)
rem relaunch 已发出后再清理下载包与 staging
if exist "%SOURCE%" (
if /I not "%SOURCE%"=="%TARGET%" (
if /I not "%SOURCE_EXE%"=="%TARGET%" (
del /F /Q "%SOURCE%" >> "%LOG_FILE%" 2>&1
)
)
)
rmdir /S /Q "%STAGED%" >> "%LOG_FILE%" 2>&1
call :log update finished
exit /b 0
@@ -1943,15 +1952,25 @@ replace_app_direct() {
}
relaunch_app() {
if /usr/bin/open -n -a "$TARGET_APP" >>"$LOG_FILE" 2>&1; then
return 0
fi
# open -a 需要应用名,不能传完整路径;路径必须用 open -n "xxx.app"
if /usr/bin/open -n "$TARGET_APP" >>"$LOG_FILE" 2>&1; then
log "relaunch via open -n path ok"
return 0
fi
log "open failed, trying binary launch"
nohup "$TARGET_APP/$APP_BIN_REL" >>"$LOG_FILE" 2>&1 &
return 0
local app_name
app_name=$(basename "$TARGET_APP" .app)
if [ -n "$app_name" ] && /usr/bin/open -n -a "$app_name" >>"$LOG_FILE" 2>&1; then
log "relaunch via open -n -a name ok: $app_name"
return 0
fi
log "open failed, trying binary launch: $TARGET_APP/$APP_BIN_REL"
if [ -x "$TARGET_APP/$APP_BIN_REL" ]; then
nohup "$TARGET_APP/$APP_BIN_REL" >>"$LOG_FILE" 2>&1 &
log "relaunch via binary pid=$!"
return 0
fi
log "relaunch failed: no launch method succeeded"
return 1
}
log "updater started package=$PACKAGE target=$TARGET_APP pid=$PID"
@@ -1980,29 +1999,36 @@ log "install target: $TARGET_APP"
if ! replace_app_direct; then
log "direct replace failed, trying admin replace"
if ! run_admin_replace >>"$LOG_FILE" 2>&1; then
log "admin replace failed"
log "admin replace failed — package kept at: $PACKAGE"
cleanup_mount
exit 1
fi
fi
if ! resolve_app_binary_rel "$TARGET_APP"; then
log "target app binary missing after replace: $TARGET_APP"
log "target app binary missing after replace: $TARGET_APP — package kept at: $PACKAGE"
cleanup_mount
exit 1
fi
if [ ! -x "$TARGET_APP/$APP_BIN_REL" ]; then
log "target app binary not executable: $TARGET_APP/$APP_BIN_REL"
log "target app binary not executable: $TARGET_APP/$APP_BIN_REL — package kept at: $PACKAGE"
cleanup_mount
exit 1
fi
cleanup_mount
# 不要删除 STAGED脚本自身所在目录只删安装包与临时解压目录
# 仅清理临时解压目录;安装包在 relaunch 成功后再删,失败则保留便于手动安装
/bin/rm -rf "$EXTRACT_DIR" >>"$LOG_FILE" 2>&1 || true
if ! relaunch_app; then
log "update files replaced but relaunch failed — package kept for manual install: $PACKAGE"
log "please open: $TARGET_APP"
exit 1
fi
# relaunch 已发出:再删安装包(用户已不需要 dmg/zip
/bin/rm -f "$PACKAGE" >>"$LOG_FILE" 2>&1 || true
relaunch_app
log "relaunch requested"
log "relaunch requested; package cleaned if possible"
exit 0
`, pid, packagePath, targetApp, stagedDir, mountDir, logPath)
}
@@ -2055,14 +2081,24 @@ func detectMacAppPath(exePath string) string {
func resolveMacUpdateTarget(exePath string) string {
targetApp := detectMacAppPath(exePath)
if targetApp == "" {
logger.Warnf("无法从可执行路径解析 .app回退 /Applications/GoNavi.appexe=%s", exePath)
return "/Applications/GoNavi.app"
}
targetApp = filepath.Clean(targetApp)
// Gatekeeper App Translocation 路径不可用于稳定覆盖更新,统一回退到 /Applications
// Gatekeeper App Translocation 路径不可用于稳定覆盖更新。
// 优先使用 /Applications 中已有的正式安装;否则仍回退到标准路径(避免写进临时隔离目录)。
if strings.Contains(targetApp, string(filepath.Separator)+"AppTranslocation"+string(filepath.Separator)) {
logger.Warnf("检测到 AppTranslocation 运行路径,更新目标回退至 /Applications/GoNavi.app%s", targetApp)
return "/Applications/GoNavi.app"
applicationsTarget := "/Applications/GoNavi.app"
if st, err := os.Stat(applicationsTarget); err == nil && st.IsDir() {
logger.Warnf("检测到 AppTranslocation更新目标使用已有 Applications 安装:%s来自 %s", applicationsTarget, targetApp)
return applicationsTarget
}
logger.Warnf("检测到 AppTranslocation 且 Applications 无安装,仍将更新到 %s来自 %s", applicationsTarget, targetApp)
return applicationsTarget
}
// 正在运行的是桌面/便携 .app含 dev 包):必须覆盖「当前这份」而不是误写到 /Applications
// 否则会出现latest 包被删、用户仍打开旧的 Desktop dev 包。
logger.Infof("macOS 更新目标使用当前运行的应用包:%s", targetApp)
return targetApp
}

View File

@@ -23,11 +23,12 @@ func TestBuildMacScriptContainsHardeningGuards(t *testing.T) {
"replace_app_direct",
"run_admin_replace",
"relaunch_app",
"open -n -a",
`open -n "$TARGET_APP"`,
// 安装包扩展名分支
"dmg)",
"zip)",
// 不要在脚本运行中 rm -rf 整个 STAGED脚本自身所在目录
// relaunch 成功后再删安装包,失败则保留
"package kept for manual install",
`/bin/rm -f "$PACKAGE"`,
}
for _, token := range mustContain {
@@ -38,6 +39,12 @@ func TestBuildMacScriptContainsHardeningGuards(t *testing.T) {
if strings.Contains(script, `rm -rf "$MOUNT_DIR" "$DMG" "$STAGED"`) {
t.Fatal("mac update script must not delete STAGED while the script may still be running from it")
}
// 确保不会在 relaunch 之前无条件删除安装包
rmIdx := strings.Index(script, `/bin/rm -f "$PACKAGE"`)
relaunchIdx := strings.Index(script, "if ! relaunch_app; then")
if rmIdx < 0 || relaunchIdx < 0 || rmIdx < relaunchIdx {
t.Fatalf("package cleanup must happen only after relaunch attempt (rmIdx=%d relaunchIdx=%d)", rmIdx, relaunchIdx)
}
if !strings.Contains(script, "/tmp/GoNavi-1.2.3-MacOS-Arm64.dmg") {
t.Fatal("expected package path embedded in script")
}

View File

@@ -741,14 +741,21 @@ func TestBuildWindowsScriptReplacesTargetWithDownloadedExe(t *testing.T) {
`:replace_binary`,
`move /Y "%TARGET%" "%TARGET_OLD%"`,
`copy /Y "%SOURCE_EXE%" "%TARGET%"`,
`if exist "%SOURCE%" del /F /Q "%SOURCE%"`,
`start "" /D "%TARGET_DIR%" "%TARGET%"`,
`package kept for manual install`,
`del /F /Q "%SOURCE%"`,
}
for _, want := range mustContain {
if !strings.Contains(script, want) {
t.Fatalf("windows update script missing required token: %s\nscript:\n%s", want, script)
}
}
// relaunch 必须在删除安装包之前
startIdx := strings.Index(script, `start "" /D "%TARGET_DIR%" "%TARGET%"`)
delIdx := strings.LastIndex(script, `del /F /Q "%SOURCE%"`)
if startIdx < 0 || delIdx < 0 || delIdx < startIdx {
t.Fatalf("source package must be deleted only after relaunch attempt (start=%d del=%d)", startIdx, delIdx)
}
if strings.Contains(script, "launch_downloaded_exe") {
t.Fatalf("windows update script should not launch downloaded exe side-by-side\nscript:\n%s", script)
}