skydive-cli 0.5.0-beta.8 → 0.5.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/CHANGELOG.md +13 -0
- package/README.md +61 -13
- package/dist/js/api-BFQ4PQDA.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +674 -307
- package/dist/js/{boot-BCClEpCM.mjs → boot-DD4T-61U.mjs} +4558 -930
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
- package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-Ct0-JZSS.mjs +5 -0
- package/dist/js/daemon-CCgNLD0H.mjs +7 -0
- package/dist/js/{daemon-k2kVkJ8D.mjs → daemon-Do1jU2UF.mjs} +123 -43
- package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
- package/dist/js/{daemon-client-fxf1A25Z.mjs → daemon-client-Dvad009G.mjs} +1 -1
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-C-f04uyE.mjs +208 -0
- package/dist/js/{profiler-DawY0V0Z.mjs → install-CtAVvERm.mjs} +545 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
- package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
- package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
- package/dist/js/rest-Dc0EEok3.mjs +6 -0
- package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
- package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
- package/package.json +12 -4
- package/dist/js/api-DG5W6iwx.mjs +0 -131
- package/dist/js/client-BuU34IVE.mjs +0 -5
- package/dist/js/daemon-BxU59xie.mjs +0 -6
- package/dist/js/daemon-client-C2BvZgKO.mjs +0 -7
- package/dist/js/forward-18QoL5dO.mjs +0 -68
- package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
- package/dist/js/rest-D29qNkto.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
|
@@ -1,6 +1,108 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
|
|
4
|
+
//#region ../sandbox-stream-protocol/src/fs.ts
|
|
5
|
+
/** fs frame type bytes. Disjoint from FRAME.* in ./index.ts. */
|
|
6
|
+
const FS_FRAME = {
|
|
7
|
+
REQ: 32,
|
|
8
|
+
RES: 33
|
|
9
|
+
};
|
|
10
|
+
/** Filesystem operations the channel supports. One byte on the wire. */
|
|
11
|
+
const FS_OP = {
|
|
12
|
+
LIST: 1,
|
|
13
|
+
STAT: 2,
|
|
14
|
+
READ: 3,
|
|
15
|
+
WRITE: 4,
|
|
16
|
+
MKDIR: 5,
|
|
17
|
+
RENAME: 6,
|
|
18
|
+
REMOVE: 7,
|
|
19
|
+
EXISTS: 8
|
|
20
|
+
};
|
|
21
|
+
/** Reply status. OK carries a result; ERR carries a message in `json.message`. */
|
|
22
|
+
const FS_STATUS = {
|
|
23
|
+
OK: 0,
|
|
24
|
+
ERR: 1
|
|
25
|
+
};
|
|
26
|
+
const FS_MAX_BLOB_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
const OP_TO_CODE = {
|
|
28
|
+
list: FS_OP.LIST,
|
|
29
|
+
stat: FS_OP.STAT,
|
|
30
|
+
read: FS_OP.READ,
|
|
31
|
+
write: FS_OP.WRITE,
|
|
32
|
+
mkdir: FS_OP.MKDIR,
|
|
33
|
+
rename: FS_OP.RENAME,
|
|
34
|
+
remove: FS_OP.REMOVE,
|
|
35
|
+
exists: FS_OP.EXISTS
|
|
36
|
+
};
|
|
37
|
+
const CODE_TO_OP = new Map(Object.entries(OP_TO_CODE).map(([op, code]) => [code, op]));
|
|
38
|
+
const textEncoder = new TextEncoder();
|
|
39
|
+
const textDecoder = new TextDecoder();
|
|
40
|
+
function frame(type, reqId, byte2, json, blob) {
|
|
41
|
+
const jsonBytes = textEncoder.encode(JSON.stringify(json ?? {}));
|
|
42
|
+
const out = new Uint8Array(10 + jsonBytes.length + blob.length);
|
|
43
|
+
const dv = new DataView(out.buffer);
|
|
44
|
+
out[0] = type;
|
|
45
|
+
dv.setUint32(1, reqId >>> 0);
|
|
46
|
+
out[5] = byte2;
|
|
47
|
+
dv.setUint32(6, jsonBytes.length);
|
|
48
|
+
out.set(jsonBytes, 10);
|
|
49
|
+
out.set(blob, 10 + jsonBytes.length);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
const EMPTY = new Uint8Array(0);
|
|
53
|
+
/** client → server: encode an fs request. `blob` is the write payload, or null. */
|
|
54
|
+
function encodeFsRequest(reqId, req, blob) {
|
|
55
|
+
return frame(FS_FRAME.REQ, reqId, OP_TO_CODE[req.op], req, blob ?? EMPTY);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decode a server fs reply frame. Returns null for a malformed frame so a peer
|
|
59
|
+
* on a newer protocol can't crash the client.
|
|
60
|
+
*/
|
|
61
|
+
function decodeFsResponse(frameBytes) {
|
|
62
|
+
const parsed = parseFrame(FS_FRAME.RES, frameBytes);
|
|
63
|
+
if (!parsed) return null;
|
|
64
|
+
if (parsed.byte2 === FS_STATUS.ERR) {
|
|
65
|
+
const message = typeof parsed.json.message === "string" ? parsed.json.message : "fs operation failed";
|
|
66
|
+
return {
|
|
67
|
+
reqId: parsed.reqId,
|
|
68
|
+
status: "error",
|
|
69
|
+
message
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
reqId: parsed.reqId,
|
|
74
|
+
status: "ok",
|
|
75
|
+
result: parsed.json,
|
|
76
|
+
blob: parsed.blob
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function parseFrame(expectedType, frameBytes) {
|
|
80
|
+
if (frameBytes.length < 10) return null;
|
|
81
|
+
if (frameBytes[0] !== expectedType) return null;
|
|
82
|
+
const dv = new DataView(frameBytes.buffer, frameBytes.byteOffset, frameBytes.byteLength);
|
|
83
|
+
const reqId = dv.getUint32(1);
|
|
84
|
+
const byte2 = frameBytes[5] ?? 0;
|
|
85
|
+
const jsonLen = dv.getUint32(6);
|
|
86
|
+
const jsonStart = 10;
|
|
87
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
88
|
+
if (jsonEnd > frameBytes.length) return null;
|
|
89
|
+
let json;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = jsonLen ? JSON.parse(textDecoder.decode(frameBytes.subarray(jsonStart, jsonEnd))) : {};
|
|
92
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
93
|
+
json = parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
reqId,
|
|
99
|
+
byte2,
|
|
100
|
+
json,
|
|
101
|
+
blob: frameBytes.subarray(jsonEnd)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
4
106
|
//#region ../sandbox-stream-protocol/src/index.ts
|
|
5
107
|
const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
|
|
6
108
|
const FRAME = {
|
|
@@ -19,11 +121,15 @@ function streamSpecToQuery(spec) {
|
|
|
19
121
|
cols: String(spec.cols),
|
|
20
122
|
rows: String(spec.rows)
|
|
21
123
|
};
|
|
22
|
-
return {
|
|
124
|
+
if (spec.mode === "exec") return {
|
|
23
125
|
agentId: spec.agentId,
|
|
24
126
|
mode: "exec",
|
|
25
127
|
command: spec.command
|
|
26
128
|
};
|
|
129
|
+
return {
|
|
130
|
+
agentId: spec.agentId,
|
|
131
|
+
mode: "fs"
|
|
132
|
+
};
|
|
27
133
|
}
|
|
28
134
|
function withType(type, payload) {
|
|
29
135
|
const frame = new Uint8Array(1 + payload.length);
|
|
@@ -166,4 +272,4 @@ function toBuffer(data) {
|
|
|
166
272
|
}
|
|
167
273
|
|
|
168
274
|
//#endregion
|
|
169
|
-
export { SandboxStream as t };
|
|
275
|
+
export { encodeFsRequest as a, decodeFsResponse as i, SANDBOX_STREAM_PATH as n, streamSpecToQuery as r, SandboxStream as t };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./client--k9cjfkX.mjs";
|
|
3
|
+
import "./tls-cert-Rua2oV7n.mjs";
|
|
4
|
+
import "./api-BFQ4PQDA.mjs";
|
|
5
|
+
import { a as runPortalDaemon, c as stopDaemonForHandoff, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-Do1jU2UF.mjs";
|
|
6
|
+
|
|
7
|
+
export { runPortalDaemon };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { i as isRecord, n as isNewerBuild, r as portalDaemonBuild, t as PortalClient } from "./client--k9cjfkX.mjs";
|
|
3
|
+
import { t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { z } from "zod";
|
|
@@ -8,43 +9,6 @@ import { createHash } from "node:crypto";
|
|
|
8
9
|
import { connect, createServer } from "node:net";
|
|
9
10
|
import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
11
|
|
|
11
|
-
//#region ../portal-daemon/src/build-stamp.ts
|
|
12
|
-
/**
|
|
13
|
-
* This build's stamp: the unix commit time (seconds) of the source it was
|
|
14
|
-
* compiled from. Empty string when unstamped (a dev source run without the
|
|
15
|
-
* env override).
|
|
16
|
-
*
|
|
17
|
-
* Why a commit time and not a package version: the daemon ships inside two
|
|
18
|
-
* independently-versioned installers (the skydive CLI and the desktop app),
|
|
19
|
-
* so their semvers are not comparable — but both build from this repo, so
|
|
20
|
-
* the commit time of the built tree is one monotonic clock they share.
|
|
21
|
-
*/
|
|
22
|
-
function portalDaemonBuild() {
|
|
23
|
-
return "1786596877";
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Whether a client carrying `mine` should replace a running daemon carrying
|
|
27
|
-
* `theirs` (newest build wins):
|
|
28
|
-
*
|
|
29
|
-
* - an unstamped client never takes over — it can't prove it's newer;
|
|
30
|
-
* - a stamped client replaces an unstamped daemon — every stamped build
|
|
31
|
-
* postdates stamping, so the unstamped daemon is older by construction;
|
|
32
|
-
* - otherwise strictly greater wins; equal keeps the incumbent, so two
|
|
33
|
-
* identical builds never bounce the daemon between them.
|
|
34
|
-
*/
|
|
35
|
-
function isNewerBuild(mine, theirs) {
|
|
36
|
-
const mineAt = parseStamp(mine);
|
|
37
|
-
if (mineAt === null) return false;
|
|
38
|
-
const theirsAt = parseStamp(theirs);
|
|
39
|
-
if (theirsAt === null) return true;
|
|
40
|
-
return mineAt > theirsAt;
|
|
41
|
-
}
|
|
42
|
-
function parseStamp(stamp) {
|
|
43
|
-
if (!/^[0-9]+$/.test(stamp)) return null;
|
|
44
|
-
return Number(stamp);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
12
|
//#region ../portal-daemon/src/local-protocol.ts
|
|
49
13
|
/**
|
|
50
14
|
* Local IPC between the portal DAEMON and the `skydive` CLI processes attached
|
|
@@ -219,6 +183,20 @@ const clientStatusSchema = z.object({ t: z.literal("status") });
|
|
|
219
183
|
* exits — cutting live exec/tunnel connections, not just refusing new ones.
|
|
220
184
|
*/
|
|
221
185
|
const clientShutdownSchema = z.object({ t: z.literal("shutdown") });
|
|
186
|
+
/**
|
|
187
|
+
* `shutdown_handoff` — a newer daemon taking over (self-update / newest-build-
|
|
188
|
+
* wins) asks the incumbent to step down WITHOUT dropping the portal connection
|
|
189
|
+
* first. The incumbent frees the control socket and disconnects attached CLIs
|
|
190
|
+
* (they reconnect to the new daemon in ~500ms, unchanged), but KEEPS its
|
|
191
|
+
* outbound portal WebSocket — and thus its presence claim — alive until the
|
|
192
|
+
* new daemon has dialled out and claimed the machine's presence slot, at which
|
|
193
|
+
* point the server supersedes the incumbent's socket and it exits. This is a
|
|
194
|
+
* make-before-break handoff: the presence key is owned by one daemon or the
|
|
195
|
+
* other for the entire takeover, so `exec`/`file write` never sees a
|
|
196
|
+
* `registered but not connected` gap mid-upgrade. A bounded fallback timeout
|
|
197
|
+
* ensures the incumbent still exits if the successor never comes up.
|
|
198
|
+
*/
|
|
199
|
+
const clientShutdownHandoffSchema = z.object({ t: z.literal("shutdown_handoff") });
|
|
222
200
|
const clientMessageSchema = z.discriminatedUnion("t", [
|
|
223
201
|
clientHelloSchema,
|
|
224
202
|
clientBindSchema,
|
|
@@ -229,7 +207,8 @@ const clientMessageSchema = z.discriminatedUnion("t", [
|
|
|
229
207
|
clientDeclineSchema,
|
|
230
208
|
clientByeSchema,
|
|
231
209
|
clientStatusSchema,
|
|
232
|
-
clientShutdownSchema
|
|
210
|
+
clientShutdownSchema,
|
|
211
|
+
clientShutdownHandoffSchema
|
|
233
212
|
]);
|
|
234
213
|
/**
|
|
235
214
|
* `state` — the shared portal status, pushed to every attached client so each
|
|
@@ -356,6 +335,14 @@ function parseDaemonMessage(line) {
|
|
|
356
335
|
*/
|
|
357
336
|
/** Grace period after the last client detaches before the daemon exits. */
|
|
358
337
|
const IDLE_SHUTDOWN_MS = 3e4;
|
|
338
|
+
/**
|
|
339
|
+
* Upper bound an incumbent daemon waits, during a graceful handoff, for the
|
|
340
|
+
* successor to dial out and claim presence before giving up and releasing the
|
|
341
|
+
* slot itself. Comfortably above a normal successor boot + WS dial (a few
|
|
342
|
+
* hundred ms to a couple of seconds), and short enough that a failed successor
|
|
343
|
+
* doesn't strand the machine as reachable-but-dead for long.
|
|
344
|
+
*/
|
|
345
|
+
const HANDOFF_MAX_WAIT_MS = 1e4;
|
|
359
346
|
var PortalDaemon = class {
|
|
360
347
|
appUrl;
|
|
361
348
|
paths;
|
|
@@ -365,8 +352,10 @@ var PortalDaemon = class {
|
|
|
365
352
|
conns = /* @__PURE__ */ new Set();
|
|
366
353
|
cwds = /* @__PURE__ */ new Map();
|
|
367
354
|
declined = /* @__PURE__ */ new Set();
|
|
355
|
+
persistedMachineName = null;
|
|
368
356
|
fallbackCwd;
|
|
369
357
|
idleTimer = null;
|
|
358
|
+
handingOff = false;
|
|
370
359
|
sessionToken = null;
|
|
371
360
|
deviceToken = null;
|
|
372
361
|
constructor(appUrl) {
|
|
@@ -476,6 +465,9 @@ var PortalDaemon = class {
|
|
|
476
465
|
case "shutdown":
|
|
477
466
|
this.forceShutdown();
|
|
478
467
|
return;
|
|
468
|
+
case "shutdown_handoff":
|
|
469
|
+
this.gracefulHandoffShutdown();
|
|
470
|
+
return;
|
|
479
471
|
default: return msg;
|
|
480
472
|
}
|
|
481
473
|
}
|
|
@@ -499,12 +491,29 @@ var PortalDaemon = class {
|
|
|
499
491
|
deviceToken: this.deviceToken
|
|
500
492
|
}),
|
|
501
493
|
resolveCwd: (conversationId) => this.resolveCwd(conversationId),
|
|
494
|
+
tlsCertSource: defaultTlsCertSource(process.env, (msg) => this.logInfo(msg)),
|
|
495
|
+
persistedMachineName: this.persistedMachineName,
|
|
496
|
+
onMachineName: (name, source) => this.onMachineName(name, source),
|
|
502
497
|
onState: (state) => {
|
|
503
498
|
this.lastState = state;
|
|
504
499
|
this.broadcastState();
|
|
505
500
|
}
|
|
506
501
|
});
|
|
507
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* The identity resolved on connect. Persist the name so a later scutil
|
|
505
|
+
* failure reuses it instead of adopting the volatile hostname, and log the
|
|
506
|
+
* source — a `hostname-fallback` after we'd previously registered under
|
|
507
|
+
* scutil is the tell that a flap just orphaned this machine's grants.
|
|
508
|
+
*/
|
|
509
|
+
onMachineName(name, source) {
|
|
510
|
+
if (source === "hostname-fallback" && this.persistedMachineName) this.logInfo(`portal identity WARNING: fell back to hostname "${name}" but had previously registered as "${this.persistedMachineName}" — grants may be orphaned`);
|
|
511
|
+
else this.logInfo(`portal identity: "${name}" (source=${source})`);
|
|
512
|
+
if (name && name !== this.persistedMachineName) {
|
|
513
|
+
this.persistedMachineName = name;
|
|
514
|
+
this.persistState();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
508
517
|
/** The cwd an exec for `conversationId` runs in. */
|
|
509
518
|
resolveCwd(conversationId) {
|
|
510
519
|
if (conversationId) {
|
|
@@ -538,7 +547,14 @@ var PortalDaemon = class {
|
|
|
538
547
|
/** Append a line to the daemon log file (best-effort, for post-hoc debugging). */
|
|
539
548
|
logError(context, error) {
|
|
540
549
|
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
-
|
|
550
|
+
this.logLine(`${context}: ${message}`);
|
|
551
|
+
}
|
|
552
|
+
/** Append an informational line to the daemon log file (best-effort). */
|
|
553
|
+
logInfo(message) {
|
|
554
|
+
this.logLine(message);
|
|
555
|
+
}
|
|
556
|
+
logLine(message) {
|
|
557
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${message}\n`;
|
|
542
558
|
appendFile(this.paths.logPath, line).catch((_error) => {});
|
|
543
559
|
}
|
|
544
560
|
/** Close the listener without exiting the process (tests own the process). */
|
|
@@ -589,6 +605,37 @@ var PortalDaemon = class {
|
|
|
589
605
|
this.server?.close();
|
|
590
606
|
process.exit(0);
|
|
591
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Graceful takeover step-down (a newer daemon is replacing us). Unlike
|
|
610
|
+
* `forceShutdown`, this does NOT drop the portal connection up front: doing so
|
|
611
|
+
* would let this machine's presence key expire (10s TTL) before the successor
|
|
612
|
+
* dials out and re-claims it, which is the `registered but not connected` gap
|
|
613
|
+
* that made `exec`/`file write` fail mid-self-update.
|
|
614
|
+
*
|
|
615
|
+
* Order matters. We free the control socket and disconnect attached CLIs
|
|
616
|
+
* first, because the successor cannot bind the singleton socket (and thus
|
|
617
|
+
* cannot start its own portal client) until we release it. But we KEEP our
|
|
618
|
+
* outbound portal WebSocket — and its presence claim — alive across that
|
|
619
|
+
* window. When the successor connects and `claim()`s the slot, the server
|
|
620
|
+
* supersedes us and closes our socket; `beginHandoff` resolves on that close
|
|
621
|
+
* and we exit. A bounded fallback inside `beginHandoff` still exits us if the
|
|
622
|
+
* successor never comes up, so we never hold a dead slot forever.
|
|
623
|
+
*/
|
|
624
|
+
async gracefulHandoffShutdown() {
|
|
625
|
+
if (this.handingOff) return;
|
|
626
|
+
this.handingOff = true;
|
|
627
|
+
for (const conn of this.conns) try {
|
|
628
|
+
conn.socket.destroy();
|
|
629
|
+
} catch (_error) {}
|
|
630
|
+
this.conns.clear();
|
|
631
|
+
this.server?.close();
|
|
632
|
+
this.server = null;
|
|
633
|
+
if (this.client) {
|
|
634
|
+
await this.client.beginHandoff(HANDOFF_MAX_WAIT_MS);
|
|
635
|
+
this.client.dispose();
|
|
636
|
+
}
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
592
639
|
async loadState() {
|
|
593
640
|
try {
|
|
594
641
|
const raw = await readFile(this.paths.statePath, "utf8");
|
|
@@ -598,6 +645,7 @@ var PortalDaemon = class {
|
|
|
598
645
|
if (Array.isArray(parsed.declined)) {
|
|
599
646
|
for (const id of parsed.declined) if (typeof id === "string") this.declined.add(id);
|
|
600
647
|
}
|
|
648
|
+
if (typeof parsed.machineName === "string" && parsed.machineName) this.persistedMachineName = parsed.machineName;
|
|
601
649
|
}
|
|
602
650
|
} catch (_error) {}
|
|
603
651
|
}
|
|
@@ -605,7 +653,8 @@ var PortalDaemon = class {
|
|
|
605
653
|
const state = {
|
|
606
654
|
version: LOCAL_PROTOCOL_VERSION,
|
|
607
655
|
cwds: Object.fromEntries(this.cwds),
|
|
608
|
-
declined: [...this.declined]
|
|
656
|
+
declined: [...this.declined],
|
|
657
|
+
machineName: this.persistedMachineName ?? void 0
|
|
609
658
|
};
|
|
610
659
|
writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
|
|
611
660
|
this.logError("persistState failed", error);
|
|
@@ -635,7 +684,7 @@ async function ensureDaemonRunning(appUrl) {
|
|
|
635
684
|
if (await isDaemonListening(socketPath)) {
|
|
636
685
|
const status = await queryDaemonStatus(appUrl);
|
|
637
686
|
if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return;
|
|
638
|
-
if (await
|
|
687
|
+
if (await stopDaemonForHandoff(appUrl) === "failed") return;
|
|
639
688
|
}
|
|
640
689
|
const entry = process.argv[1];
|
|
641
690
|
const args = entry ? [
|
|
@@ -730,6 +779,37 @@ async function queryDaemonStatus(appUrl) {
|
|
|
730
779
|
});
|
|
731
780
|
}
|
|
732
781
|
/**
|
|
782
|
+
* Ask a running incumbent daemon to step down for a make-before-break handoff
|
|
783
|
+
* (see the daemon's `gracefulHandoffShutdown`). Sends `shutdown_handoff` and
|
|
784
|
+
* waits for the incumbent to release the CONTROL SOCKET — at which point the
|
|
785
|
+
* successor can bind it and start its own portal client. Crucially, the
|
|
786
|
+
* incumbent keeps its portal connection (and presence) alive past this point,
|
|
787
|
+
* so the successor's subsequent `claim()` supersedes it with no presence gap.
|
|
788
|
+
*
|
|
789
|
+
* Returns `stopped` once the socket is free, `failed` if it never freed (the
|
|
790
|
+
* caller should then leave the incumbent alone rather than fight it — same
|
|
791
|
+
* fallback semantics as `stopDaemon`), or `not-running` if nothing was there.
|
|
792
|
+
* Unlike `stopDaemon` this never SIGKILLs: the incumbent is intentionally still
|
|
793
|
+
* alive (holding presence) after it frees the socket, so killing it by pid is
|
|
794
|
+
* exactly the gap this path exists to avoid.
|
|
795
|
+
*/
|
|
796
|
+
async function stopDaemonForHandoff(appUrl) {
|
|
797
|
+
const { socketPath } = daemonPaths(appUrl);
|
|
798
|
+
if (!await isDaemonListening(socketPath)) return "not-running";
|
|
799
|
+
const sock = await connectControl(socketPath);
|
|
800
|
+
if (!sock) return "failed";
|
|
801
|
+
sock.write(encodeLine({ t: "shutdown_handoff" }));
|
|
802
|
+
for (let i = 0; i < 50; i += 1) {
|
|
803
|
+
await sleep(100);
|
|
804
|
+
if (!await isDaemonListening(socketPath)) {
|
|
805
|
+
sock.destroy();
|
|
806
|
+
return "stopped";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
sock.destroy();
|
|
810
|
+
return "failed";
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
733
813
|
* Hard-stop a running daemon. Preferred path: send `shutdown` over the control
|
|
734
814
|
* socket so it drains clients and exits cleanly. If the socket is unresponsive
|
|
735
815
|
* (a wedged daemon), fall back to SIGTERM then SIGKILL by the pid the status
|
|
@@ -781,4 +861,4 @@ function sleep(ms) {
|
|
|
781
861
|
}
|
|
782
862
|
|
|
783
863
|
//#endregion
|
|
784
|
-
export { runPortalDaemon as a,
|
|
864
|
+
export { runPortalDaemon as a, stopDaemonForHandoff as c, daemonPaths as d, encodeLine as f, queryDaemonStatus as i, LOCAL_PROTOCOL_VERSION as l, parseDaemonMessage as m, ensureDaemonRunning as n, startPortalDaemon as o, makeLineParser as p, isDaemonListening as r, stopDaemon as s, PortalDaemon as t, PORTAL_DAEMON_FLAG as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { d as daemonPaths, f as encodeLine, l as LOCAL_PROTOCOL_VERSION, m as parseDaemonMessage, n as ensureDaemonRunning, p as makeLineParser } from "./daemon-Do1jU2UF.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
|