pid1 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/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +119 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-Bdt88RU-.d.mts +217 -0
- package/dist/client-Bdt88RU-.d.mts.map +1 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +12 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-BgRb4pSG.mjs +186 -0
- package/dist/config-BgRb4pSG.mjs.map +1 -0
- package/dist/config.d.mts +84 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +3 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/process-manager-gO56266Q.mjs +643 -0
- package/dist/process-manager-gO56266Q.mjs.map +1 -0
- package/package.json +52 -0
- package/src/api/client.ts +20 -0
- package/src/api/router.ts +150 -0
- package/src/api/server.ts +66 -0
- package/src/cli.ts +168 -0
- package/src/config.ts +89 -0
- package/src/env.ts +74 -0
- package/src/exec.ts +85 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +155 -0
- package/src/process-manager.ts +632 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { a as processLogger, i as globalLogger, r as loadConfigFile } from "./config-BgRb4pSG.mjs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
4
|
+
import * as v from "valibot";
|
|
5
|
+
import { parse } from "dotenv";
|
|
6
|
+
import { x } from "tinyexec";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { serve } from "@hono/node-server";
|
|
9
|
+
import { RPCHandler } from "@orpc/server/fetch";
|
|
10
|
+
import { ORPCError, onError, os } from "@orpc/server";
|
|
11
|
+
import { logger } from "hono/logger";
|
|
12
|
+
|
|
13
|
+
//#region src/env.ts
|
|
14
|
+
const logger$1 = globalLogger.child("env");
|
|
15
|
+
function loadEnvFile(path) {
|
|
16
|
+
if (!existsSync(path)) return {};
|
|
17
|
+
try {
|
|
18
|
+
const parsed = parse(readFileSync(path, "utf-8"));
|
|
19
|
+
logger$1.debug(`Loaded env file: ${path}`);
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
logger$1.warn(`Failed to parse env file ${path}: ${error}`);
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function loadEnvForProcess(options) {
|
|
27
|
+
const { configDir, processName, globalEnvFile, processEnvFile, configEnv = {} } = options;
|
|
28
|
+
const baseEnv = Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== void 0));
|
|
29
|
+
let globalEnv = {};
|
|
30
|
+
if (globalEnvFile !== false) globalEnv = loadEnvFile(globalEnvFile ?? join(configDir, ".env"));
|
|
31
|
+
let processEnv = {};
|
|
32
|
+
if (processEnvFile !== false) processEnv = loadEnvFile(processEnvFile ?? join(configDir, `.env.${processName}`));
|
|
33
|
+
return {
|
|
34
|
+
...baseEnv,
|
|
35
|
+
...globalEnv,
|
|
36
|
+
...configEnv,
|
|
37
|
+
...processEnv
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/exec.ts
|
|
43
|
+
function spawnWithLogging({ name, command, args, env, cwd, logFile, abortSignal, type }) {
|
|
44
|
+
const proc = x(command, args, {
|
|
45
|
+
nodeOptions: {
|
|
46
|
+
cwd,
|
|
47
|
+
env
|
|
48
|
+
},
|
|
49
|
+
signal: abortSignal,
|
|
50
|
+
throwOnError: true
|
|
51
|
+
});
|
|
52
|
+
const kill = (signal = "SIGTERM") => {
|
|
53
|
+
if (proc.pid) try {
|
|
54
|
+
process.kill(proc.pid, signal);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
proc,
|
|
63
|
+
done: (async () => {
|
|
64
|
+
const logger = processLogger({
|
|
65
|
+
name: `${type}:${name}`,
|
|
66
|
+
logFile
|
|
67
|
+
});
|
|
68
|
+
logger.info(`Starting: command=${command}, args=[${args.join(", ")}], cwd=${cwd}`);
|
|
69
|
+
for await (const line of proc) logger.info(line);
|
|
70
|
+
await proc;
|
|
71
|
+
const exitCode = proc.exitCode;
|
|
72
|
+
if (proc.aborted || proc.killed || exitCode === null || exitCode === void 0) logger.info(`${name} was terminated`);
|
|
73
|
+
else if (exitCode === 0) logger.info(`${name} completed successfully`);
|
|
74
|
+
else logger.info(`${name} exited with code ${exitCode}`);
|
|
75
|
+
return proc.exitCode ?? null;
|
|
76
|
+
})(),
|
|
77
|
+
kill
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/api/router.ts
|
|
83
|
+
const ProcessInfoSchema = v.object({
|
|
84
|
+
id: v.number(),
|
|
85
|
+
name: v.string(),
|
|
86
|
+
status: v.picklist([
|
|
87
|
+
"stopped",
|
|
88
|
+
"running",
|
|
89
|
+
"errored"
|
|
90
|
+
]),
|
|
91
|
+
pid: v.optional(v.number()),
|
|
92
|
+
exitCode: v.nullable(v.number()),
|
|
93
|
+
restarts: v.number(),
|
|
94
|
+
startedAt: v.nullable(v.date()),
|
|
95
|
+
command: v.string(),
|
|
96
|
+
args: v.array(v.string())
|
|
97
|
+
});
|
|
98
|
+
const ProcessNameSchema = v.object({ name: v.string() });
|
|
99
|
+
const base = os.$context();
|
|
100
|
+
const list = base.output(v.array(ProcessInfoSchema)).handler(async ({ context }) => {
|
|
101
|
+
return context.pm.list();
|
|
102
|
+
});
|
|
103
|
+
const get = base.input(ProcessNameSchema).output(v.nullable(ProcessInfoSchema)).handler(async ({ input, context }) => {
|
|
104
|
+
return context.pm.get(input.name) ?? null;
|
|
105
|
+
});
|
|
106
|
+
const start = base.input(v.object({ name: v.optional(v.string()) })).output(v.object({
|
|
107
|
+
success: v.boolean(),
|
|
108
|
+
message: v.string()
|
|
109
|
+
})).handler(async ({ input, context }) => {
|
|
110
|
+
try {
|
|
111
|
+
if (input.name) {
|
|
112
|
+
context.pm.start(input.name);
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
message: `Started process: ${input.name}`
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
context.pm.startAll();
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
message: "Started all processes"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new ORPCError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to start" });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const stop = base.input(v.object({ name: v.optional(v.string()) })).output(v.object({
|
|
129
|
+
success: v.boolean(),
|
|
130
|
+
message: v.string()
|
|
131
|
+
})).handler(async ({ input, context }) => {
|
|
132
|
+
try {
|
|
133
|
+
if (input.name) {
|
|
134
|
+
await context.pm.stop(input.name);
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
message: `Stopped process: ${input.name}`
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
await context.pm.stopAll();
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
message: "Stopped all processes"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new ORPCError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to stop" });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const restart = base.input(v.object({ name: v.optional(v.string()) })).output(v.object({
|
|
151
|
+
success: v.boolean(),
|
|
152
|
+
message: v.string()
|
|
153
|
+
})).handler(async ({ input, context }) => {
|
|
154
|
+
try {
|
|
155
|
+
if (input.name) {
|
|
156
|
+
await context.pm.restart(input.name);
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
message: `Restarted process: ${input.name}`
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
await context.pm.restartAll();
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
message: "Restarted all processes"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new ORPCError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to restart" });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const deleteProcess = base.input(v.object({ name: v.optional(v.string()) })).output(v.object({
|
|
173
|
+
success: v.boolean(),
|
|
174
|
+
message: v.string()
|
|
175
|
+
})).handler(async ({ input, context }) => {
|
|
176
|
+
try {
|
|
177
|
+
if (input.name) {
|
|
178
|
+
await context.pm.delete(input.name);
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
message: `Deleted process: ${input.name}`
|
|
182
|
+
};
|
|
183
|
+
} else {
|
|
184
|
+
await context.pm.deleteAll();
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
message: "Deleted all processes"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new ORPCError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to delete" });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const health = os.output(v.object({ status: v.literal("ok") })).handler(async () => {
|
|
195
|
+
return { status: "ok" };
|
|
196
|
+
});
|
|
197
|
+
const router = {
|
|
198
|
+
health,
|
|
199
|
+
process: {
|
|
200
|
+
list,
|
|
201
|
+
get,
|
|
202
|
+
start,
|
|
203
|
+
stop,
|
|
204
|
+
restart,
|
|
205
|
+
delete: deleteProcess
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/api/server.ts
|
|
211
|
+
function createServer(pm, options = {}) {
|
|
212
|
+
const { port = 3e3, host = "127.0.0.1" } = options;
|
|
213
|
+
const logger$2 = globalLogger.child("server");
|
|
214
|
+
const app = new Hono();
|
|
215
|
+
const handler = new RPCHandler(router, { interceptors: [onError((error) => {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
logger$2.error(`RPC Error: ${message}`);
|
|
218
|
+
})] });
|
|
219
|
+
app.use(logger((msg, ...args) => logger$2.debug(`${msg} ${args.join(" ")}`)));
|
|
220
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
221
|
+
app.use("/rpc/*", async (c, next) => {
|
|
222
|
+
const { matched, response } = await handler.handle(c.req.raw, {
|
|
223
|
+
prefix: "/rpc",
|
|
224
|
+
context: { pm }
|
|
225
|
+
});
|
|
226
|
+
if (matched) return c.newResponse(response.body, response);
|
|
227
|
+
await next();
|
|
228
|
+
});
|
|
229
|
+
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
230
|
+
return {
|
|
231
|
+
app,
|
|
232
|
+
start: () => {
|
|
233
|
+
const server = serve({
|
|
234
|
+
fetch: app.fetch,
|
|
235
|
+
port,
|
|
236
|
+
hostname: host
|
|
237
|
+
});
|
|
238
|
+
logger$2.info(`HTTP server listening on http://${host}:${port}`);
|
|
239
|
+
logger$2.info(`RPC endpoint: http://${host}:${port}/rpc`);
|
|
240
|
+
return server;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/process-manager.ts
|
|
247
|
+
const DEFAULT_STOP_TIMEOUT_MS = 1e4;
|
|
248
|
+
const DEFAULT_RESTART_RESET_SECONDS = 300;
|
|
249
|
+
const DEFAULT_RESTART_MAX = 10;
|
|
250
|
+
const DEFAULT_RESTART_MIN_DELAY_MS = 5e3;
|
|
251
|
+
const DEFAULT_RESTART_MAX_DELAY_MS = 6e4;
|
|
252
|
+
var ProcessManager = class {
|
|
253
|
+
logger = globalLogger.child("pm");
|
|
254
|
+
cwd;
|
|
255
|
+
configPath;
|
|
256
|
+
configDir;
|
|
257
|
+
globalEnvFile;
|
|
258
|
+
processes;
|
|
259
|
+
processIds;
|
|
260
|
+
nextProcessId = 0;
|
|
261
|
+
signalHandlersSetup = false;
|
|
262
|
+
serverOptions;
|
|
263
|
+
httpServer;
|
|
264
|
+
envWatcher = null;
|
|
265
|
+
envChangeTimers = /* @__PURE__ */ new Map();
|
|
266
|
+
envRestartTimers = /* @__PURE__ */ new Map();
|
|
267
|
+
restartOnEnvChangeGlobal = true;
|
|
268
|
+
restartMinDelayMsGlobal = DEFAULT_RESTART_MIN_DELAY_MS;
|
|
269
|
+
restartMaxDelayMsGlobal = DEFAULT_RESTART_MAX_DELAY_MS;
|
|
270
|
+
currentTask = null;
|
|
271
|
+
currentTaskName = null;
|
|
272
|
+
taskAbortController = null;
|
|
273
|
+
constructor(options = {}) {
|
|
274
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
275
|
+
this.configPath = options.configPath;
|
|
276
|
+
this.processes = /* @__PURE__ */ new Map();
|
|
277
|
+
this.processIds = /* @__PURE__ */ new Map();
|
|
278
|
+
this.serverOptions = options.server;
|
|
279
|
+
}
|
|
280
|
+
async init() {
|
|
281
|
+
const config = await loadConfigFile(this.configPath, this.cwd);
|
|
282
|
+
this.configDir = dirname(config.configPath);
|
|
283
|
+
this.globalEnvFile = config.envFile;
|
|
284
|
+
this.restartOnEnvChangeGlobal = config.restartOnEnvChange ?? true;
|
|
285
|
+
this.restartMinDelayMsGlobal = config.restartMinDelayMs ?? DEFAULT_RESTART_MIN_DELAY_MS;
|
|
286
|
+
this.restartMaxDelayMsGlobal = config.restartMaxDelayMs ?? DEFAULT_RESTART_MAX_DELAY_MS;
|
|
287
|
+
this.logger.info(`Executing ${config.tasks.length} tasks and registering ${config.processes.length} processes`);
|
|
288
|
+
this.setupSignalHandlers();
|
|
289
|
+
this.setupEnvWatchers();
|
|
290
|
+
await this.runTasks(config.tasks);
|
|
291
|
+
for (const p of config.processes) this.register(p);
|
|
292
|
+
}
|
|
293
|
+
register(config) {
|
|
294
|
+
if (this.processes.has(config.name)) throw new Error(`Process ${config.name} already registered`);
|
|
295
|
+
const id = this.nextProcessId++;
|
|
296
|
+
const managed = {
|
|
297
|
+
id,
|
|
298
|
+
config,
|
|
299
|
+
status: "stopped",
|
|
300
|
+
spawned: null,
|
|
301
|
+
exitCode: null,
|
|
302
|
+
restarts: 0,
|
|
303
|
+
restartAttempts: 0,
|
|
304
|
+
restartTimer: null,
|
|
305
|
+
stopRequested: false,
|
|
306
|
+
startedAt: null
|
|
307
|
+
};
|
|
308
|
+
this.processes.set(config.name, managed);
|
|
309
|
+
this.processIds.set(id, config.name);
|
|
310
|
+
this.logger.info(`Registered process: ${config.name} (id: ${id})`);
|
|
311
|
+
}
|
|
312
|
+
list() {
|
|
313
|
+
return [...this.processes.values()].map((m) => ({
|
|
314
|
+
id: m.id,
|
|
315
|
+
name: m.config.name,
|
|
316
|
+
status: m.status,
|
|
317
|
+
pid: m.spawned?.proc.pid,
|
|
318
|
+
exitCode: m.exitCode,
|
|
319
|
+
restarts: m.restarts,
|
|
320
|
+
startedAt: m.startedAt,
|
|
321
|
+
command: m.config.command,
|
|
322
|
+
args: m.config.args
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
start(name) {
|
|
326
|
+
const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);
|
|
327
|
+
if (managed.status === "running") {
|
|
328
|
+
this.logger.warn(`Process ${resolvedName} is already running`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.spawnProcess(managed);
|
|
332
|
+
}
|
|
333
|
+
startAll() {
|
|
334
|
+
this.setupSignalHandlers();
|
|
335
|
+
for (const [_, managed] of this.processes) if (managed.status !== "running") this.spawnProcess(managed);
|
|
336
|
+
}
|
|
337
|
+
serve(options) {
|
|
338
|
+
const serverOpts = {
|
|
339
|
+
...this.serverOptions,
|
|
340
|
+
...options
|
|
341
|
+
};
|
|
342
|
+
if (serverOpts.enabled === false) return;
|
|
343
|
+
const { enabled: _enabled, ...serverOptions } = serverOpts;
|
|
344
|
+
this.httpServer = createServer(this, serverOptions).start();
|
|
345
|
+
}
|
|
346
|
+
async stop(name, timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
|
|
347
|
+
const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);
|
|
348
|
+
managed.stopRequested = true;
|
|
349
|
+
this.clearRestartTimer(managed);
|
|
350
|
+
if (managed.status !== "running" || !managed.spawned) {
|
|
351
|
+
this.logger.warn(`Process ${resolvedName} is not running`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const { spawned } = managed;
|
|
355
|
+
this.logger.info(`Stopping process: ${resolvedName} (SIGTERM)`);
|
|
356
|
+
spawned.kill("SIGTERM");
|
|
357
|
+
if (!await Promise.race([spawned.done.then(() => true).catch(() => true), new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))]) && managed.status === "running") {
|
|
358
|
+
this.logger.warn(`Process ${resolvedName} did not stop gracefully, sending SIGKILL`);
|
|
359
|
+
spawned.kill("SIGKILL");
|
|
360
|
+
await spawned.done.catch(() => {});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async stopAll() {
|
|
364
|
+
this.logger.info("Stopping all processes...");
|
|
365
|
+
for (const managed of this.processes.values()) {
|
|
366
|
+
managed.stopRequested = true;
|
|
367
|
+
this.clearRestartTimer(managed);
|
|
368
|
+
}
|
|
369
|
+
const stopPromises = [...this.processes.entries()].filter(([, m]) => m.status === "running").map(([name]) => this.stop(name));
|
|
370
|
+
await Promise.allSettled(stopPromises);
|
|
371
|
+
}
|
|
372
|
+
async restart(name) {
|
|
373
|
+
const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);
|
|
374
|
+
this.logger.info(`Restarting process: ${resolvedName}`);
|
|
375
|
+
if (managed.status === "running") await this.stop(resolvedName);
|
|
376
|
+
managed.restarts++;
|
|
377
|
+
this.spawnProcess(managed);
|
|
378
|
+
}
|
|
379
|
+
async restartAll() {
|
|
380
|
+
this.logger.info("Restarting all processes...");
|
|
381
|
+
for (const [name] of this.processes) await this.restart(name);
|
|
382
|
+
}
|
|
383
|
+
async delete(name) {
|
|
384
|
+
const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);
|
|
385
|
+
managed.stopRequested = true;
|
|
386
|
+
this.clearRestartTimer(managed);
|
|
387
|
+
if (managed.status === "running") await this.stop(resolvedName);
|
|
388
|
+
this.processes.delete(resolvedName);
|
|
389
|
+
this.processIds.delete(managed.id);
|
|
390
|
+
this.logger.info(`Deleted process: ${resolvedName}`);
|
|
391
|
+
}
|
|
392
|
+
async deleteAll() {
|
|
393
|
+
await this.stopAll();
|
|
394
|
+
this.processes.clear();
|
|
395
|
+
this.logger.info("Deleted all processes");
|
|
396
|
+
}
|
|
397
|
+
get(name) {
|
|
398
|
+
let resolved;
|
|
399
|
+
try {
|
|
400
|
+
resolved = this.resolveProcessIdentifier(name);
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
id: resolved.managed.id,
|
|
406
|
+
name: resolved.managed.config.name,
|
|
407
|
+
status: resolved.managed.status,
|
|
408
|
+
pid: resolved.managed.spawned?.proc.pid,
|
|
409
|
+
exitCode: resolved.managed.exitCode,
|
|
410
|
+
restarts: resolved.managed.restarts,
|
|
411
|
+
startedAt: resolved.managed.startedAt,
|
|
412
|
+
command: resolved.managed.config.command,
|
|
413
|
+
args: resolved.managed.config.args
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async wait() {
|
|
417
|
+
const donePromises = [...this.processes.values()].filter((m) => m.spawned).map((m) => m.spawned.done);
|
|
418
|
+
await Promise.allSettled(donePromises);
|
|
419
|
+
this.logger.info("All processes exited");
|
|
420
|
+
}
|
|
421
|
+
async shutdown() {
|
|
422
|
+
await this.stopActiveTask();
|
|
423
|
+
await this.stopAll();
|
|
424
|
+
this.closeEnvWatcher();
|
|
425
|
+
if (this.httpServer) {
|
|
426
|
+
this.httpServer.close();
|
|
427
|
+
this.logger.info("HTTP server closed");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
spawnProcess(managed) {
|
|
431
|
+
const { config } = managed;
|
|
432
|
+
managed.stopRequested = false;
|
|
433
|
+
this.clearRestartTimer(managed);
|
|
434
|
+
const env = loadEnvForProcess({
|
|
435
|
+
configDir: this.configDir ?? this.cwd,
|
|
436
|
+
processName: config.name,
|
|
437
|
+
globalEnvFile: this.globalEnvFile,
|
|
438
|
+
processEnvFile: config.envFile,
|
|
439
|
+
configEnv: config.env
|
|
440
|
+
});
|
|
441
|
+
const spawned = spawnWithLogging({
|
|
442
|
+
cwd: config.cwd ?? this.cwd,
|
|
443
|
+
logFile: join(this.cwd, "logs", "processes", `${config.name}.log`),
|
|
444
|
+
type: "process",
|
|
445
|
+
name: config.name,
|
|
446
|
+
command: config.command,
|
|
447
|
+
args: config.args,
|
|
448
|
+
env
|
|
449
|
+
});
|
|
450
|
+
managed.spawned = spawned;
|
|
451
|
+
managed.status = "running";
|
|
452
|
+
managed.startedAt = /* @__PURE__ */ new Date();
|
|
453
|
+
managed.exitCode = null;
|
|
454
|
+
this.logger.info(`Started process: ${config.name} (pid: ${spawned.proc.pid})`);
|
|
455
|
+
const handleExit = (exitCode, error) => {
|
|
456
|
+
managed.exitCode = exitCode;
|
|
457
|
+
managed.status = exitCode === null || exitCode === void 0 || exitCode === 0 ? "stopped" : "errored";
|
|
458
|
+
const now = Date.now();
|
|
459
|
+
const startedAt = managed.startedAt?.getTime() ?? now;
|
|
460
|
+
if (Math.max(0, now - startedAt) >= (config.restartResetSeconds ?? DEFAULT_RESTART_RESET_SECONDS) * 1e3) managed.restartAttempts = 0;
|
|
461
|
+
if (exitCode === null || exitCode === void 0) this.logger.info(`Process ${config.name} stopped`);
|
|
462
|
+
else if (exitCode !== 0) this.logger.info(`Process ${config.name} exited with code ${exitCode}`);
|
|
463
|
+
else this.logger.info(`Process ${config.name} exited successfully`);
|
|
464
|
+
if (error) this.logger.error(`Process ${config.name} failed with error: ${error}`);
|
|
465
|
+
const policy = config.restartPolicy ?? "on-failure";
|
|
466
|
+
const isFailure = exitCode === null || exitCode === void 0 || exitCode !== 0;
|
|
467
|
+
if (!(!managed.stopRequested && (policy === "always" || policy === "on-failure" && isFailure))) return;
|
|
468
|
+
const maxRestarts = config.maxRestarts ?? DEFAULT_RESTART_MAX;
|
|
469
|
+
if (managed.restartAttempts >= maxRestarts) {
|
|
470
|
+
managed.status = "errored";
|
|
471
|
+
this.logger.warn(`Process ${config.name} reached restart limit (${maxRestarts}), not restarting`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
managed.restartAttempts += 1;
|
|
475
|
+
managed.restarts += 1;
|
|
476
|
+
const minDelay = config.restartMinDelayMs ?? this.restartMinDelayMsGlobal;
|
|
477
|
+
const maxDelay = config.restartMaxDelayMs ?? this.restartMaxDelayMsGlobal;
|
|
478
|
+
const delay = Math.min(maxDelay, minDelay * Math.pow(2, managed.restartAttempts - 1));
|
|
479
|
+
this.logger.warn(`Restarting process ${config.name} in ${delay}ms (attempt ${managed.restartAttempts}/${maxRestarts})`);
|
|
480
|
+
managed.restartTimer = setTimeout(() => {
|
|
481
|
+
managed.restartTimer = null;
|
|
482
|
+
if (!managed.stopRequested) this.spawnProcess(managed);
|
|
483
|
+
}, delay);
|
|
484
|
+
};
|
|
485
|
+
spawned.done.then((exitCode) => handleExit(exitCode)).catch((error) => {
|
|
486
|
+
handleExit(spawned.proc.exitCode ?? 1, error);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async runTasks(tasks) {
|
|
490
|
+
for (const task of tasks) {
|
|
491
|
+
const env = loadEnvForProcess({
|
|
492
|
+
configDir: this.configDir ?? this.cwd,
|
|
493
|
+
processName: task.name,
|
|
494
|
+
globalEnvFile: this.globalEnvFile,
|
|
495
|
+
processEnvFile: task.envFile,
|
|
496
|
+
configEnv: task.env
|
|
497
|
+
});
|
|
498
|
+
const taskAbortController = new AbortController();
|
|
499
|
+
this.taskAbortController = taskAbortController;
|
|
500
|
+
const spawned = spawnWithLogging({
|
|
501
|
+
cwd: task.cwd ?? this.cwd,
|
|
502
|
+
logFile: join(this.cwd, "logs", "tasks", `${task.name}.log`),
|
|
503
|
+
type: "task",
|
|
504
|
+
name: task.name,
|
|
505
|
+
command: task.command,
|
|
506
|
+
args: task.args,
|
|
507
|
+
env,
|
|
508
|
+
abortSignal: taskAbortController.signal
|
|
509
|
+
});
|
|
510
|
+
this.currentTask = spawned;
|
|
511
|
+
this.currentTaskName = task.name;
|
|
512
|
+
let exitCode;
|
|
513
|
+
try {
|
|
514
|
+
exitCode = await spawned.done;
|
|
515
|
+
} finally {
|
|
516
|
+
this.currentTask = null;
|
|
517
|
+
this.currentTaskName = null;
|
|
518
|
+
this.taskAbortController = null;
|
|
519
|
+
}
|
|
520
|
+
if (exitCode !== 0) {
|
|
521
|
+
this.logger.error(`Task ${task.name} failed with exit code ${exitCode}`);
|
|
522
|
+
throw new Error(`Task ${task.name} failed`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (tasks.length > 0) this.logger.info("All tasks completed");
|
|
526
|
+
}
|
|
527
|
+
clearRestartTimer(managed) {
|
|
528
|
+
if (managed.restartTimer) {
|
|
529
|
+
clearTimeout(managed.restartTimer);
|
|
530
|
+
managed.restartTimer = null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async stopActiveTask(timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
|
|
534
|
+
if (!this.currentTask) return;
|
|
535
|
+
const taskName = this.currentTaskName ?? "unknown";
|
|
536
|
+
this.logger.info(`Stopping task: ${taskName} (SIGTERM)`);
|
|
537
|
+
this.currentTask.kill("SIGTERM");
|
|
538
|
+
if (!await Promise.race([this.currentTask.done.then(() => true).catch(() => true), new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))])) {
|
|
539
|
+
this.logger.warn(`Task ${taskName} did not stop gracefully, sending SIGKILL`);
|
|
540
|
+
this.currentTask.kill("SIGKILL");
|
|
541
|
+
await this.currentTask.done.catch(() => {});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
abortActiveTask(signal) {
|
|
545
|
+
if (this.taskAbortController && !this.taskAbortController.signal.aborted) this.taskAbortController.abort();
|
|
546
|
+
if (this.currentTask) this.currentTask.kill(signal);
|
|
547
|
+
}
|
|
548
|
+
setupSignalHandlers() {
|
|
549
|
+
if (this.signalHandlersSetup) return;
|
|
550
|
+
this.signalHandlersSetup = true;
|
|
551
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
|
|
552
|
+
process.stdout.write("\r\x1B[K");
|
|
553
|
+
this.logger.info(`Received ${signal}, shutting down...`);
|
|
554
|
+
this.abortActiveTask(signal);
|
|
555
|
+
this.shutdown().then(() => process.exit(0));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
setupEnvWatchers() {
|
|
559
|
+
if (this.envWatcher) return;
|
|
560
|
+
if (!this.restartOnEnvChangeGlobal) return;
|
|
561
|
+
this.envWatcher = watch(this.configDir ?? this.cwd, (event, filename) => {
|
|
562
|
+
if (!filename) return;
|
|
563
|
+
const envFile = filename.toString();
|
|
564
|
+
if (!envFile.startsWith(".env")) return;
|
|
565
|
+
if (event !== "change" && event !== "rename") return;
|
|
566
|
+
if (envFile === ".env") {
|
|
567
|
+
this.scheduleEnvRestart("global", () => {
|
|
568
|
+
this.logger.info("Detected .env change, scheduling restarts");
|
|
569
|
+
for (const managed of this.processes.values()) this.scheduleEnvRestartForProcess(managed);
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!envFile.startsWith(".env.")) return;
|
|
574
|
+
const processName = envFile.slice(5);
|
|
575
|
+
if (!processName) return;
|
|
576
|
+
this.scheduleEnvRestart(processName, () => {
|
|
577
|
+
const managed = this.processes.get(processName);
|
|
578
|
+
if (!managed) return;
|
|
579
|
+
this.logger.info(`Detected ${envFile} change, scheduling restart for ${processName}`);
|
|
580
|
+
this.scheduleEnvRestartForProcess(managed);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
closeEnvWatcher() {
|
|
585
|
+
if (this.envWatcher) {
|
|
586
|
+
this.envWatcher.close();
|
|
587
|
+
this.envWatcher = null;
|
|
588
|
+
}
|
|
589
|
+
for (const timer of this.envChangeTimers.values()) clearTimeout(timer);
|
|
590
|
+
this.envChangeTimers.clear();
|
|
591
|
+
for (const timer of this.envRestartTimers.values()) clearTimeout(timer);
|
|
592
|
+
this.envRestartTimers.clear();
|
|
593
|
+
}
|
|
594
|
+
scheduleEnvRestart(key, restart) {
|
|
595
|
+
const existing = this.envChangeTimers.get(key);
|
|
596
|
+
if (existing) clearTimeout(existing);
|
|
597
|
+
const timer = setTimeout(() => {
|
|
598
|
+
this.envChangeTimers.delete(key);
|
|
599
|
+
restart();
|
|
600
|
+
}, 250);
|
|
601
|
+
this.envChangeTimers.set(key, timer);
|
|
602
|
+
}
|
|
603
|
+
scheduleEnvRestartForProcess(managed) {
|
|
604
|
+
if (!this.restartOnEnvChangeGlobal) return;
|
|
605
|
+
if (managed.config.restartOnEnvChange === false) return;
|
|
606
|
+
if (managed.status !== "running") return;
|
|
607
|
+
const name = managed.config.name;
|
|
608
|
+
const existing = this.envRestartTimers.get(name);
|
|
609
|
+
if (existing) clearTimeout(existing);
|
|
610
|
+
const delayMs = managed.config.restartMinDelayMs ?? this.restartMinDelayMsGlobal;
|
|
611
|
+
const timer = setTimeout(() => {
|
|
612
|
+
this.envRestartTimers.delete(name);
|
|
613
|
+
this.restart(name).catch((error) => {
|
|
614
|
+
this.logger.error(`Failed to restart ${name} after env change: ${error}`);
|
|
615
|
+
});
|
|
616
|
+
}, Math.max(0, delayMs));
|
|
617
|
+
this.envRestartTimers.set(name, timer);
|
|
618
|
+
this.logger.info(`Restarting ${name} after env change in ${Math.max(0, delayMs)}ms`);
|
|
619
|
+
}
|
|
620
|
+
resolveProcessIdentifier(nameOrId) {
|
|
621
|
+
const byName = this.processes.get(nameOrId);
|
|
622
|
+
if (byName) return {
|
|
623
|
+
name: nameOrId,
|
|
624
|
+
managed: byName
|
|
625
|
+
};
|
|
626
|
+
if (/^\d+$/.test(nameOrId)) {
|
|
627
|
+
const id = Number(nameOrId);
|
|
628
|
+
const name = this.processIds.get(id);
|
|
629
|
+
if (name) {
|
|
630
|
+
const managed = this.processes.get(name);
|
|
631
|
+
if (managed) return {
|
|
632
|
+
name,
|
|
633
|
+
managed
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw new Error(`Process ${nameOrId} not found`);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
//#endregion
|
|
642
|
+
export { spawnWithLogging as i, createServer as n, router as r, ProcessManager as t };
|
|
643
|
+
//# sourceMappingURL=process-manager-gO56266Q.mjs.map
|