mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-09 08:13:29 +08:00
fix(update): 统一更新缓存与成功清理目录 (#771)
统一 macOS、Windows 和 Linux 更新包到用户缓存目录,失败时回退系统临时目录。成功更新后清理整个 GoNavi/updates,失败时保留安装包、脚本和日志,并补充跨平台路径与清理测试。
This commit is contained in:
@@ -124,6 +124,7 @@ type stagedUpdate struct {
|
||||
Channel updateChannel
|
||||
Version string
|
||||
AssetName string
|
||||
WorkspaceDir string
|
||||
FilePath string
|
||||
StagedDir string
|
||||
InstallLogPath string
|
||||
@@ -334,7 +335,7 @@ func (a *App) InstallUpdateAndRestart(closeAllWindowsInstancesConfirmed bool) co
|
||||
return connection.QueryResult{Success: false, Message: a.appText("app.update.backend.message.no_downloaded_package", nil)}
|
||||
}
|
||||
if strings.TrimSpace(staged.InstallLogPath) == "" {
|
||||
staged.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(staged.FilePath))
|
||||
staged.InstallLogPath = buildUpdateInstallLogPath(staged.WorkspaceDir)
|
||||
}
|
||||
installTarget := ""
|
||||
if stdRuntime.GOOS == "windows" {
|
||||
@@ -536,37 +537,15 @@ func (a *App) OpenDownloadedUpdateDirectory() connection.QueryResult {
|
||||
}
|
||||
|
||||
func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
workspaceDir := strings.TrimSpace(resolveUpdateWorkspaceDirForInstallMode(info.LatestVersion, updateInstallMode(info.InstallMode)))
|
||||
if workspaceDir == "" {
|
||||
message := a.appText("app.update.backend.message.app_directory_unresolved_download", nil)
|
||||
a.emitUpdateDownloadProgress("error", 0, info.AssetSize, message)
|
||||
return connection.QueryResult{Success: false, Message: message}
|
||||
}
|
||||
if err := os.MkdirAll(workspaceDir, 0o755); err != nil {
|
||||
errMsg := a.appText("app.update.backend.message.app_directory_unavailable", map[string]any{"path": workspaceDir})
|
||||
a.emitUpdateDownloadProgress("error", 0, info.AssetSize, errMsg)
|
||||
return connection.QueryResult{Success: false, Message: errMsg}
|
||||
}
|
||||
|
||||
// 使用版本号命名的工作目录,便于识别和调试
|
||||
stagedDir := resolveUpdateStagedDir(workspaceDir, info.Channel, info.LatestVersion)
|
||||
stageBaseDir := filepath.Dir(stagedDir)
|
||||
// 清理可能残留的旧目录(上次下载失败后未清理)
|
||||
// Windows 上文件可能被杀毒软件/索引服务占用,需要重试
|
||||
for retry := 0; retry < 5; retry++ {
|
||||
err := os.RemoveAll(stagedDir)
|
||||
if err == nil {
|
||||
break
|
||||
workspaceCandidates := resolveUpdateWorkspaceDirCandidatesForInstallMode(info.LatestVersion, updateInstallMode(info.InstallMode))
|
||||
workspaceDir, stagedDir, prepareErr := prepareUpdateWorkspaceAndStagingDirs(workspaceCandidates, info.Channel, info.LatestVersion)
|
||||
if prepareErr != nil {
|
||||
preferredDir := strings.TrimSpace(resolveUpdateWorkspaceDirForInstallMode(info.LatestVersion, updateInstallMode(info.InstallMode)))
|
||||
if preferredDir == "" {
|
||||
preferredDir = os.TempDir()
|
||||
}
|
||||
if retry < 4 {
|
||||
time.Sleep(time.Duration(retry+1) * 500 * time.Millisecond)
|
||||
} else {
|
||||
// 最后一次仍然失败,换一个带时间戳的目录名避免冲突
|
||||
stagedDir = filepath.Join(stageBaseDir, fmt.Sprintf("%s-%d", buildUpdateStageDirName(info.Channel, info.LatestVersion), time.Now().UnixNano()))
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
|
||||
errMsg := a.appText("app.update.backend.message.create_workspace_failed", map[string]any{"path": stagedDir})
|
||||
logger.Error(prepareErr, "创建更新工作区失败")
|
||||
errMsg := a.appText("app.update.backend.message.create_workspace_failed", map[string]any{"path": preferredDir})
|
||||
a.emitUpdateDownloadProgress("error", 0, info.AssetSize, errMsg)
|
||||
return connection.QueryResult{Success: false, Message: errMsg}
|
||||
}
|
||||
@@ -611,6 +590,7 @@ func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
Channel: updateChannel(info.Channel),
|
||||
Version: info.LatestVersion,
|
||||
AssetName: info.AssetName,
|
||||
WorkspaceDir: workspaceDir,
|
||||
FilePath: assetPath,
|
||||
StagedDir: stagedDir,
|
||||
InstallLogPath: buildUpdateInstallLogPath(workspaceDir),
|
||||
@@ -1521,16 +1501,12 @@ func sanitizeVersionForPath(version string) string {
|
||||
}
|
||||
|
||||
result := strings.Trim(builder.String(), "-")
|
||||
if result == "" {
|
||||
if result == "" || result == "." || result == ".." {
|
||||
return "latest"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveLegacyUpdateWorkspaceDir() string {
|
||||
return filepath.Join(os.TempDir(), "gonavi-updates")
|
||||
}
|
||||
|
||||
func resolveUpdateWorkspaceDir(version string) string {
|
||||
return resolveUpdateWorkspaceDirForInstallMode(version, updateResolveInstallMode())
|
||||
}
|
||||
@@ -1541,39 +1517,81 @@ func resolveUpdateWorkspaceDirForInstallMode(version string, installMode updateI
|
||||
stdRuntime.GOOS,
|
||||
version,
|
||||
installMode,
|
||||
updateResolveInstallTarget(),
|
||||
"",
|
||||
cacheDir,
|
||||
)
|
||||
}
|
||||
|
||||
func resolveUpdateWorkspaceDirForPlatform(goos string, version string, installMode updateInstallMode, installTarget string, userCacheDir string) string {
|
||||
// macOS 更新包继续保存在桌面版本目录根级,方便用户直接处理 DMG。
|
||||
if goos == "darwin" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && strings.TrimSpace(homeDir) != "" {
|
||||
desktopDir := filepath.Join(homeDir, "Desktop")
|
||||
if st, statErr := os.Stat(desktopDir); statErr == nil && st.IsDir() {
|
||||
return filepath.Join(desktopDir, fmt.Sprintf("GoNavi-%s", sanitizeVersionForPath(version)))
|
||||
func resolveUpdateWorkspaceDirCandidatesForInstallMode(version string, installMode updateInstallMode) []string {
|
||||
preferredDir := resolveUpdateWorkspaceDirForInstallMode(version, installMode)
|
||||
fallbackDir := resolveUpdateWorkspaceDirForPlatform(stdRuntime.GOOS, version, installMode, "", "")
|
||||
candidates := make([]string, 0, 2)
|
||||
seen := make(map[string]struct{}, 2)
|
||||
for _, candidate := range []string{preferredDir, fallbackDir} {
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
key := normalizeUpdatePathForPrefixCheck(candidate)
|
||||
if stdRuntime.GOOS == "windows" {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func resolveUpdateWorkspaceDirForPlatform(_ string, version string, _ updateInstallMode, _ string, userCacheDir string) string {
|
||||
baseDir := strings.TrimSpace(userCacheDir)
|
||||
if baseDir == "" {
|
||||
baseDir = strings.TrimSpace(os.TempDir())
|
||||
}
|
||||
if baseDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(baseDir, "GoNavi", "updates", sanitizeVersionForPath(version))
|
||||
}
|
||||
|
||||
func prepareUpdateWorkspaceAndStagingDirs(workspaceCandidates []string, channel string, version string) (string, string, error) {
|
||||
var prepareErrors []error
|
||||
for _, candidate := range workspaceCandidates {
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(candidate, 0o755); err != nil {
|
||||
prepareErrors = append(prepareErrors, fmt.Errorf("create %s: %w", candidate, err))
|
||||
continue
|
||||
}
|
||||
|
||||
stagedDir := resolveUpdateStagedDir(candidate, channel, version)
|
||||
stageBaseDir := filepath.Dir(stagedDir)
|
||||
// Windows 上文件可能被杀毒软件或索引服务短暂占用,需要重试。
|
||||
for retry := 0; retry < 5; retry++ {
|
||||
err := os.RemoveAll(stagedDir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if retry < 4 {
|
||||
time.Sleep(time.Duration(retry+1) * 500 * time.Millisecond)
|
||||
} else {
|
||||
stagedDir = filepath.Join(stageBaseDir, fmt.Sprintf("%s-%d", buildUpdateStageDirName(channel, version), time.Now().UnixNano()))
|
||||
}
|
||||
}
|
||||
}
|
||||
if goos == "windows" && installMode == updateInstallModeMSI {
|
||||
if strings.TrimSpace(userCacheDir) != "" {
|
||||
return filepath.Join(userCacheDir, "GoNavi", "updates")
|
||||
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
|
||||
prepareErrors = append(prepareErrors, fmt.Errorf("create %s: %w", stagedDir, err))
|
||||
continue
|
||||
}
|
||||
return resolveLegacyUpdateWorkspaceDir()
|
||||
return candidate, stagedDir, nil
|
||||
}
|
||||
|
||||
// Windows / Linux 更新包优先落到当前应用运行目录,方便用户直接找到下载产物。
|
||||
targetPath := strings.TrimSpace(installTarget)
|
||||
if targetPath != "" {
|
||||
targetDir := strings.TrimSpace(filepath.Dir(targetPath))
|
||||
if targetDir != "" && targetDir != "." {
|
||||
return targetDir
|
||||
}
|
||||
if len(prepareErrors) == 0 {
|
||||
return "", "", errors.New("no update workspace candidates")
|
||||
}
|
||||
|
||||
return resolveLegacyUpdateWorkspaceDir()
|
||||
return "", "", errors.Join(prepareErrors...)
|
||||
}
|
||||
|
||||
func resolveUpdateAssetPath(workspaceDir string, stagedDir string, assetName string) string {
|
||||
@@ -1599,17 +1617,12 @@ func resolveUpdateStagedDir(workspaceDir string, channel string, version string)
|
||||
|
||||
func resolveUpdateStagedDirForPlatform(goos string, workspaceDir string, channel string, version string) string {
|
||||
baseDir := strings.TrimSpace(workspaceDir)
|
||||
if strings.EqualFold(strings.TrimSpace(goos), "windows") || baseDir == "" {
|
||||
baseDir = resolveLegacyUpdateWorkspaceDir()
|
||||
if baseDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(baseDir, buildUpdateStageDirNameForPlatform(goos, channel, version))
|
||||
}
|
||||
|
||||
func shouldReuseUpdateAssetFromStagedDirForPlatform(goos string, assetName string) bool {
|
||||
return !(strings.EqualFold(strings.TrimSpace(goos), "windows") &&
|
||||
shouldWindowsUpdateLaunchDownloadedAssetDirectly(assetName))
|
||||
}
|
||||
|
||||
func normalizeUpdatePathForPrefixCheck(path string) string {
|
||||
normalized := strings.ReplaceAll(strings.TrimSpace(path), "\\", "/")
|
||||
normalized = filepath.ToSlash(filepath.Clean(normalized))
|
||||
@@ -1619,6 +1632,132 @@ func normalizeUpdatePathForPrefixCheck(path string) string {
|
||||
return strings.TrimRight(normalized, "/")
|
||||
}
|
||||
|
||||
func updatePathsEqualForPlatform(goos string, left string, right string) bool {
|
||||
left = normalizeUpdatePathForPrefixCheck(left)
|
||||
right = normalizeUpdatePathForPrefixCheck(right)
|
||||
if left == "" || right == "" {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(goos), "windows") {
|
||||
return strings.EqualFold(left, right)
|
||||
}
|
||||
return left == right
|
||||
}
|
||||
|
||||
func absoluteUpdatePath(path string) (string, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", errors.New("path is empty")
|
||||
}
|
||||
cleaned := filepath.Clean(path)
|
||||
if cleaned == "." {
|
||||
return "", errors.New("path resolves to current directory")
|
||||
}
|
||||
absPath, err := filepath.Abs(cleaned)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Clean(absPath), nil
|
||||
}
|
||||
|
||||
func isUpdatePathStrictlyInsideDir(path string, dir string) bool {
|
||||
absPath, err := absoluteUpdatePath(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
absDir, err := absoluteUpdatePath(dir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
relPath, err := filepath.Rel(absDir, absPath)
|
||||
if err != nil || relPath == "." || filepath.IsAbs(relPath) {
|
||||
return false
|
||||
}
|
||||
return relPath != ".." && !strings.HasPrefix(relPath, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func isDirectChildUpdatePath(path string, parentDir string) bool {
|
||||
absPath, err := absoluteUpdatePath(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
absParent, err := absoluteUpdatePath(parentDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
relPath, err := filepath.Rel(absParent, absPath)
|
||||
if err != nil || relPath == "." || relPath == ".." || filepath.IsAbs(relPath) {
|
||||
return false
|
||||
}
|
||||
return filepath.Dir(relPath) == "."
|
||||
}
|
||||
|
||||
func allowedUpdateRootDirs() []string {
|
||||
cacheDir, _ := os.UserCacheDir()
|
||||
baseDirs := []string{cacheDir, os.TempDir()}
|
||||
roots := make([]string, 0, len(baseDirs))
|
||||
seen := make(map[string]struct{}, len(baseDirs))
|
||||
for _, baseDir := range baseDirs {
|
||||
baseDir = strings.TrimSpace(baseDir)
|
||||
if baseDir == "" {
|
||||
continue
|
||||
}
|
||||
rootDir := filepath.Join(baseDir, "GoNavi", "updates")
|
||||
key := normalizeUpdatePathForPrefixCheck(rootDir)
|
||||
if stdRuntime.GOOS == "windows" {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
roots = append(roots, rootDir)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func validateStagedUpdateWorkspace(staged *stagedUpdate) error {
|
||||
if staged == nil {
|
||||
return errors.New("staged update is nil")
|
||||
}
|
||||
workspaceDir := strings.TrimSpace(staged.WorkspaceDir)
|
||||
version := strings.TrimSpace(staged.Version)
|
||||
if workspaceDir == "" || version == "" {
|
||||
return errors.New("update workspace or version is empty")
|
||||
}
|
||||
if filepath.Base(filepath.Clean(workspaceDir)) != sanitizeVersionForPath(version) {
|
||||
return fmt.Errorf("update workspace does not match version %q", version)
|
||||
}
|
||||
allowed := false
|
||||
for _, rootDir := range allowedUpdateRootDirs() {
|
||||
if isDirectChildUpdatePath(workspaceDir, rootDir) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return fmt.Errorf("update workspace %q is outside the cache roots", workspaceDir)
|
||||
}
|
||||
for label, path := range map[string]string{
|
||||
"package": staged.FilePath,
|
||||
"staging": staged.StagedDir,
|
||||
"log": staged.InstallLogPath,
|
||||
} {
|
||||
if !isUpdatePathStrictlyInsideDir(path, workspaceDir) {
|
||||
return fmt.Errorf("%s path %q is outside update workspace %q", label, path, workspaceDir)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveUpdateCleanupDir(workspaceDir string) string {
|
||||
workspaceDir = strings.TrimSpace(workspaceDir)
|
||||
if workspaceDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Dir(filepath.Clean(workspaceDir))
|
||||
}
|
||||
|
||||
func isUpdateAssetPathInsideStagedDir(filePath string, stagedDir string) bool {
|
||||
normalizedFilePath := normalizeUpdatePathForPrefixCheck(filePath)
|
||||
normalizedStagedDir := normalizeUpdatePathForPrefixCheck(stagedDir)
|
||||
@@ -1628,20 +1767,13 @@ func isUpdateAssetPathInsideStagedDir(filePath string, stagedDir string) bool {
|
||||
return normalizedFilePath == normalizedStagedDir || strings.HasPrefix(normalizedFilePath, normalizedStagedDir+"/")
|
||||
}
|
||||
|
||||
func buildReusableUpdatePathCandidatesForPlatform(goos string, preferredWorkspaceDir string, legacyWorkspaceDir string, channel string, version string, assetName string) []updatePathCandidate {
|
||||
func buildReusableUpdatePathCandidatesForPlatform(goos string, preferredWorkspaceDir string, fallbackWorkspaceDir string, channel string, version string, assetName string) []updatePathCandidate {
|
||||
preferredWorkspaceDir = strings.TrimSpace(preferredWorkspaceDir)
|
||||
legacyWorkspaceDir = strings.TrimSpace(legacyWorkspaceDir)
|
||||
fallbackWorkspaceDir = strings.TrimSpace(fallbackWorkspaceDir)
|
||||
assetName = strings.TrimSpace(assetName)
|
||||
preferredStagedDir := resolveUpdateStagedDirForPlatform(goos, preferredWorkspaceDir, channel, version)
|
||||
stagedDirNames := []string{
|
||||
buildUpdateStageDirNameForPlatform(goos, channel, version),
|
||||
fmt.Sprintf(".gonavi-update-%s-%s", strings.TrimSpace(strings.ToLower(goos)), version),
|
||||
}
|
||||
workspaceCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
|
||||
stageBaseCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
|
||||
workspaceCandidates := []string{preferredWorkspaceDir, fallbackWorkspaceDir}
|
||||
seenWorkspace := make(map[string]struct{}, len(workspaceCandidates))
|
||||
seenStageBase := make(map[string]struct{}, len(stageBaseCandidates))
|
||||
candidates := make([]updatePathCandidate, 0, 8)
|
||||
candidates := make([]updatePathCandidate, 0, len(workspaceCandidates))
|
||||
|
||||
for _, workspaceDir := range workspaceCandidates {
|
||||
workspaceDir = strings.TrimSpace(workspaceDir)
|
||||
@@ -1655,35 +1787,11 @@ func buildReusableUpdatePathCandidatesForPlatform(goos string, preferredWorkspac
|
||||
if shouldStoreUpdateAssetInWorkspaceRoot(goos) {
|
||||
candidates = append(candidates, updatePathCandidate{
|
||||
workspaceDir: workspaceDir,
|
||||
stagedDir: preferredStagedDir,
|
||||
stagedDir: resolveUpdateStagedDirForPlatform(goos, workspaceDir, channel, version),
|
||||
assetPath: filepath.Join(workspaceDir, assetName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldReuseUpdateAssetFromStagedDirForPlatform(goos, assetName) {
|
||||
return candidates
|
||||
}
|
||||
|
||||
for _, stageBaseDir := range stageBaseCandidates {
|
||||
stageBaseDir = strings.TrimSpace(stageBaseDir)
|
||||
if stageBaseDir == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenStageBase[stageBaseDir]; exists {
|
||||
continue
|
||||
}
|
||||
seenStageBase[stageBaseDir] = struct{}{}
|
||||
for _, stagedDirName := range stagedDirNames {
|
||||
stagedDir := filepath.Join(stageBaseDir, stagedDirName)
|
||||
candidates = append(candidates, updatePathCandidate{
|
||||
workspaceDir: stageBaseDir,
|
||||
stagedDir: stagedDir,
|
||||
assetPath: filepath.Join(stagedDir, assetName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
@@ -1703,16 +1811,25 @@ func isExistingDownloadedAsset(filePath string, expectedSize int64) bool {
|
||||
}
|
||||
|
||||
func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *stagedUpdate {
|
||||
workspaceDirs := resolveUpdateWorkspaceDirCandidatesForInstallMode(strings.TrimSpace(info.LatestVersion), updateInstallMode(info.InstallMode))
|
||||
preferredWorkspaceDir := ""
|
||||
fallbackWorkspaceDir := ""
|
||||
if len(workspaceDirs) > 0 {
|
||||
preferredWorkspaceDir = workspaceDirs[0]
|
||||
}
|
||||
if len(workspaceDirs) > 1 {
|
||||
fallbackWorkspaceDir = workspaceDirs[1]
|
||||
}
|
||||
return resolveReusableStagedUpdateForPlatform(
|
||||
stdRuntime.GOOS,
|
||||
resolveUpdateWorkspaceDirForInstallMode(strings.TrimSpace(info.LatestVersion), updateInstallMode(info.InstallMode)),
|
||||
resolveLegacyUpdateWorkspaceDir(),
|
||||
preferredWorkspaceDir,
|
||||
fallbackWorkspaceDir,
|
||||
info,
|
||||
current,
|
||||
)
|
||||
}
|
||||
|
||||
func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir string, legacyWorkspaceDir string, info UpdateInfo, current *stagedUpdate) *stagedUpdate {
|
||||
func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir string, fallbackWorkspaceDir string, info UpdateInfo, current *stagedUpdate) *stagedUpdate {
|
||||
channel, err := normalizeUpdateChannel(info.Channel)
|
||||
if err != nil {
|
||||
channel = updateChannelLatest
|
||||
@@ -1722,7 +1839,14 @@ func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir s
|
||||
if version == "" || assetName == "" {
|
||||
return nil
|
||||
}
|
||||
allowStagedDirReuse := shouldReuseUpdateAssetFromStagedDirForPlatform(goos, assetName)
|
||||
candidates := buildReusableUpdatePathCandidatesForPlatform(
|
||||
goos,
|
||||
preferredWorkspaceDir,
|
||||
fallbackWorkspaceDir,
|
||||
string(channel),
|
||||
version,
|
||||
assetName,
|
||||
)
|
||||
|
||||
if current != nil {
|
||||
currentChannel := current.Channel
|
||||
@@ -1735,11 +1859,14 @@ func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir s
|
||||
current.PackageType == updatePackageType(info.PackageType) {
|
||||
currentPath := strings.TrimSpace(current.FilePath)
|
||||
if isExistingDownloadedAsset(currentPath, info.AssetSize) {
|
||||
if !allowStagedDirReuse && isUpdateAssetPathInsideStagedDir(currentPath, current.StagedDir) {
|
||||
current = nil
|
||||
} else {
|
||||
if strings.TrimSpace(current.InstallLogPath) == "" {
|
||||
current.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(currentPath))
|
||||
for _, candidate := range candidates {
|
||||
if !updatePathsEqualForPlatform(goos, currentPath, candidate.assetPath) ||
|
||||
!isUpdatePathStrictlyInsideDir(current.StagedDir, candidate.workspaceDir) {
|
||||
continue
|
||||
}
|
||||
current.WorkspaceDir = candidate.workspaceDir
|
||||
if !isUpdatePathStrictlyInsideDir(current.InstallLogPath, candidate.workspaceDir) {
|
||||
current.InstallLogPath = buildUpdateInstallLogPath(candidate.workspaceDir)
|
||||
}
|
||||
current.Channel = channel
|
||||
current.AssetName = assetName
|
||||
@@ -1752,14 +1879,6 @@ func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir s
|
||||
}
|
||||
}
|
||||
|
||||
candidates := buildReusableUpdatePathCandidatesForPlatform(
|
||||
goos,
|
||||
preferredWorkspaceDir,
|
||||
legacyWorkspaceDir,
|
||||
string(channel),
|
||||
version,
|
||||
assetName,
|
||||
)
|
||||
for _, candidate := range candidates {
|
||||
if !isExistingDownloadedAsset(candidate.assetPath, info.AssetSize) {
|
||||
continue
|
||||
@@ -1768,6 +1887,7 @@ func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir s
|
||||
Channel: channel,
|
||||
Version: version,
|
||||
AssetName: assetName,
|
||||
WorkspaceDir: candidate.workspaceDir,
|
||||
FilePath: candidate.assetPath,
|
||||
StagedDir: candidate.stagedDir,
|
||||
InstallLogPath: buildUpdateInstallLogPath(candidate.workspaceDir),
|
||||
@@ -1838,10 +1958,6 @@ func ensureWindowsUpdateTargetWritable(targetExe string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldWindowsUpdateLaunchDownloadedAssetDirectly(assetPath string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(filepath.Ext(strings.TrimSpace(assetPath))), ".exe")
|
||||
}
|
||||
|
||||
func (a *App) emitUpdateDownloadProgress(status string, downloaded, total int64, message string) {
|
||||
if a.ctx == nil {
|
||||
return
|
||||
@@ -1863,6 +1979,15 @@ func (a *App) emitUpdateDownloadProgress(status string, downloaded, total int64,
|
||||
}
|
||||
|
||||
func launchUpdateScript(staged *stagedUpdate) error {
|
||||
if staged == nil {
|
||||
return localizedUpdateError{key: "app.update.backend.message.no_downloaded_package"}
|
||||
}
|
||||
if strings.TrimSpace(staged.InstallLogPath) == "" {
|
||||
staged.InstallLogPath = buildUpdateInstallLogPath(staged.WorkspaceDir)
|
||||
}
|
||||
if err := validateStagedUpdateWorkspace(staged); err != nil {
|
||||
return fmt.Errorf("invalid update workspace: %w", err)
|
||||
}
|
||||
exePath, err := resolveExecutablePath(os.Executable, filepath.EvalSymlinks)
|
||||
if err != nil || strings.TrimSpace(exePath) == "" {
|
||||
return localizedUpdateError{key: "app.update.backend.error.install_target_unresolved"}
|
||||
@@ -1908,12 +2033,12 @@ func launchMacUpdate(staged *stagedUpdate, targetExe string, pid int) error {
|
||||
}
|
||||
logPath := strings.TrimSpace(staged.InstallLogPath)
|
||||
if logPath == "" {
|
||||
logPath = buildUpdateInstallLogPath(filepath.Dir(staged.FilePath))
|
||||
logPath = buildUpdateInstallLogPath(staged.WorkspaceDir)
|
||||
staged.InstallLogPath = logPath
|
||||
}
|
||||
|
||||
scriptPath := filepath.Join(staged.StagedDir, "update.sh")
|
||||
content := buildMacScript(staged.FilePath, targetApp, staged.StagedDir, mountDir, logPath, pid)
|
||||
content := buildMacScript(staged.FilePath, targetApp, resolveUpdateCleanupDir(staged.WorkspaceDir), staged.StagedDir, mountDir, logPath, pid)
|
||||
if err := os.WriteFile(scriptPath, []byte(content), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1935,7 +2060,7 @@ func launchMacUpdate(staged *stagedUpdate, targetExe string, pid int) error {
|
||||
|
||||
func launchLinuxUpdate(staged *stagedUpdate, targetExe string, pid int) error {
|
||||
scriptPath := filepath.Join(staged.StagedDir, "update.sh")
|
||||
content := buildLinuxScript(staged.FilePath, targetExe, staged.StagedDir, pid)
|
||||
content := buildLinuxScript(staged.FilePath, targetExe, resolveUpdateCleanupDir(staged.WorkspaceDir), staged.StagedDir, staged.InstallLogPath, pid)
|
||||
if err := os.WriteFile(scriptPath, []byte(content), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1966,6 +2091,7 @@ func buildWindowsLaunchCommand(scriptPath string, context windowsUpdateLaunchCon
|
||||
"GONAVI_UPDATE_SOURCE="+context.SourcePath,
|
||||
"GONAVI_UPDATE_TARGET="+context.TargetPath,
|
||||
"GONAVI_UPDATE_CURRENT_TARGET="+context.CurrentTargetPath,
|
||||
"GONAVI_UPDATE_ROOT_DIR="+context.UpdatesDir,
|
||||
"GONAVI_UPDATE_STAGED_DIR="+context.StagedDir,
|
||||
"GONAVI_UPDATE_LOG_PATH="+context.LogPath,
|
||||
"GONAVI_UPDATE_MAINTENANCE_EVENT_NAME="+context.MaintenanceEventName,
|
||||
@@ -1976,12 +2102,13 @@ func buildWindowsLaunchCommand(scriptPath string, context windowsUpdateLaunchCon
|
||||
return cmd
|
||||
}
|
||||
|
||||
func buildMacScript(packagePath, targetApp, stagedDir, mountDir, logPath string, pid int) string {
|
||||
func buildMacScript(packagePath, targetApp, updatesDir, stagedDir, mountDir, logPath string, pid int) string {
|
||||
return fmt.Sprintf(`#!/bin/bash
|
||||
set -uo pipefail
|
||||
PID=%d
|
||||
PACKAGE="%s"
|
||||
TARGET_APP="%s"
|
||||
UPDATES_DIR="%s"
|
||||
STAGED="%s"
|
||||
MOUNT_DIR="%s"
|
||||
LOG_FILE="%s"
|
||||
@@ -2134,7 +2261,7 @@ relaunch_app() {
|
||||
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 &
|
||||
nohup "$TARGET_APP/$APP_BIN_REL" >/dev/null 2>&1 &
|
||||
log "relaunch via binary pid=$!"
|
||||
return 0
|
||||
fi
|
||||
@@ -2195,41 +2322,68 @@ if ! relaunch_app; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# relaunch 已发出:再删安装包(用户已不需要 dmg/zip)
|
||||
/bin/rm -f "$PACKAGE" >>"$LOG_FILE" 2>&1 || true
|
||||
log "relaunch requested; package cleaned if possible"
|
||||
exit 0
|
||||
`, pid, packagePath, targetApp, stagedDir, mountDir, logPath)
|
||||
# 成功日志必须先落盘;删除工作区后不再写日志。
|
||||
log "relaunch requested; removing updates directory"
|
||||
cd /
|
||||
exec /bin/rm -rf "$UPDATES_DIR"
|
||||
`, pid, packagePath, targetApp, updatesDir, stagedDir, mountDir, logPath)
|
||||
}
|
||||
|
||||
func buildLinuxScript(tarPath, targetExe, stagedDir string, pid int) string {
|
||||
func buildLinuxScript(tarPath, targetExe, updatesDir, stagedDir, logPath string, pid int) string {
|
||||
return fmt.Sprintf(`#!/bin/bash
|
||||
set -e
|
||||
PID=%d
|
||||
ARCHIVE="%s"
|
||||
TARGET="%s"
|
||||
UPDATES_DIR="%s"
|
||||
STAGED="%s"
|
||||
LOG_FILE="%s"
|
||||
UPDATE_TMP_DIR=""
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%%Y-%%m-%%d %%H:%%M:%%S')] $*" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup_tmp() {
|
||||
if [ -n "$UPDATE_TMP_DIR" ]; then
|
||||
rm -rf "$UPDATE_TMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup_tmp EXIT
|
||||
log "updater started archive=$ARCHIVE target=$TARGET pid=$PID"
|
||||
while kill -0 $PID 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
TMPDIR=$(mktemp -d)
|
||||
tar -xzf "$ARCHIVE" -C "$TMPDIR"
|
||||
UPDATE_TMP_DIR=$(mktemp -d)
|
||||
tar -xzf "$ARCHIVE" -C "$UPDATE_TMP_DIR"
|
||||
TARGET_NAME="$(basename "$TARGET")"
|
||||
NEWBIN="$TMPDIR/$TARGET_NAME"
|
||||
NEWBIN="$UPDATE_TMP_DIR/$TARGET_NAME"
|
||||
if [ ! -f "$NEWBIN" ]; then
|
||||
NEWBIN=$(find "$TMPDIR" -type f -name "$TARGET_NAME" | head -n 1)
|
||||
NEWBIN=$(find "$UPDATE_TMP_DIR" -type f -name "$TARGET_NAME" | head -n 1)
|
||||
fi
|
||||
if [ -z "$NEWBIN" ] || [ ! -f "$NEWBIN" ]; then
|
||||
NEWBIN=$(find "$TMPDIR" -type f -name "GoNavi" | head -n 1)
|
||||
NEWBIN=$(find "$UPDATE_TMP_DIR" -type f -name "GoNavi" | head -n 1)
|
||||
fi
|
||||
if [ -z "$NEWBIN" ] || [ ! -f "$NEWBIN" ]; then
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$NEWBIN" "$TARGET"
|
||||
chmod +x "$TARGET"
|
||||
rm -rf "$TMPDIR" "$ARCHIVE" "$STAGED"
|
||||
"$TARGET" &
|
||||
`, pid, tarPath, targetExe, stagedDir)
|
||||
cleanup_tmp
|
||||
UPDATE_TMP_DIR=""
|
||||
"$TARGET" >/dev/null 2>&1 &
|
||||
NEW_PID=$!
|
||||
sleep 1
|
||||
if ! kill -0 "$NEW_PID" 2>/dev/null; then
|
||||
log "updated application exited immediately after launch; updates directory retained"
|
||||
exit 1
|
||||
fi
|
||||
log "updated application relaunched; removing updates directory"
|
||||
trap - EXIT
|
||||
cd /
|
||||
exec rm -rf "$UPDATES_DIR"
|
||||
`, pid, tarPath, targetExe, updatesDir, stagedDir, logPath)
|
||||
}
|
||||
|
||||
func detectMacAppPath(exePath string) string {
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
|
||||
func TestBuildMacScriptContainsHardeningGuards(t *testing.T) {
|
||||
script := buildMacScript(
|
||||
"/tmp/GoNavi-1.2.3-MacOS-Arm64.dmg",
|
||||
"/tmp/GoNavi/updates/1.2.3/GoNavi-1.2.3-MacOS-Arm64.dmg",
|
||||
"/Applications/GoNavi.app",
|
||||
"/tmp/stage",
|
||||
"/tmp/stage/mnt",
|
||||
"/tmp/gonavi-update-macos.log",
|
||||
"/tmp/GoNavi/updates",
|
||||
"/tmp/GoNavi/updates/1.2.3/stage",
|
||||
"/tmp/GoNavi/updates/1.2.3/stage/mnt",
|
||||
"/tmp/GoNavi/updates/1.2.3/gonavi-update-macos.log",
|
||||
4242,
|
||||
)
|
||||
|
||||
@@ -24,12 +25,13 @@ func TestBuildMacScriptContainsHardeningGuards(t *testing.T) {
|
||||
"run_admin_replace",
|
||||
"relaunch_app",
|
||||
`open -n "$TARGET_APP"`,
|
||||
`nohup "$TARGET_APP/$APP_BIN_REL" >/dev/null 2>&1 &`,
|
||||
// 安装包扩展名分支
|
||||
"dmg)",
|
||||
"zip)",
|
||||
// relaunch 成功后再删安装包,失败则保留
|
||||
// relaunch 成功后删除整个 updates 目录,失败则保留
|
||||
"package kept for manual install",
|
||||
`/bin/rm -f "$PACKAGE"`,
|
||||
`exec /bin/rm -rf "$UPDATES_DIR"`,
|
||||
}
|
||||
for _, token := range mustContain {
|
||||
if !strings.Contains(script, token) {
|
||||
@@ -39,13 +41,16 @@ 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"`)
|
||||
// 确保不会在 relaunch 之前删除 updates 目录。
|
||||
rmIdx := strings.Index(script, `exec /bin/rm -rf "$UPDATES_DIR"`)
|
||||
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)
|
||||
t.Fatalf("updates 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") {
|
||||
if strings.Contains(script[rmIdx+len(`exec /bin/rm -rf "$UPDATES_DIR"`):], `log "`) {
|
||||
t.Fatal("mac update script must not write installation logs after deleting the updates directory")
|
||||
}
|
||||
if !strings.Contains(script, "/tmp/GoNavi/updates/1.2.3/GoNavi-1.2.3-MacOS-Arm64.dmg") {
|
||||
t.Fatal("expected package path embedded in script")
|
||||
}
|
||||
if !strings.Contains(script, "/Applications/GoNavi.app") {
|
||||
|
||||
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
@@ -504,20 +506,28 @@ func TestCheckForUpdatesDoesNotMutatePublishedStagedUpdate(t *testing.T) {
|
||||
|
||||
installMode := updateResolveInstallMode()
|
||||
packageType := resolveUpdatePackageType(stdRuntime.GOOS, installMode)
|
||||
assetName, err := expectedAssetNameForInstallMode(stdRuntime.GOOS, stdRuntime.GOARCH, "v0.8.6", installMode)
|
||||
latestVersion := fmt.Sprintf("0.8.6-test-%d", time.Now().UnixNano())
|
||||
assetName, err := expectedAssetNameForInstallMode(stdRuntime.GOOS, stdRuntime.GOARCH, "v"+latestVersion, installMode)
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetNameForInstallMode returned error: %v", err)
|
||||
}
|
||||
assetPath := filepath.Join(t.TempDir(), assetName)
|
||||
workspaceDir := resolveUpdateWorkspaceDirForPlatform(stdRuntime.GOOS, latestVersion, installMode, "", "")
|
||||
t.Cleanup(func() { _ = os.RemoveAll(workspaceDir) })
|
||||
stagedDir := resolveUpdateStagedDirForPlatform(stdRuntime.GOOS, workspaceDir, string(updateChannelLatest), latestVersion)
|
||||
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll staged directory: %v", err)
|
||||
}
|
||||
assetPath := filepath.Join(workspaceDir, assetName)
|
||||
if err := os.WriteFile(assetPath, []byte("12345678"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
published := &stagedUpdate{
|
||||
Channel: updateChannelLatest,
|
||||
Version: "0.8.6",
|
||||
Version: latestVersion,
|
||||
AssetName: assetName,
|
||||
WorkspaceDir: workspaceDir,
|
||||
FilePath: assetPath,
|
||||
StagedDir: filepath.Dir(assetPath),
|
||||
StagedDir: stagedDir,
|
||||
InstallMode: installMode,
|
||||
PackageType: packageType,
|
||||
AutoRelaunch: true,
|
||||
@@ -535,9 +545,9 @@ func TestCheckForUpdatesDoesNotMutatePublishedStagedUpdate(t *testing.T) {
|
||||
defer restoreStatic()
|
||||
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
|
||||
return &githubRelease{
|
||||
TagName: "v0.8.6",
|
||||
Name: "v0.8.6",
|
||||
HTMLURL: "https://example.com/releases/v0.8.6",
|
||||
TagName: "v" + latestVersion,
|
||||
Name: "v" + latestVersion,
|
||||
HTMLURL: "https://example.com/releases/v" + latestVersion,
|
||||
Assets: []githubAsset{{
|
||||
Name: assetName,
|
||||
BrowserDownloadURL: "https://example.com/" + assetName,
|
||||
@@ -818,7 +828,7 @@ func TestResolveReusableStagedUpdateForPlatformSkipsLegacyWindowsExeStagedAsset(
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReusableStagedUpdateForPlatformPrefersWindowsExeInInstallDirectory(t *testing.T) {
|
||||
func TestResolveReusableStagedUpdateForPlatformPrefersAssetInCacheWorkspace(t *testing.T) {
|
||||
preferredWorkspaceDir := t.TempDir()
|
||||
legacyWorkspaceDir := t.TempDir()
|
||||
info := UpdateInfo{
|
||||
@@ -847,10 +857,13 @@ func TestResolveReusableStagedUpdateForPlatformPrefersWindowsExeInInstallDirecto
|
||||
|
||||
reused := resolveReusableStagedUpdateForPlatform("windows", preferredWorkspaceDir, legacyWorkspaceDir, info, nil)
|
||||
if reused == nil {
|
||||
t.Fatal("expected install-directory windows exe to be reused")
|
||||
t.Fatal("expected cache workspace windows exe to be reused")
|
||||
}
|
||||
if reused.FilePath != preferredAssetPath {
|
||||
t.Fatalf("expected preferred install-directory asset %q, got %q", preferredAssetPath, reused.FilePath)
|
||||
t.Fatalf("expected preferred cache asset %q, got %q", preferredAssetPath, reused.FilePath)
|
||||
}
|
||||
if reused.WorkspaceDir != preferredWorkspaceDir {
|
||||
t.Fatalf("expected workspace %q, got %q", preferredWorkspaceDir, reused.WorkspaceDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,7 +901,7 @@ func TestResolveReusableStagedUpdateForPlatformDoesNotReuseCurrentWindowsExeInsi
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReusableStagedUpdateForPlatformReusesPortableZipInsideStagedDir(t *testing.T) {
|
||||
func TestResolveReusableStagedUpdateForPlatformReusesPortableZipFromFallbackWorkspaceRoot(t *testing.T) {
|
||||
preferredWorkspaceDir := t.TempDir()
|
||||
legacyWorkspaceDir := t.TempDir()
|
||||
info := UpdateInfo{
|
||||
@@ -901,23 +914,16 @@ func TestResolveReusableStagedUpdateForPlatformReusesPortableZipInsideStagedDir(
|
||||
AutoRelaunch: true,
|
||||
}
|
||||
|
||||
stagedDir := filepath.Join(
|
||||
legacyWorkspaceDir,
|
||||
buildUpdateStageDirNameForPlatform("windows", info.Channel, info.LatestVersion),
|
||||
)
|
||||
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll returned error: %v", err)
|
||||
}
|
||||
assetPath := filepath.Join(stagedDir, info.AssetName)
|
||||
assetPath := filepath.Join(legacyWorkspaceDir, info.AssetName)
|
||||
if err := os.WriteFile(assetPath, []byte("12345678"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
|
||||
reused := resolveReusableStagedUpdateForPlatform("windows", preferredWorkspaceDir, legacyWorkspaceDir, info, nil)
|
||||
if reused == nil {
|
||||
t.Fatal("expected staged portable ZIP to be reused")
|
||||
t.Fatal("expected fallback workspace portable ZIP to be reused")
|
||||
}
|
||||
if reused.FilePath != assetPath || reused.PackageType != updatePackageTypePortable {
|
||||
if reused.FilePath != assetPath || reused.WorkspaceDir != legacyWorkspaceDir || reused.PackageType != updatePackageTypePortable {
|
||||
t.Fatalf("unexpected reused portable ZIP: %#v", reused)
|
||||
}
|
||||
}
|
||||
@@ -1108,24 +1114,23 @@ func TestInstallUpdateAndRestartMSISkipsPortableTargetWriteProbe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveUpdateWorkspaceDirPrefersCurrentInstallDirectory(t *testing.T) {
|
||||
if stdRuntime.GOOS == "darwin" {
|
||||
t.Skip("macOS keeps update downloads on Desktop")
|
||||
func TestResolveUpdateWorkspaceDirUsesVersionedUserCacheDirectory(t *testing.T) {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil || strings.TrimSpace(cacheDir) == "" {
|
||||
t.Skip("user cache directory is unavailable")
|
||||
}
|
||||
|
||||
targetDir := t.TempDir()
|
||||
originalResolveInstallTarget := updateResolveInstallTarget
|
||||
t.Cleanup(func() {
|
||||
updateResolveInstallTarget = originalResolveInstallTarget
|
||||
})
|
||||
|
||||
updateResolveInstallTarget = func() string {
|
||||
return filepath.Join(targetDir, "GoNavi.exe")
|
||||
}
|
||||
|
||||
got := resolveUpdateWorkspaceDir("0.8.2")
|
||||
if got != targetDir {
|
||||
t.Fatalf("expected workspace dir %q, got %q", targetDir, got)
|
||||
want := filepath.Join(cacheDir, "GoNavi", "updates", "0.8.2")
|
||||
if got != want {
|
||||
t.Fatalf("expected workspace dir %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeVersionForPathRejectsDotSegments(t *testing.T) {
|
||||
for _, version := range []string{"", ".", "..", " / "} {
|
||||
if got := sanitizeVersionForPath(version); got != "latest" {
|
||||
t.Fatalf("sanitizeVersionForPath(%q) = %q, want latest", version, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,30 +1152,95 @@ func TestShouldStoreUpdateAssetInWorkspaceRoot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveUpdateStagedDirForPlatformUsesLegacyWorkspaceOnWindows(t *testing.T) {
|
||||
func TestResolveUpdateStagedDirForPlatformStaysInsideWorkspaceOnWindows(t *testing.T) {
|
||||
workspaceDir := filepath.Join("C:\\GoNavi", "app")
|
||||
got := resolveUpdateStagedDirForPlatform("windows", workspaceDir, "dev", "dev-93dc696")
|
||||
want := filepath.Join(resolveLegacyUpdateWorkspaceDir(), buildUpdateStageDirNameForPlatform("windows", "dev", "dev-93dc696"))
|
||||
want := filepath.Join(workspaceDir, buildUpdateStageDirNameForPlatform("windows", "dev", "dev-93dc696"))
|
||||
if got != want {
|
||||
t.Fatalf("expected windows staged dir %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldWindowsUpdateLaunchDownloadedAssetDirectly(t *testing.T) {
|
||||
cases := []struct {
|
||||
assetPath string
|
||||
want bool
|
||||
}{
|
||||
{assetPath: `C:\GoNavi\GoNavi-dev-93dc696-Windows-Amd64-Portable.exe`, want: true},
|
||||
{assetPath: `C:\GoNavi\GoNavi-dev-93dc696-Windows-Amd64-Installer.msi`, want: false},
|
||||
{assetPath: `C:\GoNavi\GoNavi-0.8.2-Windows-Amd64.zip`, want: false},
|
||||
{assetPath: "", want: false},
|
||||
func TestPrepareUpdateWorkspaceAndStagingDirsFallsBackWhenPreferredIsUnavailable(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
preferredDir := filepath.Join(rootDir, "unavailable")
|
||||
if err := os.WriteFile(preferredDir, []byte("not a directory"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile preferred path: %v", err)
|
||||
}
|
||||
fallbackDir := filepath.Join(rootDir, "GoNavi", "updates", "1.2.3")
|
||||
|
||||
workspaceDir, stagedDir, err := prepareUpdateWorkspaceAndStagingDirs(
|
||||
[]string{preferredDir, fallbackDir},
|
||||
string(updateChannelLatest),
|
||||
"1.2.3",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("prepareUpdateWorkspaceAndStagingDirs returned error: %v", err)
|
||||
}
|
||||
if workspaceDir != fallbackDir {
|
||||
t.Fatalf("workspace = %q, want fallback %q", workspaceDir, fallbackDir)
|
||||
}
|
||||
if !isUpdatePathStrictlyInsideDir(stagedDir, fallbackDir) {
|
||||
t.Fatalf("staging directory %q must be inside fallback workspace %q", stagedDir, fallbackDir)
|
||||
}
|
||||
if stat, err := os.Stat(stagedDir); err != nil || !stat.IsDir() {
|
||||
t.Fatalf("fallback staging directory was not created: stat=%v err=%v", stat, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStagedUpdateWorkspaceAllowsVersionDirectoryUnderTempRoot(t *testing.T) {
|
||||
workspaceDir := filepath.Join(os.TempDir(), "GoNavi", "updates", "1.2.3")
|
||||
staged := &stagedUpdate{
|
||||
Version: "1.2.3",
|
||||
WorkspaceDir: workspaceDir,
|
||||
FilePath: filepath.Join(workspaceDir, "GoNavi-1.2.3.dmg"),
|
||||
StagedDir: filepath.Join(workspaceDir, ".gonavi-update-darwin-latest-1.2.3"),
|
||||
InstallLogPath: filepath.Join(workspaceDir, "gonavi-update-macos.log"),
|
||||
}
|
||||
if err := validateStagedUpdateWorkspace(staged); err != nil {
|
||||
t.Fatalf("valid update workspace rejected: %v", err)
|
||||
}
|
||||
wantCleanupDir := filepath.Join(os.TempDir(), "GoNavi", "updates")
|
||||
if got := resolveUpdateCleanupDir(staged.WorkspaceDir); got != wantCleanupDir {
|
||||
t.Fatalf("cleanup directory = %q, want entire updates directory %q", got, wantCleanupDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStagedUpdateWorkspaceRejectsUnsafeCleanupTargets(t *testing.T) {
|
||||
updateRoot := filepath.Join(os.TempDir(), "GoNavi", "updates")
|
||||
validWorkspace := filepath.Join(updateRoot, "1.2.3")
|
||||
newStaged := func(workspaceDir string) *stagedUpdate {
|
||||
return &stagedUpdate{
|
||||
Version: "1.2.3",
|
||||
WorkspaceDir: workspaceDir,
|
||||
FilePath: filepath.Join(workspaceDir, "GoNavi-1.2.3.dmg"),
|
||||
StagedDir: filepath.Join(workspaceDir, "stage"),
|
||||
InstallLogPath: filepath.Join(workspaceDir, "update.log"),
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
staged *stagedUpdate
|
||||
}{
|
||||
{name: "update root itself", staged: newStaged(updateRoot)},
|
||||
{name: "nested version directory", staged: newStaged(filepath.Join(updateRoot, "nested", "1.2.3"))},
|
||||
{name: "desktop directory", staged: newStaged(filepath.Join(os.TempDir(), "Desktop", "GoNavi-1.2.3"))},
|
||||
{name: "empty workspace", staged: newStaged("")},
|
||||
}
|
||||
outsidePackage := newStaged(validWorkspace)
|
||||
outsidePackage.FilePath = filepath.Join(os.TempDir(), "GoNavi-1.2.3.dmg")
|
||||
cases = append(cases, struct {
|
||||
name string
|
||||
staged *stagedUpdate
|
||||
}{name: "package outside workspace", staged: outsidePackage})
|
||||
|
||||
for _, tc := range cases {
|
||||
if got := shouldWindowsUpdateLaunchDownloadedAssetDirectly(tc.assetPath); got != tc.want {
|
||||
t.Fatalf("shouldWindowsUpdateLaunchDownloadedAssetDirectly(%q) = %v, want %v", tc.assetPath, got, tc.want)
|
||||
}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := validateStagedUpdateWorkspace(tc.staged); err == nil {
|
||||
t.Fatalf("unsafe workspace accepted: %#v", tc.staged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,23 +1337,32 @@ func TestExpectedAssetNameForExecutableSupportsLinuxArm64(t *testing.T) {
|
||||
|
||||
func TestBuildLinuxScriptPrefersTargetExecutableBasename(t *testing.T) {
|
||||
script := buildLinuxScript(
|
||||
"/tmp/GoNavi-0.6.5-Linux-Amd64-WebKit41.tar.gz",
|
||||
"/tmp/GoNavi/updates/0.6.5/GoNavi-0.6.5-Linux-Amd64-WebKit41.tar.gz",
|
||||
"/opt/GoNavi/gonavi-build-linux-amd64-webkit41",
|
||||
"/tmp/.gonavi-update-linux-0.6.5",
|
||||
"/tmp/GoNavi/updates",
|
||||
"/tmp/GoNavi/updates/0.6.5/.gonavi-update-linux-0.6.5",
|
||||
"/tmp/GoNavi/updates/0.6.5/update.log",
|
||||
12345,
|
||||
)
|
||||
|
||||
mustContain := []string{
|
||||
`TARGET_NAME="$(basename "$TARGET")"`,
|
||||
`NEWBIN="$TMPDIR/$TARGET_NAME"`,
|
||||
`NEWBIN=$(find "$TMPDIR" -type f -name "$TARGET_NAME" | head -n 1)`,
|
||||
`NEWBIN=$(find "$TMPDIR" -type f -name "GoNavi" | head -n 1)`,
|
||||
`NEWBIN="$UPDATE_TMP_DIR/$TARGET_NAME"`,
|
||||
`NEWBIN=$(find "$UPDATE_TMP_DIR" -type f -name "$TARGET_NAME" | head -n 1)`,
|
||||
`NEWBIN=$(find "$UPDATE_TMP_DIR" -type f -name "GoNavi" | head -n 1)`,
|
||||
`if ! kill -0 "$NEW_PID" 2>/dev/null; then`,
|
||||
`exec rm -rf "$UPDATES_DIR"`,
|
||||
}
|
||||
for _, want := range mustContain {
|
||||
if !strings.Contains(script, want) {
|
||||
t.Fatalf("linux update script missing required token: %s\nscript:\n%s", want, script)
|
||||
}
|
||||
}
|
||||
launchIdx := strings.Index(script, `"$TARGET" >/dev/null 2>&1 &`)
|
||||
cleanupIdx := strings.Index(script, `exec rm -rf "$UPDATES_DIR"`)
|
||||
if launchIdx < 0 || cleanupIdx < launchIdx {
|
||||
t.Fatalf("linux updates cleanup must follow successful relaunch (launch=%d cleanup=%d)\n%s", launchIdx, cleanupIdx, script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGitHubAPIRequestHeadersUsesTokenAndVersion(t *testing.T) {
|
||||
|
||||
@@ -115,6 +115,7 @@ func TestBuildWindowsLaunchCommandUsesHiddenPowerShellFile(t *testing.T) {
|
||||
SourcePath: `C:\tmp\GoNavi-0.8.5-Windows-Amd64.exe`,
|
||||
TargetPath: `C:\GoNavi\GoNavi.exe`,
|
||||
CurrentTargetPath: `C:\GoNavi\GoNavi.exe`,
|
||||
UpdatesDir: `C:\tmp\gonavi-update`,
|
||||
StagedDir: `C:\tmp\gonavi-update`,
|
||||
LogPath: `C:\tmp\gonavi-update\update.log`,
|
||||
MaintenanceEventName: `Global\GoNavi-Update-Test`,
|
||||
|
||||
@@ -50,6 +50,7 @@ func launchWindowsMSIUpdate(staged *stagedUpdate, targetExe string, pid int, wai
|
||||
context := windowsMSIUpdateLaunchContext{
|
||||
SourcePath: staged.FilePath,
|
||||
TargetPath: strings.TrimSpace(targetExe),
|
||||
UpdatesDir: resolveUpdateCleanupDir(staged.WorkspaceDir),
|
||||
StagedDir: staged.StagedDir,
|
||||
LogPath: staged.InstallLogPath,
|
||||
MSILogPath: msiLogPath,
|
||||
@@ -135,6 +136,7 @@ func launchWindowsUpdateWithCleanup(staged *stagedUpdate, targetExe string, pid
|
||||
SourcePath: staged.FilePath,
|
||||
TargetPath: finalTargetExe,
|
||||
CurrentTargetPath: currentTargetExe,
|
||||
UpdatesDir: resolveUpdateCleanupDir(staged.WorkspaceDir),
|
||||
StagedDir: staged.StagedDir,
|
||||
LogPath: staged.InstallLogPath,
|
||||
MaintenanceEventName: staged.MaintenanceEventName,
|
||||
|
||||
@@ -149,12 +149,12 @@ func TestResolveWindowsUpdateFinalTargetPathKeepsFixedExecutablePath(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWindowsPowerShellScriptSchedulesStagedDirectoryCleanupAfterSuccess(t *testing.T) {
|
||||
func TestBuildWindowsPowerShellScriptSchedulesUpdatesDirectoryCleanupAfterSuccess(t *testing.T) {
|
||||
script := buildWindowsPowerShellScript()
|
||||
|
||||
mustContain := []string{
|
||||
`Write-UpdateLog 'update finished'`,
|
||||
`$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_STAGED_DIR`,
|
||||
`$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR`,
|
||||
`$CleanupWorkingDirectory = [IO.Path]::GetTempPath()`,
|
||||
`-EncodedCommand`,
|
||||
}
|
||||
@@ -166,6 +166,12 @@ func TestBuildWindowsPowerShellScriptSchedulesStagedDirectoryCleanupAfterSuccess
|
||||
if strings.Contains(script, `cmd.exe`) {
|
||||
t.Fatalf("PowerShell updater must not route cleanup through cmd.exe\n%s", script)
|
||||
}
|
||||
relaunchIdx := strings.Index(script, `$NewProcess = Start-Process -FilePath $Target`)
|
||||
cleanupIdx := strings.Index(script, `$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR`)
|
||||
failureIdx := strings.LastIndex(script, `} catch {`)
|
||||
if relaunchIdx < 0 || cleanupIdx < relaunchIdx || failureIdx < cleanupIdx {
|
||||
t.Fatalf("updates cleanup must be scheduled only on the success path (relaunch=%d cleanup=%d failure=%d)\n%s", relaunchIdx, cleanupIdx, failureIdx, script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWindowsPowerShellScriptDoesNotEmbedUnicodeRuntimePaths(t *testing.T) {
|
||||
@@ -210,8 +216,9 @@ func TestBuildWindowsLaunchCommandPreservesSpecialPathsInEnvironment(t *testing.
|
||||
SourcePath: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\GoNavi-0.8.5-Windows-Amd64.exe`,
|
||||
TargetPath: `D:\软件 ! 100% & (便携版)\O'Brien\GoNavi.exe`,
|
||||
CurrentTargetPath: `D:\软件 ! 100% & (便携版)\O'Brien\GoNavi-dev-f930ffe.exe`,
|
||||
StagedDir: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\stage`,
|
||||
LogPath: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\stage\update.log`,
|
||||
UpdatesDir: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\updates`,
|
||||
StagedDir: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\updates\0.8.5\stage`,
|
||||
LogPath: `C:\Users\tester\AppData\Local\Temp\GoNavi %TEMP%\updates\0.8.5\stage\update.log`,
|
||||
MaintenanceEventName: `Global\GoNavi-Update-Test`,
|
||||
HandoffEventName: `Local\GoNavi-Update-Handoff-Test`,
|
||||
PID: 12345,
|
||||
@@ -222,6 +229,7 @@ func TestBuildWindowsLaunchCommandPreservesSpecialPathsInEnvironment(t *testing.
|
||||
"GONAVI_UPDATE_SOURCE": context.SourcePath,
|
||||
"GONAVI_UPDATE_TARGET": context.TargetPath,
|
||||
"GONAVI_UPDATE_CURRENT_TARGET": context.CurrentTargetPath,
|
||||
"GONAVI_UPDATE_ROOT_DIR": context.UpdatesDir,
|
||||
"GONAVI_UPDATE_STAGED_DIR": context.StagedDir,
|
||||
"GONAVI_UPDATE_LOG_PATH": context.LogPath,
|
||||
"GONAVI_UPDATE_MAINTENANCE_EVENT_NAME": context.MaintenanceEventName,
|
||||
|
||||
@@ -60,18 +60,33 @@ func TestExpectedAssetNameForWindowsInstallMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveUpdateWorkspaceDirForPlatformSeparatesMSIFromInstallDirectory(t *testing.T) {
|
||||
installTarget := filepath.Join("C:\\Program Files", "GoNavi", "GoNavi.exe")
|
||||
userCacheDir := filepath.Join("C:\\Users", "tester", "AppData", "Local")
|
||||
|
||||
msiDir := resolveUpdateWorkspaceDirForPlatform("windows", "1.2.3", updateInstallModeMSI, installTarget, userCacheDir)
|
||||
wantMSIDir := filepath.Join(userCacheDir, "GoNavi", "updates")
|
||||
if msiDir != wantMSIDir {
|
||||
t.Fatalf("MSI workspace = %q, want %q", msiDir, wantMSIDir)
|
||||
func TestResolveUpdateWorkspaceDirForPlatformUsesVersionedCacheForEveryPlatform(t *testing.T) {
|
||||
installTarget := filepath.Join("Users", "tester", "Desktop", "GoNavi.exe")
|
||||
userCacheDir := filepath.Join("Users", "tester", "cache")
|
||||
want := filepath.Join(userCacheDir, "GoNavi", "updates", "1.2.3")
|
||||
cases := []struct {
|
||||
goos string
|
||||
installMode updateInstallMode
|
||||
}{
|
||||
{goos: "darwin", installMode: updateInstallModePortable},
|
||||
{goos: "windows", installMode: updateInstallModePortable},
|
||||
{goos: "windows", installMode: updateInstallModeMSI},
|
||||
{goos: "linux", installMode: updateInstallModePortable},
|
||||
}
|
||||
portableDir := resolveUpdateWorkspaceDirForPlatform("windows", "1.2.3", updateInstallModePortable, installTarget, userCacheDir)
|
||||
if portableDir != filepath.Dir(installTarget) {
|
||||
t.Fatalf("portable workspace = %q, want install directory %q", portableDir, filepath.Dir(installTarget))
|
||||
|
||||
for _, tc := range cases {
|
||||
got := resolveUpdateWorkspaceDirForPlatform(tc.goos, "1.2.3", tc.installMode, installTarget, userCacheDir)
|
||||
if got != want {
|
||||
t.Fatalf("%s/%s workspace = %q, want %q", tc.goos, tc.installMode, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveUpdateWorkspaceDirForPlatformFallsBackToVersionedTempDirectory(t *testing.T) {
|
||||
got := resolveUpdateWorkspaceDirForPlatform("linux", "v1.2.3 beta", updateInstallModePortable, "/opt/GoNavi", "")
|
||||
want := filepath.Join(os.TempDir(), "GoNavi", "updates", "v1.2.3-beta")
|
||||
if got != want {
|
||||
t.Fatalf("temporary workspace = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ $ErrorActionPreference = 'Stop'
|
||||
|
||||
$Source = $env:GONAVI_UPDATE_SOURCE
|
||||
$Target = $env:GONAVI_UPDATE_TARGET
|
||||
$UpdatesDir = $env:GONAVI_UPDATE_ROOT_DIR
|
||||
$StagedDir = $env:GONAVI_UPDATE_STAGED_DIR
|
||||
$LogPath = $env:GONAVI_UPDATE_LOG_PATH
|
||||
$MSILogPath = $env:GONAVI_UPDATE_MSI_LOG_PATH
|
||||
@@ -73,7 +74,7 @@ function Remove-UpdateArtifact {
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($requiredPath in @($Source, $Target, $StagedDir, $LogPath, $MSILogPath, $MSIExecPath, $MaintenanceEventName, $HandoffEventName)) {
|
||||
foreach ($requiredPath in @($Source, $Target, $UpdatesDir, $StagedDir, $LogPath, $MSILogPath, $MSIExecPath, $MaintenanceEventName, $HandoffEventName)) {
|
||||
if ([string]::IsNullOrWhiteSpace($requiredPath)) {
|
||||
throw 'missing required MSI updater path'
|
||||
}
|
||||
@@ -167,7 +168,7 @@ try {
|
||||
Remove-UpdateArtifact $Source
|
||||
Write-UpdateLog 'MSI update finished'
|
||||
|
||||
$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_STAGED_DIR -Recurse -Force -ErrorAction SilentlyContinue'
|
||||
$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR -Recurse -Force -ErrorAction SilentlyContinue'
|
||||
$EncodedCleanupCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($CleanupCommand))
|
||||
$CleanupWorkingDirectory = [IO.Path]::GetTempPath()
|
||||
try {
|
||||
|
||||
@@ -16,6 +16,7 @@ var windowsShortcutRepairPowerShellScript string
|
||||
type windowsMSIUpdateLaunchContext struct {
|
||||
SourcePath string
|
||||
TargetPath string
|
||||
UpdatesDir string
|
||||
StagedDir string
|
||||
LogPath string
|
||||
MSILogPath string
|
||||
@@ -45,6 +46,7 @@ func buildWindowsMSILaunchCommand(scriptPath string, context windowsMSIUpdateLau
|
||||
cmd.Env = append(cmd.Environ(),
|
||||
"GONAVI_UPDATE_SOURCE="+context.SourcePath,
|
||||
"GONAVI_UPDATE_TARGET="+context.TargetPath,
|
||||
"GONAVI_UPDATE_ROOT_DIR="+context.UpdatesDir,
|
||||
"GONAVI_UPDATE_STAGED_DIR="+context.StagedDir,
|
||||
"GONAVI_UPDATE_LOG_PATH="+context.LogPath,
|
||||
"GONAVI_UPDATE_MSI_LOG_PATH="+context.MSILogPath,
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestBuildWindowsMSIUpdatePowerShellScriptInstallsRelaunchesAndCleans(t *tes
|
||||
`update maintenance lock could not be released before relaunch`,
|
||||
`Start-Process -FilePath $Target -WorkingDirectory $TargetDir`,
|
||||
`Remove-UpdateArtifact $Source`,
|
||||
`Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR`,
|
||||
`MSI package retained for manual install`,
|
||||
`previous application relaunched after MSI failure`,
|
||||
}
|
||||
@@ -59,6 +60,11 @@ func TestBuildWindowsMSIUpdatePowerShellScriptInstallsRelaunchesAndCleans(t *tes
|
||||
if releaseIndex < repairIndex || releaseIndex > relaunchIndex {
|
||||
t.Fatalf("maintenance lock must be released after install repair and before relaunch\n%s", script)
|
||||
}
|
||||
cleanupIndex := strings.Index(script, `$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR`)
|
||||
failureIndex := strings.LastIndex(script, `} catch {`)
|
||||
if cleanupIndex < relaunchIndex || failureIndex < cleanupIndex {
|
||||
t.Fatalf("MSI updates cleanup must be scheduled only after relaunch on the success path\n%s", script)
|
||||
}
|
||||
for _, r := range script {
|
||||
if r > 0x7f {
|
||||
t.Fatalf("MSI updater must remain ASCII-only, found %q", r)
|
||||
@@ -70,9 +76,10 @@ func TestBuildWindowsMSILaunchCommandPreservesPathsInEnvironment(t *testing.T) {
|
||||
context := windowsMSIUpdateLaunchContext{
|
||||
SourcePath: `C:\Users\tester\AppData\Local\GoNavi 100%\GoNavi-Installer.msi`,
|
||||
TargetPath: `D:\software ! 100% & portable\GoNavi.exe`,
|
||||
StagedDir: `C:\Users\tester\AppData\Local\GoNavi 100%\stage`,
|
||||
LogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\stage\update.log`,
|
||||
MSILogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\stage\msi.log`,
|
||||
UpdatesDir: `C:\Users\tester\AppData\Local\GoNavi 100%\updates`,
|
||||
StagedDir: `C:\Users\tester\AppData\Local\GoNavi 100%\updates\1.2.3\stage`,
|
||||
LogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\updates\1.2.3\stage\update.log`,
|
||||
MSILogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\updates\1.2.3\stage\msi.log`,
|
||||
MSIExecPath: `C:\Windows\System32\msiexec.exe`,
|
||||
MaintenanceEventName: `Global\GoNavi-Update-Test`,
|
||||
HandoffEventName: `Local\GoNavi-Update-Handoff-Test`,
|
||||
@@ -99,6 +106,7 @@ func TestBuildWindowsMSILaunchCommandPreservesPathsInEnvironment(t *testing.T) {
|
||||
want := map[string]string{
|
||||
"GONAVI_UPDATE_SOURCE": context.SourcePath,
|
||||
"GONAVI_UPDATE_TARGET": context.TargetPath,
|
||||
"GONAVI_UPDATE_ROOT_DIR": context.UpdatesDir,
|
||||
"GONAVI_UPDATE_STAGED_DIR": context.StagedDir,
|
||||
"GONAVI_UPDATE_LOG_PATH": context.LogPath,
|
||||
"GONAVI_UPDATE_MSI_LOG_PATH": context.MSILogPath,
|
||||
|
||||
@@ -3,6 +3,7 @@ $ErrorActionPreference = 'Stop'
|
||||
$Source = $env:GONAVI_UPDATE_SOURCE
|
||||
$Target = $env:GONAVI_UPDATE_TARGET
|
||||
$CurrentTarget = $env:GONAVI_UPDATE_CURRENT_TARGET
|
||||
$UpdatesDir = $env:GONAVI_UPDATE_ROOT_DIR
|
||||
$StagedDir = $env:GONAVI_UPDATE_STAGED_DIR
|
||||
$LogPath = $env:GONAVI_UPDATE_LOG_PATH
|
||||
$MaintenanceEventName = $env:GONAVI_UPDATE_MAINTENANCE_EVENT_NAME
|
||||
@@ -152,7 +153,7 @@ function Select-PortableExecutable {
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($requiredPath in @($Source, $Target, $CurrentTarget, $StagedDir, $LogPath, $MaintenanceEventName, $HandoffEventName)) {
|
||||
foreach ($requiredPath in @($Source, $Target, $CurrentTarget, $UpdatesDir, $StagedDir, $LogPath, $MaintenanceEventName, $HandoffEventName)) {
|
||||
if ([string]::IsNullOrWhiteSpace($requiredPath)) {
|
||||
throw 'missing required updater path'
|
||||
}
|
||||
@@ -272,7 +273,7 @@ try {
|
||||
}
|
||||
Write-UpdateLog 'update finished'
|
||||
|
||||
$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_STAGED_DIR -Recurse -Force -ErrorAction SilentlyContinue'
|
||||
$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_ROOT_DIR -Recurse -Force -ErrorAction SilentlyContinue'
|
||||
$EncodedCleanupCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($CleanupCommand))
|
||||
$CleanupWorkingDirectory = [IO.Path]::GetTempPath()
|
||||
try {
|
||||
|
||||
@@ -51,6 +51,7 @@ func TestWindowsPowerShellUpdaterHandlesUnicodeAndShellMetacharacters(t *testing
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
CurrentTargetPath: targetPath,
|
||||
UpdatesDir: stagedDir,
|
||||
StagedDir: stagedDir,
|
||||
LogPath: logPath,
|
||||
MaintenanceEventName: maintenanceName,
|
||||
@@ -128,6 +129,7 @@ func TestWindowsPowerShellUpdaterRenamesVersionedPortableExecutable(t *testing.T
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
CurrentTargetPath: currentTargetPath,
|
||||
UpdatesDir: stagedDir,
|
||||
StagedDir: stagedDir,
|
||||
LogPath: logPath,
|
||||
MaintenanceEventName: maintenanceName,
|
||||
@@ -195,6 +197,7 @@ func TestWindowsPowerShellUpdaterSelectsExactTargetFilenameRecursivelyFromZip(t
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
CurrentTargetPath: targetPath,
|
||||
UpdatesDir: stagedDir,
|
||||
StagedDir: stagedDir,
|
||||
LogPath: logPath,
|
||||
MaintenanceEventName: maintenanceName,
|
||||
@@ -248,6 +251,7 @@ func TestWindowsPowerShellUpdaterRejectsAmbiguousZipAndRetainsPackage(t *testing
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
CurrentTargetPath: targetPath,
|
||||
UpdatesDir: stagedDir,
|
||||
StagedDir: stagedDir,
|
||||
LogPath: logPath,
|
||||
MaintenanceEventName: maintenanceName,
|
||||
|
||||
@@ -15,6 +15,7 @@ type windowsUpdateLaunchContext struct {
|
||||
SourcePath string
|
||||
TargetPath string
|
||||
CurrentTargetPath string
|
||||
UpdatesDir string
|
||||
StagedDir string
|
||||
LogPath string
|
||||
MaintenanceEventName string
|
||||
|
||||
Reference in New Issue
Block a user