From 8ff4917d0c75aa4f3d34c0296f72e254c376547f Mon Sep 17 00:00:00 2001 From: cheney Date: Fri, 16 Oct 2026 17:29:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(kit):=20=E5=AE=9E=E7=8E=B0=20tcpc=20tcp=20?= =?UTF-8?q?=E6=8A=A5=E6=96=87=E5=8F=91=E6=94=B6=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tcpc 支持 hex/文本发送、--headlen 大端长度头自动填充与收包校验、超时控制 refactor(killport): 支持 --dry 模式,Linux 下通过 docker-proxy 定位容器内真实进程(拆分至 linux/portpid.js) --- kit/src/glance/monitor.js | 270 +----------------------------------- kit/src/index.js | 25 +++- kit/src/linux/net.js | 48 ++++--- kit/src/linux/portpid.js | 278 ++++++++++++++++++++++++++++++++++++++ kit/src/net.js | 116 ++++++++++++++-- kit/src/windows/net.js | 7 +- 6 files changed, 445 insertions(+), 299 deletions(-) create mode 100644 kit/src/linux/portpid.js diff --git a/kit/src/glance/monitor.js b/kit/src/glance/monitor.js index 51ee941..8438624 100644 --- a/kit/src/glance/monitor.js +++ b/kit/src/glance/monitor.js @@ -3,16 +3,10 @@ * - CPU 占用率:两次采样 /proc//stat 与 /proc/stat 差值计算,内部休眠约 1 秒 * - 内存占用:/proc//status 的 VmRSS,单位 MB * - 句柄数:/proc//fd 目录条目数 - * - 端口 -> pid: - * 1. 宿主网络命名空间中监听该端口的 fd 属主(普通端口服务,如 sshd) - * 2. 若属主是 docker-proxy,则解析其 -container-ip/-container-port, - * 在容器网络命名空间内按容器内端口查找持有监听 fd 的真实应用进程 - * (处理 0.0.0.0:6380->6379 这类端口映射不一致,并用 fib_trie 的 /32 - * 本地地址匹配容器 IP 以区分布局在网段相同、内网端口相同的多个容器) - * 3. 宿主无监听时(userland-proxy 关闭),直接扫描非宿主 netns 的监听进程 + * - 端口 -> pid:端口解析逻辑见 ../linux/portpid.js(含 docker 端口发布) */ const fs = require("fs"); -const path = require("path"); +const portpid = require("../linux/portpid"); const os = require("os"); function isLinux() { @@ -79,266 +73,8 @@ async function collectPid(pid) { } } -/** - * 读取进程所在网络命名空间标识 - * @param {number} pid - * @returns {string|null} - */ -function readNetNs(pid) { - try { - return fs.readlinkSync(`/proc/${pid}/ns/net`); - } catch (e) { - return null; - } -} - -/** - * 读取一个网络命名空间内监听指定端口的 socket inode 集合 - * @param {string} netNs - * @param {number} port - * @returns {Set} - */ -function listenInodesInNetNs(netNs, port) { - const out = new Set(); - for (const entry of fs.readdirSync("/proc")) { - if (!/^\d+$/.test(entry)) { - continue; - } - if (readNetNs(Number(entry)) !== netNs) { - continue; - } - for (const file of ["tcp", "tcp6"]) { - let text; - try { - text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8"); - } catch (e) { - continue; - } - for (const line of text.split("\n").slice(1)) { - const parts = line.trim().split(/\s+/); - if (parts.length < 10 || parts[3] !== "0A") { - continue; - } - const local = parts[1]; - const idx = local.lastIndexOf(":"); - if (idx < 0) { - continue; - } - if (parseInt(local.slice(idx + 1), 16) === Number(port)) { - out.add(parts[9]); - } - } - } - } - return out; -} - -/** - * 在指定网络命名空间内查找持有这些 socket inode 的进程 pid(归一化为 Tgid) - * @param {string} netNs - * @param {Set} inodes - * @returns {number|null} - */ -function ownerInNetNs(netNs, inodes) { - for (const entry of fs.readdirSync("/proc")) { - if (!/^\d+$/.test(entry)) { - continue; - } - if (readNetNs(Number(entry)) !== netNs) { - continue; - } - const fdDir = `/proc/${entry}/fd`; - let names; - try { - names = fs.readdirSync(fdDir); - } catch (e) { - continue; - } - for (const fd of names) { - try { - const target = fs.readlinkSync(path.join(fdDir, fd)); - const m = target.match(/^socket:\[(\d+)\]$/); - if (m && inodes.has(m[1])) { - return tgidOf(Number(entry)); - } - } catch (e) { - // 忽略无法读取的 fd - } - } - } - return null; -} - -/** - * 归一化线程 pid 为进程 Tgid - * @param {number} pid - * @returns {number} - */ -function tgidOf(pid) { - try { - const m = fs.readFileSync(`/proc/${pid}/status`, "utf8").match(/^Tgid:\s+(\d+)/m); - return m ? Number(m[1]) : pid; - } catch (e) { - return pid; - } -} - -/** - * 读取进程启动参数 - * @param {number} pid - * @returns {string[]} - */ -function cmdlineOf(pid) { - try { - return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0"); - } catch (e) { - return []; - } -} - -/** - * 读取网络命名空间内声明的本地 IPv4 地址集合(/proc//net/fib_trie) - * @param {string} netNs - * @returns {Set} - */ -function netNsIpv4s(netNs) { - const ips = new Set(); - for (const entry of fs.readdirSync("/proc")) { - if (!/^\d+$/.test(entry)) { - continue; - } - if (readNetNs(Number(entry)) !== netNs) { - continue; - } - try { - const text = fs.readFileSync(`/proc/${entry}/net/fib_trie`, "utf8"); - const re = /\b(\d+\.\d+\.\d+\.\d+)\b/g; - let m; - while ((m = re.exec(text)) !== null) { - ips.add(m[1]); - } - break; - } catch (e) { - // 继续尝试下一个候选 - } - } - return ips; -} - -/** - * 根据端口查找监听进程 pid - * - * 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致 - * (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位。 - * @param {number} port - * @returns {number|null} - */ -function findPortPid(port) { - const hostNs = readNetNs(1); - if (hostNs === null) { - return null; - } - - const hostInodes = listenInodesInNetNs(hostNs, port); - if (hostInodes.size > 0) { - const owner = ownerInNetNs(hostNs, hostInodes); - if (owner !== null) { - const cmd = cmdlineOf(owner); - if (cmd.length > 0 && cmd[0].includes("docker-proxy")) { - const args = {}; - for (let i = 1; i < cmd.length - 1; i++) { - if (cmd[i].startsWith("-")) { - args[cmd[i]] = cmd[i + 1]; - } - } - const cip = args["-container-ip"]; - const cport = Number(args["-container-port"]); - if (Number.isFinite(cport) && cport > 0) { - // 所有可能持有该容器内端口的网络命名空间 - const groups = new Map(); // netNs -> Set - for (const entry of fs.readdirSync("/proc")) { - if (!/^\d+$/.test(entry)) { - continue; - } - const ns = readNetNs(Number(entry)); - if (ns === null || ns === hostNs) { - continue; - } - const inodes = new Set(); - for (const file of ["tcp", "tcp6"]) { - let text; - try { - text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8"); - } catch (e) { - continue; - } - for (const line of text.split("\n").slice(1)) { - const parts = line.trim().split(/\s+/); - if (parts.length < 10 || parts[3] !== "0A") { - continue; - } - const idx = parts[1].lastIndexOf(":"); - if (idx >= 0 && parseInt(parts[1].slice(idx + 1), 16) === cport) { - inodes.add(parts[9]); - } - } - } - if (inodes.size > 0) { - groups.set(ns, inodes); - } - } - // 优先选 fib_trie 本地地址与容器 IP 一致的命名空间 - if (cip) { - for (const [ns, inodes] of groups) { - if (netNsIpv4s(ns).has(cip)) { - const pid = ownerInNetNs(ns, inodes); - if (pid !== null) { - return pid; - } - } - } - } - // 退而求其次:任意持有该容器内端口监听的命名空间的属主 - for (const [ns, inodes] of groups) { - const pid = ownerInNetNs(ns, inodes); - if (pid !== null) { - return pid; - } - } - } - // 容器应用未就绪:退回 docker-proxy,保留可见性 - return owner; - } - return owner; - } - } - - // 宿主无监听(userland-proxy 关闭):扫描非宿主命名空间内监听该端口的进程 - const groups = new Map(); - for (const entry of fs.readdirSync("/proc")) { - if (!/^\d+$/.test(entry)) { - continue; - } - const ns = readNetNs(Number(entry)); - if (ns === null || ns === hostNs) { - continue; - } - const inodes = listenInodesInNetNs(ns, port); - if (inodes.size > 0) { - groups.set(ns, inodes); - } - } - for (const [ns, inodes] of groups) { - const pid = ownerInNetNs(ns, inodes); - if (pid !== null) { - return pid; - } - } - return null; -} - module.exports = { isLinux, collectPid, - findPortPid + findPortPid: portpid.findPortPid }; \ No newline at end of file diff --git a/kit/src/index.js b/kit/src/index.js index 0fb6a17..f71a792 100644 --- a/kit/src/index.js +++ b/kit/src/index.js @@ -184,9 +184,10 @@ parser.addCmdLine("dockersh [--file]", "反向获取 docker 容器 }); -parser.addCmdLine("killport ", "释放端口", function (cli) { +parser.addCmdLine("killport [-dry]", "释放端口;-dry 仅显示端口与进程信息,不执行清理", async function (cli) { let port = cli.getParamValue("port") - net.killport( parseInt(port, 10) ); + let dry = cli.hasFlag("dry") + await net.killport( parseInt(port, 10), dry ); }); parser.addCmdLine("glance [--pid] [--port] [--report] [--startTime] [--endTime]", "进程监控:--pid/--port 采集监控数据(仅 Linux);--report 生成报告 HTML,传端口号时导出到临时目录并开启 http 服务在线查看;", async function (cli) { @@ -426,8 +427,24 @@ parser.addCmdLine("crlf [-d] [-r] [--path] [--filter-name] [--filter-path] 0 && cmd[0] ? cmd[0] : `pid=${pid}`; + + if (program.includes("docker-proxy")) { + // 容器内应用未就绪时 findPortPid 会退回 docker-proxy 自身; + // 杀掉它既停不掉容器内的服务,也会被 docker 重新拉起,只清理真实进程。 + $logger.error(`端口=${port} 由 docker-proxy 发布,但未定位到容器内的真实应用进程,已跳过清理(避免误杀 docker 进程)。`); + return; + } + + $logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${program};`); + + if (dry) { + $logger.info(`dry 模式,未执行清理: pid=${pid}`); + return; + } + try { - // 查 PID - let { stdout : stdout1 } = exec(`lsof -t -sTCP:LISTEN -i:${port}`) - let pid = stdout1.trim(); - $logger.debug(`绑定端口号 ${port} 的进程 PID=${pid}`) - - // 查进程信息 - let { stdout : stdout2 } = exec(`ps -p ${pid} -o comm=`) - let processName = stdout2.trim(); - $logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`); - - // Kill 进程 - exec(`kill -9 ${pid}`) - + await exec(`kill -9 ${pid}`) + $logger.info(`已结束进程 PID=${pid}`); } catch (e) { - if ( 1 === e.code ) { - $logger.error("此端口未被使用,或清理进程失败。") - return - } + $logger.error("此端口未被使用,或清理进程失败。") } } diff --git a/kit/src/linux/portpid.js b/kit/src/linux/portpid.js new file mode 100644 index 0000000..b2a9b01 --- /dev/null +++ b/kit/src/linux/portpid.js @@ -0,0 +1,278 @@ +/** + * 端口 -> 进程 pid 解析(仅支持 Linux,基于 /proc) + * + * 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致 + * (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位: + * 1. 宿主网络命名空间中监听该端口的 fd 属主(普通端口服务,如 sshd) + * 2. 若属主是 docker-proxy,则解析其 -container-ip/-container-port, + * 在容器网络命名空间内按容器内端口查找持有监听 fd 的真实应用进程 + * (处理 0.0.0.0:6380->6379 这类端口映射不一致,并用 fib_trie 的 /32 + * 本地地址匹配容器 IP 以区分布局在网段相同、内网端口相同的多个容器) + * 3. 宿主无监听时(userland-proxy 关闭),直接扫描非宿主 netns 的监听进程 + */ +const fs = require("fs"); +const path = require("path"); + +/** + * 读取进程所在网络命名空间标识 + * @param {number} pid + * @returns {string|null} + */ +function readNetNs(pid) { + try { + return fs.readlinkSync(`/proc/${pid}/ns/net`); + } catch (e) { + return null; + } +} + +/** + * 读取一个网络命名空间内监听指定端口的 socket inode 集合 + * @param {string} netNs + * @param {number} port + * @returns {Set} + */ +function listenInodesInNetNs(netNs, port) { + const out = new Set(); + for (const entry of fs.readdirSync("/proc")) { + if (!/^\d+$/.test(entry)) { + continue; + } + if (readNetNs(Number(entry)) !== netNs) { + continue; + } + for (const file of ["tcp", "tcp6"]) { + let text; + try { + text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8"); + } catch (e) { + continue; + } + for (const line of text.split("\n").slice(1)) { + const parts = line.trim().split(/\s+/); + if (parts.length < 10 || parts[3] !== "0A") { + continue; + } + const local = parts[1]; + const idx = local.lastIndexOf(":"); + if (idx < 0) { + continue; + } + if (parseInt(local.slice(idx + 1), 16) === Number(port)) { + out.add(parts[9]); + } + } + } + } + return out; +} + +/** + * 在指定网络命名空间内查找持有这些 socket inode 的进程 pid(归一化为 Tgid) + * @param {string} netNs + * @param {Set} inodes + * @returns {number|null} + */ +function ownerInNetNs(netNs, inodes) { + for (const entry of fs.readdirSync("/proc")) { + if (!/^\d+$/.test(entry)) { + continue; + } + if (readNetNs(Number(entry)) !== netNs) { + continue; + } + const fdDir = `/proc/${entry}/fd`; + let names; + try { + names = fs.readdirSync(fdDir); + } catch (e) { + continue; + } + for (const fd of names) { + try { + const target = fs.readlinkSync(path.join(fdDir, fd)); + const m = target.match(/^socket:\[(\d+)\]$/); + if (m && inodes.has(m[1])) { + return tgidOf(Number(entry)); + } + } catch (e) { + // 忽略无法读取的 fd + } + } + } + return null; +} + +/** + * 归一化线程 pid 为进程 Tgid + * @param {number} pid + * @returns {number} + */ +function tgidOf(pid) { + try { + const m = fs.readFileSync(`/proc/${pid}/status`, "utf8").match(/^Tgid:\s+(\d+)/m); + return m ? Number(m[1]) : pid; + } catch (e) { + return pid; + } +} + +/** + * 读取进程启动参数 + * @param {number} pid + * @returns {string[]} + */ +function cmdlineOf(pid) { + try { + return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0"); + } catch (e) { + return []; + } +} + +/** + * 读取网络命名空间内声明的本地 IPv4 地址集合(/proc//net/fib_trie) + * @param {string} netNs + * @returns {Set} + */ +function netNsIpv4s(netNs) { + const ips = new Set(); + for (const entry of fs.readdirSync("/proc")) { + if (!/^\d+$/.test(entry)) { + continue; + } + if (readNetNs(Number(entry)) !== netNs) { + continue; + } + try { + const text = fs.readFileSync(`/proc/${entry}/net/fib_trie`, "utf8"); + const re = /\b(\d+\.\d+\.\d+\.\d+)\b/g; + let m; + while ((m = re.exec(text)) !== null) { + ips.add(m[1]); + } + break; + } catch (e) { + // 继续尝试下一个候选 + } + } + return ips; +} + +/** + * 根据端口查找监听进程 pid + * + * 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致 + * (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位。 + * 容器应用未就绪时仍可能退回 docker-proxy 自身的 pid,调用方需自行判断。 + * @param {number} port + * @returns {number|null} + */ +function findPortPid(port) { + const hostNs = readNetNs(1); + if (hostNs === null) { + return null; + } + + const hostInodes = listenInodesInNetNs(hostNs, port); + if (hostInodes.size > 0) { + const owner = ownerInNetNs(hostNs, hostInodes); + if (owner !== null) { + const cmd = cmdlineOf(owner); + if (cmd.length > 0 && cmd[0].includes("docker-proxy")) { + const args = {}; + for (let i = 1; i < cmd.length - 1; i++) { + if (cmd[i].startsWith("-")) { + args[cmd[i]] = cmd[i + 1]; + } + } + const cip = args["-container-ip"]; + const cport = Number(args["-container-port"]); + if (Number.isFinite(cport) && cport > 0) { + // 所有可能持有该容器内端口的网络命名空间 + const groups = new Map(); // netNs -> Set + for (const entry of fs.readdirSync("/proc")) { + if (!/^\d+$/.test(entry)) { + continue; + } + const ns = readNetNs(Number(entry)); + if (ns === null || ns === hostNs) { + continue; + } + const inodes = new Set(); + for (const file of ["tcp", "tcp6"]) { + let text; + try { + text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8"); + } catch (e) { + continue; + } + for (const line of text.split("\n").slice(1)) { + const parts = line.trim().split(/\s+/); + if (parts.length < 10 || parts[3] !== "0A") { + continue; + } + const idx = parts[1].lastIndexOf(":"); + if (idx >= 0 && parseInt(parts[1].slice(idx + 1), 16) === cport) { + inodes.add(parts[9]); + } + } + } + if (inodes.size > 0) { + groups.set(ns, inodes); + } + } + // 优先选 fib_trie 本地地址与容器 IP 一致的命名空间 + if (cip) { + for (const [ns, inodes] of groups) { + if (netNsIpv4s(ns).has(cip)) { + const pid = ownerInNetNs(ns, inodes); + if (pid !== null) { + return pid; + } + } + } + } + // 退而求其次:任意持有该容器内端口监听的命名空间的属主 + for (const [ns, inodes] of groups) { + const pid = ownerInNetNs(ns, inodes); + if (pid !== null) { + return pid; + } + } + } + // 容器应用未就绪:退回 docker-proxy,保留可见性 + return owner; + } + return owner; + } + } + + // 宿主无监听(userland-proxy 关闭):扫描非宿主命名空间内监听该端口的进程 + const groups = new Map(); + for (const entry of fs.readdirSync("/proc")) { + if (!/^\d+$/.test(entry)) { + continue; + } + const ns = readNetNs(Number(entry)); + if (ns === null || ns === hostNs) { + continue; + } + const inodes = listenInodesInNetNs(ns, port); + if (inodes.size > 0) { + groups.set(ns, inodes); + } + } + for (const [ns, inodes] of groups) { + const pid = ownerInNetNs(ns, inodes); + if (pid !== null) { + return pid; + } + } + return null; +} + +module.exports = { + findPortPid, + cmdlineOf +}; \ No newline at end of file diff --git a/kit/src/net.js b/kit/src/net.js index a679752..a8fa478 100644 --- a/kit/src/net.js +++ b/kit/src/net.js @@ -1,5 +1,4 @@ require("./init") -const osutil = require("./util/osutil") const killer = require('cross-port-killer'); const net = require('net'); const dns = require('dns').promises; @@ -35,13 +34,17 @@ async function telnet(port, host, timeout=3000){ } -async function killport(port) { +async function killport(port, dry) { + // Linux:先解析监听进程。若端口由 docker-proxy 发布,会定位容器内 + // 真实应用进程再清理,避免误杀 docker 进程(见 ./linux/net.js)。 + if (process.platform === "linux") { + return require("./linux/net").killport(port, dry); + } + // dry 模式:Windows 走 netstat 解析 pid,仅展示不清理 + if (dry) { + return require("./windows/net").killport(port, dry); + } return killer.kill( port ); - // let func = osutil.choise([ - // require("./windows/net").killport, - // require("./linux/net").killport, - // ]) - // return func && func(port); } /** @@ -371,7 +374,104 @@ function getServiceName(port) { return commonServices[port] || `Unknown (${port})`; } +/** + * tcp 报文发收:以 hex 或文本发送一包数据,再接收一包并返回。 + * + * @param {Object} opts + * @param {string} opts.ip 目标 ip + * @param {string|number} opts.port 目标端口,必填 + * @param {string} opts.data 待发送数据 + * @param {boolean} [opts.hex=false] true 时 data 为 hex 格式,发送前转回 bytes + * @param {number|string} [opts.headlen] 长度头字节数(1-6):发送前按大端填入 data 长度,接收时按其声明长度读满一包 + * @param {number|string} [opts.timeout=3000] 超时毫秒 + * @param {boolean} [opts.debug=false] true 时打印过程日志 + * @returns {Promise} 接收到的一包数据 + */ +async function tcpc({ ip, port, data, hex=false, headlen=null, timeout=3000, debug=false } = {}) { + if ( null == ip || "" === String(ip).trim() ) throw "缺少 --ip"; + if ( null == port || "" === String(port).trim() ) throw "缺少 --port"; + if ( null == data ) throw "缺少 --data"; + + const log = function(){ if ( debug ) $logger.info( ...arguments ) }; + + const dport = parseInt(port, 10); + if ( ! Number.isInteger(dport) || dport < 1 || dport > 65535 ) throw "非法端口: " + port; + + const tmo = timeout ? parseInt(timeout, 10) : 3000; + if ( ! Number.isInteger(tmo) || tmo <= 0 ) throw "非法 timeout: " + timeout; + + // 组装发送载荷 + let payload; + if ( hex ) { + const h = String(data).replace(/\s+/g, ""); + if ( h.length % 2 !== 0 ) throw "hex 数据长度必须为偶数"; + if ( h.length > 0 && ! /^[0-9a-fA-F]+$/.test(h) ) throw "hex 数据包含非法字符"; + payload = Buffer.from(h, "hex"); + } else { + payload = Buffer.from(String(data), "utf8"); + } + + // 长度头:大端填入 data 字节数(不含头自身) + let hl = null; + if ( null != headlen && "" !== String(headlen) ) { + hl = parseInt(headlen, 10); + if ( ! Number.isInteger(hl) || hl < 1 || hl > 6 ) throw "--headlen 必须为 1-6 的整数"; + if ( payload.length >= Math.pow(2, hl * 8) ) throw `数据长度 ${payload.length} 超出 ${hl} 字节长度头表示范围`; + } + + let packet = payload; + if ( hl ) { + const hdr = Buffer.alloc(hl); + hdr.writeUIntBE(payload.length, 0, hl); + packet = Buffer.concat([hdr, payload]); + } + + log(`连接 ${ip}:${dport},超时 ${tmo}ms`); + log(`发送 ${packet.length} 字节: ${packet.toString("hex")}`); + + return new Promise(function (resolve, reject){ + const socket = new net.Socket(); + let timer = null; + let buf = Buffer.alloc(0); + let got = Buffer.alloc(0); + let done = false; + + const finish = function(err, val){ + if ( done ) return; + done = true; + if ( timer ) clearTimeout(timer); + socket.destroy(); + if ( err ) reject(err); else resolve(val); + }; + + timer = setTimeout(() => finish("收发超时"), tmo); + socket.setTimeout(tmo, () => finish("链接超时")); + socket.on("error", e => finish(e)); + + socket.connect(dport, ip, () => socket.write(packet)); + + socket.on("data", chunk => { + if ( hl ) { + buf = Buffer.concat([buf, chunk]); + if ( buf.length < hl ) return; + const declared = buf.readUIntBE(0, hl); + const total = hl + declared; + if ( buf.length < total ) return; + got = buf.slice(0, total); + log(`收到 ${got.length} 字节(头声明 data ${declared} 字节)`); + finish(null, got); + } else { + got = Buffer.concat([got, chunk]); + log(`收到 ${got.length} 字节`); + finish(null, got); + } + }); + + socket.on("end", () => finish("连接关闭,收包不完整")); + }); +} + module.exports = { - killport, getport, telnet, checkip, scan + killport, getport, telnet, checkip, scan, tcpc } \ No newline at end of file diff --git a/kit/src/windows/net.js b/kit/src/windows/net.js index 0f51017..5fd916c 100644 --- a/kit/src/windows/net.js +++ b/kit/src/windows/net.js @@ -1,6 +1,6 @@ -async function killport(port) { +async function killport(port, dry) { $logger.info(`kill process on port ${port}`); const util = require('util'); const exec = util.promisify(require('child_process').exec); @@ -23,6 +23,11 @@ async function killport(port) { $logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`); + if (dry) { + $logger.info(`dry 模式,未执行清理: pid=${pid}`); + return + } + // Kill 进程 await exec(`taskkill /PID ${pid} /F`)