pidnap 0.0.0-dev.3 → 0.0.0-dev.5
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 +157 -167
- package/dist/cli.mjs.map +1 -1
- package/dist/client.d.mts +0 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +2 -2
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +263 -71
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{logger-BF3KrCIK.mjs → logger-BU2RmetS.mjs} +231 -270
- package/dist/logger-BU2RmetS.mjs.map +1 -0
- package/package.json +4 -2
- package/src/api/client.ts +2 -2
- package/src/api/contract.ts +8 -14
- package/src/cli.ts +36 -36
- package/src/cron-process.ts +9 -18
- package/src/env-manager.ts +170 -163
- package/src/index.ts +0 -1
- package/src/lazy-process.ts +60 -80
- package/src/logger.ts +28 -36
- package/src/manager.ts +155 -212
- package/src/restarting-process.ts +16 -29
- package/src/task-list.ts +3 -5
- package/src/utils.ts +10 -0
- package/dist/logger-BF3KrCIK.mjs.map +0 -1
- package/dist/task-list-CIdbB3wM.d.mts +0 -230
- package/dist/task-list-CIdbB3wM.d.mts.map +0 -1
- package/src/port-utils.ts +0 -39
- package/src/tree-kill.ts +0 -131
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
2
|
+
import { _ as ProcessDefinition, a as TaskStateSchema, c as CronProcessState, h as RestartingProcessState, i as TaskList, m as RestartingProcessOptions, n as EnvManager, o as CronProcess, p as RestartingProcess, s as CronProcessOptions, t as logger } from "./logger-BU2RmetS.mjs";
|
|
3
3
|
import { createClient } from "./client.mjs";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createServer } from "node:http";
|
|
@@ -9,11 +9,8 @@ import { RPCHandler } from "@orpc/server/node";
|
|
|
9
9
|
import { implement, onError } from "@orpc/server";
|
|
10
10
|
import * as v from "valibot";
|
|
11
11
|
import { oc } from "@orpc/contract";
|
|
12
|
-
import { exec } from "node:child_process";
|
|
13
12
|
import { cwd } from "node:process";
|
|
14
13
|
import { mkdirSync } from "node:fs";
|
|
15
|
-
import { promisify } from "node:util";
|
|
16
|
-
import { tsImport } from "tsx/esm/api";
|
|
17
14
|
import Table from "cli-table3";
|
|
18
15
|
|
|
19
16
|
//#region src/api/contract.ts
|
|
@@ -34,12 +31,12 @@ const ManagerStatusSchema = v.object({
|
|
|
34
31
|
});
|
|
35
32
|
const RestartingProcessInfoSchema = v.object({
|
|
36
33
|
name: v.string(),
|
|
37
|
-
state:
|
|
34
|
+
state: RestartingProcessState,
|
|
38
35
|
restarts: v.number()
|
|
39
36
|
});
|
|
40
37
|
const CronProcessInfoSchema = v.object({
|
|
41
38
|
name: v.string(),
|
|
42
|
-
state:
|
|
39
|
+
state: CronProcessState,
|
|
43
40
|
runCount: v.number(),
|
|
44
41
|
failCount: v.number(),
|
|
45
42
|
nextRun: v.nullable(v.string())
|
|
@@ -50,12 +47,12 @@ const TaskEntryInfoSchema = v.object({
|
|
|
50
47
|
processNames: v.array(v.string())
|
|
51
48
|
});
|
|
52
49
|
const manager = { status: oc$1.output(ManagerStatusSchema) };
|
|
53
|
-
const processes
|
|
50
|
+
const processes = {
|
|
54
51
|
get: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
55
52
|
list: oc$1.output(v.array(RestartingProcessInfoSchema)),
|
|
56
53
|
add: oc$1.input(v.object({
|
|
57
54
|
name: v.string(),
|
|
58
|
-
definition:
|
|
55
|
+
definition: ProcessDefinition
|
|
59
56
|
})).output(RestartingProcessInfoSchema),
|
|
60
57
|
start: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
61
58
|
stop: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
@@ -65,7 +62,7 @@ const processes$1 = {
|
|
|
65
62
|
})).output(RestartingProcessInfoSchema),
|
|
66
63
|
reload: oc$1.input(v.object({
|
|
67
64
|
target: ResourceTarget,
|
|
68
|
-
definition:
|
|
65
|
+
definition: ProcessDefinition,
|
|
69
66
|
restartImmediately: v.optional(v.boolean())
|
|
70
67
|
})).output(RestartingProcessInfoSchema),
|
|
71
68
|
remove: oc$1.input(v.object({ target: ResourceTarget })).output(v.object({ success: v.boolean() }))
|
|
@@ -75,7 +72,7 @@ const tasks$1 = {
|
|
|
75
72
|
list: oc$1.output(v.array(TaskEntryInfoSchema)),
|
|
76
73
|
add: oc$1.input(v.object({
|
|
77
74
|
name: v.string(),
|
|
78
|
-
definition:
|
|
75
|
+
definition: ProcessDefinition
|
|
79
76
|
})).output(TaskEntryInfoSchema),
|
|
80
77
|
remove: oc$1.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema)
|
|
81
78
|
};
|
|
@@ -88,7 +85,7 @@ const crons$1 = {
|
|
|
88
85
|
};
|
|
89
86
|
const api = {
|
|
90
87
|
manager,
|
|
91
|
-
processes
|
|
88
|
+
processes,
|
|
92
89
|
tasks: tasks$1,
|
|
93
90
|
crons: crons$1
|
|
94
91
|
};
|
|
@@ -213,76 +210,52 @@ const router = os.router({
|
|
|
213
210
|
}
|
|
214
211
|
});
|
|
215
212
|
|
|
216
|
-
//#endregion
|
|
217
|
-
//#region src/port-utils.ts
|
|
218
|
-
const execAsync = promisify(exec);
|
|
219
|
-
/**
|
|
220
|
-
* Kill any process listening on the specified port, including all descendants.
|
|
221
|
-
*/
|
|
222
|
-
async function killProcessOnPort(port) {
|
|
223
|
-
try {
|
|
224
|
-
const { stdout } = await execAsync(`lsof -ti tcp:${port}`);
|
|
225
|
-
const pids = stdout.trim().split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => !isNaN(p));
|
|
226
|
-
if (pids.length === 0) return false;
|
|
227
|
-
for (const pid of pids) try {
|
|
228
|
-
await treeKill(pid, "SIGKILL");
|
|
229
|
-
} catch {}
|
|
230
|
-
return true;
|
|
231
|
-
} catch {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
213
|
//#endregion
|
|
237
214
|
//#region src/manager.ts
|
|
238
|
-
const
|
|
215
|
+
const HttpServerConfig = v.object({
|
|
239
216
|
host: v.optional(v.string()),
|
|
240
217
|
port: v.optional(v.number()),
|
|
241
218
|
authToken: v.optional(v.string())
|
|
242
219
|
});
|
|
243
|
-
const
|
|
220
|
+
const CronProcessEntry = v.object({
|
|
244
221
|
name: v.string(),
|
|
245
|
-
definition:
|
|
246
|
-
options:
|
|
222
|
+
definition: ProcessDefinition,
|
|
223
|
+
options: CronProcessOptions,
|
|
247
224
|
envFile: v.optional(v.string())
|
|
248
225
|
});
|
|
249
|
-
const
|
|
226
|
+
const EnvReloadDelay = v.union([
|
|
250
227
|
v.number(),
|
|
251
228
|
v.boolean(),
|
|
252
229
|
v.literal("immediately")
|
|
253
230
|
]);
|
|
254
|
-
const
|
|
231
|
+
const RestartingProcessEntry = v.object({
|
|
255
232
|
name: v.string(),
|
|
256
|
-
definition:
|
|
257
|
-
options: v.optional(
|
|
233
|
+
definition: ProcessDefinition,
|
|
234
|
+
options: v.optional(RestartingProcessOptions),
|
|
258
235
|
envFile: v.optional(v.string()),
|
|
259
|
-
envReloadDelay: v.optional(
|
|
260
|
-
port: v.optional(v.number())
|
|
236
|
+
envReloadDelay: v.optional(EnvReloadDelay)
|
|
261
237
|
});
|
|
262
|
-
const
|
|
238
|
+
const TaskEntry = v.object({
|
|
263
239
|
name: v.string(),
|
|
264
|
-
definition:
|
|
240
|
+
definition: ProcessDefinition,
|
|
265
241
|
envFile: v.optional(v.string())
|
|
266
242
|
});
|
|
267
|
-
const
|
|
268
|
-
http: v.optional(
|
|
243
|
+
const ManagerConfig = v.object({
|
|
244
|
+
http: v.optional(HttpServerConfig),
|
|
269
245
|
cwd: v.optional(v.string()),
|
|
270
246
|
logDir: v.optional(v.string()),
|
|
271
247
|
env: v.optional(v.record(v.string(), v.string())),
|
|
272
|
-
|
|
273
|
-
tasks: v.optional(v.array(
|
|
274
|
-
crons: v.optional(v.array(
|
|
275
|
-
processes: v.optional(v.array(
|
|
248
|
+
envFile: v.optional(v.string()),
|
|
249
|
+
tasks: v.optional(v.array(TaskEntry)),
|
|
250
|
+
crons: v.optional(v.array(CronProcessEntry)),
|
|
251
|
+
processes: v.optional(v.array(RestartingProcessEntry))
|
|
276
252
|
});
|
|
277
253
|
const DEFAULT_RESTART_OPTIONS = { restartPolicy: "always" };
|
|
278
254
|
const SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
279
|
-
const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
280
|
-
const SHUTDOWN_EXIT_CODE = 0;
|
|
281
255
|
var Manager = class {
|
|
282
256
|
config;
|
|
283
257
|
logger;
|
|
284
258
|
envManager;
|
|
285
|
-
envFileKeyByProcess = /* @__PURE__ */ new Map();
|
|
286
259
|
_state = "idle";
|
|
287
260
|
taskList = null;
|
|
288
261
|
cronProcesses = /* @__PURE__ */ new Map();
|
|
@@ -291,48 +264,93 @@ var Manager = class {
|
|
|
291
264
|
processEnvReloadConfig = /* @__PURE__ */ new Map();
|
|
292
265
|
envReloadTimers = /* @__PURE__ */ new Map();
|
|
293
266
|
envChangeUnsubscribe = null;
|
|
294
|
-
processPortConfig = /* @__PURE__ */ new Map();
|
|
295
267
|
signalHandlers = /* @__PURE__ */ new Map();
|
|
296
268
|
shutdownPromise = null;
|
|
297
269
|
isShuttingDown = false;
|
|
298
270
|
constructor(config, logger) {
|
|
271
|
+
const cwd$1 = config.cwd ?? cwd();
|
|
299
272
|
this.config = config;
|
|
300
273
|
this.logger = logger;
|
|
301
|
-
this.logDir = config.logDir ?? join(cwd
|
|
274
|
+
this.logDir = config.logDir ?? join(cwd$1, "logs");
|
|
302
275
|
this.ensureLogDirs();
|
|
276
|
+
this.validateConfigNames();
|
|
277
|
+
const customEnvFiles = {};
|
|
278
|
+
for (const task of config.tasks ?? []) if (task.envFile) customEnvFiles[task.name] = task.envFile;
|
|
279
|
+
for (const cron of config.crons ?? []) if (cron.envFile) customEnvFiles[cron.name] = cron.envFile;
|
|
280
|
+
for (const proc of config.processes ?? []) if (proc.envFile) customEnvFiles[proc.name] = proc.envFile;
|
|
303
281
|
this.envManager = new EnvManager({
|
|
304
|
-
cwd:
|
|
305
|
-
|
|
306
|
-
|
|
282
|
+
cwd: cwd$1,
|
|
283
|
+
globalEnvFile: config.envFile,
|
|
284
|
+
customEnvFiles
|
|
285
|
+
});
|
|
286
|
+
this.envChangeUnsubscribe = this.envManager.onChange((event) => this.handleEnvChange(event));
|
|
287
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
288
|
+
const handler = () => this.handleSignal(signal);
|
|
289
|
+
this.signalHandlers.set(signal, handler);
|
|
290
|
+
process.on(signal, handler);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
validateConfigNames() {
|
|
294
|
+
const allNames = [];
|
|
295
|
+
for (const task of this.config.tasks ?? []) allNames.push({
|
|
296
|
+
name: task.name,
|
|
297
|
+
type: "task"
|
|
307
298
|
});
|
|
308
|
-
|
|
309
|
-
|
|
299
|
+
for (const cron of this.config.crons ?? []) allNames.push({
|
|
300
|
+
name: cron.name,
|
|
301
|
+
type: "cron"
|
|
310
302
|
});
|
|
311
|
-
this.
|
|
303
|
+
for (const proc of this.config.processes ?? []) allNames.push({
|
|
304
|
+
name: proc.name,
|
|
305
|
+
type: "process"
|
|
306
|
+
});
|
|
307
|
+
const seen = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const { name, type } of allNames) {
|
|
309
|
+
const existingType = seen.get(name);
|
|
310
|
+
if (existingType) throw new Error(`Duplicate name "${name}" found: already used as ${existingType}, cannot use as ${type}. Names must be globally unique across tasks, crons, and processes.`);
|
|
311
|
+
seen.set(name, type);
|
|
312
|
+
}
|
|
312
313
|
}
|
|
313
314
|
/**
|
|
314
|
-
*
|
|
315
|
-
* Merge order: .env (global), config.env (global), .env.<processName>, processEnvFile, definition.env
|
|
315
|
+
* Check if a name is already used by any task, cron, or process
|
|
316
316
|
*/
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
envFromCustomFile = this.envManager.getEnvForKey(key);
|
|
317
|
+
isNameUsed(name) {
|
|
318
|
+
for (const task of this.config.tasks ?? []) if (task.name === name) return {
|
|
319
|
+
used: true,
|
|
320
|
+
type: "task"
|
|
321
|
+
};
|
|
322
|
+
if (this.taskList) {
|
|
323
|
+
for (const task of this.taskList.tasks) for (const proc of task.processes) if (proc.name === name) return {
|
|
324
|
+
used: true,
|
|
325
|
+
type: "task"
|
|
326
|
+
};
|
|
328
327
|
}
|
|
328
|
+
if (this.cronProcesses.has(name)) return {
|
|
329
|
+
used: true,
|
|
330
|
+
type: "cron"
|
|
331
|
+
};
|
|
332
|
+
for (const cron of this.config.crons ?? []) if (cron.name === name) return {
|
|
333
|
+
used: true,
|
|
334
|
+
type: "cron"
|
|
335
|
+
};
|
|
336
|
+
if (this.restartingProcesses.has(name)) return {
|
|
337
|
+
used: true,
|
|
338
|
+
type: "process"
|
|
339
|
+
};
|
|
340
|
+
for (const proc of this.config.processes ?? []) if (proc.name === name) return {
|
|
341
|
+
used: true,
|
|
342
|
+
type: "process"
|
|
343
|
+
};
|
|
344
|
+
return { used: false };
|
|
345
|
+
}
|
|
346
|
+
applyDefaults(processName, definition) {
|
|
347
|
+
const envVarsFromManager = this.envManager.getEnvVars(processName);
|
|
329
348
|
return {
|
|
330
349
|
...definition,
|
|
331
350
|
cwd: definition.cwd ?? this.config.cwd,
|
|
332
351
|
env: {
|
|
333
|
-
...
|
|
352
|
+
...envVarsFromManager,
|
|
334
353
|
...this.config.env,
|
|
335
|
-
...envFromCustomFile,
|
|
336
354
|
...definition.env
|
|
337
355
|
}
|
|
338
356
|
};
|
|
@@ -346,14 +364,6 @@ var Manager = class {
|
|
|
346
364
|
cronLogFile(name) {
|
|
347
365
|
return join(this.logDir, "cron", `${name}.log`);
|
|
348
366
|
}
|
|
349
|
-
/**
|
|
350
|
-
* Kill any process running on the configured port for a process
|
|
351
|
-
*/
|
|
352
|
-
async killPortForProcess(processName) {
|
|
353
|
-
const port = this.processPortConfig.get(processName);
|
|
354
|
-
if (port === void 0) return;
|
|
355
|
-
if (await killProcessOnPort(port)) this.logger.info(`Killed process on port ${port} before starting "${processName}"`);
|
|
356
|
-
}
|
|
357
367
|
ensureLogDirs() {
|
|
358
368
|
mkdirSync(this.logDir, { recursive: true });
|
|
359
369
|
mkdirSync(join(this.logDir, "process"), { recursive: true });
|
|
@@ -363,18 +373,21 @@ var Manager = class {
|
|
|
363
373
|
/**
|
|
364
374
|
* Handle env file changes
|
|
365
375
|
*/
|
|
366
|
-
handleEnvChange(
|
|
376
|
+
handleEnvChange(event) {
|
|
367
377
|
if (this._state !== "running") return;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
if (event.type === "global") {
|
|
379
|
+
this.logger.info("Global env file changed, reloading all processes as per their policies");
|
|
380
|
+
for (const processName of this.restartingProcesses.keys()) {
|
|
381
|
+
const reloadDelay = this.processEnvReloadConfig.get(processName);
|
|
382
|
+
if (reloadDelay === false) continue;
|
|
383
|
+
this.scheduleProcessReload(processName, reloadDelay);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
374
386
|
}
|
|
375
|
-
|
|
387
|
+
if (event.type === "process") {
|
|
388
|
+
const processName = event.key;
|
|
376
389
|
const reloadDelay = this.processEnvReloadConfig.get(processName);
|
|
377
|
-
if (reloadDelay === false)
|
|
390
|
+
if (reloadDelay === false) return;
|
|
378
391
|
this.scheduleProcessReload(processName, reloadDelay);
|
|
379
392
|
}
|
|
380
393
|
}
|
|
@@ -411,7 +424,7 @@ var Manager = class {
|
|
|
411
424
|
this.logger.warn(`Process config for "${processName}" not found`);
|
|
412
425
|
return;
|
|
413
426
|
}
|
|
414
|
-
const updatedDefinition = this.applyDefaults(processName, processConfig.definition
|
|
427
|
+
const updatedDefinition = this.applyDefaults(processName, processConfig.definition);
|
|
415
428
|
await proc.reload(updatedDefinition, true);
|
|
416
429
|
}
|
|
417
430
|
get state() {
|
|
@@ -490,7 +503,6 @@ var Manager = class {
|
|
|
490
503
|
async startProcessByTarget(target) {
|
|
491
504
|
const proc = this.getProcessByTarget(target);
|
|
492
505
|
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
493
|
-
await this.killPortForProcess(proc.name);
|
|
494
506
|
proc.start();
|
|
495
507
|
return proc;
|
|
496
508
|
}
|
|
@@ -509,7 +521,6 @@ var Manager = class {
|
|
|
509
521
|
async restartProcessByTarget(target, force = false) {
|
|
510
522
|
const proc = this.getProcessByTarget(target);
|
|
511
523
|
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
512
|
-
await this.killPortForProcess(proc.name);
|
|
513
524
|
await proc.restart(force);
|
|
514
525
|
return proc;
|
|
515
526
|
}
|
|
@@ -539,7 +550,10 @@ var Manager = class {
|
|
|
539
550
|
* Add a task to the task list
|
|
540
551
|
* Creates the task list if it doesn't exist and starts it
|
|
541
552
|
*/
|
|
542
|
-
addTask(name, definition) {
|
|
553
|
+
addTask(name, definition, envFile) {
|
|
554
|
+
const nameCheck = this.isNameUsed(name);
|
|
555
|
+
if (nameCheck.used) throw new Error(`Name "${name}" is already used as ${nameCheck.type}. Names must be globally unique across tasks, crons, and processes.`);
|
|
556
|
+
if (envFile) this.envManager.registerFile(name, envFile);
|
|
543
557
|
if (!this.taskList) this.taskList = new TaskList("runtime", this.logger.child("tasks", { logFile: this.taskLogFile("tasks") }), void 0, (processName) => {
|
|
544
558
|
return this.taskLogFile(processName);
|
|
545
559
|
});
|
|
@@ -567,15 +581,13 @@ var Manager = class {
|
|
|
567
581
|
/**
|
|
568
582
|
* Add a restarting process at runtime
|
|
569
583
|
*/
|
|
570
|
-
async addProcess(name, definition, options, envReloadDelay,
|
|
571
|
-
|
|
584
|
+
async addProcess(name, definition, options, envReloadDelay, envFile) {
|
|
585
|
+
const nameCheck = this.isNameUsed(name);
|
|
586
|
+
if (nameCheck.used) throw new Error(`Name "${name}" is already used as ${nameCheck.type}. Names must be globally unique across tasks, crons, and processes.`);
|
|
587
|
+
if (envFile) this.envManager.registerFile(name, envFile);
|
|
572
588
|
const processLogger = this.logger.child(name, { logFile: this.processLogFile(name) });
|
|
573
589
|
const restartingProcess = new RestartingProcess(name, this.applyDefaults(name, definition), options ?? DEFAULT_RESTART_OPTIONS, processLogger);
|
|
574
590
|
this.restartingProcesses.set(name, restartingProcess);
|
|
575
|
-
if (port !== void 0) {
|
|
576
|
-
this.processPortConfig.set(name, port);
|
|
577
|
-
await this.killPortForProcess(name);
|
|
578
|
-
}
|
|
579
591
|
restartingProcess.start();
|
|
580
592
|
this.processEnvReloadConfig.set(name, envReloadDelay ?? 5e3);
|
|
581
593
|
this.logger.info(`Added and started restarting process: ${name}`);
|
|
@@ -621,7 +633,7 @@ var Manager = class {
|
|
|
621
633
|
this.logger.info(`Running initialization tasks`);
|
|
622
634
|
this.taskList = new TaskList("init", this.logger.child("tasks"), this.config.tasks.map((task) => ({
|
|
623
635
|
name: task.name,
|
|
624
|
-
process: this.applyDefaults(task.name, task.definition
|
|
636
|
+
process: this.applyDefaults(task.name, task.definition)
|
|
625
637
|
})), (processName) => {
|
|
626
638
|
return this.taskLogFile(processName);
|
|
627
639
|
});
|
|
@@ -637,19 +649,15 @@ var Manager = class {
|
|
|
637
649
|
}
|
|
638
650
|
if (this.config.crons) for (const entry of this.config.crons) {
|
|
639
651
|
const processLogger = this.logger.child(entry.name, { logFile: this.cronLogFile(entry.name) });
|
|
640
|
-
const cronProcess = new CronProcess(entry.name, this.applyDefaults(entry.name, entry.definition
|
|
652
|
+
const cronProcess = new CronProcess(entry.name, this.applyDefaults(entry.name, entry.definition), entry.options, processLogger);
|
|
641
653
|
this.cronProcesses.set(entry.name, cronProcess);
|
|
642
654
|
cronProcess.start();
|
|
643
655
|
this.logger.info(`Started cron process: ${entry.name}`);
|
|
644
656
|
}
|
|
645
657
|
if (this.config.processes) for (const entry of this.config.processes) {
|
|
646
658
|
const processLogger = this.logger.child(entry.name, { logFile: this.processLogFile(entry.name) });
|
|
647
|
-
const restartingProcess = new RestartingProcess(entry.name, this.applyDefaults(entry.name, entry.definition
|
|
659
|
+
const restartingProcess = new RestartingProcess(entry.name, this.applyDefaults(entry.name, entry.definition), entry.options ?? DEFAULT_RESTART_OPTIONS, processLogger);
|
|
648
660
|
this.restartingProcesses.set(entry.name, restartingProcess);
|
|
649
|
-
if (entry.port !== void 0) {
|
|
650
|
-
this.processPortConfig.set(entry.name, entry.port);
|
|
651
|
-
await this.killPortForProcess(entry.name);
|
|
652
|
-
}
|
|
653
661
|
restartingProcess.start();
|
|
654
662
|
this.processEnvReloadConfig.set(entry.name, entry.envReloadDelay ?? 5e3);
|
|
655
663
|
this.logger.info(`Started restarting process: ${entry.name}`);
|
|
@@ -663,7 +671,6 @@ var Manager = class {
|
|
|
663
671
|
async stop(timeout) {
|
|
664
672
|
if (this._state === "idle" || this._state === "stopped") {
|
|
665
673
|
this._state = "stopped";
|
|
666
|
-
this.unregisterShutdownHandlers();
|
|
667
674
|
return;
|
|
668
675
|
}
|
|
669
676
|
this._state = "stopping";
|
|
@@ -674,42 +681,13 @@ var Manager = class {
|
|
|
674
681
|
this.envChangeUnsubscribe();
|
|
675
682
|
this.envChangeUnsubscribe = null;
|
|
676
683
|
}
|
|
677
|
-
this.envManager.
|
|
684
|
+
this.envManager.close();
|
|
678
685
|
if (this.taskList) await this.taskList.stop(timeout);
|
|
679
686
|
const cronStopPromises = Array.from(this.cronProcesses.values()).map((p) => p.stop(timeout));
|
|
680
687
|
const restartingStopPromises = Array.from(this.restartingProcesses.values()).map((p) => p.stop(timeout));
|
|
681
688
|
await Promise.all([...cronStopPromises, ...restartingStopPromises]);
|
|
682
689
|
this._state = "stopped";
|
|
683
690
|
this.logger.info(`Manager stopped`);
|
|
684
|
-
this.unregisterShutdownHandlers();
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Register signal handlers for graceful shutdown
|
|
688
|
-
* Called automatically by constructor
|
|
689
|
-
*/
|
|
690
|
-
registerShutdownHandlers() {
|
|
691
|
-
if (this.signalHandlers.size > 0) {
|
|
692
|
-
this.logger.warn(`Shutdown handlers already registered`);
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
for (const signal of SHUTDOWN_SIGNALS) {
|
|
696
|
-
const handler = () => this.handleSignal(signal);
|
|
697
|
-
this.signalHandlers.set(signal, handler);
|
|
698
|
-
process.on(signal, handler);
|
|
699
|
-
this.logger.debug(`Registered handler for ${signal}`);
|
|
700
|
-
}
|
|
701
|
-
this.logger.info(`Shutdown handlers registered for signals: ${SHUTDOWN_SIGNALS.join(", ")}`);
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Unregister signal handlers for graceful shutdown
|
|
705
|
-
*/
|
|
706
|
-
unregisterShutdownHandlers() {
|
|
707
|
-
if (this.signalHandlers.size === 0) return;
|
|
708
|
-
for (const [signal, handler] of this.signalHandlers.entries()) {
|
|
709
|
-
process.off(signal, handler);
|
|
710
|
-
this.logger.debug(`Unregistered handler for ${signal}`);
|
|
711
|
-
}
|
|
712
|
-
this.signalHandlers.clear();
|
|
713
691
|
}
|
|
714
692
|
/**
|
|
715
693
|
* Wait for shutdown to complete (useful for keeping process alive)
|
|
@@ -753,8 +731,8 @@ var Manager = class {
|
|
|
753
731
|
this.isShuttingDown = true;
|
|
754
732
|
this.shutdownPromise = this.performShutdown();
|
|
755
733
|
this.shutdownPromise.then(() => {
|
|
756
|
-
this.logger.info(`Exiting with code
|
|
757
|
-
process.exit(
|
|
734
|
+
this.logger.info(`Exiting with code 0`);
|
|
735
|
+
process.exit();
|
|
758
736
|
}).catch((err) => {
|
|
759
737
|
this.logger.error(`Shutdown error:`, err);
|
|
760
738
|
process.exit(1);
|
|
@@ -780,19 +758,31 @@ var Manager = class {
|
|
|
780
758
|
}
|
|
781
759
|
};
|
|
782
760
|
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/utils.ts
|
|
763
|
+
async function tImport(path) {
|
|
764
|
+
try {
|
|
765
|
+
return await import(path);
|
|
766
|
+
} catch {
|
|
767
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
768
|
+
return await tsImport(path, { parentURL: import.meta.url });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
783
772
|
//#endregion
|
|
784
773
|
//#region src/cli.ts
|
|
785
774
|
const program = new Command();
|
|
786
|
-
program.name("pidnap").description("Process manager with init system capabilities").version("0.0.0-dev.
|
|
775
|
+
program.name("pidnap").description("Process manager with init system capabilities").version("0.0.0-dev.5");
|
|
787
776
|
program.command("init").description("Initialize and run the process manager with config file").option("-c, --config [path]", "Path to config file", "pidnap.config.ts").action(async (options) => {
|
|
777
|
+
process.title = "pidnap";
|
|
788
778
|
const initLogger = logger({ name: "pidnap" });
|
|
789
779
|
try {
|
|
790
780
|
const configUrl = pathToFileURL(resolve(process.cwd(), options.config)).href;
|
|
791
|
-
const configModule = await
|
|
781
|
+
const configModule = await tImport(configUrl);
|
|
792
782
|
const rawConfig = configModule.default.default || configModule.default || configModule.config || {};
|
|
793
|
-
const config = v.parse(
|
|
794
|
-
const host = config.http?.host ?? "
|
|
795
|
-
const port = config.http?.port ??
|
|
783
|
+
const config = v.parse(ManagerConfig, rawConfig);
|
|
784
|
+
const host = config.http?.host ?? "127.0.0.1";
|
|
785
|
+
const port = config.http?.port ?? 9876;
|
|
796
786
|
const authToken = config.http?.authToken;
|
|
797
787
|
const managerLogger = logger({
|
|
798
788
|
name: "pidnap",
|
|
@@ -816,7 +806,7 @@ program.command("init").description("Initialize and run the process manager with
|
|
|
816
806
|
});
|
|
817
807
|
if (matched) return;
|
|
818
808
|
res.statusCode = 404;
|
|
819
|
-
res.end("Not found");
|
|
809
|
+
res.end("Not found\n");
|
|
820
810
|
});
|
|
821
811
|
server.listen(port, host, async () => {
|
|
822
812
|
managerLogger.info(`pidnap RPC server running on http://${host}:${port}`);
|
|
@@ -836,7 +826,7 @@ program.command("init").description("Initialize and run the process manager with
|
|
|
836
826
|
process.exit(1);
|
|
837
827
|
}
|
|
838
828
|
});
|
|
839
|
-
program.command("status").description("Show manager status").option("-u, --url <url>", "RPC server URL"
|
|
829
|
+
program.command("status").description("Show manager status").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
840
830
|
try {
|
|
841
831
|
const status = await createClient(options.url).manager.status();
|
|
842
832
|
const table = new Table({ head: [
|
|
@@ -857,8 +847,8 @@ program.command("status").description("Show manager status").option("-u, --url <
|
|
|
857
847
|
process.exit(1);
|
|
858
848
|
}
|
|
859
849
|
});
|
|
860
|
-
const
|
|
861
|
-
|
|
850
|
+
const processGroup = program.command("process").description("Manage restarting processes");
|
|
851
|
+
processGroup.command("list").description("List restarting processes").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
862
852
|
try {
|
|
863
853
|
const processes = await createClient(options.url).processes.list();
|
|
864
854
|
const table = new Table({ head: [
|
|
@@ -877,7 +867,7 @@ processes.command("list").description("List restarting processes").option("-u, -
|
|
|
877
867
|
process.exit(1);
|
|
878
868
|
}
|
|
879
869
|
});
|
|
880
|
-
|
|
870
|
+
processGroup.command("get").description("Get a restarting process by name or index").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
881
871
|
try {
|
|
882
872
|
const proc = await createClient(options.url).processes.get({ target: parseTarget(target) });
|
|
883
873
|
const table = new Table({ head: [
|
|
@@ -896,7 +886,7 @@ processes.command("get").description("Get a restarting process by name or index"
|
|
|
896
886
|
process.exit(1);
|
|
897
887
|
}
|
|
898
888
|
});
|
|
899
|
-
|
|
889
|
+
processGroup.command("add").description("Add a restarting process").requiredOption("-n, --name <name>", "Process name").requiredOption("-d, --definition <json>", "Process definition JSON").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
900
890
|
try {
|
|
901
891
|
const client = createClient(options.url);
|
|
902
892
|
const definition = parseDefinition(options.definition);
|
|
@@ -920,7 +910,7 @@ processes.command("add").description("Add a restarting process").requiredOption(
|
|
|
920
910
|
process.exit(1);
|
|
921
911
|
}
|
|
922
912
|
});
|
|
923
|
-
|
|
913
|
+
processGroup.command("start").description("Start a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
924
914
|
try {
|
|
925
915
|
const proc = await createClient(options.url).processes.start({ target: parseTarget(target) });
|
|
926
916
|
const table = new Table({ head: [
|
|
@@ -939,7 +929,7 @@ processes.command("start").description("Start a restarting process").argument("<
|
|
|
939
929
|
process.exit(1);
|
|
940
930
|
}
|
|
941
931
|
});
|
|
942
|
-
|
|
932
|
+
processGroup.command("stop").description("Stop a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
943
933
|
try {
|
|
944
934
|
const proc = await createClient(options.url).processes.stop({ target: parseTarget(target) });
|
|
945
935
|
const table = new Table({ head: [
|
|
@@ -958,7 +948,7 @@ processes.command("stop").description("Stop a restarting process").argument("<ta
|
|
|
958
948
|
process.exit(1);
|
|
959
949
|
}
|
|
960
950
|
});
|
|
961
|
-
|
|
951
|
+
processGroup.command("restart").description("Restart a restarting process").argument("<target>", "Process name or index").option("-f, --force", "Force restart").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
962
952
|
try {
|
|
963
953
|
const proc = await createClient(options.url).processes.restart({
|
|
964
954
|
target: parseTarget(target),
|
|
@@ -980,7 +970,7 @@ processes.command("restart").description("Restart a restarting process").argumen
|
|
|
980
970
|
process.exit(1);
|
|
981
971
|
}
|
|
982
972
|
});
|
|
983
|
-
|
|
973
|
+
processGroup.command("remove").description("Remove a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
984
974
|
try {
|
|
985
975
|
await createClient(options.url).processes.remove({ target: parseTarget(target) });
|
|
986
976
|
console.log("Process removed");
|
|
@@ -990,7 +980,7 @@ processes.command("remove").description("Remove a restarting process").argument(
|
|
|
990
980
|
}
|
|
991
981
|
});
|
|
992
982
|
const crons = program.command("crons").description("Manage cron processes");
|
|
993
|
-
crons.command("list").description("List cron processes").option("-u, --url <url>", "RPC server URL"
|
|
983
|
+
crons.command("list").description("List cron processes").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
994
984
|
try {
|
|
995
985
|
const crons = await createClient(options.url).crons.list();
|
|
996
986
|
const table = new Table({ head: [
|
|
@@ -1013,7 +1003,7 @@ crons.command("list").description("List cron processes").option("-u, --url <url>
|
|
|
1013
1003
|
process.exit(1);
|
|
1014
1004
|
}
|
|
1015
1005
|
});
|
|
1016
|
-
crons.command("get").description("Get a cron process by name or index").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL"
|
|
1006
|
+
crons.command("get").description("Get a cron process by name or index").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1017
1007
|
try {
|
|
1018
1008
|
const cron = await createClient(options.url).crons.get({ target: parseTarget(target) });
|
|
1019
1009
|
const table = new Table({ head: [
|
|
@@ -1036,7 +1026,7 @@ crons.command("get").description("Get a cron process by name or index").argument
|
|
|
1036
1026
|
process.exit(1);
|
|
1037
1027
|
}
|
|
1038
1028
|
});
|
|
1039
|
-
crons.command("trigger").description("Trigger a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL"
|
|
1029
|
+
crons.command("trigger").description("Trigger a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1040
1030
|
try {
|
|
1041
1031
|
const cron = await createClient(options.url).crons.trigger({ target: parseTarget(target) });
|
|
1042
1032
|
const table = new Table({ head: [
|
|
@@ -1059,7 +1049,7 @@ crons.command("trigger").description("Trigger a cron process").argument("<target
|
|
|
1059
1049
|
process.exit(1);
|
|
1060
1050
|
}
|
|
1061
1051
|
});
|
|
1062
|
-
crons.command("start").description("Start a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL"
|
|
1052
|
+
crons.command("start").description("Start a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1063
1053
|
try {
|
|
1064
1054
|
const cron = await createClient(options.url).crons.start({ target: parseTarget(target) });
|
|
1065
1055
|
const table = new Table({ head: [
|
|
@@ -1082,7 +1072,7 @@ crons.command("start").description("Start a cron process").argument("<target>",
|
|
|
1082
1072
|
process.exit(1);
|
|
1083
1073
|
}
|
|
1084
1074
|
});
|
|
1085
|
-
crons.command("stop").description("Stop a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL"
|
|
1075
|
+
crons.command("stop").description("Stop a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1086
1076
|
try {
|
|
1087
1077
|
const cron = await createClient(options.url).crons.stop({ target: parseTarget(target) });
|
|
1088
1078
|
const table = new Table({ head: [
|
|
@@ -1106,7 +1096,7 @@ crons.command("stop").description("Stop a cron process").argument("<target>", "C
|
|
|
1106
1096
|
}
|
|
1107
1097
|
});
|
|
1108
1098
|
const tasks = program.command("tasks").description("Manage tasks");
|
|
1109
|
-
tasks.command("list").description("List tasks").option("-u, --url <url>", "RPC server URL"
|
|
1099
|
+
tasks.command("list").description("List tasks").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
1110
1100
|
try {
|
|
1111
1101
|
const tasks = await createClient(options.url).tasks.list();
|
|
1112
1102
|
const table = new Table({ head: [
|
|
@@ -1125,7 +1115,7 @@ tasks.command("list").description("List tasks").option("-u, --url <url>", "RPC s
|
|
|
1125
1115
|
process.exit(1);
|
|
1126
1116
|
}
|
|
1127
1117
|
});
|
|
1128
|
-
tasks.command("get").description("Get a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL"
|
|
1118
|
+
tasks.command("get").description("Get a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1129
1119
|
try {
|
|
1130
1120
|
const task = await createClient(options.url).tasks.get({ target: parseTarget(target) });
|
|
1131
1121
|
const table = new Table({ head: [
|
|
@@ -1144,7 +1134,7 @@ tasks.command("get").description("Get a task by id or index").argument("<target>
|
|
|
1144
1134
|
process.exit(1);
|
|
1145
1135
|
}
|
|
1146
1136
|
});
|
|
1147
|
-
tasks.command("add").description("Add a task").requiredOption("-n, --name <name>", "Task name").requiredOption("-d, --definition <json>", "Process definition JSON").option("-u, --url <url>", "RPC server URL"
|
|
1137
|
+
tasks.command("add").description("Add a task").requiredOption("-n, --name <name>", "Task name").requiredOption("-d, --definition <json>", "Process definition JSON").option("-u, --url <url>", "RPC server URL").action(async (options) => {
|
|
1148
1138
|
try {
|
|
1149
1139
|
const client = createClient(options.url);
|
|
1150
1140
|
const definition = parseDefinition(options.definition);
|
|
@@ -1168,7 +1158,7 @@ tasks.command("add").description("Add a task").requiredOption("-n, --name <name>
|
|
|
1168
1158
|
process.exit(1);
|
|
1169
1159
|
}
|
|
1170
1160
|
});
|
|
1171
|
-
tasks.command("remove").description("Remove a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL"
|
|
1161
|
+
tasks.command("remove").description("Remove a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL").action(async (target, options) => {
|
|
1172
1162
|
try {
|
|
1173
1163
|
const task = await createClient(options.url).tasks.remove({ target: parseTarget(target) });
|
|
1174
1164
|
const table = new Table({ head: [
|
|
@@ -1195,7 +1185,7 @@ function parseTarget(value) {
|
|
|
1195
1185
|
function parseDefinition(raw) {
|
|
1196
1186
|
try {
|
|
1197
1187
|
const parsed = JSON.parse(raw);
|
|
1198
|
-
return v.parse(
|
|
1188
|
+
return v.parse(ProcessDefinition, parsed);
|
|
1199
1189
|
} catch (error) {
|
|
1200
1190
|
console.error("Invalid --definition JSON. Expected a ProcessDefinition.");
|
|
1201
1191
|
throw error;
|