Files
NetMesh/electron/scripts/scriptExecutionWorker.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

215 lines
7.6 KiB
JavaScript

"use strict";
const vm = require("node:vm");
const { parentPort } = require("node:worker_threads");
if (!parentPort) {
throw new Error("Script execution worker requires a parent port");
}
const pendingRequests = new Map();
let nextRequestId = 1;
let finished = false;
let started = false;
let heartbeat = null;
let heartbeatTimer = null;
let runtimeSnapshot = null;
let nct = null;
let maxPendingHostRequests = 128;
let maxLogNotifications = 512;
let maxTotalNotifications = 20_000;
let logNotificationCount = 0;
let totalNotificationCount = 0;
function beat() {
if (heartbeat) Atomics.store(heartbeat, 0, BigInt(Date.now()));
}
function serializeError(error) {
return {
name: error?.name || "Error",
message: error?.message || String(error),
stack: typeof error?.stack === "string" ? error.stack : undefined,
};
}
function reviveError(value) {
const error = new Error(value?.message || "Script API request failed");
error.name = value?.name || "Error";
if (typeof value?.stack === "string") error.stack = value.stack;
return error;
}
function updateSnapshot(snapshot) {
if (!snapshot || typeof snapshot !== "object") return;
if (snapshot.session && typeof snapshot.session === "object") {
Object.assign(runtimeSnapshot.session, snapshot.session);
}
if (snapshot.screen && typeof snapshot.screen === "object") {
Object.assign(runtimeSnapshot.screen, snapshot.screen);
}
}
function callHost(method, args = []) {
if (finished) return Promise.reject(new Error("Script execution finished"));
if (pendingRequests.size >= maxPendingHostRequests) {
throw new Error(
`Script exceeded the ${maxPendingHostRequests} pending host request limit`,
);
}
const requestId = nextRequestId++;
return new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject });
parentPort.postMessage({ type: "rpc", requestId, method, args });
});
}
function notifyHost(method, args = []) {
if (finished) return;
totalNotificationCount += 1;
if (totalNotificationCount > maxTotalNotifications) {
throw new Error(`Script exceeded the ${maxTotalNotifications} notification limit`);
}
if (method === "log" || method === "console.log") {
logNotificationCount += 1;
if (logNotificationCount > maxLogNotifications) {
throw new Error(`Script exceeded the ${maxLogNotifications} log notification limit`);
}
}
parentPort.postMessage({ type: "notify", method, args });
}
const sessionApi = {
get connected() { return runtimeSnapshot.session.connected; },
get name() { return runtimeSnapshot.session.name; },
get hostname() { return runtimeSnapshot.session.hostname; },
get username() { return runtimeSnapshot.session.username; },
sleep: (ms) => callHost("session.sleep", [ms]),
startLog: (path) => callHost("session.startLog", [path == null ? path : String(path)]),
stopLog: () => callHost("session.stopLog"),
disconnect: () => callHost("session.disconnect"),
};
const screenApi = {
send: (text, options) => callHost("screen.send", [
String(text ?? ""),
{ sensitive: options?.sensitive === true },
]),
sendLine: (text, options) => callHost("screen.sendLine", [
String(text ?? ""),
{ sensitive: options?.sensitive === true },
]),
waitFor: (pattern, timeoutMs) => callHost("screen.waitFor", [pattern, timeoutMs]),
waitForText: (text, timeoutMs) => callHost("screen.waitForText", [text, timeoutMs]),
waitForRegex: (pattern, timeoutMs) => callHost("screen.waitForRegex", [pattern, timeoutMs]),
waitForPrompt: (timeoutMs) => callHost("screen.waitForPrompt", [timeoutMs]),
waitForAny: (patterns, timeoutMs) => callHost("screen.waitForAny", [patterns, timeoutMs]),
getText: (startRow, endRow) => callHost("screen.getText", [startRow, endRow]),
get currentRow() { return runtimeSnapshot.screen.currentRow; },
get rows() { return runtimeSnapshot.screen.rows; },
get cols() { return runtimeSnapshot.screen.cols; },
clear: () => callHost("screen.clear"),
};
const dialogApi = {
alert: (message) => callHost("dialog.alert", [String(message ?? "")]),
confirm: (message) => callHost("dialog.confirm", [String(message ?? "")]),
prompt: (message, defaultValue, options) => callHost("dialog.prompt", [
String(message ?? ""),
String(defaultValue ?? ""),
{ sensitive: options?.sensitive === true },
]),
form: (spec) => callHost("dialog.form", [spec]),
select: (message, options, defaultValue) => callHost("dialog.select", [message, options, defaultValue]),
radio: (message, options, defaultValue) => callHost("dialog.radio", [message, options, defaultValue]),
checkbox: (message, defaultChecked) => callHost("dialog.checkbox", [message, defaultChecked]),
};
const progressApi = {
start(label, total) { notifyHost("progress.start", [label, total]); },
set(current, detail) { notifyHost("progress.set", [current, detail]); },
step(detail) { notifyHost("progress.step", [detail]); },
done() { notifyHost("progress.done"); },
};
parentPort.on("message", (message) => {
if (message?.type === "start") {
if (started) return;
started = true;
void run(message.config);
return;
}
if (message?.type === "snapshot") {
updateSnapshot(message.snapshot);
return;
}
if (message?.type !== "rpc-result") return;
const pending = pendingRequests.get(message.requestId);
if (!pending) return;
pendingRequests.delete(message.requestId);
updateSnapshot(message.snapshot);
if (message.ok) pending.resolve(message.value);
else pending.reject(reviveError(message.error));
});
async function run(config) {
maxPendingHostRequests = Math.max(1, Number(config.maxPendingHostRequests) || 128);
maxLogNotifications = Math.max(1, Number(config.maxLogNotifications) || 512);
maxTotalNotifications = Math.max(
maxLogNotifications,
Number(config.maxTotalNotifications) || 20_000,
);
heartbeat = new BigInt64Array(config.heartbeatBuffer);
const heartbeatIntervalMs = Math.max(2, Number(config.heartbeatIntervalMs) || 10);
runtimeSnapshot = {
session: {
connected: Boolean(config.snapshot?.session?.connected),
name: String(config.snapshot?.session?.name || ""),
hostname: String(config.snapshot?.session?.hostname || ""),
username: String(config.snapshot?.session?.username || ""),
},
screen: {
currentRow: Number(config.snapshot?.screen?.currentRow) || 0,
rows: Number(config.snapshot?.screen?.rows) || 24,
cols: Number(config.snapshot?.screen?.cols) || 80,
},
};
nct = {
session: sessionApi,
screen: screenApi,
dialog: dialogApi,
progress: progressApi,
version: String(config.version || "0.0.0"),
sleep: sessionApi.sleep,
log(message) { notifyHost("log", [String(message ?? "")]); },
};
heartbeatTimer = setInterval(beat, heartbeatIntervalMs);
beat();
try {
const sandbox = {
nct,
SharedArrayBuffer: undefined,
console: {
log: (...args) => notifyHost("console.log", [args.map((arg) => String(arg)).join(" ")]),
},
};
vm.createContext(sandbox, {
codeGeneration: { strings: false, wasm: false },
});
const script = new vm.Script(config.source, {
filename: config.filename || "netcatty-script.js",
});
const result = script.runInContext(sandbox, { displayErrors: true });
if (result && typeof result.then === "function") await result;
finished = true;
clearInterval(heartbeatTimer);
parentPort.postMessage({ type: "completed" });
} catch (error) {
finished = true;
clearInterval(heartbeatTimer);
parentPort.postMessage({ type: "failed", error: serializeError(error) });
}
}
parentPort.postMessage({ type: "ready" });