@iskra-bun/process-kit 0.1.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/CHANGELOG.md +7 -0
- package/README.md +29 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +152 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +1 -0
- package/src/spawner.ts +201 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @iskra-bun/process-kit
|
|
2
|
+
|
|
3
|
+
Gestion de procesos externos (scripts de Python, binarios) para Iskra. Soporta modos daemon, oneshot y stdio, con ciclo de vida y validacion de configuracion.
|
|
4
|
+
|
|
5
|
+
## Instalacion
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @iskra-bun/process-kit @iskra-bun/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Uso rapido
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { App } from '@iskra-bun/core'
|
|
15
|
+
import { ProcessManager } from '@iskra-bun/process-kit'
|
|
16
|
+
|
|
17
|
+
const app = new App({ name: 'mi-app' })
|
|
18
|
+
app.register(new ProcessManager({ command: 'python', args: ['worker.py'], mode: 'daemon' }))
|
|
19
|
+
|
|
20
|
+
await app.start()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentacion
|
|
24
|
+
|
|
25
|
+
Guia completa: [docs/process-kit.md](../../docs/process-kit.md)
|
|
26
|
+
|
|
27
|
+
## Licencia
|
|
28
|
+
|
|
29
|
+
AGPL-3.0-or-later
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Driver, App } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
declare class ProcessManager implements Driver {
|
|
4
|
+
name: string;
|
|
5
|
+
private app;
|
|
6
|
+
private processes;
|
|
7
|
+
private stopping;
|
|
8
|
+
init(app: App): void;
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
private readStdOut;
|
|
11
|
+
private processLine;
|
|
12
|
+
private handleExit;
|
|
13
|
+
private spawnProcess;
|
|
14
|
+
private readStdErr;
|
|
15
|
+
send(name: string, data: any): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { ProcessManager };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/spawner.ts
|
|
2
|
+
var ProcessManager = class {
|
|
3
|
+
name = "ProcessManager";
|
|
4
|
+
app = null;
|
|
5
|
+
processes = /* @__PURE__ */ new Map();
|
|
6
|
+
stopping = false;
|
|
7
|
+
init(app) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
}
|
|
10
|
+
async start() {
|
|
11
|
+
if (!this.app) return;
|
|
12
|
+
const processConfigs = this.app.config.processes;
|
|
13
|
+
if (!processConfigs) {
|
|
14
|
+
this.app.logger.debug("No processes configured.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.app.logger.info(`Starting ${Object.keys(processConfigs).length} processes...`);
|
|
18
|
+
for (const [name, config] of Object.entries(processConfigs)) {
|
|
19
|
+
this.spawnProcess(name, config);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async readStdOut(name, stream) {
|
|
23
|
+
const reader = stream.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let buffer = "";
|
|
26
|
+
try {
|
|
27
|
+
while (true) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done) break;
|
|
30
|
+
buffer += decoder.decode(value, { stream: true });
|
|
31
|
+
const lines = buffer.split("\n");
|
|
32
|
+
buffer = lines.pop() || "";
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.trim()) continue;
|
|
35
|
+
this.processLine(name, line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
processLine(name, line) {
|
|
42
|
+
try {
|
|
43
|
+
const json = JSON.parse(line);
|
|
44
|
+
this.app?.emit("process:message", { name, message: json });
|
|
45
|
+
} catch {
|
|
46
|
+
this.app?.emit("process:log", { name, text: line });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
handleExit(name, exitCode, signalCode) {
|
|
50
|
+
if (this.stopping) return;
|
|
51
|
+
const procInfo = this.processes.get(name);
|
|
52
|
+
if (!procInfo) return;
|
|
53
|
+
this.app?.logger.warn(`Process ${name} exited with code ${exitCode}`);
|
|
54
|
+
this.processes.delete(name);
|
|
55
|
+
if (procInfo.config.restartOnCrash) {
|
|
56
|
+
const maxRestarts = procInfo.config.maxRestarts ?? 10;
|
|
57
|
+
const cooldown = procInfo.config.restartCooldown ?? 6e4;
|
|
58
|
+
const uptime = Date.now() - procInfo.startedAt;
|
|
59
|
+
const restarts = uptime > cooldown ? 1 : procInfo.restarts + 1;
|
|
60
|
+
if (restarts > maxRestarts) {
|
|
61
|
+
this.app?.logger.error(`Process ${name} exceeded max restarts (${maxRestarts}). Not restarting.`);
|
|
62
|
+
this.app?.emit("process:max-restarts", { name, restarts: procInfo.restarts, maxRestarts });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.app?.logger.info(`Restarting process: ${name} (Attempt ${restarts}/${maxRestarts})`);
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
this.spawnProcess(name, procInfo.config, restarts);
|
|
68
|
+
}, 1e3);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Updated spawn signature to track restarts
|
|
72
|
+
spawnProcess(name, config, restarts = 0) {
|
|
73
|
+
if (this.stopping || !this.app) return;
|
|
74
|
+
this.app.logger.info(`Spawning process: ${name} (${config.command} ${config.args?.join(" ") || ""})`);
|
|
75
|
+
try {
|
|
76
|
+
const proc = Bun.spawn(
|
|
77
|
+
[config.command, ...config.args || []],
|
|
78
|
+
{
|
|
79
|
+
env: { ...process.env, ...config.env },
|
|
80
|
+
stdout: config.mode === "stdio" ? "pipe" : "inherit",
|
|
81
|
+
stderr: config.mode === "stdio" ? "pipe" : "inherit",
|
|
82
|
+
stdin: config.mode === "stdio" ? "pipe" : "ignore",
|
|
83
|
+
onExit: (proc2, exitCode, signalCode, error) => {
|
|
84
|
+
this.handleExit(name, exitCode || 0, signalCode || 0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
this.processes.set(name, {
|
|
89
|
+
process: proc,
|
|
90
|
+
config,
|
|
91
|
+
name,
|
|
92
|
+
restarts,
|
|
93
|
+
startedAt: Date.now()
|
|
94
|
+
});
|
|
95
|
+
if (config.mode === "stdio") {
|
|
96
|
+
if (proc.stdout) this.readStdOut(name, proc.stdout);
|
|
97
|
+
if (proc.stderr) this.readStdErr(name, proc.stderr);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
this.app.logger.error({ err }, `Failed to spawn process: ${name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async readStdErr(name, stream) {
|
|
104
|
+
const reader = stream.getReader();
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
const text = decoder.decode(value, { stream: true });
|
|
111
|
+
if (text.trim()) {
|
|
112
|
+
this.app?.emit("process:error", { name, text });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Send data to process stdin
|
|
119
|
+
async send(name, data) {
|
|
120
|
+
const procInfo = this.processes.get(name);
|
|
121
|
+
if (!procInfo) {
|
|
122
|
+
this.app?.logger.warn(`Cannot send message to non-existent process: ${name}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (procInfo.config.mode !== "stdio" || !procInfo.process.stdin) {
|
|
126
|
+
this.app?.logger.warn(`Cannot send message to process ${name} (mode is not stdio or stdin is closed)`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const stdin = procInfo.process.stdin;
|
|
130
|
+
try {
|
|
131
|
+
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
132
|
+
stdin.write(message + "\n");
|
|
133
|
+
stdin.flush();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.app?.logger.error({ err }, `Failed to write to process ${name}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async stop() {
|
|
139
|
+
this.stopping = true;
|
|
140
|
+
this.app?.logger.info("Stopping all processes...");
|
|
141
|
+
for (const [name, info] of this.processes) {
|
|
142
|
+
if (!info.process.killed) {
|
|
143
|
+
info.process.kill();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.processes.clear();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
export {
|
|
150
|
+
ProcessManager
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spawner.ts"],"sourcesContent":["import type { App, Driver, ProcessConfig } from '@iskra-bun/core';\nimport { Subprocess } from 'bun';\n\ninterface RunningProcess {\n process: Subprocess;\n config: ProcessConfig;\n name: string;\n restarts: number;\n startedAt: number;\n}\n\nexport class ProcessManager implements Driver {\n name = 'ProcessManager';\n private app: App | null = null;\n private processes: Map<string, RunningProcess> = new Map();\n private stopping = false;\n\n init(app: App) {\n this.app = app;\n }\n\n async start() {\n if (!this.app) return;\n const processConfigs = this.app.config.processes;\n\n if (!processConfigs) {\n this.app.logger.debug('No processes configured.');\n return;\n }\n\n this.app.logger.info(`Starting ${Object.keys(processConfigs).length} processes...`);\n\n for (const [name, config] of Object.entries(processConfigs)) {\n this.spawnProcess(name, config);\n }\n }\n\n private async readStdOut(name: string, stream: ReadableStream) {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last chunk if it's not a complete line (doesn't end with \\n)\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.trim()) continue;\n this.processLine(name, line);\n }\n }\n } catch (err) {\n // Stream closed or error\n }\n }\n\n private processLine(name: string, line: string) {\n try {\n // Try to parse as JSON Event\n const json = JSON.parse(line);\n this.app?.emit('process:message', { name, message: json });\n } catch {\n // Fallback to raw log\n this.app?.emit('process:log', { name, text: line });\n }\n }\n\n private handleExit(name: string, exitCode: number, signalCode: number) {\n if (this.stopping) return;\n\n const procInfo = this.processes.get(name);\n if (!procInfo) return;\n\n this.app?.logger.warn(`Process ${name} exited with code ${exitCode}`);\n\n // Remove from map so we don't try to kill it again on stop()\n this.processes.delete(name);\n\n if (procInfo.config.restartOnCrash) {\n const maxRestarts = procInfo.config.maxRestarts ?? 10;\n const cooldown = procInfo.config.restartCooldown ?? 60000;\n const uptime = Date.now() - procInfo.startedAt;\n\n // If the process ran longer than the cooldown, consider it stable and reset the counter\n const restarts = uptime > cooldown ? 1 : procInfo.restarts + 1;\n\n if (restarts > maxRestarts) {\n this.app?.logger.error(`Process ${name} exceeded max restarts (${maxRestarts}). Not restarting.`);\n this.app?.emit('process:max-restarts', { name, restarts: procInfo.restarts, maxRestarts });\n return;\n }\n\n this.app?.logger.info(`Restarting process: ${name} (Attempt ${restarts}/${maxRestarts})`);\n\n setTimeout(() => {\n this.spawnProcess(name, procInfo.config, restarts);\n }, 1000);\n }\n }\n\n // Updated spawn signature to track restarts\n private spawnProcess(name: string, config: ProcessConfig, restarts = 0) {\n if (this.stopping || !this.app) return;\n\n this.app.logger.info(`Spawning process: ${name} (${config.command} ${config.args?.join(' ') || ''})`);\n\n try {\n const proc = Bun.spawn(\n [config.command, ...(config.args || [])],\n {\n env: { ...process.env, ...config.env },\n stdout: config.mode === 'stdio' ? 'pipe' : 'inherit',\n stderr: config.mode === 'stdio' ? 'pipe' : 'inherit',\n stdin: config.mode === 'stdio' ? 'pipe' : 'ignore',\n onExit: (proc, exitCode, signalCode, error) => {\n this.handleExit(name, exitCode || 0, signalCode || 0);\n }\n }\n );\n\n this.processes.set(name, {\n process: proc,\n config,\n name,\n restarts,\n startedAt: Date.now(),\n });\n\n if (config.mode === 'stdio') {\n if (proc.stdout) this.readStdOut(name, proc.stdout);\n if (proc.stderr) this.readStdErr(name, proc.stderr);\n }\n\n } catch (err) {\n this.app.logger.error({ err }, `Failed to spawn process: ${name}`);\n }\n }\n\n private async readStdErr(name: string, stream: ReadableStream) {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const text = decoder.decode(value, { stream: true });\n if (text.trim()) {\n this.app?.emit('process:error', { name, text });\n }\n }\n } catch (err) {\n // Stream closed\n }\n }\n\n // Send data to process stdin\n async send(name: string, data: any) {\n const procInfo = this.processes.get(name);\n if (!procInfo) {\n this.app?.logger.warn(`Cannot send message to non-existent process: ${name}`);\n return;\n }\n\n if (procInfo.config.mode !== 'stdio' || !procInfo.process.stdin) {\n this.app?.logger.warn(`Cannot send message to process ${name} (mode is not stdio or stdin is closed)`);\n return;\n }\n\n // Bun's Subprocess.stdin is a FileSink\n const stdin = procInfo.process.stdin as any;\n\n try {\n // If data is object, stringify it and add newline\n const message = typeof data === 'string' ? data : JSON.stringify(data);\n stdin.write(message + '\\n');\n stdin.flush();\n } catch (err) {\n this.app?.logger.error({ err }, `Failed to write to process ${name}`);\n }\n }\n\n async stop() {\n this.stopping = true;\n this.app?.logger.info('Stopping all processes...');\n\n for (const [name, info] of this.processes) {\n if (!info.process.killed) {\n info.process.kill();\n }\n }\n this.processes.clear();\n }\n}\n"],"mappings":"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","names":["proc"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iskra-bun/process-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gestion de procesos externos (Python, binarios) para Iskra.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iskra",
|
|
7
|
+
"bun",
|
|
8
|
+
"typescript",
|
|
9
|
+
"process",
|
|
10
|
+
"python",
|
|
11
|
+
"ipc"
|
|
12
|
+
],
|
|
13
|
+
"author": "Joan Lascano",
|
|
14
|
+
"license": "AGPL-3.0-or-later",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/fearful/iskra.git",
|
|
18
|
+
"directory": "packages/process-kit"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/fearful/iskra/tree/main/packages/process-kit#readme",
|
|
21
|
+
"bugs": "https://github.com/fearful/iskra/issues",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"source": "./src/index.ts",
|
|
29
|
+
"bun": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"build": "tsup --config ../../tsup.config.ts"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@iskra-bun/core": "0.1.0",
|
|
50
|
+
"zod": "^3.24.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.10.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ProcessManager } from './spawner';
|
package/src/spawner.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { App, Driver, ProcessConfig } from '@iskra-bun/core';
|
|
2
|
+
import { Subprocess } from 'bun';
|
|
3
|
+
|
|
4
|
+
interface RunningProcess {
|
|
5
|
+
process: Subprocess;
|
|
6
|
+
config: ProcessConfig;
|
|
7
|
+
name: string;
|
|
8
|
+
restarts: number;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ProcessManager implements Driver {
|
|
13
|
+
name = 'ProcessManager';
|
|
14
|
+
private app: App | null = null;
|
|
15
|
+
private processes: Map<string, RunningProcess> = new Map();
|
|
16
|
+
private stopping = false;
|
|
17
|
+
|
|
18
|
+
init(app: App) {
|
|
19
|
+
this.app = app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start() {
|
|
23
|
+
if (!this.app) return;
|
|
24
|
+
const processConfigs = this.app.config.processes;
|
|
25
|
+
|
|
26
|
+
if (!processConfigs) {
|
|
27
|
+
this.app.logger.debug('No processes configured.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.app.logger.info(`Starting ${Object.keys(processConfigs).length} processes...`);
|
|
32
|
+
|
|
33
|
+
for (const [name, config] of Object.entries(processConfigs)) {
|
|
34
|
+
this.spawnProcess(name, config);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async readStdOut(name: string, stream: ReadableStream) {
|
|
39
|
+
const reader = stream.getReader();
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = '';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
|
|
48
|
+
buffer += decoder.decode(value, { stream: true });
|
|
49
|
+
|
|
50
|
+
const lines = buffer.split('\n');
|
|
51
|
+
// Keep the last chunk if it's not a complete line (doesn't end with \n)
|
|
52
|
+
buffer = lines.pop() || '';
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.trim()) continue;
|
|
56
|
+
this.processLine(name, line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Stream closed or error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private processLine(name: string, line: string) {
|
|
65
|
+
try {
|
|
66
|
+
// Try to parse as JSON Event
|
|
67
|
+
const json = JSON.parse(line);
|
|
68
|
+
this.app?.emit('process:message', { name, message: json });
|
|
69
|
+
} catch {
|
|
70
|
+
// Fallback to raw log
|
|
71
|
+
this.app?.emit('process:log', { name, text: line });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private handleExit(name: string, exitCode: number, signalCode: number) {
|
|
76
|
+
if (this.stopping) return;
|
|
77
|
+
|
|
78
|
+
const procInfo = this.processes.get(name);
|
|
79
|
+
if (!procInfo) return;
|
|
80
|
+
|
|
81
|
+
this.app?.logger.warn(`Process ${name} exited with code ${exitCode}`);
|
|
82
|
+
|
|
83
|
+
// Remove from map so we don't try to kill it again on stop()
|
|
84
|
+
this.processes.delete(name);
|
|
85
|
+
|
|
86
|
+
if (procInfo.config.restartOnCrash) {
|
|
87
|
+
const maxRestarts = procInfo.config.maxRestarts ?? 10;
|
|
88
|
+
const cooldown = procInfo.config.restartCooldown ?? 60000;
|
|
89
|
+
const uptime = Date.now() - procInfo.startedAt;
|
|
90
|
+
|
|
91
|
+
// If the process ran longer than the cooldown, consider it stable and reset the counter
|
|
92
|
+
const restarts = uptime > cooldown ? 1 : procInfo.restarts + 1;
|
|
93
|
+
|
|
94
|
+
if (restarts > maxRestarts) {
|
|
95
|
+
this.app?.logger.error(`Process ${name} exceeded max restarts (${maxRestarts}). Not restarting.`);
|
|
96
|
+
this.app?.emit('process:max-restarts', { name, restarts: procInfo.restarts, maxRestarts });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.app?.logger.info(`Restarting process: ${name} (Attempt ${restarts}/${maxRestarts})`);
|
|
101
|
+
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
this.spawnProcess(name, procInfo.config, restarts);
|
|
104
|
+
}, 1000);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Updated spawn signature to track restarts
|
|
109
|
+
private spawnProcess(name: string, config: ProcessConfig, restarts = 0) {
|
|
110
|
+
if (this.stopping || !this.app) return;
|
|
111
|
+
|
|
112
|
+
this.app.logger.info(`Spawning process: ${name} (${config.command} ${config.args?.join(' ') || ''})`);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const proc = Bun.spawn(
|
|
116
|
+
[config.command, ...(config.args || [])],
|
|
117
|
+
{
|
|
118
|
+
env: { ...process.env, ...config.env },
|
|
119
|
+
stdout: config.mode === 'stdio' ? 'pipe' : 'inherit',
|
|
120
|
+
stderr: config.mode === 'stdio' ? 'pipe' : 'inherit',
|
|
121
|
+
stdin: config.mode === 'stdio' ? 'pipe' : 'ignore',
|
|
122
|
+
onExit: (proc, exitCode, signalCode, error) => {
|
|
123
|
+
this.handleExit(name, exitCode || 0, signalCode || 0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
this.processes.set(name, {
|
|
129
|
+
process: proc,
|
|
130
|
+
config,
|
|
131
|
+
name,
|
|
132
|
+
restarts,
|
|
133
|
+
startedAt: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (config.mode === 'stdio') {
|
|
137
|
+
if (proc.stdout) this.readStdOut(name, proc.stdout);
|
|
138
|
+
if (proc.stderr) this.readStdErr(name, proc.stderr);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.app.logger.error({ err }, `Failed to spawn process: ${name}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async readStdErr(name: string, stream: ReadableStream) {
|
|
147
|
+
const reader = stream.getReader();
|
|
148
|
+
const decoder = new TextDecoder();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
while (true) {
|
|
152
|
+
const { done, value } = await reader.read();
|
|
153
|
+
if (done) break;
|
|
154
|
+
const text = decoder.decode(value, { stream: true });
|
|
155
|
+
if (text.trim()) {
|
|
156
|
+
this.app?.emit('process:error', { name, text });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// Stream closed
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Send data to process stdin
|
|
165
|
+
async send(name: string, data: any) {
|
|
166
|
+
const procInfo = this.processes.get(name);
|
|
167
|
+
if (!procInfo) {
|
|
168
|
+
this.app?.logger.warn(`Cannot send message to non-existent process: ${name}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (procInfo.config.mode !== 'stdio' || !procInfo.process.stdin) {
|
|
173
|
+
this.app?.logger.warn(`Cannot send message to process ${name} (mode is not stdio or stdin is closed)`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Bun's Subprocess.stdin is a FileSink
|
|
178
|
+
const stdin = procInfo.process.stdin as any;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// If data is object, stringify it and add newline
|
|
182
|
+
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
|
183
|
+
stdin.write(message + '\n');
|
|
184
|
+
stdin.flush();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this.app?.logger.error({ err }, `Failed to write to process ${name}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async stop() {
|
|
191
|
+
this.stopping = true;
|
|
192
|
+
this.app?.logger.info('Stopping all processes...');
|
|
193
|
+
|
|
194
|
+
for (const [name, info] of this.processes) {
|
|
195
|
+
if (!info.process.killed) {
|
|
196
|
+
info.process.kill();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.processes.clear();
|
|
200
|
+
}
|
|
201
|
+
}
|