pidnap 0.0.0-dev.0
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/README.md +45 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1166 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client.d.mts +169 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +15 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +230 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-crc5neL8.mjs +966 -0
- package/dist/logger-crc5neL8.mjs.map +1 -0
- package/dist/task-list-CIdbB3wM.d.mts +230 -0
- package/dist/task-list-CIdbB3wM.d.mts.map +1 -0
- package/package.json +50 -0
- package/src/api/client.ts +13 -0
- package/src/api/contract.ts +117 -0
- package/src/api/server.ts +180 -0
- package/src/cli.ts +462 -0
- package/src/cron-process.ts +255 -0
- package/src/env-manager.ts +237 -0
- package/src/index.ts +12 -0
- package/src/lazy-process.ts +228 -0
- package/src/logger.ts +108 -0
- package/src/manager.ts +859 -0
- package/src/restarting-process.ts +397 -0
- package/src/task-list.ts +236 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { _ as ProcessDefinitionSchema, a as TaskStateSchema, c as CronProcessStateSchema, h as RestartingProcessStateSchema, i as TaskList, m as RestartingProcessOptionsSchema, n as EnvManager, o as CronProcess, p as RestartingProcess, s as CronProcessOptionsSchema, t as logger } from "./logger-crc5neL8.mjs";
|
|
3
|
+
import { createClient } from "./client.mjs";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import { RPCHandler } from "@orpc/server/node";
|
|
9
|
+
import { implement, onError } from "@orpc/server";
|
|
10
|
+
import * as v from "valibot";
|
|
11
|
+
import { oc } from "@orpc/contract";
|
|
12
|
+
import { cwd } from "node:process";
|
|
13
|
+
import { mkdirSync } from "node:fs";
|
|
14
|
+
import { tsImport } from "tsx/esm/api";
|
|
15
|
+
import Table from "cli-table3";
|
|
16
|
+
|
|
17
|
+
//#region src/api/contract.ts
|
|
18
|
+
const oc$1 = oc.$input(v.void());
|
|
19
|
+
const ResourceTarget = v.union([v.string(), v.number()]);
|
|
20
|
+
const ManagerStateSchema = v.picklist([
|
|
21
|
+
"idle",
|
|
22
|
+
"initializing",
|
|
23
|
+
"running",
|
|
24
|
+
"stopping",
|
|
25
|
+
"stopped"
|
|
26
|
+
]);
|
|
27
|
+
const ManagerStatusSchema = v.object({
|
|
28
|
+
state: ManagerStateSchema,
|
|
29
|
+
processCount: v.number(),
|
|
30
|
+
cronCount: v.number(),
|
|
31
|
+
taskCount: v.number()
|
|
32
|
+
});
|
|
33
|
+
const RestartingProcessInfoSchema = v.object({
|
|
34
|
+
name: v.string(),
|
|
35
|
+
state: RestartingProcessStateSchema,
|
|
36
|
+
restarts: v.number()
|
|
37
|
+
});
|
|
38
|
+
const CronProcessInfoSchema = v.object({
|
|
39
|
+
name: v.string(),
|
|
40
|
+
state: CronProcessStateSchema,
|
|
41
|
+
runCount: v.number(),
|
|
42
|
+
failCount: v.number(),
|
|
43
|
+
nextRun: v.nullable(v.string())
|
|
44
|
+
});
|
|
45
|
+
const TaskEntryInfoSchema = v.object({
|
|
46
|
+
id: v.string(),
|
|
47
|
+
state: TaskStateSchema,
|
|
48
|
+
processNames: v.array(v.string())
|
|
49
|
+
});
|
|
50
|
+
const manager = { status: oc$1.output(ManagerStatusSchema) };
|
|
51
|
+
const processes$1 = {
|
|
52
|
+
get: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
53
|
+
list: oc$1.output(v.array(RestartingProcessInfoSchema)),
|
|
54
|
+
add: oc$1.input(v.object({
|
|
55
|
+
name: v.string(),
|
|
56
|
+
definition: ProcessDefinitionSchema
|
|
57
|
+
})).output(RestartingProcessInfoSchema),
|
|
58
|
+
start: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
59
|
+
stop: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
|
|
60
|
+
restart: oc$1.input(v.object({
|
|
61
|
+
target: ResourceTarget,
|
|
62
|
+
force: v.optional(v.boolean())
|
|
63
|
+
})).output(RestartingProcessInfoSchema),
|
|
64
|
+
reload: oc$1.input(v.object({
|
|
65
|
+
target: ResourceTarget,
|
|
66
|
+
definition: ProcessDefinitionSchema,
|
|
67
|
+
restartImmediately: v.optional(v.boolean())
|
|
68
|
+
})).output(RestartingProcessInfoSchema),
|
|
69
|
+
remove: oc$1.input(v.object({ target: ResourceTarget })).output(v.object({ success: v.boolean() }))
|
|
70
|
+
};
|
|
71
|
+
const tasks$1 = {
|
|
72
|
+
get: oc$1.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema),
|
|
73
|
+
list: oc$1.output(v.array(TaskEntryInfoSchema)),
|
|
74
|
+
add: oc$1.input(v.object({
|
|
75
|
+
name: v.string(),
|
|
76
|
+
definition: ProcessDefinitionSchema
|
|
77
|
+
})).output(TaskEntryInfoSchema),
|
|
78
|
+
remove: oc$1.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema)
|
|
79
|
+
};
|
|
80
|
+
const crons$1 = {
|
|
81
|
+
get: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
|
|
82
|
+
list: oc$1.output(v.array(CronProcessInfoSchema)),
|
|
83
|
+
trigger: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
|
|
84
|
+
start: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
|
|
85
|
+
stop: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema)
|
|
86
|
+
};
|
|
87
|
+
const api = {
|
|
88
|
+
manager,
|
|
89
|
+
processes: processes$1,
|
|
90
|
+
tasks: tasks$1,
|
|
91
|
+
crons: crons$1
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/api/server.ts
|
|
96
|
+
const os = implement(api).$context();
|
|
97
|
+
function serializeProcess(proc) {
|
|
98
|
+
return {
|
|
99
|
+
name: proc.name,
|
|
100
|
+
state: proc.state,
|
|
101
|
+
restarts: proc.restarts
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function serializeCron(cron) {
|
|
105
|
+
return {
|
|
106
|
+
name: cron.name,
|
|
107
|
+
state: cron.state,
|
|
108
|
+
runCount: cron.runCount,
|
|
109
|
+
failCount: cron.failCount,
|
|
110
|
+
nextRun: cron.nextRun?.toISOString() ?? null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const managerStatus = os.manager.status.handler(async ({ context }) => {
|
|
114
|
+
const manager = context.manager;
|
|
115
|
+
const taskList = manager.getTaskList();
|
|
116
|
+
return {
|
|
117
|
+
state: manager.state,
|
|
118
|
+
processCount: manager.getRestartingProcesses().size,
|
|
119
|
+
cronCount: manager.getCronProcesses().size,
|
|
120
|
+
taskCount: taskList?.tasks.length ?? 0
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
const getProcess = os.processes.get.handler(async ({ input, context }) => {
|
|
124
|
+
const proc = context.manager.getProcessByTarget(input.target);
|
|
125
|
+
if (!proc) throw new Error(`Process not found: ${input.target}`);
|
|
126
|
+
return serializeProcess(proc);
|
|
127
|
+
});
|
|
128
|
+
const listProcesses = os.processes.list.handler(async ({ context }) => {
|
|
129
|
+
return Array.from(context.manager.getRestartingProcesses().values()).map(serializeProcess);
|
|
130
|
+
});
|
|
131
|
+
const addProcess = os.processes.add.handler(async ({ input, context }) => {
|
|
132
|
+
return serializeProcess(context.manager.addProcess(input.name, input.definition));
|
|
133
|
+
});
|
|
134
|
+
const startProcess = os.processes.start.handler(async ({ input, context }) => {
|
|
135
|
+
return serializeProcess(context.manager.startProcessByTarget(input.target));
|
|
136
|
+
});
|
|
137
|
+
const stopProcess = os.processes.stop.handler(async ({ input, context }) => {
|
|
138
|
+
return serializeProcess(await context.manager.stopProcessByTarget(input.target));
|
|
139
|
+
});
|
|
140
|
+
const restartProcess = os.processes.restart.handler(async ({ input, context }) => {
|
|
141
|
+
return serializeProcess(await context.manager.restartProcessByTarget(input.target, input.force));
|
|
142
|
+
});
|
|
143
|
+
const reloadProcess = os.processes.reload.handler(async ({ input, context }) => {
|
|
144
|
+
return serializeProcess(await context.manager.reloadProcessByTarget(input.target, input.definition, { restartImmediately: input.restartImmediately }));
|
|
145
|
+
});
|
|
146
|
+
const removeProcess = os.processes.remove.handler(async ({ input, context }) => {
|
|
147
|
+
await context.manager.removeProcessByTarget(input.target);
|
|
148
|
+
return { success: true };
|
|
149
|
+
});
|
|
150
|
+
const getCron = os.crons.get.handler(async ({ input, context }) => {
|
|
151
|
+
const cron = context.manager.getCronByTarget(input.target);
|
|
152
|
+
if (!cron) throw new Error(`Cron not found: ${input.target}`);
|
|
153
|
+
return serializeCron(cron);
|
|
154
|
+
});
|
|
155
|
+
const listCrons = os.crons.list.handler(async ({ context }) => {
|
|
156
|
+
return Array.from(context.manager.getCronProcesses().values()).map(serializeCron);
|
|
157
|
+
});
|
|
158
|
+
const triggerCron = os.crons.trigger.handler(async ({ input, context }) => {
|
|
159
|
+
return serializeCron(await context.manager.triggerCronByTarget(input.target));
|
|
160
|
+
});
|
|
161
|
+
const startCron = os.crons.start.handler(async ({ input, context }) => {
|
|
162
|
+
return serializeCron(context.manager.startCronByTarget(input.target));
|
|
163
|
+
});
|
|
164
|
+
const stopCron = os.crons.stop.handler(async ({ input, context }) => {
|
|
165
|
+
return serializeCron(await context.manager.stopCronByTarget(input.target));
|
|
166
|
+
});
|
|
167
|
+
const getTask = os.tasks.get.handler(async ({ input, context }) => {
|
|
168
|
+
const task = context.manager.getTaskByTarget(input.target);
|
|
169
|
+
if (!task) throw new Error(`Task not found: ${input.target}`);
|
|
170
|
+
return task;
|
|
171
|
+
});
|
|
172
|
+
const listTasks = os.tasks.list.handler(async ({ context }) => {
|
|
173
|
+
const taskList = context.manager.getTaskList();
|
|
174
|
+
if (!taskList) return [];
|
|
175
|
+
return taskList.tasks.map((t) => ({
|
|
176
|
+
id: t.id,
|
|
177
|
+
state: t.state,
|
|
178
|
+
processNames: t.processes.map((p) => p.name)
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
const addTask = os.tasks.add.handler(async ({ input, context }) => {
|
|
182
|
+
return context.manager.addTask(input.name, input.definition);
|
|
183
|
+
});
|
|
184
|
+
const removeTask = os.tasks.remove.handler(async ({ input, context }) => {
|
|
185
|
+
return context.manager.removeTaskByTarget(input.target);
|
|
186
|
+
});
|
|
187
|
+
const router = os.router({
|
|
188
|
+
manager: { status: managerStatus },
|
|
189
|
+
processes: {
|
|
190
|
+
add: addProcess,
|
|
191
|
+
get: getProcess,
|
|
192
|
+
list: listProcesses,
|
|
193
|
+
start: startProcess,
|
|
194
|
+
stop: stopProcess,
|
|
195
|
+
restart: restartProcess,
|
|
196
|
+
reload: reloadProcess,
|
|
197
|
+
remove: removeProcess
|
|
198
|
+
},
|
|
199
|
+
crons: {
|
|
200
|
+
get: getCron,
|
|
201
|
+
list: listCrons,
|
|
202
|
+
trigger: triggerCron,
|
|
203
|
+
start: startCron,
|
|
204
|
+
stop: stopCron
|
|
205
|
+
},
|
|
206
|
+
tasks: {
|
|
207
|
+
get: getTask,
|
|
208
|
+
list: listTasks,
|
|
209
|
+
add: addTask,
|
|
210
|
+
remove: removeTask
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/manager.ts
|
|
216
|
+
const HttpServerConfigSchema = v.object({
|
|
217
|
+
host: v.optional(v.string()),
|
|
218
|
+
port: v.optional(v.number()),
|
|
219
|
+
authToken: v.optional(v.string())
|
|
220
|
+
});
|
|
221
|
+
const CronProcessEntrySchema = v.object({
|
|
222
|
+
name: v.string(),
|
|
223
|
+
definition: ProcessDefinitionSchema,
|
|
224
|
+
options: CronProcessOptionsSchema,
|
|
225
|
+
envFile: v.optional(v.string())
|
|
226
|
+
});
|
|
227
|
+
const EnvReloadDelaySchema = v.union([
|
|
228
|
+
v.number(),
|
|
229
|
+
v.boolean(),
|
|
230
|
+
v.literal("immediately")
|
|
231
|
+
]);
|
|
232
|
+
const RestartingProcessEntrySchema = v.object({
|
|
233
|
+
name: v.string(),
|
|
234
|
+
definition: ProcessDefinitionSchema,
|
|
235
|
+
options: v.optional(RestartingProcessOptionsSchema),
|
|
236
|
+
envFile: v.optional(v.string()),
|
|
237
|
+
envReloadDelay: v.optional(EnvReloadDelaySchema)
|
|
238
|
+
});
|
|
239
|
+
const TaskEntrySchema = v.object({
|
|
240
|
+
name: v.string(),
|
|
241
|
+
definition: ProcessDefinitionSchema,
|
|
242
|
+
envFile: v.optional(v.string())
|
|
243
|
+
});
|
|
244
|
+
const ManagerConfigSchema = v.object({
|
|
245
|
+
http: v.optional(HttpServerConfigSchema),
|
|
246
|
+
cwd: v.optional(v.string()),
|
|
247
|
+
logDir: v.optional(v.string()),
|
|
248
|
+
env: v.optional(v.record(v.string(), v.string())),
|
|
249
|
+
envFiles: v.optional(v.record(v.string(), v.string())),
|
|
250
|
+
tasks: v.optional(v.array(TaskEntrySchema)),
|
|
251
|
+
crons: v.optional(v.array(CronProcessEntrySchema)),
|
|
252
|
+
processes: v.optional(v.array(RestartingProcessEntrySchema))
|
|
253
|
+
});
|
|
254
|
+
const DEFAULT_RESTART_OPTIONS = { restartPolicy: "always" };
|
|
255
|
+
const SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
256
|
+
const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
257
|
+
const SHUTDOWN_EXIT_CODE = 0;
|
|
258
|
+
var Manager = class {
|
|
259
|
+
config;
|
|
260
|
+
logger;
|
|
261
|
+
envManager;
|
|
262
|
+
envFileKeyByProcess = /* @__PURE__ */ new Map();
|
|
263
|
+
_state = "idle";
|
|
264
|
+
taskList = null;
|
|
265
|
+
cronProcesses = /* @__PURE__ */ new Map();
|
|
266
|
+
restartingProcesses = /* @__PURE__ */ new Map();
|
|
267
|
+
logDir;
|
|
268
|
+
processEnvReloadConfig = /* @__PURE__ */ new Map();
|
|
269
|
+
envReloadTimers = /* @__PURE__ */ new Map();
|
|
270
|
+
envChangeUnsubscribe = null;
|
|
271
|
+
signalHandlers = /* @__PURE__ */ new Map();
|
|
272
|
+
shutdownPromise = null;
|
|
273
|
+
isShuttingDown = false;
|
|
274
|
+
constructor(config, logger) {
|
|
275
|
+
this.config = config;
|
|
276
|
+
this.logger = logger;
|
|
277
|
+
this.logDir = config.logDir ?? join(cwd(), "logs");
|
|
278
|
+
this.ensureLogDirs();
|
|
279
|
+
this.envManager = new EnvManager({
|
|
280
|
+
cwd: config.cwd,
|
|
281
|
+
files: config.envFiles,
|
|
282
|
+
watch: true
|
|
283
|
+
});
|
|
284
|
+
this.envChangeUnsubscribe = this.envManager.onChange((changedKeys) => {
|
|
285
|
+
this.handleEnvChange(changedKeys);
|
|
286
|
+
});
|
|
287
|
+
this.registerShutdownHandlers();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Merge global env with process-specific env and apply cwd inheritance
|
|
291
|
+
* Merge order: .env (global), config.env (global), .env.<processName>, processEnvFile, definition.env
|
|
292
|
+
*/
|
|
293
|
+
applyDefaults(processName, definition, envFile) {
|
|
294
|
+
const envFromFiles = this.envManager.getEnvVars(processName);
|
|
295
|
+
let envFromCustomFile = {};
|
|
296
|
+
if (envFile) {
|
|
297
|
+
let key = this.envFileKeyByProcess.get(processName);
|
|
298
|
+
if (!key) {
|
|
299
|
+
key = `custom:${processName}`;
|
|
300
|
+
this.envFileKeyByProcess.set(processName, key);
|
|
301
|
+
this.envManager.registerFile(key, envFile);
|
|
302
|
+
}
|
|
303
|
+
envFromCustomFile = this.envManager.getEnvForKey(key);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
...definition,
|
|
307
|
+
cwd: definition.cwd ?? this.config.cwd,
|
|
308
|
+
env: {
|
|
309
|
+
...envFromFiles,
|
|
310
|
+
...this.config.env,
|
|
311
|
+
...envFromCustomFile,
|
|
312
|
+
...definition.env
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
processLogFile(name) {
|
|
317
|
+
return join(this.logDir, "process", `${name}.log`);
|
|
318
|
+
}
|
|
319
|
+
taskLogFile(name) {
|
|
320
|
+
return join(this.logDir, "tasks", `${name}.log`);
|
|
321
|
+
}
|
|
322
|
+
cronLogFile(name) {
|
|
323
|
+
return join(this.logDir, "cron", `${name}.log`);
|
|
324
|
+
}
|
|
325
|
+
ensureLogDirs() {
|
|
326
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
327
|
+
mkdirSync(join(this.logDir, "process"), { recursive: true });
|
|
328
|
+
mkdirSync(join(this.logDir, "tasks"), { recursive: true });
|
|
329
|
+
mkdirSync(join(this.logDir, "cron"), { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Handle env file changes
|
|
333
|
+
*/
|
|
334
|
+
handleEnvChange(changedKeys) {
|
|
335
|
+
if (this._state !== "running") return;
|
|
336
|
+
this.logger.info(`Env files changed for keys: ${changedKeys.join(", ")}`);
|
|
337
|
+
const affectedProcesses = /* @__PURE__ */ new Set();
|
|
338
|
+
for (const key of changedKeys) if (key === "global") for (const processName of this.restartingProcesses.keys()) affectedProcesses.add(processName);
|
|
339
|
+
else {
|
|
340
|
+
if (this.restartingProcesses.has(key)) affectedProcesses.add(key);
|
|
341
|
+
for (const [processName, customKey] of this.envFileKeyByProcess.entries()) if (customKey === key && this.restartingProcesses.has(processName)) affectedProcesses.add(processName);
|
|
342
|
+
}
|
|
343
|
+
for (const processName of affectedProcesses) {
|
|
344
|
+
const reloadDelay = this.processEnvReloadConfig.get(processName);
|
|
345
|
+
if (reloadDelay === false) continue;
|
|
346
|
+
this.scheduleProcessReload(processName, reloadDelay);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Schedule a process reload with debouncing
|
|
351
|
+
*/
|
|
352
|
+
scheduleProcessReload(processName, reloadDelay) {
|
|
353
|
+
const existingTimer = this.envReloadTimers.get(processName);
|
|
354
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
355
|
+
let delayMs;
|
|
356
|
+
if (reloadDelay === false) return;
|
|
357
|
+
else if (reloadDelay === true || reloadDelay === "immediately") delayMs = 0;
|
|
358
|
+
else if (typeof reloadDelay === "number") delayMs = reloadDelay;
|
|
359
|
+
else delayMs = 5e3;
|
|
360
|
+
this.logger.info(`Scheduling reload for process "${processName}" in ${delayMs}ms`);
|
|
361
|
+
const timer = setTimeout(async () => {
|
|
362
|
+
await this.reloadProcessEnv(processName);
|
|
363
|
+
this.envReloadTimers.delete(processName);
|
|
364
|
+
}, delayMs);
|
|
365
|
+
this.envReloadTimers.set(processName, timer);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Reload a process with updated env vars
|
|
369
|
+
*/
|
|
370
|
+
async reloadProcessEnv(processName) {
|
|
371
|
+
const proc = this.restartingProcesses.get(processName);
|
|
372
|
+
if (!proc) {
|
|
373
|
+
this.logger.warn(`Process "${processName}" not found for env reload`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
this.logger.info(`Reloading process "${processName}" due to env change`);
|
|
377
|
+
const processConfig = this.config.processes?.find((p) => p.name === processName);
|
|
378
|
+
if (!processConfig) {
|
|
379
|
+
this.logger.warn(`Process config for "${processName}" not found`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const updatedDefinition = this.applyDefaults(processName, processConfig.definition, processConfig.envFile);
|
|
383
|
+
await proc.reload(updatedDefinition, true);
|
|
384
|
+
}
|
|
385
|
+
get state() {
|
|
386
|
+
return this._state;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get all cron processes (read-only access)
|
|
390
|
+
*/
|
|
391
|
+
getCronProcesses() {
|
|
392
|
+
return this.cronProcesses;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get a specific cron process by name
|
|
396
|
+
*/
|
|
397
|
+
getCronProcess(name) {
|
|
398
|
+
return this.cronProcesses.get(name);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get all restarting processes (read-only access)
|
|
402
|
+
*/
|
|
403
|
+
getRestartingProcesses() {
|
|
404
|
+
return this.restartingProcesses;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get a specific restarting process by name
|
|
408
|
+
*/
|
|
409
|
+
getRestartingProcess(name) {
|
|
410
|
+
return this.restartingProcesses.get(name);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the task list (read-only access)
|
|
414
|
+
*/
|
|
415
|
+
getTaskList() {
|
|
416
|
+
return this.taskList;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get a restarting process by name or index
|
|
420
|
+
*/
|
|
421
|
+
getProcessByTarget(target) {
|
|
422
|
+
if (typeof target === "string") return this.restartingProcesses.get(target);
|
|
423
|
+
return Array.from(this.restartingProcesses.values())[target];
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get a cron process by name or index
|
|
427
|
+
*/
|
|
428
|
+
getCronByTarget(target) {
|
|
429
|
+
if (typeof target === "string") return this.cronProcesses.get(target);
|
|
430
|
+
return Array.from(this.cronProcesses.values())[target];
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get a task by id or index
|
|
434
|
+
*/
|
|
435
|
+
getTaskByTarget(target) {
|
|
436
|
+
if (!this.taskList) return void 0;
|
|
437
|
+
const tasks = this.taskList.tasks;
|
|
438
|
+
if (typeof target === "string") {
|
|
439
|
+
const task = tasks.find((t) => t.id === target);
|
|
440
|
+
if (!task) return void 0;
|
|
441
|
+
return {
|
|
442
|
+
id: task.id,
|
|
443
|
+
state: task.state,
|
|
444
|
+
processNames: task.processes.map((p) => p.name)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const task = tasks[target];
|
|
448
|
+
if (!task) return void 0;
|
|
449
|
+
return {
|
|
450
|
+
id: task.id,
|
|
451
|
+
state: task.state,
|
|
452
|
+
processNames: task.processes.map((p) => p.name)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Start a restarting process by target
|
|
457
|
+
*/
|
|
458
|
+
startProcessByTarget(target) {
|
|
459
|
+
const proc = this.getProcessByTarget(target);
|
|
460
|
+
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
461
|
+
proc.start();
|
|
462
|
+
return proc;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Stop a restarting process by target
|
|
466
|
+
*/
|
|
467
|
+
async stopProcessByTarget(target, timeout) {
|
|
468
|
+
const proc = this.getProcessByTarget(target);
|
|
469
|
+
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
470
|
+
await proc.stop(timeout);
|
|
471
|
+
return proc;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Restart a restarting process by target
|
|
475
|
+
*/
|
|
476
|
+
async restartProcessByTarget(target, force = false) {
|
|
477
|
+
const proc = this.getProcessByTarget(target);
|
|
478
|
+
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
479
|
+
await proc.restart(force);
|
|
480
|
+
return proc;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Reload a restarting process with new definition
|
|
484
|
+
*/
|
|
485
|
+
async reloadProcessByTarget(target, newDefinition, options) {
|
|
486
|
+
const proc = this.getProcessByTarget(target);
|
|
487
|
+
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
488
|
+
const definitionWithDefaults = this.applyDefaults(proc.name, newDefinition);
|
|
489
|
+
if (options?.updateOptions) proc.updateOptions(options.updateOptions);
|
|
490
|
+
await proc.reload(definitionWithDefaults, options?.restartImmediately ?? true);
|
|
491
|
+
this.logger.info(`Reloaded process: ${proc.name}`);
|
|
492
|
+
return proc;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Remove a restarting process by target
|
|
496
|
+
*/
|
|
497
|
+
async removeProcessByTarget(target, timeout) {
|
|
498
|
+
const proc = this.getProcessByTarget(target);
|
|
499
|
+
if (!proc) throw new Error(`Process not found: ${target}`);
|
|
500
|
+
await proc.stop(timeout);
|
|
501
|
+
this.restartingProcesses.delete(proc.name);
|
|
502
|
+
this.logger.info(`Removed process: ${proc.name}`);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Add a task to the task list
|
|
506
|
+
* Creates the task list if it doesn't exist and starts it
|
|
507
|
+
*/
|
|
508
|
+
addTask(name, definition) {
|
|
509
|
+
if (!this.taskList) this.taskList = new TaskList("runtime", this.logger.child("tasks", { logFile: this.taskLogFile("tasks") }), void 0, (processName) => {
|
|
510
|
+
return this.taskLogFile(processName);
|
|
511
|
+
});
|
|
512
|
+
const namedProcess = {
|
|
513
|
+
name,
|
|
514
|
+
process: this.applyDefaults(name, definition)
|
|
515
|
+
};
|
|
516
|
+
const id = this.taskList.addTask(namedProcess);
|
|
517
|
+
if (this.taskList.state === "idle") this.taskList.start();
|
|
518
|
+
return {
|
|
519
|
+
id,
|
|
520
|
+
state: "pending",
|
|
521
|
+
processNames: [name]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
removeTaskByTarget(target) {
|
|
525
|
+
if (!this.taskList) throw new Error(`Task list not initialized`);
|
|
526
|
+
const removed = this.taskList.removeTaskByTarget(target);
|
|
527
|
+
return {
|
|
528
|
+
id: removed.id,
|
|
529
|
+
state: removed.state,
|
|
530
|
+
processNames: removed.processes.map((p) => p.name)
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Add a restarting process at runtime
|
|
535
|
+
*/
|
|
536
|
+
addProcess(name, definition, options, envReloadDelay) {
|
|
537
|
+
if (this.restartingProcesses.has(name)) throw new Error(`Process "${name}" already exists`);
|
|
538
|
+
const processLogger = this.logger.child(name, { logFile: this.processLogFile(name) });
|
|
539
|
+
const restartingProcess = new RestartingProcess(name, this.applyDefaults(name, definition), options ?? DEFAULT_RESTART_OPTIONS, processLogger);
|
|
540
|
+
this.restartingProcesses.set(name, restartingProcess);
|
|
541
|
+
restartingProcess.start();
|
|
542
|
+
this.processEnvReloadConfig.set(name, envReloadDelay ?? 5e3);
|
|
543
|
+
this.logger.info(`Added and started restarting process: ${name}`);
|
|
544
|
+
return restartingProcess;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Trigger a cron process by target
|
|
548
|
+
*/
|
|
549
|
+
async triggerCronByTarget(target) {
|
|
550
|
+
const cron = this.getCronByTarget(target);
|
|
551
|
+
if (!cron) throw new Error(`Cron not found: ${target}`);
|
|
552
|
+
await cron.trigger();
|
|
553
|
+
return cron;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Start a cron process by target
|
|
557
|
+
*/
|
|
558
|
+
startCronByTarget(target) {
|
|
559
|
+
const cron = this.getCronByTarget(target);
|
|
560
|
+
if (!cron) throw new Error(`Cron not found: ${target}`);
|
|
561
|
+
cron.start();
|
|
562
|
+
return cron;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Stop a cron process by target
|
|
566
|
+
*/
|
|
567
|
+
async stopCronByTarget(target, timeout) {
|
|
568
|
+
const cron = this.getCronByTarget(target);
|
|
569
|
+
if (!cron) throw new Error(`Cron not found: ${target}`);
|
|
570
|
+
await cron.stop(timeout);
|
|
571
|
+
return cron;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Start the manager:
|
|
575
|
+
* 1. Run task list (if configured) and wait for completion
|
|
576
|
+
* 2. Create and start all cron/restarting processes
|
|
577
|
+
*/
|
|
578
|
+
async start() {
|
|
579
|
+
if (this._state !== "idle" && this._state !== "stopped") throw new Error(`Manager is already ${this._state}`);
|
|
580
|
+
this.logger.info(`Starting manager`);
|
|
581
|
+
if (this.config.tasks && this.config.tasks.length > 0) {
|
|
582
|
+
this._state = "initializing";
|
|
583
|
+
this.logger.info(`Running initialization tasks`);
|
|
584
|
+
this.taskList = new TaskList("init", this.logger.child("tasks"), this.config.tasks.map((task) => ({
|
|
585
|
+
name: task.name,
|
|
586
|
+
process: this.applyDefaults(task.name, task.definition, task.envFile)
|
|
587
|
+
})), (processName) => {
|
|
588
|
+
return this.taskLogFile(processName);
|
|
589
|
+
});
|
|
590
|
+
this.taskList.start();
|
|
591
|
+
await this.taskList.waitUntilIdle();
|
|
592
|
+
const failedTasks = this.taskList.tasks.filter((t) => t.state === "failed");
|
|
593
|
+
if (failedTasks.length > 0) {
|
|
594
|
+
this._state = "stopped";
|
|
595
|
+
const failedNames = failedTasks.map((t) => t.id).join(", ");
|
|
596
|
+
throw new Error(`Initialization failed: tasks [${failedNames}] failed`);
|
|
597
|
+
}
|
|
598
|
+
this.logger.info(`Initialization tasks completed`);
|
|
599
|
+
}
|
|
600
|
+
if (this.config.crons) for (const entry of this.config.crons) {
|
|
601
|
+
const processLogger = this.logger.child(entry.name, { logFile: this.cronLogFile(entry.name) });
|
|
602
|
+
const cronProcess = new CronProcess(entry.name, this.applyDefaults(entry.name, entry.definition, entry.envFile), entry.options, processLogger);
|
|
603
|
+
this.cronProcesses.set(entry.name, cronProcess);
|
|
604
|
+
cronProcess.start();
|
|
605
|
+
this.logger.info(`Started cron process: ${entry.name}`);
|
|
606
|
+
}
|
|
607
|
+
if (this.config.processes) for (const entry of this.config.processes) {
|
|
608
|
+
const processLogger = this.logger.child(entry.name, { logFile: this.processLogFile(entry.name) });
|
|
609
|
+
const restartingProcess = new RestartingProcess(entry.name, this.applyDefaults(entry.name, entry.definition, entry.envFile), entry.options ?? DEFAULT_RESTART_OPTIONS, processLogger);
|
|
610
|
+
this.restartingProcesses.set(entry.name, restartingProcess);
|
|
611
|
+
restartingProcess.start();
|
|
612
|
+
this.processEnvReloadConfig.set(entry.name, entry.envReloadDelay ?? 5e3);
|
|
613
|
+
this.logger.info(`Started restarting process: ${entry.name}`);
|
|
614
|
+
}
|
|
615
|
+
this._state = "running";
|
|
616
|
+
this.logger.info(`Manager started with ${this.cronProcesses.size} cron process(es) and ${this.restartingProcesses.size} restarting process(es)`);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Stop all managed processes
|
|
620
|
+
*/
|
|
621
|
+
async stop(timeout) {
|
|
622
|
+
if (this._state === "idle" || this._state === "stopped") {
|
|
623
|
+
this._state = "stopped";
|
|
624
|
+
this.unregisterShutdownHandlers();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this._state = "stopping";
|
|
628
|
+
this.logger.info(`Stopping manager`);
|
|
629
|
+
for (const timer of this.envReloadTimers.values()) clearTimeout(timer);
|
|
630
|
+
this.envReloadTimers.clear();
|
|
631
|
+
if (this.envChangeUnsubscribe) {
|
|
632
|
+
this.envChangeUnsubscribe();
|
|
633
|
+
this.envChangeUnsubscribe = null;
|
|
634
|
+
}
|
|
635
|
+
this.envManager.dispose();
|
|
636
|
+
if (this.taskList) await this.taskList.stop(timeout);
|
|
637
|
+
const cronStopPromises = Array.from(this.cronProcesses.values()).map((p) => p.stop(timeout));
|
|
638
|
+
const restartingStopPromises = Array.from(this.restartingProcesses.values()).map((p) => p.stop(timeout));
|
|
639
|
+
await Promise.all([...cronStopPromises, ...restartingStopPromises]);
|
|
640
|
+
this._state = "stopped";
|
|
641
|
+
this.logger.info(`Manager stopped`);
|
|
642
|
+
this.unregisterShutdownHandlers();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Register signal handlers for graceful shutdown
|
|
646
|
+
* Called automatically by constructor
|
|
647
|
+
*/
|
|
648
|
+
registerShutdownHandlers() {
|
|
649
|
+
if (this.signalHandlers.size > 0) {
|
|
650
|
+
this.logger.warn(`Shutdown handlers already registered`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
654
|
+
const handler = () => this.handleSignal(signal);
|
|
655
|
+
this.signalHandlers.set(signal, handler);
|
|
656
|
+
process.on(signal, handler);
|
|
657
|
+
this.logger.debug(`Registered handler for ${signal}`);
|
|
658
|
+
}
|
|
659
|
+
this.logger.info(`Shutdown handlers registered for signals: ${SHUTDOWN_SIGNALS.join(", ")}`);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Unregister signal handlers for graceful shutdown
|
|
663
|
+
*/
|
|
664
|
+
unregisterShutdownHandlers() {
|
|
665
|
+
if (this.signalHandlers.size === 0) return;
|
|
666
|
+
for (const [signal, handler] of this.signalHandlers.entries()) {
|
|
667
|
+
process.off(signal, handler);
|
|
668
|
+
this.logger.debug(`Unregistered handler for ${signal}`);
|
|
669
|
+
}
|
|
670
|
+
this.signalHandlers.clear();
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Wait for shutdown to complete (useful for keeping process alive)
|
|
674
|
+
* Resolves when the manager has fully stopped
|
|
675
|
+
*/
|
|
676
|
+
async waitForShutdown() {
|
|
677
|
+
if (this._state === "stopped") return;
|
|
678
|
+
if (this.shutdownPromise) {
|
|
679
|
+
await this.shutdownPromise;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
return new Promise((resolve) => {
|
|
683
|
+
const checkInterval = setInterval(() => {
|
|
684
|
+
if (this._state === "stopped") {
|
|
685
|
+
clearInterval(checkInterval);
|
|
686
|
+
resolve();
|
|
687
|
+
}
|
|
688
|
+
}, 100);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Trigger graceful shutdown programmatically
|
|
693
|
+
*/
|
|
694
|
+
async shutdown() {
|
|
695
|
+
if (this.isShuttingDown) {
|
|
696
|
+
this.logger.warn(`Shutdown already in progress`);
|
|
697
|
+
if (this.shutdownPromise) await this.shutdownPromise;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
this.isShuttingDown = true;
|
|
701
|
+
this.logger.info(`Initiating graceful shutdown (timeout: ${SHUTDOWN_TIMEOUT_MS}ms)`);
|
|
702
|
+
this.shutdownPromise = this.performShutdown();
|
|
703
|
+
await this.shutdownPromise;
|
|
704
|
+
}
|
|
705
|
+
handleSignal(signal) {
|
|
706
|
+
this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
|
|
707
|
+
if (this.isShuttingDown) {
|
|
708
|
+
this.logger.warn(`Shutdown already in progress, ignoring ${signal}`);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.isShuttingDown = true;
|
|
712
|
+
this.shutdownPromise = this.performShutdown();
|
|
713
|
+
this.shutdownPromise.then(() => {
|
|
714
|
+
this.logger.info(`Exiting with code ${SHUTDOWN_EXIT_CODE}`);
|
|
715
|
+
process.exit(SHUTDOWN_EXIT_CODE);
|
|
716
|
+
}).catch((err) => {
|
|
717
|
+
this.logger.error(`Shutdown error:`, err);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async performShutdown() {
|
|
722
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
reject(/* @__PURE__ */ new Error(`Shutdown timed out after ${SHUTDOWN_TIMEOUT_MS}ms`));
|
|
725
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
726
|
+
});
|
|
727
|
+
try {
|
|
728
|
+
await Promise.race([this.stop(SHUTDOWN_TIMEOUT_MS), timeoutPromise]);
|
|
729
|
+
this.logger.info(`Graceful shutdown completed`);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
this.logger.error(`Shutdown error:`, err);
|
|
732
|
+
this._state = "stopped";
|
|
733
|
+
throw err;
|
|
734
|
+
} finally {
|
|
735
|
+
this.isShuttingDown = false;
|
|
736
|
+
this.shutdownPromise = null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/cli.ts
|
|
743
|
+
const program = new Command();
|
|
744
|
+
program.name("pidnap").description("Process manager with init system capabilities").version("0.0.0-dev.1");
|
|
745
|
+
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) => {
|
|
746
|
+
const initLogger = logger({ name: "pidnap" });
|
|
747
|
+
try {
|
|
748
|
+
const configUrl = pathToFileURL(resolve(process.cwd(), options.config)).href;
|
|
749
|
+
const configModule = await tsImport(configUrl, { parentURL: import.meta.url });
|
|
750
|
+
const rawConfig = configModule.default.default || configModule.default || configModule.config || {};
|
|
751
|
+
const config = v.parse(ManagerConfigSchema, rawConfig);
|
|
752
|
+
const host = config.http?.host ?? "localhost";
|
|
753
|
+
const port = config.http?.port ?? 3e3;
|
|
754
|
+
const authToken = config.http?.authToken;
|
|
755
|
+
const managerLogger = logger({
|
|
756
|
+
name: "pidnap",
|
|
757
|
+
logFile: resolve(config.logDir ?? resolve(process.cwd(), "logs"), "pidnap.log")
|
|
758
|
+
});
|
|
759
|
+
const manager = new Manager(config, managerLogger);
|
|
760
|
+
const handler = new RPCHandler(router, { interceptors: [onError((error) => {
|
|
761
|
+
managerLogger.error(error);
|
|
762
|
+
})] });
|
|
763
|
+
const server = createServer(async (req, res) => {
|
|
764
|
+
if (authToken) {
|
|
765
|
+
if (req.headers["authorization"]?.replace("Bearer ", "") !== authToken) {
|
|
766
|
+
res.statusCode = 401;
|
|
767
|
+
res.end("Unauthorized");
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const { matched } = await handler.handle(req, res, {
|
|
772
|
+
prefix: "/rpc",
|
|
773
|
+
context: { manager }
|
|
774
|
+
});
|
|
775
|
+
if (matched) return;
|
|
776
|
+
res.statusCode = 404;
|
|
777
|
+
res.end("Not found");
|
|
778
|
+
});
|
|
779
|
+
server.listen(port, host, async () => {
|
|
780
|
+
managerLogger.info(`pidnap RPC server running on http://${host}:${port}`);
|
|
781
|
+
if (authToken) managerLogger.info("Auth token required for API access");
|
|
782
|
+
try {
|
|
783
|
+
await manager.start();
|
|
784
|
+
} catch (err) {
|
|
785
|
+
managerLogger.error("Failed to start manager:", err);
|
|
786
|
+
server.close();
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
await manager.waitForShutdown();
|
|
791
|
+
server.close();
|
|
792
|
+
} catch (error) {
|
|
793
|
+
initLogger.error("Failed to start pidnap:", error);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
program.command("status").description("Show manager status").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
|
|
798
|
+
try {
|
|
799
|
+
const status = await createClient(options.url).manager.status();
|
|
800
|
+
const table = new Table({ head: [
|
|
801
|
+
"State",
|
|
802
|
+
"Processes",
|
|
803
|
+
"Crons",
|
|
804
|
+
"Tasks"
|
|
805
|
+
] });
|
|
806
|
+
table.push([
|
|
807
|
+
status.state,
|
|
808
|
+
status.processCount,
|
|
809
|
+
status.cronCount,
|
|
810
|
+
status.taskCount
|
|
811
|
+
]);
|
|
812
|
+
console.log(table.toString());
|
|
813
|
+
} catch (error) {
|
|
814
|
+
console.error("Failed to fetch status:", error);
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
const processes = program.command("processes").description("Manage restarting processes");
|
|
819
|
+
processes.command("list").description("List restarting processes").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
|
|
820
|
+
try {
|
|
821
|
+
const processes = await createClient(options.url).processes.list();
|
|
822
|
+
const table = new Table({ head: [
|
|
823
|
+
"Name",
|
|
824
|
+
"State",
|
|
825
|
+
"Restarts"
|
|
826
|
+
] });
|
|
827
|
+
for (const proc of processes) table.push([
|
|
828
|
+
proc.name,
|
|
829
|
+
proc.state,
|
|
830
|
+
proc.restarts
|
|
831
|
+
]);
|
|
832
|
+
console.log(table.toString());
|
|
833
|
+
} catch (error) {
|
|
834
|
+
console.error("Failed to list processes:", error);
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
processes.command("get").description("Get a restarting process by name or index").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
839
|
+
try {
|
|
840
|
+
const proc = await createClient(options.url).processes.get({ target: parseTarget(target) });
|
|
841
|
+
const table = new Table({ head: [
|
|
842
|
+
"Name",
|
|
843
|
+
"State",
|
|
844
|
+
"Restarts"
|
|
845
|
+
] });
|
|
846
|
+
table.push([
|
|
847
|
+
proc.name,
|
|
848
|
+
proc.state,
|
|
849
|
+
proc.restarts
|
|
850
|
+
]);
|
|
851
|
+
console.log(table.toString());
|
|
852
|
+
} catch (error) {
|
|
853
|
+
console.error("Failed to get process:", error);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
processes.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", "http://localhost:3000/rpc").action(async (options) => {
|
|
858
|
+
try {
|
|
859
|
+
const client = createClient(options.url);
|
|
860
|
+
const definition = parseDefinition(options.definition);
|
|
861
|
+
const proc = await client.processes.add({
|
|
862
|
+
name: options.name,
|
|
863
|
+
definition
|
|
864
|
+
});
|
|
865
|
+
const table = new Table({ head: [
|
|
866
|
+
"Name",
|
|
867
|
+
"State",
|
|
868
|
+
"Restarts"
|
|
869
|
+
] });
|
|
870
|
+
table.push([
|
|
871
|
+
proc.name,
|
|
872
|
+
proc.state,
|
|
873
|
+
proc.restarts
|
|
874
|
+
]);
|
|
875
|
+
console.log(table.toString());
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.error("Failed to add process:", error);
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
processes.command("start").description("Start a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
882
|
+
try {
|
|
883
|
+
const proc = await createClient(options.url).processes.start({ target: parseTarget(target) });
|
|
884
|
+
const table = new Table({ head: [
|
|
885
|
+
"Name",
|
|
886
|
+
"State",
|
|
887
|
+
"Restarts"
|
|
888
|
+
] });
|
|
889
|
+
table.push([
|
|
890
|
+
proc.name,
|
|
891
|
+
proc.state,
|
|
892
|
+
proc.restarts
|
|
893
|
+
]);
|
|
894
|
+
console.log(table.toString());
|
|
895
|
+
} catch (error) {
|
|
896
|
+
console.error("Failed to start process:", error);
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
processes.command("stop").description("Stop a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
901
|
+
try {
|
|
902
|
+
const proc = await createClient(options.url).processes.stop({ target: parseTarget(target) });
|
|
903
|
+
const table = new Table({ head: [
|
|
904
|
+
"Name",
|
|
905
|
+
"State",
|
|
906
|
+
"Restarts"
|
|
907
|
+
] });
|
|
908
|
+
table.push([
|
|
909
|
+
proc.name,
|
|
910
|
+
proc.state,
|
|
911
|
+
proc.restarts
|
|
912
|
+
]);
|
|
913
|
+
console.log(table.toString());
|
|
914
|
+
} catch (error) {
|
|
915
|
+
console.error("Failed to stop process:", error);
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
processes.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", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
920
|
+
try {
|
|
921
|
+
const proc = await createClient(options.url).processes.restart({
|
|
922
|
+
target: parseTarget(target),
|
|
923
|
+
force: options.force
|
|
924
|
+
});
|
|
925
|
+
const table = new Table({ head: [
|
|
926
|
+
"Name",
|
|
927
|
+
"State",
|
|
928
|
+
"Restarts"
|
|
929
|
+
] });
|
|
930
|
+
table.push([
|
|
931
|
+
proc.name,
|
|
932
|
+
proc.state,
|
|
933
|
+
proc.restarts
|
|
934
|
+
]);
|
|
935
|
+
console.log(table.toString());
|
|
936
|
+
} catch (error) {
|
|
937
|
+
console.error("Failed to restart process:", error);
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
processes.command("remove").description("Remove a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
942
|
+
try {
|
|
943
|
+
await createClient(options.url).processes.remove({ target: parseTarget(target) });
|
|
944
|
+
console.log("Process removed");
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.error("Failed to remove process:", error);
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
const crons = program.command("crons").description("Manage cron processes");
|
|
951
|
+
crons.command("list").description("List cron processes").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
|
|
952
|
+
try {
|
|
953
|
+
const crons = await createClient(options.url).crons.list();
|
|
954
|
+
const table = new Table({ head: [
|
|
955
|
+
"Name",
|
|
956
|
+
"State",
|
|
957
|
+
"Runs",
|
|
958
|
+
"Fails",
|
|
959
|
+
"Next Run"
|
|
960
|
+
] });
|
|
961
|
+
for (const cron of crons) table.push([
|
|
962
|
+
cron.name,
|
|
963
|
+
cron.state,
|
|
964
|
+
cron.runCount,
|
|
965
|
+
cron.failCount,
|
|
966
|
+
cron.nextRun ?? "-"
|
|
967
|
+
]);
|
|
968
|
+
console.log(table.toString());
|
|
969
|
+
} catch (error) {
|
|
970
|
+
console.error("Failed to list crons:", error);
|
|
971
|
+
process.exit(1);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
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", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
975
|
+
try {
|
|
976
|
+
const cron = await createClient(options.url).crons.get({ target: parseTarget(target) });
|
|
977
|
+
const table = new Table({ head: [
|
|
978
|
+
"Name",
|
|
979
|
+
"State",
|
|
980
|
+
"Runs",
|
|
981
|
+
"Fails",
|
|
982
|
+
"Next Run"
|
|
983
|
+
] });
|
|
984
|
+
table.push([
|
|
985
|
+
cron.name,
|
|
986
|
+
cron.state,
|
|
987
|
+
cron.runCount,
|
|
988
|
+
cron.failCount,
|
|
989
|
+
cron.nextRun ?? "-"
|
|
990
|
+
]);
|
|
991
|
+
console.log(table.toString());
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.error("Failed to get cron:", error);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
crons.command("trigger").description("Trigger a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
998
|
+
try {
|
|
999
|
+
const cron = await createClient(options.url).crons.trigger({ target: parseTarget(target) });
|
|
1000
|
+
const table = new Table({ head: [
|
|
1001
|
+
"Name",
|
|
1002
|
+
"State",
|
|
1003
|
+
"Runs",
|
|
1004
|
+
"Fails",
|
|
1005
|
+
"Next Run"
|
|
1006
|
+
] });
|
|
1007
|
+
table.push([
|
|
1008
|
+
cron.name,
|
|
1009
|
+
cron.state,
|
|
1010
|
+
cron.runCount,
|
|
1011
|
+
cron.failCount,
|
|
1012
|
+
cron.nextRun ?? "-"
|
|
1013
|
+
]);
|
|
1014
|
+
console.log(table.toString());
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
console.error("Failed to trigger cron:", error);
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
crons.command("start").description("Start a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
1021
|
+
try {
|
|
1022
|
+
const cron = await createClient(options.url).crons.start({ target: parseTarget(target) });
|
|
1023
|
+
const table = new Table({ head: [
|
|
1024
|
+
"Name",
|
|
1025
|
+
"State",
|
|
1026
|
+
"Runs",
|
|
1027
|
+
"Fails",
|
|
1028
|
+
"Next Run"
|
|
1029
|
+
] });
|
|
1030
|
+
table.push([
|
|
1031
|
+
cron.name,
|
|
1032
|
+
cron.state,
|
|
1033
|
+
cron.runCount,
|
|
1034
|
+
cron.failCount,
|
|
1035
|
+
cron.nextRun ?? "-"
|
|
1036
|
+
]);
|
|
1037
|
+
console.log(table.toString());
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
console.error("Failed to start cron:", error);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
crons.command("stop").description("Stop a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const cron = await createClient(options.url).crons.stop({ target: parseTarget(target) });
|
|
1046
|
+
const table = new Table({ head: [
|
|
1047
|
+
"Name",
|
|
1048
|
+
"State",
|
|
1049
|
+
"Runs",
|
|
1050
|
+
"Fails",
|
|
1051
|
+
"Next Run"
|
|
1052
|
+
] });
|
|
1053
|
+
table.push([
|
|
1054
|
+
cron.name,
|
|
1055
|
+
cron.state,
|
|
1056
|
+
cron.runCount,
|
|
1057
|
+
cron.failCount,
|
|
1058
|
+
cron.nextRun ?? "-"
|
|
1059
|
+
]);
|
|
1060
|
+
console.log(table.toString());
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
console.error("Failed to stop cron:", error);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
const tasks = program.command("tasks").description("Manage tasks");
|
|
1067
|
+
tasks.command("list").description("List tasks").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
|
|
1068
|
+
try {
|
|
1069
|
+
const tasks = await createClient(options.url).tasks.list();
|
|
1070
|
+
const table = new Table({ head: [
|
|
1071
|
+
"Id",
|
|
1072
|
+
"State",
|
|
1073
|
+
"Processes"
|
|
1074
|
+
] });
|
|
1075
|
+
for (const task of tasks) table.push([
|
|
1076
|
+
task.id,
|
|
1077
|
+
task.state,
|
|
1078
|
+
task.processNames.join(", ")
|
|
1079
|
+
]);
|
|
1080
|
+
console.log(table.toString());
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
console.error("Failed to list tasks:", error);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
tasks.command("get").description("Get a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const task = await createClient(options.url).tasks.get({ target: parseTarget(target) });
|
|
1089
|
+
const table = new Table({ head: [
|
|
1090
|
+
"Id",
|
|
1091
|
+
"State",
|
|
1092
|
+
"Processes"
|
|
1093
|
+
] });
|
|
1094
|
+
table.push([
|
|
1095
|
+
task.id,
|
|
1096
|
+
task.state,
|
|
1097
|
+
task.processNames.join(", ")
|
|
1098
|
+
]);
|
|
1099
|
+
console.log(table.toString());
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
console.error("Failed to get task:", error);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
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", "http://localhost:3000/rpc").action(async (options) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const client = createClient(options.url);
|
|
1108
|
+
const definition = parseDefinition(options.definition);
|
|
1109
|
+
const task = await client.tasks.add({
|
|
1110
|
+
name: options.name,
|
|
1111
|
+
definition
|
|
1112
|
+
});
|
|
1113
|
+
const table = new Table({ head: [
|
|
1114
|
+
"Id",
|
|
1115
|
+
"State",
|
|
1116
|
+
"Processes"
|
|
1117
|
+
] });
|
|
1118
|
+
table.push([
|
|
1119
|
+
task.id,
|
|
1120
|
+
task.state,
|
|
1121
|
+
task.processNames.join(", ")
|
|
1122
|
+
]);
|
|
1123
|
+
console.log(table.toString());
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
console.error("Failed to add task:", error);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
tasks.command("remove").description("Remove a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const task = await createClient(options.url).tasks.remove({ target: parseTarget(target) });
|
|
1132
|
+
const table = new Table({ head: [
|
|
1133
|
+
"Id",
|
|
1134
|
+
"State",
|
|
1135
|
+
"Processes"
|
|
1136
|
+
] });
|
|
1137
|
+
table.push([
|
|
1138
|
+
task.id,
|
|
1139
|
+
task.state,
|
|
1140
|
+
task.processNames.join(", ")
|
|
1141
|
+
]);
|
|
1142
|
+
console.log(table.toString());
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
console.error("Failed to remove task:", error);
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
const TargetSchema = v.union([v.string(), v.number()]);
|
|
1149
|
+
function parseTarget(value) {
|
|
1150
|
+
const asNumber = Number(value);
|
|
1151
|
+
return v.parse(TargetSchema, Number.isNaN(asNumber) ? value : asNumber);
|
|
1152
|
+
}
|
|
1153
|
+
function parseDefinition(raw) {
|
|
1154
|
+
try {
|
|
1155
|
+
const parsed = JSON.parse(raw);
|
|
1156
|
+
return v.parse(ProcessDefinitionSchema, parsed);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.error("Invalid --definition JSON. Expected a ProcessDefinition.");
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
program.parse();
|
|
1163
|
+
|
|
1164
|
+
//#endregion
|
|
1165
|
+
export { };
|
|
1166
|
+
//# sourceMappingURL=cli.mjs.map
|