




















@@ -1,5 +1,45 @@
11import { spawnSync } from "node:child_process";
223+type ProcessTreeSnapshot = {
4+childrenByParent: Map<number, number[]>;
5+cpuByPid: Map<number, number>;
6+rssByPid: Map<number, number>;
7+};
8+9+function isPlainObject(value: unknown): value is Record<string, unknown> {
10+return typeof value === "object" && value !== null && !Array.isArray(value);
11+}
12+13+function parsePositiveInteger(value: unknown): number | null {
14+const raw = typeof value === "string" ? value.trim() : value;
15+const parsed = Number(raw);
16+if (!Number.isInteger(parsed) || parsed <= 0) {
17+return null;
18+}
19+return parsed;
20+}
21+22+function parseNonNegativeInteger(value: unknown): number | null {
23+const raw = typeof value === "string" ? value.trim() : value;
24+const parsed = Number(raw);
25+if (!Number.isInteger(parsed) || parsed < 0) {
26+return null;
27+}
28+return parsed;
29+}
30+31+function parseNonNegativeNumber(value: unknown): number | null {
32+const raw = typeof value === "string" ? value.trim() : value;
33+if (raw === "") {
34+return null;
35+}
36+const parsed = Number(raw);
37+if (!Number.isFinite(parsed) || parsed < 0) {
38+return null;
39+}
40+return parsed;
41+}
42+343export function parsePsCpuTimeMs(raw: string): number | null {
444const parts = raw.trim().split(":").map(Number);
545if (parts.some((part) => !Number.isFinite(part) || part < 0)) {
@@ -26,15 +66,136 @@ export function parsePsRssBytes(raw: string): number | null {
2666return Math.round(rssKiB * 1024);
2767}
286869+export function parseWindowsProcessCpuTimeMs(params: {
70+kernelModeTime: unknown;
71+userModeTime: unknown;
72+}): number | null {
73+const kernelModeTime = parseNonNegativeNumber(params.kernelModeTime);
74+const userModeTime = parseNonNegativeNumber(params.userModeTime);
75+if (kernelModeTime === null || userModeTime === null) {
76+return null;
77+}
78+return Math.round((kernelModeTime + userModeTime) / 10_000);
79+}
80+81+export function parseWindowsWorkingSetBytes(raw: unknown): number | null {
82+const parsed = parseNonNegativeNumber(raw);
83+return parsed === null ? null : Math.round(parsed);
84+}
85+86+export function parseWindowsProcessTreeSnapshot(raw: string): ProcessTreeSnapshot | null {
87+let parsed: unknown;
88+try {
89+parsed = JSON.parse(raw);
90+} catch {
91+return null;
92+}
93+const entries = Array.isArray(parsed) ? parsed : isPlainObject(parsed) ? [parsed] : [];
94+if (entries.length === 0) {
95+return null;
96+}
97+98+const childrenByParent = new Map<number, number[]>();
99+const cpuByPid = new Map<number, number>();
100+const rssByPid = new Map<number, number>();
101+for (const entry of entries) {
102+if (!isPlainObject(entry)) {
103+continue;
104+}
105+const pid = parsePositiveInteger(entry.ProcessId);
106+const ppid = parseNonNegativeInteger(entry.ParentProcessId);
107+if (pid === null || ppid === null) {
108+continue;
109+}
110+111+const children = childrenByParent.get(ppid) ?? [];
112+children.push(pid);
113+childrenByParent.set(ppid, children);
114+115+const cpuMs = parseWindowsProcessCpuTimeMs({
116+kernelModeTime: entry.KernelModeTime,
117+userModeTime: entry.UserModeTime,
118+});
119+if (cpuMs !== null) {
120+cpuByPid.set(pid, cpuMs);
121+}
122+123+const rssBytes = parseWindowsWorkingSetBytes(entry.WorkingSetSize);
124+if (rssBytes !== null) {
125+rssByPid.set(pid, rssBytes);
126+}
127+}
128+129+return {
130+ childrenByParent,
131+ cpuByPid,
132+ rssByPid,
133+};
134+}
135+136+function collectProcessTreeMetric(
137+rootPid: number,
138+childrenByParent: Map<number, number[]>,
139+metricByPid: Map<number, number>,
140+): number | null {
141+if (!metricByPid.has(rootPid)) {
142+return null;
143+}
144+145+let total = 0;
146+const seen = new Set<number>();
147+const stack: number[] = [rootPid];
148+while (stack.length > 0) {
149+const pid = stack.pop();
150+if (pid === undefined || seen.has(pid)) {
151+continue;
152+}
153+seen.add(pid);
154+total += metricByPid.get(pid) ?? 0;
155+for (const childPid of childrenByParent.get(pid) ?? []) {
156+stack.push(childPid);
157+}
158+}
159+return total;
160+}
161+162+function readWindowsProcessTreeSnapshot(): ProcessTreeSnapshot | null {
163+const result = spawnSync(
164+"powershell.exe",
165+[
166+"-NoProfile",
167+"-ExecutionPolicy",
168+"Bypass",
169+"-Command",
170+[
171+"$ErrorActionPreference='Stop';",
172+"Get-CimInstance Win32_Process |",
173+"Select-Object ProcessId,ParentProcessId,KernelModeTime,UserModeTime,WorkingSetSize |",
174+"ConvertTo-Json -Compress",
175+].join(" "),
176+],
177+{
178+encoding: "utf8",
179+maxBuffer: 16 * 1024 * 1024,
180+stdio: ["ignore", "pipe", "ignore"],
181+},
182+);
183+if (result.status !== 0) {
184+return null;
185+}
186+return parseWindowsProcessTreeSnapshot(result.stdout);
187+}
188+29189export function readProcessTreeCpuMs(rootPid: number | null | undefined): number | null {
30-if (
31-typeof rootPid !== "number" ||
32-!Number.isInteger(rootPid) ||
33-rootPid <= 0 ||
34-process.platform === "win32"
35-) {
190+if (typeof rootPid !== "number" || !Number.isInteger(rootPid) || rootPid <= 0) {
36191return null;
37192}
193+if (process.platform === "win32") {
194+const snapshot = readWindowsProcessTreeSnapshot();
195+return snapshot
196+ ? collectProcessTreeMetric(rootPid, snapshot.childrenByParent, snapshot.cpuByPid)
197+ : null;
198+}
38199const result = spawnSync("ps", ["-eo", "pid=,ppid=,time="], {
39200encoding: "utf8",
40201stdio: ["ignore", "pipe", "ignore"],
@@ -66,32 +227,19 @@ export function readProcessTreeCpuMs(rootPid: number | null | undefined): number
66227return null;
67228}
6822969-let totalCpuMs = 0;
70-const seen = new Set<number>();
71-const stack: number[] = [rootPid];
72-while (stack.length > 0) {
73-const pid = stack.pop();
74-if (pid === undefined || seen.has(pid)) {
75-continue;
76-}
77-seen.add(pid);
78-totalCpuMs += cpuByPid.get(pid) ?? 0;
79-for (const childPid of childrenByParent.get(pid) ?? []) {
80-stack.push(childPid);
81-}
82-}
83-return totalCpuMs;
230+return collectProcessTreeMetric(rootPid, childrenByParent, cpuByPid);
84231}
8523286233export function readProcessTreeRssBytes(rootPid: number | null | undefined): number | null {
87-if (
88-typeof rootPid !== "number" ||
89-!Number.isInteger(rootPid) ||
90-rootPid <= 0 ||
91-process.platform === "win32"
92-) {
234+if (typeof rootPid !== "number" || !Number.isInteger(rootPid) || rootPid <= 0) {
93235return null;
94236}
237+if (process.platform === "win32") {
238+const snapshot = readWindowsProcessTreeSnapshot();
239+return snapshot
240+ ? collectProcessTreeMetric(rootPid, snapshot.childrenByParent, snapshot.rssByPid)
241+ : null;
242+}
95243const result = spawnSync("ps", ["-eo", "pid=,ppid=,rss="], {
96244encoding: "utf8",
97245stdio: ["ignore", "pipe", "ignore"],
@@ -123,19 +271,5 @@ export function readProcessTreeRssBytes(rootPid: number | null | undefined): num
123271return null;
124272}
125273126-let totalRssBytes = 0;
127-const seen = new Set<number>();
128-const stack: number[] = [rootPid];
129-while (stack.length > 0) {
130-const pid = stack.pop();
131-if (pid === undefined || seen.has(pid)) {
132-continue;
133-}
134-seen.add(pid);
135-totalRssBytes += rssByPid.get(pid) ?? 0;
136-for (const childPid of childrenByParent.get(pid) ?? []) {
137-stack.push(childPid);
138-}
139-}
140-return totalRssBytes;
274+return collectProcessTreeMetric(rootPid, childrenByParent, rssByPid);
141275}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。