From 072c9bfa5043ed94d81e42139cf3326b64839fc8 Mon Sep 17 00:00:00 2001 From: cheney Date: Fri, 18 Sep 2026 09:59:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20glance=20=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E7=9B=91=E6=8E=A7=E5=91=BD=E4=BB=A4=EF=BC=9A=E9=87=87?= =?UTF-8?q?=E9=9B=86=20cpu/=E5=86=85=E5=AD=98/=E5=8F=A5=E6=9F=84=E6=95=B0?= =?UTF-8?q?=E5=B9=B6=E7=94=9F=E6=88=90=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- argv/CliCmdLine.js | 2 +- argv/parser.js | 14 +- kit/src/glance/index.js | 168 ++++++++++++++++++++ kit/src/glance/monitor.js | 148 ++++++++++++++++++ kit/src/glance/report.js | 312 ++++++++++++++++++++++++++++++++++++++ kit/src/glance/store.js | 68 +++++++++ kit/src/index.js | 5 + 7 files changed, 713 insertions(+), 4 deletions(-) create mode 100644 kit/src/glance/index.js create mode 100644 kit/src/glance/monitor.js create mode 100644 kit/src/glance/report.js create mode 100644 kit/src/glance/store.js diff --git a/argv/CliCmdLine.js b/argv/CliCmdLine.js index 5d13708..5389ccb 100644 --- a/argv/CliCmdLine.js +++ b/argv/CliCmdLine.js @@ -19,7 +19,7 @@ module.exports = class CliCmdLine { let param = cmdLine.params[pname]; if ( param.isFlag ) { if ( param.needValue ){ - if ( params[pname] ) { + if ( params[pname] !== undefined ) { this.params[pname] = params[pname] } else { if ( ! param.isOptional ) { diff --git a/argv/parser.js b/argv/parser.js index 56d3be0..fb2cbe1 100644 --- a/argv/parser.js +++ b/argv/parser.js @@ -85,12 +85,21 @@ module.exports = class CliCmdParser { // 位置参数:值 const position = [] + let line = this.cmdLines[ argv[0] ]; + for (let i = 1; i < argv.length; i++ ) { if ( argv[i].startsWith("--") ) { + const name = argv[i].substring(2); if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) { - throw "flag 格式错误" + argv[i]; + const param = line.params[name]; + if ( ! param || ! param.isOptional ) { + throw "flag 格式错误" + argv[i]; + } + // 可选命名参数允许不带值,值为空串,由命令自行做默认值 + params[ name ] = ""; + } else { + params[ name ] = argv[++i]; } - params[ argv[i].substring(2) ] = argv[++i]; } else if ( argv[i].startsWith("-") ) { flags.add( argv[i].substring(1) ); @@ -99,7 +108,6 @@ module.exports = class CliCmdParser { } } - let line = this.cmdLines[ argv[0] ]; // 整理 // line 是藐视 // flags, params, position 是值 diff --git a/kit/src/glance/index.js b/kit/src/glance/index.js new file mode 100644 index 0000000..6e01e16 --- /dev/null +++ b/kit/src/glance/index.js @@ -0,0 +1,168 @@ +/** + * kit glance 命令 + * - 记录模式:--pid 或 --port ,采集一次写入 sqlite(~/.kit/glance.db) + * - 报告模式:--report [报告文件](默认 report.html),需配合 --pid/--port + * - --startTime/--endTime 格式 yyyyMMddHHmmss,指定报告时间范围 + */ +const fs = require("fs"); +const path = require("path"); +const store = require("./store"); +const report = require("./report"); + +const monitor = require("./monitor"); + +const HOUR = 60 * 60 * 1000; +const HOURS_24 = 24 * HOUR; + +function parseTime(text) { + const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(text); + if (!m) { + return null; + } + const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6])); + return d.getTime(); +} + +function resolveRange(startText, endText) { + if (startText && endText) { + const s = parseTime(startText); + const e = parseTime(endText); + if (s == null || e == null) { + return { error: "--startTime/--endTime 格式应为 yyyyMMddHHmmss" }; + } + if (e <= s) { + return { error: "--endTime 必须晚于 --startTime" }; + } + return { start: s, end: e }; + } + const now = Date.now(); + if (startText) { + const s = parseTime(startText); + if (s == null) { + return { error: "--startTime 格式应为 yyyyMMddHHmmss" }; + } + return { start: s, end: s + HOURS_24 }; + } + if (endText) { + const e = parseTime(endText); + if (e == null) { + return { error: "--endTime 格式应为 yyyyMMddHHmmss" }; + } + return { start: e - HOURS_24, end: e }; + } + return { start: now - HOURS_24, end: now }; +} + +async function record(cli) { + const db = store.open(); + let record = null; + + const pidText = cli.getParamValue("pid"); + if (pidText && pidText !== "") { + const pid = Number(pidText); + if (!Number.isInteger(pid) || pid <= 0) { + $logger.error(`pid 参数无效: ${pidText}`); + process.exit(-1); + } + const data = await monitor.collectPid(pid); + if (!data) { + $logger.warn(`进程 ${pid} 不存在或已退出,本次不记录`); + return; + } + record = { ts: Date.now(), target: "pid", pid, port: null, cpu: data.cpu, mem: data.mem, handle: data.handle }; + } else { + const portText = cli.getParamValue("port"); + const port = Number(portText); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + $logger.error(`port 参数无效: ${portText}`); + process.exit(-1); + } + const found = monitor.findPortPid(port); + if (!found) { + $logger.warn(`端口 ${port} 无监听进程,本次不记录`); + return; + } + const data = await monitor.collectPid(found); + if (!data) { + $logger.warn(`进程 ${found} 已退出,本次不记录`); + return; + } + record = { ts: Date.now(), target: "port", pid: found, port, cpu: data.cpu, mem: data.mem, handle: data.handle }; + } + + store.insert(db, record); + $logger.info(`已记录 target=${record.target} pid=${record.pid} cpu=${record.cpu}% mem=${record.mem}MB handle=${record.handle}`); +} + +function buildReport(cli, reportFile) { + const isPid = cli.getParamValue("pid") != null; + const { start, end, error } = resolveRange(cli.getParamValue("startTime"), cli.getParamValue("endTime")); + if (error) { + $logger.error(error); + process.exit(-1); + } + + const db = store.open(); + const rows = isPid + ? store.queryRange(db, { pid: Number(cli.getParamValue("pid")), start, end }) + : store.queryRange(db, { port: Number(cli.getParamValue("port")), start, end }); + + const html = report.render({ + mode: isPid ? "pid" : "port", + value: isPid ? Number(cli.getParamValue("pid")) : Number(cli.getParamValue("port")), + rows, + start, + end + }); + + fs.writeFileSync(reportFile, html, "utf8"); + $logger.info(`报告已生成: ${path.resolve(reportFile)}`); +} + +async function main(cli) { + const pidText = cli.getParamValue("pid"); + const portText = cli.getParamValue("port"); + const reportRaw = cli.getParamValue("report"); + + const isPid = pidText != null && pidText !== ""; + const isPort = portText != null && portText !== ""; + const isReport = reportRaw !== undefined; + + if (isPid && isPort) { + $logger.error("--pid 和 --port 只能传递一个"); + process.exit(-1); + } + if (!isPid && !isPort) { + $logger.error("--pid 和 --port 必须传递一个"); + process.exit(-1); + } + + if (isPid) { + const pid = Number(pidText); + if (!Number.isInteger(pid) || pid <= 0) { + $logger.error(`pid 参数无效: ${pidText}`); + process.exit(-1); + } + } else if (isPort) { + const port = Number(portText); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + $logger.error(`port 参数无效: ${portText}`); + process.exit(-1); + } + } + + if (isReport) { + const reportFile = reportRaw || "report.html"; + buildReport(cli, reportFile); + return; + } + + if (!monitor.isLinux()) { + $logger.error("glance 仅支持 Linux 操作系统"); + process.exit(-1); + } + + await record(cli); +} + +module.exports = { main }; \ No newline at end of file diff --git a/kit/src/glance/monitor.js b/kit/src/glance/monitor.js new file mode 100644 index 0000000..708e89b --- /dev/null +++ b/kit/src/glance/monitor.js @@ -0,0 +1,148 @@ +/** + * 进程监控指标采集(仅支持 Linux) + * - CPU 占用率:两次采样 /proc//stat 与 /proc/stat 差值计算,内部休眠约 1 秒 + * - 内存占用:/proc//status 的 VmRSS,单位 MB + * - 句柄数:/proc//fd 目录条目数 + * - 端口 -> pid:解析 /proc/net/tcp 与 /proc/net/tcp6,结合 /proc//fd 的 socket inode + */ +const fs = require("fs"); +const path = require("path"); +const os = require("os"); + +function isLinux() { + return os.type() === "Linux"; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function readPidTicks(pid) { + const txt = fs.readFileSync(`/proc/${pid}/stat`, "utf8"); + // 字段 14=utime 15=stime,comm 可能含空格,按最后一个 ) 截断 + const rest = txt.slice(txt.lastIndexOf(")") + 1).trim().split(/\s+/); + return Number(rest[11]) + Number(rest[12]); +} + +function readCpuTotal() { + const line = fs.readFileSync("/proc/stat", "utf8").split("\n")[0]; + // user nice system idle iowait irq softirq steal(guest 已计入 user) + const parts = line.trim().split(/\s+/).slice(1, 9).map(Number); + let total = 0; + for (const v of parts) { + total += Number.isFinite(v) ? v : 0; + } + return total; +} + +function readMemMB(pid) { + const status = fs.readFileSync(`/proc/${pid}/status`, "utf8"); + const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m); + if (!m) { + return null; + } + return Math.round(Number(m[1]) / 1024 * 100) / 100; +} + +function readHandleCount(pid) { + return fs.readdirSync(`/proc/${pid}/fd`, { withFileTypes: true }).length; +} + +/** + * 采集单个进程的监控数据 + * @param {number} pid + * @returns {Promise<{pid:number,cpu:number,mem:number,handle:number}|null>} 进程不存在/不可读返回 null + */ +async function collectPid(pid) { + try { + const beforePid = readPidTicks(pid); + const beforeTotal = readCpuTotal(); + await sleep(1000); + const afterPid = readPidTicks(pid); + const afterTotal = readCpuTotal(); + + const totalDelta = afterTotal - beforeTotal; + const pidDelta = afterPid - beforePid; + const cpu = totalDelta > 0 ? Math.round(pidDelta / totalDelta * 10000) / 100 : 0; + + const mem = readMemMB(pid); + const handle = readHandleCount(pid); + return { pid, cpu, mem, handle }; + } catch (e) { + return null; + } +} + +/** + * 根据端口查找监听进程 pid + * @param {number} port + * @returns {number|null} + */ +function findPortPid(port) { + const targetInodes = new Set(); + + for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) { + let text; + try { + text = fs.readFileSync(file, "utf8"); + } catch (e) { + continue; + } + const lines = text.split("\n").slice(1); + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length < 10) { + continue; + } + // st == 0A 表示 LISTEN + if (parts[3] !== "0A") { + continue; + } + const local = parts[1]; + const idx = local.lastIndexOf(":"); + if (idx < 0) { + continue; + } + const realPort = parseInt(local.slice(idx + 1), 16); + if (Number.isFinite(realPort) && realPort === Number(port)) { + targetInodes.add(parts[9]); + } + } + } + + if (targetInodes.size === 0) { + return null; + } + + const entries = fs.readdirSync("/proc"); + for (const entry of entries) { + if (!/^\d+$/.test(entry)) { + 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 && targetInodes.has(m[1])) { + return Number(entry); + } + } catch (e) { + // 忽略无法读取的 fd + } + } + } + return null; +} + +module.exports = { + isLinux, + collectPid, + findPortPid +}; \ No newline at end of file diff --git a/kit/src/glance/report.js b/kit/src/glance/report.js new file mode 100644 index 0000000..d9ac894 --- /dev/null +++ b/kit/src/glance/report.js @@ -0,0 +1,312 @@ +/** + * glance 报告生成:生成一个自包含 HTML 文件(内联 SVG 图表,无外部依赖)。 + * 三张图表:CPU 占用率、内存占用、句柄数。 + * - 纵轴/横轴根据数据量自动缩放 + * - 时间段内无数据或进程中停(时间缺口)时,图表会显现 + * - pid 变化(进程重启)以橙色虚线 + 标注标出 + */ +const HOUR = 60 * 60 * 1000; + +function esc(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function pad2(n) { + return String(n).padStart(2, "0"); +} + +function fmtTime(ts) { + const d = new Date(ts); + return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`; +} + +function fmtFull(ts) { + const d = new Date(ts); + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; +} + +function niceStep(raw) { + const mag = Math.pow(10, Math.floor(Math.log10(raw))); + const norm = raw / mag; + let step; + if (norm <= 1) step = 1; + else if (norm <= 2) step = 2; + else if (norm <= 5) step = 5; + else step = 10; + return step * mag; +} + +function computeYAxis(values) { + const vs = values.filter(v => Number.isFinite(v)); + if (!vs.length) { + return { min: 0, max: 1, ticks: [0, 0.2, 0.4, 0.6, 0.8, 1] }; + } + let min = Math.min(...vs); + let max = Math.max(...vs); + if (min === max) { + min -= 1; + max += 1; + } + const step = niceStep((max - min) / 5); + const lo = Math.floor(min / step) * step; + const hi = Math.ceil(max / step) * step; + const ticks = []; + for (let v = lo; v <= hi + step / 2; v += step) { + ticks.push(Math.round(v * 1e6) / 1e6); + } + return { min: lo, max: hi, ticks }; +} + +/** + * 生成折线内容:按时间缺口切成多段 polyline,缺口处绘制淡色竖带 + */ +function buildLine(entries, X, Y, gapThreshold, color) { + let svg = ""; + let segment = []; + const gaps = []; + + function flush() { + if (segment.length > 0) { + const points = segment.map(p => `${X(p.ts).toFixed(2)},${Y(p.value).toFixed(2)}`).join(" "); + svg += `\n `; + for (const p of segment) { + svg += `\n `; + } + segment = []; + } + } + + for (let i = 0; i < entries.length; i++) { + if (i > 0 && gapThreshold > 0) { + const dt = entries[i].ts - entries[i - 1].ts; + if (dt > gapThreshold) { + gaps.push({ x0: X(entries[i - 1].ts), x1: X(entries[i].ts) }); + flush(); + } + } + segment.push(entries[i]); + } + flush(); + return { svg, gaps }; +} + +function buildChart(title, unit, color, rows, start, end, gapThreshold, pidChanges) { + const W = 960, H = 300, mL = 80, mR = 40, mT = 36, mB = 52; + const pw = W - mL - mR; + const ph = H - mT - mB; + const span = (end - start) || 1; + + const entries = rows + .filter(r => r.value != null && Number.isFinite(r.value)) + .map(r => ({ ts: r.ts, value: r.value, pid: r.pid })); + + const X = ts => mL + Math.max(0, Math.min(1, (ts - start) / span)) * pw; + const axis = computeYAxis(entries.map(e => e.value)); + const Y = v => mT + ph - ((v - axis.min) / ((axis.max - axis.min) || 1)) * ph; + + let svg = ``; + svg += `\n ${esc(title)}${unit ? `(${esc(unit)})` : ""}`; + + // 网格与纵轴 + for (const t of axis.ticks) { + const y = Y(t); + svg += `\n `; + svg += `\n ${t}`; + } + + // 横轴(6 个刻度) + for (let k = 0; k <= 5; k++) { + const ts = start + (end - start) * k / 5; + const x = X(ts); + svg += `\n `; + svg += `\n ${fmtTime(ts)}`; + } + + // 坐标轴 + svg += `\n `; + svg += `\n `; + + if (entries.length === 0) { + svg += `\n 该时间范围无数据`; + } else { + const { svg: lineSvg, gaps } = buildLine(entries, X, Y, gapThreshold, color); + svg += lineSvg; + + // 时间缺口高亮(一段时间没数据) + for (const g of gaps) { + const x = Math.min(g.x0, g.x1); + const w = Math.max(1, Math.abs(g.x1 - g.x0)); + svg += `\n `; + } + + // pid 变化(进程重启)竖线 + if (pidChanges && pidChanges.length) { + for (const ch of pidChanges) { + const x = X(ch.ts); + svg += `\n `; + const label = `pid ${ch.from}→${ch.to}`; + const lx = Math.min(x + 5, W - mR - 110); + const ly = mT + 14; + svg += `\n ↻ ${esc(label)}`; + } + } + } + svg += "\n"; + return svg; +} + +function computeGapThreshold(rows) { + const tsList = rows + .map(r => r.ts) + .filter(ts => Number.isFinite(ts)) + .sort((a, b) => a - b); + if (tsList.length < 2) { + return 0; + } + const deltas = []; + for (let i = 1; i < tsList.length; i++) { + const d = tsList[i] - tsList[i - 1]; + if (d > 0) { + deltas.push(d); + } + } + if (!deltas.length) { + return 0; + } + deltas.sort((a, b) => a - b); + const typical = deltas[Math.floor(deltas.length / 2)]; + return Math.max(typical * 3, 30 * 1000); +} + +function collectPidChanges(rows) { + const changes = []; + for (let i = 1; i < rows.length; i++) { + const prev = rows[i - 1].pid; + const cur = rows[i].pid; + if (prev != null && cur != null && prev !== cur) { + changes.push({ ts: rows[i].ts, from: prev, to: cur }); + } + } + return changes; +} + +function summary(rows, key) { + const vs = rows.map(r => r[key]).filter(v => v != null && Number.isFinite(v)); + if (!vs.length) { + return null; + } + const min = Math.min(...vs); + const max = Math.max(...vs); + const avg = vs.reduce((a, b) => a + b, 0) / vs.length; + return { min, max, avg }; +} + +function fmtNum(v, digits) { + if (v == null || !Number.isFinite(v)) { + return "-"; + } + return Number(v.toFixed(digits)); +} + +/** + * 生成报告 HTML + * @param opts {mode:'pid'|'port', value, rows, start, end} + */ +function render(opts) { + const { mode, value, rows, start, end } = opts; + const spanHours = Math.round((end - start) / HOUR * 10) / 10; + const targetText = mode === "pid" ? `--pid ${value}` : `--port ${value}`; + const gapThreshold = computeGapThreshold(rows); + const pidChanges = collectPidChanges(rows); + const restarts = pidChanges.length; + const generatedAt = fmtFull(Date.now()); + + const series = [ + { key: "cpu", title: "CPU 占用率", unit: "%", color: "#2563eb" }, + { key: "mem", title: "内存占用", unit: "MB", color: "#16a34a" }, + { key: "handle", title: "句柄数", unit: "", color: "#dc2626" } + ]; + + const summaryRows = series.map(s => { + const st = summary(rows, s.key); + return { + title: s.title, + unit: s.unit, + min: st ? fmtNum(st.min, 2) : "-", + max: st ? fmtNum(st.max, 2) : "-", + avg: st ? fmtNum(st.avg, 2) : "-" + }; + }); + + let chartsHtml = ""; + for (const s of series) { + chartsHtml += ` +
+

${esc(s.title)}

+ ${buildChart(s.title, s.unit, s.color, rows.map(r => ({ ts: r.ts, value: r[s.key], pid: r.pid })), start, end, gapThreshold, pidChanges)} +
`; + } + + const summaryHtml = summaryRows.map(r => ` + + ${esc(r.title)}${r.unit ? `(${esc(r.unit)})` : ""} + ${r.min} + ${r.max} + ${r.avg} + `).join(""); + + return ` + + + + +进程监控报告 ${esc(targetText)} + + + +
+
+

进程监控报告

+
+ 监控目标:${esc(targetText)} + 时间范围:${fmtFull(start)} ~ ${fmtFull(end)}(约 ${spanHours} 小时) + 样本数:${rows.length} + 重启次数:${restarts} + 生成时间:${generatedAt} +
+ + + ${summaryHtml} +
指标最小值最大值平均值
+
说明:时间轴覆盖请求范围;灰色竖带表示其间无采样数据(进程中断/采集缺失);橙色虚线表示 pid 变化(进程重启)。
+
+
+ ${chartsHtml} +
+
generated by kit glance
+
+ +`; +} + +module.exports = { render }; \ No newline at end of file diff --git a/kit/src/glance/store.js b/kit/src/glance/store.js new file mode 100644 index 0000000..c116ed7 --- /dev/null +++ b/kit/src/glance/store.js @@ -0,0 +1,68 @@ +/** + * glance 数据存储:默认 sqlite 位于 ~/.kit/glance.db(不存在则创建) + * 依赖 bun:sqlite,仅在调用时加载,避免影响 node 环境下的其他命令。 + */ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +function defaultDbPath() { + return path.join(os.homedir(), ".kit", "glance.db"); +} + +function open(dbPath) { + dbPath = dbPath || defaultDbPath(); + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + + const { Database } = require("bun:sqlite"); + const db = new Database(dbPath); + db.exec(`CREATE TABLE IF NOT EXISTS glance ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + target TEXT NOT NULL, + pid INTEGER, + port INTEGER, + cpu REAL, + mem REAL, + handle INTEGER + )`); + db.exec("CREATE INDEX IF NOT EXISTS idx_glance_lookup ON glance(pid, port, ts)"); + return db; +} + +/** + * 插入一条监控记录 + * @param db database + * @param record {ts, target, pid, port, cpu, mem, handle} + */ +function insert(db, record) { + db.query("INSERT INTO glance (ts, target, pid, port, cpu, mem, handle) VALUES (?, ?, ?, ?, ?, ?, ?)") + .run(record.ts, record.target, record.pid ?? null, record.port ?? null, record.cpu ?? null, record.mem ?? null, record.handle ?? null); +} + +/** + * 按时间范围查询监控数据(忽略 target,pid/port 二者取其一) + * @param db database + * @param opts {pid?, port?, start, end} + * @returns rows + */ +function queryRange(db, opts) { + let where; + const params = []; + if (opts.pid != null) { + where = "pid = ?"; + params.push(opts.pid); + } else { + where = "port = ?"; + params.push(opts.port); + } + params.push(opts.start, opts.end); + return db.query(`SELECT ts, target, pid, port, cpu, mem, handle FROM glance WHERE ${where} AND ts >= ? AND ts < ? ORDER BY ts ASC`).all(...params); +} + +module.exports = { + defaultDbPath, + open, + insert, + queryRange +}; \ No newline at end of file diff --git a/kit/src/index.js b/kit/src/index.js index ecc3688..e2fb84f 100644 --- a/kit/src/index.js +++ b/kit/src/index.js @@ -15,6 +15,7 @@ const zlib = require("zlib"); const mem = require("./mem") const { getAllDeviceInfo } = require("./platform") const kitcommand = require("./kitcommand") +const glance = require("./glance") kitcommand.init() kitcommand.regTo( parser ) @@ -188,6 +189,10 @@ parser.addCmdLine("killport ", "释放端口", function (cli) { net.killport( parseInt(port, 10) ); }); +parser.addCmdLine("glance [--pid] [--port] [--report] [--startTime] [--endTime]", "进程监控:--pid/--port 采集监控数据(仅 Linux),--report 生成报告并输出 HTML;", async function (cli) { + await glance.main(cli); +}); + parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) { const getPort = await import('get-port').then(psl => psl.default);