[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,482 @@
/* eslint-disable no-undef */
"use strict";
const { PORT_LIST_PS_COMMAND } = require("./windowsPowerShell.cjs");
/**
* Listening-port collectors.
* Parsing approach inspired by Portwatch (ss -tlnp + process field), adapted for
* remote SSH exec, UDP, IPv6, macOS netstat/lsof, and BusyBox netstat.
*/
const LISTEN_PORTS_INNER = [
'printf "%s\\n" "__NC_PORTS_BEGIN__"; ',
// Run every available collector. macOS often has netstat without useful PID
// columns; lsof fills that gap. Prefer merging over elif exclusivity.
'if command -v ss >/dev/null 2>&1; then ',
'printf "%s\\n" "__NC_SS__"; ',
"ss -H -tulnp 2>/dev/null || ss -tulnp 2>/dev/null || true; ",
"fi; ",
'if command -v netstat >/dev/null 2>&1; then ',
'printf "%s\\n" "__NC_NETSTAT__"; ',
// `-lntp` is TCP-only; prefer `-lntup`, else TCP+UDP separately. macOS uses `-anv -p`.
"if netstat -lntup 2>/dev/null; then :; ",
"elif netstat -lntp 2>/dev/null; then ",
"netstat -lnup 2>/dev/null || true; ",
"elif netstat -anv -p tcp >/dev/null 2>&1; then ",
// Prefer LISTEN-only TCP to keep stdout under maxBuffer on busy hosts.
"netstat -anv -p tcp 2>/dev/null | grep -i LISTEN || true; ",
"netstat -anv -p udp 2>/dev/null || true; ",
"else ",
"netstat -anp 2>/dev/null | grep -Ei 'LISTEN|^udp' || netstat -an 2>/dev/null | grep -Ei 'LISTEN|^udp' || true; ",
"fi; ",
"fi; ",
'if command -v lsof >/dev/null 2>&1; then ',
'printf "%s\\n" "__NC_LSOF__"; ',
"lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null || true; ",
// Idle-only: Linux has no UDP state names, so this no-ops there (ss/netstat cover UDP).
// Do not fall back to bare `-iUDP` — that dumps client binds as fake listeners.
"lsof -nP -iUDP -sUDP:Idle 2>/dev/null || true; ",
"fi; ",
'printf "%s\\n" "__NC_PORTS_END__"',
].join("");
const LISTEN_PORTS_SCRIPT = `exec sh -c ${JSON.stringify(LISTEN_PORTS_INNER)}`;
const LISTEN_PORTS_WINDOWS = [
'Write-Output "__NC_PORTS_BEGIN__"; ',
'Write-Output "__NC_WIN__"; ',
"$rows = @(); ",
"$rows += @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ",
"Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'tcp'}}); ",
// UDP has no listen state; drop high ephemeral ports on non-wildcard/non-loopback
// addresses so client binds do not flood the Ports tab.
"$rows += @(Get-NetUDPEndpoint -ErrorAction SilentlyContinue | Where-Object { ",
"$a = [string]$_.LocalAddress; ",
"$wildcard = ($a -eq '0.0.0.0' -or $a -eq '::' -or $a -eq '*'); ",
"$loopback = ($a -eq '127.0.0.1' -or $a -eq '::1'); ",
"if ($wildcard -or $loopback) { $true } else { $_.LocalPort -lt 49152 } ",
"} | Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'udp'}}); ",
"if ($rows.Count -gt 0) { $rows | ConvertTo-Json -Compress } else { Write-Output '[]' }; ",
'Write-Output "__NC_PORTS_END__"',
].join("");
const LISTEN_PORTS_MAX_BUFFER = 16 * 1024 * 1024;
function normalizeProtocol(raw) {
const text = String(raw || "").trim().toLowerCase();
if (text === "tcp" || text === "tcp4") return "tcp";
// macOS dual-stack (tcp46/udp46) is an IPv6 socket with v6only off; map to
// tcp6/udp6 so netstat rows merge with lsof IPv6 listeners.
if (text === "tcp6" || text === "tcp46") return "tcp6";
if (text === "udp" || text === "udp4") return "udp";
if (text === "udp6" || text === "udp46") return "udp6";
return "unknown";
}
function parseListenAddress(addr) {
const text = String(addr || "").trim();
if (!text) return null;
// macOS / BSD netstat: "*.22", "127.0.0.1.53", "::1.631", "fe80::1%lo0.22"
// Prefer dotted port even when the address contains ':' (IPv6).
const dotted = text.match(/^(.*)\.(\d+)$/);
if (dotted) {
const port = Number(dotted[2]);
if (Number.isFinite(port) && port >= 0 && port <= 65535) {
let address = dotted[1] || "*";
if (address.includes("%")) address = address.split("%")[0];
if (address === "*" || address === "0.0.0.0" || address === "::") address = "*";
return { address, port };
}
}
const lastColon = text.lastIndexOf(":");
if (lastColon <= 0) return null;
const portText = text.slice(lastColon + 1);
if (!/^\d+$/.test(portText)) return null;
const port = Number(portText);
if (!Number.isFinite(port) || port < 0 || port > 65535) return null;
let address = text.slice(0, lastColon);
if (address.startsWith("[") && address.endsWith("]")) {
address = address.slice(1, -1);
}
if (address.includes("%")) address = address.split("%")[0];
if (address === "*" || address === "0.0.0.0" || address === "::") {
address = "*";
}
return { address, port };
}
function parseSsProcesses(info) {
const text = String(info || "");
// users:(("app",pid=11,fd=3),("app",pid=12,fd=4))
const entries = [];
const re = /\("([^"]+)",pid=(\d+)/g;
let match = re.exec(text);
while (match) {
entries.push({ processName: match[1] || "", pid: Number(match[2]) });
match = re.exec(text);
}
if (entries.length) return entries;
return [{ processName: "", pid: null }];
}
function portKey(protocol, address, port, pid) {
// Include pid so SO_REUSEPORT / multi-process listeners stay distinct.
return `${normalizeProtocol(protocol)}|${address || "*"}|${port}|${pid == null ? "-" : pid}`;
}
function makePortId(protocol, address, port, pid) {
return `${normalizeProtocol(protocol)}|${address}|${port}|${pid == null ? "-" : pid}`;
}
function sameSocket(a, protocol, address, port) {
return a.protocol === protocol && a.address === address && a.port === port;
}
function pushPort(entries, byKey, row) {
if (!row || !Number.isFinite(row.port)) return;
const protocol = normalizeProtocol(row.protocol);
const address = row.address || "*";
const port = Number(row.port);
const pid = Number.isFinite(row.pid) && row.pid > 0 ? Number(row.pid) : null;
const processName = String(row.processName || "");
if (pid != null) {
// Drop anonymous placeholder once a PID-bearing collector reports the socket.
const anonKey = portKey(protocol, address, port, null);
const anon = byKey.get(anonKey);
if (anon) {
byKey.delete(anonKey);
const idx = entries.indexOf(anon);
if (idx >= 0) entries.splice(idx, 1);
}
} else {
for (const existing of byKey.values()) {
if (sameSocket(existing, protocol, address, port) && existing.pid != null) {
return;
}
}
}
const key = portKey(protocol, address, port, pid);
const existing = byKey.get(key);
if (existing) {
if (!existing.processName && processName) existing.processName = processName;
return;
}
const entry = {
id: makePortId(protocol, address, port, pid),
protocol,
address,
port,
pid,
processName,
};
byKey.set(key, entry);
entries.push(entry);
}
function isWildcardPeer(peer) {
const text = String(peer || "").trim();
return (
text === "*.*"
|| text === "*:*"
|| text === "0.0.0.0:*"
|| text === ":::*"
|| text === "[::]:*"
|| text === "*."
);
}
function parseSsOutput(stdout) {
const entries = [];
const byKey = new Map();
for (const line of String(stdout || "").split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
if (/^Netid\b/i.test(trimmed) || /^State\b/i.test(trimmed)) continue;
const parts = trimmed.split(/\s+/);
// Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
if (parts.length < 5) continue;
let protocol;
let state = "";
let localAddr;
let peerAddr = "";
let processField = "";
if (/^(tcp|udp)/i.test(parts[0])) {
protocol = parts[0];
// With state column: parts[1]=state, parts[4]=local, parts[5]=peer
if (parts.length >= 6 && (parts[4].includes(":") || parts[4].includes("."))) {
state = parts[1] || "";
localAddr = parts[4];
peerAddr = parts[5] || "";
processField = parts.slice(6).join(" ");
} else {
localAddr = parts[3];
peerAddr = parts[4] || "";
processField = parts.slice(5).join(" ");
}
} else {
continue;
}
const isUdp = /^udp/i.test(protocol);
if (!isUdp && state && !/^LISTEN$/i.test(state)) continue;
if (isUdp && state && !/^(UNCONN|IDLE|LISTEN)$/i.test(state)) continue;
if (isUdp && peerAddr && !isWildcardPeer(peerAddr)) continue;
const parsed = parseListenAddress(localAddr);
if (!parsed) continue;
for (const proc of parseSsProcesses(processField)) {
pushPort(entries, byKey, {
protocol,
address: parsed.address,
port: parsed.port,
pid: proc.pid,
processName: proc.processName,
});
}
}
return entries;
}
function parseNetstatOutput(stdout) {
const entries = [];
const byKey = new Map();
for (const line of String(stdout || "").split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
if (/^Proto\b/i.test(trimmed) || /^Active\b/i.test(trimmed)) continue;
// Linux: tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd
// BusyBox UDP often omits state: udp 0 0 127.0.0.1:53 0.0.0.0:* 456/dnsmasq
// macOS: tcp4 0 0 *.22 *.* LISTEN
const m = trimmed.match(
/^(tcp46|udp46|tcp[46]?|udp[46]?)\s+\d+\s+\d+\s+(\S+)\s+(\S+)(?:\s+(.*))?$/i,
);
if (!m) continue;
const protocol = m[1];
const local = m[2];
const peer = m[3];
const rest = String(m[4] || "").trim();
const isUdp = /^udp/i.test(protocol);
let state = "";
let pidField = "";
if (rest) {
const restParts = rest.split(/\s+/);
if (/^LISTEN$/i.test(restParts[0])) {
state = "LISTEN";
pidField = restParts.slice(1).join(" ");
} else if (isUdp && /^\d+\//.test(restParts[0])) {
// No State column — remainder is pid/program (may contain spaces).
pidField = rest;
} else if (!isUdp) {
// TCP without LISTEN (ESTABLISHED, or a bare pid token) is not a listener.
continue;
}
}
if (!isUdp) {
if (!/^LISTEN$/i.test(state)) continue;
} else if (!isWildcardPeer(peer)) {
continue;
}
const parsed = parseListenAddress(local);
if (!parsed) continue;
let pid = null;
let processName = "";
const pidMatch = String(pidField).match(/^(\d+)\/(.+)$/);
if (pidMatch) {
pid = Number(pidMatch[1]);
processName = pidMatch[2];
}
pushPort(entries, byKey, {
protocol,
address: parsed.address,
port: parsed.port,
pid,
processName,
});
}
return entries;
}
function parseLsofOutput(stdout) {
const entries = [];
const byKey = new Map();
for (const line of String(stdout || "").split("\n")) {
const trimmed = line.trim();
if (!trimmed || /^COMMAND\b/i.test(trimmed)) continue;
// COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
const parts = trimmed.split(/\s+/);
if (parts.length < 9) continue;
const processName = parts[0];
const pid = Number(parts[1]);
const typeField = String(parts[4] || "").toUpperCase();
const nodeField = String(parts[7] || "").toUpperCase();
const nameField = parts.slice(8).join(" ");
if (nameField.includes("->")) continue;
const isUdpNode = nodeField === "UDP" || /\bUDP\b/.test(nameField);
const isTcpNode = nodeField === "TCP" || /\bTCP\b/.test(nameField) || /\(LISTEN\)/i.test(nameField);
if (!isUdpNode && !isTcpNode) continue;
if (isTcpNode && !/\(LISTEN\)/i.test(nameField)) continue;
// NAME forms: "TCP *:80 (LISTEN)", "TCP [::1]:80 (LISTEN)", "*:22 (LISTEN)"
const cleaned = nameField
.replace(/^(?:TCP|UDP)\s+/i, "")
.replace(/\s+\((LISTEN|UDP)\)\s*$/i, "")
.trim();
const parsed = parseListenAddress(cleaned);
if (!parsed) continue;
let protocol = isUdpNode ? "udp" : "tcp";
// typeField is uppercased; match IPV6 / IPv6 before uppercasing would also work.
if (typeField.includes("IPV6") || parsed.address.includes(":")) {
protocol = isUdpNode ? "udp6" : "tcp6";
}
pushPort(entries, byKey, {
protocol,
address: parsed.address,
port: parsed.port,
pid: Number.isFinite(pid) ? pid : null,
processName,
});
}
return entries;
}
function parseWindowsPortsJson(stdout) {
const entries = [];
const byKey = new Map();
const text = String(stdout || "").trim();
if (!text) return entries;
let raw;
try {
raw = JSON.parse(text);
} catch {
return entries;
}
const list = Array.isArray(raw) ? raw : [raw];
for (const row of list) {
if (!row) continue;
const port = Number(row.LocalPort);
const pid = Number(row.OwningProcess);
let address = String(row.LocalAddress || "*");
const isV6 = address.includes(":");
if (address === "0.0.0.0" || address === "::" || address === "*") address = "*";
const protoRaw = String(row.Protocol || "tcp").toLowerCase();
const isUdp = protoRaw.startsWith("udp");
pushPort(entries, byKey, {
protocol: isUdp ? (isV6 ? "udp6" : "udp") : (isV6 ? "tcp6" : "tcp"),
address,
port,
pid: Number.isFinite(pid) && pid > 0 ? pid : null,
processName: "",
});
}
return entries;
}
function extractSection(stdout, beginMarker) {
const text = String(stdout || "");
const begin = text.indexOf(beginMarker);
if (begin < 0) return "";
const after = text.slice(begin + beginMarker.length);
const end = after.search(/\n__NC_(SS|NETSTAT|LSOF|WIN|PORTS_END)__/);
return end >= 0 ? after.slice(0, end) : after;
}
function sortPorts(entries) {
return entries.slice().sort((a, b) => a.port - b.port || a.protocol.localeCompare(b.protocol));
}
function parseListeningPorts(stdout) {
const text = String(stdout || "");
const entries = [];
const byKey = new Map();
const merge = (rows) => {
for (const row of rows) {
pushPort(entries, byKey, row);
}
};
if (text.includes("__NC_SS__")) merge(parseSsOutput(extractSection(text, "__NC_SS__")));
if (text.includes("__NC_NETSTAT__")) merge(parseNetstatOutput(extractSection(text, "__NC_NETSTAT__")));
if (text.includes("__NC_LSOF__")) merge(parseLsofOutput(extractSection(text, "__NC_LSOF__")));
if (text.includes("__NC_WIN__")) merge(parseWindowsPortsJson(extractSection(text, "__NC_WIN__")));
if (entries.length) return sortPorts(entries);
// Bare output without markers (fallback probes)
merge(parseSsOutput(text));
if (entries.length) return sortPorts(entries);
merge(parseNetstatOutput(text));
if (entries.length) return sortPorts(entries);
merge(parseLsofOutput(text));
return sortPorts(entries);
}
function createPortOpsApi({
execOnSession,
execOnLocalMachine,
isLocalSession,
process,
}) {
async function listListeningPorts(event, sessionId) {
if (!sessionId) return { success: false, error: "Missing sessionId" };
// Local Windows — already PowerShell (Base64-encoded).
if (isLocalSession(sessionId) && process.platform === "win32") {
const result = await execOnLocalMachine(PORT_LIST_PS_COMMAND, 12000, {
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
});
if (!result.success) return { success: false, error: result.error || "Failed to list ports" };
return { success: true, ports: parseListeningPorts(result.stdout) };
}
// POSIX first (Linux / macOS / BSD).
const posixResult = await execOnSession(event, sessionId, LISTEN_PORTS_SCRIPT, 12000, {
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
});
if (posixResult.pending) return { success: false, pending: true };
const posixOk = posixResult.success;
const posixPorts = posixOk ? parseListeningPorts(posixResult.stdout) : [];
// POSIX gave zero ports — try Windows PowerShell (Windows OpenSSH host).
if (!posixOk || posixPorts.length === 0) {
console.log(`[Ports] POSIX failed=${!posixOk}, got ${posixPorts.length} ports → trying PowerShell fallback.`);
const psResult = await execOnSession(event, sessionId, PORT_LIST_PS_COMMAND, 12000, {
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
});
if (psResult.pending) return { success: false, pending: true };
console.log(`[Ports] PowerShell result: success=${psResult.success}, code=${psResult.code}, error=${JSON.stringify(psResult.error||'').slice(0,120)}, stdoutSnippet=${JSON.stringify((psResult.stdout||'').slice(0,300))}`);
if (psResult.success) {
const psPorts = parseListeningPorts(psResult.stdout);
console.log(`[Ports] PowerShell parsed ${psPorts.length} ports.`);
if (psPorts.length > 0) {
return { success: true, ports: psPorts };
}
}
}
if (!posixOk) return { success: false, error: posixResult.error || "Failed to list ports" };
return { success: true, ports: posixPorts };
}
return {
listListeningPorts,
parseListeningPorts,
parseSsOutput,
parseNetstatOutput,
parseLsofOutput,
};
}
module.exports = {
createPortOpsApi,
parseListeningPorts,
parseSsOutput,
parseNetstatOutput,
parseLsofOutput,
LISTEN_PORTS_SCRIPT,
};