@pi-in-go/pigpen-herdr 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/LICENSE +21 -0
- package/README.md +20 -0
- package/extensions/herdr/bench_test.go +55 -0
- package/extensions/herdr/diagnostics_test.go +92 -0
- package/extensions/herdr/extension.go +554 -0
- package/extensions/herdr/extension_test.go +772 -0
- package/extensions/herdr/go.mod +6 -0
- package/extensions/herdr/go.sum +2 -0
- package/extensions/herdr/harness_test.go +449 -0
- package/extensions/herdr/procattr_other.go +13 -0
- package/extensions/herdr/procattr_windows.go +16 -0
- package/package.json +35 -0
- package/provenance.json +7 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
package herdr_test
|
|
2
|
+
|
|
3
|
+
// Test harness: a fake herdr binary and a fake PiG host.
|
|
4
|
+
//
|
|
5
|
+
// The fake host speaks PiG's documented extension wire protocol (`pig docs show
|
|
6
|
+
// extension-api`) over an in-memory pipe and drives the real SDK through the
|
|
7
|
+
// public `Extension.RunWithConn`. So the tests cross the same boundary a real
|
|
8
|
+
// host does, and use no PiG-internal package.
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"encoding/binary"
|
|
12
|
+
"encoding/json"
|
|
13
|
+
"fmt"
|
|
14
|
+
"io"
|
|
15
|
+
"net"
|
|
16
|
+
"os"
|
|
17
|
+
"path/filepath"
|
|
18
|
+
"slices"
|
|
19
|
+
"strconv"
|
|
20
|
+
"strings"
|
|
21
|
+
"sync"
|
|
22
|
+
"testing"
|
|
23
|
+
"time"
|
|
24
|
+
|
|
25
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// ── Fake herdr ───────────────────────────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// The test binary doubles as the fake `$HERDR_BIN_PATH`: when HERDR_FAKE_LOG is
|
|
31
|
+
// set, it records one line of tab-separated arguments and exits. That works on
|
|
32
|
+
// every platform, with no shell script.
|
|
33
|
+
|
|
34
|
+
func TestMain(m *testing.M) {
|
|
35
|
+
if log := os.Getenv("HERDR_FAKE_LOG"); log != "" {
|
|
36
|
+
os.Exit(runFakeHerdr(log))
|
|
37
|
+
}
|
|
38
|
+
os.Exit(m.Run())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func runFakeHerdr(log string) int {
|
|
42
|
+
args := os.Args[1:]
|
|
43
|
+
// Only reports are slow, so a release that does not wait for them lands first.
|
|
44
|
+
if ms, _ := strconv.Atoi(os.Getenv("HERDR_FAKE_DELAY_MS")); ms > 0 && len(args) > 1 && args[1] == "report-agent" {
|
|
45
|
+
time.Sleep(time.Duration(ms) * time.Millisecond)
|
|
46
|
+
}
|
|
47
|
+
// A hung report never returns and never logs; only the call timeout ends it.
|
|
48
|
+
if os.Getenv("HERDR_FAKE_HANG") == "1" && len(args) > 1 && args[1] == "report-agent" {
|
|
49
|
+
time.Sleep(time.Hour)
|
|
50
|
+
}
|
|
51
|
+
f, err := os.OpenFile(log, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
52
|
+
if err == nil {
|
|
53
|
+
_, _ = f.WriteString(strings.Join(args, "\t") + "\t\n")
|
|
54
|
+
_ = f.Close()
|
|
55
|
+
}
|
|
56
|
+
// A resume command is refused the way real herdr refuses it. The refused call
|
|
57
|
+
// is logged above, so tests see the attempt and the retry.
|
|
58
|
+
if slices.Contains(args, "--") {
|
|
59
|
+
switch os.Getenv("HERDR_FAKE_RESUME") {
|
|
60
|
+
case "old": // herdr < 0.9.2: the CLI does not know the `--` separator
|
|
61
|
+
fmt.Fprintln(os.Stderr, "unknown option: --")
|
|
62
|
+
return 2
|
|
63
|
+
case "invalid": // herdr >= 0.9.2 rejecting the command; the report is not applied
|
|
64
|
+
fmt.Println(`{"error":{"code":"invalid_resume_argv","message":"resume_argv must not contain apostrophes"},"id":"cli:request"}`)
|
|
65
|
+
return 1
|
|
66
|
+
case "message-option": // an old CLI naming another option it does not know
|
|
67
|
+
fmt.Fprintln(os.Stderr, "unknown option: --message=Apply the plan?")
|
|
68
|
+
return 2
|
|
69
|
+
case "broken": // a failure that says nothing about resume support
|
|
70
|
+
fmt.Fprintln(os.Stderr, "error: failed to connect to herdr socket")
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
code, _ := strconv.Atoi(os.Getenv("HERDR_FAKE_EXIT"))
|
|
75
|
+
if msg := os.Getenv("HERDR_FAKE_STDERR"); msg != "" && code != 0 {
|
|
76
|
+
fmt.Fprintln(os.Stderr, msg)
|
|
77
|
+
}
|
|
78
|
+
return code
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type call struct{ args []string }
|
|
82
|
+
|
|
83
|
+
type fakeHerdr struct {
|
|
84
|
+
t *testing.T
|
|
85
|
+
log string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// installFakeHerdr points the process environment at a fresh fake herdr pane.
|
|
89
|
+
func installFakeHerdr(t *testing.T) *fakeHerdr {
|
|
90
|
+
t.Helper()
|
|
91
|
+
exe, err := os.Executable()
|
|
92
|
+
if err != nil {
|
|
93
|
+
t.Fatal(err)
|
|
94
|
+
}
|
|
95
|
+
h := &fakeHerdr{t: t, log: filepath.Join(t.TempDir(), "calls.log")}
|
|
96
|
+
t.Setenv("HERDR_ENV", "1")
|
|
97
|
+
t.Setenv("HERDR_PANE_ID", "p_7")
|
|
98
|
+
t.Setenv("HERDR_BIN_PATH", exe)
|
|
99
|
+
t.Setenv("HERDR_SOCKET_PATH", filepath.Join(t.TempDir(), "herdr.sock"))
|
|
100
|
+
t.Setenv("HERDR_FAKE_LOG", h.log)
|
|
101
|
+
t.Setenv("HERDR_FAKE_DELAY_MS", "")
|
|
102
|
+
t.Setenv("HERDR_FAKE_EXIT", "")
|
|
103
|
+
t.Setenv("HERDR_FAKE_STDERR", "")
|
|
104
|
+
t.Setenv("HERDR_FAKE_HANG", "")
|
|
105
|
+
t.Setenv("HERDR_FAKE_RESUME", "")
|
|
106
|
+
return h
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (h *fakeHerdr) slow(delay time.Duration) {
|
|
110
|
+
h.t.Setenv("HERDR_FAKE_DELAY_MS", strconv.Itoa(int(delay/time.Millisecond)))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func (h *fakeHerdr) hang() { h.t.Setenv("HERDR_FAKE_HANG", "1") }
|
|
114
|
+
|
|
115
|
+
// refuseResume makes herdr refuse any report that carries a resume command: "old"
|
|
116
|
+
// (herdr before 0.9.2), "invalid" (invalid_resume_argv) or "broken" (an unrelated failure).
|
|
117
|
+
func (h *fakeHerdr) refuseResume(kind string) { h.t.Setenv("HERDR_FAKE_RESUME", kind) }
|
|
118
|
+
|
|
119
|
+
// failWithMessage makes every call exit with code after printing msg, as herdr
|
|
120
|
+
// prints why it refused a request.
|
|
121
|
+
func (h *fakeHerdr) failWithMessage(code int, msg string) {
|
|
122
|
+
h.failWith(code)
|
|
123
|
+
h.t.Setenv("HERDR_FAKE_STDERR", msg)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func (h *fakeHerdr) failWith(code int) { h.t.Setenv("HERDR_FAKE_EXIT", strconv.Itoa(code)) }
|
|
127
|
+
|
|
128
|
+
func (h *fakeHerdr) calls() []call {
|
|
129
|
+
data, err := os.ReadFile(h.log)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
var out []call
|
|
134
|
+
for _, line := range strings.Split(string(data), "\n") {
|
|
135
|
+
if line == "" {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
out = append(out, call{args: strings.Split(strings.TrimSuffix(line, "\t"), "\t")})
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (h *fakeHerdr) waitForCalls(count int) []call {
|
|
144
|
+
h.t.Helper()
|
|
145
|
+
deadline := time.Now().Add(3 * time.Second)
|
|
146
|
+
for {
|
|
147
|
+
now := h.calls()
|
|
148
|
+
if len(now) >= count {
|
|
149
|
+
return now
|
|
150
|
+
}
|
|
151
|
+
if time.Now().After(deadline) {
|
|
152
|
+
h.t.Fatalf("expected %d herdr calls, saw %d: %v", count, len(now), now)
|
|
153
|
+
}
|
|
154
|
+
time.Sleep(10 * time.Millisecond)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// settle gives a call that must not happen time to show up in the log.
|
|
159
|
+
func settle() { time.Sleep(200 * time.Millisecond) }
|
|
160
|
+
|
|
161
|
+
// flag reads `--name value` or `--name=value`.
|
|
162
|
+
func flag(c call, name string) (string, bool) {
|
|
163
|
+
for i, arg := range c.args {
|
|
164
|
+
if arg == name && i+1 < len(c.args) {
|
|
165
|
+
return c.args[i+1], true
|
|
166
|
+
}
|
|
167
|
+
if strings.HasPrefix(arg, name+"=") {
|
|
168
|
+
return strings.TrimPrefix(arg, name+"="), true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return "", false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// resume returns the command after `--`, and whether the call has one.
|
|
175
|
+
func (c call) resume() ([]string, bool) {
|
|
176
|
+
i := slices.Index(c.args, "--")
|
|
177
|
+
if i < 0 {
|
|
178
|
+
return nil, false
|
|
179
|
+
}
|
|
180
|
+
return c.args[i+1:], true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func (c call) verb() string {
|
|
184
|
+
if len(c.args) < 2 {
|
|
185
|
+
return ""
|
|
186
|
+
}
|
|
187
|
+
return c.args[1]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func states(all []call) []string {
|
|
191
|
+
out := make([]string, 0, len(all))
|
|
192
|
+
for _, c := range all {
|
|
193
|
+
s, _ := flag(c, "--state")
|
|
194
|
+
out = append(out, s)
|
|
195
|
+
}
|
|
196
|
+
return out
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func seqs(t *testing.T, all []call) []int64 {
|
|
200
|
+
t.Helper()
|
|
201
|
+
out := make([]int64, 0, len(all))
|
|
202
|
+
for _, c := range all {
|
|
203
|
+
s, _ := flag(c, "--seq")
|
|
204
|
+
n, err := strconv.ParseInt(s, 10, 64)
|
|
205
|
+
if err != nil {
|
|
206
|
+
t.Fatalf("seq %q is not an integer: %v", s, err)
|
|
207
|
+
}
|
|
208
|
+
out = append(out, n)
|
|
209
|
+
}
|
|
210
|
+
return out
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func verbs(all []call) []string {
|
|
214
|
+
out := make([]string, 0, len(all))
|
|
215
|
+
for _, c := range all {
|
|
216
|
+
out = append(out, c.verb())
|
|
217
|
+
}
|
|
218
|
+
return out
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Fake PiG host ────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
type wireEnvelope struct {
|
|
224
|
+
Type string `json:"type"`
|
|
225
|
+
ID string `json:"id,omitempty"`
|
|
226
|
+
Register *wireRegister `json:"register,omitempty"`
|
|
227
|
+
Ready map[string]any `json:"ready,omitempty"`
|
|
228
|
+
Request map[string]any `json:"request,omitempty"`
|
|
229
|
+
Response *wireResponse `json:"response,omitempty"`
|
|
230
|
+
Call *wireCall `json:"call,omitempty"`
|
|
231
|
+
CallResult map[string]any `json:"call_result,omitempty"`
|
|
232
|
+
Shutdown map[string]any `json:"shutdown,omitempty"`
|
|
233
|
+
Raw json.RawMessage `json:"-"`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type wireRegister struct {
|
|
237
|
+
Name string `json:"name"`
|
|
238
|
+
Handlers []struct {
|
|
239
|
+
Event string `json:"event"`
|
|
240
|
+
HandlerID int `json:"handler_id"`
|
|
241
|
+
} `json:"handlers"`
|
|
242
|
+
Tools []json.RawMessage `json:"tools"`
|
|
243
|
+
Commands []json.RawMessage `json:"commands"`
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type wireResponse struct {
|
|
247
|
+
Error *struct {
|
|
248
|
+
Message string `json:"message"`
|
|
249
|
+
} `json:"error"`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type wireCall struct {
|
|
253
|
+
Method string `json:"method"`
|
|
254
|
+
Args json.RawMessage `json:"args"`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// hostState is what the fake host answers for the session getters.
|
|
258
|
+
type hostState struct {
|
|
259
|
+
sessionFile string
|
|
260
|
+
sessionID string
|
|
261
|
+
idle bool
|
|
262
|
+
sessionFailure bool
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type fakeHost struct {
|
|
266
|
+
t *testing.T
|
|
267
|
+
nc net.Conn
|
|
268
|
+
register wireRegister
|
|
269
|
+
runDone chan error
|
|
270
|
+
|
|
271
|
+
writeMu sync.Mutex
|
|
272
|
+
|
|
273
|
+
mu sync.Mutex
|
|
274
|
+
state hostState
|
|
275
|
+
pending map[string]chan *wireResponse
|
|
276
|
+
nextID int
|
|
277
|
+
handlers map[string]int
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// startHost runs the extension against a fake host in the given mode.
|
|
281
|
+
func startHost(t *testing.T, ext *sdk.Extension, mode string) *fakeHost {
|
|
282
|
+
t.Helper()
|
|
283
|
+
hostSide, extSide := net.Pipe()
|
|
284
|
+
h := &fakeHost{
|
|
285
|
+
t: t,
|
|
286
|
+
nc: hostSide,
|
|
287
|
+
runDone: make(chan error, 1),
|
|
288
|
+
pending: map[string]chan *wireResponse{},
|
|
289
|
+
handlers: map[string]int{},
|
|
290
|
+
state: hostState{
|
|
291
|
+
sessionFile: "/home/u/.pig/sessions/s1.jsonl",
|
|
292
|
+
sessionID: "s1",
|
|
293
|
+
idle: true,
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
go func() { h.runDone <- ext.RunWithConn(extSide) }()
|
|
297
|
+
|
|
298
|
+
first := h.readFrame()
|
|
299
|
+
if first.Type != "register" || first.Register == nil {
|
|
300
|
+
t.Fatalf("expected register, got %+v", first)
|
|
301
|
+
}
|
|
302
|
+
h.register = *first.Register
|
|
303
|
+
for _, handler := range h.register.Handlers {
|
|
304
|
+
h.handlers[handler.Event] = handler.HandlerID
|
|
305
|
+
}
|
|
306
|
+
h.write(map[string]any{"type": "ready", "ready": map[string]any{"session_name": "", "cwd": t.TempDir(), "mode": mode, "width": 80}})
|
|
307
|
+
go h.serve()
|
|
308
|
+
t.Cleanup(h.stop)
|
|
309
|
+
return h
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func (h *fakeHost) readFrame() wireEnvelope {
|
|
313
|
+
var hdr [4]byte
|
|
314
|
+
if _, err := io.ReadFull(h.nc, hdr[:]); err != nil {
|
|
315
|
+
h.t.Fatalf("read header: %v", err)
|
|
316
|
+
}
|
|
317
|
+
data := make([]byte, binary.BigEndian.Uint32(hdr[:]))
|
|
318
|
+
if _, err := io.ReadFull(h.nc, data); err != nil {
|
|
319
|
+
h.t.Fatalf("read frame: %v", err)
|
|
320
|
+
}
|
|
321
|
+
var env wireEnvelope
|
|
322
|
+
if err := json.Unmarshal(data, &env); err != nil {
|
|
323
|
+
h.t.Fatalf("decode frame %s: %v", data, err)
|
|
324
|
+
}
|
|
325
|
+
return env
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func (h *fakeHost) write(v any) {
|
|
329
|
+
data, err := json.Marshal(v)
|
|
330
|
+
if err != nil {
|
|
331
|
+
h.t.Errorf("marshal: %v", err)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
var hdr [4]byte
|
|
335
|
+
binary.BigEndian.PutUint32(hdr[:], uint32(len(data)))
|
|
336
|
+
h.writeMu.Lock()
|
|
337
|
+
defer h.writeMu.Unlock()
|
|
338
|
+
if _, err := h.nc.Write(hdr[:]); err != nil {
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
_, _ = h.nc.Write(data)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// serve answers the extension's host calls and routes event responses.
|
|
345
|
+
func (h *fakeHost) serve() {
|
|
346
|
+
for {
|
|
347
|
+
var hdr [4]byte
|
|
348
|
+
if _, err := io.ReadFull(h.nc, hdr[:]); err != nil {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
data := make([]byte, binary.BigEndian.Uint32(hdr[:]))
|
|
352
|
+
if _, err := io.ReadFull(h.nc, data); err != nil {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
var env wireEnvelope
|
|
356
|
+
if json.Unmarshal(data, &env) != nil {
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
switch env.Type {
|
|
360
|
+
case "call":
|
|
361
|
+
go h.answer(env)
|
|
362
|
+
case "response":
|
|
363
|
+
h.mu.Lock()
|
|
364
|
+
ch := h.pending[env.ID]
|
|
365
|
+
delete(h.pending, env.ID)
|
|
366
|
+
h.mu.Unlock()
|
|
367
|
+
if ch != nil {
|
|
368
|
+
ch <- env.Response
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
func (h *fakeHost) answer(env wireEnvelope) {
|
|
375
|
+
h.mu.Lock()
|
|
376
|
+
st := h.state
|
|
377
|
+
h.mu.Unlock()
|
|
378
|
+
result := map[string]any{}
|
|
379
|
+
switch env.Call.Method {
|
|
380
|
+
case "getSessionFile", "getSessionID":
|
|
381
|
+
if st.sessionFailure {
|
|
382
|
+
h.write(map[string]any{"type": "call_result", "id": env.ID, "call_result": map[string]any{"error": map[string]any{"message": "no session"}}})
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if env.Call.Method == "getSessionFile" {
|
|
386
|
+
result["sessionFile"] = st.sessionFile
|
|
387
|
+
} else {
|
|
388
|
+
result["sessionId"] = st.sessionID
|
|
389
|
+
}
|
|
390
|
+
case "isIdle":
|
|
391
|
+
result["idle"] = st.idle
|
|
392
|
+
}
|
|
393
|
+
h.write(map[string]any{"type": "call_result", "id": env.ID, "call_result": map[string]any{"result": result}})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func (h *fakeHost) setState(update func(*hostState)) {
|
|
397
|
+
h.mu.Lock()
|
|
398
|
+
defer h.mu.Unlock()
|
|
399
|
+
update(&h.state)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// fire delivers one event and waits for the extension's response, as PiG's host does.
|
|
403
|
+
// An event the extension did not register for is not delivered.
|
|
404
|
+
func (h *fakeHost) fire(event string, data map[string]any) {
|
|
405
|
+
h.t.Helper()
|
|
406
|
+
h.mu.Lock()
|
|
407
|
+
id, ok := h.handlers[event]
|
|
408
|
+
h.nextID++
|
|
409
|
+
reqID := fmt.Sprintf("r%d", h.nextID)
|
|
410
|
+
ch := make(chan *wireResponse, 1)
|
|
411
|
+
if ok {
|
|
412
|
+
h.pending[reqID] = ch
|
|
413
|
+
}
|
|
414
|
+
h.mu.Unlock()
|
|
415
|
+
if !ok {
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if data == nil {
|
|
419
|
+
data = map[string]any{}
|
|
420
|
+
}
|
|
421
|
+
data["type"] = event
|
|
422
|
+
args, _ := json.Marshal(data)
|
|
423
|
+
h.write(map[string]any{"type": "request", "id": reqID, "request": map[string]any{"method": "event", "event": event, "handler_id": id, "args": json.RawMessage(args)}})
|
|
424
|
+
select {
|
|
425
|
+
case resp := <-ch:
|
|
426
|
+
if resp != nil && resp.Error != nil {
|
|
427
|
+
h.t.Errorf("%s handler failed: %s", event, resp.Error.Message)
|
|
428
|
+
}
|
|
429
|
+
case <-time.After(10 * time.Second):
|
|
430
|
+
h.t.Fatalf("no response to %s within 10s", event)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
func (h *fakeHost) registered(event string) bool {
|
|
435
|
+
h.mu.Lock()
|
|
436
|
+
defer h.mu.Unlock()
|
|
437
|
+
_, ok := h.handlers[event]
|
|
438
|
+
return ok
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func (h *fakeHost) stop() {
|
|
442
|
+
h.write(map[string]any{"type": "shutdown", "shutdown": map[string]any{"reason": "test"}})
|
|
443
|
+
select {
|
|
444
|
+
case <-h.runDone:
|
|
445
|
+
case <-time.After(10 * time.Second):
|
|
446
|
+
h.t.Errorf("extension did not stop after shutdown")
|
|
447
|
+
}
|
|
448
|
+
_ = h.nc.Close()
|
|
449
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package herdr
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"os/exec"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// newCommand builds the herdr command.
|
|
11
|
+
func newCommand(ctx context.Context, bin string, args []string) *exec.Cmd {
|
|
12
|
+
return exec.CommandContext(ctx, bin, args...)
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package herdr
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"syscall"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// newCommand builds the herdr command without a console window.
|
|
12
|
+
func newCommand(ctx context.Context, bin string, args []string) *exec.Cmd {
|
|
13
|
+
cmd := exec.CommandContext(ctx, bin, args...)
|
|
14
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
15
|
+
return cmd
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-in-go/pigpen-herdr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PiG agent-state reporter (Go extension) for the herdr terminal workspace manager",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pig-package",
|
|
7
|
+
"pigpen",
|
|
8
|
+
"extension"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Michael Kinsy",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/MichaelKinsy/pigpen.git",
|
|
15
|
+
"directory": "components/herdr"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/herdr#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/MichaelKinsy/pigpen/issues"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"extensions",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"provenance.json"
|
|
29
|
+
],
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"extensions/herdr"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|