blun-king-cli 9.1.601 → 9.1.603
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 +105 -94
- package/LIESMICH.txt +57 -57
- package/README.md +90 -90
- package/bin/agentspine-king-goal-inbox.mjs +127 -127
- package/bin/agentspine-king-goal-intake.mjs +106 -106
- package/bin/agentspine-king-host-runner.mjs +109 -109
- package/bin/agentspine-king-snapshot-policy.cjs +80 -80
- package/bin/agentspine-king-status-policy.mjs +226 -226
- package/bin/agentspine-king-worker-host.mjs +160 -160
- package/bin/core-bootstrap.js +77 -77
- package/bin/launcher-mode.js +93 -93
- package/bin/launcher-restart-policy.cjs +131 -131
- package/bin/launcher-runtime.js +491 -491
- package/bin/managed-node.js +186 -186
- package/bin/managed-plugin-selection.cjs +115 -115
- package/bin/native-module-repair.js +201 -201
- package/bin/node-runtime.js +34 -34
- package/bin/node-version.js +49 -49
- package/bin/plugin-bootstrap.js +162 -162
- package/bin/private-paths.js +1606 -1606
- package/bin/runtime-exit-ledger.cjs +144 -144
- package/bin/runtime-exit-ledger.d.cts +23 -23
- package/bin/standard-tools-bootstrap.js +272 -272
- package/bin/turn-thinking-policy.cjs +82 -82
- package/bin/update-copy.js +200 -200
- package/bin/update-lease.js +597 -597
- package/bin/update-notice.js +1568 -1568
- package/bin/verify-agent-behavior.cjs +122 -122
- package/bin/verify-agent-components.cjs +104 -104
- package/bin/verify-bundled-agent-sources.cjs +83 -83
- package/bin/windows-node-crash-dump.cjs +289 -289
- package/blun.mjs +1709 -1601
- package/codebase-index/codebase_index.py +470 -470
- package/dist-web/assets/{CodeBlockNode-CXnTLMNr.js → CodeBlockNode-TKKkJX8L.js} +1 -1
- package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-CCF8Ntnq.js → CodeBlockShell.vue_vue_type_style_index_0_lang-CUj2Ds5x.js} +1 -1
- package/dist-web/assets/DesignSystemView-DwK1aQMo.js +12 -0
- package/dist-web/assets/DesignSystemView-szjTj7ns.css +1 -0
- package/dist-web/assets/{Tooltip-CFQEzqC-.js → Tooltip-CTWv7WCU.js} +1 -1
- package/dist-web/assets/{abnfDiagram-N423BO3Z-Z1APZG5m.js → abnfDiagram-N423BO3Z-DA8V8Zjh.js} +1 -1
- package/dist-web/assets/{arc-C4VyWloT.js → arc-YtddKbdY.js} +1 -1
- package/dist-web/assets/{architectureDiagram-T3A2C74G-DpddbUcM.js → architectureDiagram-T3A2C74G-CGEiObmo.js} +1 -1
- package/dist-web/assets/{blockDiagram-VBNYF7ZC-BqccOSMr.js → blockDiagram-VBNYF7ZC-Bd5Vt50e.js} +1 -1
- package/dist-web/assets/{c4Diagram-5PPSVZJV-OHAcJrTy.js → c4Diagram-5PPSVZJV-D2ApPM54.js} +1 -1
- package/dist-web/assets/channel-BnluexuN.js +1 -0
- package/dist-web/assets/{chunk-2GRJ4B5K-DqJHs8Fw.js → chunk-2GRJ4B5K-BDcIEZSl.js} +1 -1
- package/dist-web/assets/{chunk-2Q5K7J3B-B1nCd91o.js → chunk-2Q5K7J3B-6aSabCX7.js} +1 -1
- package/dist-web/assets/{chunk-5RXB4S5H-neG70kZx.js → chunk-5RXB4S5H-BFRQvIJS.js} +1 -1
- package/dist-web/assets/{chunk-5VM5RSS4-l7biGqeH.js → chunk-5VM5RSS4-ChAa-DNY.js} +1 -1
- package/dist-web/assets/{chunk-6Q2QTUOP-BO-F11Gs.js → chunk-6Q2QTUOP-BIfrb2Jc.js} +1 -1
- package/dist-web/assets/{chunk-GF5L2VYU-BtC69fw0.js → chunk-GF5L2VYU-BR0gvtWI.js} +1 -1
- package/dist-web/assets/{chunk-JWPE2WC7-DIOubEqr.js → chunk-JWPE2WC7-BgXKsx5E.js} +1 -1
- package/dist-web/assets/{chunk-KBJHAD2P-DDUeH_Ba.js → chunk-KBJHAD2P-I8mrZhpz.js} +1 -1
- package/dist-web/assets/{chunk-RYQCIY6F-B9gFMVbS.js → chunk-RYQCIY6F-BMPaem92.js} +1 -1
- package/dist-web/assets/{chunk-XXDRQBXY-Bn1Xz6TQ.js → chunk-XXDRQBXY-COy0J-0j.js} +1 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +1 -0
- package/dist-web/assets/{cose-bilkent-JH36ORCC-DqigO3T4.js → cose-bilkent-JH36ORCC-MosTTRdo.js} +1 -1
- package/dist-web/assets/{cynefin-VYW2F7L2-rpD93tQ-.js → cynefin-VYW2F7L2-5tjPgrkP.js} +1 -1
- package/dist-web/assets/{cynefinDiagram-MW4NZA55-DbTqnkVJ.js → cynefinDiagram-MW4NZA55-CbDcdInR.js} +1 -1
- package/dist-web/assets/{dagre-VZM6K2ZE-DcM8aEVY.js → dagre-VZM6K2ZE-YaBom1ZH.js} +1 -1
- package/dist-web/assets/{diagram-7IWD3JNH-UymgsBNa.js → diagram-7IWD3JNH-Da4m-Hgz.js} +1 -1
- package/dist-web/assets/{diagram-B4RE2ZJO-BiYu5WPF.js → diagram-B4RE2ZJO-CAUUegxg.js} +1 -1
- package/dist-web/assets/{diagram-LBJQPF4R-BD3kkgOO.js → diagram-LBJQPF4R-o21mWKG4.js} +1 -1
- package/dist-web/assets/{diagram-Q27KOJAE-D1mYQ_cI.js → diagram-Q27KOJAE-BA3z3sXf.js} +1 -1
- package/dist-web/assets/{diagram-UB23O5K3-BtX8u6HG.js → diagram-UB23O5K3-CGRdSSDK.js} +1 -1
- package/dist-web/assets/{ebnfDiagram-BXEA7PRR-CBAnFkRF.js → ebnfDiagram-BXEA7PRR-Bw-iU9FP.js} +1 -1
- package/dist-web/assets/{erDiagram-JOGREHBK-3vKcWsxm.js → erDiagram-JOGREHBK-BwM1yiR0.js} +1 -1
- package/dist-web/assets/{flowDiagram-UKHOOZJN-D9wBbHm2.js → flowDiagram-UKHOOZJN-z05eOR1Q.js} +1 -1
- package/dist-web/assets/{ganttDiagram-PKOTCBZU-Cd_dViF-.js → ganttDiagram-PKOTCBZU-BMUubNTl.js} +1 -1
- package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-BiEEtTo7.js → gitGraphDiagram-DS77QQ5N-Dccbi1wU.js} +1 -1
- package/dist-web/assets/{index-DdEakFJM.js → index-BBbC5LRF.js} +1 -1
- package/dist-web/assets/{index-BmKj7CB1.css → index-C9eDH3u-.css} +1 -1
- package/dist-web/assets/{index-Ba1OvLjf.js → index-D48p0M2S.js} +2 -2
- package/dist-web/assets/{index-CiPA8Kex.js → index-h0KEOmkU.js} +14 -14
- package/dist-web/assets/{index10-CNr4U-Nw.js → index10-Qbuw1N6u.js} +1 -1
- package/dist-web/assets/{index11-Dto_yZNz.js → index11-g8v025wN.js} +1 -1
- package/dist-web/assets/{index5-B2PldaO2.js → index5-BYNcV8BY.js} +1 -1
- package/dist-web/assets/{index6-CBdZmmyx.js → index6-BetANtnV.js} +1 -1
- package/dist-web/assets/{index7-Dz9oCsUF.js → index7-BcPbgCKN.js} +1 -1
- package/dist-web/assets/{index8-B6k25jtU.js → index8-B1lp-RvF.js} +2 -2
- package/dist-web/assets/{infoDiagram-6WML65LV-D4ph2sIH.js → infoDiagram-6WML65LV-BF_pbUnN.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-DjKjbMm9.js → ishikawaDiagram-WSZJBQD7-Dedv4urS.js} +1 -1
- package/dist-web/assets/{journeyDiagram-NVQOT4AX-DttHDfPQ.js → journeyDiagram-NVQOT4AX-B1Ex3b6o.js} +1 -1
- package/dist-web/assets/{kanban-definition-27J2QSJJ-C5gB-W0d.js → kanban-definition-27J2QSJJ-BtCGliBH.js} +1 -1
- package/dist-web/assets/{linear-BRGe4-d6.js → linear-ByItc8Qr.js} +1 -1
- package/dist-web/assets/{mermaid.core-D-9Sx1No.js → mermaid.core-B1M2O7nN.js} +4 -4
- package/dist-web/assets/{mindmap-definition-FAOFIHXS-Cg_hoVIq.js → mindmap-definition-FAOFIHXS-D0UHKUyj.js} +1 -1
- package/dist-web/assets/{pegDiagram-VL7TDLO6-CSX9hitD.js → pegDiagram-VL7TDLO6-D7zYCVmG.js} +1 -1
- package/dist-web/assets/{pieDiagram-7S7Q4E2Y-0L0mxahZ.js → pieDiagram-7S7Q4E2Y-Bd0i9CHa.js} +1 -1
- package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-U483z0er.js → quadrantDiagram-CIZ2JOQS-MG-Ug47d.js} +1 -1
- package/dist-web/assets/{railroadDiagram-AXF67PYL-D80a2_DJ.js → railroadDiagram-AXF67PYL-AZdl90Gj.js} +1 -1
- package/dist-web/assets/{requirementDiagram-LRYGKXZP-CeyHfq8t.js → requirementDiagram-LRYGKXZP-DjplF9kt.js} +1 -1
- package/dist-web/assets/{sankeyDiagram-W5VNT64P-CCISkkrR.js → sankeyDiagram-W5VNT64P-Bi9Zgfjs.js} +1 -1
- package/dist-web/assets/{sequenceDiagram-SI44F4Z6-jQfm2XS7.js → sequenceDiagram-SI44F4Z6-ChEpdCvb.js} +1 -1
- package/dist-web/assets/{sizeCapture-X5ZJPWSS-lRZsmCoS.js → sizeCapture-X5ZJPWSS-B5RFUdaa.js} +1 -1
- package/dist-web/assets/{stateDiagram-OKZ733FA-ClCgTdUm.js → stateDiagram-OKZ733FA-BkRblSKp.js} +1 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +1 -0
- package/dist-web/assets/{swimlanes-SLNWSIFB-BCdOUN05.js → swimlanes-SLNWSIFB-DLXH6Zhc.js} +2 -2
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +8 -0
- package/dist-web/assets/{timeline-definition-Z64GVDOM-DpWutSKa.js → timeline-definition-Z64GVDOM-C_N88siO.js} +1 -1
- package/dist-web/assets/{vennDiagram-T6HMQDX7-DVC5JVsc.js → vennDiagram-T6HMQDX7-DMsnvDt0.js} +1 -1
- package/dist-web/assets/{vue.runtime.esm-bundler-BwwRtQwQ.js → vue.runtime.esm-bundler-B-3uIy85.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-T6FBY63Y-BK2mrq7L.js → wardleyDiagram-T6FBY63Y-C9SJIPzq.js} +1 -1
- package/dist-web/assets/{xychartDiagram-ELKLHX3M-DUJu9F7-.js → xychartDiagram-ELKLHX3M-DZLWA6Go.js} +1 -1
- package/dist-web/index.html +32 -32
- package/dist-web/vis/index.html.gz +0 -0
- package/package.json +1 -1
- package/scripts/fix-node-pty-perms.js +53 -53
- package/standard-skills/blun-app-design-system/SKILL.md +30 -30
- package/standard-skills/blun-app-design-system/provenance.json +17 -17
- package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
- package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
- package/telegram-plugin/blun.plugin.json +20 -20
- package/telegram-plugin/commands/access.md +37 -37
- package/telegram-plugin/dist/bridge.mjs +81 -81
- package/telegram-plugin/dist/mcp-server.mjs +158 -158
- package/worker-host.mjs +932 -932
- package/dist-web/assets/DesignSystemView-6HgAUEmC.css +0 -1
- package/dist-web/assets/DesignSystemView-BbMOu-XO.js +0 -12
- package/dist-web/assets/channel-B2VOQOI-.js +0 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +0 -1
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +0 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +0 -1
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +0 -8
package/bin/update-lease.js
CHANGED
|
@@ -1,597 +1,597 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { createHash } = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const net = require('node:net');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
const { spawn } = require('node:child_process');
|
|
9
|
-
|
|
10
|
-
const LEASE_ADOPT_MESSAGE = 'blun-update-lease-adopted';
|
|
11
|
-
const LEASE_CANCEL_MESSAGE = 'blun-update-lease-cancel';
|
|
12
|
-
const LEASE_COMMIT_MESSAGE = 'blun-update-lease-commit';
|
|
13
|
-
const LEASE_HANDOFF_MESSAGE = 'blun-update-lease-handoff';
|
|
14
|
-
const LEASE_BUSY_MESSAGE = 'blun-update-lease-busy';
|
|
15
|
-
const LEASE_UNAVAILABLE_MESSAGE = 'blun-update-lease-unavailable';
|
|
16
|
-
const LEASE_PROTOCOL_VERSION = 'BLUN_UPDATE_LEASE_V3';
|
|
17
|
-
const LEASE_PROBE_TIMEOUT_MS = 500;
|
|
18
|
-
const MAX_POSIX_SOCKET_PATH_BYTES = 96;
|
|
19
|
-
const LEASE_CONNECTIONS = Symbol('blunLeaseConnections');
|
|
20
|
-
|
|
21
|
-
function canonicalPackageIdentity(packageRoot, options = {}) {
|
|
22
|
-
if (typeof packageRoot !== 'string' || packageRoot.trim().length === 0) {
|
|
23
|
-
throw new Error('UPDATE_PACKAGE_ROOT_REQUIRED');
|
|
24
|
-
}
|
|
25
|
-
const fsImpl = options.fsImpl || fs;
|
|
26
|
-
const platform = options.platform || process.platform;
|
|
27
|
-
const realpath = fsImpl.realpathSync.native || fsImpl.realpathSync;
|
|
28
|
-
let canonical = realpath(path.resolve(packageRoot));
|
|
29
|
-
if (!fsImpl.statSync(canonical).isDirectory()) throw new Error('INVALID_UPDATE_PACKAGE_ROOT');
|
|
30
|
-
canonical = canonical.replaceAll('\\', '/');
|
|
31
|
-
if (platform === 'win32') canonical = canonical.toLowerCase();
|
|
32
|
-
return canonical;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function deriveUpdateLeaseEndpoint(packageRoot, options = {}) {
|
|
36
|
-
const platform = options.platform || process.platform;
|
|
37
|
-
const role = options.role === 'install'
|
|
38
|
-
? 'install'
|
|
39
|
-
: options.role === 'runtime'
|
|
40
|
-
? 'runtime'
|
|
41
|
-
: 'notice';
|
|
42
|
-
const identity = canonicalPackageIdentity(packageRoot, options);
|
|
43
|
-
const digest = createHash('sha256')
|
|
44
|
-
.update(`blun-update-v3\0${role}\0${identity}`)
|
|
45
|
-
.digest('hex');
|
|
46
|
-
const magic = `${LEASE_PROTOCOL_VERSION} ${digest}\n`;
|
|
47
|
-
if (platform === 'win32') {
|
|
48
|
-
return Object.freeze({
|
|
49
|
-
magic,
|
|
50
|
-
path: `\\\\.\\pipe\\blun-update-v3-${digest.slice(0, 32)}`,
|
|
51
|
-
platform,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const uid = options.uid ?? (typeof process.getuid === 'function' ? process.getuid() : undefined);
|
|
56
|
-
const directoryName = `blun-update-${uid ?? digest.slice(0, 12)}`;
|
|
57
|
-
const requestedRoot = String(options.socketDirectory || options.tempDir || os.tmpdir())
|
|
58
|
-
.replaceAll('\\', '/');
|
|
59
|
-
let directory = options.socketDirectory
|
|
60
|
-
? requestedRoot
|
|
61
|
-
: path.posix.join(requestedRoot, directoryName);
|
|
62
|
-
let socketPath = path.posix.join(directory, `lease-${digest.slice(0, 24)}.sock`);
|
|
63
|
-
if (Buffer.byteLength(socketPath, 'utf8') > MAX_POSIX_SOCKET_PATH_BYTES) {
|
|
64
|
-
directory = path.posix.join('/tmp', directoryName);
|
|
65
|
-
socketPath = path.posix.join(directory, `lease-${digest.slice(0, 24)}.sock`);
|
|
66
|
-
}
|
|
67
|
-
if (Buffer.byteLength(socketPath, 'utf8') > MAX_POSIX_SOCKET_PATH_BYTES) {
|
|
68
|
-
throw new Error('UPDATE_LEASE_SOCKET_PATH_TOO_LONG');
|
|
69
|
-
}
|
|
70
|
-
return Object.freeze({ directory, magic, path: socketPath, platform });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function connectRuntimeBroker(endpoint, options = {}) {
|
|
74
|
-
const netImpl = options.netImpl || net;
|
|
75
|
-
const timeoutMs = options.probeTimeoutMs || LEASE_PROBE_TIMEOUT_MS;
|
|
76
|
-
return new Promise((resolve) => {
|
|
77
|
-
let settled = false;
|
|
78
|
-
let received = '';
|
|
79
|
-
let socket;
|
|
80
|
-
const finish = (result) => {
|
|
81
|
-
if (settled) return;
|
|
82
|
-
settled = true;
|
|
83
|
-
clearTimeout(timer);
|
|
84
|
-
if (!result.acquired) {
|
|
85
|
-
try {
|
|
86
|
-
socket?.destroy();
|
|
87
|
-
} catch {}
|
|
88
|
-
}
|
|
89
|
-
resolve(result);
|
|
90
|
-
};
|
|
91
|
-
const timer = setTimeout(() => finish({ acquired: false, reason: 'unavailable' }), timeoutMs);
|
|
92
|
-
timer.unref?.();
|
|
93
|
-
try {
|
|
94
|
-
socket = netImpl.createConnection({ path: endpoint.path });
|
|
95
|
-
socket.setEncoding?.('utf8');
|
|
96
|
-
socket.once('connect', () => socket.write(endpoint.magic));
|
|
97
|
-
socket.on('data', (chunk) => {
|
|
98
|
-
received += String(chunk);
|
|
99
|
-
if (!received.includes('\n')) return;
|
|
100
|
-
if (received !== endpoint.magic) {
|
|
101
|
-
finish({ acquired: false, reason: 'unavailable' });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
let owned = true;
|
|
105
|
-
let released = false;
|
|
106
|
-
socket.once('close', () => { owned = false; });
|
|
107
|
-
socket.once('error', () => { owned = false; });
|
|
108
|
-
const lease = Object.freeze({
|
|
109
|
-
assertOwned() {
|
|
110
|
-
if (!owned || socket.destroyed) throw new Error('RUNTIME_LEASE_LOST');
|
|
111
|
-
},
|
|
112
|
-
async release() {
|
|
113
|
-
if (released) return;
|
|
114
|
-
released = true;
|
|
115
|
-
owned = false;
|
|
116
|
-
if (socket.destroyed) return;
|
|
117
|
-
socket.destroy();
|
|
118
|
-
},
|
|
119
|
-
unref() {
|
|
120
|
-
socket.unref?.();
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
finish({ acquired: true, lease });
|
|
124
|
-
});
|
|
125
|
-
socket.once('error', () => finish({ acquired: false, reason: 'unavailable' }));
|
|
126
|
-
socket.once('close', () => finish({ acquired: false, reason: 'unavailable' }));
|
|
127
|
-
} catch {
|
|
128
|
-
finish({ acquired: false, reason: 'unavailable' });
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function spawnRuntimeBroker(packageRoot, options = {}) {
|
|
134
|
-
const spawnImpl = options.spawnImpl || spawn;
|
|
135
|
-
const child = spawnImpl(
|
|
136
|
-
options.execPath || process.execPath,
|
|
137
|
-
[__filename, '--runtime-broker'],
|
|
138
|
-
{
|
|
139
|
-
cwd: os.tmpdir(),
|
|
140
|
-
detached: true,
|
|
141
|
-
env: {
|
|
142
|
-
...process.env,
|
|
143
|
-
BLUN_RUNTIME_BROKER_PACKAGE_ROOT: packageRoot,
|
|
144
|
-
},
|
|
145
|
-
stdio: 'ignore',
|
|
146
|
-
windowsHide: true,
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
child.unref?.();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function acquireSharedRuntimeLease(options = {}) {
|
|
153
|
-
const packageRoot = canonicalPackageIdentity(options.packageRoot, options);
|
|
154
|
-
const endpoint = deriveUpdateLeaseEndpoint(packageRoot, { ...options, role: 'runtime' });
|
|
155
|
-
const deadline = Date.now() + (options.startTimeoutMs || 3_000);
|
|
156
|
-
let spawned = false;
|
|
157
|
-
while (Date.now() < deadline) {
|
|
158
|
-
const connected = await connectRuntimeBroker(endpoint, options);
|
|
159
|
-
if (connected.acquired) return connected;
|
|
160
|
-
if (!spawned) {
|
|
161
|
-
spawned = true;
|
|
162
|
-
spawnRuntimeBroker(packageRoot, options);
|
|
163
|
-
}
|
|
164
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
165
|
-
}
|
|
166
|
-
const install = await tryAcquireUpdateLease({ ...options, packageRoot, role: 'install' });
|
|
167
|
-
if (install.acquired) {
|
|
168
|
-
await install.lease.release();
|
|
169
|
-
return { acquired: false, reason: 'unavailable' };
|
|
170
|
-
}
|
|
171
|
-
return { acquired: false, reason: install.reason };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function runRuntimeBroker(options = {}) {
|
|
175
|
-
const packageRoot = options.packageRoot
|
|
176
|
-
|| String(process.env.BLUN_RUNTIME_BROKER_PACKAGE_ROOT || '');
|
|
177
|
-
if (!path.isAbsolute(packageRoot)) throw new Error('RUNTIME_BROKER_PACKAGE_ROOT_REQUIRED');
|
|
178
|
-
const install = await tryAcquireUpdateLease({ packageRoot, role: 'install' });
|
|
179
|
-
if (!install.acquired) return false;
|
|
180
|
-
|
|
181
|
-
const endpoint = deriveUpdateLeaseEndpoint(packageRoot, { role: 'runtime' });
|
|
182
|
-
ensurePrivateSocketDirectory(endpoint);
|
|
183
|
-
const holders = new Set();
|
|
184
|
-
let idleTimer;
|
|
185
|
-
let closing = false;
|
|
186
|
-
let identity;
|
|
187
|
-
const closeBroker = async (server) => {
|
|
188
|
-
if (closing) return;
|
|
189
|
-
closing = true;
|
|
190
|
-
clearTimeout(idleTimer);
|
|
191
|
-
await closeServer(server);
|
|
192
|
-
removeOwnedSocket(endpoint, identity);
|
|
193
|
-
await install.lease.release();
|
|
194
|
-
};
|
|
195
|
-
const server = net.createServer((socket) => {
|
|
196
|
-
socket.setEncoding('utf8');
|
|
197
|
-
let received = '';
|
|
198
|
-
let admitted = false;
|
|
199
|
-
const reject = () => {
|
|
200
|
-
try {
|
|
201
|
-
socket.destroy();
|
|
202
|
-
} catch {}
|
|
203
|
-
};
|
|
204
|
-
const onData = (chunk) => {
|
|
205
|
-
if (admitted) return;
|
|
206
|
-
received += String(chunk);
|
|
207
|
-
if (Buffer.byteLength(received, 'utf8') > Buffer.byteLength(endpoint.magic, 'utf8')) {
|
|
208
|
-
reject();
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!received.includes('\n')) return;
|
|
212
|
-
if (received !== endpoint.magic) {
|
|
213
|
-
reject();
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
admitted = true;
|
|
217
|
-
clearTimeout(idleTimer);
|
|
218
|
-
holders.add(socket);
|
|
219
|
-
socket.write(endpoint.magic);
|
|
220
|
-
};
|
|
221
|
-
socket.on('data', onData);
|
|
222
|
-
socket.on('error', () => {});
|
|
223
|
-
socket.once('close', () => {
|
|
224
|
-
holders.delete(socket);
|
|
225
|
-
if (holders.size === 0 && !closing) {
|
|
226
|
-
idleTimer = setTimeout(() => { void closeBroker(server); }, 0);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
try {
|
|
231
|
-
await new Promise((resolve, reject) => {
|
|
232
|
-
const onError = (error) => {
|
|
233
|
-
server.removeListener('error', onError);
|
|
234
|
-
reject(error);
|
|
235
|
-
};
|
|
236
|
-
server.once('error', onError);
|
|
237
|
-
server.listen({ path: endpoint.path, exclusive: true }, () => {
|
|
238
|
-
server.removeListener('error', onError);
|
|
239
|
-
resolve();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
identity = recordSocketIdentity(endpoint);
|
|
243
|
-
} catch {
|
|
244
|
-
await closeServer(server);
|
|
245
|
-
await install.lease.release();
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
install.lease.unref();
|
|
249
|
-
server.unref?.();
|
|
250
|
-
idleTimer = setTimeout(() => { void closeBroker(server); }, 5_000);
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function closeServer(server) {
|
|
255
|
-
for (const socket of server[LEASE_CONNECTIONS] || []) {
|
|
256
|
-
try {
|
|
257
|
-
socket.destroy();
|
|
258
|
-
} catch {}
|
|
259
|
-
}
|
|
260
|
-
if (!server.listening) return Promise.resolve();
|
|
261
|
-
try {
|
|
262
|
-
server.close();
|
|
263
|
-
server.unref?.();
|
|
264
|
-
} catch {}
|
|
265
|
-
return Promise.resolve();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function ensurePrivateSocketDirectory(endpoint, options = {}) {
|
|
269
|
-
if (endpoint.platform === 'win32') return;
|
|
270
|
-
const fsImpl = options.fsImpl || fs;
|
|
271
|
-
fsImpl.mkdirSync(endpoint.directory, { recursive: true, mode: 0o700 });
|
|
272
|
-
const stat = fsImpl.lstatSync(endpoint.directory);
|
|
273
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
274
|
-
throw new Error('INVALID_UPDATE_LEASE_DIRECTORY');
|
|
275
|
-
}
|
|
276
|
-
if (typeof process.getuid === 'function'
|
|
277
|
-
&& Number.isInteger(stat.uid)
|
|
278
|
-
&& stat.uid !== process.getuid()) {
|
|
279
|
-
throw new Error('INVALID_UPDATE_LEASE_DIRECTORY_OWNER');
|
|
280
|
-
}
|
|
281
|
-
fsImpl.chmodSync(endpoint.directory, 0o700);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function listenForLease(endpoint, options = {}) {
|
|
285
|
-
const netImpl = options.netImpl || net;
|
|
286
|
-
return new Promise((resolve, reject) => {
|
|
287
|
-
const server = netImpl.createServer((socket) => {
|
|
288
|
-
server[LEASE_CONNECTIONS].add(socket);
|
|
289
|
-
socket.once('close', () => server[LEASE_CONNECTIONS].delete(socket));
|
|
290
|
-
socket.on('error', () => {});
|
|
291
|
-
socket.end(endpoint.magic);
|
|
292
|
-
});
|
|
293
|
-
server[LEASE_CONNECTIONS] = new Set();
|
|
294
|
-
const onError = (error) => {
|
|
295
|
-
server.removeListener('error', onError);
|
|
296
|
-
reject(error);
|
|
297
|
-
};
|
|
298
|
-
server.once('error', onError);
|
|
299
|
-
server.listen({ path: endpoint.path, exclusive: true }, () => {
|
|
300
|
-
server.removeListener('error', onError);
|
|
301
|
-
resolve(server);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function probeExistingLease(endpoint, options = {}) {
|
|
307
|
-
const netImpl = options.netImpl || net;
|
|
308
|
-
const timeoutMs = options.probeTimeoutMs || LEASE_PROBE_TIMEOUT_MS;
|
|
309
|
-
return new Promise((resolve) => {
|
|
310
|
-
let settled = false;
|
|
311
|
-
let received = '';
|
|
312
|
-
let socket;
|
|
313
|
-
const finish = (state, error) => {
|
|
314
|
-
if (settled) return;
|
|
315
|
-
settled = true;
|
|
316
|
-
clearTimeout(timer);
|
|
317
|
-
try {
|
|
318
|
-
socket?.destroy();
|
|
319
|
-
} catch {}
|
|
320
|
-
// oxlint-disable-next-line promise/no-multiple-resolved -- settled is the idempotence guard.
|
|
321
|
-
resolve({ error, state });
|
|
322
|
-
};
|
|
323
|
-
const timer = setTimeout(() => finish('foreign'), timeoutMs);
|
|
324
|
-
timer.unref?.();
|
|
325
|
-
try {
|
|
326
|
-
socket = netImpl.createConnection({ path: endpoint.path });
|
|
327
|
-
socket.setEncoding?.('utf8');
|
|
328
|
-
socket.on('data', (chunk) => {
|
|
329
|
-
received += String(chunk);
|
|
330
|
-
if (Buffer.byteLength(received, 'utf8') > Buffer.byteLength(endpoint.magic, 'utf8')) {
|
|
331
|
-
finish('foreign');
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
socket.once('end', () => finish(received === endpoint.magic ? 'owner' : 'foreign'));
|
|
335
|
-
socket.once('error', (error) => {
|
|
336
|
-
const stale = error?.code === 'ECONNREFUSED' || error?.code === 'ENOENT';
|
|
337
|
-
finish(stale ? 'stale' : 'foreign', error);
|
|
338
|
-
});
|
|
339
|
-
socket.once('close', () => {
|
|
340
|
-
if (!settled) finish(received === endpoint.magic ? 'owner' : 'foreign');
|
|
341
|
-
});
|
|
342
|
-
} catch (error) {
|
|
343
|
-
const stale = error?.code === 'ECONNREFUSED' || error?.code === 'ENOENT';
|
|
344
|
-
finish(stale ? 'stale' : 'foreign', error);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function observeSocketPath(endpoint, options = {}) {
|
|
350
|
-
if (endpoint.platform === 'win32') return { state: 'not_applicable' };
|
|
351
|
-
const fsImpl = options.fsImpl || fs;
|
|
352
|
-
try {
|
|
353
|
-
const stat = fsImpl.lstatSync(endpoint.path);
|
|
354
|
-
if (!stat.isSocket() || stat.isSymbolicLink()) return { state: 'unsafe' };
|
|
355
|
-
return { dev: stat.dev, ino: stat.ino, state: 'socket' };
|
|
356
|
-
} catch (error) {
|
|
357
|
-
return error?.code === 'ENOENT' ? { state: 'missing' } : { state: 'unsafe' };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function removeObservedStaleSocket(endpoint, observed, options = {}) {
|
|
362
|
-
if (endpoint.platform === 'win32') return false;
|
|
363
|
-
const fsImpl = options.fsImpl || fs;
|
|
364
|
-
try {
|
|
365
|
-
const stat = fsImpl.lstatSync(endpoint.path);
|
|
366
|
-
if (observed.state !== 'socket'
|
|
367
|
-
|| !stat.isSocket()
|
|
368
|
-
|| stat.isSymbolicLink()
|
|
369
|
-
|| stat.dev !== observed.dev
|
|
370
|
-
|| stat.ino !== observed.ino) {
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
fsImpl.unlinkSync(endpoint.path);
|
|
374
|
-
return true;
|
|
375
|
-
} catch (error) {
|
|
376
|
-
return error?.code === 'ENOENT' && observed.state === 'missing';
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function recordSocketIdentity(endpoint, options = {}) {
|
|
381
|
-
if (endpoint.platform === 'win32') return undefined;
|
|
382
|
-
const fsImpl = options.fsImpl || fs;
|
|
383
|
-
const stat = fsImpl.lstatSync(endpoint.path);
|
|
384
|
-
if (!stat.isSocket() || stat.isSymbolicLink()) throw new Error('INVALID_UPDATE_LEASE_SOCKET');
|
|
385
|
-
fsImpl.chmodSync(endpoint.path, 0o600);
|
|
386
|
-
return { dev: stat.dev, ino: stat.ino };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function removeOwnedSocket(endpoint, identity, options = {}) {
|
|
390
|
-
if (endpoint.platform === 'win32' || !identity) return;
|
|
391
|
-
const fsImpl = options.fsImpl || fs;
|
|
392
|
-
try {
|
|
393
|
-
const stat = fsImpl.lstatSync(endpoint.path);
|
|
394
|
-
if (stat.isSocket()
|
|
395
|
-
&& !stat.isSymbolicLink()
|
|
396
|
-
&& stat.dev === identity.dev
|
|
397
|
-
&& stat.ino === identity.ino) {
|
|
398
|
-
fsImpl.unlinkSync(endpoint.path);
|
|
399
|
-
}
|
|
400
|
-
} catch {}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function tryAcquireUpdateLease(options = {}) {
|
|
404
|
-
let endpoint;
|
|
405
|
-
try {
|
|
406
|
-
endpoint = deriveUpdateLeaseEndpoint(options.packageRoot, options);
|
|
407
|
-
ensurePrivateSocketDirectory(endpoint, options);
|
|
408
|
-
} catch {
|
|
409
|
-
return { acquired: false, reason: 'unavailable' };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
let server;
|
|
413
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
414
|
-
try {
|
|
415
|
-
server = await listenForLease(endpoint, options);
|
|
416
|
-
break;
|
|
417
|
-
} catch (error) {
|
|
418
|
-
if (error?.code !== 'EADDRINUSE') {
|
|
419
|
-
return { acquired: false, reason: 'unavailable' };
|
|
420
|
-
}
|
|
421
|
-
const observed = observeSocketPath(endpoint, options);
|
|
422
|
-
const probe = await probeExistingLease(endpoint, options);
|
|
423
|
-
if (probe.state === 'owner') return { acquired: false, reason: 'busy' };
|
|
424
|
-
if (probe.state !== 'stale' || attempt > 0) {
|
|
425
|
-
return { acquired: false, reason: 'unavailable' };
|
|
426
|
-
}
|
|
427
|
-
if (endpoint.platform === 'win32') continue;
|
|
428
|
-
if (!removeObservedStaleSocket(endpoint, observed, options)) {
|
|
429
|
-
return { acquired: false, reason: 'unavailable' };
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (!server) return { acquired: false, reason: 'unavailable' };
|
|
434
|
-
|
|
435
|
-
let identity;
|
|
436
|
-
try {
|
|
437
|
-
identity = recordSocketIdentity(endpoint, options);
|
|
438
|
-
} catch {
|
|
439
|
-
await closeServer(server);
|
|
440
|
-
removeOwnedSocket(endpoint, identity, options);
|
|
441
|
-
return { acquired: false, reason: 'unavailable' };
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
let owned = true;
|
|
445
|
-
let released = false;
|
|
446
|
-
server.on('error', () => {
|
|
447
|
-
owned = false;
|
|
448
|
-
});
|
|
449
|
-
server.on('close', () => {
|
|
450
|
-
owned = false;
|
|
451
|
-
});
|
|
452
|
-
const lease = Object.freeze({
|
|
453
|
-
assertOwned() {
|
|
454
|
-
if (!owned || !server.listening) throw new Error('UPDATE_LEASE_LOST');
|
|
455
|
-
},
|
|
456
|
-
async release() {
|
|
457
|
-
if (released) return;
|
|
458
|
-
released = true;
|
|
459
|
-
owned = false;
|
|
460
|
-
await closeServer(server);
|
|
461
|
-
removeOwnedSocket(endpoint, identity, options);
|
|
462
|
-
},
|
|
463
|
-
unref() {
|
|
464
|
-
server.unref?.();
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
return { acquired: true, lease };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function isInstallerMessage(message) {
|
|
471
|
-
if (!message || message.type !== LEASE_HANDOFF_MESSAGE) return false;
|
|
472
|
-
const installer = message.installer;
|
|
473
|
-
return installer
|
|
474
|
-
&& typeof installer === 'object'
|
|
475
|
-
&& typeof message.packageRoot === 'string'
|
|
476
|
-
&& path.isAbsolute(message.packageRoot)
|
|
477
|
-
&& typeof installer.npmCliPath === 'string'
|
|
478
|
-
&& path.isAbsolute(installer.npmCliPath)
|
|
479
|
-
&& Array.isArray(installer.npmArgs)
|
|
480
|
-
&& installer.npmArgs.every((entry) => typeof entry === 'string')
|
|
481
|
-
&& typeof installer.installCwd === 'string'
|
|
482
|
-
&& path.isAbsolute(installer.installCwd);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function runInstallerWorker() {
|
|
486
|
-
let state = 'BOOT';
|
|
487
|
-
let started = false;
|
|
488
|
-
let installLease;
|
|
489
|
-
let installCwd;
|
|
490
|
-
const isControlMessage = (message, type) => message
|
|
491
|
-
&& typeof message === 'object'
|
|
492
|
-
&& Object.getPrototypeOf(message) === Object.prototype
|
|
493
|
-
&& Reflect.ownKeys(message).length === 1
|
|
494
|
-
&& message.type === type;
|
|
495
|
-
const exitWithoutInstall = async (code = 0) => {
|
|
496
|
-
if (state === 'DONE') return;
|
|
497
|
-
state = 'DONE';
|
|
498
|
-
try {
|
|
499
|
-
await installLease?.release();
|
|
500
|
-
} catch {}
|
|
501
|
-
process.exit(code);
|
|
502
|
-
};
|
|
503
|
-
const startInstaller = (installer) => {
|
|
504
|
-
if (started || state !== 'COMMITTED') return;
|
|
505
|
-
started = true;
|
|
506
|
-
try {
|
|
507
|
-
installLease.assertOwned();
|
|
508
|
-
process.chdir(installer.installCwd);
|
|
509
|
-
process.argv = [process.execPath, installer.npmCliPath, ...installer.npmArgs];
|
|
510
|
-
require(installer.npmCliPath);
|
|
511
|
-
} catch {
|
|
512
|
-
process.exit(1);
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
process.once('message', async (message) => {
|
|
517
|
-
if (state !== 'BOOT' || !isInstallerMessage(message)) {
|
|
518
|
-
process.exit(1);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
state = 'PREPARED';
|
|
522
|
-
installCwd = message.installer.installCwd;
|
|
523
|
-
process.once('exit', () => {
|
|
524
|
-
if (!installCwd) return;
|
|
525
|
-
try {
|
|
526
|
-
process.chdir(path.dirname(installCwd));
|
|
527
|
-
fs.rmSync(installCwd, { recursive: true, force: true });
|
|
528
|
-
} catch {}
|
|
529
|
-
});
|
|
530
|
-
const leaseResult = await tryAcquireUpdateLease({
|
|
531
|
-
packageRoot: message.packageRoot,
|
|
532
|
-
role: 'install',
|
|
533
|
-
});
|
|
534
|
-
if (!leaseResult.acquired) {
|
|
535
|
-
const type = leaseResult.reason === 'busy'
|
|
536
|
-
? LEASE_BUSY_MESSAGE
|
|
537
|
-
: LEASE_UNAVAILABLE_MESSAGE;
|
|
538
|
-
if (process.connected) {
|
|
539
|
-
try {
|
|
540
|
-
process.send({ type }, () => process.exit(75));
|
|
541
|
-
return;
|
|
542
|
-
} catch {}
|
|
543
|
-
}
|
|
544
|
-
process.exit(75);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
installLease = leaseResult.lease;
|
|
548
|
-
state = 'LEASED';
|
|
549
|
-
if (!process.connected) {
|
|
550
|
-
await exitWithoutInstall();
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const onControl = (control) => {
|
|
554
|
-
if (state !== 'LEASED') {
|
|
555
|
-
void exitWithoutInstall(1);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if (isControlMessage(control, LEASE_CANCEL_MESSAGE)) {
|
|
559
|
-
void exitWithoutInstall();
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (!isControlMessage(control, LEASE_COMMIT_MESSAGE)) {
|
|
563
|
-
void exitWithoutInstall(1);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
state = 'COMMITTED';
|
|
567
|
-
setImmediate(() => startInstaller(message.installer));
|
|
568
|
-
};
|
|
569
|
-
process.once('message', onControl);
|
|
570
|
-
try {
|
|
571
|
-
process.send({ type: LEASE_ADOPT_MESSAGE });
|
|
572
|
-
} catch {
|
|
573
|
-
process.removeListener('message', onControl);
|
|
574
|
-
await exitWithoutInstall();
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
process.once('disconnect', () => {
|
|
578
|
-
if (state !== 'COMMITTED' && state !== 'DONE') void exitWithoutInstall();
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (require.main === module && process.argv[2] === '--installer-worker') {
|
|
583
|
-
runInstallerWorker();
|
|
584
|
-
} else if (require.main === module && process.argv[2] === '--runtime-broker') {
|
|
585
|
-
void runRuntimeBroker().then((started) => {
|
|
586
|
-
if (!started) process.exitCode = 75;
|
|
587
|
-
}).catch(() => {
|
|
588
|
-
process.exitCode = 1;
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
module.exports = {
|
|
593
|
-
acquireSharedRuntimeLease,
|
|
594
|
-
deriveUpdateLeaseEndpoint,
|
|
595
|
-
runRuntimeBroker,
|
|
596
|
-
tryAcquireUpdateLease,
|
|
597
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const net = require('node:net');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawn } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const LEASE_ADOPT_MESSAGE = 'blun-update-lease-adopted';
|
|
11
|
+
const LEASE_CANCEL_MESSAGE = 'blun-update-lease-cancel';
|
|
12
|
+
const LEASE_COMMIT_MESSAGE = 'blun-update-lease-commit';
|
|
13
|
+
const LEASE_HANDOFF_MESSAGE = 'blun-update-lease-handoff';
|
|
14
|
+
const LEASE_BUSY_MESSAGE = 'blun-update-lease-busy';
|
|
15
|
+
const LEASE_UNAVAILABLE_MESSAGE = 'blun-update-lease-unavailable';
|
|
16
|
+
const LEASE_PROTOCOL_VERSION = 'BLUN_UPDATE_LEASE_V3';
|
|
17
|
+
const LEASE_PROBE_TIMEOUT_MS = 500;
|
|
18
|
+
const MAX_POSIX_SOCKET_PATH_BYTES = 96;
|
|
19
|
+
const LEASE_CONNECTIONS = Symbol('blunLeaseConnections');
|
|
20
|
+
|
|
21
|
+
function canonicalPackageIdentity(packageRoot, options = {}) {
|
|
22
|
+
if (typeof packageRoot !== 'string' || packageRoot.trim().length === 0) {
|
|
23
|
+
throw new Error('UPDATE_PACKAGE_ROOT_REQUIRED');
|
|
24
|
+
}
|
|
25
|
+
const fsImpl = options.fsImpl || fs;
|
|
26
|
+
const platform = options.platform || process.platform;
|
|
27
|
+
const realpath = fsImpl.realpathSync.native || fsImpl.realpathSync;
|
|
28
|
+
let canonical = realpath(path.resolve(packageRoot));
|
|
29
|
+
if (!fsImpl.statSync(canonical).isDirectory()) throw new Error('INVALID_UPDATE_PACKAGE_ROOT');
|
|
30
|
+
canonical = canonical.replaceAll('\\', '/');
|
|
31
|
+
if (platform === 'win32') canonical = canonical.toLowerCase();
|
|
32
|
+
return canonical;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deriveUpdateLeaseEndpoint(packageRoot, options = {}) {
|
|
36
|
+
const platform = options.platform || process.platform;
|
|
37
|
+
const role = options.role === 'install'
|
|
38
|
+
? 'install'
|
|
39
|
+
: options.role === 'runtime'
|
|
40
|
+
? 'runtime'
|
|
41
|
+
: 'notice';
|
|
42
|
+
const identity = canonicalPackageIdentity(packageRoot, options);
|
|
43
|
+
const digest = createHash('sha256')
|
|
44
|
+
.update(`blun-update-v3\0${role}\0${identity}`)
|
|
45
|
+
.digest('hex');
|
|
46
|
+
const magic = `${LEASE_PROTOCOL_VERSION} ${digest}\n`;
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
magic,
|
|
50
|
+
path: `\\\\.\\pipe\\blun-update-v3-${digest.slice(0, 32)}`,
|
|
51
|
+
platform,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uid = options.uid ?? (typeof process.getuid === 'function' ? process.getuid() : undefined);
|
|
56
|
+
const directoryName = `blun-update-${uid ?? digest.slice(0, 12)}`;
|
|
57
|
+
const requestedRoot = String(options.socketDirectory || options.tempDir || os.tmpdir())
|
|
58
|
+
.replaceAll('\\', '/');
|
|
59
|
+
let directory = options.socketDirectory
|
|
60
|
+
? requestedRoot
|
|
61
|
+
: path.posix.join(requestedRoot, directoryName);
|
|
62
|
+
let socketPath = path.posix.join(directory, `lease-${digest.slice(0, 24)}.sock`);
|
|
63
|
+
if (Buffer.byteLength(socketPath, 'utf8') > MAX_POSIX_SOCKET_PATH_BYTES) {
|
|
64
|
+
directory = path.posix.join('/tmp', directoryName);
|
|
65
|
+
socketPath = path.posix.join(directory, `lease-${digest.slice(0, 24)}.sock`);
|
|
66
|
+
}
|
|
67
|
+
if (Buffer.byteLength(socketPath, 'utf8') > MAX_POSIX_SOCKET_PATH_BYTES) {
|
|
68
|
+
throw new Error('UPDATE_LEASE_SOCKET_PATH_TOO_LONG');
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze({ directory, magic, path: socketPath, platform });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function connectRuntimeBroker(endpoint, options = {}) {
|
|
74
|
+
const netImpl = options.netImpl || net;
|
|
75
|
+
const timeoutMs = options.probeTimeoutMs || LEASE_PROBE_TIMEOUT_MS;
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
let settled = false;
|
|
78
|
+
let received = '';
|
|
79
|
+
let socket;
|
|
80
|
+
const finish = (result) => {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
if (!result.acquired) {
|
|
85
|
+
try {
|
|
86
|
+
socket?.destroy();
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
resolve(result);
|
|
90
|
+
};
|
|
91
|
+
const timer = setTimeout(() => finish({ acquired: false, reason: 'unavailable' }), timeoutMs);
|
|
92
|
+
timer.unref?.();
|
|
93
|
+
try {
|
|
94
|
+
socket = netImpl.createConnection({ path: endpoint.path });
|
|
95
|
+
socket.setEncoding?.('utf8');
|
|
96
|
+
socket.once('connect', () => socket.write(endpoint.magic));
|
|
97
|
+
socket.on('data', (chunk) => {
|
|
98
|
+
received += String(chunk);
|
|
99
|
+
if (!received.includes('\n')) return;
|
|
100
|
+
if (received !== endpoint.magic) {
|
|
101
|
+
finish({ acquired: false, reason: 'unavailable' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let owned = true;
|
|
105
|
+
let released = false;
|
|
106
|
+
socket.once('close', () => { owned = false; });
|
|
107
|
+
socket.once('error', () => { owned = false; });
|
|
108
|
+
const lease = Object.freeze({
|
|
109
|
+
assertOwned() {
|
|
110
|
+
if (!owned || socket.destroyed) throw new Error('RUNTIME_LEASE_LOST');
|
|
111
|
+
},
|
|
112
|
+
async release() {
|
|
113
|
+
if (released) return;
|
|
114
|
+
released = true;
|
|
115
|
+
owned = false;
|
|
116
|
+
if (socket.destroyed) return;
|
|
117
|
+
socket.destroy();
|
|
118
|
+
},
|
|
119
|
+
unref() {
|
|
120
|
+
socket.unref?.();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
finish({ acquired: true, lease });
|
|
124
|
+
});
|
|
125
|
+
socket.once('error', () => finish({ acquired: false, reason: 'unavailable' }));
|
|
126
|
+
socket.once('close', () => finish({ acquired: false, reason: 'unavailable' }));
|
|
127
|
+
} catch {
|
|
128
|
+
finish({ acquired: false, reason: 'unavailable' });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function spawnRuntimeBroker(packageRoot, options = {}) {
|
|
134
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
135
|
+
const child = spawnImpl(
|
|
136
|
+
options.execPath || process.execPath,
|
|
137
|
+
[__filename, '--runtime-broker'],
|
|
138
|
+
{
|
|
139
|
+
cwd: os.tmpdir(),
|
|
140
|
+
detached: true,
|
|
141
|
+
env: {
|
|
142
|
+
...process.env,
|
|
143
|
+
BLUN_RUNTIME_BROKER_PACKAGE_ROOT: packageRoot,
|
|
144
|
+
},
|
|
145
|
+
stdio: 'ignore',
|
|
146
|
+
windowsHide: true,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
child.unref?.();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function acquireSharedRuntimeLease(options = {}) {
|
|
153
|
+
const packageRoot = canonicalPackageIdentity(options.packageRoot, options);
|
|
154
|
+
const endpoint = deriveUpdateLeaseEndpoint(packageRoot, { ...options, role: 'runtime' });
|
|
155
|
+
const deadline = Date.now() + (options.startTimeoutMs || 3_000);
|
|
156
|
+
let spawned = false;
|
|
157
|
+
while (Date.now() < deadline) {
|
|
158
|
+
const connected = await connectRuntimeBroker(endpoint, options);
|
|
159
|
+
if (connected.acquired) return connected;
|
|
160
|
+
if (!spawned) {
|
|
161
|
+
spawned = true;
|
|
162
|
+
spawnRuntimeBroker(packageRoot, options);
|
|
163
|
+
}
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
165
|
+
}
|
|
166
|
+
const install = await tryAcquireUpdateLease({ ...options, packageRoot, role: 'install' });
|
|
167
|
+
if (install.acquired) {
|
|
168
|
+
await install.lease.release();
|
|
169
|
+
return { acquired: false, reason: 'unavailable' };
|
|
170
|
+
}
|
|
171
|
+
return { acquired: false, reason: install.reason };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runRuntimeBroker(options = {}) {
|
|
175
|
+
const packageRoot = options.packageRoot
|
|
176
|
+
|| String(process.env.BLUN_RUNTIME_BROKER_PACKAGE_ROOT || '');
|
|
177
|
+
if (!path.isAbsolute(packageRoot)) throw new Error('RUNTIME_BROKER_PACKAGE_ROOT_REQUIRED');
|
|
178
|
+
const install = await tryAcquireUpdateLease({ packageRoot, role: 'install' });
|
|
179
|
+
if (!install.acquired) return false;
|
|
180
|
+
|
|
181
|
+
const endpoint = deriveUpdateLeaseEndpoint(packageRoot, { role: 'runtime' });
|
|
182
|
+
ensurePrivateSocketDirectory(endpoint);
|
|
183
|
+
const holders = new Set();
|
|
184
|
+
let idleTimer;
|
|
185
|
+
let closing = false;
|
|
186
|
+
let identity;
|
|
187
|
+
const closeBroker = async (server) => {
|
|
188
|
+
if (closing) return;
|
|
189
|
+
closing = true;
|
|
190
|
+
clearTimeout(idleTimer);
|
|
191
|
+
await closeServer(server);
|
|
192
|
+
removeOwnedSocket(endpoint, identity);
|
|
193
|
+
await install.lease.release();
|
|
194
|
+
};
|
|
195
|
+
const server = net.createServer((socket) => {
|
|
196
|
+
socket.setEncoding('utf8');
|
|
197
|
+
let received = '';
|
|
198
|
+
let admitted = false;
|
|
199
|
+
const reject = () => {
|
|
200
|
+
try {
|
|
201
|
+
socket.destroy();
|
|
202
|
+
} catch {}
|
|
203
|
+
};
|
|
204
|
+
const onData = (chunk) => {
|
|
205
|
+
if (admitted) return;
|
|
206
|
+
received += String(chunk);
|
|
207
|
+
if (Buffer.byteLength(received, 'utf8') > Buffer.byteLength(endpoint.magic, 'utf8')) {
|
|
208
|
+
reject();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!received.includes('\n')) return;
|
|
212
|
+
if (received !== endpoint.magic) {
|
|
213
|
+
reject();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
admitted = true;
|
|
217
|
+
clearTimeout(idleTimer);
|
|
218
|
+
holders.add(socket);
|
|
219
|
+
socket.write(endpoint.magic);
|
|
220
|
+
};
|
|
221
|
+
socket.on('data', onData);
|
|
222
|
+
socket.on('error', () => {});
|
|
223
|
+
socket.once('close', () => {
|
|
224
|
+
holders.delete(socket);
|
|
225
|
+
if (holders.size === 0 && !closing) {
|
|
226
|
+
idleTimer = setTimeout(() => { void closeBroker(server); }, 0);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
await new Promise((resolve, reject) => {
|
|
232
|
+
const onError = (error) => {
|
|
233
|
+
server.removeListener('error', onError);
|
|
234
|
+
reject(error);
|
|
235
|
+
};
|
|
236
|
+
server.once('error', onError);
|
|
237
|
+
server.listen({ path: endpoint.path, exclusive: true }, () => {
|
|
238
|
+
server.removeListener('error', onError);
|
|
239
|
+
resolve();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
identity = recordSocketIdentity(endpoint);
|
|
243
|
+
} catch {
|
|
244
|
+
await closeServer(server);
|
|
245
|
+
await install.lease.release();
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
install.lease.unref();
|
|
249
|
+
server.unref?.();
|
|
250
|
+
idleTimer = setTimeout(() => { void closeBroker(server); }, 5_000);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function closeServer(server) {
|
|
255
|
+
for (const socket of server[LEASE_CONNECTIONS] || []) {
|
|
256
|
+
try {
|
|
257
|
+
socket.destroy();
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
if (!server.listening) return Promise.resolve();
|
|
261
|
+
try {
|
|
262
|
+
server.close();
|
|
263
|
+
server.unref?.();
|
|
264
|
+
} catch {}
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ensurePrivateSocketDirectory(endpoint, options = {}) {
|
|
269
|
+
if (endpoint.platform === 'win32') return;
|
|
270
|
+
const fsImpl = options.fsImpl || fs;
|
|
271
|
+
fsImpl.mkdirSync(endpoint.directory, { recursive: true, mode: 0o700 });
|
|
272
|
+
const stat = fsImpl.lstatSync(endpoint.directory);
|
|
273
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
274
|
+
throw new Error('INVALID_UPDATE_LEASE_DIRECTORY');
|
|
275
|
+
}
|
|
276
|
+
if (typeof process.getuid === 'function'
|
|
277
|
+
&& Number.isInteger(stat.uid)
|
|
278
|
+
&& stat.uid !== process.getuid()) {
|
|
279
|
+
throw new Error('INVALID_UPDATE_LEASE_DIRECTORY_OWNER');
|
|
280
|
+
}
|
|
281
|
+
fsImpl.chmodSync(endpoint.directory, 0o700);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function listenForLease(endpoint, options = {}) {
|
|
285
|
+
const netImpl = options.netImpl || net;
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const server = netImpl.createServer((socket) => {
|
|
288
|
+
server[LEASE_CONNECTIONS].add(socket);
|
|
289
|
+
socket.once('close', () => server[LEASE_CONNECTIONS].delete(socket));
|
|
290
|
+
socket.on('error', () => {});
|
|
291
|
+
socket.end(endpoint.magic);
|
|
292
|
+
});
|
|
293
|
+
server[LEASE_CONNECTIONS] = new Set();
|
|
294
|
+
const onError = (error) => {
|
|
295
|
+
server.removeListener('error', onError);
|
|
296
|
+
reject(error);
|
|
297
|
+
};
|
|
298
|
+
server.once('error', onError);
|
|
299
|
+
server.listen({ path: endpoint.path, exclusive: true }, () => {
|
|
300
|
+
server.removeListener('error', onError);
|
|
301
|
+
resolve(server);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function probeExistingLease(endpoint, options = {}) {
|
|
307
|
+
const netImpl = options.netImpl || net;
|
|
308
|
+
const timeoutMs = options.probeTimeoutMs || LEASE_PROBE_TIMEOUT_MS;
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
let settled = false;
|
|
311
|
+
let received = '';
|
|
312
|
+
let socket;
|
|
313
|
+
const finish = (state, error) => {
|
|
314
|
+
if (settled) return;
|
|
315
|
+
settled = true;
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
try {
|
|
318
|
+
socket?.destroy();
|
|
319
|
+
} catch {}
|
|
320
|
+
// oxlint-disable-next-line promise/no-multiple-resolved -- settled is the idempotence guard.
|
|
321
|
+
resolve({ error, state });
|
|
322
|
+
};
|
|
323
|
+
const timer = setTimeout(() => finish('foreign'), timeoutMs);
|
|
324
|
+
timer.unref?.();
|
|
325
|
+
try {
|
|
326
|
+
socket = netImpl.createConnection({ path: endpoint.path });
|
|
327
|
+
socket.setEncoding?.('utf8');
|
|
328
|
+
socket.on('data', (chunk) => {
|
|
329
|
+
received += String(chunk);
|
|
330
|
+
if (Buffer.byteLength(received, 'utf8') > Buffer.byteLength(endpoint.magic, 'utf8')) {
|
|
331
|
+
finish('foreign');
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
socket.once('end', () => finish(received === endpoint.magic ? 'owner' : 'foreign'));
|
|
335
|
+
socket.once('error', (error) => {
|
|
336
|
+
const stale = error?.code === 'ECONNREFUSED' || error?.code === 'ENOENT';
|
|
337
|
+
finish(stale ? 'stale' : 'foreign', error);
|
|
338
|
+
});
|
|
339
|
+
socket.once('close', () => {
|
|
340
|
+
if (!settled) finish(received === endpoint.magic ? 'owner' : 'foreign');
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const stale = error?.code === 'ECONNREFUSED' || error?.code === 'ENOENT';
|
|
344
|
+
finish(stale ? 'stale' : 'foreign', error);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function observeSocketPath(endpoint, options = {}) {
|
|
350
|
+
if (endpoint.platform === 'win32') return { state: 'not_applicable' };
|
|
351
|
+
const fsImpl = options.fsImpl || fs;
|
|
352
|
+
try {
|
|
353
|
+
const stat = fsImpl.lstatSync(endpoint.path);
|
|
354
|
+
if (!stat.isSocket() || stat.isSymbolicLink()) return { state: 'unsafe' };
|
|
355
|
+
return { dev: stat.dev, ino: stat.ino, state: 'socket' };
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return error?.code === 'ENOENT' ? { state: 'missing' } : { state: 'unsafe' };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function removeObservedStaleSocket(endpoint, observed, options = {}) {
|
|
362
|
+
if (endpoint.platform === 'win32') return false;
|
|
363
|
+
const fsImpl = options.fsImpl || fs;
|
|
364
|
+
try {
|
|
365
|
+
const stat = fsImpl.lstatSync(endpoint.path);
|
|
366
|
+
if (observed.state !== 'socket'
|
|
367
|
+
|| !stat.isSocket()
|
|
368
|
+
|| stat.isSymbolicLink()
|
|
369
|
+
|| stat.dev !== observed.dev
|
|
370
|
+
|| stat.ino !== observed.ino) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
fsImpl.unlinkSync(endpoint.path);
|
|
374
|
+
return true;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return error?.code === 'ENOENT' && observed.state === 'missing';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function recordSocketIdentity(endpoint, options = {}) {
|
|
381
|
+
if (endpoint.platform === 'win32') return undefined;
|
|
382
|
+
const fsImpl = options.fsImpl || fs;
|
|
383
|
+
const stat = fsImpl.lstatSync(endpoint.path);
|
|
384
|
+
if (!stat.isSocket() || stat.isSymbolicLink()) throw new Error('INVALID_UPDATE_LEASE_SOCKET');
|
|
385
|
+
fsImpl.chmodSync(endpoint.path, 0o600);
|
|
386
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function removeOwnedSocket(endpoint, identity, options = {}) {
|
|
390
|
+
if (endpoint.platform === 'win32' || !identity) return;
|
|
391
|
+
const fsImpl = options.fsImpl || fs;
|
|
392
|
+
try {
|
|
393
|
+
const stat = fsImpl.lstatSync(endpoint.path);
|
|
394
|
+
if (stat.isSocket()
|
|
395
|
+
&& !stat.isSymbolicLink()
|
|
396
|
+
&& stat.dev === identity.dev
|
|
397
|
+
&& stat.ino === identity.ino) {
|
|
398
|
+
fsImpl.unlinkSync(endpoint.path);
|
|
399
|
+
}
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function tryAcquireUpdateLease(options = {}) {
|
|
404
|
+
let endpoint;
|
|
405
|
+
try {
|
|
406
|
+
endpoint = deriveUpdateLeaseEndpoint(options.packageRoot, options);
|
|
407
|
+
ensurePrivateSocketDirectory(endpoint, options);
|
|
408
|
+
} catch {
|
|
409
|
+
return { acquired: false, reason: 'unavailable' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let server;
|
|
413
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
414
|
+
try {
|
|
415
|
+
server = await listenForLease(endpoint, options);
|
|
416
|
+
break;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error?.code !== 'EADDRINUSE') {
|
|
419
|
+
return { acquired: false, reason: 'unavailable' };
|
|
420
|
+
}
|
|
421
|
+
const observed = observeSocketPath(endpoint, options);
|
|
422
|
+
const probe = await probeExistingLease(endpoint, options);
|
|
423
|
+
if (probe.state === 'owner') return { acquired: false, reason: 'busy' };
|
|
424
|
+
if (probe.state !== 'stale' || attempt > 0) {
|
|
425
|
+
return { acquired: false, reason: 'unavailable' };
|
|
426
|
+
}
|
|
427
|
+
if (endpoint.platform === 'win32') continue;
|
|
428
|
+
if (!removeObservedStaleSocket(endpoint, observed, options)) {
|
|
429
|
+
return { acquired: false, reason: 'unavailable' };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!server) return { acquired: false, reason: 'unavailable' };
|
|
434
|
+
|
|
435
|
+
let identity;
|
|
436
|
+
try {
|
|
437
|
+
identity = recordSocketIdentity(endpoint, options);
|
|
438
|
+
} catch {
|
|
439
|
+
await closeServer(server);
|
|
440
|
+
removeOwnedSocket(endpoint, identity, options);
|
|
441
|
+
return { acquired: false, reason: 'unavailable' };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let owned = true;
|
|
445
|
+
let released = false;
|
|
446
|
+
server.on('error', () => {
|
|
447
|
+
owned = false;
|
|
448
|
+
});
|
|
449
|
+
server.on('close', () => {
|
|
450
|
+
owned = false;
|
|
451
|
+
});
|
|
452
|
+
const lease = Object.freeze({
|
|
453
|
+
assertOwned() {
|
|
454
|
+
if (!owned || !server.listening) throw new Error('UPDATE_LEASE_LOST');
|
|
455
|
+
},
|
|
456
|
+
async release() {
|
|
457
|
+
if (released) return;
|
|
458
|
+
released = true;
|
|
459
|
+
owned = false;
|
|
460
|
+
await closeServer(server);
|
|
461
|
+
removeOwnedSocket(endpoint, identity, options);
|
|
462
|
+
},
|
|
463
|
+
unref() {
|
|
464
|
+
server.unref?.();
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
return { acquired: true, lease };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isInstallerMessage(message) {
|
|
471
|
+
if (!message || message.type !== LEASE_HANDOFF_MESSAGE) return false;
|
|
472
|
+
const installer = message.installer;
|
|
473
|
+
return installer
|
|
474
|
+
&& typeof installer === 'object'
|
|
475
|
+
&& typeof message.packageRoot === 'string'
|
|
476
|
+
&& path.isAbsolute(message.packageRoot)
|
|
477
|
+
&& typeof installer.npmCliPath === 'string'
|
|
478
|
+
&& path.isAbsolute(installer.npmCliPath)
|
|
479
|
+
&& Array.isArray(installer.npmArgs)
|
|
480
|
+
&& installer.npmArgs.every((entry) => typeof entry === 'string')
|
|
481
|
+
&& typeof installer.installCwd === 'string'
|
|
482
|
+
&& path.isAbsolute(installer.installCwd);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function runInstallerWorker() {
|
|
486
|
+
let state = 'BOOT';
|
|
487
|
+
let started = false;
|
|
488
|
+
let installLease;
|
|
489
|
+
let installCwd;
|
|
490
|
+
const isControlMessage = (message, type) => message
|
|
491
|
+
&& typeof message === 'object'
|
|
492
|
+
&& Object.getPrototypeOf(message) === Object.prototype
|
|
493
|
+
&& Reflect.ownKeys(message).length === 1
|
|
494
|
+
&& message.type === type;
|
|
495
|
+
const exitWithoutInstall = async (code = 0) => {
|
|
496
|
+
if (state === 'DONE') return;
|
|
497
|
+
state = 'DONE';
|
|
498
|
+
try {
|
|
499
|
+
await installLease?.release();
|
|
500
|
+
} catch {}
|
|
501
|
+
process.exit(code);
|
|
502
|
+
};
|
|
503
|
+
const startInstaller = (installer) => {
|
|
504
|
+
if (started || state !== 'COMMITTED') return;
|
|
505
|
+
started = true;
|
|
506
|
+
try {
|
|
507
|
+
installLease.assertOwned();
|
|
508
|
+
process.chdir(installer.installCwd);
|
|
509
|
+
process.argv = [process.execPath, installer.npmCliPath, ...installer.npmArgs];
|
|
510
|
+
require(installer.npmCliPath);
|
|
511
|
+
} catch {
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
process.once('message', async (message) => {
|
|
517
|
+
if (state !== 'BOOT' || !isInstallerMessage(message)) {
|
|
518
|
+
process.exit(1);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
state = 'PREPARED';
|
|
522
|
+
installCwd = message.installer.installCwd;
|
|
523
|
+
process.once('exit', () => {
|
|
524
|
+
if (!installCwd) return;
|
|
525
|
+
try {
|
|
526
|
+
process.chdir(path.dirname(installCwd));
|
|
527
|
+
fs.rmSync(installCwd, { recursive: true, force: true });
|
|
528
|
+
} catch {}
|
|
529
|
+
});
|
|
530
|
+
const leaseResult = await tryAcquireUpdateLease({
|
|
531
|
+
packageRoot: message.packageRoot,
|
|
532
|
+
role: 'install',
|
|
533
|
+
});
|
|
534
|
+
if (!leaseResult.acquired) {
|
|
535
|
+
const type = leaseResult.reason === 'busy'
|
|
536
|
+
? LEASE_BUSY_MESSAGE
|
|
537
|
+
: LEASE_UNAVAILABLE_MESSAGE;
|
|
538
|
+
if (process.connected) {
|
|
539
|
+
try {
|
|
540
|
+
process.send({ type }, () => process.exit(75));
|
|
541
|
+
return;
|
|
542
|
+
} catch {}
|
|
543
|
+
}
|
|
544
|
+
process.exit(75);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
installLease = leaseResult.lease;
|
|
548
|
+
state = 'LEASED';
|
|
549
|
+
if (!process.connected) {
|
|
550
|
+
await exitWithoutInstall();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const onControl = (control) => {
|
|
554
|
+
if (state !== 'LEASED') {
|
|
555
|
+
void exitWithoutInstall(1);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (isControlMessage(control, LEASE_CANCEL_MESSAGE)) {
|
|
559
|
+
void exitWithoutInstall();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!isControlMessage(control, LEASE_COMMIT_MESSAGE)) {
|
|
563
|
+
void exitWithoutInstall(1);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
state = 'COMMITTED';
|
|
567
|
+
setImmediate(() => startInstaller(message.installer));
|
|
568
|
+
};
|
|
569
|
+
process.once('message', onControl);
|
|
570
|
+
try {
|
|
571
|
+
process.send({ type: LEASE_ADOPT_MESSAGE });
|
|
572
|
+
} catch {
|
|
573
|
+
process.removeListener('message', onControl);
|
|
574
|
+
await exitWithoutInstall();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
process.once('disconnect', () => {
|
|
578
|
+
if (state !== 'COMMITTED' && state !== 'DONE') void exitWithoutInstall();
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (require.main === module && process.argv[2] === '--installer-worker') {
|
|
583
|
+
runInstallerWorker();
|
|
584
|
+
} else if (require.main === module && process.argv[2] === '--runtime-broker') {
|
|
585
|
+
void runRuntimeBroker().then((started) => {
|
|
586
|
+
if (!started) process.exitCode = 75;
|
|
587
|
+
}).catch(() => {
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
module.exports = {
|
|
593
|
+
acquireSharedRuntimeLease,
|
|
594
|
+
deriveUpdateLeaseEndpoint,
|
|
595
|
+
runRuntimeBroker,
|
|
596
|
+
tryAcquireUpdateLease,
|
|
597
|
+
};
|