返回 DeepSeek-Reasonix
status.go
1 // Package desktopinstance coordinates installed Desktop process ownership.
2 package desktopinstance
3
4 import (
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "path/filepath"
11 "strings"
12
13 "reasonix/internal/installlayout"
14 )
15
16 const StatusLimit = 16 * 1024
17 const QuitRequest = "--reasonix-lifecycle-request=quit"
18 const unsupportedPortableLocationMessage = "Reasonix portable cannot run from a network or virtual shared folder.\n" +
19 "Copy the entire extracted folder to a local Windows drive, then start Reasonix.exe, or use the installer.\n\n" +
20 "Reasonix 便携版无法从网络或虚拟机共享目录启动。\n" +
21 "请将整个解压目录复制到 Windows 本地磁盘后运行 Reasonix.exe,或使用安装器。"
22
23 type Code string
24
25 const (
26 UnknownOwner Code = "unknown_owner"
27 ConfirmationRequired Code = "confirmation_required"
28 Cancelled Code = "cancelled"
29 ExitTimeout Code = "exit_timeout"
30 StartupFailed Code = "startup_failed"
31 OtherInstallation Code = "other_installation"
32 UnsupportedPortableLocation Code = "unsupported_portable_location"
33 )
34
35 type Error struct {
36 Code Code
37 Detail string
38 }
39
40 func (e *Error) Error() string { return string(e.Code) + ": " + e.Detail }
41
42 // NewUnsupportedPortableLocationError reports a portable launch from storage
43 // whose execution semantics cannot support the desktop service lifecycle.
44 func NewUnsupportedPortableLocationError(locationType string) error {
45 return outcome(UnsupportedPortableLocation, "%s\nlocation_type=%s", unsupportedPortableLocationMessage, portableLocationType(locationType))
46 }
47
48 func portableLocationType(locationType string) string {
49 switch locationType {
50 case "unc", "remote_drive":
51 return locationType
52 default:
53 return "unknown"
54 }
55 }
56
57 // ExitCode preserves a machine-readable distinction for silent installers.
58 func ExitCode(err error) int {
59 if err == nil {
60 return 0
61 }
62 var failure *Error
63 if errors.As(err, &failure) {
64 switch failure.Code {
65 case Cancelled:
66 return 1602
67 case ConfirmationRequired, OtherInstallation, UnknownOwner, ExitTimeout:
68 return 1618
69 case StartupFailed:
70 return 1603
71 }
72 }
73 return 1
74 }
75
76 type Status struct {
77 SchemaVersion int `json:"schemaVersion"`
78 Product string `json:"product"`
79 PID uint32 `json:"pid"`
80 Version string `json:"version"`
81 Generation string `json:"generation"`
82 HomeKey string `json:"homeKey"`
83 Lifecycle string `json:"lifecycle"`
84 Service string `json:"service"`
85 ServicePID uint32 `json:"servicePID"`
86 Visible bool `json:"visible"`
87 RendererVersion string `json:"rendererVersion"`
88 Healthy bool `json:"healthy"`
89 }
90
91 func DecodeStatus(data []byte, pid uint32) (Status, error) {
92 var s Status
93 if len(data) > StatusLimit {
94 return s, errors.New("shell status exceeds limit")
95 }
96 if err := json.Unmarshal(data, &s); err != nil {
97 return s, err
98 }
99 if s.SchemaVersion != 1 || s.Product != "com.reasonix.desktop" || s.PID != pid || s.Generation == "" || len(s.HomeKey) != 64 || s.Version == "" {
100 return s, errors.New("unverified shell status identity")
101 }
102 if _, err := hex.DecodeString(s.HomeKey); err != nil {
103 return s, err
104 }
105 switch s.Lifecycle {
106 case "starting", "ready", "failed", "quitting", "done":
107 default:
108 return s, errors.New("unknown shell lifecycle")
109 }
110 switch s.Service {
111 case "starting", "restarting", "ready", "failed", "exited":
112 default:
113 return s, errors.New("unknown service state")
114 }
115 return s, nil
116 }
117
118 func (s Status) Ready(version string) bool {
119 return s.Lifecycle == "ready" && s.Service == "ready" && s.ServicePID != 0 && s.Visible && s.Healthy && s.RendererVersion == s.Version && (version == "" || s.Version == version)
120 }
121
122 func ProfileKey(profile string) string {
123 sum := sha256.Sum256([]byte(strings.ToLower(strings.ReplaceAll(profile, "/", `\`))))
124 return hex.EncodeToString(sum[:])
125 }
126
127 // ImageRole accepts only installed product paths, never a process-name match.
128 func ImageRole(root, image string) string {
129 rel, err := filepath.Rel(root, image)
130 if err != nil {
131 return ""
132 }
133 parts := strings.Split(strings.ToLower(filepath.ToSlash(rel)), "/")
134 if len(parts) >= 3 && parts[0] == "versions" {
135 if installlayout.ValidateVersionName(parts[1]) != nil {
136 return ""
137 }
138 parts = parts[2:]
139 }
140 switch strings.Join(parts, "/") {
141 case "app/reasonix.exe":
142 return "shell"
143 case "reasonix-desktop.exe":
144 return "service"
145 case "app/resources/service/reasonix-desktop.exe":
146 return "service"
147 }
148 return ""
149 }
150
151 func outcome(code Code, detail string, args ...any) error {
152 return &Error{code, fmt.Sprintf(detail, args...)}
153 }
154
154 lines GO