Files
Netcatty/electron/bridges/systemManagerBridge.cjs
陈大猫 ecadc1fc2d
Some checks failed
build-packages / dedupe push run (push) Has been cancelled
build-packages / dedupe result (push) Has been cancelled
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 / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-x64' || 'build-linux-x64' }} (push) Has been cancelled
build-packages / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-arm64' || 'build-linux-arm64' }} (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
[codex] Enable sudo fallback for Docker panel (#1466)
* Enable sudo fallback for Docker panel

* Prefer sudo for Docker panel commands

* Use pending saved sudo password immediately

* Try plain Docker before sudo fallback

* Detect Docker before sudo fallback

* Add sudo fallback for Docker popup commands

* Harden Docker popup sudo fallback
2026-06-14 10:47:21 +08:00

309 lines
12 KiB
JavaScript

"use strict";
const { createExecOnSessionApi } = require("./systemManager/execOnSession.cjs");
const { createTmuxOpsApi } = require("./systemManager/tmuxOps.cjs");
const { createDockerOpsApi } = require("./systemManager/dockerOps.cjs");
const CAPABILITY_SCRIPT_POSIX = [
"exec sh -c ",
"'",
'printf "%s\\n" "__NC_OS__=$(uname -s)"; ',
'command -v tmux >/dev/null 2>&1 && printf "%s\\n" __NC_TMUX__=1; ',
'command -v docker >/dev/null 2>&1 && printf "%s\\n" __NC_DOCKER__=1',
"'",
].join("");
const PROCESS_LIST_SCRIPT_POSIX = [
"exec sh -c ",
"'",
// Safety cap: head -n 2000 prevents maxBuffer/timeout on process-dense hosts.
// This is NOT a functional limit — monitored processes still show accurate metrics.
"ps -eo pid= -o ppid= -o user= -o stat= -o pcpu= -o pmem= -o rss= -o vsz= -o etime= -o args= 2>/dev/null | head -n 2000",
"'",
].join("");
function parseCapabilities(stdout, isLocal, localPlatform) {
const text = stdout || "";
let targetOs = "unknown";
if (isLocal) {
if (localPlatform === "linux") targetOs = "linux";
else if (localPlatform === "darwin") targetOs = "darwin";
else if (localPlatform === "win32") targetOs = "win32";
} else {
const osMatch = text.match(/__NC_OS__=([^\r\n]+)/);
const uname = (osMatch?.[1] || "").trim().toLowerCase();
if (uname.includes("linux")) targetOs = "linux";
else if (uname.includes("darwin")) targetOs = "darwin";
else if (uname.includes("windows") || uname.includes("mingw")) targetOs = "win32";
}
const hasTmux = text.includes("__NC_TMUX__=1");
const hasDocker = text.includes("__NC_DOCKER__=1");
return { targetOs, hasTmux, hasDocker, probedAt: Date.now() };
}
function parseProcessLines(stdout) {
const processes = [];
for (const line of (stdout || "").split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const m = trimmed.match(/^(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
if (!m) continue;
processes.push({
pid: Number(m[1]),
ppid: Number(m[2]),
user: m[3],
stat: m[4],
cpuPercent: Number(m[5]),
memPercent: Number(m[6]),
rssKb: Number(m[7]),
vszKb: Number(m[8]),
elapsed: m[9],
command: m[10],
});
}
return processes;
}
const ALLOWED_SIGNALS = new Set([
"TERM", "KILL", "STOP", "CONT", "HUP", "INT", "USR1", "USR2",
"1", "2", "9", "15", "18", "19",
]);
function buildProcessSignalCommand(pid, signal, nice) {
if (nice !== undefined && nice !== null) {
const n = Number(nice);
if (!Number.isFinite(n) || n < -20 || n > 19) {
return { error: "Invalid nice value" };
}
return { command: `renice ${Math.trunc(n)} -p ${Number(pid)}` };
}
const sig = String(signal || "TERM").toUpperCase();
if (!ALLOWED_SIGNALS.has(sig)) {
return { error: "Invalid signal" };
}
const numericPid = Number(pid);
if (!Number.isFinite(numericPid) || numericPid <= 0) {
return { error: "Invalid pid" };
}
if (sig === "KILL" || sig === "9") {
return { command: `kill -9 ${numericPid}` };
}
if (sig === "TERM" || sig === "15") {
return { command: `kill -15 ${numericPid}` };
}
if (/^\d+$/.test(sig)) {
return { command: `kill -${sig} ${numericPid}` };
}
return { command: `kill -s ${sig} ${numericPid}` };
}
function createSystemManagerBridge(deps) {
const {
getSessions,
execOnEtSession,
ensureMoshStatsConnection,
process,
} = deps;
const execApi = createExecOnSessionApi({
sessions: { get: (id) => getSessions()?.get(id) },
execOnEtSession,
ensureMoshStatsConnection,
});
const { execOnSession, execOnLocalMachine, isLocalSession, getSession } = execApi;
const tmuxOps = createTmuxOpsApi({ execOnSession });
const dockerOps = createDockerOpsApi({ execOnSession, getSession });
async function probeCapabilities(event, payload) {
const sessionId = payload?.sessionId;
if (!sessionId) return { success: false, error: "Missing sessionId" };
if (isLocalSession(sessionId)) {
const platform = process.platform;
let script = CAPABILITY_SCRIPT_POSIX;
if (platform === "win32") {
const result = await execOnLocalMachine(
"$os=[System.Environment]::OSVersion.Platform; Write-Output \"__NC_OS__=Windows\"; if (Get-Command tmux -ErrorAction SilentlyContinue) { Write-Output '__NC_TMUX__=1' }; docker info 2>$null; if ($LASTEXITCODE -eq 0) { Write-Output '__NC_DOCKER__=1' }",
8000,
);
if (!result.success) return { success: false, error: result.error || "Probe failed" };
return { success: true, capabilities: parseCapabilities(result.stdout, true, platform) };
}
const result = await execOnLocalMachine(
script.replace(/^exec sh -c '/, "").replace(/'$/, ""),
8000,
);
if (!result.success) {
const fallback = await execOnLocalMachine("uname -s; command -v tmux; command -v docker >/dev/null 2>&1 && echo docker_ok", 8000);
if (!fallback.success) return { success: false, error: fallback.error || "Probe failed" };
const text = fallback.stdout || "";
return {
success: true,
capabilities: {
targetOs: platform === "linux" ? "linux" : platform === "darwin" ? "darwin" : "unknown",
hasTmux: text.includes("tmux") && !text.includes("not found"),
hasDocker: text.includes("docker_ok"),
probedAt: Date.now(),
},
};
}
return { success: true, capabilities: parseCapabilities(result.stdout, true, platform) };
}
const result = await execOnSession(event, sessionId, CAPABILITY_SCRIPT_POSIX, 8000);
if (result.pending) return { success: false, pending: true };
if (!result.success) return { success: false, error: result.error || "Probe failed" };
return {
success: true,
capabilities: parseCapabilities(result.stdout, false, process.platform),
};
}
async function listProcesses(event, payload) {
const sessionId = payload?.sessionId;
if (!sessionId) return { success: false, error: "Missing sessionId" };
if (isLocalSession(sessionId) && process.platform === "win32") {
// Safety cap: -First 2000 prevents maxBuffer/timeout on process-dense hosts.
// This is NOT a functional limit — monitored processes still show accurate metrics.
const result = await execOnLocalMachine(
"Get-CimInstance Win32_Process | Sort-Object KernelModeTime -Descending | Select-Object -First 2000 ProcessId,ParentProcessId,Name,WorkingSetSize | ConvertTo-Json -Compress",
10000,
);
if (!result.success) return { success: false, error: result.error };
try {
const raw = JSON.parse(result.stdout || "[]");
const list = Array.isArray(raw) ? raw : [raw];
const processes = list.map((p) => ({
pid: Number(p.ProcessId),
ppid: Number(p.ParentProcessId) || 0,
user: "",
stat: "R",
cpuPercent: 0,
memPercent: 0,
rssKb: Math.round((Number(p.WorkingSetSize) || 0) / 1024),
vszKb: 0,
elapsed: "",
command: String(p.Name || ""),
}));
return { success: true, processes };
} catch {
return { success: false, error: "Failed to parse process list" };
}
}
const result = await execOnSession(event, sessionId, PROCESS_LIST_SCRIPT_POSIX, 12000);
if (result.pending) return { success: false, pending: true };
if (!result.success) return { success: false, error: result.error || "Failed to list processes" };
return { success: true, processes: parseProcessLines(result.stdout) };
}
async function signalProcess(event, payload) {
const { sessionId, pid, signal = "TERM", nice } = payload || {};
if (!sessionId || !pid) return { success: false, error: "Missing sessionId or pid" };
const built = buildProcessSignalCommand(pid, signal, nice);
if (built.error) return { success: false, error: built.error };
const result = await execOnSession(event, sessionId, `exec sh -c ${JSON.stringify(built.command)}`, 5000);
if (!result.success) return { success: false, error: result.error };
return { success: true, code: result.code };
}
async function listTmuxSessions(event, payload) {
const sessionId = typeof payload === "string" ? payload : payload?.sessionId;
if (!sessionId) return { success: false, error: "Missing sessionId" };
return tmuxOps.listSessions(event, sessionId);
}
async function createTmuxSession(event, payload) {
return tmuxOps.createSession(event, payload);
}
async function listTmuxWindows(event, payload) {
return tmuxOps.listWindows(event, payload);
}
async function listTmuxPanes(event, payload) {
return tmuxOps.listPanes(event, payload);
}
async function listTmuxClients(event, payload) {
return tmuxOps.listClients(event, payload);
}
async function tmuxAction(event, payload) {
const result = await tmuxOps.tmuxAction(event, payload);
if (result.success === false && result.error) {
return { success: false, error: result.error || result.stderr };
}
if (result.success === false) {
return { success: false, error: result.stderr || "tmux command failed" };
}
return { success: true };
}
async function listDockerContainers(event, payload) {
const sessionId = payload?.sessionId;
if (!sessionId) return { success: false, error: "Missing sessionId" };
return dockerOps.listContainers(event, sessionId);
}
async function listDockerImages(event, payload) {
const sessionId = payload?.sessionId;
if (!sessionId) return { success: false, error: "Missing sessionId" };
return dockerOps.listImages(event, sessionId);
}
async function dockerStats(event, payload) {
return dockerOps.getStats(event, payload);
}
async function dockerInspect(event, payload) {
return dockerOps.inspectContainer(event, payload);
}
async function dockerImageInspect(event, payload) {
return dockerOps.inspectImage(event, payload);
}
async function dockerAction(event, payload) {
const result = await dockerOps.containerAction(event, payload);
if (result.success === false) {
return { success: false, error: result.error || result.stderr || "docker command failed" };
}
return { success: true };
}
async function dockerImageAction(event, payload) {
const result = await dockerOps.imageAction(event, payload);
if (result.success === false) {
return { success: false, error: result.error || result.stderr || "docker command failed" };
}
return { success: true, output: result.stdout };
}
function registerHandlers(ipcMain) {
ipcMain.handle("netcatty:system:probeCapabilities", probeCapabilities);
ipcMain.handle("netcatty:system:listProcesses", listProcesses);
ipcMain.handle("netcatty:system:signalProcess", signalProcess);
ipcMain.handle("netcatty:system:listTmuxSessions", listTmuxSessions);
ipcMain.handle("netcatty:system:createTmuxSession", createTmuxSession);
ipcMain.handle("netcatty:system:listTmuxWindows", listTmuxWindows);
ipcMain.handle("netcatty:system:listTmuxPanes", listTmuxPanes);
ipcMain.handle("netcatty:system:listTmuxClients", listTmuxClients);
ipcMain.handle("netcatty:system:tmuxAction", tmuxAction);
ipcMain.handle("netcatty:system:listDockerContainers", listDockerContainers);
ipcMain.handle("netcatty:system:listDockerImages", listDockerImages);
ipcMain.handle("netcatty:system:dockerStats", dockerStats);
ipcMain.handle("netcatty:system:dockerInspect", dockerInspect);
ipcMain.handle("netcatty:system:dockerImageInspect", dockerImageInspect);
ipcMain.handle("netcatty:system:dockerAction", dockerAction);
ipcMain.handle("netcatty:system:dockerImageAction", dockerImageAction);
}
return { registerHandlers, probeCapabilities, listProcesses };
}
module.exports = { createSystemManagerBridge };