返回 DeepSeek-Reasonix
updater_install_linux.go
根目录 / desktop / updater_install_linux.go
1 //go:build linux
2
3 package main
4
5 import (
6 "bytes"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 )
12
13 // dirIsWritable reports whether the process can create a temporary file in dir.
14 func dirIsWritable(dir string) bool {
15 if dir == "" {
16 return false
17 }
18 info, err := os.Stat(dir)
19 if err != nil || !info.IsDir() {
20 return false
21 }
22 f, err := os.CreateTemp(dir, ".reasonix-write-test-*")
23 if err != nil {
24 return false
25 }
26 name := f.Name()
27 _ = f.Close()
28 _ = os.Remove(name)
29 return true
30 }
31
32 // resolveExecutablePath returns the real path of the running binary.
33 func resolveExecutablePath() string {
34 exe, err := os.Executable()
35 if err != nil {
36 return ""
37 }
38 if resolved, err := filepath.EvalSymlinks(exe); err == nil {
39 exe = resolved
40 }
41 return exe
42 }
43
44 // detectLinuxInstallProfile classifies the running Linux install.
45 //
46 // deb — dpkg owns the absolute executable path as reasonix-desktop, and the
47 // Polkit helper + pkexec are present for authorized upgrades.
48 // portable — not dpkg-managed, install directory is writable (tarball flow).
49 // manual — system directory not writable, package ownership unclear, or
50 // authorization components missing.
51 func detectLinuxInstallProfile() installProfile {
52 exe := resolveExecutablePath()
53 if exe == "" {
54 return installProfile{
55 Mode: installModeManual,
56 CanSelfUpdate: false,
57 ManualReason: "cannot resolve the running executable path",
58 }
59 }
60
61 if isDpkgOwnedReasonix(exe) {
62 if linuxDebHelperReady() {
63 return installProfile{
64 Mode: installModeDeb,
65 CanSelfUpdate: true,
66 RequiresElev: true,
67 ArtifactKind: artifactKindDeb,
68 }
69 }
70 return installProfile{
71 Mode: installModeManual,
72 CanSelfUpdate: false,
73 ManualReason: manualDebInstallHint() + " (Polkit helper or pkexec is missing)",
74 }
75 }
76
77 dir := filepath.Dir(exe)
78 if dirIsWritable(dir) {
79 return installProfile{
80 Mode: installModePortable,
81 CanSelfUpdate: true,
82 ArtifactKind: artifactKindTarball,
83 }
84 }
85
86 return installProfile{
87 Mode: installModeManual,
88 CanSelfUpdate: false,
89 ManualReason: "this install is not writable and is not managed by the reasonix-desktop package; download the package from the download page",
90 }
91 }
92
93 // isDpkgOwnedReasonix reports whether absolute path belongs to the reasonix-desktop
94 // package. Uses absolute dpkg-query and requires both package name and path match.
95 func isDpkgOwnedReasonix(absPath string) bool {
96 if absPath == "" || !filepath.IsAbs(absPath) {
97 return false
98 }
99 if _, err := os.Stat("/usr/bin/dpkg-query"); err != nil {
100 return false
101 }
102 cmd := exec.Command("/usr/bin/dpkg-query", "-S", absPath)
103 var stdout, stderr bytes.Buffer
104 cmd.Stdout = &stdout
105 cmd.Stderr = &stderr
106 if err := cmd.Run(); err != nil {
107 return false
108 }
109 // Output shape: "reasonix-desktop: /usr/bin/reasonix-desktop"
110 line := strings.TrimSpace(stdout.String())
111 if line == "" {
112 return false
113 }
114 // dpkg-query may return multiple lines for diversions; require an exact package hit.
115 for _, raw := range strings.Split(line, "\n") {
116 raw = strings.TrimSpace(raw)
117 pkg, path, ok := strings.Cut(raw, ":")
118 if !ok {
119 continue
120 }
121 pkg = strings.TrimSpace(pkg)
122 path = strings.TrimSpace(path)
123 if pkg != linuxDebPackageName {
124 continue
125 }
126 if path == absPath {
127 return true
128 }
129 // Some dpkg versions report the path relative or with //; compare cleaned.
130 if filepath.Clean(path) == filepath.Clean(absPath) {
131 return true
132 }
133 }
134 return false
135 }
136
136 lines GO