- tcpc 支持 hex/文本发送、--headlen 大端长度头自动填充与收包校验、超时控制 refactor(killport): 支持 --dry 模式,Linux 下通过 docker-proxy 定位容器内真实进程(拆分至 linux/portpid.js)
This commit is contained in:
+3
-267
@@ -3,16 +3,10 @@
|
|||||||
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
|
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
|
||||||
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
|
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
|
||||||
* - 句柄数:/proc/<pid>/fd 目录条目数
|
* - 句柄数:/proc/<pid>/fd 目录条目数
|
||||||
* - 端口 -> pid:
|
* - 端口 -> pid:端口解析逻辑见 ../linux/portpid.js(含 docker 端口发布)
|
||||||
* 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 fs = require("fs");
|
||||||
const path = require("path");
|
const portpid = require("../linux/portpid");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|
||||||
function isLinux() {
|
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<string>}
|
|
||||||
*/
|
|
||||||
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<string>} 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/<pid>/net/fib_trie)
|
|
||||||
* @param {string} netNs
|
|
||||||
* @returns {Set<string>}
|
|
||||||
*/
|
|
||||||
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<inode>
|
|
||||||
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 = {
|
module.exports = {
|
||||||
isLinux,
|
isLinux,
|
||||||
collectPid,
|
collectPid,
|
||||||
findPortPid
|
findPortPid: portpid.findPortPid
|
||||||
};
|
};
|
||||||
+21
-4
@@ -184,9 +184,10 @@ parser.addCmdLine("dockersh <container> [--file]", "反向获取 docker 容器
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
parser.addCmdLine("killport <port>", "释放端口", function (cli) {
|
parser.addCmdLine("killport <port> [-dry]", "释放端口;-dry 仅显示端口与进程信息,不执行清理", async function (cli) {
|
||||||
let port = cli.getParamValue("port")
|
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) {
|
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] <crlf
|
|||||||
$logger.info( "暂未实现" );
|
$logger.info( "暂未实现" );
|
||||||
});
|
});
|
||||||
|
|
||||||
parser.addCmdLine("tcpc [-d] [-x] [--host] [--ip] [--port] [--headlen] [--timeout] [--filepath]", "tcp 报文发收", function (cli) {
|
parser.addCmdLine("tcpc [-d] [-x] [--ip] [--port] [--headlen] [--timeout] [--data]", "tcp 报文发收", async function (cli) {
|
||||||
$logger.info( "暂未实现" );
|
const hex = cli.hasFlag("x");
|
||||||
|
try {
|
||||||
|
const recv = await net.tcpc({
|
||||||
|
ip: cli.getParamValue("ip"),
|
||||||
|
port: cli.getParamValue("port"),
|
||||||
|
data: cli.getParamValue("data"),
|
||||||
|
hex,
|
||||||
|
headlen: cli.getParamValue("headlen"),
|
||||||
|
timeout: cli.getParamValue("timeout"),
|
||||||
|
debug: cli.hasFlag("d")
|
||||||
|
});
|
||||||
|
$logger.info( hex ? recv.toString("hex") : recv.toString("utf8") );
|
||||||
|
process.exit(0)
|
||||||
|
} catch ( e ) {
|
||||||
|
$logger.error( e )
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main(){
|
async function main(){
|
||||||
|
|||||||
+29
-19
@@ -1,29 +1,39 @@
|
|||||||
|
const { findPortPid, cmdlineOf } = require("./portpid");
|
||||||
|
|
||||||
|
async function killport(port, dry) {
|
||||||
async function killport(port) {
|
|
||||||
$logger.info(`kill process on port ${port}`);
|
$logger.info(`kill process on port ${port}`);
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
|
||||||
|
// 解析监听进程:若由 docker-proxy 发布端口,会优先返回容器内真实应用进程
|
||||||
|
const pid = findPortPid(port);
|
||||||
|
if (pid == null) {
|
||||||
|
$logger.error("此端口未被使用,或清理进程失败。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = cmdlineOf(pid);
|
||||||
|
const program = cmd.length > 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 {
|
try {
|
||||||
// 查 PID
|
await exec(`kill -9 ${pid}`)
|
||||||
let { stdout : stdout1 } = exec(`lsof -t -sTCP:LISTEN -i:${port}`)
|
$logger.info(`已结束进程 PID=${pid}`);
|
||||||
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}`)
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ( 1 === e.code ) {
|
$logger.error("此端口未被使用,或清理进程失败。")
|
||||||
$logger.error("此端口未被使用,或清理进程失败。")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<string>}
|
||||||
|
*/
|
||||||
|
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<string>} 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/<pid>/net/fib_trie)
|
||||||
|
* @param {string} netNs
|
||||||
|
* @returns {Set<string>}
|
||||||
|
*/
|
||||||
|
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<inode>
|
||||||
|
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
|
||||||
|
};
|
||||||
+108
-8
@@ -1,5 +1,4 @@
|
|||||||
require("./init")
|
require("./init")
|
||||||
const osutil = require("./util/osutil")
|
|
||||||
const killer = require('cross-port-killer');
|
const killer = require('cross-port-killer');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const dns = require('dns').promises;
|
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 );
|
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})`;
|
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<Buffer>} 接收到的一包数据
|
||||||
|
*/
|
||||||
|
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 = {
|
module.exports = {
|
||||||
killport, getport, telnet, checkip, scan
|
killport, getport, telnet, checkip, scan, tcpc
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
async function killport(port) {
|
async function killport(port, dry) {
|
||||||
$logger.info(`kill process on port ${port}`);
|
$logger.info(`kill process on port ${port}`);
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require('child_process').exec);
|
||||||
@@ -23,6 +23,11 @@ async function killport(port) {
|
|||||||
|
|
||||||
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
|
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
|
||||||
|
|
||||||
|
if (dry) {
|
||||||
|
$logger.info(`dry 模式,未执行清理: pid=${pid}`);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Kill 进程
|
// Kill 进程
|
||||||
await exec(`taskkill /PID ${pid} /F`)
|
await exec(`taskkill /PID ${pid} /F`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user