Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 83 additions & 8 deletions JS/edgechains/arakoodev/src/sync-rpc/lib/sync-rpc.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
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<string>();
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)) {
throw new Error(`File not found: ${absolutePath}`);
}

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`);

Expand All @@ -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 });
})
Expand All @@ -33,29 +80,37 @@ 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);
});
`;

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 {
Expand All @@ -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) {
Expand All @@ -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;
30 changes: 30 additions & 0 deletions JS/edgechains/arakoodev/src/sync-rpc/tests/sync-rpc.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading