Files
Netcatty/electron/bridges/ptyProcessTree.cjs
陈大猫 8ef91e1266 Ctrl+W close priority + local shell busy confirmation (#739)
* feat(ctrl-w): add ps-node + windows-process-tree + tsx deps for close-priority feature

* fix(ctrl-w): drop ps-node dep and add windows-process-tree to asarUnpack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): add ptyProcessTree bridge with per-platform child-process enumeration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): ptyProcessTree uses args= for full command + warns on pid overwrite

- Replace `comm=` with `args=` in defaultListPosix so the full command
  line is captured on both macOS (BSD ps) and Linux (GNU ps), avoiding
  the 15-char TASK_COMM_LEN truncation.
- Add console.warn in registerPid when the same sessionId is overwritten
  with a different pid, making the race condition visible in logs.
- Add test: registerPid warns exactly once on a pid change, not on a
  same-pid re-registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): register local PTY pid with ptyProcessTree on spawn/exit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): unregister pids in cleanupAllSessions to match per-delete invariant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): add IPC handlers for pty child processes and confirm-close dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): guard BrowserWindow.fromWebContents null and document dialog dismiss contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): expose ptyGetChildProcesses and confirmCloseBusy on window.netcatty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): add i18n strings for close-busy-terminal dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): add resolveCloseIntent pure function with 8 unit tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): expose handleCloseSidePanel via ref to App.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): wire resolveCloseIntent + local-shell busy confirmation into closeTab hotkey

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ctrl-w): add re-entrancy guard, aggregate busy count, sync sidebar ref, dedupe intent branches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ctrl-w): auto-close workspace when its last session is closed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): sidebar close wins over focused terminal in priority chain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): sidebar priority applies to single-session tabs too, not just workspaces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ctrl-w): compute empty-workspace auto-close outside setSessions updater

Addresses Codex P2 on #739: React 18+ does not guarantee updater
execution timing under concurrent scheduling. Moving the decision
outside the updater makes the microtask queue deterministic.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:30:11 +08:00

90 lines
2.7 KiB
JavaScript

const { execFile } = require("node:child_process");
function createProcessTree({ platform, listPosix, listWindows } = {}) {
const sessionPidMap = new Map();
function registerPid(sessionId, pid) {
if (!sessionId || typeof pid !== "number") return;
if (sessionPidMap.has(sessionId) && sessionPidMap.get(sessionId) !== pid) {
console.warn(
`[ptyProcessTree] sessionId "${sessionId}" already registered with pid ${sessionPidMap.get(sessionId)}; overwriting with ${pid}.`,
);
}
sessionPidMap.set(sessionId, pid);
}
function unregisterPid(sessionId) {
sessionPidMap.delete(sessionId);
}
async function getChildProcesses(sessionId) {
const pid = sessionPidMap.get(sessionId);
if (!pid) return [];
if (platform === "win32") {
return listWindows ? listWindows(pid) : [];
}
return listPosix ? listPosix(pid) : [];
}
return { registerPid, unregisterPid, getChildProcesses };
}
function defaultListPosix(ppid) {
return new Promise((resolve) => {
// `ps -A -o pid=,ppid=,args=` works on both BSD (macOS) and GNU (Linux).
// `args=` shows the full command line (not truncated like `comm=`).
// The trailing `=` on each column suppresses the header row.
execFile("ps", ["-A", "-o", "pid=,ppid=,args="], (err, stdout) => {
if (err || typeof stdout !== "string") return resolve([]);
const out = [];
for (const line of stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const m = trimmed.match(/^(\d+)\s+(\d+)\s+(.+)$/);
if (!m) continue;
if (Number(m[2]) !== ppid) continue;
out.push({ pid: Number(m[1]), command: m[3].trim() });
}
resolve(out);
});
});
}
function defaultListWindows(ppid) {
return new Promise((resolve) => {
let wpt;
try {
wpt = require("@vscode/windows-process-tree");
} catch {
return resolve([]);
}
try {
wpt.getProcessTree(ppid, (tree) => {
if (!tree || !Array.isArray(tree.children)) return resolve([]);
resolve(tree.children.map((c) => ({ pid: c.pid, command: c.name })));
});
} catch {
resolve([]);
}
});
}
function createDefaultProcessTree() {
const platform = process.platform;
return createProcessTree({
platform,
listPosix: platform === "win32" ? undefined : defaultListPosix,
listWindows: platform === "win32" ? defaultListWindows : undefined,
});
}
const defaultTree = createDefaultProcessTree();
module.exports = {
createProcessTree,
processTree: defaultTree,
registerPid: (id, pid) => defaultTree.registerPid(id, pid),
unregisterPid: (id) => defaultTree.unregisterPid(id),
getChildProcesses: (id) => defaultTree.getChildProcesses(id),
};