Skip to content

Commit f41a412

Browse files
committed
feat: each exec runs as its own named PM2 process visible in pm2 list\n\n- Add src/runtime.js: extracts binary finding and child spawn logic for all\n supported runtimes (nodejs, python, bash, powershell, go, rust, c, cpp, java, deno)\n- Add src/exec-process.js: PM2 fork-mode child that reads TASK_ID/PORT/RUNTIME/CWD/CODE_FILE\n from env, spawns the runtime child, streams stdout/stderr back via HTTP RPC appendOutput,\n calls completeTask/failTask on exit, handles process.on('message') for stdin via PM2 IPC\n- Rewrite src/task-runner.js execute RPC: writes code to temp file, starts PM2 process\n per task (gm-exec-task-{id}), polls backgroundStore for fast completion (<timeout),\n returns inline result or {persisted:true} for slow tasks\n- sendStdin RPC uses pm2lib.sendDataToProcessId with taskPm2Ids Map\n- deleteTask RPC calls pm2lib.delete to remove the PM2 process from pm2 list\n- Remove WorkerPool dependency from task-runner entirely\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0e0873c commit f41a412

3 files changed

Lines changed: 280 additions & 58 deletions

File tree

src/exec-process.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import http from 'http';
2+
import { readFileSync, unlinkSync } from 'fs';
3+
import { spawnProcess, killChild } from './runtime.js';
4+
5+
const { TASK_ID, PORT, RUNTIME, CWD, CODE_FILE } = process.env;
6+
const taskId = parseInt(TASK_ID, 10);
7+
const port = parseInt(PORT, 10);
8+
9+
function rpc(method, params) {
10+
return new Promise((resolve) => {
11+
try {
12+
const body = JSON.stringify({ method, params });
13+
const req = http.request(
14+
{ hostname: '127.0.0.1', port, path: '/rpc', method: 'POST',
15+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
16+
res => { res.on('data', () => {}); res.on('end', () => resolve()); }
17+
);
18+
req.on('error', () => resolve());
19+
req.write(body); req.end();
20+
} catch { resolve(); }
21+
});
22+
}
23+
24+
const code = readFileSync(CODE_FILE, 'utf8');
25+
try { unlinkSync(CODE_FILE); } catch {}
26+
27+
let activeChild = null;
28+
29+
process.on('message', (msg) => {
30+
if (msg?.data?.type === 'stdin' && activeChild?.stdin && !activeChild.stdin.destroyed) {
31+
try { activeChild.stdin.write(msg.data.data); } catch {}
32+
}
33+
});
34+
35+
async function runChild(child, cleanup) {
36+
activeChild = child;
37+
let stdout = '', stderr = '';
38+
child.stdout?.on('data', async (d) => {
39+
const str = d.toString('utf8');
40+
stdout += str;
41+
await rpc('appendOutput', { taskId, type: 'stdout', data: str });
42+
});
43+
child.stderr?.on('data', async (d) => {
44+
const str = d.toString('utf8');
45+
stderr += str;
46+
await rpc('appendOutput', { taskId, type: 'stderr', data: str });
47+
});
48+
return new Promise((resolve) => {
49+
child.on('error', async (err) => {
50+
cleanup();
51+
await rpc('failTask', { taskId, error: err.message });
52+
resolve({ ok: false, error: err.message });
53+
});
54+
child.on('close', (code) => {
55+
cleanup();
56+
resolve({ ok: code === 0, exitCode: code, stdout, stderr });
57+
});
58+
});
59+
}
60+
61+
async function runCompiled(spawnResult) {
62+
const { child, cleanup, binPath, dir, cp, className, isCompile } = spawnResult;
63+
const compileResult = await runChild(child, () => {});
64+
if (!compileResult.ok) {
65+
cleanup();
66+
await rpc('failTask', { taskId, error: compileResult.stderr || 'Compilation failed' });
67+
return;
68+
}
69+
let runChild2, runCleanup;
70+
if (RUNTIME === 'java') {
71+
const { spawn } = await import('child_process');
72+
const JAVA = 'java';
73+
runChild2 = spawn(JAVA, ['-cp', cp, className], { cwd: CWD, stdio: ['pipe','pipe','pipe'] });
74+
runCleanup = cleanup;
75+
} else {
76+
const { spawn } = await import('child_process');
77+
runChild2 = spawn(binPath, [], { cwd: CWD, stdio: ['pipe','pipe','pipe'] });
78+
runCleanup = cleanup;
79+
}
80+
const result = await runChild(runChild2, runCleanup);
81+
await rpc(result.ok ? 'completeTask' : 'failTask', result.ok
82+
? { taskId, result: { success: true, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr } }
83+
: { taskId, error: result.stderr || 'Execution failed' });
84+
}
85+
86+
const spawnResult = spawnProcess(RUNTIME, code, CWD);
87+
if (spawnResult.isCompile) {
88+
await runCompiled(spawnResult);
89+
} else {
90+
const result = await runChild(spawnResult.child, spawnResult.cleanup);
91+
await rpc(result.ok ? 'completeTask' : 'failTask', result.ok
92+
? { taskId, result: { success: true, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr } }
93+
: { taskId, error: result.error || result.stderr || 'Execution failed' });
94+
}
95+
process.exit(0);

src/runtime.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { spawn, execSync } from 'child_process';
2+
import { writeFileSync, mkdtempSync, rmSync } from 'fs';
3+
import path from 'path';
4+
import os from 'os';
5+
6+
function findBin(...candidates) {
7+
const probe = process.platform === 'win32' ? b => `where ${b}` : b => `which ${b}`;
8+
for (const bin of candidates) {
9+
try { execSync(probe(bin), { stdio: 'ignore', timeout: 3000 }); return bin; } catch {}
10+
}
11+
return candidates[0];
12+
}
13+
14+
const IS_WIN = process.platform === 'win32';
15+
const PYTHON = findBin('python3', 'python');
16+
const SHELL = IS_WIN ? 'cmd.exe' : findBin('bash', 'sh');
17+
const POWERSHELL = findBin('pwsh', 'powershell');
18+
const DENO = findBin('deno');
19+
const GO = findBin('go');
20+
const RUSTC = findBin('rustc');
21+
const GCC = findBin('gcc');
22+
const GPP = findBin('g++');
23+
const JAVA = findBin('java');
24+
const JAVAC = findBin('javac');
25+
26+
const SIGTERM_TIMEOUT = 5000;
27+
28+
function killChild(child) {
29+
try {
30+
if (IS_WIN) spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
31+
else { child.kill('SIGTERM'); setTimeout(() => { try { if (!child.killed) child.kill('SIGKILL'); } catch {} }, SIGTERM_TIMEOUT); }
32+
} catch {}
33+
}
34+
35+
function makeTmp(ext, content) {
36+
const dir = mkdtempSync(path.join(os.tmpdir(), 'glootie_'));
37+
const file = path.join(dir, `code${ext}`);
38+
writeFileSync(file, content);
39+
return { dir, file };
40+
}
41+
42+
function spawnOpts(cwd) {
43+
return { cwd: cwd || process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], detached: false };
44+
}
45+
46+
export function spawnProcess(runtime, code, cwd) {
47+
let tmpDir = null;
48+
const cleanup = () => { if (tmpDir) { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} tmpDir = null; } };
49+
50+
if (runtime === 'nodejs' || runtime === 'typescript') {
51+
const child = spawn('bun', ['-e', code], spawnOpts(cwd));
52+
return { child, cleanup };
53+
}
54+
if (runtime === 'python') {
55+
const child = spawn(PYTHON, ['-c', code], spawnOpts(cwd));
56+
return { child, cleanup };
57+
}
58+
if (runtime === 'powershell') {
59+
const child = spawn(POWERSHELL, ['-NoProfile', '-NonInteractive', '-Command', code], spawnOpts(cwd));
60+
return { child, cleanup };
61+
}
62+
if (runtime === 'cmd') {
63+
const child = spawn('cmd.exe', ['/c', code], spawnOpts(cwd));
64+
return { child, cleanup };
65+
}
66+
if (runtime === 'bash') {
67+
if (IS_WIN) {
68+
const { dir, file } = makeTmp('.ps1', `$ErrorActionPreference = 'Continue'\n${code}`);
69+
tmpDir = dir;
70+
const child = spawn(POWERSHELL, ['-NoProfile', '-NonInteractive', '-File', file], spawnOpts(cwd));
71+
return { child, cleanup };
72+
}
73+
const child = spawn(SHELL, ['-c', code], spawnOpts(cwd));
74+
return { child, cleanup };
75+
}
76+
if (runtime === 'deno') {
77+
const { dir, file } = makeTmp('.ts', code);
78+
tmpDir = dir;
79+
const child = spawn(DENO, ['run', '--no-check', file], spawnOpts(cwd));
80+
return { child, cleanup };
81+
}
82+
if (['go', 'rust', 'c', 'cpp'].includes(runtime)) {
83+
const ext = { go: '.go', rust: '.rs', c: '.c', cpp: '.cpp' }[runtime];
84+
const { dir, file } = makeTmp(ext, code);
85+
tmpDir = dir;
86+
const binExt = IS_WIN ? '.exe' : '';
87+
const binPath = path.join(dir, `code${binExt}`);
88+
if (runtime === 'go') {
89+
const child = spawn(GO, ['run', file], spawnOpts(cwd));
90+
return { child, cleanup };
91+
}
92+
const compiler = { rust: RUSTC, c: GCC, cpp: GPP }[runtime];
93+
const compileArgs = runtime === 'rust' ? [file, '-o', binPath] : [file, '-o', binPath, '-I', cwd];
94+
const compileChild = spawn(compiler, compileArgs, spawnOpts(cwd));
95+
return { child: compileChild, isCompile: true, binPath, cleanup, dir, killChild };
96+
}
97+
if (runtime === 'java') {
98+
const className = 'Main';
99+
const { dir, file } = makeTmp('.java', `public class ${className} {\n public static void main(String[] args) {\n${code.split('\n').map(l => ' ' + l).join('\n')}\n }\n}`);
100+
tmpDir = dir;
101+
const cpSep = IS_WIN ? ';' : ':';
102+
const cp = [dir, cwd].join(cpSep);
103+
const compileChild = spawn(JAVAC, ['-cp', cp, file.replace('.java', '.java')], spawnOpts(cwd));
104+
return { child: compileChild, isCompile: true, runtime: 'java', dir, cp, className, cleanup, killChild };
105+
}
106+
throw new Error(`Unsupported runtime: ${runtime}`);
107+
}
108+
109+
export { killChild };

0 commit comments

Comments
 (0)