[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
"use strict";
const assert = require("node:assert/strict");
const EventEmitter = require("node:events");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const test = require("node:test");
const fileWatcherBridge = require("./fileWatcherBridge.cjs");
function createHarness(t) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-filewatch-test-"));
const localPath = path.join(tempDir, "edit.txt");
fs.writeFileSync(localPath, "initial");
const listenersByPath = new Map();
const originalWatchFile = fs.watchFile;
const originalUnwatchFile = fs.unwatchFile;
fs.watchFile = (filePath, _options, listener) => {
const listeners = listenersByPath.get(filePath) ?? new Set();
listeners.add(listener);
listenersByPath.set(filePath, listeners);
};
fs.unwatchFile = (filePath, listener) => {
const listeners = listenersByPath.get(filePath);
if (!listeners) return;
if (typeof listener === "function") listeners.delete(listener);
else listeners.clear();
if (listeners.size === 0) listenersByPath.delete(filePath);
};
const handlers = new Map();
fileWatcherBridge.init({
sftpClients: new Map(),
electronModule: {},
transferBridge: require("./transferBridge.cjs"),
});
fileWatcherBridge.registerHandlers({
handle(channel, handler) {
handlers.set(channel, handler);
},
});
const sender = new EventEmitter();
sender.id = Math.floor(Math.random() * 1_000_000) + 1;
sender.isDestroyed = () => false;
sender.send = () => {};
const event = { sender };
t.after(() => {
fileWatcherBridge.stopWatchersForSession(`sftp-${sender.id}`, false);
fs.watchFile = originalWatchFile;
fs.unwatchFile = originalUnwatchFile;
fs.rmSync(tempDir, { recursive: true, force: true });
});
return {
event,
handlers,
listenersByPath,
localPath,
sender,
sftpId: `sftp-${sender.id}`,
};
}
test("opening the same external edit twice reuses one polling watcher", async (t) => {
const harness = createHarness(t);
const payload = {
localPath: harness.localPath,
remotePath: "/remote/edit.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
};
const first = await harness.handlers.get("netcatty:filewatch:start")(harness.event, payload);
const second = await harness.handlers.get("netcatty:filewatch:start")(harness.event, payload);
const listed = await harness.handlers.get("netcatty:filewatch:list")(harness.event, {});
assert.equal(second.watchId, first.watchId);
assert.equal(harness.listenersByPath.get(harness.localPath)?.size, 1);
assert.equal(listed.length, 1);
});
test("concurrent identical starts still install only one polling watcher", async (t) => {
const harness = createHarness(t);
const payload = {
localPath: harness.localPath,
remotePath: "/remote/concurrent.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
};
const [first, second] = await Promise.all([
harness.handlers.get("netcatty:filewatch:start")(harness.event, payload),
harness.handlers.get("netcatty:filewatch:start")(harness.event, payload),
]);
assert.equal(second.watchId, first.watchId);
assert.equal(harness.listenersByPath.get(harness.localPath)?.size, 1);
});
test("stopping one watch removes only its listener from a shared local path", async (t) => {
const harness = createHarness(t);
const first = await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/first.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
const second = await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/second.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
assert.notEqual(first.watchId, second.watchId);
assert.equal(harness.listenersByPath.get(harness.localPath)?.size, 2);
await harness.handlers.get("netcatty:filewatch:stop")(harness.event, { watchId: first.watchId });
const listed = await harness.handlers.get("netcatty:filewatch:list")(harness.event, {});
assert.equal(harness.listenersByPath.get(harness.localPath)?.size, 1);
assert.deepEqual(listed.map((watch) => watch.watchId), [second.watchId]);
});
test("destroying the owner window releases its external file watches", async (t) => {
const harness = createHarness(t);
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/edit.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
harness.sender.emit("destroyed");
await new Promise((resolve) => setImmediate(resolve));
const listed = await harness.handlers.get("netcatty:filewatch:list")(harness.event, {});
assert.equal(listed.length, 0);
assert.equal(harness.listenersByPath.has(harness.localPath), false);
});
test("closing an SFTP session releases every watcher for that session", async (t) => {
const harness = createHarness(t);
const stoppedEvents = [];
harness.sender.send = (channel, payload) => {
if (channel === "netcatty:filewatch:stopped") stoppedEvents.push(payload);
};
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/first.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/second.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
fileWatcherBridge.stopWatchersForSession(harness.sftpId, false);
const listed = await harness.handlers.get("netcatty:filewatch:list")(harness.event, {});
assert.equal(listed.length, 0);
assert.equal(harness.listenersByPath.has(harness.localPath), false);
assert.equal(stoppedEvents.length, 2);
assert.deepEqual(new Set(stoppedEvents.map((event) => event.sftpId)), new Set([harness.sftpId]));
});
test("worker proxy releases renderer-owned watchers when its window is destroyed", async () => {
const handlers = new Map();
const requests = [];
const terminalWorkerManager = {
request(channel, payload, options) {
requests.push({ channel, payload, options });
return Promise.resolve(channel === "netcatty:filewatch:start" ? { watchId: "worker-watch" } : null);
},
};
fileWatcherBridge.registerHandlers({
handle(channel, handler) {
handlers.set(channel, handler);
},
}, { terminalWorkerManager });
const sender = new EventEmitter();
sender.id = 72;
const event = { sender };
await handlers.get("netcatty:filewatch:start")(event, {
localPath: "/tmp/edit.txt",
remotePath: "/remote/edit.txt",
sftpId: "sftp-worker",
});
sender.emit("destroyed");
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(requests.at(-1), {
channel: "netcatty:filewatch:releaseOwner",
payload: { webContentsId: 72, cleanupTempFiles: true },
options: { webContentsId: 72 },
});
});
test("session close cancels a watch whose initial stat is still pending", async (t) => {
const harness = createHarness(t);
const originalStat = fs.promises.stat;
let finishStat;
fs.promises.stat = () => new Promise((resolve) => {
finishStat = () => resolve({ mtimeMs: 1, size: 7 });
});
t.after(() => { fs.promises.stat = originalStat; });
const starting = harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/pending.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
await new Promise((resolve) => setImmediate(resolve));
fileWatcherBridge.stopWatchersForSession(harness.sftpId, true);
finishStat();
await assert.rejects(starting, /cancelled|closed/i);
const listed = await harness.handlers.get("netcatty:filewatch:list")(harness.event, {});
assert.equal(listed.length, 0);
assert.equal(harness.listenersByPath.has(harness.localPath), false);
});
test("external edit sync streams through the unified transfer runtime without buffering the whole file", async (t) => {
const harness = createHarness(t);
const transferBridge = require("./transferBridge.cjs");
const originalStartInternalTransfer = transferBridge.startInternalTransfer;
const originalReadFile = fs.promises.readFile;
let directPutCalls = 0;
let wholeFileReadCalls = 0;
const internalTransfers = [];
const sent = [];
transferBridge.startInternalTransfer = async (_event, payload) => {
internalTransfers.push(payload);
return { transferId: payload.transferId };
};
fs.promises.readFile = async (...args) => {
wholeFileReadCalls += 1;
return originalReadFile(...args);
};
harness.sender.send = (channel, payload) => {
sent.push({ channel, payload });
};
fileWatcherBridge.init({
sftpClients: new Map([[
harness.sftpId,
{
put: async () => { directPutCalls += 1; },
},
]]),
electronModule: {},
transferBridge,
});
t.after(() => {
transferBridge.startInternalTransfer = originalStartInternalTransfer;
fs.promises.readFile = originalReadFile;
});
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/edit.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
const listener = [...harness.listenersByPath.get(harness.localPath)][0];
const previousStat = fs.statSync(harness.localPath);
fs.writeFileSync(harness.localPath, "modified external edit");
const currentStat = fs.statSync(harness.localPath);
listener(
currentStat,
previousStat,
);
const deadline = Date.now() + 2_000;
while (!sent.some((event) => event.channel === "netcatty:filewatch:synced")) {
if (Date.now() >= deadline) throw new Error("Timed out waiting for external edit sync");
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.equal(internalTransfers.length, 1);
assert.equal(internalTransfers[0].sourcePath, harness.localPath);
assert.equal(internalTransfers[0].targetPath, "/remote/edit.txt");
assert.equal(internalTransfers[0].sourceType, "local");
assert.equal(internalTransfers[0].targetType, "sftp");
assert.equal(internalTransfers[0].targetSftpId, harness.sftpId);
assert.equal(wholeFileReadCalls, 0);
assert.equal(directPutCalls, 0);
});
test("closing the SFTP session cancels an active external edit stream", async (t) => {
const harness = createHarness(t);
const transferBridge = require("./transferBridge.cjs");
const originalStartInternalTransfer = transferBridge.startInternalTransfer;
const originalCancelTransfer = transferBridge.cancelTransfer;
let activeTransferId;
let finishTransfer;
const cancelledTransferIds = [];
transferBridge.startInternalTransfer = async (_event, payload) => {
activeTransferId = payload.transferId;
return new Promise((resolve) => {
finishTransfer = resolve;
});
};
transferBridge.cancelTransfer = async (_event, payload) => {
cancelledTransferIds.push(payload.transferId);
finishTransfer?.({ transferId: payload.transferId, cancelled: true, error: "Transfer cancelled" });
return { success: true };
};
fileWatcherBridge.init({
sftpClients: new Map([[harness.sftpId, {}]]),
electronModule: {},
transferBridge,
});
t.after(() => {
transferBridge.startInternalTransfer = originalStartInternalTransfer;
transferBridge.cancelTransfer = originalCancelTransfer;
});
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/edit.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
const listener = [...harness.listenersByPath.get(harness.localPath)][0];
const previousStat = fs.statSync(harness.localPath);
fs.writeFileSync(harness.localPath, "modified before close");
listener(fs.statSync(harness.localPath), previousStat);
const startDeadline = Date.now() + 2_000;
while (!activeTransferId) {
if (Date.now() >= startDeadline) throw new Error("Timed out waiting for external edit stream");
await new Promise((resolve) => setTimeout(resolve, 10));
}
fileWatcherBridge.stopWatchersForSession(harness.sftpId, false);
const cancelDeadline = Date.now() + 1_000;
while (cancelledTransferIds.length === 0) {
if (Date.now() >= cancelDeadline) throw new Error("Timed out waiting for stream cancellation");
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.deepEqual(cancelledTransferIds, [activeTransferId]);
});
test("session cleanup retries a temporarily locked external edit file", async (t) => {
const harness = createHarness(t);
const originalUnlink = fs.promises.unlink;
let unlinkAttempts = 0;
fs.promises.unlink = async (filePath) => {
if (filePath !== harness.localPath) return originalUnlink(filePath);
unlinkAttempts += 1;
if (unlinkAttempts === 1) {
const error = new Error("file is still in use");
error.code = "EBUSY";
throw error;
}
return originalUnlink(filePath);
};
t.after(() => { fs.promises.unlink = originalUnlink; });
await harness.handlers.get("netcatty:filewatch:registerTempFile")(harness.event, {
sftpId: harness.sftpId,
localPath: harness.localPath,
});
await harness.handlers.get("netcatty:filewatch:start")(harness.event, {
localPath: harness.localPath,
remotePath: "/remote/edit.txt",
sftpId: harness.sftpId,
encoding: "utf-8",
});
fileWatcherBridge.stopWatchersForSession(harness.sftpId, true);
const deadline = Date.now() + 2_000;
while (Date.now() < deadline) {
try {
await fs.promises.stat(harness.localPath);
} catch (error) {
if (error?.code === "ENOENT") break;
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.ok(unlinkAttempts >= 2, "a transient Windows file lock must be retried");
await assert.rejects(fs.promises.stat(harness.localPath), { code: "ENOENT" });
});
test("failed external app launch can unregister and immediately delete its temp file", async (t) => {
const harness = createHarness(t);
await harness.handlers.get("netcatty:filewatch:registerTempFile")(harness.event, {
sftpId: harness.sftpId,
localPath: harness.localPath,
});
const result = await harness.handlers.get("netcatty:filewatch:unregisterTempFile")(harness.event, {
sftpId: harness.sftpId,
localPath: harness.localPath,
});
assert.equal(result.success, true);
assert.equal(result.retained, false);
await assert.rejects(fs.promises.stat(harness.localPath), { code: "ENOENT" });
});