@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.
@@ -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
- process.env.MAX_RESTART_COUNT != null && process.env.MAX_RESTART_COUNT !== ''
38
- ? parseInt(process.env.MAX_RESTART_COUNT, 10)
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 LEGACY_SERVER_PORT = process.env.SERVER_PORT || '3000';
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.getMonth() + 1).padStart(2, '0') +
84
- '-' +
85
- String(now.getDate()).padStart(2, '0') +
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, ...environment },
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
- input: stream,
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
- signalManagedProcess(entry, 'SIGTERM');
169
+ killProcessGroup(entry.pid, 'SIGTERM');
683
170
  await sleep(2000);
684
- signalManagedProcess(entry, 'SIGKILL');
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
- RESTART_DELAY * (1 << Math.max(0, restartCount - 1)),
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
- signalManagedProcess(entry, 'SIGTERM');
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
- 'WARN',
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
- if (!BACKEND_OWNED_BY_HANDOFF) killOrphansByPort(LEGACY_SERVER_PORT);
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
- fs.closeSync(devStdLogFd);
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
- const initialHandoffOwner = BACKEND_OWNED_BY_HANDOFF
904
- ? readStrictHandoffOwner()
905
- : null;
906
- const handoffOwnsBackend = Boolean(initialHandoffOwner);
907
- if (BACKEND_OWNED_BY_HANDOFF && !handoffOwnsBackend) {
908
- logEvent(
909
- 'WARN',
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 = handoffOwnsBackend
923
- ? Promise.resolve()
924
- : startProcess({
925
- name: 'server',
926
- command: 'npm',
927
- args: ['run', 'dev:server'],
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
  });