pidnap 0.0.0-dev.0 → 0.0.0-dev.2
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/cli.mjs +48 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{logger-crc5neL8.mjs → logger-BF3KrCIK.mjs} +102 -10
- package/dist/{logger-crc5neL8.mjs.map → logger-BF3KrCIK.mjs.map} +1 -1
- package/dist/task-list-CIdbB3wM.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/api/server.ts +2 -2
- package/src/index.ts +1 -0
- package/src/lazy-process.ts +25 -10
- package/src/manager.ts +40 -3
- package/src/port-utils.ts +39 -0
- package/src/tree-kill.ts +131 -0
package/src/manager.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type RestartingProcessOptions,
|
|
13
13
|
} from "./restarting-process.ts";
|
|
14
14
|
import { EnvManager } from "./env-manager.ts";
|
|
15
|
+
import { killProcessOnPort } from "./port-utils.ts";
|
|
15
16
|
|
|
16
17
|
// Valibot schemas
|
|
17
18
|
|
|
@@ -46,6 +47,7 @@ export const RestartingProcessEntrySchema = v.object({
|
|
|
46
47
|
options: v.optional(RestartingProcessOptionsSchema),
|
|
47
48
|
envFile: v.optional(v.string()),
|
|
48
49
|
envReloadDelay: v.optional(EnvReloadDelaySchema), // Default 5000ms
|
|
50
|
+
port: v.optional(v.number()), // Kill any process on this port before spawning
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
export type RestartingProcessEntry = v.InferOutput<typeof RestartingProcessEntrySchema>;
|
|
@@ -108,6 +110,9 @@ export class Manager {
|
|
|
108
110
|
private envReloadTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
109
111
|
private envChangeUnsubscribe: (() => void) | null = null;
|
|
110
112
|
|
|
113
|
+
// Port tracking for processes
|
|
114
|
+
private processPortConfig: Map<string, number> = new Map();
|
|
115
|
+
|
|
111
116
|
// Shutdown handling
|
|
112
117
|
private signalHandlers: Map<NodeJS.Signals, () => void> = new Map();
|
|
113
118
|
private shutdownPromise: Promise<void> | null = null;
|
|
@@ -182,6 +187,19 @@ export class Manager {
|
|
|
182
187
|
return join(this.logDir, "cron", `${name}.log`);
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Kill any process running on the configured port for a process
|
|
192
|
+
*/
|
|
193
|
+
private async killPortForProcess(processName: string): Promise<void> {
|
|
194
|
+
const port = this.processPortConfig.get(processName);
|
|
195
|
+
if (port === undefined) return;
|
|
196
|
+
|
|
197
|
+
const killed = await killProcessOnPort(port);
|
|
198
|
+
if (killed) {
|
|
199
|
+
this.logger.info(`Killed process on port ${port} before starting "${processName}"`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
private ensureLogDirs(): void {
|
|
186
204
|
mkdirSync(this.logDir, { recursive: true });
|
|
187
205
|
mkdirSync(join(this.logDir, "process"), { recursive: true });
|
|
@@ -389,11 +407,13 @@ export class Manager {
|
|
|
389
407
|
/**
|
|
390
408
|
* Start a restarting process by target
|
|
391
409
|
*/
|
|
392
|
-
startProcessByTarget(target: string | number): RestartingProcess {
|
|
410
|
+
async startProcessByTarget(target: string | number): Promise<RestartingProcess> {
|
|
393
411
|
const proc = this.getProcessByTarget(target);
|
|
394
412
|
if (!proc) {
|
|
395
413
|
throw new Error(`Process not found: ${target}`);
|
|
396
414
|
}
|
|
415
|
+
// Kill any process on the configured port before starting
|
|
416
|
+
await this.killPortForProcess(proc.name);
|
|
397
417
|
proc.start();
|
|
398
418
|
return proc;
|
|
399
419
|
}
|
|
@@ -421,6 +441,8 @@ export class Manager {
|
|
|
421
441
|
if (!proc) {
|
|
422
442
|
throw new Error(`Process not found: ${target}`);
|
|
423
443
|
}
|
|
444
|
+
// Kill any process on the configured port before restarting
|
|
445
|
+
await this.killPortForProcess(proc.name);
|
|
424
446
|
await proc.restart(force);
|
|
425
447
|
return proc;
|
|
426
448
|
}
|
|
@@ -527,12 +549,13 @@ export class Manager {
|
|
|
527
549
|
/**
|
|
528
550
|
* Add a restarting process at runtime
|
|
529
551
|
*/
|
|
530
|
-
addProcess(
|
|
552
|
+
async addProcess(
|
|
531
553
|
name: string,
|
|
532
554
|
definition: ProcessDefinition,
|
|
533
555
|
options?: RestartingProcessOptions,
|
|
534
556
|
envReloadDelay?: EnvReloadDelay,
|
|
535
|
-
|
|
557
|
+
port?: number,
|
|
558
|
+
): Promise<RestartingProcess> {
|
|
536
559
|
if (this.restartingProcesses.has(name)) {
|
|
537
560
|
throw new Error(`Process "${name}" already exists`);
|
|
538
561
|
}
|
|
@@ -545,6 +568,13 @@ export class Manager {
|
|
|
545
568
|
processLogger,
|
|
546
569
|
);
|
|
547
570
|
this.restartingProcesses.set(name, restartingProcess);
|
|
571
|
+
|
|
572
|
+
// Track port config and kill any process on that port before starting
|
|
573
|
+
if (port !== undefined) {
|
|
574
|
+
this.processPortConfig.set(name, port);
|
|
575
|
+
await this.killPortForProcess(name);
|
|
576
|
+
}
|
|
577
|
+
|
|
548
578
|
restartingProcess.start();
|
|
549
579
|
|
|
550
580
|
// Track env reload config for this process
|
|
@@ -662,6 +692,13 @@ export class Manager {
|
|
|
662
692
|
processLogger,
|
|
663
693
|
);
|
|
664
694
|
this.restartingProcesses.set(entry.name, restartingProcess);
|
|
695
|
+
|
|
696
|
+
// Track port config and kill any process on that port before starting
|
|
697
|
+
if (entry.port !== undefined) {
|
|
698
|
+
this.processPortConfig.set(entry.name, entry.port);
|
|
699
|
+
await this.killPortForProcess(entry.name);
|
|
700
|
+
}
|
|
701
|
+
|
|
665
702
|
restartingProcess.start();
|
|
666
703
|
|
|
667
704
|
// Track env reload config for this process
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { treeKill } from "./tree-kill.ts";
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Kill any process listening on the specified port, including all descendants.
|
|
9
|
+
*/
|
|
10
|
+
export async function killProcessOnPort(port: number): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
// Use lsof to find the process ID listening on the port
|
|
13
|
+
const { stdout } = await execAsync(`lsof -ti tcp:${port}`);
|
|
14
|
+
const pids = stdout
|
|
15
|
+
.trim()
|
|
16
|
+
.split("\n")
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map((p) => parseInt(p, 10))
|
|
19
|
+
.filter((p) => !isNaN(p));
|
|
20
|
+
|
|
21
|
+
if (pids.length === 0) {
|
|
22
|
+
return false; // No process found on port
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Tree kill all processes found on the port (kills children/grandchildren too)
|
|
26
|
+
for (const pid of pids) {
|
|
27
|
+
try {
|
|
28
|
+
await treeKill(pid, "SIGKILL");
|
|
29
|
+
} catch {
|
|
30
|
+
// Process may have already exited
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
// Command failed (e.g., no process on port)
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/tree-kill.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
type ProcessTree = Record<number, number[]>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Kill a process and all its descendants (children, grandchildren, etc.)
|
|
7
|
+
* @param pid - The process ID to kill
|
|
8
|
+
* @param signal - The signal to send (default: SIGTERM)
|
|
9
|
+
* @returns Promise that resolves when all processes have been signaled
|
|
10
|
+
*/
|
|
11
|
+
export async function treeKill(pid: number, signal: NodeJS.Signals = "SIGTERM"): Promise<void> {
|
|
12
|
+
if (Number.isNaN(pid)) {
|
|
13
|
+
throw new Error("pid must be a number");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const tree: ProcessTree = { [pid]: [] };
|
|
17
|
+
const pidsToProcess = new Set<number>([pid]);
|
|
18
|
+
|
|
19
|
+
// Build the process tree
|
|
20
|
+
await buildProcessTree(pid, tree, pidsToProcess);
|
|
21
|
+
|
|
22
|
+
// Kill all processes in the tree (children first, then parents)
|
|
23
|
+
killAll(tree, signal);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a tree of all child processes recursively
|
|
28
|
+
*/
|
|
29
|
+
async function buildProcessTree(
|
|
30
|
+
parentPid: number,
|
|
31
|
+
tree: ProcessTree,
|
|
32
|
+
pidsToProcess: Set<number>,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const ps = spawn("ps", ["-o", "pid", "--no-headers", "--ppid", String(parentPid)]);
|
|
36
|
+
|
|
37
|
+
let allData = "";
|
|
38
|
+
|
|
39
|
+
ps.stdout.on("data", (data: Buffer) => {
|
|
40
|
+
allData += data.toString("ascii");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
ps.on("close", async (code) => {
|
|
44
|
+
pidsToProcess.delete(parentPid);
|
|
45
|
+
|
|
46
|
+
if (code !== 0) {
|
|
47
|
+
// No child processes found for this parent
|
|
48
|
+
if (pidsToProcess.size === 0) {
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const childPids = allData.match(/\d+/g);
|
|
55
|
+
if (!childPids) {
|
|
56
|
+
if (pidsToProcess.size === 0) {
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Process all child PIDs concurrently
|
|
63
|
+
const promises: Promise<void>[] = [];
|
|
64
|
+
|
|
65
|
+
for (const pidStr of childPids) {
|
|
66
|
+
const childPid = parseInt(pidStr, 10);
|
|
67
|
+
tree[parentPid].push(childPid);
|
|
68
|
+
tree[childPid] = [];
|
|
69
|
+
pidsToProcess.add(childPid);
|
|
70
|
+
promises.push(buildProcessTree(childPid, tree, pidsToProcess));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await Promise.all(promises);
|
|
74
|
+
|
|
75
|
+
if (pidsToProcess.size === 0) {
|
|
76
|
+
resolve();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ps.on("error", () => {
|
|
81
|
+
pidsToProcess.delete(parentPid);
|
|
82
|
+
if (pidsToProcess.size === 0) {
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Kill all processes in the tree
|
|
91
|
+
* Kills children before parents to ensure clean shutdown
|
|
92
|
+
*/
|
|
93
|
+
function killAll(tree: ProcessTree, signal: NodeJS.Signals): void {
|
|
94
|
+
const killed = new Set<number>();
|
|
95
|
+
|
|
96
|
+
// Get all PIDs and sort by depth (deepest first)
|
|
97
|
+
const allPids = Object.keys(tree).map(Number);
|
|
98
|
+
|
|
99
|
+
// Kill children first, then parents
|
|
100
|
+
for (const pid of allPids) {
|
|
101
|
+
// Kill all children of this pid
|
|
102
|
+
for (const childPid of tree[pid]) {
|
|
103
|
+
if (!killed.has(childPid)) {
|
|
104
|
+
killPid(childPid, signal);
|
|
105
|
+
killed.add(childPid);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Kill all parent pids
|
|
111
|
+
for (const pid of allPids) {
|
|
112
|
+
if (!killed.has(pid)) {
|
|
113
|
+
killPid(pid, signal);
|
|
114
|
+
killed.add(pid);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Kill a single process, ignoring ESRCH errors (process already dead)
|
|
121
|
+
*/
|
|
122
|
+
function killPid(pid: number, signal: NodeJS.Signals): void {
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, signal);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// ESRCH = No such process (already dead) - ignore this
|
|
127
|
+
if ((err as NodeJS.ErrnoException).code !== "ESRCH") {
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|