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.
@@ -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. Tries to kill the process group first (if available),
29
- * then falls back to killing just the process.
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 killProcess(child, signal) {
117
+ async function killProcessTree(child, signal) {
118
+ const pid = child.pid;
119
+ if (pid === void 0) return false;
32
120
  try {
33
- return child.kill(signal);
121
+ await treeKill(pid, signal);
122
+ return true;
34
123
  } catch {
35
- return false;
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
- killProcess(this.childProcess, "SIGTERM");
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
- killProcess(this.childProcess, "SIGKILL");
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
- killProcess(this.childProcess, "SIGKILL");
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-crc5neL8.mjs.map
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidnap",
3
- "version": "0.0.0-dev.0",
3
+ "version": "0.0.0-dev.1",
4
4
  "bin": {
5
5
  "pidnap": "./dist/cli.mjs"
6
6
  },
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
@@ -10,3 +10,4 @@ export * from "./task-list.ts";
10
10
  export * from "./lazy-process.ts";
11
11
  export * from "./env-manager.ts";
12
12
  export * from "./logger.ts";
13
+ export * from "./tree-kill.ts";
@@ -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. Tries to kill the process group first (if available),
29
- * then falls back to killing just the process.
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 killProcess(child: ChildProcess, signal: NodeJS.Signals): boolean {
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
- return child.kill(signal);
42
+ await treeKill(pid, signal);
43
+ return true;
34
44
  } catch {
35
- return false;
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 group)
162
- killProcess(this.childProcess, "SIGTERM");
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
- killProcess(this.childProcess, "SIGKILL");
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 group if running
193
- killProcess(this.childProcess, "SIGKILL");
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
- ): RestartingProcess {
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
+ }