返回 DeepSeek-Reasonix
launcher.go
根目录 / internal / desktoplauncher / launcher.go
1 // Package desktoplauncher implements the permanent Reasonix desktop entry
2 // point. It deliberately owns no crash-loop, rollback, or safe-mode policy.
3 package desktoplauncher
4
5 import (
6 "fmt"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "runtime"
11 "strings"
12
13 "reasonix/internal/installlayout"
14 )
15
16 // Run resolves the active desktop, performs the one-time legacy handoff when
17 // needed, and starts the desktop process.
18 func Run(args []string, buildVersion string) int {
19 if len(args) == 1 {
20 switch args[0] {
21 case "version", "--version", "-v":
22 fmt.Println("reasonix-launcher", buildVersion)
23 return 0
24 case "help", "--help", "-h":
25 usage()
26 return 0
27 }
28 }
29
30 installRoot, err := ResolveInstallRoot()
31 if err != nil {
32 fmt.Fprintln(os.Stderr, "error:", err)
33 return 1
34 }
35 if err := runLegacyMigratorIfNeeded(installRoot); err != nil {
36 fmt.Fprintln(os.Stderr, "error:", err)
37 return 1
38 }
39
40 desktopPath, err := ResolveDesktopPath(installRoot)
41 if err != nil {
42 fmt.Fprintln(os.Stderr, "error:", err)
43 return 1
44 }
45
46 cmd := exec.Command(desktopPath, StripLegacyLaunchArgs(args)...)
47 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
48 cmd.Dir = installRoot
49 if DetachByDefault() {
50 if err := cmd.Start(); err != nil {
51 fmt.Fprintln(os.Stderr, "error:", err)
52 return 1
53 }
54 return 0
55 }
56 if err := cmd.Run(); err != nil {
57 if exit, ok := err.(*exec.ExitError); ok {
58 return exit.ExitCode()
59 }
60 fmt.Fprintln(os.Stderr, "error:", err)
61 return 1
62 }
63 return 0
64 }
65
66 // ResolveInstallRoot returns the directory containing the launcher.
67 func ResolveInstallRoot() (string, error) {
68 exe, err := os.Executable()
69 if err != nil {
70 return "", fmt.Errorf("resolve launcher path: %w", err)
71 }
72 return resolveInstallRoot(exe)
73 }
74
75 func resolveInstallRoot(exe string) (string, error) {
76 resolved, err := resolveExecutablePath(exe)
77 if err != nil {
78 return "", fmt.Errorf("resolve launcher path: %w", err)
79 }
80 return filepath.Clean(filepath.Dir(resolved)), nil
81 }
82
83 // ResolveDesktopPath resolves current.json when present. A present but invalid
84 // pointer is always fatal; only a genuinely absent pointer may use the flat
85 // sibling fallback needed by package-managed installs.
86 func ResolveDesktopPath(installRoot string) (string, error) {
87 currentPath := filepath.Join(installRoot, installlayout.CurrentFileName)
88 if _, err := os.Lstat(currentPath); err == nil {
89 return installlayout.ActiveDesktopPath(installRoot)
90 } else if !os.IsNotExist(err) {
91 return "", fmt.Errorf("inspect current.json: %w", err)
92 }
93 if path := siblingDesktop(installRoot); path != "" {
94 return path, nil
95 }
96 return "", fmt.Errorf("cannot locate reasonix-desktop under %s (missing current.json)", installRoot)
97 }
98
99 func runLegacyMigratorIfNeeded(installRoot string) error {
100 currentPath := filepath.Join(installRoot, installlayout.CurrentFileName)
101 if _, err := os.Lstat(currentPath); err == nil {
102 return nil
103 } else if !os.IsNotExist(err) {
104 return fmt.Errorf("inspect current.json before migration: %w", err)
105 }
106
107 migratorName := "reasonix-guard"
108 if runtime.GOOS == "windows" {
109 migratorName += ".exe"
110 }
111 migratorPath := filepath.Join(installRoot, migratorName)
112 info, err := os.Lstat(migratorPath)
113 if os.IsNotExist(err) {
114 return nil
115 }
116 if err != nil {
117 return fmt.Errorf("inspect legacy migrator: %w", err)
118 }
119 if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
120 return fmt.Errorf("legacy migrator is not a regular file")
121 }
122
123 cmd := exec.Command(migratorPath, "--install-root", installRoot, "--no-relaunch")
124 cmd.Dir = installRoot
125 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
126 if err := cmd.Run(); err != nil {
127 return fmt.Errorf("legacy layout migration failed: %w", err)
128 }
129 if _, err := installlayout.ReadCurrent(installRoot); err != nil {
130 return fmt.Errorf("legacy layout migration did not commit current.json: %w", err)
131 }
132 // The parent launcher owns Windows cleanup after the migrator process exits.
133 // Unix migrators normally unlink themselves, so NotExist is also success.
134 if err := os.Remove(migratorPath); err != nil && !os.IsNotExist(err) {
135 return fmt.Errorf("remove completed legacy migrator: %w", err)
136 }
137 return nil
138 }
139
140 func siblingDesktop(installRoot string) string {
141 path := filepath.Join(installRoot, installlayout.DesktopBinaryName())
142 info, err := os.Lstat(path)
143 if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
144 return ""
145 }
146 return path
147 }
148
149 // StripLegacyLaunchArgs removes tokens emitted by old Guard shortcuts.
150 func StripLegacyLaunchArgs(args []string) []string {
151 out := make([]string, 0, len(args))
152 skipNext := false
153 for i := 0; i < len(args); i++ {
154 if skipNext {
155 skipNext = false
156 continue
157 }
158 arg := args[i]
159 switch arg {
160 case "launch", "--detach", "--safe-mode", "-safe-mode":
161 continue
162 case "--app":
163 skipNext = true
164 continue
165 case "--":
166 return append(out, args[i:]...)
167 }
168 if strings.HasPrefix(arg, "--app=") {
169 continue
170 }
171 out = append(out, arg)
172 }
173 return out
174 }
175
176 // DetachByDefault reports whether the Windows packaged entry should start the
177 // desktop and exit immediately.
178 func DetachByDefault() bool {
179 if runtime.GOOS != "windows" {
180 return false
181 }
182 exe, err := os.Executable()
183 if err != nil {
184 return false
185 }
186 name := strings.ToLower(filepath.Base(exe))
187 return name == "reasonix-launcher.exe" || name == "reasonix.exe"
188 }
189
190 func usage() {
191 fmt.Println("usage: reasonix-launcher [args...]")
192 fmt.Println(" Starts the active Reasonix desktop from current.json.")
193 fmt.Println(" Legacy --safe-mode / launch --detach tokens are ignored.")
194 }
195
195 lines GO