pidnap 0.0.0-dev.0 → 0.0.0-dev.1
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 +67 -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-uliALovz.mjs} +103 -10
- package/dist/logger-uliALovz.mjs.map +1 -0
- 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 +76 -0
- package/src/tree-kill.ts +138 -0
- package/dist/logger-crc5neL8.mjs.map +0 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { ManagerConfig } from \"./manager.ts\";\n\nexport function defineConfig(config: ManagerConfig) {\n return config;\n}\n\nexport * from \"./restarting-process.ts\";\nexport * from \"./cron-process.ts\";\nexport * from \"./task-list.ts\";\nexport * from \"./lazy-process.ts\";\nexport * from \"./env-manager.ts\";\nexport * from \"./logger.ts\";\n"],"mappings":";;;AAEA,SAAgB,aAAa,QAAuB;AAClD,QAAO"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { ManagerConfig } from \"./manager.ts\";\n\nexport function defineConfig(config: ManagerConfig) {\n return config;\n}\n\nexport * from \"./restarting-process.ts\";\nexport * from \"./cron-process.ts\";\nexport * from \"./task-list.ts\";\nexport * from \"./lazy-process.ts\";\nexport * from \"./env-manager.ts\";\nexport * from \"./logger.ts\";\nexport * from \"./tree-kill.ts\";\n"],"mappings":";;;AAEA,SAAgB,aAAa,QAAuB;AAClD,QAAO"}
|
|
@@ -9,6 +9,92 @@ import { parse } from "dotenv";
|
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { format } from "node:util";
|
|
11
11
|
|
|
12
|
+
//#region src/tree-kill.ts
|
|
13
|
+
/**
|
|
14
|
+
* Kill a process and all its descendants (children, grandchildren, etc.)
|
|
15
|
+
* @param pid - The process ID to kill
|
|
16
|
+
* @param signal - The signal to send (default: SIGTERM)
|
|
17
|
+
* @returns Promise that resolves when all processes have been signaled
|
|
18
|
+
*/
|
|
19
|
+
async function treeKill(pid, signal = "SIGTERM") {
|
|
20
|
+
if (Number.isNaN(pid)) throw new Error("pid must be a number");
|
|
21
|
+
const platform = process.platform;
|
|
22
|
+
const tree = { [pid]: [] };
|
|
23
|
+
await buildProcessTree(pid, tree, new Set([pid]), platform);
|
|
24
|
+
killAll(tree, signal);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a tree of all child processes recursively
|
|
28
|
+
*/
|
|
29
|
+
async function buildProcessTree(parentPid, tree, pidsToProcess, platform) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const ps = platform === "darwin" ? spawn("pgrep", ["-P", String(parentPid)]) : spawn("ps", [
|
|
32
|
+
"-o",
|
|
33
|
+
"pid",
|
|
34
|
+
"--no-headers",
|
|
35
|
+
"--ppid",
|
|
36
|
+
String(parentPid)
|
|
37
|
+
]);
|
|
38
|
+
let allData = "";
|
|
39
|
+
ps.stdout.on("data", (data) => {
|
|
40
|
+
allData += data.toString("ascii");
|
|
41
|
+
});
|
|
42
|
+
ps.on("close", async (code) => {
|
|
43
|
+
pidsToProcess.delete(parentPid);
|
|
44
|
+
if (code !== 0) {
|
|
45
|
+
if (pidsToProcess.size === 0) resolve();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const childPids = allData.match(/\d+/g);
|
|
49
|
+
if (!childPids) {
|
|
50
|
+
if (pidsToProcess.size === 0) resolve();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const promises = [];
|
|
54
|
+
for (const pidStr of childPids) {
|
|
55
|
+
const childPid = parseInt(pidStr, 10);
|
|
56
|
+
tree[parentPid].push(childPid);
|
|
57
|
+
tree[childPid] = [];
|
|
58
|
+
pidsToProcess.add(childPid);
|
|
59
|
+
promises.push(buildProcessTree(childPid, tree, pidsToProcess, platform));
|
|
60
|
+
}
|
|
61
|
+
await Promise.all(promises);
|
|
62
|
+
if (pidsToProcess.size === 0) resolve();
|
|
63
|
+
});
|
|
64
|
+
ps.on("error", () => {
|
|
65
|
+
pidsToProcess.delete(parentPid);
|
|
66
|
+
if (pidsToProcess.size === 0) resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Kill all processes in the tree
|
|
72
|
+
* Kills children before parents to ensure clean shutdown
|
|
73
|
+
*/
|
|
74
|
+
function killAll(tree, signal) {
|
|
75
|
+
const killed = /* @__PURE__ */ new Set();
|
|
76
|
+
const allPids = Object.keys(tree).map(Number);
|
|
77
|
+
for (const pid of allPids) for (const childPid of tree[pid]) if (!killed.has(childPid)) {
|
|
78
|
+
killPid(childPid, signal);
|
|
79
|
+
killed.add(childPid);
|
|
80
|
+
}
|
|
81
|
+
for (const pid of allPids) if (!killed.has(pid)) {
|
|
82
|
+
killPid(pid, signal);
|
|
83
|
+
killed.add(pid);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Kill a single process, ignoring ESRCH errors (process already dead)
|
|
88
|
+
*/
|
|
89
|
+
function killPid(pid, signal) {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, signal);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err.code !== "ESRCH") throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
12
98
|
//#region src/lazy-process.ts
|
|
13
99
|
const ProcessDefinitionSchema = v.object({
|
|
14
100
|
command: v.string(),
|
|
@@ -25,14 +111,21 @@ const ProcessStateSchema = v.picklist([
|
|
|
25
111
|
"error"
|
|
26
112
|
]);
|
|
27
113
|
/**
|
|
28
|
-
* Kill a process
|
|
29
|
-
*
|
|
114
|
+
* Kill a process and all its descendants (children, grandchildren, etc.)
|
|
115
|
+
* Falls back to simple child.kill() if tree kill fails.
|
|
30
116
|
*/
|
|
31
|
-
function
|
|
117
|
+
async function killProcessTree(child, signal) {
|
|
118
|
+
const pid = child.pid;
|
|
119
|
+
if (pid === void 0) return false;
|
|
32
120
|
try {
|
|
33
|
-
|
|
121
|
+
await treeKill(pid, signal);
|
|
122
|
+
return true;
|
|
34
123
|
} catch {
|
|
35
|
-
|
|
124
|
+
try {
|
|
125
|
+
return child.kill(signal);
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
36
129
|
}
|
|
37
130
|
}
|
|
38
131
|
var LazyProcess = class {
|
|
@@ -129,12 +222,12 @@ var LazyProcess = class {
|
|
|
129
222
|
}
|
|
130
223
|
this._state = "stopping";
|
|
131
224
|
this.logger.info(`Stopping process with SIGTERM`);
|
|
132
|
-
|
|
225
|
+
await killProcessTree(this.childProcess, "SIGTERM");
|
|
133
226
|
const timeoutMs = timeout ?? 5e3;
|
|
134
227
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
|
|
135
228
|
if (await Promise.race([this.processExit.promise.then(() => "exited"), timeoutPromise]) === "timeout" && this.childProcess) {
|
|
136
229
|
this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL`);
|
|
137
|
-
|
|
230
|
+
await killProcessTree(this.childProcess, "SIGKILL");
|
|
138
231
|
const killTimeout = new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
139
232
|
await Promise.race([this.processExit.promise, killTimeout]);
|
|
140
233
|
}
|
|
@@ -144,7 +237,7 @@ var LazyProcess = class {
|
|
|
144
237
|
}
|
|
145
238
|
async reset() {
|
|
146
239
|
if (this.childProcess) {
|
|
147
|
-
|
|
240
|
+
await killProcessTree(this.childProcess, "SIGKILL");
|
|
148
241
|
await this.processExit.promise;
|
|
149
242
|
this.cleanup();
|
|
150
243
|
}
|
|
@@ -962,5 +1055,5 @@ const logger = (input) => {
|
|
|
962
1055
|
};
|
|
963
1056
|
|
|
964
1057
|
//#endregion
|
|
965
|
-
export { ProcessDefinitionSchema as _, TaskStateSchema as a, CronProcessStateSchema as c, CrashLoopConfigSchema as d, RestartPolicySchema as f, LazyProcess as g, RestartingProcessStateSchema as h, TaskList as i, RetryConfigSchema as l, RestartingProcessOptionsSchema as m, EnvManager as n, CronProcess as o, RestartingProcess as p, NamedProcessDefinitionSchema as r, CronProcessOptionsSchema as s, logger as t, BackoffStrategySchema as u, ProcessStateSchema as v };
|
|
966
|
-
//# sourceMappingURL=logger-
|
|
1058
|
+
export { ProcessDefinitionSchema as _, TaskStateSchema as a, CronProcessStateSchema as c, CrashLoopConfigSchema as d, RestartPolicySchema as f, LazyProcess as g, RestartingProcessStateSchema as h, TaskList as i, RetryConfigSchema as l, RestartingProcessOptionsSchema as m, EnvManager as n, CronProcess as o, RestartingProcess as p, NamedProcessDefinitionSchema as r, CronProcessOptionsSchema as s, logger as t, BackoffStrategySchema as u, ProcessStateSchema as v, treeKill as y };
|
|
1059
|
+
//# sourceMappingURL=logger-uliALovz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-uliALovz.mjs","names":[],"sources":["../src/tree-kill.ts","../src/lazy-process.ts","../src/restarting-process.ts","../src/cron-process.ts","../src/task-list.ts","../src/env-manager.ts","../src/logger.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\n\ntype ProcessTree = Record<number, number[]>;\n\n/**\n * Kill a process and all its descendants (children, grandchildren, etc.)\n * @param pid - The process ID to kill\n * @param signal - The signal to send (default: SIGTERM)\n * @returns Promise that resolves when all processes have been signaled\n */\nexport async function treeKill(pid: number, signal: NodeJS.Signals = \"SIGTERM\"): Promise<void> {\n if (Number.isNaN(pid)) {\n throw new Error(\"pid must be a number\");\n }\n\n const platform = process.platform;\n\n // Unix-like systems (darwin, linux, etc.)\n const tree: ProcessTree = { [pid]: [] };\n const pidsToProcess = new Set<number>([pid]);\n\n // Build the process tree\n await buildProcessTree(pid, tree, pidsToProcess, platform);\n\n // Kill all processes in the tree (children first, then parents)\n killAll(tree, signal);\n}\n\n/**\n * Build a tree of all child processes recursively\n */\nasync function buildProcessTree(\n parentPid: number,\n tree: ProcessTree,\n pidsToProcess: Set<number>,\n platform: string,\n): Promise<void> {\n return new Promise((resolve) => {\n const ps =\n platform === \"darwin\"\n ? spawn(\"pgrep\", [\"-P\", String(parentPid)])\n : spawn(\"ps\", [\"-o\", \"pid\", \"--no-headers\", \"--ppid\", String(parentPid)]);\n\n let allData = \"\";\n\n ps.stdout.on(\"data\", (data: Buffer) => {\n allData += data.toString(\"ascii\");\n });\n\n ps.on(\"close\", async (code) => {\n pidsToProcess.delete(parentPid);\n\n if (code !== 0) {\n // No child processes found for this parent\n if (pidsToProcess.size === 0) {\n resolve();\n }\n return;\n }\n\n const childPids = allData.match(/\\d+/g);\n if (!childPids) {\n if (pidsToProcess.size === 0) {\n resolve();\n }\n return;\n }\n\n // Process all child PIDs concurrently\n const promises: Promise<void>[] = [];\n\n for (const pidStr of childPids) {\n const childPid = parseInt(pidStr, 10);\n tree[parentPid].push(childPid);\n tree[childPid] = [];\n pidsToProcess.add(childPid);\n promises.push(buildProcessTree(childPid, tree, pidsToProcess, platform));\n }\n\n await Promise.all(promises);\n\n if (pidsToProcess.size === 0) {\n resolve();\n }\n });\n\n ps.on(\"error\", () => {\n pidsToProcess.delete(parentPid);\n if (pidsToProcess.size === 0) {\n resolve();\n }\n });\n });\n}\n\n/**\n * Kill all processes in the tree\n * Kills children before parents to ensure clean shutdown\n */\nfunction killAll(tree: ProcessTree, signal: NodeJS.Signals): void {\n const killed = new Set<number>();\n\n // Get all PIDs and sort by depth (deepest first)\n const allPids = Object.keys(tree).map(Number);\n\n // Kill children first, then parents\n for (const pid of allPids) {\n // Kill all children of this pid\n for (const childPid of tree[pid]) {\n if (!killed.has(childPid)) {\n killPid(childPid, signal);\n killed.add(childPid);\n }\n }\n }\n\n // Kill all parent pids\n for (const pid of allPids) {\n if (!killed.has(pid)) {\n killPid(pid, signal);\n killed.add(pid);\n }\n }\n}\n\n/**\n * Kill a single process, ignoring ESRCH errors (process already dead)\n */\nfunction killPid(pid: number, signal: NodeJS.Signals): void {\n try {\n process.kill(pid, signal);\n } catch (err) {\n // ESRCH = No such process (already dead) - ignore this\n if ((err as NodeJS.ErrnoException).code !== \"ESRCH\") {\n throw err;\n }\n }\n}\n","import { spawn, type ChildProcess } from \"node:child_process\";\nimport readline from \"node:readline\";\nimport { PassThrough } from \"node:stream\";\nimport * as v from \"valibot\";\nimport type { Logger } from \"./logger.ts\";\nimport { treeKill } from \"./tree-kill.ts\";\n\nexport const ProcessDefinitionSchema = v.object({\n command: v.string(),\n args: v.optional(v.array(v.string())),\n cwd: v.optional(v.string()),\n env: v.optional(v.record(v.string(), v.string())),\n});\n\nexport type ProcessDefinition = v.InferOutput<typeof ProcessDefinitionSchema>;\n\nexport const ProcessStateSchema = v.picklist([\n \"idle\",\n \"starting\",\n \"running\",\n \"stopping\",\n \"stopped\",\n \"error\",\n]);\n\nexport type ProcessState = v.InferOutput<typeof ProcessStateSchema>;\n\n/**\n * Kill a process and all its descendants (children, grandchildren, etc.)\n * Falls back to simple child.kill() if tree kill fails.\n */\nasync function killProcessTree(\n child: ChildProcess,\n signal: NodeJS.Signals,\n): Promise<boolean> {\n const pid = child.pid;\n if (pid === undefined) {\n return false;\n }\n\n try {\n await treeKill(pid, signal);\n return true;\n } catch {\n // Fallback to simple kill if tree kill fails\n try {\n return child.kill(signal);\n } catch {\n return false;\n }\n }\n}\n\nexport class LazyProcess {\n readonly name: string;\n private definition: ProcessDefinition;\n private logger: Logger;\n private childProcess: ChildProcess | null = null;\n private _state: ProcessState = \"idle\";\n private processExit = Promise.withResolvers<void>();\n public exitCode: number | null = null;\n\n constructor(name: string, definition: ProcessDefinition, logger: Logger) {\n this.name = name;\n this.definition = definition;\n this.logger = logger;\n }\n\n get state(): ProcessState {\n return this._state;\n }\n\n start(): void {\n if (this._state === \"running\" || this._state === \"starting\") {\n throw new Error(`Process \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`Process \"${this.name}\" is currently stopping`);\n }\n\n this._state = \"starting\";\n this.logger.info(`Starting process: ${this.definition.command}`);\n\n try {\n const env = this.definition.env ? { ...process.env, ...this.definition.env } : process.env;\n\n this.childProcess = spawn(this.definition.command, this.definition.args ?? [], {\n cwd: this.definition.cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n this._state = \"running\";\n\n // Combine stdout and stderr into a single stream for unified logging\n const combined = new PassThrough();\n let streamCount = 0;\n\n if (this.childProcess.stdout) {\n streamCount++;\n this.childProcess.stdout.pipe(combined, { end: false });\n this.childProcess.stdout.on(\"end\", () => {\n if (--streamCount === 0) combined.end();\n });\n }\n\n if (this.childProcess.stderr) {\n streamCount++;\n this.childProcess.stderr.pipe(combined, { end: false });\n this.childProcess.stderr.on(\"end\", () => {\n if (--streamCount === 0) combined.end();\n });\n }\n\n // Use readline to handle line-by-line output properly\n const rl = readline.createInterface({ input: combined });\n rl.on(\"line\", (line) => {\n this.logger.info(line);\n });\n\n // Handle process exit\n this.childProcess.on(\"exit\", (code, signal) => {\n this.exitCode = code;\n\n if (this._state === \"running\") {\n if (code === 0) {\n this._state = \"stopped\";\n this.logger.info(`Process exited with code ${code}`);\n } else if (signal) {\n this._state = \"stopped\";\n this.logger.info(`Process killed with signal ${signal}`);\n } else {\n this._state = \"error\";\n this.logger.error(`Process exited with code ${code}`);\n }\n }\n\n this.processExit.resolve();\n });\n\n // Handle spawn errors\n this.childProcess.on(\"error\", (err) => {\n if (this._state !== \"stopping\" && this._state !== \"stopped\") {\n this._state = \"error\";\n this.logger.error(`Process error:`, err);\n }\n this.processExit.resolve();\n });\n } catch (err) {\n this._state = \"error\";\n this.logger.error(`Failed to start process:`, err);\n throw err;\n }\n }\n\n async stop(timeout?: number): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\" || this._state === \"error\") {\n return;\n }\n\n if (this._state === \"stopping\") {\n // Already stopping, wait for completion\n await this.processExit.promise;\n return;\n }\n\n if (!this.childProcess) {\n this._state = \"stopped\";\n return;\n }\n\n this._state = \"stopping\";\n this.logger.info(`Stopping process with SIGTERM`);\n\n // Send SIGTERM for graceful shutdown (to entire process tree)\n await killProcessTree(this.childProcess, \"SIGTERM\");\n\n const timeoutMs = timeout ?? 5000;\n\n // Wait for process to exit or timeout\n const timeoutPromise = new Promise<\"timeout\">((resolve) =>\n setTimeout(() => resolve(\"timeout\"), timeoutMs),\n );\n\n const result = await Promise.race([\n this.processExit.promise.then(() => \"exited\" as const),\n timeoutPromise,\n ]);\n\n if (result === \"timeout\" && this.childProcess) {\n this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL`);\n await killProcessTree(this.childProcess, \"SIGKILL\");\n\n // Give SIGKILL a short timeout\n const killTimeout = new Promise<void>((resolve) => setTimeout(resolve, 1000));\n await Promise.race([this.processExit.promise, killTimeout]);\n }\n\n this._state = \"stopped\";\n this.cleanup();\n this.logger.info(`Process stopped`);\n }\n\n async reset(): Promise<void> {\n if (this.childProcess) {\n // Kill the entire process tree if running\n await killProcessTree(this.childProcess, \"SIGKILL\");\n await this.processExit.promise;\n this.cleanup();\n }\n\n this._state = \"idle\";\n // Create a fresh promise for the next process lifecycle\n this.processExit = Promise.withResolvers<void>();\n this.logger.info(`Process reset to idle`);\n }\n\n updateDefinition(definition: ProcessDefinition): void {\n this.definition = definition;\n }\n\n async waitForExit(): Promise<ProcessState> {\n if (!this.childProcess) {\n return this._state;\n }\n\n await this.processExit.promise;\n return this._state;\n }\n\n private cleanup(): void {\n if (this.childProcess) {\n // Remove all listeners to prevent memory leaks\n this.childProcess.stdout?.removeAllListeners();\n this.childProcess.stderr?.removeAllListeners();\n this.childProcess.removeAllListeners();\n this.childProcess = null;\n }\n\n this.exitCode = null;\n }\n}\n","import * as v from \"valibot\";\nimport { LazyProcess, type ProcessDefinition, type ProcessState } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\n// Restart policies\nexport const RestartPolicySchema = v.picklist([\n \"always\",\n \"on-failure\",\n \"never\",\n \"unless-stopped\",\n \"on-success\",\n]);\n\nexport type RestartPolicy = v.InferOutput<typeof RestartPolicySchema>;\n\n// Backoff strategy schema\nexport const BackoffStrategySchema = v.union([\n v.object({\n type: v.literal(\"fixed\"),\n delayMs: v.number(),\n }),\n v.object({\n type: v.literal(\"exponential\"),\n initialDelayMs: v.number(),\n maxDelayMs: v.number(),\n multiplier: v.optional(v.number()),\n }),\n]);\n\nexport type BackoffStrategy = v.InferOutput<typeof BackoffStrategySchema>;\n\n// Crash loop detection config schema\nexport const CrashLoopConfigSchema = v.object({\n maxRestarts: v.number(),\n windowMs: v.number(),\n backoffMs: v.number(),\n});\n\nexport type CrashLoopConfig = v.InferOutput<typeof CrashLoopConfigSchema>;\n\n// Restarting process options schema\nexport const RestartingProcessOptionsSchema = v.object({\n restartPolicy: RestartPolicySchema,\n backoff: v.optional(BackoffStrategySchema),\n crashLoop: v.optional(CrashLoopConfigSchema),\n minUptimeMs: v.optional(v.number()),\n maxTotalRestarts: v.optional(v.number()),\n});\n\nexport type RestartingProcessOptions = v.InferOutput<typeof RestartingProcessOptionsSchema>;\n\n// State\nexport const RestartingProcessStateSchema = v.picklist([\n \"idle\",\n \"running\",\n \"restarting\",\n \"stopping\",\n \"stopped\",\n \"crash-loop-backoff\",\n \"max-restarts-reached\",\n]);\n\nexport type RestartingProcessState = v.InferOutput<typeof RestartingProcessStateSchema>;\n\nconst DEFAULT_BACKOFF: BackoffStrategy = { type: \"fixed\", delayMs: 1000 };\nconst DEFAULT_CRASH_LOOP: CrashLoopConfig = { maxRestarts: 5, windowMs: 60000, backoffMs: 60000 };\n\nexport class RestartingProcess {\n readonly name: string;\n private lazyProcess: LazyProcess;\n private definition: ProcessDefinition;\n private options: Required<Omit<RestartingProcessOptions, \"maxTotalRestarts\">> & {\n maxTotalRestarts?: number;\n };\n private logger: Logger;\n\n // State tracking\n private _state: RestartingProcessState = \"idle\";\n private _restartCount: number = 0;\n private restartTimestamps: number[] = []; // For crash loop detection\n private consecutiveFailures: number = 0; // For exponential backoff\n private lastStartTime: number | null = null;\n private stopRequested: boolean = false;\n private pendingDelayTimeout: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n name: string,\n definition: ProcessDefinition,\n options: RestartingProcessOptions,\n logger: Logger,\n ) {\n this.name = name;\n this.definition = definition;\n this.logger = logger;\n this.options = {\n restartPolicy: options.restartPolicy,\n backoff: options.backoff ?? DEFAULT_BACKOFF,\n crashLoop: options.crashLoop ?? DEFAULT_CRASH_LOOP,\n minUptimeMs: options.minUptimeMs ?? 0,\n maxTotalRestarts: options.maxTotalRestarts,\n };\n this.lazyProcess = new LazyProcess(name, definition, logger);\n }\n\n get state(): RestartingProcessState {\n return this._state;\n }\n\n get restarts(): number {\n return this._restartCount;\n }\n\n start(): void {\n if (this._state === \"running\" || this._state === \"restarting\") {\n throw new Error(`Process \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`Process \"${this.name}\" is currently stopping`);\n }\n\n // Fresh start from terminal states - reset counters\n if (\n this._state === \"stopped\" ||\n this._state === \"idle\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this.resetCounters();\n }\n\n this.stopRequested = false;\n this.startProcess();\n }\n\n async stop(timeout?: number): Promise<void> {\n this.stopRequested = true;\n\n // Clear any pending delays\n if (this.pendingDelayTimeout) {\n clearTimeout(this.pendingDelayTimeout);\n this.pendingDelayTimeout = null;\n }\n\n if (\n this._state === \"idle\" ||\n this._state === \"stopped\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this._state = \"stopped\";\n return;\n }\n\n this._state = \"stopping\";\n await this.lazyProcess.stop(timeout);\n this._state = \"stopped\";\n this.logger.info(`RestartingProcess stopped`);\n }\n\n async restart(force: boolean = false): Promise<void> {\n // Fresh start from terminal states - reset counters and no delay\n if (\n this._state === \"stopped\" ||\n this._state === \"idle\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this.resetCounters();\n this.stopRequested = false;\n this.startProcess();\n return;\n }\n\n // Stop the current process first\n await this.stop();\n\n this.stopRequested = false;\n\n if (force) {\n // Force restart - no delay\n this.startProcess();\n } else {\n // Follow normal delay strategy\n const delay = this.calculateDelay();\n if (delay > 0) {\n this._state = \"restarting\";\n this.logger.info(`Restarting in ${delay}ms`);\n await this.delay(delay);\n if (this.stopRequested) return;\n }\n this.startProcess();\n }\n }\n\n /**\n * Update process definition and optionally restart with new config\n */\n async reload(\n newDefinition: ProcessDefinition,\n restartImmediately: boolean = true,\n ): Promise<void> {\n this.logger.info(`Reloading process with new definition`);\n this.definition = newDefinition;\n this.lazyProcess.updateDefinition(newDefinition);\n\n if (restartImmediately) {\n // Restart with force=true to apply changes immediately\n await this.restart(true);\n }\n }\n\n /**\n * Update restart options\n */\n updateOptions(newOptions: Partial<RestartingProcessOptions>): void {\n this.logger.info(`Updating restart options`);\n this.options = {\n ...this.options,\n restartPolicy: newOptions.restartPolicy ?? this.options.restartPolicy,\n backoff: newOptions.backoff ?? this.options.backoff,\n crashLoop: newOptions.crashLoop ?? this.options.crashLoop,\n minUptimeMs: newOptions.minUptimeMs ?? this.options.minUptimeMs,\n maxTotalRestarts: newOptions.maxTotalRestarts ?? this.options.maxTotalRestarts,\n };\n }\n\n private resetCounters(): void {\n this._restartCount = 0;\n this.consecutiveFailures = 0;\n this.restartTimestamps = [];\n }\n\n private startProcess(): void {\n this.lastStartTime = Date.now();\n this._state = \"running\";\n\n this.lazyProcess\n .reset()\n .then(() => {\n if (this.stopRequested) return;\n this.lazyProcess.start();\n return this.lazyProcess.waitForExit();\n })\n .then((exitState) => {\n if (!exitState) return;\n if (this.stopRequested && exitState === \"error\") {\n this._state = \"stopped\";\n return;\n }\n if (exitState === \"stopped\" || exitState === \"error\") {\n this.handleProcessExit(exitState);\n }\n })\n .catch((err) => {\n if (this.stopRequested) return;\n this._state = \"stopped\";\n this.logger.error(`Failed to start process:`, err);\n });\n }\n\n private handleProcessExit(exitState: ProcessState): void {\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n const uptime = this.lastStartTime ? Date.now() - this.lastStartTime : 0;\n const wasHealthy = uptime >= this.options.minUptimeMs;\n const exitedWithError = exitState === \"error\";\n\n // Reset consecutive failures if the process ran long enough\n if (wasHealthy) {\n this.consecutiveFailures = 0;\n } else {\n this.consecutiveFailures++;\n }\n\n // Check if policy allows restart\n if (!this.shouldRestart(exitedWithError)) {\n this._state = \"stopped\";\n this.logger.info(\n `Process exited, policy \"${this.options.restartPolicy}\" does not allow restart`,\n );\n return;\n }\n\n // Check max total restarts\n if (\n this.options.maxTotalRestarts !== undefined &&\n this._restartCount >= this.options.maxTotalRestarts\n ) {\n this._state = \"max-restarts-reached\";\n this.logger.warn(`Max total restarts (${this.options.maxTotalRestarts}) reached`);\n return;\n }\n\n // Record restart timestamp for crash loop detection\n const now = Date.now();\n this.restartTimestamps.push(now);\n\n // Check for crash loop\n if (this.isInCrashLoop()) {\n this._state = \"crash-loop-backoff\";\n this.logger.warn(\n `Crash loop detected (${this.options.crashLoop.maxRestarts} restarts in ${this.options.crashLoop.windowMs}ms), backing off for ${this.options.crashLoop.backoffMs}ms`,\n );\n this.scheduleCrashLoopRecovery();\n return;\n }\n\n // Schedule restart with delay\n this._restartCount++;\n this.scheduleRestart();\n }\n\n private shouldRestart(exitedWithError: boolean): boolean {\n switch (this.options.restartPolicy) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"on-failure\":\n return exitedWithError;\n case \"on-success\":\n return !exitedWithError;\n case \"unless-stopped\":\n return !this.stopRequested;\n default:\n return false;\n }\n }\n\n private isInCrashLoop(): boolean {\n const { maxRestarts, windowMs } = this.options.crashLoop;\n const now = Date.now();\n const cutoff = now - windowMs;\n\n // Clean up old timestamps\n this.restartTimestamps = this.restartTimestamps.filter((ts) => ts > cutoff);\n\n return this.restartTimestamps.length >= maxRestarts;\n }\n\n private calculateDelay(): number {\n const { backoff } = this.options;\n\n if (backoff.type === \"fixed\") {\n return backoff.delayMs;\n }\n\n // Exponential backoff\n const multiplier = backoff.multiplier ?? 2;\n const delay = backoff.initialDelayMs * Math.pow(multiplier, this.consecutiveFailures);\n return Math.min(delay, backoff.maxDelayMs);\n }\n\n private scheduleRestart(): void {\n this._state = \"restarting\";\n const delay = this.calculateDelay();\n\n this.logger.info(`Restarting in ${delay}ms (restart #${this._restartCount})`);\n\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n this.startProcess();\n }, delay);\n }\n\n private scheduleCrashLoopRecovery(): void {\n const { backoffMs } = this.options.crashLoop;\n\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n // Reset crash loop timestamps after backoff\n this.restartTimestamps = [];\n this._restartCount++;\n this.logger.info(`Crash loop backoff complete, restarting (restart #${this._restartCount})`);\n this.startProcess();\n }, backoffMs);\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n resolve();\n }, ms);\n });\n }\n}\n","import { Cron } from \"croner\";\nimport * as v from \"valibot\";\nimport { LazyProcess, type ProcessDefinition } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\n// Retry configuration schema\nexport const RetryConfigSchema = v.object({\n maxRetries: v.number(),\n delayMs: v.optional(v.number()),\n});\n\nexport type RetryConfig = v.InferOutput<typeof RetryConfigSchema>;\n\n// Cron process options schema\nexport const CronProcessOptionsSchema = v.object({\n schedule: v.string(),\n retry: v.optional(RetryConfigSchema),\n runOnStart: v.optional(v.boolean()),\n});\n\nexport type CronProcessOptions = v.InferOutput<typeof CronProcessOptionsSchema>;\n\n// State\nexport const CronProcessStateSchema = v.picklist([\n \"idle\",\n \"scheduled\",\n \"running\",\n \"retrying\",\n \"queued\",\n \"stopping\",\n \"stopped\",\n]);\n\nexport type CronProcessState = v.InferOutput<typeof CronProcessStateSchema>;\n\nconst DEFAULT_RETRY_DELAY = 1000;\n\nexport class CronProcess {\n readonly name: string;\n private lazyProcess: LazyProcess;\n private options: CronProcessOptions;\n private logger: Logger;\n private cronJob: Cron | null = null;\n\n // State tracking\n private _state: CronProcessState = \"idle\";\n private _runCount: number = 0;\n private _failCount: number = 0;\n private currentRetryAttempt: number = 0;\n private queuedRun: boolean = false;\n private stopRequested: boolean = false;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n name: string,\n definition: ProcessDefinition,\n options: CronProcessOptions,\n logger: Logger,\n ) {\n this.name = name;\n this.options = options;\n this.logger = logger;\n this.lazyProcess = new LazyProcess(name, definition, logger);\n }\n\n get state(): CronProcessState {\n return this._state;\n }\n\n get runCount(): number {\n return this._runCount;\n }\n\n get failCount(): number {\n return this._failCount;\n }\n\n get nextRun(): Date | null {\n if (!this.cronJob) return null;\n const next = this.cronJob.nextRun();\n return next ?? null;\n }\n\n start(): void {\n if (this._state === \"scheduled\" || this._state === \"running\" || this._state === \"queued\") {\n throw new Error(`CronProcess \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`CronProcess \"${this.name}\" is currently stopping`);\n }\n\n this.stopRequested = false;\n this.logger.info(`Starting cron schedule: ${this.options.schedule}`);\n\n // Create cron job with UTC timezone\n this.cronJob = new Cron(this.options.schedule, { timezone: \"UTC\" }, () => {\n this.onCronTick();\n });\n\n this._state = \"scheduled\";\n\n // Run immediately if configured\n if (this.options.runOnStart) {\n this.executeJob();\n }\n }\n\n async stop(timeout?: number): Promise<void> {\n this.stopRequested = true;\n\n // Stop the cron job\n if (this.cronJob) {\n this.cronJob.stop();\n this.cronJob = null;\n }\n\n // Clear any pending retry timeout\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n\n if (this._state === \"idle\" || this._state === \"stopped\") {\n this._state = \"stopped\";\n return;\n }\n\n // If running, stop the current job\n if (this._state === \"running\" || this._state === \"retrying\" || this._state === \"queued\") {\n this._state = \"stopping\";\n await this.lazyProcess.stop(timeout);\n }\n\n this._state = \"stopped\";\n this.queuedRun = false;\n this.logger.info(`CronProcess stopped`);\n }\n\n async trigger(): Promise<void> {\n if (this.stopRequested) {\n throw new Error(`CronProcess \"${this.name}\" is stopped`);\n }\n\n // If already queued, just return (already have a run pending)\n if (this._state === \"queued\") {\n return;\n }\n\n // If already running, queue this trigger\n if (this._state === \"running\" || this._state === \"retrying\") {\n this.queuedRun = true;\n this._state = \"queued\";\n this.logger.info(`Run queued (current job still running)`);\n return;\n }\n\n await this.executeJob();\n }\n\n private onCronTick(): void {\n if (this.stopRequested) return;\n\n // If already running, queue the next run\n if (this._state === \"running\" || this._state === \"retrying\" || this._state === \"queued\") {\n this.queuedRun = true;\n if (this._state !== \"queued\") {\n this._state = \"queued\";\n }\n this.logger.info(`Cron tick: run queued (current job still running)`);\n return;\n }\n\n this.executeJob();\n }\n\n private async executeJob(): Promise<void> {\n if (this.stopRequested) return;\n\n this._state = \"running\";\n this.currentRetryAttempt = 0;\n this.logger.info(`Executing job`);\n\n await this.runJobWithRetry();\n }\n\n private async runJobWithRetry(): Promise<void> {\n if (this.stopRequested) return;\n\n // Reset and start the process\n await this.lazyProcess.reset();\n this.lazyProcess.start();\n\n const exitState = await this.lazyProcess.waitForExit();\n if (this.stopRequested && exitState === \"error\") {\n this._state = \"stopped\";\n return;\n }\n this.handleJobComplete(exitState === \"error\");\n }\n\n private handleJobComplete(failed: boolean): void {\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n if (failed) {\n const maxRetries = this.options.retry?.maxRetries ?? 0;\n\n if (this.currentRetryAttempt < maxRetries) {\n // Retry\n this.currentRetryAttempt++;\n this._state = \"retrying\";\n const delayMs = this.options.retry?.delayMs ?? DEFAULT_RETRY_DELAY;\n\n this.logger.warn(\n `Job failed, retrying in ${delayMs}ms (attempt ${this.currentRetryAttempt}/${maxRetries})`,\n );\n\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n this.runJobWithRetry();\n }, delayMs);\n return;\n }\n\n // All retries exhausted\n this._failCount++;\n this.logger.error(`Job failed after ${this.currentRetryAttempt} retries`);\n } else {\n this._runCount++;\n this.logger.info(`Job completed successfully`);\n }\n\n // Check for queued run\n if (this.queuedRun) {\n this.queuedRun = false;\n this.logger.info(`Starting queued run`);\n this.executeJob();\n return;\n }\n\n // Back to scheduled state\n if (this.cronJob) {\n this._state = \"scheduled\";\n } else {\n this._state = \"stopped\";\n }\n }\n}\n","import * as v from \"valibot\";\nimport { LazyProcess, ProcessDefinitionSchema } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\n// Per-task state\nexport const TaskStateSchema = v.picklist([\"pending\", \"running\", \"completed\", \"failed\", \"skipped\"]);\n\nexport type TaskState = v.InferOutput<typeof TaskStateSchema>;\n\n// Schema for named process definition\nexport const NamedProcessDefinitionSchema = v.object({\n name: v.string(),\n process: ProcessDefinitionSchema,\n});\n\nexport type NamedProcessDefinition = v.InferOutput<typeof NamedProcessDefinitionSchema>;\n\n// A task entry (single or parallel processes) with its state\nexport interface TaskEntry {\n id: string; // Unique task ID\n processes: NamedProcessDefinition[]; // Array (length 1 = sequential, >1 = parallel)\n state: TaskState;\n}\n\n// Simple TaskList state (just running or not)\nexport type TaskListState = \"idle\" | \"running\" | \"stopped\";\n\nexport class TaskList {\n readonly name: string;\n private _tasks: TaskEntry[] = [];\n private _state: TaskListState = \"idle\";\n private logger: Logger;\n private logFileResolver?: (processName: string) => string | undefined;\n private taskIdCounter: number = 0;\n private runningProcesses: LazyProcess[] = [];\n private stopRequested: boolean = false;\n private runLoopPromise: Promise<void> | null = null;\n\n constructor(\n name: string,\n logger: Logger,\n initialTasks?: (NamedProcessDefinition | NamedProcessDefinition[])[],\n logFileResolver?: (processName: string) => string | undefined,\n ) {\n this.name = name;\n this.logger = logger;\n this.logFileResolver = logFileResolver;\n\n // Add initial tasks if provided\n if (initialTasks) {\n for (const task of initialTasks) {\n this.addTask(task);\n }\n }\n }\n\n get state(): TaskListState {\n return this._state;\n }\n\n get tasks(): ReadonlyArray<TaskEntry> {\n return this._tasks;\n }\n\n removeTaskByTarget(target: string | number): TaskEntry {\n const index =\n typeof target === \"number\" ? target : this._tasks.findIndex((t) => t.id === target);\n if (index < 0 || index >= this._tasks.length) {\n throw new Error(`Task not found: ${target}`);\n }\n\n const task = this._tasks[index];\n if (task.state === \"running\") {\n throw new Error(`Cannot remove running task: ${task.id}`);\n }\n\n this._tasks.splice(index, 1);\n this.logger.info(`Task \"${task.id}\" removed`);\n return task;\n }\n\n /**\n * Add a single process or parallel processes as a new task\n * @returns The unique task ID\n */\n addTask(task: NamedProcessDefinition | NamedProcessDefinition[]): string {\n const id = `task-${++this.taskIdCounter}`;\n const processes = Array.isArray(task) ? task : [task];\n\n const entry: TaskEntry = {\n id,\n processes,\n state: \"pending\",\n };\n\n this._tasks.push(entry);\n this.logger.info(`Task \"${id}\" added with ${processes.length} process(es)`);\n\n return id;\n }\n\n /**\n * Begin executing pending tasks\n */\n start(): void {\n if (this._state === \"running\") {\n throw new Error(`TaskList \"${this.name}\" is already running`);\n }\n\n this.stopRequested = false;\n this._state = \"running\";\n this.logger.info(`TaskList started`);\n\n // Start the run loop (non-blocking)\n this.runLoopPromise = this.runLoop();\n }\n\n /**\n * Wait until the TaskList becomes idle (all pending tasks completed)\n */\n async waitUntilIdle(): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\") {\n return;\n }\n\n // Wait for the run loop to complete\n if (this.runLoopPromise) {\n await this.runLoopPromise;\n }\n }\n\n /**\n * Stop execution and mark remaining tasks as skipped\n */\n async stop(timeout?: number): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\") {\n this._state = \"stopped\";\n return;\n }\n\n this.stopRequested = true;\n this.logger.info(`Stopping TaskList...`);\n\n // Stop all currently running processes\n const stopPromises = this.runningProcesses.map((p) => p.stop(timeout));\n await Promise.all(stopPromises);\n this.runningProcesses = [];\n\n // Mark all pending tasks as skipped\n for (const task of this._tasks) {\n if (task.state === \"pending\") {\n task.state = \"skipped\";\n }\n }\n\n // Wait for run loop to finish\n if (this.runLoopPromise) {\n await this.runLoopPromise;\n this.runLoopPromise = null;\n }\n\n this._state = \"stopped\";\n this.logger.info(`TaskList stopped`);\n }\n\n private async runLoop(): Promise<void> {\n while (this._state === \"running\" && !this.stopRequested) {\n // Find the next pending task\n const nextTask = this._tasks.find((t) => t.state === \"pending\");\n\n if (!nextTask) {\n // No more pending tasks, go back to idle\n this._state = \"idle\";\n this.logger.info(`All tasks completed, TaskList is idle`);\n break;\n }\n\n await this.executeTask(nextTask);\n }\n }\n\n private async executeTask(task: TaskEntry): Promise<void> {\n if (this.stopRequested) {\n task.state = \"skipped\";\n return;\n }\n\n task.state = \"running\";\n const taskNames = task.processes.map((p) => p.name).join(\", \");\n this.logger.info(`Executing task \"${task.id}\": [${taskNames}]`);\n\n // Create LazyProcess instances for each process in the task\n const lazyProcesses: LazyProcess[] = task.processes.map((p) => {\n const logFile = this.logFileResolver?.(p.name);\n const childLogger = logFile\n ? this.logger.child(p.name, { logFile })\n : this.logger.child(p.name);\n return new LazyProcess(p.name, p.process, childLogger);\n });\n\n this.runningProcesses = lazyProcesses;\n\n try {\n // Start all processes (parallel if multiple)\n for (const lp of lazyProcesses) {\n lp.start();\n }\n\n // Wait for all processes to complete\n const results = await Promise.all(lazyProcesses.map((lp) => this.waitForProcess(lp)));\n\n // Check if any failed\n const anyFailed = results.some((r) => r === \"error\");\n\n if (this.stopRequested) {\n task.state = \"skipped\";\n } else if (anyFailed) {\n task.state = \"failed\";\n this.logger.warn(`Task \"${task.id}\" failed`);\n } else {\n task.state = \"completed\";\n this.logger.info(`Task \"${task.id}\" completed`);\n }\n } catch (err) {\n task.state = \"failed\";\n this.logger.error(`Task \"${task.id}\" error:`, err);\n } finally {\n this.runningProcesses = [];\n }\n }\n\n private async waitForProcess(lp: LazyProcess): Promise<\"stopped\" | \"error\"> {\n const state = await lp.waitForExit();\n return state === \"error\" ? \"error\" : \"stopped\";\n }\n}\n","import { parse } from \"dotenv\";\nimport { existsSync, globSync, watch, readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve, join, basename } from \"node:path\";\n\nexport type EnvChangeCallback = (changedKeys: string[]) => void;\n\nexport interface EnvManagerConfig {\n /**\n * Directory to search for .env files\n * Defaults to process.cwd()\n */\n cwd?: string;\n\n /**\n * Explicit env file paths to load\n * Key is the identifier (e.g., \"global\", \"app1\")\n * Value is the file path relative to cwd or absolute\n */\n files?: Record<string, string>;\n\n /**\n * Enable file watching for env files\n * Defaults to false\n */\n watch?: boolean;\n}\n\nexport class EnvManager {\n private env: Map<string, Record<string, string>> = new Map();\n private cwd: string;\n private watchEnabled: boolean;\n private watchers: Map<string, ReturnType<typeof watch>> = new Map();\n private fileToKeys: Map<string, Set<string>> = new Map();\n private changeCallbacks: Set<EnvChangeCallback> = new Set();\n private reloadDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();\n\n constructor(config: EnvManagerConfig = {}) {\n this.cwd = config.cwd ?? process.cwd();\n this.watchEnabled = config.watch ?? false;\n\n // Load .env and .env.* files from cwd\n this.loadEnvFilesFromCwd();\n\n // Load explicitly specified files\n if (config.files) {\n for (const [key, filePath] of Object.entries(config.files)) {\n this.loadEnvFile(key, filePath);\n }\n }\n }\n\n registerFile(key: string, filePath: string): void {\n this.loadEnvFile(key, filePath);\n }\n\n getEnvForKey(key: string): Record<string, string> {\n return this.env.get(key) ?? {};\n }\n\n /**\n * Load .env and .env.* files from the cwd\n */\n private loadEnvFilesFromCwd(): void {\n // Load .env file as global\n const dotEnvPath = resolve(this.cwd, \".env\");\n if (existsSync(dotEnvPath)) {\n this.loadEnvFile(\"global\", dotEnvPath);\n }\n\n // Load .env.* files\n try {\n const pattern = join(this.cwd, \".env.*\");\n const envFiles = globSync(pattern);\n\n for (const filePath of envFiles) {\n // Extract the suffix after .env.\n const fileName = basename(filePath);\n const match = fileName.match(/^\\.env\\.(.+)$/);\n if (match) {\n const suffix = match[1];\n this.loadEnvFile(suffix, filePath);\n }\n }\n } catch (err) {\n console.warn(\"Failed to scan env files:\", err);\n }\n }\n\n /**\n * Load a single env file and store it in the map\n */\n private loadEnvFile(key: string, filePath: string): void {\n const absolutePath = resolve(this.cwd, filePath);\n\n if (!existsSync(absolutePath)) {\n return; // Silently skip non-existent files\n }\n\n try {\n const content = readFileSync(absolutePath, \"utf-8\");\n const parsed = parse(content);\n this.env.set(key, parsed);\n\n // Track which file maps to which key\n if (!this.fileToKeys.has(absolutePath)) {\n this.fileToKeys.set(absolutePath, new Set());\n }\n this.fileToKeys.get(absolutePath)!.add(key);\n\n // Start watching if enabled and not already watching\n if (this.watchEnabled && !this.watchers.has(absolutePath)) {\n this.watchFile(absolutePath);\n }\n } catch (err) {\n console.warn(`Failed to load env file: ${absolutePath}`, err);\n }\n }\n\n /**\n * Watch a file for changes\n */\n private watchFile(absolutePath: string): void {\n try {\n const watcher = watch(absolutePath, (eventType) => {\n if (eventType === \"change\") {\n this.handleFileChange(absolutePath);\n }\n });\n\n this.watchers.set(absolutePath, watcher);\n } catch (err) {\n console.warn(`Failed to watch env file: ${absolutePath}`, err);\n }\n }\n\n /**\n * Handle file change with debouncing\n */\n private handleFileChange(absolutePath: string): void {\n // Clear existing timer if any\n const existingTimer = this.reloadDebounceTimers.get(absolutePath);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n // Debounce reload by 100ms to avoid multiple rapid reloads\n const timer = setTimeout(() => {\n this.reloadFile(absolutePath);\n this.reloadDebounceTimers.delete(absolutePath);\n }, 100);\n\n this.reloadDebounceTimers.set(absolutePath, timer);\n }\n\n /**\n * Reload a file and notify callbacks\n */\n private reloadFile(absolutePath: string): void {\n const keys = this.fileToKeys.get(absolutePath);\n if (!keys) return;\n\n readFile(absolutePath, \"utf-8\")\n .then((content) => parse(content))\n .then((parsed) => {\n const changedKeys: string[] = [];\n for (const key of keys) {\n this.env.set(key, parsed);\n changedKeys.push(key);\n }\n\n // Notify all callbacks\n if (changedKeys.length > 0) {\n for (const callback of this.changeCallbacks) {\n callback(changedKeys);\n }\n }\n })\n .catch((err) => {\n console.warn(`Failed to reload env file: ${absolutePath}`, err);\n });\n }\n\n /**\n * Register a callback to be called when env files change\n * Returns a function to unregister the callback\n */\n onChange(callback: EnvChangeCallback): () => void {\n this.changeCallbacks.add(callback);\n return () => {\n this.changeCallbacks.delete(callback);\n };\n }\n\n /**\n * Stop watching all files and cleanup\n */\n dispose(): void {\n // Clear all timers\n for (const timer of this.reloadDebounceTimers.values()) {\n clearTimeout(timer);\n }\n this.reloadDebounceTimers.clear();\n\n // Close all watchers\n for (const watcher of this.watchers.values()) {\n watcher.close();\n }\n this.watchers.clear();\n\n // Clear callbacks\n this.changeCallbacks.clear();\n }\n\n /**\n * Get environment variables for a specific process\n * Merges global env with process-specific env\n * Process-specific env variables override global ones\n */\n getEnvVars(processKey?: string): Record<string, string> {\n const globalEnv = this.env.get(\"global\") ?? {};\n\n if (!processKey) {\n return { ...globalEnv };\n }\n\n const processEnv = this.env.get(processKey) ?? {};\n return { ...globalEnv, ...processEnv };\n }\n\n /**\n * Get all loaded env maps (for debugging/inspection)\n */\n getAllEnv(): ReadonlyMap<string, Record<string, string>> {\n return this.env;\n }\n}\n","import { appendFileSync } from \"node:fs\";\nimport { format } from \"node:util\";\n\nconst colors = {\n reset: \"\\x1b[0m\",\n gray: \"\\x1b[90m\",\n white: \"\\x1b[37m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n red: \"\\x1b[31m\",\n bold: \"\\x1b[1m\",\n} as const;\n\nconst levelColors = {\n debug: colors.gray,\n info: colors.green,\n warn: colors.yellow,\n error: colors.red,\n} as const;\n\nconst formatTime = (date: Date) =>\n Intl.DateTimeFormat(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n fractionalSecondDigits: 3,\n hourCycle: \"h23\",\n }).format(date);\n\nconst formatPrefixNoColor = (level: string, name: string, time: Date) => {\n const levelFormatted = level.toUpperCase().padStart(5);\n const timestamp = formatTime(time);\n return `[${timestamp}] ${levelFormatted} (${name})`;\n};\n\nconst formatPrefixWithColor = (level: string, name: string, time: Date) => {\n const levelFormatted = level.toUpperCase().padStart(5);\n const timestamp = formatTime(time);\n const levelTint = levelColors[level as keyof typeof levelColors] ?? \"\";\n return `${colors.gray}[${timestamp}]${colors.reset} ${levelTint}${levelFormatted}${colors.reset} (${name})`;\n};\n\ntype LoggerConfig = {\n name: string;\n stdout: boolean;\n logFile?: string;\n};\n\ntype LoggerInput = {\n name: string;\n stdout?: boolean;\n logFile?: string;\n};\n\nconst writeLogFile = (logFile: string | undefined, line: string) => {\n if (!logFile) return;\n appendFileSync(logFile, `${line}\\n`);\n};\n\nconst logLine = (config: LoggerConfig, level: \"debug\" | \"info\" | \"warn\" | \"error\", args: any[]) => {\n const message = args.length > 0 ? format(...args) : \"\";\n const time = new Date();\n const plainPrefix = formatPrefixNoColor(level, config.name, time);\n const plainLine = `${plainPrefix} ${message}`;\n\n writeLogFile(config.logFile, plainLine);\n\n if (!config.stdout) return;\n const coloredPrefix = formatPrefixWithColor(level, config.name, time);\n const coloredLine = `${coloredPrefix} ${message}`;\n\n switch (level) {\n case \"error\":\n console.error(coloredLine);\n break;\n case \"warn\":\n console.warn(coloredLine);\n break;\n case \"info\":\n console.info(coloredLine);\n break;\n default:\n console.debug(coloredLine);\n break;\n }\n};\n\nexport const logger = (input: LoggerInput) => {\n const config: LoggerConfig = {\n stdout: true,\n ...input,\n };\n\n return {\n info: (...args: any[]) => logLine(config, \"info\", args),\n error: (...args: any[]) => logLine(config, \"error\", args),\n warn: (...args: any[]) => logLine(config, \"warn\", args),\n debug: (...args: any[]) => logLine(config, \"debug\", args),\n child: (suffix: string, overrides: Partial<Omit<LoggerConfig, \"name\">> = {}) =>\n logger({\n ...config,\n ...overrides,\n name: `${config.name}:${suffix}`,\n }),\n };\n};\n\nexport type Logger = ReturnType<typeof logger>;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-list-CIdbB3wM.d.mts","names":[],"sources":["../src/logger.ts","../src/lazy-process.ts","../src/restarting-process.ts","../src/cron-process.ts","../src/task-list.ts"],"mappings":";;;KA0CK,YAAA;EACH,IAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,KAGG,WAAA;EACH,IAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,cAoCW,MAAA,GAAU,KAAA,EAAO,WAAA;;;;;0BAWJ,SAAA,GAAa,OAAA,CAAQ,IAAA,CAAK,YAAA,eAlDpC;AAAA;AAAA,KA2DJ,MAAA,GAAS,UAAA,QAAkB,MAAA;;;
|
|
1
|
+
{"version":3,"file":"task-list-CIdbB3wM.d.mts","names":[],"sources":["../src/logger.ts","../src/lazy-process.ts","../src/restarting-process.ts","../src/cron-process.ts","../src/task-list.ts"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/api/server.ts
CHANGED
|
@@ -59,12 +59,12 @@ const listProcesses = os.processes.list.handler(async ({ context }) => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
const addProcess = os.processes.add.handler(async ({ input, context }) => {
|
|
62
|
-
const proc = context.manager.addProcess(input.name, input.definition);
|
|
62
|
+
const proc = await context.manager.addProcess(input.name, input.definition);
|
|
63
63
|
return serializeProcess(proc);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
const startProcess = os.processes.start.handler(async ({ input, context }) => {
|
|
67
|
-
const proc = context.manager.startProcessByTarget(input.target);
|
|
67
|
+
const proc = await context.manager.startProcessByTarget(input.target);
|
|
68
68
|
return serializeProcess(proc);
|
|
69
69
|
});
|
|
70
70
|
|
package/src/index.ts
CHANGED
package/src/lazy-process.ts
CHANGED
|
@@ -3,6 +3,7 @@ import readline from "node:readline";
|
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
4
|
import * as v from "valibot";
|
|
5
5
|
import type { Logger } from "./logger.ts";
|
|
6
|
+
import { treeKill } from "./tree-kill.ts";
|
|
6
7
|
|
|
7
8
|
export const ProcessDefinitionSchema = v.object({
|
|
8
9
|
command: v.string(),
|
|
@@ -25,14 +26,28 @@ export const ProcessStateSchema = v.picklist([
|
|
|
25
26
|
export type ProcessState = v.InferOutput<typeof ProcessStateSchema>;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Kill a process
|
|
29
|
-
*
|
|
29
|
+
* Kill a process and all its descendants (children, grandchildren, etc.)
|
|
30
|
+
* Falls back to simple child.kill() if tree kill fails.
|
|
30
31
|
*/
|
|
31
|
-
function
|
|
32
|
+
async function killProcessTree(
|
|
33
|
+
child: ChildProcess,
|
|
34
|
+
signal: NodeJS.Signals,
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
const pid = child.pid;
|
|
37
|
+
if (pid === undefined) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
try {
|
|
33
|
-
|
|
42
|
+
await treeKill(pid, signal);
|
|
43
|
+
return true;
|
|
34
44
|
} catch {
|
|
35
|
-
|
|
45
|
+
// Fallback to simple kill if tree kill fails
|
|
46
|
+
try {
|
|
47
|
+
return child.kill(signal);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
|
|
@@ -158,8 +173,8 @@ export class LazyProcess {
|
|
|
158
173
|
this._state = "stopping";
|
|
159
174
|
this.logger.info(`Stopping process with SIGTERM`);
|
|
160
175
|
|
|
161
|
-
// Send SIGTERM for graceful shutdown (to entire process
|
|
162
|
-
|
|
176
|
+
// Send SIGTERM for graceful shutdown (to entire process tree)
|
|
177
|
+
await killProcessTree(this.childProcess, "SIGTERM");
|
|
163
178
|
|
|
164
179
|
const timeoutMs = timeout ?? 5000;
|
|
165
180
|
|
|
@@ -175,7 +190,7 @@ export class LazyProcess {
|
|
|
175
190
|
|
|
176
191
|
if (result === "timeout" && this.childProcess) {
|
|
177
192
|
this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL`);
|
|
178
|
-
|
|
193
|
+
await killProcessTree(this.childProcess, "SIGKILL");
|
|
179
194
|
|
|
180
195
|
// Give SIGKILL a short timeout
|
|
181
196
|
const killTimeout = new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
|
@@ -189,8 +204,8 @@ export class LazyProcess {
|
|
|
189
204
|
|
|
190
205
|
async reset(): Promise<void> {
|
|
191
206
|
if (this.childProcess) {
|
|
192
|
-
// Kill the process
|
|
193
|
-
|
|
207
|
+
// Kill the entire process tree if running
|
|
208
|
+
await killProcessTree(this.childProcess, "SIGKILL");
|
|
194
209
|
await this.processExit.promise;
|
|
195
210
|
this.cleanup();
|
|
196
211
|
}
|
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,76 @@
|
|
|
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
|
+
* Works on macOS, Linux, and Windows.
|
|
10
|
+
*/
|
|
11
|
+
export async function killProcessOnPort(port: number): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
|
|
15
|
+
if (platform === "darwin" || platform === "linux") {
|
|
16
|
+
// Use lsof to find the process ID listening on the port
|
|
17
|
+
const { stdout } = await execAsync(`lsof -ti tcp:${port}`);
|
|
18
|
+
const pids = stdout
|
|
19
|
+
.trim()
|
|
20
|
+
.split("\n")
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((p) => parseInt(p, 10))
|
|
23
|
+
.filter((p) => !isNaN(p));
|
|
24
|
+
|
|
25
|
+
if (pids.length === 0) {
|
|
26
|
+
return false; // No process found on port
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Tree kill all processes found on the port (kills children/grandchildren too)
|
|
30
|
+
for (const pid of pids) {
|
|
31
|
+
try {
|
|
32
|
+
await treeKill(pid, "SIGKILL");
|
|
33
|
+
} catch {
|
|
34
|
+
// Process may have already exited
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} else if (platform === "win32") {
|
|
40
|
+
// Windows: use netstat to find PIDs
|
|
41
|
+
const { stdout } = await execAsync(
|
|
42
|
+
`netstat -ano | findstr :${port} | findstr LISTENING`,
|
|
43
|
+
);
|
|
44
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
45
|
+
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pids = new Set<number>();
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const parts = line.trim().split(/\s+/);
|
|
53
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
54
|
+
if (!isNaN(pid) && pid !== 0) {
|
|
55
|
+
pids.add(pid);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// treeKill handles /T flag on Windows
|
|
60
|
+
for (const pid of pids) {
|
|
61
|
+
try {
|
|
62
|
+
await treeKill(pid, "SIGKILL");
|
|
63
|
+
} catch {
|
|
64
|
+
// Process may have already exited
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return pids.size > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
} catch {
|
|
73
|
+
// Command failed (e.g., no process on port)
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|