@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
package/templates/scripts/dev.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const net = require('net');
|
|
7
5
|
const path = require('path');
|
|
8
6
|
const { spawn, execSync } = require('child_process');
|
|
9
7
|
const readline = require('readline');
|
|
@@ -33,39 +31,13 @@ loadEnv();
|
|
|
33
31
|
|
|
34
32
|
// ── Configuration ─────────────────────────────────────────────────────────────
|
|
35
33
|
const LOG_DIR = process.env.LOG_DIR || 'logs';
|
|
36
|
-
const MAX_RESTART_COUNT =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: Infinity;
|
|
34
|
+
const MAX_RESTART_COUNT = process.env.MAX_RESTART_COUNT != null && process.env.MAX_RESTART_COUNT !== ''
|
|
35
|
+
? parseInt(process.env.MAX_RESTART_COUNT, 10)
|
|
36
|
+
: Infinity;
|
|
40
37
|
const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
|
|
41
38
|
const MAX_DELAY = 8;
|
|
42
|
-
const
|
|
43
|
-
const PLATFORM_HANDOFF_PUBLIC_PORT = 3000;
|
|
44
|
-
const MAX_HANDOFF_READINESS_RESPONSE_BYTES = 64 * 1024;
|
|
39
|
+
const SERVER_PORT = process.env.SERVER_PORT || '3000';
|
|
45
40
|
const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
|
|
46
|
-
// This is an ownership receipt, not the raw TCC flag. The sandbox entrypoint
|
|
47
|
-
// sets it only after the handoff runtime has bound the public port and
|
|
48
|
-
// published its owner marker. If that handshake fails, the legacy path below
|
|
49
|
-
// remains the sole backend owner.
|
|
50
|
-
const BACKEND_OWNED_BY_HANDOFF =
|
|
51
|
-
process.env.MIAODA_NEST_BACKEND_OWNER === 'handoff';
|
|
52
|
-
const HANDOFF_CONTROL_ROOT = '/tmp/miaoda-cold-start';
|
|
53
|
-
const HANDOFF_DECISION_FILE = path.join(HANDOFF_CONTROL_ROOT, 'decision.json');
|
|
54
|
-
const HANDOFF_OWNER_FILE = path.join(
|
|
55
|
-
HANDOFF_CONTROL_ROOT,
|
|
56
|
-
'runtime-owner.json'
|
|
57
|
-
);
|
|
58
|
-
const HANDOFF_PROCESS_REGISTRY_FILE = path.join(
|
|
59
|
-
HANDOFF_CONTROL_ROOT,
|
|
60
|
-
'runtime-processes.json'
|
|
61
|
-
);
|
|
62
|
-
const HANDOFF_DECISION = BACKEND_OWNED_BY_HANDOFF
|
|
63
|
-
? readStrictColdStartDecision()
|
|
64
|
-
: null;
|
|
65
|
-
const HANDOFF_BOOT_ID = HANDOFF_DECISION?.bootId || '';
|
|
66
|
-
const HANDOFF_RESTORE_EPOCH_MS = HANDOFF_DECISION?.restoreEpochMs || 0;
|
|
67
|
-
const PUBLIC_SERVER_PORT =
|
|
68
|
-
HANDOFF_DECISION?.publicPort || Number(LEGACY_SERVER_PORT);
|
|
69
41
|
|
|
70
42
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
71
43
|
|
|
@@ -78,16 +50,11 @@ const devLogFd = fs.openSync(devLogPath, 'a');
|
|
|
78
50
|
function timestamp() {
|
|
79
51
|
const now = new Date();
|
|
80
52
|
return (
|
|
81
|
-
now.getFullYear() +
|
|
82
|
-
'-' +
|
|
83
|
-
String(now.
|
|
84
|
-
'
|
|
85
|
-
String(now.
|
|
86
|
-
' ' +
|
|
87
|
-
String(now.getHours()).padStart(2, '0') +
|
|
88
|
-
':' +
|
|
89
|
-
String(now.getMinutes()).padStart(2, '0') +
|
|
90
|
-
':' +
|
|
53
|
+
now.getFullYear() + '-' +
|
|
54
|
+
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
|
55
|
+
String(now.getDate()).padStart(2, '0') + ' ' +
|
|
56
|
+
String(now.getHours()).padStart(2, '0') + ':' +
|
|
57
|
+
String(now.getMinutes()).padStart(2, '0') + ':' +
|
|
91
58
|
String(now.getSeconds()).padStart(2, '0')
|
|
92
59
|
);
|
|
93
60
|
}
|
|
@@ -96,55 +63,26 @@ function timestamp() {
|
|
|
96
63
|
// Each pending fs.write retains ~1.5 KB (msg + libuv/V8 wrappers); 1000 ≈ ~1.5 MB ceiling.
|
|
97
64
|
let _stdoutInFlight = 0;
|
|
98
65
|
const STDOUT_MAX_INFLIGHT = 1000;
|
|
99
|
-
const reportedBestEffortFailures = new Set();
|
|
100
|
-
|
|
101
|
-
function reportBestEffortFailure(operation, error) {
|
|
102
|
-
if (reportedBestEffortFailures.has(operation)) return;
|
|
103
|
-
reportedBestEffortFailures.add(operation);
|
|
104
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
105
|
-
try {
|
|
106
|
-
fs.write(2, `[dev.js] ${operation}: ${detail}\n`, () => {});
|
|
107
|
-
} catch {
|
|
108
|
-
// fd 2 is the last-resort diagnostic sink; there is no safe recursive
|
|
109
|
-
// fallback if submitting this best-effort write itself fails.
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
66
|
|
|
113
67
|
/** Write to dev.std.log (sync, guaranteed) + mirror to terminal (async, non-blocking) */
|
|
114
68
|
function writeOutput(msg) {
|
|
115
69
|
// File first and synchronously — read-logs reads this file; it must never be gated
|
|
116
70
|
// by the terminal consumer.
|
|
117
|
-
try {
|
|
118
|
-
fs.writeSync(devStdLogFd, msg);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
reportBestEffortFailure('write dev.std.log failed', error);
|
|
121
|
-
}
|
|
71
|
+
try { fs.writeSync(devStdLogFd, msg); } catch {}
|
|
122
72
|
// stdout mirror via async fs.write: if process.stdout is a pty/pipe whose consumer
|
|
123
73
|
// stalls, the block happens on the libuv threadpool, NOT the event loop — so the
|
|
124
74
|
// synchronous log-FILE writes above (and subsequent readline callbacks) keep running.
|
|
125
75
|
// Drop overflow when too many writes are already pending (best-effort mirror).
|
|
126
|
-
if (_stdoutInFlight >= STDOUT_MAX_INFLIGHT) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
76
|
+
if (_stdoutInFlight >= STDOUT_MAX_INFLIGHT) { return; }
|
|
129
77
|
_stdoutInFlight++;
|
|
130
|
-
try {
|
|
131
|
-
fs.write(1, msg, () => {
|
|
132
|
-
_stdoutInFlight--;
|
|
133
|
-
});
|
|
134
|
-
} catch {
|
|
135
|
-
_stdoutInFlight--;
|
|
136
|
-
}
|
|
78
|
+
try { fs.write(1, msg, () => { _stdoutInFlight--; }); } catch { _stdoutInFlight--; }
|
|
137
79
|
}
|
|
138
80
|
|
|
139
81
|
/** Structured event log → terminal + dev.std.log + dev.log */
|
|
140
82
|
function logEvent(level, name, message) {
|
|
141
83
|
const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
|
|
142
84
|
writeOutput(msg);
|
|
143
|
-
try {
|
|
144
|
-
fs.writeSync(devLogFd, msg);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
reportBestEffortFailure('write dev.log failed', error);
|
|
147
|
-
}
|
|
85
|
+
try { fs.writeSync(devLogFd, msg); } catch {}
|
|
148
86
|
}
|
|
149
87
|
|
|
150
88
|
// ── Process group management ──────────────────────────────────────────────────
|
|
@@ -156,20 +94,11 @@ function killProcessGroup(pid, signal) {
|
|
|
156
94
|
|
|
157
95
|
function killOrphansByPort(port) {
|
|
158
96
|
try {
|
|
159
|
-
const pids = execSync(`lsof -ti :${port}`, {
|
|
160
|
-
encoding: 'utf8',
|
|
161
|
-
timeout: 5000,
|
|
162
|
-
}).trim();
|
|
97
|
+
const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
163
98
|
if (pids) {
|
|
164
99
|
const pidList = pids.split('\n').filter(Boolean);
|
|
165
100
|
for (const p of pidList) {
|
|
166
|
-
try {
|
|
167
|
-
process.kill(parseInt(p, 10), 'SIGKILL');
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (error?.code !== 'ESRCH') {
|
|
170
|
-
reportBestEffortFailure('kill legacy port owner failed', error);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
101
|
+
try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
|
|
173
102
|
}
|
|
174
103
|
return pidList;
|
|
175
104
|
}
|
|
@@ -182,440 +111,18 @@ let stopping = false;
|
|
|
182
111
|
const managedProcesses = []; // { name, pid, child }
|
|
183
112
|
|
|
184
113
|
function sleep(ms) {
|
|
185
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hasExactKeys(value, expected) {
|
|
189
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
190
|
-
const actual = Object.keys(value).sort();
|
|
191
|
-
const sortedExpected = [...expected].sort();
|
|
192
|
-
return (
|
|
193
|
-
actual.length === sortedExpected.length &&
|
|
194
|
-
actual.every((key, index) => key === sortedExpected[index])
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function readLinuxProcessIdentity(pid) {
|
|
199
|
-
if (process.platform !== 'linux' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
200
|
-
return null;
|
|
201
|
-
try {
|
|
202
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
203
|
-
const commandEnd = stat.lastIndexOf(')');
|
|
204
|
-
if (commandEnd < 0) return null;
|
|
205
|
-
const fields = stat
|
|
206
|
-
.slice(commandEnd + 1)
|
|
207
|
-
.trim()
|
|
208
|
-
.split(/\s+/);
|
|
209
|
-
if (!/^\d+$/.test(fields[19] || '') || !/^\d+$/.test(fields[2] || ''))
|
|
210
|
-
return null;
|
|
211
|
-
return {
|
|
212
|
-
state: fields[0],
|
|
213
|
-
processStartTicks: fields[19],
|
|
214
|
-
processGroupId: Number(fields[2]),
|
|
215
|
-
};
|
|
216
|
-
} catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function processIdentityIsLive(pid, expectedStartTicks) {
|
|
222
|
-
if (process.platform === 'linux') {
|
|
223
|
-
const identity = readLinuxProcessIdentity(pid);
|
|
224
|
-
return Boolean(
|
|
225
|
-
identity &&
|
|
226
|
-
identity.state !== 'Z' &&
|
|
227
|
-
identity.processStartTicks === expectedStartTicks
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
process.kill(pid, 0);
|
|
232
|
-
return true;
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function readJsonNoFollow(file) {
|
|
239
|
-
try {
|
|
240
|
-
const parent = fs.lstatSync(path.dirname(file));
|
|
241
|
-
if (
|
|
242
|
-
parent.isSymbolicLink() ||
|
|
243
|
-
!parent.isDirectory() ||
|
|
244
|
-
(parent.mode & 0o777) !== 0o700 ||
|
|
245
|
-
(typeof process.getuid === 'function' && parent.uid !== process.getuid())
|
|
246
|
-
)
|
|
247
|
-
return null;
|
|
248
|
-
const before = fs.lstatSync(file);
|
|
249
|
-
if (
|
|
250
|
-
before.isSymbolicLink() ||
|
|
251
|
-
!before.isFile() ||
|
|
252
|
-
(before.mode & 0o777) !== 0o600 ||
|
|
253
|
-
(typeof process.getuid === 'function' && before.uid !== process.getuid())
|
|
254
|
-
)
|
|
255
|
-
return null;
|
|
256
|
-
const fd = fs.openSync(
|
|
257
|
-
file,
|
|
258
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
259
|
-
);
|
|
260
|
-
try {
|
|
261
|
-
const opened = fs.fstatSync(fd);
|
|
262
|
-
if (
|
|
263
|
-
!opened.isFile() ||
|
|
264
|
-
before.dev !== opened.dev ||
|
|
265
|
-
before.ino !== opened.ino
|
|
266
|
-
)
|
|
267
|
-
return null;
|
|
268
|
-
return { payload: JSON.parse(fs.readFileSync(fd, 'utf8')), stat: opened };
|
|
269
|
-
} finally {
|
|
270
|
-
fs.closeSync(fd);
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function readStrictColdStartDecision() {
|
|
278
|
-
const decision = readJsonNoFollow(HANDOFF_DECISION_FILE)?.payload;
|
|
279
|
-
if (
|
|
280
|
-
!hasExactKeys(decision, [
|
|
281
|
-
'schemaVersion',
|
|
282
|
-
'appId',
|
|
283
|
-
'bootId',
|
|
284
|
-
'restoreEpochMs',
|
|
285
|
-
'publicPort',
|
|
286
|
-
'viteDepsCacheEnabled',
|
|
287
|
-
'nestBundleHandoffEnabled',
|
|
288
|
-
]) ||
|
|
289
|
-
decision.schemaVersion !== 1 ||
|
|
290
|
-
typeof decision.appId !== 'string' ||
|
|
291
|
-
decision.appId.length === 0 ||
|
|
292
|
-
typeof decision.bootId !== 'string' ||
|
|
293
|
-
!/^[a-zA-Z0-9._:-]{1,128}$/.test(decision.bootId) ||
|
|
294
|
-
!Number.isSafeInteger(decision.restoreEpochMs) ||
|
|
295
|
-
decision.restoreEpochMs <= 0 ||
|
|
296
|
-
decision.publicPort !== PLATFORM_HANDOFF_PUBLIC_PORT ||
|
|
297
|
-
typeof decision.viteDepsCacheEnabled !== 'boolean' ||
|
|
298
|
-
decision.nestBundleHandoffEnabled !== true
|
|
299
|
-
) {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
return decision;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const OWNER_KEYS = [
|
|
306
|
-
'schemaVersion',
|
|
307
|
-
'bootId',
|
|
308
|
-
'restoreEpochMs',
|
|
309
|
-
'generationId',
|
|
310
|
-
'pid',
|
|
311
|
-
'processStartTicks',
|
|
312
|
-
'publicPort',
|
|
313
|
-
'createdAtMs',
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
function readStrictHandoffOwner() {
|
|
317
|
-
const opened = readJsonNoFollow(HANDOFF_OWNER_FILE);
|
|
318
|
-
const owner = opened?.payload;
|
|
319
|
-
if (
|
|
320
|
-
!hasExactKeys(owner, OWNER_KEYS) ||
|
|
321
|
-
owner.schemaVersion !== 1 ||
|
|
322
|
-
owner.bootId !== HANDOFF_BOOT_ID ||
|
|
323
|
-
owner.restoreEpochMs !== HANDOFF_RESTORE_EPOCH_MS ||
|
|
324
|
-
!/^[a-f0-9]{64}$/.test(owner.generationId || '') ||
|
|
325
|
-
!Number.isSafeInteger(owner.pid) ||
|
|
326
|
-
owner.pid <= 0 ||
|
|
327
|
-
!/^\d+$/.test(owner.processStartTicks || '') ||
|
|
328
|
-
owner.publicPort !== PUBLIC_SERVER_PORT ||
|
|
329
|
-
!Number.isSafeInteger(owner.createdAtMs) ||
|
|
330
|
-
owner.createdAtMs < HANDOFF_RESTORE_EPOCH_MS ||
|
|
331
|
-
!processIdentityIsLive(owner.pid, owner.processStartTicks)
|
|
332
|
-
)
|
|
333
|
-
return null;
|
|
334
|
-
return owner;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function publicServerReachable() {
|
|
338
|
-
return new Promise(resolve => {
|
|
339
|
-
let settled = false;
|
|
340
|
-
const finish = reachable => {
|
|
341
|
-
if (settled) return;
|
|
342
|
-
settled = true;
|
|
343
|
-
resolve(reachable);
|
|
344
|
-
};
|
|
345
|
-
const basePath =
|
|
346
|
-
process.env.CLIENT_BASE_PATH?.startsWith('/') &&
|
|
347
|
-
!process.env.CLIENT_BASE_PATH.startsWith('//')
|
|
348
|
-
? process.env.CLIENT_BASE_PATH.replace(/\/+$/, '')
|
|
349
|
-
: '';
|
|
350
|
-
const request = http.get(
|
|
351
|
-
{
|
|
352
|
-
host: '127.0.0.1',
|
|
353
|
-
port: PUBLIC_SERVER_PORT,
|
|
354
|
-
path: `${basePath}/__innerapi__/capability/list`,
|
|
355
|
-
},
|
|
356
|
-
response => {
|
|
357
|
-
if (response.statusCode !== 200) {
|
|
358
|
-
response.resume();
|
|
359
|
-
response.once('end', () => finish(false));
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
const chunks = [];
|
|
363
|
-
let responseBytes = 0;
|
|
364
|
-
response.on('data', chunk => {
|
|
365
|
-
if (settled) return;
|
|
366
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
367
|
-
responseBytes += buffer.length;
|
|
368
|
-
if (responseBytes > MAX_HANDOFF_READINESS_RESPONSE_BYTES) {
|
|
369
|
-
response.destroy();
|
|
370
|
-
finish(false);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
chunks.push(buffer);
|
|
374
|
-
});
|
|
375
|
-
response.once('end', () => {
|
|
376
|
-
if (settled) return;
|
|
377
|
-
try {
|
|
378
|
-
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
379
|
-
finish(
|
|
380
|
-
payload?.status_code === '0' &&
|
|
381
|
-
payload.data &&
|
|
382
|
-
typeof payload.data === 'object' &&
|
|
383
|
-
!Array.isArray(payload.data) &&
|
|
384
|
-
Array.isArray(payload.data.capabilities)
|
|
385
|
-
);
|
|
386
|
-
} catch {
|
|
387
|
-
finish(false);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
response.once('error', () => finish(false));
|
|
391
|
-
}
|
|
392
|
-
);
|
|
393
|
-
request.setTimeout(250, () => {
|
|
394
|
-
request.destroy();
|
|
395
|
-
finish(false);
|
|
396
|
-
});
|
|
397
|
-
request.once('error', () => finish(false));
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function registryMatchesOwner(registry, owner) {
|
|
402
|
-
if (
|
|
403
|
-
!hasExactKeys(registry, [
|
|
404
|
-
'schemaVersion',
|
|
405
|
-
'bootId',
|
|
406
|
-
'restoreEpochMs',
|
|
407
|
-
'runtimePid',
|
|
408
|
-
'runtimeProcessStartTicks',
|
|
409
|
-
'updatedAtMs',
|
|
410
|
-
'processes',
|
|
411
|
-
]) ||
|
|
412
|
-
registry.schemaVersion !== 1 ||
|
|
413
|
-
registry.bootId !== HANDOFF_BOOT_ID ||
|
|
414
|
-
registry.restoreEpochMs !== HANDOFF_RESTORE_EPOCH_MS ||
|
|
415
|
-
registry.runtimePid !== owner.pid ||
|
|
416
|
-
registry.runtimeProcessStartTicks !== owner.processStartTicks ||
|
|
417
|
-
!Number.isSafeInteger(registry.updatedAtMs) ||
|
|
418
|
-
registry.updatedAtMs < HANDOFF_RESTORE_EPOCH_MS ||
|
|
419
|
-
!Array.isArray(registry.processes)
|
|
420
|
-
)
|
|
421
|
-
return false;
|
|
422
|
-
const roles = new Set();
|
|
423
|
-
return registry.processes.every(owned => {
|
|
424
|
-
const valid =
|
|
425
|
-
hasExactKeys(owned, ['role', 'pid', 'processStartTicks']) &&
|
|
426
|
-
['cache', 'source', 'action-plugin-init'].includes(owned.role) &&
|
|
427
|
-
!roles.has(owned.role) &&
|
|
428
|
-
Number.isSafeInteger(owned.pid) &&
|
|
429
|
-
owned.pid > 0 &&
|
|
430
|
-
/^\d+$/.test(owned.processStartTicks || '');
|
|
431
|
-
roles.add(owned.role);
|
|
432
|
-
return valid;
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function ownedGroupIdentityMatches(owned) {
|
|
437
|
-
const leaderIdentity = readLinuxProcessIdentity(owned.pid);
|
|
438
|
-
if (
|
|
439
|
-
leaderIdentity &&
|
|
440
|
-
(leaderIdentity.processStartTicks !== owned.processStartTicks ||
|
|
441
|
-
leaderIdentity.processGroupId !== owned.pid)
|
|
442
|
-
) {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
// Linux reserves a numeric PGID while any member of that group remains, so
|
|
446
|
-
// the dead leader's PID cannot be reused until the leaderless owned group is
|
|
447
|
-
// empty. Re-check both PID identity and group existence immediately before
|
|
448
|
-
// every signal; if a new leader appeared, fail closed above.
|
|
449
|
-
if (!leaderIdentity) {
|
|
450
|
-
try {
|
|
451
|
-
process.kill(owned.pid, 0);
|
|
452
|
-
return false;
|
|
453
|
-
} catch (error) {
|
|
454
|
-
if (error?.code !== 'ESRCH') return false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
for (const name of fs.readdirSync('/proc')) {
|
|
459
|
-
if (!/^\d+$/.test(name)) continue;
|
|
460
|
-
const member = readLinuxProcessIdentity(Number(name));
|
|
461
|
-
if (member?.processGroupId === owned.pid && member.state !== 'Z') {
|
|
462
|
-
const currentLeader = readLinuxProcessIdentity(owned.pid);
|
|
463
|
-
return Boolean(
|
|
464
|
-
!currentLeader ||
|
|
465
|
-
(currentLeader.processStartTicks === owned.processStartTicks &&
|
|
466
|
-
currentLeader.processGroupId === owned.pid)
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
} catch {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function runtimeGroupIdentityMatches(owner) {
|
|
477
|
-
const identity = readLinuxProcessIdentity(owner.pid);
|
|
478
|
-
return Boolean(
|
|
479
|
-
identity &&
|
|
480
|
-
identity.state !== 'Z' &&
|
|
481
|
-
identity.processStartTicks === owner.processStartTicks &&
|
|
482
|
-
identity.processGroupId === owner.pid
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async function terminateStrictRuntimeOwner(owner) {
|
|
487
|
-
if (process.platform !== 'linux' || !runtimeGroupIdentityMatches(owner))
|
|
488
|
-
return;
|
|
489
|
-
// Re-check pid birth identity and PGID immediately before every signal. A
|
|
490
|
-
// numeric pid from the marker is never sufficient authority after waiting.
|
|
491
|
-
if (runtimeGroupIdentityMatches(owner)) {
|
|
492
|
-
try {
|
|
493
|
-
process.kill(-owner.pid, 'SIGTERM');
|
|
494
|
-
} catch {}
|
|
495
|
-
}
|
|
496
|
-
const deadline = Date.now() + 2000;
|
|
497
|
-
while (Date.now() < deadline && runtimeGroupIdentityMatches(owner)) {
|
|
498
|
-
await sleep(25);
|
|
499
|
-
}
|
|
500
|
-
if (runtimeGroupIdentityMatches(owner)) {
|
|
501
|
-
try {
|
|
502
|
-
process.kill(-owner.pid, 'SIGKILL');
|
|
503
|
-
} catch {}
|
|
504
|
-
}
|
|
505
|
-
const killDeadline = Date.now() + 1000;
|
|
506
|
-
while (Date.now() < killDeadline && runtimeGroupIdentityMatches(owner)) {
|
|
507
|
-
await sleep(25);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async function cleanupDeadRuntimeRegistry(owner) {
|
|
512
|
-
if (process.platform !== 'linux') return;
|
|
513
|
-
const opened = readJsonNoFollow(HANDOFF_PROCESS_REGISTRY_FILE);
|
|
514
|
-
const registry = opened?.payload;
|
|
515
|
-
if (!opened || !registryMatchesOwner(registry, owner)) return;
|
|
516
|
-
for (const owned of registry.processes) {
|
|
517
|
-
if (!ownedGroupIdentityMatches(owned)) continue;
|
|
518
|
-
try {
|
|
519
|
-
process.kill(-owned.pid, 'SIGTERM');
|
|
520
|
-
} catch {}
|
|
521
|
-
}
|
|
522
|
-
const deadline = Date.now() + 2000;
|
|
523
|
-
while (
|
|
524
|
-
Date.now() < deadline &&
|
|
525
|
-
registry.processes.some(ownedGroupIdentityMatches)
|
|
526
|
-
) {
|
|
527
|
-
await sleep(25);
|
|
528
|
-
}
|
|
529
|
-
for (const owned of registry.processes) {
|
|
530
|
-
if (!ownedGroupIdentityMatches(owned)) continue;
|
|
531
|
-
try {
|
|
532
|
-
process.kill(-owned.pid, 'SIGKILL');
|
|
533
|
-
} catch {}
|
|
534
|
-
}
|
|
535
|
-
const killDeadline = Date.now() + 1000;
|
|
536
|
-
while (
|
|
537
|
-
Date.now() < killDeadline &&
|
|
538
|
-
registry.processes.some(ownedGroupIdentityMatches)
|
|
539
|
-
) {
|
|
540
|
-
await sleep(25);
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
const after = fs.lstatSync(HANDOFF_PROCESS_REGISTRY_FILE);
|
|
544
|
-
if (
|
|
545
|
-
after.dev === opened.stat.dev &&
|
|
546
|
-
after.ino === opened.stat.ino &&
|
|
547
|
-
!registry.processes.some(ownedGroupIdentityMatches)
|
|
548
|
-
) {
|
|
549
|
-
fs.unlinkSync(HANDOFF_PROCESS_REGISTRY_FILE);
|
|
550
|
-
}
|
|
551
|
-
} catch {}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function removeDeadOwnerMarker(owner) {
|
|
555
|
-
if (processIdentityIsLive(owner.pid, owner.processStartTicks)) return;
|
|
556
|
-
const opened = readJsonNoFollow(HANDOFF_OWNER_FILE);
|
|
557
|
-
const payload = opened?.payload;
|
|
558
|
-
if (
|
|
559
|
-
!opened ||
|
|
560
|
-
!hasExactKeys(payload, OWNER_KEYS) ||
|
|
561
|
-
payload.bootId !== owner.bootId ||
|
|
562
|
-
payload.restoreEpochMs !== owner.restoreEpochMs ||
|
|
563
|
-
payload.pid !== owner.pid ||
|
|
564
|
-
payload.processStartTicks !== owner.processStartTicks
|
|
565
|
-
)
|
|
566
|
-
return;
|
|
567
|
-
try {
|
|
568
|
-
const after = fs.lstatSync(HANDOFF_OWNER_FILE);
|
|
569
|
-
if (after.dev === opened.stat.dev && after.ino === opened.stat.ino) {
|
|
570
|
-
fs.unlinkSync(HANDOFF_OWNER_FILE);
|
|
571
|
-
}
|
|
572
|
-
} catch {}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function publicPortBindable() {
|
|
576
|
-
return new Promise(resolve => {
|
|
577
|
-
const server = net.createServer();
|
|
578
|
-
let settled = false;
|
|
579
|
-
const finish = bindable => {
|
|
580
|
-
if (settled) return;
|
|
581
|
-
settled = true;
|
|
582
|
-
server.close(() => resolve(bindable));
|
|
583
|
-
};
|
|
584
|
-
server.once('error', () => resolve(false));
|
|
585
|
-
server.listen(PUBLIC_SERVER_PORT, '127.0.0.1', () => finish(true));
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
async function waitForPublicPortRelease() {
|
|
590
|
-
while (!stopping) {
|
|
591
|
-
if (await publicPortBindable()) return true;
|
|
592
|
-
await sleep(50);
|
|
593
|
-
}
|
|
594
|
-
return false;
|
|
114
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
595
115
|
}
|
|
596
116
|
|
|
597
117
|
/**
|
|
598
118
|
* Start and supervise a process with auto-restart and log piping.
|
|
599
119
|
* Returns a promise that resolves when the process loop ends.
|
|
600
120
|
*/
|
|
601
|
-
function startProcess({
|
|
602
|
-
name,
|
|
603
|
-
command,
|
|
604
|
-
args,
|
|
605
|
-
cleanupPort,
|
|
606
|
-
environment,
|
|
607
|
-
strictOwnership = false,
|
|
608
|
-
}) {
|
|
121
|
+
function startProcess({ name, command, args, cleanupPort }) {
|
|
609
122
|
const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
|
|
610
123
|
const logFd = fs.openSync(logFilePath, 'a');
|
|
611
124
|
|
|
612
|
-
const entry = {
|
|
613
|
-
name,
|
|
614
|
-
pid: null,
|
|
615
|
-
child: null,
|
|
616
|
-
processStartTicks: null,
|
|
617
|
-
strictOwnership,
|
|
618
|
-
};
|
|
125
|
+
const entry = { name, pid: null, child: null };
|
|
619
126
|
managedProcesses.push(entry);
|
|
620
127
|
|
|
621
128
|
const run = async () => {
|
|
@@ -627,41 +134,21 @@ function startProcess({
|
|
|
627
134
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
628
135
|
shell: true,
|
|
629
136
|
cwd: PROJECT_ROOT,
|
|
630
|
-
env: { ...process.env
|
|
137
|
+
env: { ...process.env },
|
|
631
138
|
});
|
|
632
139
|
|
|
633
140
|
entry.pid = child.pid;
|
|
634
141
|
entry.child = child;
|
|
635
|
-
entry.processStartTicks = readLinuxProcessIdentity(
|
|
636
|
-
child.pid
|
|
637
|
-
)?.processStartTicks;
|
|
638
|
-
if (
|
|
639
|
-
strictOwnership &&
|
|
640
|
-
process.platform === 'linux' &&
|
|
641
|
-
!entry.processStartTicks
|
|
642
|
-
) {
|
|
643
|
-
child.kill('SIGKILL');
|
|
644
|
-
throw new Error(`Strict process identity unavailable: ${name}`);
|
|
645
|
-
}
|
|
646
142
|
|
|
647
143
|
const startTime = Date.now();
|
|
648
|
-
logEvent(
|
|
649
|
-
'INFO',
|
|
650
|
-
name,
|
|
651
|
-
`Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`
|
|
652
|
-
);
|
|
144
|
+
logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
|
|
653
145
|
|
|
654
146
|
// Pipe stdout and stderr through readline for timestamped logging
|
|
655
|
-
const pipeLines = stream => {
|
|
656
|
-
const rl = readline.createInterface({
|
|
657
|
-
|
|
658
|
-
crlfDelay: Infinity,
|
|
659
|
-
});
|
|
660
|
-
rl.on('line', line => {
|
|
147
|
+
const pipeLines = (stream) => {
|
|
148
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
149
|
+
rl.on('line', (line) => {
|
|
661
150
|
const msg = `[${timestamp()}] [${name}] ${line}\n`;
|
|
662
|
-
try {
|
|
663
|
-
fs.writeSync(logFd, msg);
|
|
664
|
-
} catch {}
|
|
151
|
+
try { fs.writeSync(logFd, msg); } catch {}
|
|
665
152
|
writeOutput(msg);
|
|
666
153
|
});
|
|
667
154
|
};
|
|
@@ -672,30 +159,25 @@ function startProcess({
|
|
|
672
159
|
// NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
|
|
673
160
|
// (e.g. nest's server) inherit stdout pipes. 'close' won't fire until ALL
|
|
674
161
|
// pipe holders exit, causing dev.js to hang when npm/nest dies but server survives.
|
|
675
|
-
const exitCode = await new Promise(resolve => {
|
|
676
|
-
child.on('exit', code => resolve(code ?? 1));
|
|
162
|
+
const exitCode = await new Promise((resolve) => {
|
|
163
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
677
164
|
child.on('error', () => resolve(1));
|
|
678
165
|
});
|
|
679
166
|
|
|
680
167
|
// Kill the entire process group
|
|
681
168
|
if (entry.pid) {
|
|
682
|
-
|
|
169
|
+
killProcessGroup(entry.pid, 'SIGTERM');
|
|
683
170
|
await sleep(2000);
|
|
684
|
-
|
|
171
|
+
killProcessGroup(entry.pid, 'SIGKILL');
|
|
685
172
|
}
|
|
686
173
|
entry.pid = null;
|
|
687
174
|
entry.child = null;
|
|
688
|
-
entry.processStartTicks = null;
|
|
689
175
|
|
|
690
176
|
// Port cleanup fallback
|
|
691
177
|
if (cleanupPort) {
|
|
692
178
|
const orphans = killOrphansByPort(cleanupPort);
|
|
693
179
|
if (orphans.length > 0) {
|
|
694
|
-
logEvent(
|
|
695
|
-
'WARN',
|
|
696
|
-
name,
|
|
697
|
-
`Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`
|
|
698
|
-
);
|
|
180
|
+
logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
|
|
699
181
|
await sleep(500);
|
|
700
182
|
}
|
|
701
183
|
}
|
|
@@ -705,133 +187,26 @@ function startProcess({
|
|
|
705
187
|
const runDuration = (Date.now() - startTime) / 1000;
|
|
706
188
|
if (runDuration >= 60) {
|
|
707
189
|
restartCount = 0;
|
|
708
|
-
logEvent(
|
|
709
|
-
'INFO',
|
|
710
|
-
name,
|
|
711
|
-
`Ran for ${Math.round(runDuration)}s, resetting restart counter`
|
|
712
|
-
);
|
|
190
|
+
logEvent('INFO', name, `Ran for ${Math.round(runDuration)}s, resetting restart counter`);
|
|
713
191
|
} else {
|
|
714
192
|
restartCount++;
|
|
715
193
|
}
|
|
716
194
|
if (restartCount >= MAX_RESTART_COUNT) {
|
|
717
|
-
logEvent(
|
|
718
|
-
'ERROR',
|
|
719
|
-
name,
|
|
720
|
-
`Max restart count (${MAX_RESTART_COUNT}) reached, giving up`
|
|
721
|
-
);
|
|
195
|
+
logEvent('ERROR', name, `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`);
|
|
722
196
|
break;
|
|
723
197
|
}
|
|
724
198
|
|
|
725
|
-
const delay = Math.min(
|
|
726
|
-
|
|
727
|
-
MAX_DELAY
|
|
728
|
-
);
|
|
729
|
-
logEvent(
|
|
730
|
-
'WARN',
|
|
731
|
-
name,
|
|
732
|
-
`Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`
|
|
733
|
-
);
|
|
199
|
+
const delay = Math.min(RESTART_DELAY * (1 << Math.max(0, restartCount - 1)), MAX_DELAY);
|
|
200
|
+
logEvent('WARN', name, `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`);
|
|
734
201
|
await sleep(delay * 1000);
|
|
735
202
|
}
|
|
736
203
|
|
|
737
|
-
try {
|
|
738
|
-
fs.closeSync(logFd);
|
|
739
|
-
} catch {}
|
|
204
|
+
try { fs.closeSync(logFd); } catch {}
|
|
740
205
|
};
|
|
741
206
|
|
|
742
207
|
return run();
|
|
743
208
|
}
|
|
744
209
|
|
|
745
|
-
function signalManagedProcess(entry, signal) {
|
|
746
|
-
if (!entry.pid) return;
|
|
747
|
-
if (entry.strictOwnership && process.platform === 'linux') {
|
|
748
|
-
if (
|
|
749
|
-
!ownedGroupIdentityMatches({
|
|
750
|
-
pid: entry.pid,
|
|
751
|
-
processStartTicks: entry.processStartTicks,
|
|
752
|
-
})
|
|
753
|
-
) {
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
killProcessGroup(entry.pid, signal);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function initializeActionPlugins() {
|
|
761
|
-
writeOutput('\n🔌 Initializing action plugins...\n');
|
|
762
|
-
try {
|
|
763
|
-
execSync('fullstack-cli action-plugin init', {
|
|
764
|
-
cwd: PROJECT_ROOT,
|
|
765
|
-
stdio: 'inherit',
|
|
766
|
-
});
|
|
767
|
-
writeOutput('✅ Action plugins initialized\n\n');
|
|
768
|
-
} catch {
|
|
769
|
-
writeOutput(
|
|
770
|
-
'⚠️ Action plugin initialization failed, continuing anyway...\n\n'
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
let handoffFailbackStarted = false;
|
|
776
|
-
|
|
777
|
-
async function startHandoffFailback(owner, reason) {
|
|
778
|
-
if (handoffFailbackStarted || stopping) return;
|
|
779
|
-
handoffFailbackStarted = true;
|
|
780
|
-
logEvent(
|
|
781
|
-
'ERROR',
|
|
782
|
-
'handoff-watchdog',
|
|
783
|
-
`Backend owner lost (${reason}); taking over the public server lifecycle`
|
|
784
|
-
);
|
|
785
|
-
await terminateStrictRuntimeOwner(owner);
|
|
786
|
-
await cleanupDeadRuntimeRegistry(owner);
|
|
787
|
-
removeDeadOwnerMarker(owner);
|
|
788
|
-
if (!(await waitForPublicPortRelease())) return;
|
|
789
|
-
initializeActionPlugins();
|
|
790
|
-
void startProcess({
|
|
791
|
-
name: 'server',
|
|
792
|
-
command: 'npm',
|
|
793
|
-
args: ['run', 'dev:server'],
|
|
794
|
-
environment: {
|
|
795
|
-
SERVER_HOST: '127.0.0.1',
|
|
796
|
-
SERVER_PORT: String(PUBLIC_SERVER_PORT),
|
|
797
|
-
},
|
|
798
|
-
strictOwnership: true,
|
|
799
|
-
// Never use lsof/pkill in handoff failback. The strict per-boot registry is
|
|
800
|
-
// the only authority for removing the dead runtime's children.
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
async function monitorHandoffOwner(initialOwner) {
|
|
805
|
-
let expectedOwner = initialOwner;
|
|
806
|
-
let identityFailures = 0;
|
|
807
|
-
let portFailures = 0;
|
|
808
|
-
let stableSuccesses = 0;
|
|
809
|
-
while (!stopping && !handoffFailbackStarted) {
|
|
810
|
-
const currentOwner = readStrictHandoffOwner();
|
|
811
|
-
const reachable = await publicServerReachable();
|
|
812
|
-
if (currentOwner) {
|
|
813
|
-
expectedOwner = currentOwner;
|
|
814
|
-
identityFailures = 0;
|
|
815
|
-
} else {
|
|
816
|
-
identityFailures += 1;
|
|
817
|
-
}
|
|
818
|
-
portFailures = reachable ? 0 : portFailures + 1;
|
|
819
|
-
stableSuccesses = currentOwner && reachable ? stableSuccesses + 1 : 0;
|
|
820
|
-
if (identityFailures >= 2 || portFailures >= 5) {
|
|
821
|
-
await startHandoffFailback(
|
|
822
|
-
expectedOwner,
|
|
823
|
-
identityFailures >= 2
|
|
824
|
-
? 'owner-identity-stale'
|
|
825
|
-
: 'public-port-unreachable'
|
|
826
|
-
);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
// Stay responsive while establishing health or after the first failure,
|
|
830
|
-
// then reduce steady-state readiness traffic from 5 QPS to below 1 QPS.
|
|
831
|
-
await sleep(stableSuccesses >= 3 ? 1500 : 200);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
210
|
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
836
211
|
let cleanupDone = false;
|
|
837
212
|
|
|
@@ -846,7 +221,7 @@ async function cleanup() {
|
|
|
846
221
|
for (const entry of managedProcesses) {
|
|
847
222
|
if (entry.pid) {
|
|
848
223
|
logEvent('INFO', 'main', `Stopping process group (PGID: ${entry.pid})`);
|
|
849
|
-
|
|
224
|
+
killProcessGroup(entry.pid, 'SIGTERM');
|
|
850
225
|
}
|
|
851
226
|
}
|
|
852
227
|
|
|
@@ -856,27 +231,19 @@ async function cleanup() {
|
|
|
856
231
|
// Force kill any remaining
|
|
857
232
|
for (const entry of managedProcesses) {
|
|
858
233
|
if (entry.pid) {
|
|
859
|
-
logEvent(
|
|
860
|
-
|
|
861
|
-
'main',
|
|
862
|
-
`Force killing process group (PGID: ${entry.pid})`
|
|
863
|
-
);
|
|
864
|
-
signalManagedProcess(entry, 'SIGKILL');
|
|
234
|
+
logEvent('WARN', 'main', `Force killing process group (PGID: ${entry.pid})`);
|
|
235
|
+
killProcessGroup(entry.pid, 'SIGKILL');
|
|
865
236
|
}
|
|
866
237
|
}
|
|
867
238
|
|
|
868
239
|
// Port cleanup fallback
|
|
869
|
-
|
|
240
|
+
killOrphansByPort(SERVER_PORT);
|
|
870
241
|
killOrphansByPort(CLIENT_DEV_PORT);
|
|
871
242
|
|
|
872
243
|
logEvent('INFO', 'main', 'All processes stopped');
|
|
873
244
|
|
|
874
|
-
try {
|
|
875
|
-
|
|
876
|
-
} catch {}
|
|
877
|
-
try {
|
|
878
|
-
fs.closeSync(devLogFd);
|
|
879
|
-
} catch {}
|
|
245
|
+
try { fs.closeSync(devStdLogFd); } catch {}
|
|
246
|
+
try { fs.closeSync(devLogFd); } catch {}
|
|
880
247
|
|
|
881
248
|
process.exit(0);
|
|
882
249
|
}
|
|
@@ -900,35 +267,22 @@ async function main() {
|
|
|
900
267
|
|
|
901
268
|
cleanStaleDist();
|
|
902
269
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
'handoff-watchdog',
|
|
911
|
-
'Static handoff claim has no matching live owner; using legacy backend ownership'
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (!handoffOwnsBackend) {
|
|
916
|
-
// Initialize action plugins. In handoff mode the backend runtime owns this
|
|
917
|
-
// predecessor and the source process as one lifecycle.
|
|
918
|
-
initializeActionPlugins();
|
|
270
|
+
// Initialize action plugins
|
|
271
|
+
writeOutput('\n🔌 Initializing action plugins...\n');
|
|
272
|
+
try {
|
|
273
|
+
execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
274
|
+
writeOutput('✅ Action plugins initialized\n\n');
|
|
275
|
+
} catch {
|
|
276
|
+
writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
|
|
919
277
|
}
|
|
920
278
|
|
|
921
279
|
// Start server and client
|
|
922
|
-
const serverPromise =
|
|
923
|
-
|
|
924
|
-
:
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
cleanupPort: BACKEND_OWNED_BY_HANDOFF ? undefined : LEGACY_SERVER_PORT,
|
|
929
|
-
strictOwnership: BACKEND_OWNED_BY_HANDOFF,
|
|
930
|
-
});
|
|
931
|
-
if (handoffOwnsBackend) void monitorHandoffOwner(initialHandoffOwner);
|
|
280
|
+
const serverPromise = startProcess({
|
|
281
|
+
name: 'server',
|
|
282
|
+
command: 'npm',
|
|
283
|
+
args: ['run', 'dev:server'],
|
|
284
|
+
cleanupPort: SERVER_PORT,
|
|
285
|
+
});
|
|
932
286
|
|
|
933
287
|
const clientPromise = startProcess({
|
|
934
288
|
name: 'client',
|
|
@@ -948,7 +302,7 @@ async function main() {
|
|
|
948
302
|
}
|
|
949
303
|
}
|
|
950
304
|
|
|
951
|
-
main().catch(err => {
|
|
305
|
+
main().catch((err) => {
|
|
952
306
|
console.error('Fatal error:', err);
|
|
953
307
|
process.exit(1);
|
|
954
308
|
});
|