返回 DeepSeek-Reasonix
server.go
根目录 / desktop / internal / hostrpc / server.go
1 package hostrpc
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "log/slog"
8 "maps"
9 "os"
10 "sync"
11 "sync/atomic"
12
13 "reasonix/desktop/internal/instanceidentity"
14 "reasonix/internal/extension/rpcwire"
15 "reasonix/internal/pathidentity"
16 )
17
18 // Hooks are the lifecycle owners behind the desktop/* requests. A nil hook
19 // answers its request with success and no effect.
20 type Hooks struct {
21 Hello func(HelloParams) (HelloResult, error)
22 Start func(ctx context.Context) error
23 DOMReady func(ctx context.Context) error
24 RendererAttached func(ctx context.Context, generation int) error
25 BeforeClose func(ctx context.Context, reason string) (prevent bool)
26 Shutdown func(ctx context.Context, params ShutdownParams) (ShutdownResult, error)
27 ShutdownStatus func(ctx context.Context, params ShutdownStatusParams) (ShutdownResult, error)
28 HostEvent func(ctx context.Context, name string, payload json.RawMessage) error
29 BrowserControl func(ctx context.Context, enabled bool) error
30 }
31
32 // ServerConfig assembles one service process's identity around its registry.
33 type ServerConfig struct {
34 Registry *Registry
35 Contract Contract
36 Hooks Hooks
37 Identity Identity
38 Generation string
39 }
40
41 // Event is the params object of a desktop/event notification.
42 type Event struct {
43 Seq int64 `json:"seq"`
44 Generation string `json:"generation"`
45 Name string `json:"name"`
46 Args []any `json:"args"`
47 }
48
49 // Server answers the shell over one rpcwire connection.
50 type Server struct {
51 conn *rpcwire.Conn
52 cfg ServerConfig
53 digest string
54
55 ready atomic.Bool
56 helloMu sync.Mutex
57
58 emitMu sync.Mutex
59 seq int64
60
61 done chan struct{}
62 doneOnce sync.Once
63 }
64
65 // NewServer registers the desktop/* handlers on conn. Call Serve afterwards;
66 // conn must not be served by anyone else.
67 func NewServer(conn *rpcwire.Conn, cfg ServerConfig) *Server {
68 s := &Server{conn: conn, cfg: cfg, digest: cfg.Contract.Digest(), done: make(chan struct{})}
69 conn.Handle("desktop/hello", s.hello)
70 conn.Handle("desktop/start", s.gated(s.start))
71 conn.Handle("desktop/domReady", s.gated(s.domReady))
72 conn.Handle("desktop/rendererAttached", s.gated(s.rendererAttached))
73 conn.Handle("desktop/beforeClose", s.gated(s.beforeClose))
74 conn.Handle("desktop/shutdown", s.gated(s.shutdown))
75 conn.Handle("desktop/shutdownStatus", s.gated(s.shutdownStatus))
76 conn.Handle("desktop/hostEvent", s.gated(s.hostEvent))
77 conn.Handle("desktop/browserControl", s.gated(s.browserControl))
78 conn.Handle("desktop/invoke", s.gated(s.invoke))
79 return s
80 }
81
82 // Serve pumps the connection until the shell closes its end, the context
83 // ends, or desktop/shutdown has been acknowledged. A clean end returns nil.
84 func (s *Server) Serve(ctx context.Context) error {
85 errc := make(chan error, 1)
86 go func() { errc <- s.conn.Serve(ctx) }()
87 select {
88 case err := <-errc:
89 return err
90 case <-s.done:
91 return nil
92 case <-ctx.Done():
93 return ctx.Err()
94 }
95 }
96
97 // Emit writes one desktop/event notification. Sequence numbers are assigned
98 // and written under one lock, so the wire order equals the call order.
99 func (s *Server) Emit(name string, args ...any) {
100 if args == nil {
101 args = []any{}
102 }
103 s.emitMu.Lock()
104 defer s.emitMu.Unlock()
105 s.seq++
106 err := s.conn.Notify("desktop/event", Event{Seq: s.seq, Generation: s.cfg.Generation, Name: name, Args: args})
107 if err != nil {
108 slog.Warn("desktop host: event not delivered", "name", name, "seq", s.seq, "err", err)
109 }
110 }
111
112 // Request issues a host/* reverse request and decodes the result into result
113 // when it is non-nil.
114 func (s *Server) Request(ctx context.Context, method string, params any, result any) error {
115 raw, err := s.conn.Request(ctx, method, params)
116 if err != nil {
117 return err
118 }
119 if result == nil || len(raw) == 0 {
120 return nil
121 }
122 return json.Unmarshal(raw, result)
123 }
124
125 func (s *Server) gated(h rpcwire.RequestHandler) rpcwire.RequestHandler {
126 return func(ctx context.Context, params json.RawMessage) (any, error) {
127 if !s.ready.Load() {
128 return nil, notReady()
129 }
130 return h(ctx, params)
131 }
132 }
133
134 func (s *Server) hello(_ context.Context, raw json.RawMessage) (any, error) {
135 var p HelloParams
136 if err := decodeParams(raw, &p); err != nil {
137 return nil, err
138 }
139 s.helloMu.Lock()
140 defer s.helloMu.Unlock()
141 if s.ready.Load() {
142 return nil, &rpcwire.RPCError{Code: rpcwire.ErrInvalidRequest, Message: "desktop/hello already completed"}
143 }
144 if err := validateHello(p, s.digest, s.cfg.Identity); err != nil {
145 return nil, err
146 }
147 var result HelloResult
148 if s.cfg.Hooks.Hello != nil {
149 var err error
150 if result, err = s.cfg.Hooks.Hello(p); err != nil {
151 return nil, &rpcwire.RPCError{Code: rpcwire.ErrInternal, Message: "hello: " + err.Error()}
152 }
153 }
154 result.ProtocolVersion = ProtocolVersion
155 result.ContractDigest = s.digest
156 result.Service = ServiceInfo{
157 BuildInfo: BuildInfo{Version: s.cfg.Identity.Version, Channel: s.cfg.Identity.Channel, Commit: s.cfg.Identity.Commit},
158 PID: os.Getpid(),
159 }
160 result.RuntimeGeneration = s.cfg.Generation
161 result.Instance = &InstanceInfo{
162 IdentityVersion: pathidentity.Version,
163 IdentityDigest: instanceidentity.Digest(s.cfg.Identity.Home),
164 LegacyID: instanceidentity.ForHome(s.cfg.Identity.Home),
165 }
166 s.ready.Store(true)
167 return result, nil
168 }
169
170 func (s *Server) start(ctx context.Context, _ json.RawMessage) (any, error) {
171 return empty(runHook(ctx, s.cfg.Hooks.Start))
172 }
173
174 func (s *Server) domReady(ctx context.Context, _ json.RawMessage) (any, error) {
175 return empty(runHook(ctx, s.cfg.Hooks.DOMReady))
176 }
177
178 func (s *Server) rendererAttached(ctx context.Context, raw json.RawMessage) (any, error) {
179 var p struct {
180 RendererGeneration int `json:"rendererGeneration"`
181 }
182 if err := decodeParams(raw, &p); err != nil {
183 return nil, err
184 }
185 if s.cfg.Hooks.RendererAttached == nil {
186 return empty(nil)
187 }
188 return empty(s.cfg.Hooks.RendererAttached(ctx, p.RendererGeneration))
189 }
190
191 func (s *Server) beforeClose(ctx context.Context, raw json.RawMessage) (any, error) {
192 var p struct {
193 Reason string `json:"reason"`
194 }
195 if err := decodeParams(raw, &p); err != nil {
196 return nil, err
197 }
198 prevent := false
199 if s.cfg.Hooks.BeforeClose != nil {
200 prevent = s.cfg.Hooks.BeforeClose(ctx, p.Reason)
201 }
202 return map[string]bool{"prevent": prevent}, nil
203 }
204
205 func (s *Server) shutdown(ctx context.Context, raw json.RawMessage) (any, error) {
206 var params ShutdownParams
207 if err := decodeParams(raw, &params); err != nil {
208 return nil, err
209 }
210 if s.cfg.Hooks.Shutdown == nil {
211 return ShutdownResult{RequestID: params.RequestID, Reason: params.Reason, Phase: "completed", Outcome: "success", Completed: true}, nil
212 }
213 result, err := s.cfg.Hooks.Shutdown(ctx, params)
214 if err != nil {
215 // Shutdown failures are returned as typed results so the shell can retain
216 // the window and offer a retry without parsing an RPC error string.
217 return result, nil
218 }
219 if !result.Completed {
220 return result, nil
221 }
222 return rpcwire.RespondThen(result, func(error) {
223 s.doneOnce.Do(func() { close(s.done) })
224 }), nil
225 }
226
227 func (s *Server) shutdownStatus(ctx context.Context, raw json.RawMessage) (any, error) {
228 var params ShutdownStatusParams
229 if err := decodeParams(raw, &params); err != nil {
230 return nil, err
231 }
232 if s.cfg.Hooks.ShutdownStatus == nil {
233 return ShutdownResult{RequestID: params.RequestID, Phase: "idle", Outcome: "not_started", Retryable: true}, nil
234 }
235 result, err := s.cfg.Hooks.ShutdownStatus(ctx, params)
236 if err != nil {
237 return nil, internalError(err)
238 }
239 return result, nil
240 }
241
242 func (s *Server) hostEvent(ctx context.Context, raw json.RawMessage) (any, error) {
243 var p struct {
244 Name string `json:"name"`
245 Payload json.RawMessage `json:"payload"`
246 }
247 if err := decodeParams(raw, &p); err != nil {
248 return nil, err
249 }
250 if s.cfg.Hooks.HostEvent == nil {
251 return empty(nil)
252 }
253 return empty(s.cfg.Hooks.HostEvent(ctx, p.Name, p.Payload))
254 }
255
256 // browserControl carries the shell's capability switch for the built-in
257 // browser; the shell owns the persisted value and pushes it on every change.
258 func (s *Server) browserControl(ctx context.Context, raw json.RawMessage) (any, error) {
259 var p struct {
260 Enabled *bool `json:"enabled"`
261 }
262 if err := decodeParams(raw, &p); err != nil {
263 return nil, err
264 }
265 if p.Enabled == nil {
266 return nil, &rpcwire.RPCError{Code: rpcwire.ErrInvalidParams, Message: "enabled is required"}
267 }
268 if s.cfg.Hooks.BrowserControl == nil {
269 return empty(nil)
270 }
271 return empty(s.cfg.Hooks.BrowserControl(ctx, *p.Enabled))
272 }
273
274 func (s *Server) invoke(ctx context.Context, raw json.RawMessage) (any, error) {
275 var p struct {
276 Method string `json:"method"`
277 Args []json.RawMessage `json:"args"`
278 }
279 if err := decodeParams(raw, &p); err != nil {
280 return nil, err
281 }
282 result, err := s.cfg.Registry.Invoke(ctx, p.Method, p.Args)
283 if err == nil {
284 return result, nil
285 }
286 data := map[string]any{"method": p.Method}
287 var detailed interface{ RPCErrorData() map[string]any }
288 if errors.As(err, &detailed) {
289 maps.Copy(data, detailed.RPCErrorData())
290 }
291 var unknown *UnknownMethodError
292 var invalid *InvalidArgsError
293 var panicked *PanicError
294 switch {
295 case errors.As(err, &unknown):
296 return nil, &rpcwire.RPCError{Code: rpcwire.ErrMethodNotFound, Message: err.Error(), Data: data}
297 case errors.As(err, &invalid):
298 return nil, &rpcwire.RPCError{Code: rpcwire.ErrInvalidParams, Message: err.Error(), Data: data}
299 case errors.As(err, &panicked):
300 slog.Error("desktop host: bound method panicked", "method", p.Method, "panic", panicked.Value, "stack", string(panicked.Stack))
301 return nil, &rpcwire.RPCError{Code: rpcwire.ErrInternal, Message: err.Error(), Data: data}
302 }
303 return nil, &rpcwire.RPCError{Code: CodeBusiness, Message: err.Error(), Data: data}
304 }
305
306 func decodeParams(raw json.RawMessage, into any) error {
307 if len(raw) == 0 {
308 return nil
309 }
310 if err := json.Unmarshal(raw, into); err != nil {
311 return &rpcwire.RPCError{Code: rpcwire.ErrInvalidParams, Message: "invalid params: " + err.Error()}
312 }
313 return nil
314 }
315
316 func runHook(ctx context.Context, hook func(context.Context) error) error {
317 if hook == nil {
318 return nil
319 }
320 return hook(ctx)
321 }
322
323 func empty(err error) (any, error) {
324 if err != nil {
325 return nil, internalError(err)
326 }
327 return struct{}{}, nil
328 }
329
330 func internalError(err error) error {
331 var rpcErr *rpcwire.RPCError
332 if errors.As(err, &rpcErr) {
333 return rpcErr
334 }
335 return &rpcwire.RPCError{Code: rpcwire.ErrInternal, Message: err.Error()}
336 }
337
337 lines GO