返回 DeepSeek-Reasonix
server.go
根目录 / internal / acp / server.go
1 package acp
2
3 import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "strconv"
10 "sync"
11 "sync/atomic"
12 )
13
14 // maxMessageBytes caps a single inbound NDJSON line. ACP messages can embed
15 // resource text, so the limit is generous; a line past it is a framing error.
16 const maxMessageBytes = 32 << 20 // 32 MiB
17
18 // RequestHandler answers an inbound JSON-RPC request. The returned value is
19 // marshaled as the response result. To control the error code, return a
20 // *RPCError; any other error becomes ErrInternal.
21 type RequestHandler func(ctx context.Context, params json.RawMessage) (any, error)
22
23 // responseWithAfter lets a request handler schedule work that must run only
24 // after its successful JSON-RPC response has been written to the wire.
25 type responseWithAfter interface {
26 Response() any
27 AfterResponse()
28 }
29
30 // NotificationHandler reacts to an inbound notification. It cannot reply, so it
31 // returns nothing — errors have nowhere to go on the wire (stderr would corrupt
32 // stdout, which is the JSON-RPC channel).
33 type NotificationHandler func(ctx context.Context, params json.RawMessage)
34
35 // RPCError lets a handler choose the JSON-RPC error code returned to the client.
36 type RPCError struct {
37 Code int
38 Message string
39 }
40
41 func (e *RPCError) Error() string { return e.Message }
42
43 // Conn is one NDJSON JSON-RPC 2.0 connection over a reader/writer pair (stdin/
44 // stdout in production). It dispatches inbound requests and notifications to
45 // registered handlers, and can itself send outbound notifications (session/update)
46 // and requests (session/request_permission), correlating replies by id.
47 //
48 // Writes are serialized by a mutex, so handlers running on separate goroutines
49 // (a long session/prompt alongside a session/cancel) never interleave a line.
50 // It implements notifier, the dependency the dispatch sink takes.
51 type Conn struct {
52 r io.Reader
53
54 wmu sync.Mutex
55 enc *json.Encoder
56
57 nextID atomic.Int64
58
59 pmu sync.Mutex
60 pending map[int64]chan rpcResult
61
62 reqH map[string]RequestHandler
63 notH map[string]NotificationHandler
64
65 wg sync.WaitGroup
66 closeOnce sync.Once
67 closed chan struct{}
68 }
69
70 // rpcResult is the outcome of an outbound request, delivered to the waiter.
71 type rpcResult struct {
72 result json.RawMessage
73 err error
74 }
75
76 // rpcError is the JSON-RPC error object on the wire.
77 type rpcError struct {
78 Code int `json:"code"`
79 Message string `json:"message"`
80 Data json.RawMessage `json:"data,omitempty"`
81 }
82
83 // outbound is a JSON-RPC frame we send. omitempty fields select between request,
84 // notification, success response, and error response shapes.
85 type outbound struct {
86 JSONRPC string `json:"jsonrpc"`
87 ID json.RawMessage `json:"id,omitempty"`
88 Method string `json:"method,omitempty"`
89 Params json.RawMessage `json:"params,omitempty"`
90 Result json.RawMessage `json:"result,omitempty"`
91 Error *rpcError `json:"error,omitempty"`
92 }
93
94 // inbound is a parsed JSON-RPC frame we received. The combination of id/method
95 // presence distinguishes request, notification, and response.
96 type inbound struct {
97 ID json.RawMessage `json:"id"`
98 Method string `json:"method"`
99 Params json.RawMessage `json:"params"`
100 Result json.RawMessage `json:"result"`
101 Error *rpcError `json:"error"`
102 }
103
104 // NewConn wires a connection over r (inbound) and w (outbound). Register handlers
105 // with Handle / HandleNotify before calling Serve. The encoder disables HTML
106 // escaping so payloads match main's JSON.stringify output byte-for-byte.
107 func NewConn(r io.Reader, w io.Writer) *Conn {
108 enc := json.NewEncoder(w)
109 enc.SetEscapeHTML(false)
110 return &Conn{
111 r: r,
112 enc: enc,
113 pending: make(map[int64]chan rpcResult),
114 reqH: make(map[string]RequestHandler),
115 notH: make(map[string]NotificationHandler),
116 closed: make(chan struct{}),
117 }
118 }
119
120 // Handle registers a request handler for method. Not safe to call concurrently
121 // with Serve; wire all handlers up first.
122 func (c *Conn) Handle(method string, h RequestHandler) { c.reqH[method] = h }
123
124 // HandleNotify registers a notification handler for method.
125 func (c *Conn) HandleNotify(method string, h NotificationHandler) { c.notH[method] = h }
126
127 // Serve reads inbound frames until the reader ends or ctx is cancelled. Each
128 // inbound request/notification runs on its own goroutine so a long-running prompt
129 // does not block cancellation or permission replies. When the read loop ends it
130 // cancels in-flight handlers (so prompts abort) and waits for them to return —
131 // flushing fast responses and unwinding aborted ones — before failing any
132 // outstanding outbound requests. Returns nil on clean EOF.
133 func (c *Conn) Serve(ctx context.Context) error {
134 ctx, cancel := context.WithCancel(ctx)
135 defer cancel()
136
137 br := bufio.NewReaderSize(c.r, 64<<10)
138 var loopErr error
139 for {
140 line, err := readLine(br)
141 if len(line) > 0 {
142 c.dispatch(ctx, line)
143 }
144 if err != nil {
145 if !errors.Is(err, io.EOF) {
146 loopErr = err
147 }
148 break
149 }
150 if err := ctx.Err(); err != nil {
151 loopErr = err
152 break
153 }
154 }
155
156 cancel() // abort in-flight handlers (prompts unwind via ctx)
157 c.wg.Wait() // let them flush their responses before we tear down
158 c.shutdown() // fail any still-pending outbound requests
159 return loopErr
160 }
161
162 // readLine reads one NDJSON line (without the trailing newline), enforcing the
163 // size cap. It returns the line and any read error; on EOF it still returns the
164 // trailing partial line so a final newline-less frame is processed.
165 func readLine(br *bufio.Reader) ([]byte, error) {
166 var buf []byte
167 for {
168 chunk, err := br.ReadSlice('\n')
169 buf = append(buf, chunk...)
170 if len(buf) > maxMessageBytes {
171 return nil, errors.New("acp: message exceeds size limit")
172 }
173 if err == bufio.ErrBufferFull {
174 continue
175 }
176 // Trim the trailing newline (and CR) if present.
177 n := len(buf)
178 for n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r') {
179 n--
180 }
181 return trimSpace(buf[:n]), err
182 }
183 }
184
185 // trimSpace drops leading/trailing ASCII whitespace without allocating.
186 func trimSpace(b []byte) []byte {
187 i, j := 0, len(b)
188 for i < j && isSpace(b[i]) {
189 i++
190 }
191 for j > i && isSpace(b[j-1]) {
192 j--
193 }
194 return b[i:j]
195 }
196
197 func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' }
198
199 // dispatch parses one frame and routes it. Requests and notifications fan out to
200 // goroutines; responses resolve inline (they are cheap and need ordering only
201 // against the pending map, which is mutex-guarded).
202 func (c *Conn) dispatch(ctx context.Context, line []byte) {
203 var in inbound
204 if err := json.Unmarshal(line, &in); err != nil {
205 c.writeError(json.RawMessage("null"), ErrParse, "parse error")
206 return
207 }
208 hasID := len(in.ID) > 0
209 switch {
210 case in.Method != "" && hasID:
211 c.wg.Add(1)
212 go func() {
213 defer c.wg.Done()
214 c.serveRequest(ctx, in.ID, in.Method, in.Params)
215 }()
216 case in.Method != "" && !hasID:
217 if h := c.notH[in.Method]; h != nil {
218 c.wg.Add(1)
219 go func() {
220 defer c.wg.Done()
221 h(ctx, in.Params)
222 }()
223 }
224 case in.Method == "" && hasID:
225 c.resolve(in)
226 default:
227 c.writeError(json.RawMessage("null"), ErrInvalidRequest, "invalid request")
228 }
229 }
230
231 // serveRequest runs a request handler and writes its response (or error).
232 func (c *Conn) serveRequest(ctx context.Context, id json.RawMessage, method string, params json.RawMessage) {
233 h := c.reqH[method]
234 if h == nil {
235 c.writeError(id, ErrMethodNotFound, "method not found: "+method)
236 return
237 }
238 result, err := h(ctx, params)
239 if err != nil {
240 code := ErrInternal
241 var re *RPCError
242 if errors.As(err, &re) {
243 code = re.Code
244 }
245 c.writeError(id, code, err.Error())
246 return
247 }
248 var after func()
249 if r, ok := result.(responseWithAfter); ok {
250 result = r.Response()
251 after = r.AfterResponse
252 }
253 raw, err := json.Marshal(result)
254 if err != nil {
255 c.writeError(id, ErrInternal, "marshal result: "+err.Error())
256 return
257 }
258 if err := c.write(outbound{JSONRPC: "2.0", ID: id, Result: raw}); err != nil {
259 return
260 }
261 if after != nil {
262 after()
263 }
264 }
265
266 // resolve delivers a response to the goroutine waiting on its outbound request.
267 func (c *Conn) resolve(in inbound) {
268 id, err := strconv.ParseInt(string(in.ID), 10, 64)
269 if err != nil {
270 return // we only issue integer ids; an unparsable id isn't ours
271 }
272 c.pmu.Lock()
273 ch := c.pending[id]
274 delete(c.pending, id)
275 c.pmu.Unlock()
276 if ch == nil {
277 return
278 }
279 if in.Error != nil {
280 ch <- rpcResult{err: errors.New(in.Error.Message)}
281 return
282 }
283 ch <- rpcResult{result: in.Result}
284 }
285
286 // Notify sends a fire-and-forget notification. Satisfies notifier.
287 func (c *Conn) Notify(method string, params any) error {
288 raw, err := json.Marshal(params)
289 if err != nil {
290 return err
291 }
292 return c.write(outbound{JSONRPC: "2.0", Method: method, Params: raw})
293 }
294
295 // Request sends an outbound request and blocks until the peer responds, ctx is
296 // cancelled, or the connection closes. Satisfies notifier.
297 func (c *Conn) Request(ctx context.Context, method string, params any) (json.RawMessage, error) {
298 raw, err := json.Marshal(params)
299 if err != nil {
300 return nil, err
301 }
302 id := c.nextID.Add(1)
303 ch := make(chan rpcResult, 1)
304 c.pmu.Lock()
305 c.pending[id] = ch
306 c.pmu.Unlock()
307 defer func() {
308 c.pmu.Lock()
309 delete(c.pending, id)
310 c.pmu.Unlock()
311 }()
312
313 idRaw, _ := json.Marshal(id)
314 if err := c.write(outbound{JSONRPC: "2.0", ID: idRaw, Method: method, Params: raw}); err != nil {
315 return nil, err
316 }
317 select {
318 case res := <-ch:
319 return res.result, res.err
320 case <-ctx.Done():
321 return nil, ctx.Err()
322 case <-c.closed:
323 return nil, errors.New("acp: connection closed")
324 }
325 }
326
327 func (c *Conn) write(m outbound) error {
328 c.wmu.Lock()
329 defer c.wmu.Unlock()
330 return c.enc.Encode(m)
331 }
332
333 func (c *Conn) writeError(id json.RawMessage, code int, msg string) {
334 _ = c.write(outbound{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg}})
335 }
336
337 // shutdown fails every in-flight outbound request so their goroutines unblock.
338 func (c *Conn) shutdown() {
339 c.closeOnce.Do(func() {
340 close(c.closed)
341 c.pmu.Lock()
342 for id, ch := range c.pending {
343 ch <- rpcResult{err: errors.New("acp: connection closed")}
344 delete(c.pending, id)
345 }
346 c.pmu.Unlock()
347 })
348 }
349
349 lines GO