[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
214
electron/scripts/scriptExecutionWorker.cjs
Normal file
214
electron/scripts/scriptExecutionWorker.cjs
Normal file
@@ -0,0 +1,214 @@
|
||||
"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" });
|
||||
Reference in New Issue
Block a user