@lark-apaas/fullstack-cli 1.1.64-alpha.20260902123941 → 1.1.64
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/index.js +41 -2617
- package/package.json +1 -4
- package/templates/scripts/build.sh +12 -0
- package/templates/scripts/dev.js +54 -700
- package/templates/scripts/server-handoff-runtime.mjs +0 -1904
|
@@ -1,1904 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import http from 'node:http';
|
|
5
|
-
import net from 'node:net';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
8
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
|
|
11
|
-
const SCHEMA_VERSION = 7;
|
|
12
|
-
const BUNDLE_ENTRY = 'payload/server.bundle.cjs';
|
|
13
|
-
const MAX_GENERATION_FILES = 50_000;
|
|
14
|
-
const MAX_GENERATION_BYTES = 256 * 1024 * 1024;
|
|
15
|
-
const MAX_GENERATION_FILE_BYTES = 64 * 1024 * 1024;
|
|
16
|
-
const MAX_FILE_INDEX_BYTES = 16 * 1024 * 1024;
|
|
17
|
-
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
18
|
-
const MAX_READINESS_RESPONSE_BYTES = 64 * 1024;
|
|
19
|
-
const SOURCE_RETRY_INITIAL_DELAY_MS = 250;
|
|
20
|
-
const SOURCE_RETRY_MAX_DELAY_MS = 5_000;
|
|
21
|
-
const RUNTIME_CONTROL_ENV_KEYS = new Set([
|
|
22
|
-
'MIAODA_COLD_START_DECISION_FILE',
|
|
23
|
-
'MIAODA_DEPENDENCY_READY_FILE',
|
|
24
|
-
'MIAODA_NEST_BACKEND_OWNER',
|
|
25
|
-
'MIAODA_NEST_BUNDLE_HANDOFF_ENABLED',
|
|
26
|
-
'MIAODA_NEST_BUNDLE_ROOT',
|
|
27
|
-
'MIAODA_NEST_READINESS_PATH',
|
|
28
|
-
'MIAODA_PLATFORM_ENV_NOT_BEFORE_MS',
|
|
29
|
-
'MIAODA_RUNTIME_ENV_FILE',
|
|
30
|
-
'MIAODA_RUNTIME_OWNER_FILE',
|
|
31
|
-
'MIAODA_RUNTIME_PROCESS_REGISTRY_FILE',
|
|
32
|
-
'MIAODA_STARTUP_BOOT_ID',
|
|
33
|
-
'MIAODA_WORKSPACE_ROOT',
|
|
34
|
-
'PWD',
|
|
35
|
-
'WORKSPACE_DIR',
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
const projectRoot = path.resolve(
|
|
39
|
-
process.env.MIAODA_WORKSPACE_ROOT || process.cwd()
|
|
40
|
-
);
|
|
41
|
-
const platformWorkspaceRoot = path.resolve(
|
|
42
|
-
process.env.WORKSPACE_DIR || projectRoot
|
|
43
|
-
);
|
|
44
|
-
const cacheRoot = path.resolve(
|
|
45
|
-
projectRoot,
|
|
46
|
-
process.env.MIAODA_NEST_BUNDLE_ROOT || '.miaoda-cache/server'
|
|
47
|
-
);
|
|
48
|
-
const ownerFile = path.resolve(
|
|
49
|
-
process.env.MIAODA_RUNTIME_OWNER_FILE ||
|
|
50
|
-
'/tmp/miaoda-cold-start/runtime-owner.json'
|
|
51
|
-
);
|
|
52
|
-
const handoffStateFile = path.join(
|
|
53
|
-
path.dirname(ownerFile),
|
|
54
|
-
'handoff-state.json'
|
|
55
|
-
);
|
|
56
|
-
const processRegistryFile = path.resolve(
|
|
57
|
-
process.env.MIAODA_RUNTIME_PROCESS_REGISTRY_FILE ||
|
|
58
|
-
'/tmp/miaoda-cold-start/runtime-processes.json'
|
|
59
|
-
);
|
|
60
|
-
const receiptFile = path.resolve(
|
|
61
|
-
process.env.MIAODA_DEPENDENCY_READY_FILE ||
|
|
62
|
-
'/tmp/miaoda-cold-start/restore-receipt.json'
|
|
63
|
-
);
|
|
64
|
-
const runtimeEnvFile = path.resolve(
|
|
65
|
-
process.env.MIAODA_RUNTIME_ENV_FILE ||
|
|
66
|
-
path.join(platformWorkspaceRoot, '.force', 'environment', 'env')
|
|
67
|
-
);
|
|
68
|
-
const bootId = process.env.MIAODA_STARTUP_BOOT_ID || '';
|
|
69
|
-
const restoreEpochMs = Number(process.env.MIAODA_PLATFORM_ENV_NOT_BEFORE_MS);
|
|
70
|
-
const publicHost = process.env.SERVER_HOST || '0.0.0.0';
|
|
71
|
-
const publicPort = Number(process.env.SERVER_PORT || 3000);
|
|
72
|
-
const logDir = path.resolve(projectRoot, process.env.LOG_DIR || 'logs');
|
|
73
|
-
const configuredClientBasePath = process.env.CLIENT_BASE_PATH;
|
|
74
|
-
const readinessBasePath =
|
|
75
|
-
configuredClientBasePath?.startsWith('/') &&
|
|
76
|
-
!configuredClientBasePath.startsWith('//')
|
|
77
|
-
? configuredClientBasePath.replace(/\/+$/, '')
|
|
78
|
-
: '';
|
|
79
|
-
const readinessPath =
|
|
80
|
-
process.env.MIAODA_NEST_READINESS_PATH ||
|
|
81
|
-
`${readinessBasePath}/__innerapi__/capability/list`;
|
|
82
|
-
const sourceReadyTimeoutMs = positiveDuration(
|
|
83
|
-
'MIAODA_NEST_SOURCE_READY_TIMEOUT_MS',
|
|
84
|
-
60_000
|
|
85
|
-
);
|
|
86
|
-
const shutdownTimeoutMs = positiveDuration(
|
|
87
|
-
'MIAODA_NEST_SHUTDOWN_TIMEOUT_MS',
|
|
88
|
-
5_000
|
|
89
|
-
);
|
|
90
|
-
const drainTimeoutMs = positiveDuration('MIAODA_NEST_DRAIN_TIMEOUT_MS', 5_000);
|
|
91
|
-
const processStartTicks =
|
|
92
|
-
readLinuxProcessStartTicks(process.pid) ||
|
|
93
|
-
String(
|
|
94
|
-
Math.max(1, Math.floor((Date.now() - process.uptime() * 1000) * 1000))
|
|
95
|
-
);
|
|
96
|
-
const rootSourceHashIgnoredDirectories = new Set([
|
|
97
|
-
'.agents',
|
|
98
|
-
'.claude',
|
|
99
|
-
'.git',
|
|
100
|
-
'.miaoda-cache',
|
|
101
|
-
'.miaoda-runtime',
|
|
102
|
-
'.turbo',
|
|
103
|
-
'coverage',
|
|
104
|
-
'dist',
|
|
105
|
-
'logs',
|
|
106
|
-
]);
|
|
107
|
-
const ownedProcessWrapperSource = String.raw`
|
|
108
|
-
const fs = require('node:fs');
|
|
109
|
-
const { spawn, spawnSync } = require('node:child_process');
|
|
110
|
-
const command = process.argv[1];
|
|
111
|
-
const args = JSON.parse(process.argv[2]);
|
|
112
|
-
let finishing;
|
|
113
|
-
|
|
114
|
-
function linuxIdentity(pid) {
|
|
115
|
-
if (process.platform !== 'linux') return undefined;
|
|
116
|
-
try {
|
|
117
|
-
const stat = fs.readFileSync('/proc/' + pid + '/stat', 'utf8');
|
|
118
|
-
const fields = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/);
|
|
119
|
-
return {
|
|
120
|
-
state: fields[0],
|
|
121
|
-
processGroupId: Number(fields[2]),
|
|
122
|
-
processStartTicks: fields[19],
|
|
123
|
-
};
|
|
124
|
-
} catch {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function ownedMembers() {
|
|
130
|
-
const members = [];
|
|
131
|
-
if (process.platform !== 'linux') {
|
|
132
|
-
try {
|
|
133
|
-
const listing = spawnSync('/bin/ps', ['-axo', 'pid=,pgid='], {
|
|
134
|
-
encoding: 'utf8',
|
|
135
|
-
});
|
|
136
|
-
if (listing.status !== 0) return members;
|
|
137
|
-
for (const line of listing.stdout.split(/\r?\n/)) {
|
|
138
|
-
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
139
|
-
if (!match) continue;
|
|
140
|
-
const pid = Number(match[1]);
|
|
141
|
-
if (pid !== process.pid && Number(match[2]) === process.pid) {
|
|
142
|
-
members.push({ pid });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch {}
|
|
146
|
-
return members;
|
|
147
|
-
}
|
|
148
|
-
for (const name of fs.readdirSync('/proc')) {
|
|
149
|
-
if (!/^\d+$/.test(name)) continue;
|
|
150
|
-
const pid = Number(name);
|
|
151
|
-
if (pid === process.pid) continue;
|
|
152
|
-
const identity = linuxIdentity(pid);
|
|
153
|
-
if (
|
|
154
|
-
identity?.processGroupId === process.pid &&
|
|
155
|
-
identity.state !== 'Z'
|
|
156
|
-
) {
|
|
157
|
-
members.push({ pid, processStartTicks: identity.processStartTicks });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return members;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
164
|
-
async function finish(exitCode) {
|
|
165
|
-
if (finishing) return finishing;
|
|
166
|
-
finishing = (async () => {
|
|
167
|
-
try { process.kill(-process.pid, 'SIGTERM'); } catch {}
|
|
168
|
-
const deadline = Date.now() + 500;
|
|
169
|
-
while (ownedMembers().length > 0 && Date.now() < deadline) await sleep(25);
|
|
170
|
-
if (process.platform !== 'linux' && ownedMembers().length > 0) {
|
|
171
|
-
try { process.kill(-process.pid, 'SIGKILL'); } catch {}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
for (const member of ownedMembers()) {
|
|
175
|
-
const current = linuxIdentity(member.pid);
|
|
176
|
-
if (current?.processGroupId !== process.pid ||
|
|
177
|
-
current.processStartTicks !== member.processStartTicks) continue;
|
|
178
|
-
try { process.kill(member.pid, 'SIGKILL'); } catch {}
|
|
179
|
-
}
|
|
180
|
-
const killDeadline = Date.now() + 500;
|
|
181
|
-
while (ownedMembers().length > 0 && Date.now() < killDeadline) await sleep(25);
|
|
182
|
-
process.exit(exitCode);
|
|
183
|
-
})();
|
|
184
|
-
return finishing;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
188
|
-
process.on(signal, () => { void finish(0); });
|
|
189
|
-
}
|
|
190
|
-
const child = spawn(command, args, {
|
|
191
|
-
cwd: process.cwd(),
|
|
192
|
-
env: process.env,
|
|
193
|
-
detached: false,
|
|
194
|
-
shell: false,
|
|
195
|
-
stdio: 'inherit',
|
|
196
|
-
});
|
|
197
|
-
child.once('error', () => { void finish(1); });
|
|
198
|
-
child.once('exit', (code, signal) => {
|
|
199
|
-
void finish(signal || code !== 0 ? (code || 1) : 0);
|
|
200
|
-
});
|
|
201
|
-
`;
|
|
202
|
-
|
|
203
|
-
const children = new Set();
|
|
204
|
-
const proxySockets = new Set();
|
|
205
|
-
const upgradeSockets = new Set();
|
|
206
|
-
let proxy;
|
|
207
|
-
let proxyListening = false;
|
|
208
|
-
let proxyOwnedPublicPort = false;
|
|
209
|
-
let activeUpstream;
|
|
210
|
-
let cacheUpstream;
|
|
211
|
-
let sourceUpstream;
|
|
212
|
-
let promoted = false;
|
|
213
|
-
let cacheReadyAtMs = 0;
|
|
214
|
-
let currentGenerationId = '';
|
|
215
|
-
let shuttingDown = false;
|
|
216
|
-
let shutdownPromise;
|
|
217
|
-
let resolveLifetime;
|
|
218
|
-
const lifetime = new Promise(resolve => {
|
|
219
|
-
resolveLifetime = resolve;
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
function positiveDuration(name, fallback) {
|
|
223
|
-
const value = Number(process.env[name] || fallback);
|
|
224
|
-
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function validateRuntimeConfiguration() {
|
|
228
|
-
if (process.env.MIAODA_NEST_BUNDLE_HANDOFF_ENABLED !== 'true') {
|
|
229
|
-
throw new Error('MIAODA_NEST_BUNDLE_HANDOFF_DISABLED');
|
|
230
|
-
}
|
|
231
|
-
if (!bootId) throw new Error('MIAODA_STARTUP_BOOT_ID_REQUIRED');
|
|
232
|
-
if (!Number.isSafeInteger(restoreEpochMs) || restoreEpochMs <= 0) {
|
|
233
|
-
throw new Error('MIAODA_PLATFORM_ENV_NOT_BEFORE_MS_INVALID');
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
!Number.isSafeInteger(publicPort) ||
|
|
237
|
-
publicPort <= 0 ||
|
|
238
|
-
publicPort > 65_535
|
|
239
|
-
) {
|
|
240
|
-
throw new Error('MIAODA_NEST_PUBLIC_PORT_INVALID');
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function readLinuxProcessStartTicks(pid) {
|
|
245
|
-
return readLinuxProcessIdentity(pid)?.processStartTicks;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function readLinuxProcessIdentity(pid) {
|
|
249
|
-
if (process.platform !== 'linux' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
250
|
-
return undefined;
|
|
251
|
-
try {
|
|
252
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
253
|
-
const commandEnd = stat.lastIndexOf(')');
|
|
254
|
-
if (commandEnd < 0) return undefined;
|
|
255
|
-
const fieldsFromState = stat
|
|
256
|
-
.slice(commandEnd + 1)
|
|
257
|
-
.trim()
|
|
258
|
-
.split(/\s+/);
|
|
259
|
-
const startTicks = fieldsFromState[19];
|
|
260
|
-
const processGroupId = fieldsFromState[2];
|
|
261
|
-
return /^\d+$/.test(startTicks || '') && /^\d+$/.test(processGroupId || '')
|
|
262
|
-
? {
|
|
263
|
-
state: fieldsFromState[0],
|
|
264
|
-
processStartTicks: startTicks,
|
|
265
|
-
processGroupId: Number(processGroupId),
|
|
266
|
-
}
|
|
267
|
-
: undefined;
|
|
268
|
-
} catch {
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function hash(contents) {
|
|
274
|
-
return createHash('sha256').update(contents).digest('hex');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function hasExactKeys(value, expected) {
|
|
278
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
279
|
-
const actual = Object.keys(value).sort();
|
|
280
|
-
return (
|
|
281
|
-
actual.length === expected.length &&
|
|
282
|
-
actual.every((key, index) => key === [...expected].sort()[index])
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function isInside(root, candidate) {
|
|
287
|
-
const relative = path.relative(root, candidate);
|
|
288
|
-
return (
|
|
289
|
-
relative === '' ||
|
|
290
|
-
(!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function walkFiles(root, directory, output) {
|
|
295
|
-
if (!fs.existsSync(directory)) return;
|
|
296
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
297
|
-
if (
|
|
298
|
-
entry.name === 'node_modules' ||
|
|
299
|
-
(directory === root && rootSourceHashIgnoredDirectories.has(entry.name))
|
|
300
|
-
) {
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
const candidate = path.join(directory, entry.name);
|
|
304
|
-
if (entry.isSymbolicLink()) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
`MIAODA_NEST_SOURCE_SYMLINK_REJECTED:${path.relative(root, candidate)}`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (entry.isDirectory()) walkFiles(root, candidate, output);
|
|
310
|
-
else if (entry.isFile()) output.push(candidate);
|
|
311
|
-
else {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`MIAODA_NEST_SOURCE_SPECIAL_FILE_REJECTED:${path.relative(root, candidate)}`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function workspaceSourceSha256() {
|
|
320
|
-
const files = [];
|
|
321
|
-
walkFiles(projectRoot, projectRoot, files);
|
|
322
|
-
const digest = createHash('sha256');
|
|
323
|
-
for (const file of files.sort()) {
|
|
324
|
-
digest.update(path.relative(projectRoot, file).split(path.sep).join('/'));
|
|
325
|
-
digest.update('\0');
|
|
326
|
-
updateHashFromFile(digest, file);
|
|
327
|
-
digest.update('\0');
|
|
328
|
-
}
|
|
329
|
-
return digest.digest('hex');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function updateHashFromFile(digest, file) {
|
|
333
|
-
const descriptor = fs.openSync(file, 'r');
|
|
334
|
-
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
335
|
-
try {
|
|
336
|
-
const before = fs.fstatSync(descriptor);
|
|
337
|
-
let position = 0;
|
|
338
|
-
while (position < before.size) {
|
|
339
|
-
const bytesRead = fs.readSync(
|
|
340
|
-
descriptor,
|
|
341
|
-
buffer,
|
|
342
|
-
0,
|
|
343
|
-
Math.min(buffer.length, before.size - position),
|
|
344
|
-
position
|
|
345
|
-
);
|
|
346
|
-
if (bytesRead === 0) break;
|
|
347
|
-
digest.update(buffer.subarray(0, bytesRead));
|
|
348
|
-
position += bytesRead;
|
|
349
|
-
}
|
|
350
|
-
const after = fs.fstatSync(descriptor);
|
|
351
|
-
if (
|
|
352
|
-
position !== before.size ||
|
|
353
|
-
after.size !== before.size ||
|
|
354
|
-
after.mtimeMs !== before.mtimeMs
|
|
355
|
-
) {
|
|
356
|
-
throw new Error(`MIAODA_NEST_SOURCE_CHANGED_WHILE_HASHING:${file}`);
|
|
357
|
-
}
|
|
358
|
-
} finally {
|
|
359
|
-
fs.closeSync(descriptor);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function stableJson(value) {
|
|
364
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
365
|
-
if (value && typeof value === 'object') {
|
|
366
|
-
return `{${Object.entries(value)
|
|
367
|
-
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
368
|
-
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
369
|
-
.join(',')}}`;
|
|
370
|
-
}
|
|
371
|
-
return JSON.stringify(value);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function compareCodeUnits(left, right) {
|
|
375
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function runtimeIdentity() {
|
|
379
|
-
const header = process.report?.getReport()?.header;
|
|
380
|
-
return {
|
|
381
|
-
nodeVersion: process.versions.node,
|
|
382
|
-
nodeModulesAbi: process.versions.modules,
|
|
383
|
-
platform: process.platform,
|
|
384
|
-
arch: process.arch,
|
|
385
|
-
libc:
|
|
386
|
-
typeof header?.glibcVersionRuntime === 'string'
|
|
387
|
-
? `glibc-${header.glibcVersionRuntime}`
|
|
388
|
-
: process.platform === 'linux'
|
|
389
|
-
? 'linux-unknown-libc'
|
|
390
|
-
: 'not-applicable',
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function validatedMetadataFile(generationRoot, name, maxBytes) {
|
|
395
|
-
const requested = path.join(generationRoot, name);
|
|
396
|
-
const requestedStats = fs.lstatSync(requested);
|
|
397
|
-
if (requestedStats.isSymbolicLink()) {
|
|
398
|
-
throw new Error(`MIAODA_NEST_CACHE_ARTIFACT_SYMLINK:${name}`);
|
|
399
|
-
}
|
|
400
|
-
if (!requestedStats.isFile() || requestedStats.size > maxBytes) {
|
|
401
|
-
throw new Error(`MIAODA_NEST_CACHE_METADATA_SIZE_INVALID:${name}`);
|
|
402
|
-
}
|
|
403
|
-
const real = fs.realpathSync(requested);
|
|
404
|
-
if (!isInside(generationRoot, real) || !fs.statSync(real).isFile()) {
|
|
405
|
-
throw new Error(`MIAODA_NEST_CACHE_ARTIFACT_ESCAPED:${name}`);
|
|
406
|
-
}
|
|
407
|
-
return real;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function validateFileIndex(generationRoot, manifest, fileIndexFile) {
|
|
411
|
-
const indexStats = fs.statSync(fileIndexFile);
|
|
412
|
-
if (indexStats.size > MAX_FILE_INDEX_BYTES) {
|
|
413
|
-
throw new Error('MIAODA_NEST_CACHE_FILE_INDEX_TOO_LARGE');
|
|
414
|
-
}
|
|
415
|
-
const indexContents = fs.readFileSync(fileIndexFile);
|
|
416
|
-
if (hash(indexContents) !== manifest.fileIndexSha256) {
|
|
417
|
-
throw new Error('MIAODA_NEST_CACHE_FILE_INDEX_HASH_MISMATCH');
|
|
418
|
-
}
|
|
419
|
-
const files = JSON.parse(indexContents.toString('utf8'));
|
|
420
|
-
if (
|
|
421
|
-
!Array.isArray(files) ||
|
|
422
|
-
files.length === 0 ||
|
|
423
|
-
files.length > MAX_GENERATION_FILES
|
|
424
|
-
) {
|
|
425
|
-
throw new Error('MIAODA_NEST_CACHE_FILE_INDEX_INVALID');
|
|
426
|
-
}
|
|
427
|
-
const expected = new Map();
|
|
428
|
-
let previous = '';
|
|
429
|
-
let totalBytes = 0;
|
|
430
|
-
for (const file of files) {
|
|
431
|
-
if (
|
|
432
|
-
!hasExactKeys(file, ['path', 'sha256', 'size', 'mode']) ||
|
|
433
|
-
typeof file.path !== 'string' ||
|
|
434
|
-
!file.path.startsWith('payload/') ||
|
|
435
|
-
file.path.split('/').includes('node_modules') ||
|
|
436
|
-
file.path.includes('\\') ||
|
|
437
|
-
path.posix.isAbsolute(file.path) ||
|
|
438
|
-
path.posix.normalize(file.path) !== file.path ||
|
|
439
|
-
file.path <= previous ||
|
|
440
|
-
!/^[a-f0-9]{64}$/.test(file.sha256 || '') ||
|
|
441
|
-
!Number.isSafeInteger(file.size) ||
|
|
442
|
-
file.size < 0 ||
|
|
443
|
-
file.size > MAX_GENERATION_FILE_BYTES ||
|
|
444
|
-
!Number.isSafeInteger(file.mode) ||
|
|
445
|
-
file.mode < 0 ||
|
|
446
|
-
file.mode > 0o777
|
|
447
|
-
) {
|
|
448
|
-
throw new Error('MIAODA_NEST_CACHE_FILE_INDEX_INVALID');
|
|
449
|
-
}
|
|
450
|
-
previous = file.path;
|
|
451
|
-
totalBytes += file.size;
|
|
452
|
-
if (totalBytes > MAX_GENERATION_BYTES) {
|
|
453
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATION_TOO_LARGE');
|
|
454
|
-
}
|
|
455
|
-
expected.set(file.path, file);
|
|
456
|
-
}
|
|
457
|
-
const payloadRoot = path.join(generationRoot, 'payload');
|
|
458
|
-
if (fs.lstatSync(payloadRoot).isSymbolicLink()) {
|
|
459
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_SYMLINK');
|
|
460
|
-
}
|
|
461
|
-
const realPayloadRoot = fs.realpathSync(payloadRoot);
|
|
462
|
-
if (!isInside(generationRoot, realPayloadRoot)) {
|
|
463
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_ESCAPED');
|
|
464
|
-
}
|
|
465
|
-
const seen = new Set();
|
|
466
|
-
const visit = directory => {
|
|
467
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
468
|
-
const requested = path.join(directory, entry.name);
|
|
469
|
-
if (entry.isSymbolicLink()) {
|
|
470
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_SYMLINK');
|
|
471
|
-
}
|
|
472
|
-
if (entry.isDirectory()) {
|
|
473
|
-
if (entry.name === 'node_modules') {
|
|
474
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_NODE_MODULES_FORBIDDEN');
|
|
475
|
-
}
|
|
476
|
-
visit(requested);
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (!entry.isFile()) {
|
|
480
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_SPECIAL_FILE');
|
|
481
|
-
}
|
|
482
|
-
const real = fs.realpathSync(requested);
|
|
483
|
-
if (!isInside(realPayloadRoot, real)) {
|
|
484
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_ESCAPED');
|
|
485
|
-
}
|
|
486
|
-
const relative = `payload/${path
|
|
487
|
-
.relative(realPayloadRoot, real)
|
|
488
|
-
.split(path.sep)
|
|
489
|
-
.join('/')}`;
|
|
490
|
-
const record = expected.get(relative);
|
|
491
|
-
const stats = fs.statSync(real);
|
|
492
|
-
const contents = fs.readFileSync(real);
|
|
493
|
-
if (
|
|
494
|
-
!record ||
|
|
495
|
-
stats.nlink !== 1 ||
|
|
496
|
-
contents.length !== record.size ||
|
|
497
|
-
(stats.mode & 0o777) !== record.mode ||
|
|
498
|
-
hash(contents) !== record.sha256
|
|
499
|
-
) {
|
|
500
|
-
throw new Error(`MIAODA_NEST_CACHE_PAYLOAD_MISMATCH:${relative}`);
|
|
501
|
-
}
|
|
502
|
-
seen.add(relative);
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
visit(realPayloadRoot);
|
|
506
|
-
if (seen.size !== expected.size) {
|
|
507
|
-
throw new Error('MIAODA_NEST_CACHE_PAYLOAD_FILE_SET_MISMATCH');
|
|
508
|
-
}
|
|
509
|
-
return expected;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function readValidatedGeneration() {
|
|
513
|
-
const realProjectRoot = fs.realpathSync(projectRoot);
|
|
514
|
-
const realCacheRoot = fs.realpathSync(cacheRoot);
|
|
515
|
-
if (!isInside(realProjectRoot, realCacheRoot)) {
|
|
516
|
-
throw new Error('MIAODA_NEST_CACHE_ROOT_ESCAPED_PROJECT');
|
|
517
|
-
}
|
|
518
|
-
const requestedGenerationsRoot = path.join(realCacheRoot, 'generations');
|
|
519
|
-
if (fs.lstatSync(requestedGenerationsRoot).isSymbolicLink()) {
|
|
520
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATIONS_SYMLINK');
|
|
521
|
-
}
|
|
522
|
-
const generationsRoot = fs.realpathSync(requestedGenerationsRoot);
|
|
523
|
-
if (!isInside(realCacheRoot, generationsRoot)) {
|
|
524
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATIONS_ESCAPED');
|
|
525
|
-
}
|
|
526
|
-
const entries = fs.readdirSync(generationsRoot, { withFileTypes: true });
|
|
527
|
-
if (
|
|
528
|
-
entries.length !== 1 ||
|
|
529
|
-
!entries[0].isDirectory() ||
|
|
530
|
-
entries[0].isSymbolicLink() ||
|
|
531
|
-
!/^[a-f0-9]{64}$/.test(entries[0].name)
|
|
532
|
-
) {
|
|
533
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATION_SELECTION_INVALID');
|
|
534
|
-
}
|
|
535
|
-
const generationId = entries[0].name;
|
|
536
|
-
const requestedGenerationRoot = path.join(generationsRoot, generationId);
|
|
537
|
-
if (fs.lstatSync(requestedGenerationRoot).isSymbolicLink()) {
|
|
538
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATION_SYMLINK');
|
|
539
|
-
}
|
|
540
|
-
const generationRoot = fs.realpathSync(requestedGenerationRoot);
|
|
541
|
-
if (!isInside(generationsRoot, generationRoot)) {
|
|
542
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATION_ESCAPED');
|
|
543
|
-
}
|
|
544
|
-
const generationEntries = fs.readdirSync(generationRoot).sort();
|
|
545
|
-
if (
|
|
546
|
-
stableJson(generationEntries) !==
|
|
547
|
-
stableJson(['files.mtree.json', 'manifest.json', 'payload'])
|
|
548
|
-
) {
|
|
549
|
-
throw new Error('MIAODA_NEST_CACHE_GENERATION_FILE_SET_INVALID');
|
|
550
|
-
}
|
|
551
|
-
const manifestFile = validatedMetadataFile(
|
|
552
|
-
generationRoot,
|
|
553
|
-
'manifest.json',
|
|
554
|
-
MAX_MANIFEST_BYTES
|
|
555
|
-
);
|
|
556
|
-
const fileIndexFile = validatedMetadataFile(
|
|
557
|
-
generationRoot,
|
|
558
|
-
'files.mtree.json',
|
|
559
|
-
MAX_FILE_INDEX_BYTES
|
|
560
|
-
);
|
|
561
|
-
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
562
|
-
if (
|
|
563
|
-
!hasExactKeys(manifest, [
|
|
564
|
-
'schemaVersion',
|
|
565
|
-
'generationId',
|
|
566
|
-
'sourceSha256',
|
|
567
|
-
'expectedCapabilityIds',
|
|
568
|
-
'runtime',
|
|
569
|
-
'fileIndexSha256',
|
|
570
|
-
]) ||
|
|
571
|
-
!hasExactKeys(manifest.runtime, [
|
|
572
|
-
'nodeVersion',
|
|
573
|
-
'nodeModulesAbi',
|
|
574
|
-
'platform',
|
|
575
|
-
'arch',
|
|
576
|
-
'libc',
|
|
577
|
-
])
|
|
578
|
-
) {
|
|
579
|
-
throw new Error('MIAODA_NEST_CACHE_MANIFEST_SCHEMA_INVALID');
|
|
580
|
-
}
|
|
581
|
-
if (
|
|
582
|
-
manifest.schemaVersion !== SCHEMA_VERSION ||
|
|
583
|
-
manifest.generationId !== generationId ||
|
|
584
|
-
!Array.isArray(manifest.expectedCapabilityIds) ||
|
|
585
|
-
manifest.expectedCapabilityIds.some(
|
|
586
|
-
(id, index) =>
|
|
587
|
-
typeof id !== 'string' ||
|
|
588
|
-
id.length === 0 ||
|
|
589
|
-
id.length > 256 ||
|
|
590
|
-
(index > 0 && id <= manifest.expectedCapabilityIds[index - 1])
|
|
591
|
-
) ||
|
|
592
|
-
manifest.sourceSha256 !== workspaceSourceSha256() ||
|
|
593
|
-
stableJson(manifest.runtime) !== stableJson(runtimeIdentity()) ||
|
|
594
|
-
!/^[a-f0-9]{64}$/.test(manifest.fileIndexSha256 || '')
|
|
595
|
-
) {
|
|
596
|
-
throw new Error('MIAODA_NEST_CACHE_MANIFEST_MISMATCH');
|
|
597
|
-
}
|
|
598
|
-
const files = validateFileIndex(generationRoot, manifest, fileIndexFile);
|
|
599
|
-
if (!files.has(BUNDLE_ENTRY)) {
|
|
600
|
-
throw new Error('MIAODA_NEST_CACHE_ENTRY_INVALID');
|
|
601
|
-
}
|
|
602
|
-
const computedGenerationId = hash(
|
|
603
|
-
stableJson({
|
|
604
|
-
expectedCapabilityIds: manifest.expectedCapabilityIds,
|
|
605
|
-
fileIndexSha256: manifest.fileIndexSha256,
|
|
606
|
-
runtime: manifest.runtime,
|
|
607
|
-
schemaVersion: manifest.schemaVersion,
|
|
608
|
-
sourceSha256: manifest.sourceSha256,
|
|
609
|
-
})
|
|
610
|
-
);
|
|
611
|
-
if (computedGenerationId !== generationId) {
|
|
612
|
-
throw new Error('MIAODA_NEST_CACHE_MANIFEST_MISMATCH:generationId');
|
|
613
|
-
}
|
|
614
|
-
const entryFile = path.join(generationRoot, ...BUNDLE_ENTRY.split('/'));
|
|
615
|
-
return {
|
|
616
|
-
generationId,
|
|
617
|
-
entryFile,
|
|
618
|
-
expectedCapabilityIds: manifest.expectedCapabilityIds,
|
|
619
|
-
workspaceNodeModulesPath: path.join(realProjectRoot, 'node_modules'),
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function probeHost(host) {
|
|
624
|
-
if (!host || host === '0.0.0.0') return '127.0.0.1';
|
|
625
|
-
if (host === '::' || host === '[::]') return '::1';
|
|
626
|
-
return host.replace(/^\[|\]$/g, '');
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function reservePort(host) {
|
|
630
|
-
return new Promise((resolve, reject) => {
|
|
631
|
-
const server = net.createServer();
|
|
632
|
-
server.once('error', reject);
|
|
633
|
-
server.listen(0, host, () => {
|
|
634
|
-
const address = server.address();
|
|
635
|
-
if (!address || typeof address === 'string') {
|
|
636
|
-
server.close(() =>
|
|
637
|
-
reject(new Error('MIAODA_NEST_INTERNAL_PORT_RESERVATION_FAILED'))
|
|
638
|
-
);
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
server.close(error => (error ? reject(error) : resolve(address.port)));
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function writeLog(stream, file, mirror) {
|
|
647
|
-
if (!stream) return;
|
|
648
|
-
const output = fs.createWriteStream(file, { flags: 'a', mode: 0o600 });
|
|
649
|
-
stream.on('data', chunk => {
|
|
650
|
-
output.write(chunk);
|
|
651
|
-
mirror.write(chunk);
|
|
652
|
-
});
|
|
653
|
-
stream.once('end', () => output.end());
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function childRunning(record) {
|
|
657
|
-
const child = record?.child;
|
|
658
|
-
return Boolean(
|
|
659
|
-
child?.pid && child.exitCode === null && child.signalCode === null
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export function ownedGroupRunning(
|
|
664
|
-
record,
|
|
665
|
-
{
|
|
666
|
-
readIdentity = readLinuxProcessIdentity,
|
|
667
|
-
listProcessIds = () => fs.readdirSync('/proc'),
|
|
668
|
-
} = {}
|
|
669
|
-
) {
|
|
670
|
-
const pid = record?.child?.pid;
|
|
671
|
-
if (!pid) return false;
|
|
672
|
-
if (process.platform === 'linux') {
|
|
673
|
-
const leaderIdentity = readIdentity(pid);
|
|
674
|
-
if (
|
|
675
|
-
leaderIdentity &&
|
|
676
|
-
(leaderIdentity.processStartTicks !== record.processStartTicks ||
|
|
677
|
-
leaderIdentity.processGroupId !== pid)
|
|
678
|
-
) {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
// If the group leader already exited, Linux keeps its numeric process-group
|
|
682
|
-
// id reserved while owned descendants remain. A later foreign leader can
|
|
683
|
-
// only reuse the pid after the old group disappears, and is rejected above
|
|
684
|
-
// by its different birth identity. A zombie is already dead: it cannot hold
|
|
685
|
-
// a port or execute code, and a container PID 1 is not guaranteed to reap it
|
|
686
|
-
// promptly. Require at least one non-zombie member before signaling/waiting.
|
|
687
|
-
try {
|
|
688
|
-
for (const name of listProcessIds()) {
|
|
689
|
-
if (!/^\d+$/.test(name)) continue;
|
|
690
|
-
const member = readIdentity(Number(name));
|
|
691
|
-
if (member?.processGroupId === pid && member.state !== 'Z') {
|
|
692
|
-
const currentLeader = readIdentity(pid);
|
|
693
|
-
return Boolean(
|
|
694
|
-
!currentLeader ||
|
|
695
|
-
(currentLeader.processStartTicks === record.processStartTicks &&
|
|
696
|
-
currentLeader.processGroupId === pid)
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return false;
|
|
701
|
-
} catch {}
|
|
702
|
-
}
|
|
703
|
-
try {
|
|
704
|
-
process.kill(-pid, 0);
|
|
705
|
-
return true;
|
|
706
|
-
} catch (error) {
|
|
707
|
-
return error?.code === 'EPERM';
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function waitForChild(record) {
|
|
712
|
-
if (!childRunning(record)) return Promise.resolve();
|
|
713
|
-
return new Promise(resolve => record.child.once('exit', () => resolve()));
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function isCapabilityReadinessPayload(contents, expectedCapabilityIds = []) {
|
|
717
|
-
if (contents.length > MAX_READINESS_RESPONSE_BYTES) return false;
|
|
718
|
-
try {
|
|
719
|
-
const payload = JSON.parse(contents.toString('utf8'));
|
|
720
|
-
if (
|
|
721
|
-
!(
|
|
722
|
-
payload &&
|
|
723
|
-
typeof payload === 'object' &&
|
|
724
|
-
payload.status_code === '0' &&
|
|
725
|
-
payload.data &&
|
|
726
|
-
typeof payload.data === 'object' &&
|
|
727
|
-
!Array.isArray(payload.data) &&
|
|
728
|
-
Array.isArray(payload.data.capabilities)
|
|
729
|
-
)
|
|
730
|
-
) {
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
const actualIds = new Set(
|
|
734
|
-
payload.data.capabilities
|
|
735
|
-
.map(capability => capability?.id)
|
|
736
|
-
.filter(id => typeof id === 'string' && id.length > 0)
|
|
737
|
-
);
|
|
738
|
-
return expectedCapabilityIds.every(id => actualIds.has(id));
|
|
739
|
-
} catch {
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function spawnOwned(name, command, args, options = {}) {
|
|
745
|
-
const child = spawn(
|
|
746
|
-
process.execPath,
|
|
747
|
-
['-e', ownedProcessWrapperSource, command, JSON.stringify(args)],
|
|
748
|
-
{
|
|
749
|
-
cwd: projectRoot,
|
|
750
|
-
detached: true,
|
|
751
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
752
|
-
env: {
|
|
753
|
-
...(options.baseEnv || process.env),
|
|
754
|
-
...options.env,
|
|
755
|
-
PWD: projectRoot,
|
|
756
|
-
},
|
|
757
|
-
shell: false,
|
|
758
|
-
}
|
|
759
|
-
);
|
|
760
|
-
const childProcessStartTicks =
|
|
761
|
-
readLinuxProcessStartTicks(child.pid) ||
|
|
762
|
-
(process.platform === 'linux'
|
|
763
|
-
? undefined
|
|
764
|
-
: String(Math.max(1, Date.now() * 1000 + (child.pid || 0))));
|
|
765
|
-
if (!child.pid || !childProcessStartTicks) {
|
|
766
|
-
if (child.pid) {
|
|
767
|
-
try {
|
|
768
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
769
|
-
} catch {}
|
|
770
|
-
}
|
|
771
|
-
throw new Error(`MIAODA_NEST_CHILD_IDENTITY_UNAVAILABLE:${name}`);
|
|
772
|
-
}
|
|
773
|
-
const record = {
|
|
774
|
-
name,
|
|
775
|
-
child,
|
|
776
|
-
processStartTicks: childProcessStartTicks,
|
|
777
|
-
expectedExit: false,
|
|
778
|
-
};
|
|
779
|
-
children.add(record);
|
|
780
|
-
try {
|
|
781
|
-
publishProcessRegistry();
|
|
782
|
-
} catch (error) {
|
|
783
|
-
children.delete(record);
|
|
784
|
-
try {
|
|
785
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
786
|
-
} catch {}
|
|
787
|
-
throw error;
|
|
788
|
-
}
|
|
789
|
-
const logFile = path.join(logDir, options.logName || `${name}.std.log`);
|
|
790
|
-
writeLog(child.stdout, logFile, process.stdout);
|
|
791
|
-
writeLog(child.stderr, logFile, process.stderr);
|
|
792
|
-
child.once('exit', (code, signal) => {
|
|
793
|
-
if (shuttingDown || record.expectedExit) return;
|
|
794
|
-
if (name === 'cache' && promoted) return;
|
|
795
|
-
if (name === 'source' && activeUpstream?.record !== record) return;
|
|
796
|
-
void shutdown(
|
|
797
|
-
`${name}-exit:${signal || (code ?? 1)}`,
|
|
798
|
-
code === 0 ? 1 : (code ?? 1)
|
|
799
|
-
);
|
|
800
|
-
});
|
|
801
|
-
child.once('error', error => {
|
|
802
|
-
if (
|
|
803
|
-
!shuttingDown &&
|
|
804
|
-
!record.expectedExit &&
|
|
805
|
-
!(name === 'source' && activeUpstream?.record !== record)
|
|
806
|
-
) {
|
|
807
|
-
void shutdown(`${name}-spawn-error:${error.message}`, 1);
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
return record;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function terminateOwned(record) {
|
|
814
|
-
record.expectedExit = true;
|
|
815
|
-
const pid = record.child.pid;
|
|
816
|
-
if (!pid) {
|
|
817
|
-
children.delete(record);
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const waitForGroup = async () => {
|
|
821
|
-
const deadline = Date.now() + shutdownTimeoutMs;
|
|
822
|
-
while (ownedGroupRunning(record) && Date.now() < deadline) {
|
|
823
|
-
await new Promise(resolve => setTimeout(resolve, 25));
|
|
824
|
-
}
|
|
825
|
-
return !ownedGroupRunning(record);
|
|
826
|
-
};
|
|
827
|
-
if (ownedGroupRunning(record)) {
|
|
828
|
-
try {
|
|
829
|
-
process.kill(-pid, 'SIGTERM');
|
|
830
|
-
} catch {}
|
|
831
|
-
}
|
|
832
|
-
if (!(await waitForGroup())) {
|
|
833
|
-
try {
|
|
834
|
-
process.kill(-pid, 'SIGKILL');
|
|
835
|
-
} catch {}
|
|
836
|
-
if (!(await waitForGroup())) {
|
|
837
|
-
throw new Error(
|
|
838
|
-
`MIAODA_NEST_OWNED_GROUP_STILL_RUNNING:${record.name}:${pid}`
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
await waitForChild(record);
|
|
843
|
-
children.delete(record);
|
|
844
|
-
publishProcessRegistry();
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function waitForHttp(record, upstream, timeoutMs, expectedCapabilityIds) {
|
|
848
|
-
const deadline = Date.now() + timeoutMs;
|
|
849
|
-
return new Promise((resolve, reject) => {
|
|
850
|
-
let settled = false;
|
|
851
|
-
let retryTimer;
|
|
852
|
-
const finish = error => {
|
|
853
|
-
if (settled) return;
|
|
854
|
-
settled = true;
|
|
855
|
-
if (retryTimer) clearTimeout(retryTimer);
|
|
856
|
-
if (error) reject(error);
|
|
857
|
-
else resolve();
|
|
858
|
-
};
|
|
859
|
-
const retry = () => {
|
|
860
|
-
if (settled) return;
|
|
861
|
-
const remaining = deadline - Date.now();
|
|
862
|
-
if (remaining <= 0) {
|
|
863
|
-
finish(new Error(`${record.name} HTTP readiness timeout`));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
retryTimer = setTimeout(attempt, Math.min(50, remaining));
|
|
867
|
-
};
|
|
868
|
-
const attempt = () => {
|
|
869
|
-
if (settled) return;
|
|
870
|
-
if (shuttingDown)
|
|
871
|
-
return finish(new Error('MIAODA_NEST_RUNTIME_STOPPING'));
|
|
872
|
-
if (!childRunning(record))
|
|
873
|
-
return finish(new Error(`${record.name} exited before HTTP readiness`));
|
|
874
|
-
const remaining = deadline - Date.now();
|
|
875
|
-
if (remaining <= 0)
|
|
876
|
-
return finish(new Error(`${record.name} HTTP readiness timeout`));
|
|
877
|
-
let attemptSettled = false;
|
|
878
|
-
const request = http.get(
|
|
879
|
-
{
|
|
880
|
-
host: upstream.host,
|
|
881
|
-
port: upstream.port,
|
|
882
|
-
path: readinessPath,
|
|
883
|
-
},
|
|
884
|
-
response => {
|
|
885
|
-
if (attemptSettled || settled) {
|
|
886
|
-
response.destroy();
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
if (response.statusCode !== 200) {
|
|
890
|
-
attemptSettled = true;
|
|
891
|
-
response.resume();
|
|
892
|
-
retry();
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const chunks = [];
|
|
896
|
-
let responseBytes = 0;
|
|
897
|
-
response.on('data', chunk => {
|
|
898
|
-
if (attemptSettled || settled) return;
|
|
899
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
900
|
-
responseBytes += buffer.length;
|
|
901
|
-
if (responseBytes > MAX_READINESS_RESPONSE_BYTES) {
|
|
902
|
-
attemptSettled = true;
|
|
903
|
-
response.destroy();
|
|
904
|
-
retry();
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
chunks.push(buffer);
|
|
908
|
-
});
|
|
909
|
-
response.once('end', () => {
|
|
910
|
-
if (attemptSettled || settled) return;
|
|
911
|
-
attemptSettled = true;
|
|
912
|
-
if (
|
|
913
|
-
isCapabilityReadinessPayload(
|
|
914
|
-
Buffer.concat(chunks),
|
|
915
|
-
expectedCapabilityIds
|
|
916
|
-
)
|
|
917
|
-
)
|
|
918
|
-
finish();
|
|
919
|
-
else retry();
|
|
920
|
-
});
|
|
921
|
-
response.once('error', () => {
|
|
922
|
-
if (attemptSettled || settled) return;
|
|
923
|
-
attemptSettled = true;
|
|
924
|
-
retry();
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
);
|
|
928
|
-
request.setTimeout(remaining, () => {
|
|
929
|
-
request.destroy(new Error(`${record.name} HTTP readiness timeout`));
|
|
930
|
-
});
|
|
931
|
-
request.once('error', error => {
|
|
932
|
-
if (attemptSettled || settled) return;
|
|
933
|
-
attemptSettled = true;
|
|
934
|
-
if (
|
|
935
|
-
Date.now() >= deadline ||
|
|
936
|
-
error.message === `${record.name} HTTP readiness timeout`
|
|
937
|
-
) {
|
|
938
|
-
finish(new Error(`${record.name} HTTP readiness timeout`));
|
|
939
|
-
} else {
|
|
940
|
-
retry();
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
};
|
|
944
|
-
attempt();
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function retain(upstream) {
|
|
949
|
-
upstream.inflight += 1;
|
|
950
|
-
let released = false;
|
|
951
|
-
return () => {
|
|
952
|
-
if (released) return;
|
|
953
|
-
released = true;
|
|
954
|
-
upstream.inflight = Math.max(0, upstream.inflight - 1);
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function createProxy() {
|
|
959
|
-
const server = http.createServer((request, response) => {
|
|
960
|
-
const upstream = activeUpstream;
|
|
961
|
-
if (!upstream) {
|
|
962
|
-
response.writeHead(503, { 'content-type': 'application/json' });
|
|
963
|
-
response.end('{"error":"MIAODA_NEST_UPSTREAM_PENDING"}\n');
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const release = retain(upstream);
|
|
967
|
-
const upstreamRequest = http.request(
|
|
968
|
-
{
|
|
969
|
-
host: upstream.host,
|
|
970
|
-
port: upstream.port,
|
|
971
|
-
method: request.method,
|
|
972
|
-
path: request.url,
|
|
973
|
-
headers: request.headers,
|
|
974
|
-
},
|
|
975
|
-
upstreamResponse => {
|
|
976
|
-
response.writeHead(
|
|
977
|
-
upstreamResponse.statusCode || 502,
|
|
978
|
-
upstreamResponse.statusMessage || '',
|
|
979
|
-
upstreamResponse.headers
|
|
980
|
-
);
|
|
981
|
-
upstreamResponse.pipe(response);
|
|
982
|
-
}
|
|
983
|
-
);
|
|
984
|
-
upstreamRequest.once('error', error => {
|
|
985
|
-
if (!response.headersSent) {
|
|
986
|
-
response.writeHead(502, { 'content-type': 'application/json' });
|
|
987
|
-
response.end(
|
|
988
|
-
`${JSON.stringify({ error: 'MIAODA_NEST_UPSTREAM_UNAVAILABLE', upstream: upstream.name })}\n`
|
|
989
|
-
);
|
|
990
|
-
} else {
|
|
991
|
-
response.destroy(error);
|
|
992
|
-
}
|
|
993
|
-
});
|
|
994
|
-
request.once('aborted', () => upstreamRequest.destroy());
|
|
995
|
-
response.once('finish', release);
|
|
996
|
-
response.once('close', release);
|
|
997
|
-
request.pipe(upstreamRequest);
|
|
998
|
-
});
|
|
999
|
-
server.on('connection', socket => {
|
|
1000
|
-
proxySockets.add(socket);
|
|
1001
|
-
socket.once('close', () => proxySockets.delete(socket));
|
|
1002
|
-
});
|
|
1003
|
-
server.on('upgrade', (request, clientSocket, head) => {
|
|
1004
|
-
const upstream = activeUpstream;
|
|
1005
|
-
if (!upstream) return clientSocket.destroy();
|
|
1006
|
-
const release = retain(upstream);
|
|
1007
|
-
const upstreamSocket = net.createConnection({
|
|
1008
|
-
host: upstream.host,
|
|
1009
|
-
port: upstream.port,
|
|
1010
|
-
});
|
|
1011
|
-
upgradeSockets.add(clientSocket);
|
|
1012
|
-
upgradeSockets.add(upstreamSocket);
|
|
1013
|
-
let released = false;
|
|
1014
|
-
const close = () => {
|
|
1015
|
-
if (!released) {
|
|
1016
|
-
released = true;
|
|
1017
|
-
release();
|
|
1018
|
-
}
|
|
1019
|
-
upgradeSockets.delete(clientSocket);
|
|
1020
|
-
upgradeSockets.delete(upstreamSocket);
|
|
1021
|
-
clientSocket.destroy();
|
|
1022
|
-
upstreamSocket.destroy();
|
|
1023
|
-
};
|
|
1024
|
-
upstreamSocket.once('connect', () => {
|
|
1025
|
-
let headers = `${request.method} ${request.url} HTTP/${request.httpVersion}\r\n`;
|
|
1026
|
-
for (let index = 0; index < request.rawHeaders.length; index += 2) {
|
|
1027
|
-
headers += `${request.rawHeaders[index]}: ${request.rawHeaders[index + 1]}\r\n`;
|
|
1028
|
-
}
|
|
1029
|
-
upstreamSocket.write(`${headers}\r\n`);
|
|
1030
|
-
if (head.length > 0) upstreamSocket.write(head);
|
|
1031
|
-
clientSocket.pipe(upstreamSocket).pipe(clientSocket);
|
|
1032
|
-
});
|
|
1033
|
-
clientSocket.once('error', close);
|
|
1034
|
-
clientSocket.once('close', close);
|
|
1035
|
-
upstreamSocket.once('error', close);
|
|
1036
|
-
upstreamSocket.once('close', close);
|
|
1037
|
-
});
|
|
1038
|
-
return server;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function listenProxy() {
|
|
1042
|
-
proxy = createProxy();
|
|
1043
|
-
return new Promise((resolve, reject) => {
|
|
1044
|
-
const onError = error => {
|
|
1045
|
-
proxy.off('listening', onListening);
|
|
1046
|
-
reject(error);
|
|
1047
|
-
};
|
|
1048
|
-
const onListening = () => {
|
|
1049
|
-
proxy.off('error', onError);
|
|
1050
|
-
proxyListening = true;
|
|
1051
|
-
proxyOwnedPublicPort = true;
|
|
1052
|
-
resolve();
|
|
1053
|
-
};
|
|
1054
|
-
proxy.once('error', onError);
|
|
1055
|
-
proxy.once('listening', onListening);
|
|
1056
|
-
proxy.listen(publicPort, publicHost);
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function assertNoSymlinkParents(file, label = 'OWNER') {
|
|
1061
|
-
let current = path.dirname(file);
|
|
1062
|
-
const existing = [];
|
|
1063
|
-
while (!fs.existsSync(current)) {
|
|
1064
|
-
existing.push(current);
|
|
1065
|
-
const parent = path.dirname(current);
|
|
1066
|
-
if (parent === current) break;
|
|
1067
|
-
current = parent;
|
|
1068
|
-
}
|
|
1069
|
-
while (true) {
|
|
1070
|
-
if (fs.lstatSync(current).isSymbolicLink())
|
|
1071
|
-
throw new Error(`MIAODA_RUNTIME_${label}_PARENT_SYMLINK`);
|
|
1072
|
-
const parent = path.dirname(current);
|
|
1073
|
-
if (parent === current) break;
|
|
1074
|
-
current = parent;
|
|
1075
|
-
}
|
|
1076
|
-
for (const directory of existing.reverse())
|
|
1077
|
-
fs.mkdirSync(directory, { mode: 0o700 });
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function atomicWritePrivateJson(file, payload, label) {
|
|
1081
|
-
assertNoSymlinkParents(file, label);
|
|
1082
|
-
if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) {
|
|
1083
|
-
throw new Error(`MIAODA_RUNTIME_${label}_TARGET_SYMLINK`);
|
|
1084
|
-
}
|
|
1085
|
-
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1086
|
-
const flags =
|
|
1087
|
-
fs.constants.O_WRONLY |
|
|
1088
|
-
fs.constants.O_CREAT |
|
|
1089
|
-
fs.constants.O_EXCL |
|
|
1090
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
1091
|
-
const fd = fs.openSync(temporary, flags, 0o600);
|
|
1092
|
-
try {
|
|
1093
|
-
fs.writeFileSync(fd, `${JSON.stringify(payload)}\n`);
|
|
1094
|
-
fs.fsyncSync(fd);
|
|
1095
|
-
} finally {
|
|
1096
|
-
fs.closeSync(fd);
|
|
1097
|
-
}
|
|
1098
|
-
try {
|
|
1099
|
-
fs.renameSync(temporary, file);
|
|
1100
|
-
} finally {
|
|
1101
|
-
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function readPrivateJson(file, validator) {
|
|
1106
|
-
try {
|
|
1107
|
-
const stat = fs.lstatSync(file);
|
|
1108
|
-
if (stat.isSymbolicLink() || !stat.isFile()) return undefined;
|
|
1109
|
-
const fd = fs.openSync(
|
|
1110
|
-
file,
|
|
1111
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1112
|
-
);
|
|
1113
|
-
try {
|
|
1114
|
-
const opened = fs.fstatSync(fd);
|
|
1115
|
-
if (
|
|
1116
|
-
!opened.isFile() ||
|
|
1117
|
-
opened.dev !== stat.dev ||
|
|
1118
|
-
opened.ino !== stat.ino
|
|
1119
|
-
)
|
|
1120
|
-
return undefined;
|
|
1121
|
-
const payload = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
1122
|
-
return validator(payload) ? payload : undefined;
|
|
1123
|
-
} finally {
|
|
1124
|
-
fs.closeSync(fd);
|
|
1125
|
-
}
|
|
1126
|
-
} catch {
|
|
1127
|
-
return undefined;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
const OWNER_KEYS = [
|
|
1132
|
-
'schemaVersion',
|
|
1133
|
-
'bootId',
|
|
1134
|
-
'restoreEpochMs',
|
|
1135
|
-
'generationId',
|
|
1136
|
-
'pid',
|
|
1137
|
-
'processStartTicks',
|
|
1138
|
-
'publicPort',
|
|
1139
|
-
'createdAtMs',
|
|
1140
|
-
];
|
|
1141
|
-
|
|
1142
|
-
function ownerPayloadIsStrict(payload) {
|
|
1143
|
-
return (
|
|
1144
|
-
hasExactKeys(payload, OWNER_KEYS) &&
|
|
1145
|
-
payload.schemaVersion === 1 &&
|
|
1146
|
-
typeof payload.bootId === 'string' &&
|
|
1147
|
-
payload.bootId.length > 0 &&
|
|
1148
|
-
Number.isSafeInteger(payload.restoreEpochMs) &&
|
|
1149
|
-
payload.restoreEpochMs > 0 &&
|
|
1150
|
-
typeof payload.generationId === 'string' &&
|
|
1151
|
-
/^[a-f0-9]{64}$/.test(payload.generationId) &&
|
|
1152
|
-
Number.isSafeInteger(payload.pid) &&
|
|
1153
|
-
payload.pid > 0 &&
|
|
1154
|
-
typeof payload.processStartTicks === 'string' &&
|
|
1155
|
-
/^\d+$/.test(payload.processStartTicks) &&
|
|
1156
|
-
Number.isSafeInteger(payload.publicPort) &&
|
|
1157
|
-
payload.publicPort > 0 &&
|
|
1158
|
-
payload.publicPort <= 65_535 &&
|
|
1159
|
-
Number.isSafeInteger(payload.createdAtMs) &&
|
|
1160
|
-
payload.createdAtMs >= payload.restoreEpochMs
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function readStrictOwnerMarker() {
|
|
1165
|
-
return readPrivateJson(ownerFile, ownerPayloadIsStrict);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function ownerProcessIdentityIsLive(owner) {
|
|
1169
|
-
if (!ownerPayloadIsStrict(owner)) return false;
|
|
1170
|
-
if (owner.pid === process.pid)
|
|
1171
|
-
return owner.processStartTicks === processStartTicks;
|
|
1172
|
-
const actualStartTicks = readLinuxProcessStartTicks(owner.pid);
|
|
1173
|
-
return Boolean(
|
|
1174
|
-
actualStartTicks && actualStartTicks === owner.processStartTicks
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const PROCESS_REGISTRY_KEYS = [
|
|
1179
|
-
'schemaVersion',
|
|
1180
|
-
'bootId',
|
|
1181
|
-
'restoreEpochMs',
|
|
1182
|
-
'runtimePid',
|
|
1183
|
-
'runtimeProcessStartTicks',
|
|
1184
|
-
'updatedAtMs',
|
|
1185
|
-
'processes',
|
|
1186
|
-
];
|
|
1187
|
-
|
|
1188
|
-
function processRegistryPayloadIsStrict(payload) {
|
|
1189
|
-
if (
|
|
1190
|
-
!hasExactKeys(payload, PROCESS_REGISTRY_KEYS) ||
|
|
1191
|
-
payload.schemaVersion !== 1 ||
|
|
1192
|
-
typeof payload.bootId !== 'string' ||
|
|
1193
|
-
payload.bootId.length === 0 ||
|
|
1194
|
-
!Number.isSafeInteger(payload.restoreEpochMs) ||
|
|
1195
|
-
payload.restoreEpochMs <= 0 ||
|
|
1196
|
-
!Number.isSafeInteger(payload.runtimePid) ||
|
|
1197
|
-
payload.runtimePid <= 0 ||
|
|
1198
|
-
typeof payload.runtimeProcessStartTicks !== 'string' ||
|
|
1199
|
-
!/^\d+$/.test(payload.runtimeProcessStartTicks) ||
|
|
1200
|
-
!Number.isSafeInteger(payload.updatedAtMs) ||
|
|
1201
|
-
payload.updatedAtMs < payload.restoreEpochMs ||
|
|
1202
|
-
!Array.isArray(payload.processes)
|
|
1203
|
-
) {
|
|
1204
|
-
return false;
|
|
1205
|
-
}
|
|
1206
|
-
const roles = new Set();
|
|
1207
|
-
for (const owned of payload.processes) {
|
|
1208
|
-
if (
|
|
1209
|
-
!hasExactKeys(owned, ['role', 'pid', 'processStartTicks']) ||
|
|
1210
|
-
!['cache', 'source', 'action-plugin-init'].includes(owned.role) ||
|
|
1211
|
-
roles.has(owned.role) ||
|
|
1212
|
-
!Number.isSafeInteger(owned.pid) ||
|
|
1213
|
-
owned.pid <= 0 ||
|
|
1214
|
-
typeof owned.processStartTicks !== 'string' ||
|
|
1215
|
-
!/^\d+$/.test(owned.processStartTicks)
|
|
1216
|
-
) {
|
|
1217
|
-
return false;
|
|
1218
|
-
}
|
|
1219
|
-
roles.add(owned.role);
|
|
1220
|
-
}
|
|
1221
|
-
return true;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function registryRuntimeIdentityIsLive(registry) {
|
|
1225
|
-
if (!processRegistryPayloadIsStrict(registry)) return false;
|
|
1226
|
-
if (registry.runtimePid === process.pid) {
|
|
1227
|
-
return registry.runtimeProcessStartTicks === processStartTicks;
|
|
1228
|
-
}
|
|
1229
|
-
const actualStartTicks = readLinuxProcessStartTicks(registry.runtimePid);
|
|
1230
|
-
return Boolean(
|
|
1231
|
-
actualStartTicks && actualStartTicks === registry.runtimeProcessStartTicks
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function publishProcessRegistry() {
|
|
1236
|
-
const existing = readPrivateJson(
|
|
1237
|
-
processRegistryFile,
|
|
1238
|
-
processRegistryPayloadIsStrict
|
|
1239
|
-
);
|
|
1240
|
-
if (
|
|
1241
|
-
existing &&
|
|
1242
|
-
registryRuntimeIdentityIsLive(existing) &&
|
|
1243
|
-
(existing.runtimePid !== process.pid ||
|
|
1244
|
-
existing.runtimeProcessStartTicks !== processStartTicks)
|
|
1245
|
-
) {
|
|
1246
|
-
throw new Error('MIAODA_RUNTIME_PROCESS_REGISTRY_ALREADY_LIVE');
|
|
1247
|
-
}
|
|
1248
|
-
const processes = [...children]
|
|
1249
|
-
.filter(record => record.child?.pid && record.processStartTicks)
|
|
1250
|
-
.map(record => ({
|
|
1251
|
-
role: record.name,
|
|
1252
|
-
pid: record.child.pid,
|
|
1253
|
-
processStartTicks: record.processStartTicks,
|
|
1254
|
-
}))
|
|
1255
|
-
.sort((left, right) => left.role.localeCompare(right.role));
|
|
1256
|
-
atomicWritePrivateJson(
|
|
1257
|
-
processRegistryFile,
|
|
1258
|
-
{
|
|
1259
|
-
schemaVersion: 1,
|
|
1260
|
-
bootId,
|
|
1261
|
-
restoreEpochMs,
|
|
1262
|
-
runtimePid: process.pid,
|
|
1263
|
-
runtimeProcessStartTicks: processStartTicks,
|
|
1264
|
-
updatedAtMs: Date.now(),
|
|
1265
|
-
processes,
|
|
1266
|
-
},
|
|
1267
|
-
'PROCESS_REGISTRY'
|
|
1268
|
-
);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function removeOwnedProcessRegistry() {
|
|
1272
|
-
try {
|
|
1273
|
-
const before = fs.lstatSync(processRegistryFile);
|
|
1274
|
-
if (before.isSymbolicLink()) return;
|
|
1275
|
-
const fd = fs.openSync(
|
|
1276
|
-
processRegistryFile,
|
|
1277
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1278
|
-
);
|
|
1279
|
-
let payload;
|
|
1280
|
-
let opened;
|
|
1281
|
-
try {
|
|
1282
|
-
opened = fs.fstatSync(fd);
|
|
1283
|
-
payload = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
1284
|
-
} finally {
|
|
1285
|
-
fs.closeSync(fd);
|
|
1286
|
-
}
|
|
1287
|
-
const after = fs.lstatSync(processRegistryFile);
|
|
1288
|
-
if (
|
|
1289
|
-
processRegistryPayloadIsStrict(payload) &&
|
|
1290
|
-
payload.runtimePid === process.pid &&
|
|
1291
|
-
payload.runtimeProcessStartTicks === processStartTicks &&
|
|
1292
|
-
payload.bootId === bootId &&
|
|
1293
|
-
payload.restoreEpochMs === restoreEpochMs &&
|
|
1294
|
-
opened.isFile() &&
|
|
1295
|
-
before.dev === opened.dev &&
|
|
1296
|
-
before.ino === opened.ino &&
|
|
1297
|
-
before.dev === after.dev &&
|
|
1298
|
-
before.ino === after.ino
|
|
1299
|
-
) {
|
|
1300
|
-
fs.unlinkSync(processRegistryFile);
|
|
1301
|
-
}
|
|
1302
|
-
} catch {}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function publishOwner(generationId) {
|
|
1306
|
-
assertNoSymlinkParents(ownerFile, 'OWNER');
|
|
1307
|
-
if (fs.existsSync(ownerFile)) {
|
|
1308
|
-
const target = fs.lstatSync(ownerFile);
|
|
1309
|
-
if (target.isSymbolicLink())
|
|
1310
|
-
throw new Error('MIAODA_RUNTIME_OWNER_TARGET_SYMLINK');
|
|
1311
|
-
const existingOwner = readStrictOwnerMarker();
|
|
1312
|
-
if (existingOwner && ownerProcessIdentityIsLive(existingOwner)) {
|
|
1313
|
-
throw new Error('MIAODA_RUNTIME_OWNER_ALREADY_LIVE');
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
const payload = {
|
|
1317
|
-
schemaVersion: 1,
|
|
1318
|
-
bootId,
|
|
1319
|
-
restoreEpochMs,
|
|
1320
|
-
generationId,
|
|
1321
|
-
pid: process.pid,
|
|
1322
|
-
processStartTicks,
|
|
1323
|
-
publicPort,
|
|
1324
|
-
createdAtMs: Date.now(),
|
|
1325
|
-
};
|
|
1326
|
-
atomicWritePrivateJson(ownerFile, payload, 'OWNER');
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const HANDOFF_STATE_KEYS = [
|
|
1330
|
-
'schemaVersion',
|
|
1331
|
-
'bootId',
|
|
1332
|
-
'restoreEpochMs',
|
|
1333
|
-
'generationId',
|
|
1334
|
-
'runtimePid',
|
|
1335
|
-
'runtimeProcessStartTicks',
|
|
1336
|
-
'phase',
|
|
1337
|
-
'activeUpstream',
|
|
1338
|
-
'publicPort',
|
|
1339
|
-
'cachePort',
|
|
1340
|
-
'sourcePort',
|
|
1341
|
-
'cacheReadyAtMs',
|
|
1342
|
-
'sourcePromotedAtMs',
|
|
1343
|
-
'updatedAtMs',
|
|
1344
|
-
];
|
|
1345
|
-
|
|
1346
|
-
function handoffStatePayloadIsStrict(payload) {
|
|
1347
|
-
return (
|
|
1348
|
-
hasExactKeys(payload, HANDOFF_STATE_KEYS) &&
|
|
1349
|
-
payload.schemaVersion === 1 &&
|
|
1350
|
-
payload.bootId === bootId &&
|
|
1351
|
-
payload.restoreEpochMs === restoreEpochMs &&
|
|
1352
|
-
/^[a-f0-9]{64}$/.test(payload.generationId || '') &&
|
|
1353
|
-
payload.runtimePid === process.pid &&
|
|
1354
|
-
payload.runtimeProcessStartTicks === processStartTicks &&
|
|
1355
|
-
['cache-ready', 'source-promoted'].includes(payload.phase) &&
|
|
1356
|
-
payload.activeUpstream ===
|
|
1357
|
-
(payload.phase === 'cache-ready' ? 'cache' : 'source') &&
|
|
1358
|
-
payload.publicPort === publicPort &&
|
|
1359
|
-
Number.isSafeInteger(payload.cachePort) &&
|
|
1360
|
-
payload.cachePort > 0 &&
|
|
1361
|
-
(payload.sourcePort === null ||
|
|
1362
|
-
(Number.isSafeInteger(payload.sourcePort) && payload.sourcePort > 0)) &&
|
|
1363
|
-
Number.isSafeInteger(payload.cacheReadyAtMs) &&
|
|
1364
|
-
payload.cacheReadyAtMs >= restoreEpochMs &&
|
|
1365
|
-
(payload.sourcePromotedAtMs === null ||
|
|
1366
|
-
(Number.isSafeInteger(payload.sourcePromotedAtMs) &&
|
|
1367
|
-
payload.sourcePromotedAtMs >= payload.cacheReadyAtMs)) &&
|
|
1368
|
-
Number.isSafeInteger(payload.updatedAtMs) &&
|
|
1369
|
-
payload.updatedAtMs >= payload.cacheReadyAtMs
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
function publishHandoffState(phase, sourcePort = null) {
|
|
1374
|
-
const now = Date.now();
|
|
1375
|
-
if (!cacheReadyAtMs) cacheReadyAtMs = now;
|
|
1376
|
-
const payload = {
|
|
1377
|
-
schemaVersion: 1,
|
|
1378
|
-
bootId,
|
|
1379
|
-
restoreEpochMs,
|
|
1380
|
-
generationId: currentGenerationId,
|
|
1381
|
-
runtimePid: process.pid,
|
|
1382
|
-
runtimeProcessStartTicks: processStartTicks,
|
|
1383
|
-
phase,
|
|
1384
|
-
activeUpstream: phase === 'cache-ready' ? 'cache' : 'source',
|
|
1385
|
-
publicPort,
|
|
1386
|
-
cachePort: cacheUpstream.port,
|
|
1387
|
-
sourcePort,
|
|
1388
|
-
cacheReadyAtMs,
|
|
1389
|
-
sourcePromotedAtMs: phase === 'source-promoted' ? now : null,
|
|
1390
|
-
updatedAtMs: now,
|
|
1391
|
-
};
|
|
1392
|
-
if (!handoffStatePayloadIsStrict(payload)) {
|
|
1393
|
-
throw new Error('MIAODA_NEST_HANDOFF_STATE_INVALID');
|
|
1394
|
-
}
|
|
1395
|
-
atomicWritePrivateJson(handoffStateFile, payload, 'HANDOFF_STATE');
|
|
1396
|
-
process.stderr.write(
|
|
1397
|
-
`${JSON.stringify({
|
|
1398
|
-
event:
|
|
1399
|
-
phase === 'cache-ready'
|
|
1400
|
-
? 'miaoda_nest_cache_ready'
|
|
1401
|
-
: 'miaoda_nest_source_promoted',
|
|
1402
|
-
bootId,
|
|
1403
|
-
generationId: currentGenerationId,
|
|
1404
|
-
publicPort,
|
|
1405
|
-
cachePort: cacheUpstream.port,
|
|
1406
|
-
sourcePort,
|
|
1407
|
-
elapsedMs: now - restoreEpochMs,
|
|
1408
|
-
})}\n`
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
function removeOwnedHandoffState() {
|
|
1413
|
-
try {
|
|
1414
|
-
const before = fs.lstatSync(handoffStateFile);
|
|
1415
|
-
if (before.isSymbolicLink()) return;
|
|
1416
|
-
const fd = fs.openSync(
|
|
1417
|
-
handoffStateFile,
|
|
1418
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1419
|
-
);
|
|
1420
|
-
let payload;
|
|
1421
|
-
let opened;
|
|
1422
|
-
try {
|
|
1423
|
-
opened = fs.fstatSync(fd);
|
|
1424
|
-
payload = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
1425
|
-
} finally {
|
|
1426
|
-
fs.closeSync(fd);
|
|
1427
|
-
}
|
|
1428
|
-
const after = fs.lstatSync(handoffStateFile);
|
|
1429
|
-
if (
|
|
1430
|
-
handoffStatePayloadIsStrict(payload) &&
|
|
1431
|
-
opened.isFile() &&
|
|
1432
|
-
before.dev === opened.dev &&
|
|
1433
|
-
before.ino === opened.ino &&
|
|
1434
|
-
before.dev === after.dev &&
|
|
1435
|
-
before.ino === after.ino
|
|
1436
|
-
) {
|
|
1437
|
-
fs.unlinkSync(handoffStateFile);
|
|
1438
|
-
}
|
|
1439
|
-
} catch {}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
function removeOwnedMarker() {
|
|
1443
|
-
try {
|
|
1444
|
-
const before = fs.lstatSync(ownerFile);
|
|
1445
|
-
if (before.isSymbolicLink()) return;
|
|
1446
|
-
const fd = fs.openSync(
|
|
1447
|
-
ownerFile,
|
|
1448
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1449
|
-
);
|
|
1450
|
-
let payload;
|
|
1451
|
-
let opened;
|
|
1452
|
-
try {
|
|
1453
|
-
opened = fs.fstatSync(fd);
|
|
1454
|
-
payload = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
1455
|
-
} finally {
|
|
1456
|
-
fs.closeSync(fd);
|
|
1457
|
-
}
|
|
1458
|
-
const after = fs.lstatSync(ownerFile);
|
|
1459
|
-
if (
|
|
1460
|
-
ownerPayloadIsStrict(payload) &&
|
|
1461
|
-
payload?.pid === process.pid &&
|
|
1462
|
-
payload?.processStartTicks === processStartTicks &&
|
|
1463
|
-
payload?.bootId === bootId &&
|
|
1464
|
-
Number(payload?.restoreEpochMs) === restoreEpochMs &&
|
|
1465
|
-
opened.isFile() &&
|
|
1466
|
-
before.dev === opened.dev &&
|
|
1467
|
-
before.ino === opened.ino &&
|
|
1468
|
-
before.dev === after.dev &&
|
|
1469
|
-
before.ino === after.ino
|
|
1470
|
-
) {
|
|
1471
|
-
fs.unlinkSync(ownerFile);
|
|
1472
|
-
}
|
|
1473
|
-
} catch {}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
function receiptReady(expectedGenerationId) {
|
|
1477
|
-
try {
|
|
1478
|
-
const receipt = JSON.parse(fs.readFileSync(receiptFile, 'utf8'));
|
|
1479
|
-
if (
|
|
1480
|
-
receipt?.schemaVersion !== 1 ||
|
|
1481
|
-
receipt.bootId !== bootId ||
|
|
1482
|
-
Number(receipt.restoreEpochMs) !== restoreEpochMs ||
|
|
1483
|
-
receipt.generationId !== expectedGenerationId ||
|
|
1484
|
-
typeof receipt.nodeModulesPath !== 'string'
|
|
1485
|
-
)
|
|
1486
|
-
return false;
|
|
1487
|
-
const nodeModules = fs.realpathSync(receipt.nodeModulesPath);
|
|
1488
|
-
return (
|
|
1489
|
-
fs.statSync(nodeModules).isDirectory() &&
|
|
1490
|
-
isInside(fs.realpathSync(projectRoot), nodeModules)
|
|
1491
|
-
);
|
|
1492
|
-
} catch {
|
|
1493
|
-
return false;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function parseRuntimeEnvironment(contents) {
|
|
1498
|
-
const environment = Object.create(null);
|
|
1499
|
-
const lines = contents.split(/\r?\n/);
|
|
1500
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
1501
|
-
const rawLine = lines[lineIndex];
|
|
1502
|
-
const line = rawLine.trim();
|
|
1503
|
-
if (!line || line.startsWith('#')) continue;
|
|
1504
|
-
const assignment = line.match(
|
|
1505
|
-
/^export[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*/
|
|
1506
|
-
);
|
|
1507
|
-
if (!assignment)
|
|
1508
|
-
throw new Error(`MIAODA_RUNTIME_ENV_INVALID_LINE:${lineIndex + 1}`);
|
|
1509
|
-
const key = assignment[1];
|
|
1510
|
-
if (Object.hasOwn(environment, key)) {
|
|
1511
|
-
throw new Error(`MIAODA_RUNTIME_ENV_DUPLICATE_KEY:${lineIndex + 1}`);
|
|
1512
|
-
}
|
|
1513
|
-
let encoded = line.slice(assignment[0].length);
|
|
1514
|
-
if (!encoded.startsWith('"')) {
|
|
1515
|
-
throw new Error(
|
|
1516
|
-
`MIAODA_RUNTIME_ENV_VALUE_NOT_DOUBLE_QUOTED:${lineIndex + 1}`
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
let value = '';
|
|
1520
|
-
let closedAt = -1;
|
|
1521
|
-
let index = 1;
|
|
1522
|
-
while (closedAt < 0) {
|
|
1523
|
-
for (; index < encoded.length; index += 1) {
|
|
1524
|
-
const character = encoded[index];
|
|
1525
|
-
if (character === '"') {
|
|
1526
|
-
closedAt = index;
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
if (character === '\\') {
|
|
1530
|
-
const next = encoded[index + 1];
|
|
1531
|
-
if (next === undefined) break;
|
|
1532
|
-
if (next === '\\' || next === '"') value += next;
|
|
1533
|
-
else value += `\\${next}`;
|
|
1534
|
-
index += 1;
|
|
1535
|
-
} else {
|
|
1536
|
-
if (character === '\0') {
|
|
1537
|
-
throw new Error(`MIAODA_RUNTIME_ENV_NUL:${lineIndex + 1}`);
|
|
1538
|
-
}
|
|
1539
|
-
value += character;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (closedAt >= 0) break;
|
|
1543
|
-
if (lineIndex + 1 >= lines.length) {
|
|
1544
|
-
throw new Error(
|
|
1545
|
-
`MIAODA_RUNTIME_ENV_UNTERMINATED_VALUE:${lineIndex + 1}`
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
if (encoded.endsWith('\\')) value += '\\';
|
|
1549
|
-
value += '\n';
|
|
1550
|
-
encoded += `\n${lines[(lineIndex += 1)]}`;
|
|
1551
|
-
index += 1;
|
|
1552
|
-
}
|
|
1553
|
-
if (encoded.slice(closedAt + 1).trim() !== '') {
|
|
1554
|
-
throw new Error(`MIAODA_RUNTIME_ENV_UNTERMINATED_VALUE:${lineIndex + 1}`);
|
|
1555
|
-
}
|
|
1556
|
-
if (!RUNTIME_CONTROL_ENV_KEYS.has(key)) environment[key] = value;
|
|
1557
|
-
}
|
|
1558
|
-
return environment;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function readRuntimeEnvironment() {
|
|
1562
|
-
const requested = fs.lstatSync(runtimeEnvFile);
|
|
1563
|
-
if (requested.isSymbolicLink())
|
|
1564
|
-
throw new Error('MIAODA_RUNTIME_ENV_FILE_SYMLINK');
|
|
1565
|
-
const realFile = fs.realpathSync(runtimeEnvFile);
|
|
1566
|
-
if (
|
|
1567
|
-
!isInside(fs.realpathSync(platformWorkspaceRoot), realFile) ||
|
|
1568
|
-
!fs.statSync(realFile).isFile()
|
|
1569
|
-
) {
|
|
1570
|
-
throw new Error('MIAODA_RUNTIME_ENV_FILE_ESCAPED_WORKSPACE');
|
|
1571
|
-
}
|
|
1572
|
-
const size = fs.statSync(realFile).size;
|
|
1573
|
-
if (size > 1024 * 1024) throw new Error('MIAODA_RUNTIME_ENV_FILE_TOO_LARGE');
|
|
1574
|
-
return parseRuntimeEnvironment(fs.readFileSync(realFile, 'utf8'));
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
async function waitForReceipt(expectedGenerationId) {
|
|
1578
|
-
while (!shuttingDown && !receiptReady(expectedGenerationId)) {
|
|
1579
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1580
|
-
}
|
|
1581
|
-
if (shuttingDown) throw new Error('MIAODA_NEST_RUNTIME_STOPPING');
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
async function runActionPluginInit(baseEnv) {
|
|
1585
|
-
const record = spawnOwned(
|
|
1586
|
-
'action-plugin-init',
|
|
1587
|
-
process.platform === 'win32' ? 'fullstack-cli.cmd' : 'fullstack-cli',
|
|
1588
|
-
['action-plugin', 'init'],
|
|
1589
|
-
{ baseEnv, logName: 'server-handoff.std.log' }
|
|
1590
|
-
);
|
|
1591
|
-
record.expectedExit = true;
|
|
1592
|
-
await waitForChild(record);
|
|
1593
|
-
const exitCode = record.child.exitCode;
|
|
1594
|
-
await terminateOwned(record);
|
|
1595
|
-
if (exitCode !== 0) {
|
|
1596
|
-
process.stderr.write(
|
|
1597
|
-
`${JSON.stringify({
|
|
1598
|
-
event: 'miaoda_action_plugin_init_failed',
|
|
1599
|
-
exitCode,
|
|
1600
|
-
continueStartup: true,
|
|
1601
|
-
})}\n`
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
async function waitForDrain(upstream) {
|
|
1607
|
-
const deadline = Date.now() + drainTimeoutMs;
|
|
1608
|
-
while (upstream.inflight > 0 && Date.now() < deadline && !shuttingDown) {
|
|
1609
|
-
await new Promise(resolve => setTimeout(resolve, 25));
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function reportSourcePromotionFailure(stage, error, retryDelayMs) {
|
|
1614
|
-
process.stderr.write(
|
|
1615
|
-
`${JSON.stringify({
|
|
1616
|
-
event: 'miaoda_nest_source_promotion_retry',
|
|
1617
|
-
stage,
|
|
1618
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1619
|
-
retryDelayMs,
|
|
1620
|
-
cacheContinuesServing: true,
|
|
1621
|
-
})}\n`
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
async function waitForSourceRetry(delayMs) {
|
|
1626
|
-
const deadline = Date.now() + delayMs;
|
|
1627
|
-
while (!shuttingDown && Date.now() < deadline) {
|
|
1628
|
-
await new Promise(resolve =>
|
|
1629
|
-
setTimeout(resolve, Math.min(50, deadline - Date.now()))
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
async function startAndPromoteSource(baseEnv, expectedCapabilityIds) {
|
|
1635
|
-
const sourcePort = await reservePort(probeHost(publicHost));
|
|
1636
|
-
const candidate = {
|
|
1637
|
-
name: 'source',
|
|
1638
|
-
host: probeHost(publicHost),
|
|
1639
|
-
port: sourcePort,
|
|
1640
|
-
inflight: 0,
|
|
1641
|
-
};
|
|
1642
|
-
sourceUpstream = candidate;
|
|
1643
|
-
const sourceRecord = spawnOwned(
|
|
1644
|
-
'source',
|
|
1645
|
-
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
1646
|
-
['run', 'dev:server'],
|
|
1647
|
-
{
|
|
1648
|
-
baseEnv,
|
|
1649
|
-
env: {
|
|
1650
|
-
SERVER_HOST: candidate.host,
|
|
1651
|
-
SERVER_PORT: String(sourcePort),
|
|
1652
|
-
},
|
|
1653
|
-
logName: 'server.std.log',
|
|
1654
|
-
}
|
|
1655
|
-
);
|
|
1656
|
-
candidate.record = sourceRecord;
|
|
1657
|
-
try {
|
|
1658
|
-
await waitForHttp(
|
|
1659
|
-
sourceRecord,
|
|
1660
|
-
candidate,
|
|
1661
|
-
sourceReadyTimeoutMs,
|
|
1662
|
-
expectedCapabilityIds
|
|
1663
|
-
);
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
try {
|
|
1666
|
-
await terminateOwned(sourceRecord);
|
|
1667
|
-
} catch (cleanupError) {
|
|
1668
|
-
if (sourceUpstream === candidate) sourceUpstream = undefined;
|
|
1669
|
-
await shutdown(
|
|
1670
|
-
`source-cleanup-failed:${
|
|
1671
|
-
cleanupError instanceof Error
|
|
1672
|
-
? cleanupError.message
|
|
1673
|
-
: String(cleanupError)
|
|
1674
|
-
}`,
|
|
1675
|
-
1
|
|
1676
|
-
);
|
|
1677
|
-
throw cleanupError;
|
|
1678
|
-
}
|
|
1679
|
-
if (sourceUpstream === candidate) sourceUpstream = undefined;
|
|
1680
|
-
throw error;
|
|
1681
|
-
}
|
|
1682
|
-
activeUpstream = candidate;
|
|
1683
|
-
try {
|
|
1684
|
-
await waitForDrain(cacheUpstream);
|
|
1685
|
-
await terminateOwned(cacheUpstream.record);
|
|
1686
|
-
publishHandoffState('source-promoted', candidate.port);
|
|
1687
|
-
promoted = true;
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
await shutdown(
|
|
1690
|
-
`miaoda_nest_source_cutover_finalize_failed:${
|
|
1691
|
-
error instanceof Error ? error.message : String(error)
|
|
1692
|
-
}`,
|
|
1693
|
-
1
|
|
1694
|
-
);
|
|
1695
|
-
throw error;
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
async function promoteSource(expectedGenerationId, expectedCapabilityIds) {
|
|
1700
|
-
await waitForReceipt(expectedGenerationId);
|
|
1701
|
-
let retryDelayMs = SOURCE_RETRY_INITIAL_DELAY_MS;
|
|
1702
|
-
let baseEnv;
|
|
1703
|
-
while (!shuttingDown && !baseEnv) {
|
|
1704
|
-
try {
|
|
1705
|
-
baseEnv = { ...process.env, ...readRuntimeEnvironment() };
|
|
1706
|
-
await runActionPluginInit(baseEnv);
|
|
1707
|
-
} catch (error) {
|
|
1708
|
-
baseEnv = undefined;
|
|
1709
|
-
reportSourcePromotionFailure('environment-and-init', error, retryDelayMs);
|
|
1710
|
-
await waitForSourceRetry(retryDelayMs);
|
|
1711
|
-
retryDelayMs = Math.min(retryDelayMs * 2, SOURCE_RETRY_MAX_DELAY_MS);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
retryDelayMs = SOURCE_RETRY_INITIAL_DELAY_MS;
|
|
1715
|
-
while (!shuttingDown && !promoted) {
|
|
1716
|
-
try {
|
|
1717
|
-
await startAndPromoteSource(baseEnv, expectedCapabilityIds);
|
|
1718
|
-
} catch (error) {
|
|
1719
|
-
if (shuttingDown) break;
|
|
1720
|
-
reportSourcePromotionFailure('source-readiness', error, retryDelayMs);
|
|
1721
|
-
await waitForSourceRetry(retryDelayMs);
|
|
1722
|
-
retryDelayMs = Math.min(retryDelayMs * 2, SOURCE_RETRY_MAX_DELAY_MS);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function closeProxyBounded() {
|
|
1728
|
-
if (!proxy || !proxyListening) return Promise.resolve();
|
|
1729
|
-
proxyListening = false;
|
|
1730
|
-
return new Promise(resolve => {
|
|
1731
|
-
let settled = false;
|
|
1732
|
-
let timer;
|
|
1733
|
-
const finish = () => {
|
|
1734
|
-
if (settled) return;
|
|
1735
|
-
settled = true;
|
|
1736
|
-
if (timer) clearTimeout(timer);
|
|
1737
|
-
resolve();
|
|
1738
|
-
};
|
|
1739
|
-
proxy.close(finish);
|
|
1740
|
-
timer = setTimeout(() => {
|
|
1741
|
-
for (const socket of proxySockets) socket.destroy();
|
|
1742
|
-
for (const socket of upgradeSockets) socket.destroy();
|
|
1743
|
-
proxy.closeAllConnections?.();
|
|
1744
|
-
finish();
|
|
1745
|
-
}, shutdownTimeoutMs);
|
|
1746
|
-
timer.unref?.();
|
|
1747
|
-
});
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
function verifyOwnedPortReleased() {
|
|
1751
|
-
if (!proxyOwnedPublicPort) return Promise.resolve();
|
|
1752
|
-
return new Promise((resolve, reject) => {
|
|
1753
|
-
const server = net.createServer();
|
|
1754
|
-
server.once('error', reject);
|
|
1755
|
-
server.listen(publicPort, publicHost, () => {
|
|
1756
|
-
server.close(error => (error ? reject(error) : resolve()));
|
|
1757
|
-
});
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
function shutdown(reason, exitCode = 0) {
|
|
1762
|
-
if (shutdownPromise) return shutdownPromise;
|
|
1763
|
-
shuttingDown = true;
|
|
1764
|
-
shutdownPromise = (async () => {
|
|
1765
|
-
process.stderr.write(
|
|
1766
|
-
`${JSON.stringify({ event: 'miaoda_nest_handoff_shutdown', reason, exitCode })}\n`
|
|
1767
|
-
);
|
|
1768
|
-
await closeProxyBounded();
|
|
1769
|
-
const terminations = await Promise.allSettled(
|
|
1770
|
-
[...children].map(record => terminateOwned(record))
|
|
1771
|
-
);
|
|
1772
|
-
let ownedProcessesReleased = true;
|
|
1773
|
-
for (const result of terminations) {
|
|
1774
|
-
if (result.status !== 'rejected') continue;
|
|
1775
|
-
ownedProcessesReleased = false;
|
|
1776
|
-
process.stderr.write(
|
|
1777
|
-
`${JSON.stringify({
|
|
1778
|
-
event: 'miaoda_nest_owned_group_release_failed',
|
|
1779
|
-
error:
|
|
1780
|
-
result.reason instanceof Error
|
|
1781
|
-
? result.reason.message
|
|
1782
|
-
: String(result.reason),
|
|
1783
|
-
})}\n`
|
|
1784
|
-
);
|
|
1785
|
-
exitCode = exitCode || 1;
|
|
1786
|
-
}
|
|
1787
|
-
try {
|
|
1788
|
-
await verifyOwnedPortReleased();
|
|
1789
|
-
} catch (error) {
|
|
1790
|
-
process.stderr.write(
|
|
1791
|
-
`${JSON.stringify({
|
|
1792
|
-
event: 'miaoda_nest_public_port_release_failed',
|
|
1793
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1794
|
-
})}\n`
|
|
1795
|
-
);
|
|
1796
|
-
exitCode = exitCode || 1;
|
|
1797
|
-
}
|
|
1798
|
-
removeOwnedHandoffState();
|
|
1799
|
-
removeOwnedMarker();
|
|
1800
|
-
if (ownedProcessesReleased) removeOwnedProcessRegistry();
|
|
1801
|
-
process.exitCode = exitCode;
|
|
1802
|
-
resolveLifetime();
|
|
1803
|
-
})();
|
|
1804
|
-
return shutdownPromise;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
async function main() {
|
|
1808
|
-
validateRuntimeConfiguration();
|
|
1809
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
1810
|
-
const generation = readValidatedGeneration();
|
|
1811
|
-
currentGenerationId = generation.generationId;
|
|
1812
|
-
const cachePort = await reservePort(probeHost(publicHost));
|
|
1813
|
-
cacheUpstream = {
|
|
1814
|
-
name: 'cache',
|
|
1815
|
-
host: probeHost(publicHost),
|
|
1816
|
-
port: cachePort,
|
|
1817
|
-
inflight: 0,
|
|
1818
|
-
};
|
|
1819
|
-
cacheUpstream.record = spawnOwned(
|
|
1820
|
-
'cache',
|
|
1821
|
-
process.execPath,
|
|
1822
|
-
[generation.entryFile],
|
|
1823
|
-
{
|
|
1824
|
-
env: {
|
|
1825
|
-
NODE_PATH: generation.workspaceNodeModulesPath,
|
|
1826
|
-
NODE_OPTIONS: '',
|
|
1827
|
-
NODE_ENV: 'development',
|
|
1828
|
-
SERVER_HOST: cacheUpstream.host,
|
|
1829
|
-
SERVER_PORT: String(cachePort),
|
|
1830
|
-
MIAODA_SERVER_CACHE_PROBE: '',
|
|
1831
|
-
DEPRECATED_SKIP_INIT_DB_CONNECTION:
|
|
1832
|
-
process.env.DEPRECATED_SKIP_INIT_DB_CONNECTION || 'true',
|
|
1833
|
-
FORCE_AUTHN_INNERAPI_DOMAIN:
|
|
1834
|
-
process.env.FORCE_AUTHN_INNERAPI_DOMAIN || 'http://127.0.0.1',
|
|
1835
|
-
FORCE_AUTHN_ACCESS_KEY:
|
|
1836
|
-
process.env.FORCE_AUTHN_ACCESS_KEY || 'server-startup-cache',
|
|
1837
|
-
FORCE_AUTHN_ACCESS_SECRET:
|
|
1838
|
-
process.env.FORCE_AUTHN_ACCESS_SECRET || 'server-startup-cache',
|
|
1839
|
-
},
|
|
1840
|
-
logName: 'server-cache.std.log',
|
|
1841
|
-
}
|
|
1842
|
-
);
|
|
1843
|
-
await waitForHttp(
|
|
1844
|
-
cacheUpstream.record,
|
|
1845
|
-
cacheUpstream,
|
|
1846
|
-
sourceReadyTimeoutMs,
|
|
1847
|
-
generation.expectedCapabilityIds
|
|
1848
|
-
);
|
|
1849
|
-
activeUpstream = cacheUpstream;
|
|
1850
|
-
await listenProxy();
|
|
1851
|
-
publishOwner(generation.generationId);
|
|
1852
|
-
publishHandoffState('cache-ready');
|
|
1853
|
-
void promoteSource(
|
|
1854
|
-
generation.generationId,
|
|
1855
|
-
generation.expectedCapabilityIds
|
|
1856
|
-
).catch(error => {
|
|
1857
|
-
if (!shuttingDown) {
|
|
1858
|
-
process.stderr.write(
|
|
1859
|
-
`${JSON.stringify({
|
|
1860
|
-
event: 'miaoda_nest_source_promotion_stopped',
|
|
1861
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1862
|
-
cacheContinuesServing: true,
|
|
1863
|
-
})}\n`
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
});
|
|
1867
|
-
await lifetime;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
function isDirectExecution() {
|
|
1871
|
-
if (!process.argv[1]) return false;
|
|
1872
|
-
try {
|
|
1873
|
-
return (
|
|
1874
|
-
fs.realpathSync(process.argv[1]) ===
|
|
1875
|
-
fs.realpathSync(fileURLToPath(import.meta.url))
|
|
1876
|
-
);
|
|
1877
|
-
} catch {
|
|
1878
|
-
return false;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
if (isDirectExecution()) {
|
|
1883
|
-
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
1884
|
-
process.on(signal, () => {
|
|
1885
|
-
void shutdown(`signal:${signal}`, 0);
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
process.on('uncaughtException', error => {
|
|
1889
|
-
void shutdown(`uncaughtException:${error.message}`, 1);
|
|
1890
|
-
});
|
|
1891
|
-
process.on('unhandledRejection', error => {
|
|
1892
|
-
void shutdown(
|
|
1893
|
-
`unhandledRejection:${error instanceof Error ? error.message : String(error)}`,
|
|
1894
|
-
1
|
|
1895
|
-
);
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
main().catch(error =>
|
|
1899
|
-
shutdown(
|
|
1900
|
-
`fatal:${error instanceof Error ? error.message : String(error)}`,
|
|
1901
|
-
1
|
|
1902
|
-
)
|
|
1903
|
-
);
|
|
1904
|
-
}
|