返回 DeepSeek-Reasonix
registry_windows.go
根目录 / desktop / internal / winuninstall / registry_windows.go
1 //go:build windows
2
3 package winuninstall
4
5 import (
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "golang.org/x/sys/windows/registry"
13 )
14
15 const (
16 uninstallRegistryBase = `Software\Microsoft\Windows\CurrentVersion\Uninstall\`
17 legacyProductRegistryPath = `Software\reasonix\Reasonix`
18 )
19
20 // Reconcile refreshes the current Wails per-user registration after a
21 // successful version activation. It intentionally refuses legacy-only and
22 // portable trees; those require the full signed installer to establish a
23 // trustworthy uninstaller first.
24 func Reconcile(installRoot, version string) (bool, error) {
25 installRoot = filepath.Clean(strings.TrimSpace(installRoot))
26 if installRoot == "" || installRoot == "." || !filepath.IsAbs(installRoot) {
27 return false, fmt.Errorf("reconcile Windows uninstall registration: invalid install root")
28 }
29 uninstaller := filepath.Join(installRoot, "uninstall.exe")
30 info, err := os.Lstat(uninstaller)
31 if err != nil {
32 if os.IsNotExist(err) {
33 return false, nil
34 }
35 return false, err
36 }
37 if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
38 return false, nil
39 }
40
41 current, err := readRegistration(CurrentKeyName)
42 if err != nil {
43 return false, err
44 }
45 legacy, err := readRegistration(LegacyKeyName)
46 if err != nil {
47 return false, err
48 }
49 plan, err := Plan(current, legacy, installRoot, version, true)
50 if err != nil || !plan.Managed {
51 return false, err
52 }
53 if err := writeRegistration(CurrentKeyName, plan.Desired); err != nil {
54 return false, err
55 }
56 written, err := readRegistration(CurrentKeyName)
57 if err != nil {
58 return false, err
59 }
60 if !sameRegistration(written, &plan.Desired) {
61 return false, fmt.Errorf("reconcile Windows uninstall registration: registry verification failed")
62 }
63 // Tauri 0.53 stored its install root separately from the Add/Remove
64 // Programs entry and restores it before every later install. Remove only a
65 // same-root default value so an old installer cannot overwrite the current
66 // uninstaller; named values and separate legacy installations remain intact.
67 if err := deleteInstallLocationIfOwned(legacyProductRegistryPath, installRoot); err != nil {
68 return false, err
69 }
70 if plan.DeleteLegacy {
71 if err := registry.DeleteKey(registry.CURRENT_USER, uninstallRegistryBase+LegacyKeyName); err != nil && !errors.Is(err, registry.ErrNotExist) {
72 return false, err
73 }
74 }
75 return true, nil
76 }
77
78 func deleteInstallLocationIfOwned(keyPath, installRoot string) error {
79 key, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE|registry.SET_VALUE)
80 if errors.Is(err, registry.ErrNotExist) {
81 return nil
82 }
83 if err != nil {
84 return err
85 }
86 defer key.Close()
87
88 location, _, err := key.GetStringValue("")
89 if errors.Is(err, registry.ErrNotExist) {
90 return nil
91 }
92 if err != nil {
93 return err
94 }
95 if !sameWindowsPath(location, installRoot) {
96 return nil
97 }
98 if err := key.DeleteValue(""); err != nil && !errors.Is(err, registry.ErrNotExist) {
99 return err
100 }
101 return nil
102 }
103
104 func readRegistration(name string) (*Registration, error) {
105 key, err := registry.OpenKey(registry.CURRENT_USER, uninstallRegistryBase+name, registry.QUERY_VALUE)
106 if errors.Is(err, registry.ErrNotExist) {
107 return nil, nil
108 }
109 if err != nil {
110 return nil, err
111 }
112 defer key.Close()
113 read := func(value string) string {
114 got, _, valueErr := key.GetStringValue(value)
115 if valueErr != nil {
116 return ""
117 }
118 return got
119 }
120 return &Registration{
121 DisplayName: read("DisplayName"),
122 DisplayVersion: read("DisplayVersion"),
123 Publisher: read("Publisher"),
124 InstallLocation: read("InstallLocation"),
125 DisplayIcon: read("DisplayIcon"),
126 UninstallString: read("UninstallString"),
127 QuietUninstallString: read("QuietUninstallString"),
128 }, nil
129 }
130
131 func writeRegistration(name string, value Registration) error {
132 key, _, err := registry.CreateKey(registry.CURRENT_USER, uninstallRegistryBase+name, registry.QUERY_VALUE|registry.SET_VALUE)
133 if err != nil {
134 return err
135 }
136 defer key.Close()
137 for name, value := range map[string]string{
138 "DisplayName": value.DisplayName,
139 "DisplayVersion": value.DisplayVersion,
140 "Publisher": value.Publisher,
141 "InstallLocation": value.InstallLocation,
142 "DisplayIcon": value.DisplayIcon,
143 "UninstallString": value.UninstallString,
144 "QuietUninstallString": value.QuietUninstallString,
145 } {
146 if err := key.SetStringValue(name, value); err != nil {
147 return err
148 }
149 }
150 if err := key.SetDWordValue("NoModify", 1); err != nil {
151 return err
152 }
153 return key.SetDWordValue("NoRepair", 1)
154 }
155
156 func sameRegistration(left, right *Registration) bool {
157 if left == nil || right == nil {
158 return left == right
159 }
160 return left.DisplayName == right.DisplayName &&
161 left.DisplayVersion == right.DisplayVersion &&
162 left.Publisher == right.Publisher &&
163 sameWindowsPath(left.InstallLocation, right.InstallLocation) &&
164 sameWindowsPath(left.DisplayIcon, right.DisplayIcon) &&
165 sameWindowsPath(uninstallExecutable(left.UninstallString), uninstallExecutable(right.UninstallString)) &&
166 strings.EqualFold(strings.TrimSpace(left.QuietUninstallString), strings.TrimSpace(right.QuietUninstallString))
167 }
168
168 lines GO