返回 DeepSeek-Reasonix
icon_repair_windows.go
根目录 / desktop / icon_repair_windows.go
1 //go:build windows
2
3 package main
4
5 import (
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "runtime"
11 "strconv"
12 "strings"
13 "unsafe"
14
15 "github.com/go-ole/go-ole"
16 "github.com/go-ole/go-ole/oleutil"
17 "golang.org/x/sys/windows"
18
19 "reasonix/internal/installlayout"
20 )
21
22 var (
23 windowsKnownFolderPath = windows.KnownFolderPath
24 windowsRepairShortcut = repairWindowsShortcut
25 windowsNotifyShortcutChange = notifyWindowsShortcutChanged
26 )
27
28 func repairDesktopIconIntegration() error {
29 executable, err := os.Executable()
30 if err != nil {
31 return nil
32 }
33 installRoot, err := installlayout.ResolveInstallRoot(executable)
34 if err != nil || installRoot == "" {
35 return nil
36 }
37 launcher := filepath.Join(installRoot, "reasonix-launcher.exe")
38 if info, err := os.Lstat(launcher); err != nil || !info.Mode().IsRegular() {
39 return nil
40 }
41 paths, err := reasonixWindowsShortcutPaths()
42 if err != nil {
43 return err
44 }
45 return repairExistingWindowsShortcuts(paths, launcher, windowsRepairShortcut)
46 }
47
48 func reasonixWindowsShortcutPaths() ([]string, error) {
49 desktop, desktopErr := windowsKnownFolderPath(windows.FOLDERID_Desktop, windows.KF_FLAG_DEFAULT)
50 programs, programsErr := windowsKnownFolderPath(windows.FOLDERID_Programs, windows.KF_FLAG_DEFAULT)
51 if desktopErr != nil || programsErr != nil {
52 return nil, errors.Join(desktopErr, programsErr)
53 }
54 return []string{
55 filepath.Join(desktop, "Reasonix.lnk"),
56 filepath.Join(programs, "Reasonix.lnk"),
57 }, nil
58 }
59
60 func repairExistingWindowsShortcuts(paths []string, launcher string, repair func(string, string) (bool, error)) error {
61 var repairErr error
62 for _, path := range paths {
63 info, err := os.Lstat(path)
64 if err != nil {
65 if !os.IsNotExist(err) {
66 repairErr = errors.Join(repairErr, err)
67 }
68 continue
69 }
70 if !info.Mode().IsRegular() {
71 continue
72 }
73 repaired, err := repair(path, launcher)
74 if err != nil {
75 repairErr = errors.Join(repairErr, fmt.Errorf("%s: %w", path, err))
76 continue
77 }
78 if repaired {
79 windowsNotifyShortcutChange(path)
80 }
81 }
82 return repairErr
83 }
84
85 func repairWindowsShortcut(shortcutPath, launcher string) (bool, error) {
86 runtime.LockOSThread()
87 defer runtime.UnlockOSThread()
88 if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil {
89 return false, err
90 }
91 defer ole.CoUninitialize()
92
93 unknown, err := oleutil.CreateObject("WScript.Shell")
94 if err != nil {
95 return false, err
96 }
97 defer unknown.Release()
98 shell, err := unknown.QueryInterface(ole.IID_IDispatch)
99 if err != nil {
100 return false, err
101 }
102 defer shell.Release()
103 created, err := oleutil.CallMethod(shell, "CreateShortcut", shortcutPath)
104 if err != nil {
105 return false, err
106 }
107 shortcut := created.ToIDispatch()
108 if shortcut == nil {
109 _ = created.Clear()
110 return false, fmt.Errorf("WScript.Shell returned no shortcut object")
111 }
112 defer shortcut.Release()
113
114 targetValue, err := oleutil.GetProperty(shortcut, "TargetPath")
115 if err != nil {
116 return false, fmt.Errorf("read TargetPath: %w", err)
117 }
118 target := targetValue.ToString()
119 _ = targetValue.Clear()
120 if !reasonixWindowsShortcutTarget(target, launcher) {
121 return false, nil
122 }
123 iconValue, err := oleutil.GetProperty(shortcut, "IconLocation")
124 if err != nil {
125 return false, fmt.Errorf("read IconLocation: %w", err)
126 }
127 iconLocation := iconValue.ToString()
128 _ = iconValue.Clear()
129 repointTarget, fixIcon := repairWindowsShortcutPlan(target, iconLocation, launcher)
130 if !repointTarget && !fixIcon {
131 return false, nil
132 }
133 if repointTarget {
134 // A version-scoped target points into versions/<v>/reasonix-desktop.exe,
135 // which the updater deletes when it switches or prunes versions. Repoint
136 // it at the stable launcher so the shortcut survives updates.
137 result, err := oleutil.PutProperty(shortcut, "TargetPath", launcher)
138 if result != nil {
139 _ = result.Clear()
140 }
141 if err != nil {
142 return false, fmt.Errorf("set TargetPath: %w", err)
143 }
144 result, err = oleutil.PutProperty(shortcut, "WorkingDirectory", filepath.Dir(launcher))
145 if result != nil {
146 _ = result.Clear()
147 }
148 if err != nil {
149 return false, fmt.Errorf("set WorkingDirectory: %w", err)
150 }
151 }
152 if fixIcon {
153 result, err := oleutil.PutProperty(shortcut, "IconLocation", launcher+",0")
154 if result != nil {
155 _ = result.Clear()
156 }
157 if err != nil {
158 return false, fmt.Errorf("set IconLocation: %w", err)
159 }
160 }
161 result, err := oleutil.CallMethod(shortcut, "Save")
162 if result != nil {
163 _ = result.Clear()
164 }
165 return err == nil, err
166 }
167
168 func reasonixWindowsShortcutTarget(target, launcher string) bool {
169 target = filepath.Clean(strings.TrimSpace(target))
170 launcher = filepath.Clean(strings.TrimSpace(launcher))
171 if target == "." || launcher == "." {
172 return false
173 }
174 root := filepath.Dir(launcher)
175 for _, owned := range []string{
176 launcher,
177 filepath.Join(root, "Reasonix.exe"),
178 filepath.Join(root, "reasonix-desktop.exe"),
179 } {
180 if strings.EqualFold(target, owned) {
181 return true
182 }
183 }
184 return reasonixWindowsVersionedTarget(target, launcher)
185 }
186
187 // reasonixWindowsVersionedTarget reports whether target points into this
188 // install's versions/<version>/reasonix-desktop.exe, a version-scoped path
189 // that the updater deletes when it switches or prunes versions. Such targets
190 // dangle after an update, so repair must repoint them at the stable launcher.
191 func reasonixWindowsVersionedTarget(target, launcher string) bool {
192 root := filepath.Dir(filepath.Clean(strings.TrimSpace(launcher)))
193 rel, err := filepath.Rel(root, filepath.Clean(strings.TrimSpace(target)))
194 if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
195 return false
196 }
197 parts := strings.Split(rel, string(filepath.Separator))
198 return len(parts) == 3 && strings.EqualFold(parts[0], "versions") &&
199 strings.EqualFold(parts[2], "reasonix-desktop.exe")
200 }
201
202 // repairWindowsShortcutPlan decides which owned-shortcut properties need
203 // rewriting. repointTarget is true when TargetPath points into a versioned
204 // directory the updater can delete; fixIcon is true when IconLocation points
205 // at the versioned desktop binary instead of the stable launcher.
206 func repairWindowsShortcutPlan(target, iconLocation, launcher string) (repointTarget, fixIcon bool) {
207 return reasonixWindowsVersionedTarget(target, launcher), reasonixWindowsStaleIcon(iconLocation, launcher)
208 }
209
210 func reasonixWindowsStaleIcon(iconLocation, launcher string) bool {
211 iconPath := strings.TrimSpace(iconLocation)
212 if comma := strings.LastIndex(iconPath, ","); comma >= 0 {
213 if _, err := strconv.Atoi(strings.TrimSpace(iconPath[comma+1:])); err == nil {
214 iconPath = iconPath[:comma]
215 }
216 }
217 iconPath = strings.Trim(strings.TrimSpace(iconPath), `"`)
218 if iconPath == "" {
219 return false
220 }
221 root := filepath.Dir(filepath.Clean(strings.TrimSpace(launcher)))
222 rel, err := filepath.Rel(root, filepath.Clean(iconPath))
223 if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
224 return false
225 }
226 parts := strings.Split(rel, string(filepath.Separator))
227 return len(parts) == 3 && strings.EqualFold(parts[0], "versions") &&
228 strings.EqualFold(parts[2], "reasonix-desktop.exe")
229 }
230
231 func notifyWindowsShortcutChanged(path string) {
232 pathPtr, err := windows.UTF16PtrFromString(path)
233 if err != nil {
234 return
235 }
236 // SHCNE_UPDATEITEM + SHCNF_PATHW asks Explorer to invalidate only this
237 // shortcut instead of flushing the entire association cache.
238 const (
239 shcneUpdateItem = 0x00002000
240 shcnfPathW = 0x0005
241 )
242 proc := windows.NewLazySystemDLL("shell32.dll").NewProc("SHChangeNotify")
243 _, _, _ = proc.Call(shcneUpdateItem, shcnfPathW, uintptr(unsafe.Pointer(pathPtr)), 0)
244 }
245
245 lines GO