返回 DeepSeek-Reasonix
registry.go
根目录 / desktop / internal / hostrpc / registry.go
1 package hostrpc
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "reflect"
10 "runtime/debug"
11 "slices"
12 )
13
14 // Command is one exported method of the bound target as the shell calls it.
15 type Command struct {
16 CommandOwnership
17 Name string `json:"name"`
18 Params []TypeRef `json:"params"`
19 Result *TypeRef `json:"result,omitempty"`
20 ReturnsError bool `json:"returnsError,omitempty"`
21 Cancellation string `json:"cancellation"`
22 }
23
24 // Skip names exported methods the registry leaves out of the contract.
25 type Skip map[string]bool
26
27 // UnknownMethodError reports an invoke of a name the registry never accepted.
28 type UnknownMethodError struct{ Method string }
29
30 func (e *UnknownMethodError) Error() string { return "unknown method: " + e.Method }
31
32 // InvalidArgsError reports arguments the method cannot take.
33 type InvalidArgsError struct {
34 Method string
35 Reason string
36 }
37
38 func (e *InvalidArgsError) Error() string { return e.Method + ": " + e.Reason }
39
40 // PanicError carries a panic raised inside a bound method so one faulty call
41 // cannot take the whole service down.
42 type PanicError struct {
43 Method string
44 Value any
45 Stack []byte
46 }
47
48 func (e *PanicError) Error() string { return fmt.Sprintf("%s: panic: %v", e.Method, e.Value) }
49
50 type boundMethod struct {
51 fn reflect.Value
52 params []reflect.Type
53 result bool
54 errAt int
55 context bool
56 }
57
58 // Registry is the set of methods the shell may invoke on one target value.
59 type Registry struct {
60 target reflect.Value
61 commands []Command
62 methods map[string]boundMethod
63 types map[string]ObjectType
64 }
65
66 // NewRegistry reflects over the exported methods of target, which must be a
67 // pointer to a struct. A typed nil pointer yields a registry that can
68 // describe the contract but not invoke. Any method whose signature the shell
69 // could not call fails the whole registry so the surface never drifts
70 // silently; skip excludes methods by name before that check.
71 func NewRegistry(target any, skip Skip) (*Registry, error) {
72 return newRegistry(target, skip, nil)
73 }
74
75 // NewRegistryWithOwners requires source-derived ownership for every bound
76 // command. Metadata describes the existing owner; it does not grant access.
77 func NewRegistryWithOwners(target any, skip Skip, owners map[string]CommandOwnership) (*Registry, error) {
78 if owners == nil {
79 return nil, errors.New("hostrpc: ownership metadata is required")
80 }
81 return newRegistry(target, skip, owners)
82 }
83
84 func newRegistry(target any, skip Skip, owners map[string]CommandOwnership) (*Registry, error) {
85 if target == nil {
86 return nil, errors.New("hostrpc: nil target")
87 }
88 rt := reflect.TypeOf(target)
89 if rt.Kind() != reflect.Pointer || rt.Elem().Kind() != reflect.Struct {
90 return nil, fmt.Errorf("hostrpc: target must be a pointer to a struct, got %s", rt)
91 }
92 collector := newTypeCollector()
93 r := &Registry{
94 target: reflect.ValueOf(target),
95 commands: []Command{},
96 methods: map[string]boundMethod{},
97 }
98 owner := rt.Elem().Name()
99 for i := range rt.NumMethod() {
100 m := rt.Method(i)
101 if skip[m.Name] {
102 continue
103 }
104 cmd, bound, err := describeMethod(collector, owner, m)
105 if err != nil {
106 return nil, fmt.Errorf("hostrpc: %s.%s: %w", owner, m.Name, err)
107 }
108 if owners != nil {
109 metadata, ok := owners[m.Name]
110 if !ok || metadata.Owner != owner+"."+m.Name || metadata.Domain == "" || len(metadata.Sources) == 0 || metadata.Scope.Resolver != metadata.Owner || len(metadata.Scope.Inputs) != len(cmd.Params) {
111 return nil, fmt.Errorf("hostrpc: %s.%s: missing or invalid ownership metadata", owner, m.Name)
112 }
113 kind := "owner-inputs"
114 if len(cmd.Params) == 0 {
115 kind = "owner-state"
116 }
117 if metadata.Scope.Kind != kind {
118 return nil, fmt.Errorf("hostrpc: %s.%s: invalid scope kind", owner, m.Name)
119 }
120 cmd.CommandOwnership = metadata
121 }
122 r.commands = append(r.commands, cmd)
123 r.methods[m.Name] = bound
124 }
125 r.types = collector.types
126 return r, nil
127 }
128
129 func describeMethod(c *typeCollector, owner string, m reflect.Method) (Command, boundMethod, error) {
130 ft := m.Type
131 if ft.IsVariadic() {
132 return Command{}, boundMethod{}, errors.New("variadic parameters are not supported")
133 }
134 cmd := Command{Name: m.Name, Params: []TypeRef{}, Cancellation: "before-dispatch"}
135 bound := boundMethod{fn: m.Func, errAt: -1}
136 first := 1
137 if ft.NumIn() > 1 && ft.In(1) == reflect.TypeFor[context.Context]() {
138 bound.context, first, cmd.Cancellation = true, 2, "cooperative-context"
139 }
140 for i := first; i < ft.NumIn(); i++ {
141 pt := ft.In(i)
142 if pt.Kind() == reflect.Interface && pt.NumMethod() > 0 {
143 return Command{}, boundMethod{}, fmt.Errorf("parameter %d: %s cannot be decoded from JSON", i-first, pt)
144 }
145 ref, err := c.ref(pt, fmt.Sprintf("%s.%s.arg%d", owner, m.Name, i-first))
146 if err != nil {
147 return Command{}, boundMethod{}, fmt.Errorf("parameter %d: %w", i-first, err)
148 }
149 cmd.Params = append(cmd.Params, ref)
150 bound.params = append(bound.params, pt)
151 }
152 switch ft.NumOut() {
153 case 0:
154 case 1:
155 if ft.Out(0) == errorType {
156 cmd.ReturnsError, bound.errAt = true, 0
157 break
158 }
159 if err := describeResult(c, owner, m.Name, ft.Out(0), &cmd, &bound); err != nil {
160 return Command{}, boundMethod{}, err
161 }
162 case 2:
163 if ft.Out(1) != errorType || ft.Out(0) == errorType {
164 return Command{}, boundMethod{}, fmt.Errorf("results must be (T, error), got (%s, %s)", ft.Out(0), ft.Out(1))
165 }
166 if err := describeResult(c, owner, m.Name, ft.Out(0), &cmd, &bound); err != nil {
167 return Command{}, boundMethod{}, err
168 }
169 cmd.ReturnsError, bound.errAt = true, 1
170 default:
171 return Command{}, boundMethod{}, fmt.Errorf("%d results; want (), (T), (error) or (T, error)", ft.NumOut())
172 }
173 return cmd, bound, nil
174 }
175
176 func describeResult(c *typeCollector, owner, method string, t reflect.Type, cmd *Command, bound *boundMethod) error {
177 ref, err := c.ref(t, owner+"."+method+".result")
178 if err != nil {
179 return fmt.Errorf("result: %w", err)
180 }
181 cmd.Result = &ref
182 bound.result = true
183 return nil
184 }
185
186 // Commands lists the accepted methods sorted by name; never nil.
187 func (r *Registry) Commands() []Command { return slices.Clone(r.commands) }
188
189 // Invoke decodes args into the method's parameters, calls it, and returns
190 // its result (nil for void) or its error. Missing trailing arguments decode
191 // as zero values, matching the retired shell's tolerance; extra ones are rejected.
192 func (r *Registry) Invoke(ctx context.Context, name string, args []json.RawMessage) (result any, err error) {
193 m, ok := r.methods[name]
194 if !ok {
195 return nil, &UnknownMethodError{Method: name}
196 }
197 if r.target.IsNil() {
198 return nil, errors.New("hostrpc: registry has no target instance")
199 }
200 if err := ctx.Err(); err != nil {
201 return nil, err
202 }
203 if len(args) > len(m.params) {
204 return nil, &InvalidArgsError{Method: name, Reason: fmt.Sprintf("expected at most %d arguments, got %d", len(m.params), len(args))}
205 }
206 in := make([]reflect.Value, 0, len(m.params)+1)
207 in = append(in, r.target)
208 if m.context {
209 in = append(in, reflect.ValueOf(ctx))
210 }
211 for i, pt := range m.params {
212 v := reflect.New(pt)
213 if i < len(args) && len(bytes.TrimSpace(args[i])) > 0 {
214 if err := json.Unmarshal(args[i], v.Interface()); err != nil {
215 return nil, &InvalidArgsError{Method: name, Reason: fmt.Sprintf("argument %d: %v", i, err)}
216 }
217 }
218 in = append(in, v.Elem())
219 }
220 defer func() {
221 if rec := recover(); rec != nil {
222 result, err = nil, &PanicError{Method: name, Value: rec, Stack: debug.Stack()}
223 }
224 }()
225 // Decoders may take time or trigger cancellation. This is the final
226 // cancellation boundary for synchronous methods; never discard their
227 // result after dispatch, since a write may already have committed.
228 if err := ctx.Err(); err != nil {
229 return nil, err
230 }
231 out := m.fn.Call(in)
232 if m.errAt >= 0 {
233 if callErr, _ := out[m.errAt].Interface().(error); callErr != nil {
234 return nil, callErr
235 }
236 }
237 if m.result {
238 return out[0].Interface(), nil
239 }
240 return nil, nil
241 }
242
242 lines GO