posipaki 0.35.0 → 0.35.2
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/dist/{client-DHI-6XPF.d.ts → client-C4LZE65N.d.ts} +2 -2
- package/dist/{client-DHI-6XPF.d.ts.map → client-C4LZE65N.d.ts.map} +1 -1
- package/dist/hooks-CgXhmY4r.js.map +1 -1
- package/dist/{hooks-CfQ8EhKW.d.ts → hooks-Dj59y_jN.d.ts} +28 -15
- package/dist/hooks-Dj59y_jN.d.ts.map +1 -0
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{client-D23TlXPf.js → json1-DzPx9oX3.js} +47 -3
- package/dist/json1-DzPx9oX3.js.map +1 -0
- package/dist/plugins/debug-logger.d.ts +1 -1
- package/dist/plugins/tree-introspection.d.ts +1 -1
- package/dist/remote/gateway-cli.js +409 -1
- package/dist/remote/gateway-cli.js.map +1 -1
- package/dist/remote/index.d.ts +2 -2
- package/dist/remote/index.js +1 -2
- package/dist/remote/index.js.map +1 -1
- package/dist/remote/node.d.ts +2 -2
- package/dist/remote/node.js +534 -7
- package/dist/remote/node.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/{version-DDAdqTTr.js → version-0mlM8dbC.js} +2 -2
- package/dist/{version-DDAdqTTr.js.map → version-0mlM8dbC.js.map} +1 -1
- package/package.json +1 -1
- package/dist/client-D23TlXPf.js.map +0 -1
- package/dist/gateway-Cj3HSrl3.js +0 -537
- package/dist/gateway-Cj3HSrl3.js.map +0 -1
- package/dist/hooks-CfQ8EhKW.d.ts.map +0 -1
- package/dist/json1-DS3xxg13.js +0 -48
- package/dist/json1-DS3xxg13.js.map +0 -1
- package/dist/remote/gateway-cli.d.ts +0 -1
package/dist/remote/node.js
CHANGED
|
@@ -1,13 +1,164 @@
|
|
|
1
|
-
import { t as LIB_VERSION } from "../version-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { execSync, spawn } from "node:child_process";
|
|
1
|
+
import { t as LIB_VERSION } from "../version-0mlM8dbC.js";
|
|
2
|
+
import { a as remoteClient, d as isProto, i as json1Channel, s as serveRemoteActor, t as VERSION } from "../json1-DzPx9oX3.js";
|
|
3
|
+
import { mkdtemp, open, readFile, rm, unlink } from "node:fs/promises";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import { execFile, execSync, spawn } from "node:child_process";
|
|
7
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
7
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { basename, join } from "node:path";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
//#region src/remote/transports/fifo.ts
|
|
13
|
+
var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.pvtOnMessage = null;
|
|
16
|
+
this.pvtOnClose = null;
|
|
17
|
+
this.closed = false;
|
|
18
|
+
this.closingPromise = null;
|
|
19
|
+
this.pvtError = null;
|
|
20
|
+
this.readFd = opts.readFd ?? null;
|
|
21
|
+
this.writeFd = opts.writeFd ?? null;
|
|
22
|
+
if (this.readFd) {
|
|
23
|
+
this.rs = this.readFd.createReadStream({ encoding: "utf-8" });
|
|
24
|
+
this.rl = readline.createInterface({ input: this.rs });
|
|
25
|
+
this.rl.on("line", (line) => {
|
|
26
|
+
if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);
|
|
27
|
+
});
|
|
28
|
+
this.rl.on("close", () => {
|
|
29
|
+
this.pvtOnClose?.();
|
|
30
|
+
this.close();
|
|
31
|
+
});
|
|
32
|
+
this.rs.on("error", (err) => {
|
|
33
|
+
this.pvtError = err;
|
|
34
|
+
this.close();
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
this.rs = null;
|
|
38
|
+
this.rl = null;
|
|
39
|
+
}
|
|
40
|
+
if (this.writeFd) {
|
|
41
|
+
this.ws = this.writeFd.createWriteStream({ encoding: "utf-8" });
|
|
42
|
+
this.ws.on("error", (err) => {
|
|
43
|
+
this.pvtError = err;
|
|
44
|
+
this.close();
|
|
45
|
+
});
|
|
46
|
+
} else this.ws = null;
|
|
47
|
+
}
|
|
48
|
+
static openReaderFd(readFd) {
|
|
49
|
+
return new FifoUtf8NlineTransport({ readFd });
|
|
50
|
+
}
|
|
51
|
+
static openWriterFd(writeFd) {
|
|
52
|
+
return new FifoUtf8NlineTransport({ writeFd });
|
|
53
|
+
}
|
|
54
|
+
static fromFds(readFd, writeFd) {
|
|
55
|
+
return new FifoUtf8NlineTransport({
|
|
56
|
+
readFd,
|
|
57
|
+
writeFd
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Start opening a bidirectional connection. Opens readPath for reading
|
|
62
|
+
* in the background, returns a promise. The caller should do whatever
|
|
63
|
+
* setup is needed to unblock the read (e.g. spawn a process that opens
|
|
64
|
+
* readPath for writing), then await the returned transport.
|
|
65
|
+
*
|
|
66
|
+
* The transport itself is built only once both directions are open. A read
|
|
67
|
+
* stream created while no writer exists on its fifo is handed an immediate
|
|
68
|
+
* end-of-stream by some runtimes (bun), which closes a channel that has not
|
|
69
|
+
* carried a byte yet — so the writes side is opened before the reader exists.
|
|
70
|
+
*/
|
|
71
|
+
static beginConnect(readPath, writePath) {
|
|
72
|
+
return { transport: open(readPath, "r").then(async (readFd) => {
|
|
73
|
+
const writeFd = await open(writePath, "w");
|
|
74
|
+
return FifoUtf8NlineTransport.fromFds(readFd, writeFd);
|
|
75
|
+
}) };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Open a bidirectional connection. Opens writePath for writing first
|
|
79
|
+
* (unblocking the peer's read), starts reading readPath in the
|
|
80
|
+
* background, then awaits both.
|
|
81
|
+
*
|
|
82
|
+
* Use this when you are the side that responds to the peer's beginConnect
|
|
83
|
+
* (i.e. you don't need to interleave any setup between the read and write
|
|
84
|
+
* opens). Built like `beginConnect`: the transport exists only after both
|
|
85
|
+
* directions do.
|
|
86
|
+
*/
|
|
87
|
+
static async connect(readPath, writePath) {
|
|
88
|
+
const readFdPromise = open(readPath, "r");
|
|
89
|
+
const writeFd = await open(writePath, "w");
|
|
90
|
+
const readFd = await readFdPromise;
|
|
91
|
+
return FifoUtf8NlineTransport.fromFds(readFd, writeFd);
|
|
92
|
+
}
|
|
93
|
+
get canSend() {
|
|
94
|
+
return this.writeFd !== null && !this.closed;
|
|
95
|
+
}
|
|
96
|
+
onMessage(handler) {
|
|
97
|
+
if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
|
|
98
|
+
if (this.readFd === null) throw new Error("FifoUtf8NlineTransport: not a reader");
|
|
99
|
+
if (this.pvtOnMessage !== null) throw new Error("FifoUtf8NlineTransport: handler already set — call removeHandler() first");
|
|
100
|
+
this.pvtOnMessage = handler;
|
|
101
|
+
}
|
|
102
|
+
removeHandler() {
|
|
103
|
+
const prev = this.pvtOnMessage;
|
|
104
|
+
this.pvtOnMessage = null;
|
|
105
|
+
return prev;
|
|
106
|
+
}
|
|
107
|
+
onClose(handler) {
|
|
108
|
+
this.pvtOnClose = handler;
|
|
109
|
+
}
|
|
110
|
+
get hasHandler() {
|
|
111
|
+
return this.pvtOnMessage !== null;
|
|
112
|
+
}
|
|
113
|
+
get lastError() {
|
|
114
|
+
return this.pvtError;
|
|
115
|
+
}
|
|
116
|
+
async send(line) {
|
|
117
|
+
if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
|
|
118
|
+
if (this.writeFd === null) throw new Error("FifoUtf8NlineTransport: not a writer");
|
|
119
|
+
if (!line.endsWith("\n")) line += "\n";
|
|
120
|
+
await this.ws?.write(line);
|
|
121
|
+
}
|
|
122
|
+
async close() {
|
|
123
|
+
this.closingPromise = this.closingPromise || this.pvtClose();
|
|
124
|
+
await this.closingPromise;
|
|
125
|
+
this.closed = true;
|
|
126
|
+
}
|
|
127
|
+
async pvtClose() {
|
|
128
|
+
await Promise.resolve();
|
|
129
|
+
if (this.rl) {
|
|
130
|
+
this.rl.close();
|
|
131
|
+
this.rl = null;
|
|
132
|
+
}
|
|
133
|
+
if (this.rs) {
|
|
134
|
+
this.rs.destroy();
|
|
135
|
+
this.rs = null;
|
|
136
|
+
}
|
|
137
|
+
if (this.ws) {
|
|
138
|
+
await new Promise((resolve) => {
|
|
139
|
+
try {
|
|
140
|
+
this.ws?.end("", resolve);
|
|
141
|
+
} catch {
|
|
142
|
+
resolve();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this.ws = null;
|
|
146
|
+
}
|
|
147
|
+
if (this.readFd) {
|
|
148
|
+
try {
|
|
149
|
+
await this.readFd.close();
|
|
150
|
+
} catch {}
|
|
151
|
+
this.readFd = null;
|
|
152
|
+
}
|
|
153
|
+
if (this.writeFd) {
|
|
154
|
+
try {
|
|
155
|
+
await this.writeFd.close();
|
|
156
|
+
} catch {}
|
|
157
|
+
this.writeFd = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
11
162
|
//#region src/remote/spawners/fifo-command.ts
|
|
12
163
|
function commandSpawner(command) {
|
|
13
164
|
return async () => {
|
|
@@ -333,6 +484,382 @@ function defineSubprocessActor(actor, url, opts = {}) {
|
|
|
333
484
|
};
|
|
334
485
|
}
|
|
335
486
|
//#endregion
|
|
487
|
+
//#region src/remote/stdio.ts
|
|
488
|
+
/** How long to wait for the far end's first protocol frame. */
|
|
489
|
+
const HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
490
|
+
const OUTPUT_KEY = "$fd";
|
|
491
|
+
const ERROR_KEY = "$error";
|
|
492
|
+
/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */
|
|
493
|
+
var StdioWireError = class extends Error {
|
|
494
|
+
constructor(message) {
|
|
495
|
+
super(message);
|
|
496
|
+
this.name = "StdioWireError";
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function outputFrame(fd, data) {
|
|
500
|
+
return JSON.stringify({
|
|
501
|
+
[OUTPUT_KEY]: fd,
|
|
502
|
+
data
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/** The output frame a line carries, or null when it carries something else. */
|
|
506
|
+
function parseOutputFrame(line) {
|
|
507
|
+
if (!line.startsWith("{\"$fd\"")) return null;
|
|
508
|
+
let parsed;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(line);
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
515
|
+
const frame = parsed;
|
|
516
|
+
const fd = frame[OUTPUT_KEY];
|
|
517
|
+
const data = frame.data;
|
|
518
|
+
if (fd !== 1 && fd !== 2) return null;
|
|
519
|
+
return {
|
|
520
|
+
fd,
|
|
521
|
+
data: typeof data === "string" ? data : JSON.stringify(data)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function errorFrame(reason) {
|
|
525
|
+
return JSON.stringify({ [ERROR_KEY]: reason });
|
|
526
|
+
}
|
|
527
|
+
/** The reason an `$error` frame carries, or null when it is not one. */
|
|
528
|
+
function errorReason(frame) {
|
|
529
|
+
const reason = frame[ERROR_KEY];
|
|
530
|
+
return typeof reason === "string" ? reason : null;
|
|
531
|
+
}
|
|
532
|
+
/** Is this write error just a peer that already left? */
|
|
533
|
+
function peerGone(code) {
|
|
534
|
+
return code === "EPIPE" || code === "ERR_STREAM_DESTROYED" || code === "ERR_STREAM_WRITE_AFTER_END" || code === "ERR_STREAM_ALREADY_FINISHED";
|
|
535
|
+
}
|
|
536
|
+
function errorText$2(err) {
|
|
537
|
+
return err instanceof Error ? err.message : String(err);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* A `StringTransport` over a read/write stream pair, one JSON frame per line.
|
|
541
|
+
*
|
|
542
|
+
* `close()` is idempotent and never throws: teardown races are normal here —
|
|
543
|
+
* the peer exits, the read side ends, and a write arrives a moment later.
|
|
544
|
+
*/
|
|
545
|
+
var LineTransport = class {
|
|
546
|
+
constructor(streams) {
|
|
547
|
+
this.pvtOnMessage = null;
|
|
548
|
+
this.pvtOnClose = null;
|
|
549
|
+
this.pvtClosed = false;
|
|
550
|
+
this.pvtClosing = null;
|
|
551
|
+
this.streams = streams;
|
|
552
|
+
const lines = readline.createInterface({ input: streams.read });
|
|
553
|
+
lines.on("line", (line) => {
|
|
554
|
+
if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);
|
|
555
|
+
});
|
|
556
|
+
lines.on("close", () => {
|
|
557
|
+
this.pvtOnClose?.();
|
|
558
|
+
this.close();
|
|
559
|
+
});
|
|
560
|
+
streams.read.on("error", () => {
|
|
561
|
+
this.close();
|
|
562
|
+
});
|
|
563
|
+
streams.write.on("error", () => {
|
|
564
|
+
this.close();
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
get closed() {
|
|
568
|
+
return this.pvtClosed;
|
|
569
|
+
}
|
|
570
|
+
get hasHandler() {
|
|
571
|
+
return this.pvtOnMessage !== null;
|
|
572
|
+
}
|
|
573
|
+
onMessage(handler) {
|
|
574
|
+
if (this.pvtClosed) throw new StdioWireError("stdio transport: closed");
|
|
575
|
+
if (this.pvtOnMessage !== null) throw new StdioWireError("stdio transport: handler already set — call removeHandler()");
|
|
576
|
+
this.pvtOnMessage = handler;
|
|
577
|
+
}
|
|
578
|
+
removeHandler() {
|
|
579
|
+
const previous = this.pvtOnMessage;
|
|
580
|
+
this.pvtOnMessage = null;
|
|
581
|
+
return previous;
|
|
582
|
+
}
|
|
583
|
+
onClose(handler) {
|
|
584
|
+
this.pvtOnClose = handler;
|
|
585
|
+
}
|
|
586
|
+
send(frame) {
|
|
587
|
+
if (this.pvtClosed) return Promise.resolve();
|
|
588
|
+
const line = frame.endsWith("\n") ? frame : `${frame}\n`;
|
|
589
|
+
return new Promise((resolve, reject) => {
|
|
590
|
+
try {
|
|
591
|
+
this.streams.write.write(line, (err) => {
|
|
592
|
+
if (!err) return resolve();
|
|
593
|
+
const code = err.code;
|
|
594
|
+
if (peerGone(code)) return resolve();
|
|
595
|
+
reject(new StdioWireError(`stdio transport: ${err.message}`));
|
|
596
|
+
});
|
|
597
|
+
} catch (err) {
|
|
598
|
+
reject(new StdioWireError(`stdio transport: ${errorText$2(err)}`));
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async close() {
|
|
603
|
+
this.pvtClosing ??= this.pvtClose();
|
|
604
|
+
await this.pvtClosing;
|
|
605
|
+
}
|
|
606
|
+
async pvtClose() {
|
|
607
|
+
await Promise.resolve();
|
|
608
|
+
this.pvtClosed = true;
|
|
609
|
+
try {
|
|
610
|
+
this.streams.read.destroy?.();
|
|
611
|
+
} catch {}
|
|
612
|
+
try {
|
|
613
|
+
if (!this.streams.write.writableEnded) this.streams.write.end();
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
/** Streams over two fds of *our own* process — the child side of an fd wire. */
|
|
618
|
+
function fdStreams(readFd, writeFd) {
|
|
619
|
+
return {
|
|
620
|
+
read: createReadStream("", {
|
|
621
|
+
fd: readFd,
|
|
622
|
+
encoding: "utf-8",
|
|
623
|
+
autoClose: false
|
|
624
|
+
}),
|
|
625
|
+
write: createWriteStream("", {
|
|
626
|
+
fd: writeFd,
|
|
627
|
+
encoding: "utf-8",
|
|
628
|
+
autoClose: false
|
|
629
|
+
})
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A transport that lifts output frames out of the stream: everything else is
|
|
634
|
+
* passed through untouched, so the protocol sees frames and only frames.
|
|
635
|
+
*/
|
|
636
|
+
var OutputFilter = class {
|
|
637
|
+
constructor(inner, sink) {
|
|
638
|
+
this.inner = inner;
|
|
639
|
+
this.sink = sink;
|
|
640
|
+
}
|
|
641
|
+
send(frame) {
|
|
642
|
+
return this.inner.send(frame);
|
|
643
|
+
}
|
|
644
|
+
onMessage(handler) {
|
|
645
|
+
this.inner.onMessage((line) => {
|
|
646
|
+
const output = parseOutputFrame(line);
|
|
647
|
+
if (output) this.sink(output.fd, output.data);
|
|
648
|
+
else handler(line);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
removeHandler() {
|
|
652
|
+
this.inner.removeHandler();
|
|
653
|
+
}
|
|
654
|
+
onClose(handler) {
|
|
655
|
+
this.inner.onClose(handler);
|
|
656
|
+
}
|
|
657
|
+
close() {
|
|
658
|
+
return this.inner.close();
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
/** Write the far end's output to our stderr, tagged with its name. */
|
|
662
|
+
function stderrSink(name = "") {
|
|
663
|
+
const tag = name ? `[${name}] ` : "";
|
|
664
|
+
return (fd, data) => {
|
|
665
|
+
const lead = fd === 1 ? tag : `${tag}err: `;
|
|
666
|
+
for (const line of data.split("\n")) if (line.length > 0) process.stderr.write(`${lead}${line}\n`);
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/** The next frame that arrives, or a timeout. */
|
|
670
|
+
async function nextFrame(channel, timeoutMs) {
|
|
671
|
+
return await new Promise((resolve, reject) => {
|
|
672
|
+
const timer = setTimeout(() => reject(new StdioWireError(`no protocol frame within ${timeoutMs}ms`)), timeoutMs);
|
|
673
|
+
timer.unref?.();
|
|
674
|
+
channel.onMessage((frame) => {
|
|
675
|
+
clearTimeout(timer);
|
|
676
|
+
resolve(frame);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The client end of the wire: hand the streams to the seam (minus output
|
|
682
|
+
* frames) and wait for the peer's `$proto`. A first stage reports a failed
|
|
683
|
+
* start as an `$error` frame instead.
|
|
684
|
+
*/
|
|
685
|
+
async function clientChannel(streams, opts = {}) {
|
|
686
|
+
const transport = new LineTransport(streams);
|
|
687
|
+
const channel = json1Channel(new OutputFilter(transport, opts.onOutput ?? stderrSink()));
|
|
688
|
+
const frame = await nextFrame(channel, opts.timeoutMs ?? 15e3);
|
|
689
|
+
channel.removeHandler();
|
|
690
|
+
const reason = errorReason(frame);
|
|
691
|
+
if (reason) throw new StdioWireError(reason);
|
|
692
|
+
if (!isProto(frame)) throw new StdioWireError(`unexpected first frame: ${JSON.stringify(frame).slice(0, 120)}`);
|
|
693
|
+
if (frame.$proto !== "json.v1") throw new StdioWireError(`unsupported protocol ${String(frame.$proto)}, expected ${VERSION}`);
|
|
694
|
+
return channel;
|
|
695
|
+
}
|
|
696
|
+
/** The server end of the wire — a payload speaking stdio itself. */
|
|
697
|
+
async function serverChannel(streams) {
|
|
698
|
+
const channel = json1Channel(new LineTransport(streams));
|
|
699
|
+
await channel.send({ $proto: VERSION });
|
|
700
|
+
return channel;
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/remote/gateway.ts
|
|
704
|
+
const run = promisify(execFile);
|
|
705
|
+
/** Exit code for "this environment could not be set up". */
|
|
706
|
+
const GATEWAY_FAILED = 1;
|
|
707
|
+
function errorText$1(err) {
|
|
708
|
+
return err instanceof Error ? err.message : String(err);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* A label for the gateway's own temp directory: the program this environment was made for.
|
|
712
|
+
* It is the gateway's business alone — the payload is not told which label it is.
|
|
713
|
+
*/
|
|
714
|
+
function label(worker) {
|
|
715
|
+
const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, "-").slice(0, 60);
|
|
716
|
+
return name === "" ? "payload" : name;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The argv that boots this gateway — built by the client, read by
|
|
720
|
+
* {@link gatewayBoot}, so the two halves cannot drift apart.
|
|
721
|
+
*
|
|
722
|
+
* The payload comes first and its arguments after it, so a positional argument of the
|
|
723
|
+
* payload's own can never be mistaken for the payload.
|
|
724
|
+
*/
|
|
725
|
+
function gatewayArgs(boot) {
|
|
726
|
+
return [boot.worker, ...boot.passthrough];
|
|
727
|
+
}
|
|
728
|
+
/** The boot a gateway was started with, or the reason it cannot start. */
|
|
729
|
+
function gatewayBoot(argv) {
|
|
730
|
+
const [worker, ...passthrough] = argv;
|
|
731
|
+
if (worker === void 0 || worker.startsWith("--")) throw new Error("no <payload>: the gateway relays to a payload, named as its first argument");
|
|
732
|
+
return {
|
|
733
|
+
passthrough,
|
|
734
|
+
worker
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* End a start that failed. The reason has already gone out on the wire, and
|
|
739
|
+
* returning is not enough: the `open()` on a fifo no payload will ever write sits
|
|
740
|
+
* in the threadpool forever, so the loop never drains and the gateway would linger
|
|
741
|
+
* with a dead payload on its books.
|
|
742
|
+
*/
|
|
743
|
+
function abandon() {
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
/** Carry a failed start to the client, then let the caller exit non-zero. */
|
|
747
|
+
async function fail(wire, reason) {
|
|
748
|
+
process.stderr.write(`gateway: ${reason}\n`);
|
|
749
|
+
try {
|
|
750
|
+
await wire.send(errorFrame(reason));
|
|
751
|
+
} catch {}
|
|
752
|
+
}
|
|
753
|
+
/** Copy everything the payload prints onto the wire as output frames. */
|
|
754
|
+
function forwardOutput(stream, wire, fd) {
|
|
755
|
+
stream.setEncoding("utf-8");
|
|
756
|
+
stream.on("data", (chunk) => {
|
|
757
|
+
wire.send(outputFrame(fd, chunk)).catch(() => {});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Serve one environment over stdin/stdout. Returns the exit code; the caller
|
|
762
|
+
* turns it into `process.exitCode`.
|
|
763
|
+
*/
|
|
764
|
+
async function runGateway(argv = process.argv.slice(2)) {
|
|
765
|
+
const wire = new LineTransport({
|
|
766
|
+
read: process.stdin,
|
|
767
|
+
write: process.stdout
|
|
768
|
+
});
|
|
769
|
+
let booted;
|
|
770
|
+
try {
|
|
771
|
+
booted = gatewayBoot(argv);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
await fail(wire, errorText$1(err));
|
|
774
|
+
abandon();
|
|
775
|
+
}
|
|
776
|
+
const { passthrough, worker } = booted;
|
|
777
|
+
const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));
|
|
778
|
+
const fifoIn = join(dir, "in");
|
|
779
|
+
const fifoOut = join(dir, "out");
|
|
780
|
+
let child = null;
|
|
781
|
+
let fifo = null;
|
|
782
|
+
/**
|
|
783
|
+
* Everything this gateway owns, undone. All of it is idempotent, because the
|
|
784
|
+
* exit path and the signal path can both arrive.
|
|
785
|
+
*/
|
|
786
|
+
const teardown = async () => {
|
|
787
|
+
child?.kill();
|
|
788
|
+
await fifo?.close().catch(() => {});
|
|
789
|
+
await rm(dir, {
|
|
790
|
+
recursive: true,
|
|
791
|
+
force: true
|
|
792
|
+
}).catch(() => {});
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* A client that gives up stops us with a signal, and a gateway that died on the
|
|
796
|
+
* spot would leave its payload behind in the environment — a process holding a
|
|
797
|
+
* fifo nobody will ever write, which is a leak the environment cannot clean up
|
|
798
|
+
* for us. So the signals do what the exit path does.
|
|
799
|
+
*/
|
|
800
|
+
const onTerminate = () => {
|
|
801
|
+
teardown().catch(() => {}).then(() => process.exit(1));
|
|
802
|
+
};
|
|
803
|
+
for (const signal of [
|
|
804
|
+
"SIGTERM",
|
|
805
|
+
"SIGINT",
|
|
806
|
+
"SIGHUP"
|
|
807
|
+
]) process.on(signal, onTerminate);
|
|
808
|
+
try {
|
|
809
|
+
await run("mkfifo", [
|
|
810
|
+
"-m",
|
|
811
|
+
"600",
|
|
812
|
+
fifoIn,
|
|
813
|
+
fifoOut
|
|
814
|
+
]);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
await fail(wire, `cannot create the environment's fifos: ${errorText$1(err)}`);
|
|
817
|
+
await teardown();
|
|
818
|
+
abandon();
|
|
819
|
+
}
|
|
820
|
+
const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);
|
|
821
|
+
const workerArgs = [
|
|
822
|
+
worker,
|
|
823
|
+
...passthrough,
|
|
824
|
+
`--fifo-in=${fifoIn}`,
|
|
825
|
+
`--fifo-out=${fifoOut}`
|
|
826
|
+
];
|
|
827
|
+
const workerProc = spawn(workerArgs[0], workerArgs.slice(1), { stdio: [
|
|
828
|
+
"ignore",
|
|
829
|
+
"pipe",
|
|
830
|
+
"pipe"
|
|
831
|
+
] });
|
|
832
|
+
child = workerProc;
|
|
833
|
+
forwardOutput(workerProc.stdout, wire, 1);
|
|
834
|
+
forwardOutput(workerProc.stderr, wire, 2);
|
|
835
|
+
const exited = new Promise((settle) => {
|
|
836
|
+
workerProc.once("exit", (code) => settle(code ?? 0));
|
|
837
|
+
});
|
|
838
|
+
try {
|
|
839
|
+
fifo = await Promise.race([connection.transport, exited.then((code) => {
|
|
840
|
+
throw new Error(`the payload exited with code ${code} before it opened its channel`);
|
|
841
|
+
})]);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
await fail(wire, errorText$1(err));
|
|
844
|
+
await teardown();
|
|
845
|
+
abandon();
|
|
846
|
+
}
|
|
847
|
+
const relay = fifo;
|
|
848
|
+
relay.onMessage((line) => {
|
|
849
|
+
wire.send(line).catch(() => {});
|
|
850
|
+
});
|
|
851
|
+
wire.onMessage((line) => {
|
|
852
|
+
relay.send(line).catch(() => {});
|
|
853
|
+
});
|
|
854
|
+
wire.onClose(() => {
|
|
855
|
+
child?.kill();
|
|
856
|
+
});
|
|
857
|
+
const code = await exited;
|
|
858
|
+
await teardown();
|
|
859
|
+
await wire.close();
|
|
860
|
+
return code;
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
336
863
|
//#region src/remote/host.ts
|
|
337
864
|
/** The real thing: spawn it, feed it, collect it. */
|
|
338
865
|
const runHost = (command, stdin) => new Promise((settle, fail) => {
|