@1e0zj/dsh-plugin-mall 0.4.0 → 0.4.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/README.md +2 -2
- package/package.json +1 -1
- package/src/cli.js +47 -1
- package/src/client.js +44 -4
- package/src/github.js +15 -8
- package/src/guard.js +209 -6
- package/src/index.js +748 -100
- package/src/installer.js +310 -35
- package/src/restart-protocol.js +144 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Host-independent restart handoff protocol shared by the Web plugin and the
|
|
2
|
+
// standalone guard CLI. The parent must not infer readiness from a child pid:
|
|
3
|
+
// an incompatible CLI can spawn successfully and then die on argument parsing.
|
|
4
|
+
|
|
5
|
+
export const RESTART_HELPER_READY_TYPE = "@1e0zj/dsh-plugin-mall:restart-helper-ready";
|
|
6
|
+
export const RESTART_HELPER_PROTOCOL_VERSION = 1;
|
|
7
|
+
export const RESTART_RESPONSE_DRAIN_MS = 1000;
|
|
8
|
+
|
|
9
|
+
export function createRestartHelperReadyMessage(awaitExitPid) {
|
|
10
|
+
return {
|
|
11
|
+
type: RESTART_HELPER_READY_TYPE,
|
|
12
|
+
protocol: RESTART_HELPER_PROTOCOL_VERSION,
|
|
13
|
+
awaitExitPid,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function describeExit(code, signal) {
|
|
18
|
+
if (typeof code === "number") return ` with code ${code}`;
|
|
19
|
+
if (signal) return ` from signal ${signal}`;
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keep the outgoing Host alive until the detached helper explicitly confirms
|
|
25
|
+
* that it parsed the current protocol and is waiting for that Host's pid.
|
|
26
|
+
*
|
|
27
|
+
* The returned disposer is suitable for ctx.effect(): unloading the plugin
|
|
28
|
+
* cancels every timer, removes listeners, and terminates the waiting helper.
|
|
29
|
+
*/
|
|
30
|
+
export function superviseRestartHelper(child, {
|
|
31
|
+
awaitExitPid,
|
|
32
|
+
handshakeTimeoutMs = 2000,
|
|
33
|
+
stabilityMs = 600,
|
|
34
|
+
responseDelayMs = RESTART_RESPONSE_DRAIN_MS,
|
|
35
|
+
onHostExit = () => process.exit(0),
|
|
36
|
+
onFailure = () => {},
|
|
37
|
+
} = {}) {
|
|
38
|
+
let phase = "handshake";
|
|
39
|
+
let timer;
|
|
40
|
+
let readySettled = false;
|
|
41
|
+
let resolveReady;
|
|
42
|
+
const ready = new Promise((resolvePromise) => { resolveReady = resolvePromise; });
|
|
43
|
+
|
|
44
|
+
const clearTimer = () => {
|
|
45
|
+
if (timer === undefined) return;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
timer = undefined;
|
|
48
|
+
};
|
|
49
|
+
const removeListeners = () => {
|
|
50
|
+
child.removeListener("message", onMessage);
|
|
51
|
+
child.removeListener("error", onError);
|
|
52
|
+
child.removeListener("exit", onExit);
|
|
53
|
+
};
|
|
54
|
+
const terminateChild = () => {
|
|
55
|
+
if (child.exitCode !== null && child.exitCode !== undefined) return;
|
|
56
|
+
if (child.signalCode !== null && child.signalCode !== undefined) return;
|
|
57
|
+
if (child.killed === true) return;
|
|
58
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
59
|
+
};
|
|
60
|
+
const settleReady = (result) => {
|
|
61
|
+
if (readySettled) return;
|
|
62
|
+
readySettled = true;
|
|
63
|
+
resolveReady(result);
|
|
64
|
+
};
|
|
65
|
+
const fail = (message, { terminate = false } = {}) => {
|
|
66
|
+
if (phase === "failed" || phase === "disposed" || phase === "committed") return;
|
|
67
|
+
const afterReady = readySettled;
|
|
68
|
+
phase = "failed";
|
|
69
|
+
clearTimer();
|
|
70
|
+
removeListeners();
|
|
71
|
+
if (terminate) terminateChild();
|
|
72
|
+
settleReady({ ok: false, error: message });
|
|
73
|
+
try { onFailure(message, { afterReady }); } catch { /* diagnostics are best effort */ }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function onError(error) {
|
|
77
|
+
fail(`restart helper failed to start: ${error?.message ?? String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
function onExit(code, signal) {
|
|
80
|
+
fail(`restart helper exited before handoff${describeExit(code, signal)}`);
|
|
81
|
+
}
|
|
82
|
+
function onMessage(message) {
|
|
83
|
+
if (phase !== "handshake" || message?.type !== RESTART_HELPER_READY_TYPE) return;
|
|
84
|
+
if (message.protocol !== RESTART_HELPER_PROTOCOL_VERSION || message.awaitExitPid !== awaitExitPid) {
|
|
85
|
+
fail(
|
|
86
|
+
`restart helper protocol mismatch (expected v${RESTART_HELPER_PROTOCOL_VERSION} for pid ${awaitExitPid})`,
|
|
87
|
+
{ terminate: true },
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
phase = "stability";
|
|
92
|
+
clearTimer();
|
|
93
|
+
// A successful helper is blocked in --await-exit until this Host leaves.
|
|
94
|
+
// Observe it briefly so a parse/startup failure cannot masquerade as an
|
|
95
|
+
// accepted handoff merely because the IPC message won a scheduling race.
|
|
96
|
+
timer = setTimeout(() => {
|
|
97
|
+
if (phase !== "stability") return;
|
|
98
|
+
phase = "accepted";
|
|
99
|
+
try {
|
|
100
|
+
if (child.connected === true) child.disconnect();
|
|
101
|
+
} catch { /* the ready message already proved the channel worked */ }
|
|
102
|
+
try { child.unref(); } catch { /* ChildProcess-compatible fakes may omit it */ }
|
|
103
|
+
settleReady({ ok: true });
|
|
104
|
+
// Preserve the old implementation's one-second response-drain window.
|
|
105
|
+
// The RPC layer does not expose a response-flushed callback. The exit
|
|
106
|
+
// listener remains active: if the helper dies here, the timer is
|
|
107
|
+
// cancelled and the old Host stays.
|
|
108
|
+
timer = setTimeout(() => {
|
|
109
|
+
if (phase !== "accepted") return;
|
|
110
|
+
phase = "committed";
|
|
111
|
+
timer = undefined;
|
|
112
|
+
removeListeners();
|
|
113
|
+
try {
|
|
114
|
+
onHostExit();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
phase = "failed";
|
|
117
|
+
try { onFailure(`could not exit old Host: ${error?.message ?? String(error)}`, { afterReady: true }); } catch { /* best effort */ }
|
|
118
|
+
}
|
|
119
|
+
}, responseDelayMs);
|
|
120
|
+
}, stabilityMs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
child.on("message", onMessage);
|
|
124
|
+
child.once("error", onError);
|
|
125
|
+
child.once("exit", onExit);
|
|
126
|
+
timer = setTimeout(() => {
|
|
127
|
+
fail(`restart helper did not acknowledge protocol v${RESTART_HELPER_PROTOCOL_VERSION} within ${handshakeTimeoutMs}ms`, { terminate: true });
|
|
128
|
+
}, handshakeTimeoutMs);
|
|
129
|
+
|
|
130
|
+
const dispose = () => {
|
|
131
|
+
if (phase === "failed" || phase === "disposed" || phase === "committed") return;
|
|
132
|
+
phase = "disposed";
|
|
133
|
+
clearTimer();
|
|
134
|
+
removeListeners();
|
|
135
|
+
terminateChild();
|
|
136
|
+
settleReady({ ok: false, error: "restart handoff cancelled because the plugin unloaded" });
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
ready,
|
|
141
|
+
dispose,
|
|
142
|
+
state: () => phase,
|
|
143
|
+
};
|
|
144
|
+
}
|