@pi-in-go/pigpen-acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +23 -0
- package/LICENSE +22 -0
- package/README.md +110 -0
- package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
- package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
- package/extensions/acp/cmd/pig-acp/go.mod +6 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/main.go +177 -0
- package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
- package/extensions/acp/extension.go +63 -0
- package/extensions/acp/extension_test.go +111 -0
- package/extensions/acp/fakehost_test.go +548 -0
- package/extensions/acp/go.mod +6 -0
- package/extensions/acp/go.sum +2 -0
- package/package.json +41 -0
- package/port/PORT.md +147 -0
- package/port/interop/package-lock.json +89 -0
- package/port/interop/package.json +13 -0
- package/port/interop/run.mjs +229 -0
- package/port/mutate.mjs +110 -0
- package/port/mutation-results.txt +107 -0
- package/port/mutations.json +748 -0
- package/port/oracle/pi-acp/LICENSE +21 -0
- package/port/oracle/pi-acp/README.md +209 -0
- package/port/oracle/pi-acp/package.json +65 -0
- package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
- package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
- package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
- package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
- package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
- package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
- package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
- package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
- package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
- package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
- package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
- package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
- package/port/oracle/pi-acp/src/index.ts +82 -0
- package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
- package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
- package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
- package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
- package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
- package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
- package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
- package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
- package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
- package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
- package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
- package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
- package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
- package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
- package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
- package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
- package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
- package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
- package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
- package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
- package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
- package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
- package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
- package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
- package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
- package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
- package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
- package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
- package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
- package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
- package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
- package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
- package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
- package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
- package/port/schema/LICENSE +191 -0
- package/port/schema/schema.json +8015 -0
- package/port/upstream-tests.json +201 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package jsonrpc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"io"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// One session/update notification written through the connection's queue and writer goroutine.
|
|
9
|
+
//
|
|
10
|
+
// go test -run xxx -bench . -benchmem
|
|
11
|
+
func BenchmarkNotify(b *testing.B) {
|
|
12
|
+
pr, pw := io.Pipe()
|
|
13
|
+
defer pw.Close()
|
|
14
|
+
c := New(pr, io.Discard, func(*Request) (any, error) { return nil, nil })
|
|
15
|
+
params := map[string]any{"sessionId": "s1", "update": map[string]any{"sessionUpdate": "agent_message_chunk", "content": map[string]any{"type": "text", "text": "some streamed text from the model"}}}
|
|
16
|
+
b.ReportAllocs()
|
|
17
|
+
b.ResetTimer()
|
|
18
|
+
for i := 0; i < b.N; i++ {
|
|
19
|
+
if err := c.Notify("session/update", params); err != nil {
|
|
20
|
+
b.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
c.Drain(0)
|
|
24
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Package jsonrpc is newline-delimited JSON-RPC 2.0 over a pair of streams, the transport
|
|
2
|
+
// ACP uses on stdio (ndJsonStream and Connection of @agentclientprotocol/sdk).
|
|
3
|
+
package jsonrpc
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"bufio"
|
|
7
|
+
"bytes"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"sync"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// Error is a JSON-RPC error object.
|
|
17
|
+
type Error struct {
|
|
18
|
+
Code int
|
|
19
|
+
Message string
|
|
20
|
+
Data any
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func (e *Error) Error() string { return e.Message }
|
|
24
|
+
|
|
25
|
+
// Coded is implemented by errors that carry a JSON-RPC code (acp.RequestError does).
|
|
26
|
+
type Coded interface {
|
|
27
|
+
error
|
|
28
|
+
RPCError() (code int, message string, data any)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Request is an incoming request or notification.
|
|
32
|
+
type Request struct {
|
|
33
|
+
Method string
|
|
34
|
+
Params json.RawMessage
|
|
35
|
+
// Notification is true when the message has no id.
|
|
36
|
+
Notification bool
|
|
37
|
+
after []func()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// AfterResponse runs fn once the response to this request has been queued for writing, so
|
|
41
|
+
// anything fn sends follows the response on the wire.
|
|
42
|
+
func (r *Request) AfterResponse(fn func()) { r.after = append(r.after, fn) }
|
|
43
|
+
|
|
44
|
+
// Handler serves one incoming message. For a notification the result is dropped.
|
|
45
|
+
type Handler func(req *Request) (any, error)
|
|
46
|
+
|
|
47
|
+
// maxLine bounds one message (an embedded image can be many megabytes).
|
|
48
|
+
const maxLine = 64 << 20
|
|
49
|
+
|
|
50
|
+
// Conn is one JSON-RPC connection. Outgoing messages go through an unbounded FIFO queue and a
|
|
51
|
+
// single writer goroutine, so a slow or stalled peer never blocks the agent, lines never
|
|
52
|
+
// interleave, and the order in which messages were queued is the order on the wire.
|
|
53
|
+
type Conn struct {
|
|
54
|
+
h Handler
|
|
55
|
+
|
|
56
|
+
mu sync.Mutex
|
|
57
|
+
queue [][]byte
|
|
58
|
+
wake chan struct{}
|
|
59
|
+
failed bool
|
|
60
|
+
writing bool
|
|
61
|
+
active int
|
|
62
|
+
nextID int64
|
|
63
|
+
pending map[string]chan message
|
|
64
|
+
|
|
65
|
+
done chan struct{}
|
|
66
|
+
doneOnce sync.Once
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type message struct {
|
|
70
|
+
ID json.RawMessage `json:"id,omitempty"`
|
|
71
|
+
Method string `json:"method,omitempty"`
|
|
72
|
+
Params json.RawMessage `json:"params,omitempty"`
|
|
73
|
+
Result json.RawMessage `json:"result,omitempty"`
|
|
74
|
+
Error *wireError `json:"error,omitempty"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type wireError struct {
|
|
78
|
+
Code int `json:"code"`
|
|
79
|
+
Message string `json:"message"`
|
|
80
|
+
Data any `json:"data,omitempty"`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ErrClosed is returned when the connection can no longer carry messages.
|
|
84
|
+
var ErrClosed = errors.New("jsonrpc: connection closed")
|
|
85
|
+
|
|
86
|
+
// New starts reading from r and writing to w; every incoming message runs h on its own goroutine.
|
|
87
|
+
func New(r io.Reader, w io.Writer, h Handler) *Conn {
|
|
88
|
+
c := &Conn{h: h, wake: make(chan struct{}, 1), pending: map[string]chan message{}, done: make(chan struct{})}
|
|
89
|
+
go c.writeLoop(w)
|
|
90
|
+
go c.readLoop(r)
|
|
91
|
+
return c
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func (c *Conn) closeDone() {
|
|
95
|
+
c.doneOnce.Do(func() {
|
|
96
|
+
close(c.done)
|
|
97
|
+
c.mu.Lock()
|
|
98
|
+
for id, ch := range c.pending {
|
|
99
|
+
close(ch)
|
|
100
|
+
delete(c.pending, id)
|
|
101
|
+
}
|
|
102
|
+
c.mu.Unlock()
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func (c *Conn) writeLoop(w io.Writer) {
|
|
107
|
+
for {
|
|
108
|
+
c.mu.Lock()
|
|
109
|
+
if len(c.queue) == 0 {
|
|
110
|
+
c.mu.Unlock()
|
|
111
|
+
<-c.wake
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
line := c.queue[0]
|
|
115
|
+
c.queue = c.queue[1:]
|
|
116
|
+
c.writing = true
|
|
117
|
+
c.mu.Unlock()
|
|
118
|
+
_, err := w.Write(line)
|
|
119
|
+
c.mu.Lock()
|
|
120
|
+
c.writing = false
|
|
121
|
+
c.mu.Unlock()
|
|
122
|
+
if err != nil {
|
|
123
|
+
c.mu.Lock()
|
|
124
|
+
c.failed = true
|
|
125
|
+
c.queue = nil
|
|
126
|
+
c.mu.Unlock()
|
|
127
|
+
c.closeDone()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (c *Conn) enqueue(v any) error {
|
|
134
|
+
b, err := json.Marshal(v)
|
|
135
|
+
if err != nil {
|
|
136
|
+
return err
|
|
137
|
+
}
|
|
138
|
+
c.mu.Lock()
|
|
139
|
+
if c.failed {
|
|
140
|
+
c.mu.Unlock()
|
|
141
|
+
return ErrClosed
|
|
142
|
+
}
|
|
143
|
+
c.queue = append(c.queue, append(b, '\n'))
|
|
144
|
+
c.mu.Unlock()
|
|
145
|
+
select {
|
|
146
|
+
case c.wake <- struct{}{}:
|
|
147
|
+
default:
|
|
148
|
+
}
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func (c *Conn) readLoop(r io.Reader) {
|
|
153
|
+
br := bufio.NewReaderSize(r, 1<<20)
|
|
154
|
+
for {
|
|
155
|
+
line, err := readLine(br)
|
|
156
|
+
if len(bytes.TrimSpace(line)) > 0 {
|
|
157
|
+
var m message
|
|
158
|
+
if json.Unmarshal(line, &m) == nil {
|
|
159
|
+
c.dispatch(m)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if err != nil {
|
|
163
|
+
c.closeDone()
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func readLine(br *bufio.Reader) ([]byte, error) {
|
|
170
|
+
var out []byte
|
|
171
|
+
for {
|
|
172
|
+
part, err := br.ReadSlice('\n')
|
|
173
|
+
out = append(out, part...)
|
|
174
|
+
if len(out) > maxLine {
|
|
175
|
+
return nil, io.ErrShortBuffer
|
|
176
|
+
}
|
|
177
|
+
if err == bufio.ErrBufferFull {
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
return out, err
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func (c *Conn) dispatch(m message) {
|
|
185
|
+
if m.Method == "" {
|
|
186
|
+
if len(m.ID) == 0 {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
c.mu.Lock()
|
|
190
|
+
ch, ok := c.pending[string(m.ID)]
|
|
191
|
+
delete(c.pending, string(m.ID))
|
|
192
|
+
c.mu.Unlock()
|
|
193
|
+
if ok {
|
|
194
|
+
ch <- m
|
|
195
|
+
}
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
notification := len(m.ID) == 0
|
|
199
|
+
c.mu.Lock()
|
|
200
|
+
c.active++
|
|
201
|
+
c.mu.Unlock()
|
|
202
|
+
go c.serve(m, notification)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (c *Conn) serve(m message, notification bool) {
|
|
206
|
+
defer func() {
|
|
207
|
+
c.mu.Lock()
|
|
208
|
+
c.active--
|
|
209
|
+
c.mu.Unlock()
|
|
210
|
+
}()
|
|
211
|
+
req := &Request{Method: m.Method, Params: m.Params, Notification: notification}
|
|
212
|
+
res, err := c.call(req)
|
|
213
|
+
if notification {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
resp := map[string]any{"jsonrpc": "2.0", "id": m.ID}
|
|
217
|
+
if err != nil {
|
|
218
|
+
resp["error"] = toWireError(err)
|
|
219
|
+
} else {
|
|
220
|
+
resp["result"] = res
|
|
221
|
+
}
|
|
222
|
+
_ = c.enqueue(resp)
|
|
223
|
+
for _, fn := range req.after {
|
|
224
|
+
fn()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func (c *Conn) call(req *Request) (res any, err error) {
|
|
229
|
+
defer func() {
|
|
230
|
+
if p := recover(); p != nil {
|
|
231
|
+
res, err = nil, fmt.Errorf("panic: %v", p)
|
|
232
|
+
}
|
|
233
|
+
}()
|
|
234
|
+
return c.h(req)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func toWireError(err error) *wireError {
|
|
238
|
+
var coded Coded
|
|
239
|
+
if errors.As(err, &coded) {
|
|
240
|
+
code, msg, data := coded.RPCError()
|
|
241
|
+
return &wireError{Code: code, Message: msg, Data: data}
|
|
242
|
+
}
|
|
243
|
+
return &wireError{Code: -32603, Message: "Internal error", Data: map[string]any{"details": err.Error()}}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Call sends a request and decodes its result into result (may be nil).
|
|
247
|
+
func (c *Conn) Call(method string, params, result any) error {
|
|
248
|
+
c.mu.Lock()
|
|
249
|
+
c.nextID++
|
|
250
|
+
id := c.nextID
|
|
251
|
+
key := fmt.Sprint(id)
|
|
252
|
+
ch := make(chan message, 1)
|
|
253
|
+
select {
|
|
254
|
+
case <-c.done:
|
|
255
|
+
c.mu.Unlock()
|
|
256
|
+
return ErrClosed
|
|
257
|
+
default:
|
|
258
|
+
}
|
|
259
|
+
c.pending[key] = ch
|
|
260
|
+
c.mu.Unlock()
|
|
261
|
+
if err := c.enqueue(map[string]any{"jsonrpc": "2.0", "id": id, "method": method, "params": params}); err != nil {
|
|
262
|
+
c.mu.Lock()
|
|
263
|
+
delete(c.pending, key)
|
|
264
|
+
c.mu.Unlock()
|
|
265
|
+
return err
|
|
266
|
+
}
|
|
267
|
+
m, ok := <-ch
|
|
268
|
+
if !ok {
|
|
269
|
+
return ErrClosed
|
|
270
|
+
}
|
|
271
|
+
if m.Error != nil {
|
|
272
|
+
return &Error{Code: m.Error.Code, Message: m.Error.Message, Data: m.Error.Data}
|
|
273
|
+
}
|
|
274
|
+
if result != nil && len(m.Result) > 0 {
|
|
275
|
+
return json.Unmarshal(m.Result, result)
|
|
276
|
+
}
|
|
277
|
+
return nil
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Notify sends a notification.
|
|
281
|
+
func (c *Conn) Notify(method string, params any) error {
|
|
282
|
+
return c.enqueue(map[string]any{"jsonrpc": "2.0", "method": method, "params": params})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Done is closed when the input ends or a write fails for good.
|
|
286
|
+
func (c *Conn) Done() <-chan struct{} { return c.done }
|
|
287
|
+
|
|
288
|
+
// Drain waits until every request being served has been answered and every queued message has been
|
|
289
|
+
// written, or until timeout. It lets a client that closes its output right after sending a request
|
|
290
|
+
// (a one-shot script) still receive the answer.
|
|
291
|
+
func (c *Conn) Drain(timeout time.Duration) {
|
|
292
|
+
deadline := time.Now().Add(timeout)
|
|
293
|
+
for time.Now().Before(deadline) {
|
|
294
|
+
c.mu.Lock()
|
|
295
|
+
idle := c.active == 0 && len(c.queue) == 0 && !c.writing || c.failed
|
|
296
|
+
c.mu.Unlock()
|
|
297
|
+
if idle {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
time.Sleep(5 * time.Millisecond)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
package jsonrpc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"strings"
|
|
10
|
+
"sync"
|
|
11
|
+
"testing"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// peer is the client end of a Conn under test.
|
|
16
|
+
type peer struct {
|
|
17
|
+
toConn *io.PipeWriter
|
|
18
|
+
fromConn *bufio.Scanner
|
|
19
|
+
conn *Conn
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func newPeer(t *testing.T, h Handler) *peer {
|
|
23
|
+
t.Helper()
|
|
24
|
+
inR, inW := io.Pipe()
|
|
25
|
+
outR, outW := io.Pipe()
|
|
26
|
+
p := &peer{toConn: inW, fromConn: bufio.NewScanner(outR)}
|
|
27
|
+
p.fromConn.Buffer(make([]byte, 1<<20), 1<<26)
|
|
28
|
+
p.conn = New(inR, outW, h)
|
|
29
|
+
t.Cleanup(func() { inW.Close(); outR.Close() })
|
|
30
|
+
return p
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (p *peer) send(t *testing.T, v any) {
|
|
34
|
+
t.Helper()
|
|
35
|
+
var line string
|
|
36
|
+
if s, ok := v.(string); ok {
|
|
37
|
+
line = s
|
|
38
|
+
} else {
|
|
39
|
+
b, _ := json.Marshal(v)
|
|
40
|
+
line = string(b)
|
|
41
|
+
}
|
|
42
|
+
done := make(chan error, 1)
|
|
43
|
+
go func() { _, err := p.toConn.Write([]byte(line + "\n")); done <- err }()
|
|
44
|
+
select {
|
|
45
|
+
case err := <-done:
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
case <-time.After(3 * time.Second):
|
|
50
|
+
p.toConn.CloseWithError(io.ErrClosedPipe)
|
|
51
|
+
t.Fatal("the connection does not read its input")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (p *peer) recv(t *testing.T) map[string]any {
|
|
56
|
+
t.Helper()
|
|
57
|
+
done := make(chan map[string]any, 1)
|
|
58
|
+
go func() {
|
|
59
|
+
if !p.fromConn.Scan() {
|
|
60
|
+
done <- nil
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
var m map[string]any
|
|
64
|
+
if err := json.Unmarshal(p.fromConn.Bytes(), &m); err != nil {
|
|
65
|
+
t.Errorf("output %q is not JSON: %v", p.fromConn.Text(), err)
|
|
66
|
+
}
|
|
67
|
+
done <- m
|
|
68
|
+
}()
|
|
69
|
+
select {
|
|
70
|
+
case m := <-done:
|
|
71
|
+
return m
|
|
72
|
+
case <-time.After(5 * time.Second):
|
|
73
|
+
t.Fatal("no message from the connection")
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type coded struct {
|
|
79
|
+
code int
|
|
80
|
+
msg string
|
|
81
|
+
data any
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func (c coded) Error() string { return c.msg }
|
|
85
|
+
func (c coded) RPCError() (int, string, any) { return c.code, c.msg, c.data }
|
|
86
|
+
|
|
87
|
+
func TestConn(t *testing.T) {
|
|
88
|
+
t.Run("answers a request with its result and the same id", func(t *testing.T) {
|
|
89
|
+
p := newPeer(t, func(r *Request) (any, error) { return map[string]any{"echo": r.Method}, nil })
|
|
90
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 7, "method": "ping", "params": map[string]any{}})
|
|
91
|
+
m := p.recv(t)
|
|
92
|
+
if m["jsonrpc"] != "2.0" || m["id"] != float64(7) {
|
|
93
|
+
t.Fatalf("response = %v", m)
|
|
94
|
+
}
|
|
95
|
+
if r, _ := m["result"].(map[string]any); r["echo"] != "ping" {
|
|
96
|
+
t.Errorf("result = %v", m["result"])
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
t.Run("keeps string ids and answers a null result as null", func(t *testing.T) {
|
|
101
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, nil })
|
|
102
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": "abc", "method": "m"})
|
|
103
|
+
m := p.recv(t)
|
|
104
|
+
if m["id"] != "abc" {
|
|
105
|
+
t.Fatalf("response = %v", m)
|
|
106
|
+
}
|
|
107
|
+
if v, ok := m["result"]; !ok || v != nil {
|
|
108
|
+
t.Errorf("result = %v (present %v), want null", v, ok)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
t.Run("maps a coded error to code, message and data", func(t *testing.T) {
|
|
113
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, coded{-32602, "Invalid params: x", map[string]any{"k": 1}} })
|
|
114
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 1, "method": "m"})
|
|
115
|
+
e, _ := p.recv(t)["error"].(map[string]any)
|
|
116
|
+
if e["code"] != float64(-32602) || e["message"] != "Invalid params: x" {
|
|
117
|
+
t.Fatalf("error = %v", e)
|
|
118
|
+
}
|
|
119
|
+
if d, _ := e["data"].(map[string]any); d["k"] != float64(1) {
|
|
120
|
+
t.Errorf("data = %v", e["data"])
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
t.Run("maps any other error to Internal error with the details", func(t *testing.T) {
|
|
125
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, errors.New("boom") })
|
|
126
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 1, "method": "m"})
|
|
127
|
+
e, _ := p.recv(t)["error"].(map[string]any)
|
|
128
|
+
if e["code"] != float64(-32603) || e["message"] != "Internal error" {
|
|
129
|
+
t.Fatalf("error = %v", e)
|
|
130
|
+
}
|
|
131
|
+
if d, _ := e["data"].(map[string]any); d["details"] != "boom" {
|
|
132
|
+
t.Errorf("data = %v", e["data"])
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
t.Run("skips a line that is not JSON and keeps serving", func(t *testing.T) {
|
|
137
|
+
p := newPeer(t, func(*Request) (any, error) { return "ok", nil })
|
|
138
|
+
p.send(t, "this is not json")
|
|
139
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 2, "method": "m"})
|
|
140
|
+
if m := p.recv(t); m["id"] != float64(2) || m["result"] != "ok" {
|
|
141
|
+
t.Fatalf("response = %v", m)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
t.Run("does not answer a notification", func(t *testing.T) {
|
|
146
|
+
got := make(chan string, 1)
|
|
147
|
+
p := newPeer(t, func(r *Request) (any, error) {
|
|
148
|
+
if r.Method == "session/cancel" {
|
|
149
|
+
if !r.Notification {
|
|
150
|
+
t.Error("Notification flag not set")
|
|
151
|
+
}
|
|
152
|
+
got <- r.Method
|
|
153
|
+
}
|
|
154
|
+
return "ignored", nil
|
|
155
|
+
})
|
|
156
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "method": "session/cancel", "params": map[string]any{"sessionId": "s"}})
|
|
157
|
+
if m := <-got; m != "session/cancel" {
|
|
158
|
+
t.Fatalf("method = %q", m)
|
|
159
|
+
}
|
|
160
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 1, "method": "after"})
|
|
161
|
+
if m := p.recv(t); m["id"] != float64(1) {
|
|
162
|
+
t.Fatalf("the first output was %v, not the answer to the request", m)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
t.Run("a slow request does not block a later one", func(t *testing.T) {
|
|
167
|
+
release := make(chan struct{})
|
|
168
|
+
p := newPeer(t, func(r *Request) (any, error) {
|
|
169
|
+
if r.Method == "slow" {
|
|
170
|
+
<-release
|
|
171
|
+
}
|
|
172
|
+
return r.Method, nil
|
|
173
|
+
})
|
|
174
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 1, "method": "slow"})
|
|
175
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 2, "method": "fast"})
|
|
176
|
+
if m := p.recv(t); m["id"] != float64(2) {
|
|
177
|
+
t.Fatalf("first response = %v", m)
|
|
178
|
+
}
|
|
179
|
+
close(release)
|
|
180
|
+
if m := p.recv(t); m["id"] != float64(1) {
|
|
181
|
+
t.Fatalf("second response = %v", m)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
t.Run("delivers the request params untouched", func(t *testing.T) {
|
|
186
|
+
got := make(chan string, 1)
|
|
187
|
+
p := newPeer(t, func(r *Request) (any, error) { got <- string(r.Params); return nil, nil })
|
|
188
|
+
p.send(t, `{"jsonrpc":"2.0","id":1,"method":"m","params":{"a":[1,2,{"b":null}]}}`)
|
|
189
|
+
if s := <-got; s != `{"a":[1,2,{"b":null}]}` {
|
|
190
|
+
t.Errorf("params = %s", s)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
t.Run("correlates an outgoing request with its response", func(t *testing.T) {
|
|
195
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, nil })
|
|
196
|
+
type res struct {
|
|
197
|
+
Outcome string `json:"outcome"`
|
|
198
|
+
}
|
|
199
|
+
done := make(chan error, 1)
|
|
200
|
+
var out res
|
|
201
|
+
go func() { done <- p.conn.Call("session/request_permission", map[string]any{"sessionId": "s"}, &out) }()
|
|
202
|
+
req := p.recv(t)
|
|
203
|
+
if req["method"] != "session/request_permission" || req["jsonrpc"] != "2.0" {
|
|
204
|
+
t.Fatalf("request = %v", req)
|
|
205
|
+
}
|
|
206
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": req["id"], "result": map[string]any{"outcome": "selected"}})
|
|
207
|
+
if err := <-done; err != nil || out.Outcome != "selected" {
|
|
208
|
+
t.Fatalf("err=%v out=%+v", err, out)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
t.Run("an error response to an outgoing request is returned as *Error", func(t *testing.T) {
|
|
213
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, nil })
|
|
214
|
+
done := make(chan error, 1)
|
|
215
|
+
go func() { done <- p.conn.Call("x", nil, nil) }()
|
|
216
|
+
req := p.recv(t)
|
|
217
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": req["id"], "error": map[string]any{"code": -32601, "message": "nope"}})
|
|
218
|
+
var e *Error
|
|
219
|
+
if err := <-done; !errors.As(err, &e) || e.Code != -32601 || e.Message != "nope" {
|
|
220
|
+
t.Fatalf("err = %v", err)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
t.Run("closing the input fails pending requests and closes Done", func(t *testing.T) {
|
|
225
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, nil })
|
|
226
|
+
done := make(chan error, 1)
|
|
227
|
+
go func() { done <- p.conn.Call("x", nil, nil) }()
|
|
228
|
+
p.recv(t)
|
|
229
|
+
p.toConn.Close()
|
|
230
|
+
select {
|
|
231
|
+
case err := <-done:
|
|
232
|
+
if err == nil {
|
|
233
|
+
t.Error("pending request succeeded")
|
|
234
|
+
}
|
|
235
|
+
case <-time.After(5 * time.Second):
|
|
236
|
+
t.Fatal("pending request never failed")
|
|
237
|
+
}
|
|
238
|
+
select {
|
|
239
|
+
case <-p.conn.Done():
|
|
240
|
+
case <-time.After(5 * time.Second):
|
|
241
|
+
t.Fatal("Done never closed")
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
t.Run("serializes concurrent writes into whole lines", func(t *testing.T) {
|
|
246
|
+
p := newPeer(t, func(*Request) (any, error) { return nil, nil })
|
|
247
|
+
var wg sync.WaitGroup
|
|
248
|
+
for i := 0; i < 200; i++ {
|
|
249
|
+
wg.Add(1)
|
|
250
|
+
go func(i int) {
|
|
251
|
+
defer wg.Done()
|
|
252
|
+
_ = p.conn.Notify("session/update", map[string]any{"sessionId": "s", "n": i, "pad": strings.Repeat("x", 4096)})
|
|
253
|
+
}(i)
|
|
254
|
+
}
|
|
255
|
+
seen := map[float64]bool{}
|
|
256
|
+
for i := 0; i < 200; i++ {
|
|
257
|
+
m := p.recv(t)
|
|
258
|
+
seen[m["params"].(map[string]any)["n"].(float64)] = true
|
|
259
|
+
}
|
|
260
|
+
wg.Wait()
|
|
261
|
+
if len(seen) != 200 {
|
|
262
|
+
t.Errorf("saw %d distinct notifications", len(seen))
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
t.Run("accepts a very large message", func(t *testing.T) {
|
|
267
|
+
got := make(chan int, 1)
|
|
268
|
+
p := newPeer(t, func(r *Request) (any, error) { got <- len(r.Params); return nil, nil })
|
|
269
|
+
big := strings.Repeat("y", 8<<20)
|
|
270
|
+
p.send(t, fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"m","params":{"blob":%q}}`, big))
|
|
271
|
+
if n := <-got; n < 8<<20 {
|
|
272
|
+
t.Errorf("params length %d", n)
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
t.Run("runs AfterResponse hooks after the response was written", func(t *testing.T) {
|
|
277
|
+
order := make(chan string, 2)
|
|
278
|
+
p := newPeer(t, func(r *Request) (any, error) {
|
|
279
|
+
r.AfterResponse(func() { order <- "hook" })
|
|
280
|
+
return "done", nil
|
|
281
|
+
})
|
|
282
|
+
p.send(t, map[string]any{"jsonrpc": "2.0", "id": 1, "method": "m"})
|
|
283
|
+
select {
|
|
284
|
+
case <-order:
|
|
285
|
+
// The hook may only run once the response is on the wire; reading it now proves it was written.
|
|
286
|
+
case <-time.After(5 * time.Second):
|
|
287
|
+
t.Fatal("hook never ran")
|
|
288
|
+
}
|
|
289
|
+
if m := p.recv(t); m["result"] != "done" {
|
|
290
|
+
t.Fatalf("response = %v", m)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// failingWriter is a stdout that was destroyed (test/unit/stdout-destroyed-does-not-crash.test.ts).
|
|
296
|
+
type failingWriter struct{}
|
|
297
|
+
|
|
298
|
+
func (failingWriter) Write([]byte) (int, error) { return 0, io.ErrClosedPipe }
|
|
299
|
+
|
|
300
|
+
func TestDestroyedOutput(t *testing.T) {
|
|
301
|
+
tw(t, "unit/stdout-destroyed-does-not-crash", "stdout writer: resolves even if stdout is destroyed", func(t *testing.T) {
|
|
302
|
+
in, _ := io.Pipe()
|
|
303
|
+
c := New(in, failingWriter{}, func(*Request) (any, error) { return nil, nil })
|
|
304
|
+
done := make(chan struct{})
|
|
305
|
+
go func() {
|
|
306
|
+
_ = c.Notify("session/update", map[string]any{"sessionId": "s"})
|
|
307
|
+
close(done)
|
|
308
|
+
}()
|
|
309
|
+
select {
|
|
310
|
+
case <-done:
|
|
311
|
+
case <-time.After(5 * time.Second):
|
|
312
|
+
t.Fatal("Notify blocked on a destroyed output")
|
|
313
|
+
}
|
|
314
|
+
select {
|
|
315
|
+
case <-c.Done():
|
|
316
|
+
case <-time.After(5 * time.Second):
|
|
317
|
+
t.Fatal("a failed write did not end the connection")
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package jsonrpc
|
|
2
|
+
|
|
3
|
+
// Twin helpers for a port whose original has tests. Every upstream test case gets one
|
|
4
|
+
// tw(...) call carrying the upstream title unchanged, or one tskip(...) with the reason it
|
|
5
|
+
// cannot be ported. `pigeq twins check` reads these calls and fails on a missing title, an
|
|
6
|
+
// unknown one (a misspelling) or one reason shared by many skips.
|
|
7
|
+
//
|
|
8
|
+
// pigeq twins list --tests <upstream>/test > port/upstream-tests.json
|
|
9
|
+
// pigeq twins check --ledger port/upstream-tests.json --go extensions/<name> [--files a,b]
|
|
10
|
+
//
|
|
11
|
+
// Copy this file into the port as twin_test.go and replace jsonrpc with the package name (in
|
|
12
|
+
// an external test package use `jsonrpc_test`). A parametrized upstream title (it contains
|
|
13
|
+
// `${`) is twinned once, with the template text unchanged.
|
|
14
|
+
|
|
15
|
+
import "testing"
|
|
16
|
+
|
|
17
|
+
// tw runs one exact twin: file is the upstream test file (its name without .test.mjs), title
|
|
18
|
+
// the upstream test title, unchanged.
|
|
19
|
+
func tw(t *testing.T, file, title string, fn func(t *testing.T)) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
t.Run(title, fn)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tskip records a named gap: the upstream case has no Go twin, with its own reason.
|
|
25
|
+
func tskip(t *testing.T, file, title, reason string) {
|
|
26
|
+
t.Helper()
|
|
27
|
+
t.Run(title, func(t *testing.T) { t.Skip(reason) })
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package pirpc
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"os/exec"
|
|
7
|
+
"syscall"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// terminate asks the child to stop (SIGTERM, like the original) and kills it if it lingers.
|
|
12
|
+
func terminate(cmd *exec.Cmd) {
|
|
13
|
+
if cmd.Process == nil {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
_ = cmd.Process.Signal(syscall.SIGTERM)
|
|
17
|
+
proc := cmd.Process
|
|
18
|
+
time.AfterFunc(5*time.Second, func() { _ = proc.Kill() })
|
|
19
|
+
}
|