mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-25 02:00:09 +08:00
🐛 fix(update): 支持 Windows 便携版 ZIP 自动更新
- 将便携版更新资产切换为普通 ZIP,并兼容复用已下载包 - 解压后映射版本化目标文件名,保留旧版 EXE 更新兼容 - 将内嵌 PowerShell 更新脚本策略收紧为 RemoteSigned - 补充 ZIP、目标路径与启动参数回归测试
This commit is contained in:
@@ -969,7 +969,7 @@ func expectedAssetNameForExecutableAndInstallMode(goos, goarch, version, executa
|
|||||||
|
|
||||||
switch goos {
|
switch goos {
|
||||||
case "windows":
|
case "windows":
|
||||||
suffix := "-Portable.exe"
|
suffix := "-Portable.zip"
|
||||||
if installMode == updateInstallModeMSI {
|
if installMode == updateInstallModeMSI {
|
||||||
suffix = "-Installer.msi"
|
suffix = "-Installer.msi"
|
||||||
} else if installMode != updateInstallModePortable {
|
} else if installMode != updateInstallModePortable {
|
||||||
@@ -1862,7 +1862,7 @@ func buildWindowsLaunchCommand(scriptPath string, context windowsUpdateLaunchCon
|
|||||||
"-NoProfile",
|
"-NoProfile",
|
||||||
"-NonInteractive",
|
"-NonInteractive",
|
||||||
"-ExecutionPolicy",
|
"-ExecutionPolicy",
|
||||||
"Bypass",
|
windowsUpdatePowerShellExecutionPolicy,
|
||||||
"-File",
|
"-File",
|
||||||
scriptPath,
|
scriptPath,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -595,6 +595,40 @@ func TestResolveReusableStagedUpdateForPlatformDoesNotReuseCurrentWindowsExeInsi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveReusableStagedUpdateForPlatformReusesPortableZipInsideStagedDir(t *testing.T) {
|
||||||
|
preferredWorkspaceDir := t.TempDir()
|
||||||
|
legacyWorkspaceDir := t.TempDir()
|
||||||
|
info := UpdateInfo{
|
||||||
|
Channel: string(updateChannelLatest),
|
||||||
|
LatestVersion: "0.8.5",
|
||||||
|
AssetName: "GoNavi-0.8.5-Windows-Amd64-Portable.zip",
|
||||||
|
AssetSize: 8,
|
||||||
|
InstallMode: string(updateInstallModePortable),
|
||||||
|
PackageType: string(updatePackageTypePortable),
|
||||||
|
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)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
if reused.FilePath != assetPath || reused.PackageType != updatePackageTypePortable {
|
||||||
|
t.Fatalf("unexpected reused portable ZIP: %#v", reused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloadUpdateUsesCurrentLanguageForBackendMessage(t *testing.T) {
|
func TestDownloadUpdateUsesCurrentLanguageForBackendMessage(t *testing.T) {
|
||||||
app := NewApp()
|
app := NewApp()
|
||||||
app.SetLanguage("en-US")
|
app.SetLanguage("en-US")
|
||||||
@@ -797,13 +831,13 @@ func TestExpectedAssetNameForExecutableUsesWindowsPortableSuffix(t *testing.T) {
|
|||||||
name: "amd64 release",
|
name: "amd64 release",
|
||||||
goarch: "amd64",
|
goarch: "amd64",
|
||||||
version: "v1.2.3",
|
version: "v1.2.3",
|
||||||
want: "GoNavi-1.2.3-Windows-Amd64-Portable.exe",
|
want: "GoNavi-1.2.3-Windows-Amd64-Portable.zip",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "arm64 dev",
|
name: "arm64 dev",
|
||||||
goarch: "arm64",
|
goarch: "arm64",
|
||||||
version: "dev-a1b2c3d",
|
version: "dev-a1b2c3d",
|
||||||
want: "GoNavi-dev-a1b2c3d-Windows-Arm64-Portable.exe",
|
want: "GoNavi-dev-a1b2c3d-Windows-Arm64-Portable.zip",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestBuildWindowsLaunchCommandUsesHiddenPowerShellFile(t *testing.T) {
|
|||||||
"-NoProfile",
|
"-NoProfile",
|
||||||
"-NonInteractive",
|
"-NonInteractive",
|
||||||
"-ExecutionPolicy",
|
"-ExecutionPolicy",
|
||||||
"Bypass",
|
"RemoteSigned",
|
||||||
"-File",
|
"-File",
|
||||||
scriptPath,
|
scriptPath,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,8 +173,9 @@ func resolveWindowsUpdateFinalTargetPath(currentTarget string, sourcePath string
|
|||||||
}
|
}
|
||||||
currentName := filepath.Base(currentTarget)
|
currentName := filepath.Base(currentTarget)
|
||||||
sourceName := filepath.Base(strings.TrimSpace(sourcePath))
|
sourceName := filepath.Base(strings.TrimSpace(sourcePath))
|
||||||
if isVersionedWindowsUpdatePackageName(currentName) && isVersionedWindowsUpdatePackageName(sourceName) {
|
sourceExecutableName := resolveVersionedWindowsUpdateExecutableName(sourceName)
|
||||||
return filepath.Join(filepath.Dir(currentTarget), sourceName)
|
if isVersionedWindowsUpdatePackageName(currentName) && sourceExecutableName != "" {
|
||||||
|
return filepath.Join(filepath.Dir(currentTarget), sourceExecutableName)
|
||||||
}
|
}
|
||||||
return currentTarget
|
return currentTarget
|
||||||
}
|
}
|
||||||
@@ -187,6 +188,22 @@ func isVersionedWindowsUpdatePackageName(name string) bool {
|
|||||||
strings.HasSuffix(lower, ".exe")
|
strings.HasSuffix(lower, ".exe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveVersionedWindowsUpdateExecutableName(name string) string {
|
||||||
|
trimmed := strings.TrimSpace(name)
|
||||||
|
if !strings.HasPrefix(trimmed, "GoNavi-") || !strings.Contains(trimmed, "-Windows-") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
extension := filepath.Ext(trimmed)
|
||||||
|
switch strings.ToLower(extension) {
|
||||||
|
case ".exe":
|
||||||
|
return trimmed
|
||||||
|
case ".zip":
|
||||||
|
return strings.TrimSuffix(trimmed, extension) + ".exe"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func prepareWindowsStagedUpdateAsset(sourcePath string, stagedDir string) (string, error) {
|
func prepareWindowsStagedUpdateAsset(sourcePath string, stagedDir string) (string, error) {
|
||||||
sourcePath = strings.TrimSpace(sourcePath)
|
sourcePath = strings.TrimSpace(sourcePath)
|
||||||
stagedDir = strings.TrimSpace(stagedDir)
|
stagedDir = strings.TrimSpace(stagedDir)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func TestShouldRemoveWindowsUpdateArtifact(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{name: "GoNavi-dev-abc-Windows-Amd64.exe", want: true},
|
{name: "GoNavi-dev-abc-Windows-Amd64.exe", want: true},
|
||||||
{name: "GoNavi-dev-abc-Windows-Amd64-Installer.msi", want: true},
|
{name: "GoNavi-dev-abc-Windows-Amd64-Installer.msi", want: true},
|
||||||
{name: "GoNavi-0.8.4-Windows-Amd64.zip", want: true},
|
{name: "GoNavi-0.8.4-Windows-Amd64-Portable.zip", want: true},
|
||||||
{name: "gonavi-update-windows-123.log", want: true},
|
{name: "gonavi-update-windows-123.log", want: true},
|
||||||
{name: ".gonavi-update-windows-dev-dev-abc", isDir: true, want: true},
|
{name: ".gonavi-update-windows-dev-dev-abc", isDir: true, want: true},
|
||||||
{name: "GoNavi-dev-abc-MacOS-Arm64.dmg", want: false},
|
{name: "GoNavi-dev-abc-MacOS-Arm64.dmg", want: false},
|
||||||
@@ -120,19 +120,29 @@ func TestPrepareWindowsStagedUpdateAssetMovesPackageIntoStagedDir(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveWindowsUpdateFinalTargetPathUsesDownloadedVersionedPortableName(t *testing.T) {
|
func TestResolveWindowsUpdateFinalTargetPathMapsDownloadedPortableZipToExe(t *testing.T) {
|
||||||
currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi-dev-f930ffe-Windows-Amd64.exe")
|
currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi-dev-f930ffe-Windows-Amd64.exe")
|
||||||
stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe")
|
stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.zip")
|
||||||
want := filepath.Join(filepath.Dir(currentTarget), filepath.Base(stagedSource))
|
want := filepath.Join(filepath.Dir(currentTarget), "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe")
|
||||||
|
|
||||||
if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != want {
|
if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != want {
|
||||||
t.Fatalf("Windows update target = %q, want downloaded versioned path %q", got, want)
|
t.Fatalf("Windows update target = %q, want downloaded versioned path %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveWindowsUpdateFinalTargetPathKeepsLegacyPortableExeName(t *testing.T) {
|
||||||
|
currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi-dev-f930ffe-Windows-Amd64.exe")
|
||||||
|
stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe")
|
||||||
|
want := filepath.Join(filepath.Dir(currentTarget), filepath.Base(stagedSource))
|
||||||
|
|
||||||
|
if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != want {
|
||||||
|
t.Fatalf("Windows update target = %q, want legacy executable path %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveWindowsUpdateFinalTargetPathKeepsFixedExecutablePath(t *testing.T) {
|
func TestResolveWindowsUpdateFinalTargetPathKeepsFixedExecutablePath(t *testing.T) {
|
||||||
currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi.exe")
|
currentTarget := filepath.Join("D:", "软件", "数据库管理工具", "GoNavi", "GoNavi.exe")
|
||||||
stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.exe")
|
stagedSource := filepath.Join("C:", "Temp", "gonavi-updates", "GoNavi-dev-2d5f246-Windows-Amd64-Portable.zip")
|
||||||
|
|
||||||
if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != currentTarget {
|
if got := resolveWindowsUpdateFinalTargetPath(currentTarget, stagedSource); got != currentTarget {
|
||||||
t.Fatalf("Windows update target = %q, want fixed executable path %q", got, currentTarget)
|
t.Fatalf("Windows update target = %q, want fixed executable path %q", got, currentTarget)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func isUpdatePackageCompatibleWithInstallMode(goos string, installMode updateIns
|
|||||||
case updateInstallModeMSI:
|
case updateInstallModeMSI:
|
||||||
return packageType == updatePackageTypeMSI && extension == ".msi"
|
return packageType == updatePackageTypeMSI && extension == ".msi"
|
||||||
case updateInstallModePortable:
|
case updateInstallModePortable:
|
||||||
return packageType == updatePackageTypePortable && extension == ".exe"
|
return packageType == updatePackageTypePortable && (extension == ".zip" || extension == ".exe")
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ func TestExpectedAssetNameForWindowsInstallMode(t *testing.T) {
|
|||||||
installMode updateInstallMode
|
installMode updateInstallMode
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{name: "amd64 portable", arch: "amd64", installMode: updateInstallModePortable, want: "GoNavi-1.2.3-Windows-Amd64-Portable.exe"},
|
{name: "amd64 portable", arch: "amd64", installMode: updateInstallModePortable, want: "GoNavi-1.2.3-Windows-Amd64-Portable.zip"},
|
||||||
{name: "amd64 msi", arch: "amd64", installMode: updateInstallModeMSI, want: "GoNavi-1.2.3-Windows-Amd64-Installer.msi"},
|
{name: "amd64 msi", arch: "amd64", installMode: updateInstallModeMSI, want: "GoNavi-1.2.3-Windows-Amd64-Installer.msi"},
|
||||||
{name: "arm64 portable", arch: "arm64", installMode: updateInstallModePortable, want: "GoNavi-1.2.3-Windows-Arm64-Portable.exe"},
|
{name: "arm64 portable", arch: "arm64", installMode: updateInstallModePortable, want: "GoNavi-1.2.3-Windows-Arm64-Portable.zip"},
|
||||||
{name: "arm64 msi", arch: "arm64", installMode: updateInstallModeMSI, want: "GoNavi-1.2.3-Windows-Arm64-Installer.msi"},
|
{name: "arm64 msi", arch: "arm64", installMode: updateInstallModeMSI, want: "GoNavi-1.2.3-Windows-Arm64-Installer.msi"},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -90,3 +90,17 @@ func TestValidateUpdatePackageForCurrentInstallModeRejectsModeAndSuffixMismatch(
|
|||||||
t.Fatal("expected invalid MSI suffix to be rejected")
|
t.Fatal("expected invalid MSI suffix to be rejected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPortableUpdatePackageAcceptsZipAndLegacyExe(t *testing.T) {
|
||||||
|
for _, assetPath := range []string{
|
||||||
|
`C:\\tmp\\GoNavi-Portable.zip`,
|
||||||
|
`C:\\tmp\\GoNavi-Portable.exe`,
|
||||||
|
} {
|
||||||
|
if !isUpdatePackageCompatibleWithInstallMode("windows", updateInstallModePortable, updatePackageTypePortable, assetPath) {
|
||||||
|
t.Fatalf("valid portable package rejected: %s", assetPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isUpdatePackageCompatibleWithInstallMode("windows", updateInstallModePortable, updatePackageTypePortable, `C:\\tmp\\GoNavi-Installer.msi`) {
|
||||||
|
t.Fatal("expected MSI suffix to be rejected for portable mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func buildWindowsMSILaunchCommand(scriptPath string, context windowsMSIUpdateLau
|
|||||||
"-NoProfile",
|
"-NoProfile",
|
||||||
"-NonInteractive",
|
"-NonInteractive",
|
||||||
"-ExecutionPolicy",
|
"-ExecutionPolicy",
|
||||||
"Bypass",
|
windowsUpdatePowerShellExecutionPolicy,
|
||||||
"-File",
|
"-File",
|
||||||
scriptPath,
|
scriptPath,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ func TestBuildWindowsMSILaunchCommandPreservesPathsInEnvironment(t *testing.T) {
|
|||||||
PID: 12345,
|
PID: 12345,
|
||||||
}
|
}
|
||||||
cmd := buildWindowsMSILaunchCommand(filepath.Join(context.StagedDir, "update-msi.ps1"), context)
|
cmd := buildWindowsMSILaunchCommand(filepath.Join(context.StagedDir, "update-msi.ps1"), context)
|
||||||
|
wantArgs := []string{
|
||||||
|
"powershell.exe",
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"RemoteSigned",
|
||||||
|
"-File",
|
||||||
|
filepath.Join(context.StagedDir, "update-msi.ps1"),
|
||||||
|
}
|
||||||
|
if len(cmd.Args) != len(wantArgs) {
|
||||||
|
t.Fatalf("unexpected arg length: got %d want %d, args=%v", len(cmd.Args), len(wantArgs), cmd.Args)
|
||||||
|
}
|
||||||
|
for index := range wantArgs {
|
||||||
|
if cmd.Args[index] != wantArgs[index] {
|
||||||
|
t.Fatalf("unexpected arg[%d]: got %q want %q", index, cmd.Args[index], wantArgs[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
want := map[string]string{
|
want := map[string]string{
|
||||||
"GONAVI_UPDATE_SOURCE": context.SourcePath,
|
"GONAVI_UPDATE_SOURCE": context.SourcePath,
|
||||||
"GONAVI_UPDATE_TARGET": context.TargetPath,
|
"GONAVI_UPDATE_TARGET": context.TargetPath,
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ if (-not (Test-SameFilePath $restoredMatchingShortcut.TargetPath $target)) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
command := exec.Command(powerShell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath)
|
command := exec.Command(powerShell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "RemoteSigned", "-File", scriptPath)
|
||||||
command.Env = append(os.Environ(),
|
command.Env = append(os.Environ(),
|
||||||
"GONAVI_TEST_TARGET="+targetPath,
|
"GONAVI_TEST_TARGET="+targetPath,
|
||||||
"GONAVI_TEST_FOREIGN_TARGET="+foreignTargetPath,
|
"GONAVI_TEST_FOREIGN_TARGET="+foreignTargetPath,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
//go:embed windows_update.ps1
|
//go:embed windows_update.ps1
|
||||||
var windowsUpdatePowerShellScript string
|
var windowsUpdatePowerShellScript string
|
||||||
|
|
||||||
|
// The updater writes these embedded scripts locally, so RemoteSigned runs them without disabling policy checks.
|
||||||
|
const windowsUpdatePowerShellExecutionPolicy = "RemoteSigned"
|
||||||
|
|
||||||
type windowsUpdateLaunchContext struct {
|
type windowsUpdateLaunchContext struct {
|
||||||
SourcePath string
|
SourcePath string
|
||||||
TargetPath string
|
TargetPath string
|
||||||
|
|||||||
Reference in New Issue
Block a user