diff --git a/JS/edgechains/arakoodev/src/sync-rpc/lib/sync-rpc.ts b/JS/edgechains/arakoodev/src/sync-rpc/lib/sync-rpc.ts index fb6a881eb..960d64fda 100644 --- a/JS/edgechains/arakoodev/src/sync-rpc/lib/sync-rpc.ts +++ b/JS/edgechains/arakoodev/src/sync-rpc/lib/sync-rpc.ts @@ -1,10 +1,42 @@ -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import path from "node:path"; import fs from "node:fs"; import crypto from "node:crypto"; import os from "node:os"; +const activeTempDirs = new Set(); +let cleanupRegistered = false; + +function cleanupAllTempDirs() { + for (const dir of Array.from(activeTempDirs)) { + try { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + } catch { + // Ignore cleanup error during process teardown + } + } + activeTempDirs.clear(); +} + +function registerTempCleanupOnce() { + if (cleanupRegistered) return; + cleanupRegistered = true; + + process.once("exit", cleanupAllTempDirs); + process.once("SIGINT", () => { + cleanupAllTempDirs(); + process.exit(130); + }); + process.once("SIGTERM", () => { + cleanupAllTempDirs(); + process.exit(143); + }); +} + function createSyncRPC(filename: string) { + registerTempCleanupOnce(); const absolutePath = path.resolve(filename); if (!fs.existsSync(absolutePath)) { @@ -12,6 +44,8 @@ function createSyncRPC(filename: string) { } const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sync-rpc-")); + activeTempDirs.add(tempDir); + const hash = crypto.createHash("md5").update(absolutePath).digest("hex"); const wrapperPath = path.join(tempDir, `wrapper_${hash}.js`); @@ -20,8 +54,21 @@ function createSyncRPC(filename: string) { if (typeof fn !== 'function') { throw new Error('Exported value is not a function'); } + const parentPid = process.ppid; + if (parentPid && parentPid > 1) { + const watchdog = setInterval(() => { + try { + process.kill(parentPid, 0); + } catch (_) { + process.exit(0); + } + }, 1000); + if (watchdog && typeof watchdog.unref === 'function') watchdog.unref(); + } + process.once('SIGTERM', () => process.exit(0)); + process.once('SIGINT', () => process.exit(0)); process.on('message', (message) => { - fn(message) + Promise.resolve(fn(message)) .then(result => { process.send({ success: true, result }); }) @@ -33,15 +80,25 @@ function createSyncRPC(filename: string) { fs.writeFileSync(wrapperPath, wrapperCode); - return function syncRPC(args: any) { + const syncRPC = function syncRPC(args: any) { const scriptPath = path.join(tempDir, `script_${Date.now()}.js`); const scriptContent = ` const cp = require('child_process'); const child = cp.fork(${JSON.stringify(wrapperPath)}); + function reapChild() { + try { + if (!child.killed && child.exitCode === null) { + child.kill('SIGTERM'); + } + } catch (_) {} + } + process.once('SIGTERM', () => { reapChild(); process.exit(143); }); + process.once('SIGINT', () => { reapChild(); process.exit(130); }); + process.once('exit', reapChild); child.send(${JSON.stringify(args)}); child.on('message', (message) => { console.log(JSON.stringify(message)); - child.kill(); + reapChild(); process.exit(0); }); `; @@ -49,13 +106,11 @@ function createSyncRPC(filename: string) { fs.writeFileSync(scriptPath, scriptContent); try { - const output = execSync(`node ${scriptPath}`, { + const output = execFileSync(process.execPath, [scriptPath], { encoding: "utf8", stdio: ["pipe", "pipe", "inherit"], }); - fs.unlinkSync(scriptPath); - const trimmedOutput = output.trim(); try { @@ -71,7 +126,6 @@ function createSyncRPC(filename: string) { ); } } catch (error: any) { - fs.unlinkSync(scriptPath); if (error) { console.error("Execution error:", error.message); if (error.stderr) { @@ -80,8 +134,29 @@ function createSyncRPC(filename: string) { throw new Error(`Execution error: ${error.message}`); } throw error; + } finally { + try { + if (fs.existsSync(scriptPath)) { + fs.unlinkSync(scriptPath); + } + } catch { + // Preserve original error + } } }; + + (syncRPC as any).dispose = () => { + try { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + activeTempDirs.delete(tempDir); + } catch { + // Ignore cleanup failure + } + }; + + return syncRPC; } export = createSyncRPC as Function; diff --git a/JS/edgechains/arakoodev/src/sync-rpc/tests/sync-rpc.test.ts b/JS/edgechains/arakoodev/src/sync-rpc/tests/sync-rpc.test.ts new file mode 100644 index 000000000..2349a33f1 --- /dev/null +++ b/JS/edgechains/arakoodev/src/sync-rpc/tests/sync-rpc.test.ts @@ -0,0 +1,30 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import createSyncRPC from "../lib/sync-rpc"; + +describe("sync-rpc graceful shutdown and temp directory reaper", () => { + let workerModulePath: string; + + beforeEach(() => { + workerModulePath = path.join(os.tmpdir(), `edgechains-test-worker-${Date.now()}.cjs`); + fs.writeFileSync( + workerModulePath, + "module.exports = async function (input) { return { echoed: input }; };\n" + ); + }); + + afterEach(() => { + if (fs.existsSync(workerModulePath)) { + fs.unlinkSync(workerModulePath); + } + }); + + test("executes synchronous RPC call and cleans up temporary directory via dispose()", () => { + const rpc: any = createSyncRPC(workerModulePath); + const output = rpc({ hello: "world" }); + expect(JSON.parse(output)).toEqual({ echoed: { hello: "world" } }); + expect(typeof rpc.dispose).toBe("function"); + rpc.dispose(); + }); +});