返回 DeepSeek-Reasonix
updater_deb_linux.go
根目录 / desktop / updater_deb_linux.go
1 //go:build linux
2
3 package main
4
5 import (
6 "bufio"
7 "bytes"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "os"
13 "os/exec"
14 "strings"
15 )
16
17 // Linux privileged-install error classes. errorClass maps these substrings into
18 // anonymous metrics buckets; authorization_cancelled is not recorded as a failure.
19 var (
20 errUpdateAuthCancelled = errors.New("update: authorization cancelled")
21 errUpdateAuthFailed = errors.New("update: authorization failed")
22 errUpdatePkgBusy = errors.New("update: package manager busy")
23 errUpdatePkgInstall = errors.New("update: package install failed")
24 errUpdatePkgVerify = errors.New("update: package verify failed")
25 errUpdateCacheMismatch = errors.New("update: cached artifact does not match current install mode")
26 )
27
28 // helperPhasePrefix must match desktop/cmd/update-helper phasePrefix.
29 const helperPhasePrefix = "REASONIX_UPDATE_PHASE="
30
31 // applyDebLinux asks Polkit (via pkexec) to run the root-owned helper against the
32 // cached .deb + signature. The helper re-verifies the signature and runs apt.
33 // onPhase is invoked when the helper emits a progress phase on stderr (typically
34 // "installing" once validation finishes and before apt-get starts).
35 func applyDebLinux(packagePath, signaturePath string, onPhase func(phase string)) error {
36 if packagePath == "" || signaturePath == "" {
37 return fmt.Errorf("update: deb install requires package and signature paths")
38 }
39 if _, err := os.Stat(linuxUpdateHelperPath); err != nil {
40 return fmt.Errorf("%w: helper missing", errUpdateAuthFailed)
41 }
42 if _, err := os.Stat(linuxPkexecPath); err != nil {
43 return fmt.Errorf("%w: pkexec missing", errUpdateAuthFailed)
44 }
45
46 cmd := exec.Command(
47 linuxPkexecPath,
48 linuxUpdateHelperPath,
49 "install",
50 "--package", packagePath,
51 "--signature", signaturePath,
52 )
53 var stdout bytes.Buffer
54 var stderrBuf bytes.Buffer
55 stderrR, stderrW := io.Pipe()
56 cmd.Stdout = &stdout
57 cmd.Stderr = io.MultiWriter(stderrW, &stderrBuf)
58
59 if err := cmd.Start(); err != nil {
60 _ = stderrW.Close()
61 return fmt.Errorf("%w: %v", errUpdateAuthFailed, err)
62 }
63
64 // Drain stderr concurrently so phase lines arrive while apt still runs.
65 scanDone := make(chan struct{})
66 go func() {
67 defer close(scanDone)
68 sc := bufio.NewScanner(stderrR)
69 // Phase lines are short; allow slightly larger lines for apt noise.
70 sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
71 for sc.Scan() {
72 line := sc.Text()
73 if phase, ok := parseHelperPhaseLine(line); ok && onPhase != nil {
74 onPhase(phase)
75 }
76 }
77 }()
78
79 err := cmd.Wait()
80 _ = stderrW.Close()
81 <-scanDone
82
83 if err == nil {
84 // Helper prints a structured result; treat missing ok as success only when
85 // exit 0 (helper always emits JSON on success).
86 var result struct {
87 OK bool `json:"ok"`
88 }
89 if json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &result) == nil && !result.OK {
90 return errUpdatePkgInstall
91 }
92 return nil
93 }
94
95 var exitErr *exec.ExitError
96 if errors.As(err, &exitErr) {
97 code := exitErr.ExitCode()
98 // pkexec: 126 = auth dialog dismissed / not authorized by user
99 // 127 = pkexec not found / cannot run
100 if code == 126 {
101 return errUpdateAuthCancelled
102 }
103 if code == 127 {
104 return fmt.Errorf("%w: cannot authorize package install", errUpdateAuthFailed)
105 }
106 // Helper exit codes from main_linux.go
107 switch code {
108 case 10, 11, 12, 13:
109 // not_root / bad_input / verify / package rejected
110 return fmt.Errorf("%w: %s", errUpdatePkgVerify, helperErrorMessage(stdout.Bytes(), stderrBuf.Bytes()))
111 case 14:
112 return errUpdatePkgBusy
113 case 15:
114 return fmt.Errorf("%w: %s", errUpdatePkgInstall, helperErrorMessage(stdout.Bytes(), stderrBuf.Bytes()))
115 case 16:
116 return errUpdatePkgVerify
117 }
118 // Prefer structured helper code when present.
119 if msg, class := parseHelperFailure(stdout.Bytes()); msg != "" {
120 switch class {
121 case "package_manager_busy":
122 return errUpdatePkgBusy
123 case "package_verify_failed", "verify_failed", "package_rejected", "bad_input":
124 return fmt.Errorf("%w: %s", errUpdatePkgVerify, msg)
125 case "install_failed":
126 return fmt.Errorf("%w: %s", errUpdatePkgInstall, msg)
127 }
128 }
129 }
130 return fmt.Errorf("%w: %s", errUpdatePkgInstall, helperErrorMessage(stdout.Bytes(), stderrBuf.Bytes()))
131 }
132
133 func parseHelperPhaseLine(line string) (phase string, ok bool) {
134 line = strings.TrimSpace(line)
135 if !strings.HasPrefix(line, helperPhasePrefix) {
136 return "", false
137 }
138 phase = strings.TrimSpace(strings.TrimPrefix(line, helperPhasePrefix))
139 return phase, phase != ""
140 }
141
142 func helperErrorMessage(stdout, stderr []byte) string {
143 if msg, _ := parseHelperFailure(stdout); msg != "" {
144 return msg
145 }
146 // Strip protocol lines from stderr diagnostics.
147 var kept []string
148 for _, line := range strings.Split(string(stderr), "\n") {
149 if _, ok := parseHelperPhaseLine(line); ok {
150 continue
151 }
152 line = strings.TrimSpace(line)
153 if line != "" {
154 kept = append(kept, line)
155 }
156 }
157 s := strings.TrimSpace(strings.Join(kept, " "))
158 if s == "" {
159 s = strings.TrimSpace(string(stdout))
160 }
161 if s == "" {
162 return "install failed"
163 }
164 // Never surface absolute paths in UI-facing errors.
165 fields := strings.Fields(s)
166 for i, f := range fields {
167 if strings.HasPrefix(f, "/") {
168 fields[i] = "<path>"
169 }
170 }
171 out := strings.Join(fields, " ")
172 if len(out) > 240 {
173 out = out[:240]
174 }
175 return out
176 }
177
178 func parseHelperFailure(stdout []byte) (msg, code string) {
179 var result struct {
180 OK bool `json:"ok"`
181 Error string `json:"error"`
182 Code string `json:"code"`
183 }
184 if err := json.Unmarshal(bytes.TrimSpace(stdout), &result); err != nil {
185 return "", ""
186 }
187 if result.OK {
188 return "", ""
189 }
190 return strings.TrimSpace(result.Error), strings.TrimSpace(result.Code)
191 }
192
193 func isAuthCancelled(err error) bool {
194 return errors.Is(err, errUpdateAuthCancelled)
195 }
196
197 // ensureDebCacheMatchesProfile re-detects install mode at install time so a
198 // download made as portable cannot be applied after the install path changes.
199 func ensureDebCacheMatchesProfile(meta *cachedUpdate, profile installProfile) error {
200 kind := artifactKindFromMeta(meta.ArtifactKind)
201 switch profile.Mode {
202 case installModeDeb:
203 if kind != artifactKindDeb {
204 return errUpdateCacheMismatch
205 }
206 if meta.SignaturePath == "" {
207 return errUpdateCacheMismatch
208 }
209 case installModePortable:
210 if kind != artifactKindTarball {
211 return errUpdateCacheMismatch
212 }
213 default:
214 return fmt.Errorf("update: install mode %s cannot self-update", profile.Mode)
215 }
216 return nil
217 }
218
218 lines GO