@aion0/forge 0.10.39 → 0.10.40
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/RELEASE_NOTES.md +4 -5
- package/bin/forge-server.mjs +91 -29
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.40
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-05
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.39
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
-
|
|
9
|
-
- fix(chat): heal orphan tool_use blocks in session history
|
|
8
|
+
- fix(server): portable port-pid lookup so Linux without lsof can stop (#33)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
11
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.39...v0.10.40
|
package/bin/forge-server.mjs
CHANGED
|
@@ -289,17 +289,59 @@ if (!isStop) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// ── Find pids listening on a TCP port (portable) ──
|
|
293
|
+
// macOS ships lsof; minimal Linux containers (RHEL/CentOS/Alpine) often
|
|
294
|
+
// don't. Try lsof → ss (iproute2, ~universal on modern Linux) → fuser
|
|
295
|
+
// (psmisc) in order. Empty result means no listener, period.
|
|
296
|
+
//
|
|
297
|
+
// Without this, `forge server stop` on Linux silently leaked next-server
|
|
298
|
+
// because `lsof -ti:8403` threw ENOENT and the whole stop path was
|
|
299
|
+
// wrapped in try/catch — the port lookup just disappeared.
|
|
300
|
+
function findPortPids(port) {
|
|
301
|
+
// 1) lsof
|
|
302
|
+
try {
|
|
303
|
+
const out = execSync(`lsof -ti:${port}`, {
|
|
304
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
305
|
+
}).trim();
|
|
306
|
+
if (out) return [...new Set(out.split('\n').map(s => s.trim()).filter(Boolean))];
|
|
307
|
+
} catch { /* fall through */ }
|
|
308
|
+
// 2) ss -tlnp — line looks like:
|
|
309
|
+
// LISTEN 0 511 *:8403 ... users:(("next-server",pid=903438,fd=14))
|
|
310
|
+
try {
|
|
311
|
+
const out = execSync(`ss -tlnp 2>/dev/null`, {
|
|
312
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
313
|
+
});
|
|
314
|
+
const pids = new Set();
|
|
315
|
+
for (const line of out.split('\n')) {
|
|
316
|
+
// Match port at end of local-address field, before either whitespace
|
|
317
|
+
// (IPv4 `*:8403 `) or end-of-field (IPv6 `[::]:8403 `).
|
|
318
|
+
if (!new RegExp(`:${port}\\b`).test(line)) continue;
|
|
319
|
+
for (const m of line.matchAll(/pid=(\d+)/g)) pids.add(m[1]);
|
|
320
|
+
}
|
|
321
|
+
if (pids.size) return [...pids];
|
|
322
|
+
} catch { /* fall through */ }
|
|
323
|
+
// 3) fuser
|
|
324
|
+
try {
|
|
325
|
+
const out = execSync(`fuser ${port}/tcp 2>/dev/null`, {
|
|
326
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
327
|
+
});
|
|
328
|
+
const pids = out.trim().split(/\s+/).filter(p => /^\d+$/.test(p));
|
|
329
|
+
if (pids.length) return [...new Set(pids)];
|
|
330
|
+
} catch { /* nothing more */ }
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
292
334
|
// ── Reset terminal server (kill port + tmux sessions) ──
|
|
293
335
|
if (resetTerminal) {
|
|
294
336
|
console.log(`[forge] Resetting terminal server (port ${terminalPort})...`);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
337
|
+
const pids = findPortPids(terminalPort);
|
|
338
|
+
if (pids.length === 0) {
|
|
339
|
+
console.log(`[forge] No process on port ${terminalPort}`);
|
|
340
|
+
} else {
|
|
341
|
+
for (const pid of pids) {
|
|
342
|
+
try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
|
|
299
343
|
}
|
|
300
344
|
console.log(`[forge] Killed terminal server on port ${terminalPort}`);
|
|
301
|
-
} catch {
|
|
302
|
-
console.log(`[forge] No process on port ${terminalPort}`);
|
|
303
345
|
}
|
|
304
346
|
}
|
|
305
347
|
|
|
@@ -338,14 +380,10 @@ function cleanupOrphans() {
|
|
|
338
380
|
try {
|
|
339
381
|
// Kill processes on our ports
|
|
340
382
|
for (const port of [webPort, terminalPort]) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (p === myPid || protectedPids.has(p)) continue;
|
|
346
|
-
try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
|
|
347
|
-
}
|
|
348
|
-
} catch {}
|
|
383
|
+
for (const p of findPortPids(port)) {
|
|
384
|
+
if (p === myPid || protectedPids.has(p)) continue;
|
|
385
|
+
try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
|
|
386
|
+
}
|
|
349
387
|
}
|
|
350
388
|
// Kill standalone processes: our instance's + orphans without any tag
|
|
351
389
|
try {
|
|
@@ -365,11 +403,25 @@ function cleanupOrphans() {
|
|
|
365
403
|
// imported lib/task-manager (directly or via lib/pipeline) starts its
|
|
366
404
|
// own setInterval task runner that never exits — those run in parallel
|
|
367
405
|
// with the real runner and silently steal tasks. Detect via lsof on
|
|
368
|
-
// workflow.db,
|
|
406
|
+
// workflow.db (Mac), with a fuser fallback for lsof-less Linux.
|
|
369
407
|
try {
|
|
370
408
|
const dbPath = join(DATA_DIR, 'workflow.db');
|
|
371
|
-
|
|
372
|
-
|
|
409
|
+
let holders = [];
|
|
410
|
+
try {
|
|
411
|
+
const out = execSync(`lsof -t "${dbPath}"`, {
|
|
412
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
413
|
+
}).trim();
|
|
414
|
+
holders = out.split('\n').map(s => s.trim()).filter(Boolean);
|
|
415
|
+
} catch {
|
|
416
|
+
try {
|
|
417
|
+
// `fuser <file>` writes pids to stderr, not stdout. Merge streams.
|
|
418
|
+
const out = execSync(`fuser "${dbPath}" 2>&1`, {
|
|
419
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
420
|
+
});
|
|
421
|
+
holders = out.replace(/^.*?:/, '').trim().split(/\s+/).filter(p => /^\d+$/.test(p));
|
|
422
|
+
} catch { /* both unavailable — zombie scan disabled */ }
|
|
423
|
+
}
|
|
424
|
+
for (const pid of holders) {
|
|
373
425
|
if (pid === myPid || protectedPids.has(pid)) continue;
|
|
374
426
|
let cmd = '';
|
|
375
427
|
try {
|
|
@@ -458,25 +510,35 @@ async function stopServer() {
|
|
|
458
510
|
} catch {}
|
|
459
511
|
try { unlinkSync(PID_FILE); } catch {}
|
|
460
512
|
|
|
461
|
-
// Also kill by port (in case PID file is stale)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (pids) console.log(`[forge] Killed processes on port ${webPort}`);
|
|
470
|
-
} catch {}
|
|
513
|
+
// Also kill by port (in case PID file is stale). Use findPortPids
|
|
514
|
+
// so Linux-without-lsof still works.
|
|
515
|
+
const portPids = findPortPids(webPort);
|
|
516
|
+
for (const p of portPids) {
|
|
517
|
+
const pid = parseInt(p);
|
|
518
|
+
try { process.kill(pid, 'SIGTERM'); stopped = true; } catch {}
|
|
519
|
+
}
|
|
520
|
+
if (portPids.length > 0) console.log(`[forge] Killed processes on port ${webPort}`);
|
|
471
521
|
|
|
472
|
-
// Force kill after 2 seconds
|
|
522
|
+
// Force kill survivors after 2 seconds.
|
|
473
523
|
if (portPids.length > 0) {
|
|
474
524
|
await new Promise(r => setTimeout(r, 2000));
|
|
475
525
|
for (const pid of portPids) {
|
|
476
|
-
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
526
|
+
try { process.kill(parseInt(pid), 'SIGKILL'); } catch {}
|
|
477
527
|
}
|
|
478
528
|
}
|
|
479
529
|
|
|
530
|
+
// Final verify — if anything still listens on the port (e.g. a child
|
|
531
|
+
// re-bound, or our SIGKILL hit EPERM), surface it loudly. Silent leak
|
|
532
|
+
// is exactly the bug we're fixing.
|
|
533
|
+
const survivors = findPortPids(webPort);
|
|
534
|
+
if (survivors.length > 0) {
|
|
535
|
+
console.warn(
|
|
536
|
+
`[forge] WARNING: port ${webPort} still bound by pid(s) ${survivors.join(', ')} after stop. ` +
|
|
537
|
+
`Likely a different user / cron-launched / sudo'd instance. ` +
|
|
538
|
+
`Run: kill ${survivors.join(' ')} (or with sudo) to free it.`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
480
542
|
if (!stopped) {
|
|
481
543
|
console.log('[forge] No running server found');
|
|
482
544
|
}
|
package/package.json
CHANGED