fix(desktop): repoint dangling versioned shortcut targets to the launcher

This commit is contained in:
Nontee committed 2026-08-06 18:24:07 +08:00
1 parent 6c22ae0a3f
commit a80f75bbf2
2 files changed
+101 -9

No files matched your search

+47 -9
View File
@@ -126,18 +126,39 @@ func repairWindowsShortcut(shortcutPath, launcher string) (bool, error) {
}
iconLocation := iconValue.ToString()
_ = iconValue.Clear()
if !reasonixWindowsStaleIcon(iconLocation, launcher) {
repointTarget, fixIcon := repairWindowsShortcutPlan(target, iconLocation, launcher)
if !repointTarget && !fixIcon {
return false, nil
}
result, err := oleutil.PutProperty(shortcut, "IconLocation", launcher+",0")
if result != nil {
_ = result.Clear()
if repointTarget {
// A version-scoped target points into versions/<v>/reasonix-desktop.exe,
// which the updater deletes when it switches or prunes versions. Repoint
// it at the stable launcher so the shortcut survives updates.
result, err := oleutil.PutProperty(shortcut, "TargetPath", launcher)
if result != nil {
_ = result.Clear()
}
if err != nil {
return false, fmt.Errorf("set TargetPath: %w", err)
}
result, err = oleutil.PutProperty(shortcut, "WorkingDirectory", filepath.Dir(launcher))
if result != nil {
_ = result.Clear()
}
if err != nil {
return false, fmt.Errorf("set WorkingDirectory: %w", err)
}
}
if err != nil {
return false, fmt.Errorf("set IconLocation: %w", err)
if fixIcon {
result, err := oleutil.PutProperty(shortcut, "IconLocation", launcher+",0")
if result != nil {
_ = result.Clear()
}
if err != nil {
return false, fmt.Errorf("set IconLocation: %w", err)
}
}
result, err = oleutil.CallMethod(shortcut, "Save")
result, err := oleutil.CallMethod(shortcut, "Save")
if result != nil {
_ = result.Clear()
}
@@ -160,7 +181,16 @@ func reasonixWindowsShortcutTarget(target, launcher string) bool {
return true
}
}
rel, err := filepath.Rel(root, target)
return reasonixWindowsVersionedTarget(target, launcher)
}
// reasonixWindowsVersionedTarget reports whether target points into this
// install's versions/<version>/reasonix-desktop.exe, a version-scoped path
// that the updater deletes when it switches or prunes versions. Such targets
// dangle after an update, so repair must repoint them at the stable launcher.
func reasonixWindowsVersionedTarget(target, launcher string) bool {
root := filepath.Dir(filepath.Clean(strings.TrimSpace(launcher)))
rel, err := filepath.Rel(root, filepath.Clean(strings.TrimSpace(target)))
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return false
}
@@ -169,6 +199,14 @@ func reasonixWindowsShortcutTarget(target, launcher string) bool {
strings.EqualFold(parts[2], "reasonix-desktop.exe")
}
// repairWindowsShortcutPlan decides which owned-shortcut properties need
// rewriting. repointTarget is true when TargetPath points into a versioned
// directory the updater can delete; fixIcon is true when IconLocation points
// at the versioned desktop binary instead of the stable launcher.
func repairWindowsShortcutPlan(target, iconLocation, launcher string) (repointTarget, fixIcon bool) {
return reasonixWindowsVersionedTarget(target, launcher), reasonixWindowsStaleIcon(iconLocation, launcher)
}
func reasonixWindowsStaleIcon(iconLocation, launcher string) bool {
iconPath := strings.TrimSpace(iconLocation)
if comma := strings.LastIndex(iconPath, ","); comma >= 0 {
+54
View File
@@ -95,3 +95,57 @@ func TestReasonixWindowsStaleIconOnlyMatchesVersionedDesktop(t *testing.T) {
})
}
}
func TestReasonixWindowsVersionedTarget(t *testing.T) {
root := filepath.Join(`C:\Program Files`, "Reasonix")
launcher := filepath.Join(root, "reasonix-launcher.exe")
tests := []struct {
name string
target string
want bool
}{
{name: "versioned desktop", target: filepath.Join(root, "versions", "v1.19.3", "reasonix-desktop.exe"), want: true},
{name: "case-insensitive", target: filepath.Join(root, "Versions", "V1.19.3", "Reasonix-Desktop.exe"), want: true},
{name: "launcher", target: launcher, want: false},
{name: "launcher alias", target: filepath.Join(root, "Reasonix.exe"), want: false},
{name: "flat desktop", target: filepath.Join(root, "reasonix-desktop.exe"), want: false},
{name: "deeper versioned path", target: filepath.Join(root, "versions", "v1.19.3", "sub", "reasonix-desktop.exe"), want: false},
{name: "wrong binary in versions", target: filepath.Join(root, "versions", "v1.19.3", "reasonix-cli.exe"), want: false},
{name: "other install", target: filepath.Join(`D:\Apps`, "Reasonix", "versions", "v1.19.3", "reasonix-desktop.exe"), want: false},
{name: "empty", target: "", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := reasonixWindowsVersionedTarget(tt.target, launcher); got != tt.want {
t.Fatalf("reasonixWindowsVersionedTarget(%q, %q) = %v, want %v", tt.target, launcher, got, tt.want)
}
})
}
}
func TestRepairWindowsShortcutPlan(t *testing.T) {
root := filepath.Join(`C:\Program Files`, "Reasonix")
launcher := filepath.Join(root, "reasonix-launcher.exe")
versioned := filepath.Join(root, "versions", "v1.19.3", "reasonix-desktop.exe")
tests := []struct {
name string
target string
icon string
wantRepoint bool
wantFixIcon bool
}{
{name: "versioned target + versioned icon", target: versioned, icon: versioned + ",0", wantRepoint: true, wantFixIcon: true},
{name: "versioned target + clean icon", target: versioned, icon: launcher + ",0", wantRepoint: true, wantFixIcon: false},
{name: "stable target + versioned icon", target: launcher, icon: versioned + ",0", wantRepoint: false, wantFixIcon: true},
{name: "stable target + clean icon", target: launcher, icon: launcher + ",0", wantRepoint: false, wantFixIcon: false},
{name: "custom target + custom icon", target: filepath.Join(root, "custom.ico"), icon: filepath.Join(root, "custom.ico") + ",0", wantRepoint: false, wantFixIcon: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repoint, fixIcon := repairWindowsShortcutPlan(tt.target, tt.icon, launcher)
if repoint != tt.wantRepoint || fixIcon != tt.wantFixIcon {
t.Fatalf("repairWindowsShortcutPlan(%q, %q) = (%v, %v), want (%v, %v)", tt.target, tt.icon, repoint, fixIcon, tt.wantRepoint, tt.wantFixIcon)
}
})
}
}