z-clean 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.ko.md +118 -0
- package/README.md +241 -97
- package/README.zh.md +118 -0
- package/assets/zclean-hero.png +0 -0
- package/bin/zclean.js +95 -59
- package/package.json +45 -8
- package/src/audit-actions.js +88 -0
- package/src/audit-candidates.js +86 -0
- package/src/audit-history.js +69 -0
- package/src/audit-printer.js +83 -0
- package/src/audit-report.js +135 -0
- package/src/audit.js +35 -0
- package/src/cache.js +256 -0
- package/src/commands/scheduler.js +58 -0
- package/src/commands/trust.js +157 -0
- package/src/config.js +125 -20
- package/src/detector/orphan.js +21 -131
- package/src/detector/patterns.js +97 -14
- package/src/detector/whitelist.js +26 -105
- package/src/doctor/checks.js +229 -0
- package/src/doctor/render.js +37 -0
- package/src/doctor.js +22 -0
- package/src/installer/bin-path.js +106 -0
- package/src/installer/hook.js +15 -4
- package/src/installer/launchd.js +57 -28
- package/src/installer/systemd.js +18 -29
- package/src/installer/taskscheduler.js +29 -36
- package/src/killer.js +68 -52
- package/src/process-tree.js +338 -0
- package/src/reporter.js +61 -2
- package/src/scanner.js +97 -241
- package/src/windows-processes.js +257 -0
package/src/scanner.js
CHANGED
|
@@ -1,90 +1,91 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
5
|
-
const { matchPattern } = require('./detector/patterns');
|
|
6
|
-
const { checkOrphan, isInTerminalMultiplexer } = require('./detector/orphan');
|
|
3
|
+
const { ProcessTree } = require('./process-tree');
|
|
4
|
+
const { matchPattern, AI_DIR_REGEX, buildAiDirRegex } = require('./detector/patterns');
|
|
7
5
|
const { isWhitelisted } = require('./detector/whitelist');
|
|
8
6
|
const { parseDuration, parseMemory } = require('./config');
|
|
9
7
|
|
|
10
|
-
const platform = os.platform();
|
|
11
|
-
|
|
12
8
|
/**
|
|
13
9
|
* Scan for zombie/orphan processes left by AI coding tools.
|
|
14
10
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Uses ProcessTree for all process data — single ps/wmic call,
|
|
12
|
+
* zero execSync during scan loop.
|
|
17
13
|
*
|
|
18
14
|
* @param {object} config — loaded zclean config
|
|
19
|
-
* @param {object} opts — { sessionPid?: number }
|
|
15
|
+
* @param {object} opts — { sessionPid?: number }
|
|
16
|
+
* @returns {Array<{pid, name, cmd, ppid, mem, age, startTime, reason, pattern}>}
|
|
20
17
|
*/
|
|
21
18
|
function scan(config, opts = {}) {
|
|
22
|
-
const
|
|
19
|
+
const tree = ProcessTree.build();
|
|
20
|
+
const aiDirRegex = buildAiDirRegex(config.customAiDirs);
|
|
23
21
|
const zombies = [];
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const warnings = Array.isArray(tree.warnings) ? [...tree.warnings] : [];
|
|
23
|
+
const errors = Array.isArray(tree.errors) ? [...tree.errors] : [];
|
|
24
|
+
const sessionPid = normalizePid(opts.sessionPid);
|
|
25
|
+
const session = sessionPid
|
|
26
|
+
? { pid: sessionPid, filtered: true, matched: 0, excluded: 0, unattributed: 0 }
|
|
27
|
+
: null;
|
|
28
|
+
|
|
29
|
+
// Multiplexer detection function for tree queries
|
|
30
|
+
const isMultiplexer = (proc) => {
|
|
31
|
+
const comm = proc.cmd.split(/\s+/)[0].split('/').pop();
|
|
32
|
+
return comm === 'tmux' || comm === 'screen';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Iterate all processes in the tree
|
|
36
|
+
for (const [, proc] of tree.byPid) {
|
|
26
37
|
// Match against known AI tool patterns
|
|
27
|
-
const pattern = matchPattern(proc.cmd);
|
|
38
|
+
const pattern = matchPattern(proc.cmd, config);
|
|
28
39
|
if (!pattern) continue;
|
|
29
40
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
proc.ppid = orphanResult.ppid;
|
|
41
|
+
// AI path filter: skip generic patterns if command isn't in an AI tool directory
|
|
42
|
+
if (pattern.aiPathRequired && !aiDirRegex.test(proc.cmd)) continue;
|
|
33
43
|
|
|
34
|
-
//
|
|
35
|
-
|
|
44
|
+
// Check orphan status via tree (no execSync)
|
|
45
|
+
const orphanResult = tree.isOrphan(proc.pid);
|
|
46
|
+
const sessionRelated = sessionPid
|
|
47
|
+
? tree.hasAncestorMatching(proc.pid, (p) => p.pid === sessionPid)
|
|
48
|
+
: false;
|
|
36
49
|
|
|
37
|
-
//
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
// Check whitelist
|
|
41
|
-
const whitelistResult = isWhitelisted(proc, config);
|
|
42
|
-
if (whitelistResult.protected) continue;
|
|
43
|
-
|
|
44
|
-
// Check maxOrphanAge if defined on the pattern
|
|
45
|
-
if (pattern.maxOrphanAge) {
|
|
46
|
-
const maxAge = parseDuration(pattern.maxOrphanAge);
|
|
47
|
-
if (maxAge && proc.age < maxAge) continue;
|
|
48
|
-
}
|
|
50
|
+
// If pattern requires orphan status and process isn't orphaned, skip
|
|
51
|
+
if (pattern.orphanOnly && !orphanResult.isOrphan && !sessionRelated) continue;
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (threshold && proc.mem < threshold) {
|
|
56
|
-
// Also check if age exceeds maxOrphanAge
|
|
57
|
-
if (pattern.maxOrphanAge) {
|
|
58
|
-
const maxAge = parseDuration(pattern.maxOrphanAge);
|
|
59
|
-
if (maxAge && proc.age < maxAge) continue;
|
|
60
|
-
} else {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
53
|
+
if (sessionPid && !sessionRelated) {
|
|
54
|
+
if (orphanResult.isOrphan) {
|
|
55
|
+
session.unattributed++;
|
|
56
|
+
} else {
|
|
57
|
+
session.excluded++;
|
|
63
58
|
}
|
|
59
|
+
continue;
|
|
64
60
|
}
|
|
61
|
+
if (sessionRelated) session.matched++;
|
|
65
62
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
// tmux/screen protection — but NOT for orphans (PPID=1).
|
|
64
|
+
// If a process is orphaned, it's already detached from any tmux session,
|
|
65
|
+
// so the multiplexer check is irrelevant. This fixes the tmux orphan bug.
|
|
66
|
+
if (!orphanResult.isOrphan && tree.hasAncestorMatching(proc.pid, isMultiplexer)) continue;
|
|
67
|
+
|
|
68
|
+
// Check whitelist (now tree-aware — no execSync)
|
|
69
|
+
const whitelistResult = isWhitelisted(proc, config, tree);
|
|
70
|
+
if (whitelistResult.protected) continue;
|
|
71
|
+
|
|
72
|
+
const threshold = evaluateThresholds(pattern, config, proc);
|
|
73
|
+
if (!threshold.passed) continue;
|
|
72
74
|
|
|
73
75
|
// Build reason string
|
|
74
76
|
const reasons = [];
|
|
75
77
|
reasons.push(`pattern:${pattern.name}`);
|
|
78
|
+
if (sessionRelated) reasons.push(`session-pid:${sessionPid}`);
|
|
76
79
|
if (orphanResult.isOrphan) reasons.push(`orphan:${orphanResult.reason}`);
|
|
77
|
-
if (
|
|
78
|
-
if (
|
|
79
|
-
reasons.push('memory-exceeded');
|
|
80
|
-
}
|
|
80
|
+
if (threshold.ageExceeded) reasons.push('age-exceeded');
|
|
81
|
+
if (threshold.memoryExceeded) reasons.push('memory-exceeded');
|
|
81
82
|
|
|
82
83
|
zombies.push({
|
|
83
84
|
pid: proc.pid,
|
|
84
85
|
name: pattern.name,
|
|
85
86
|
cmd: proc.cmd,
|
|
86
|
-
ppid:
|
|
87
|
-
mem: proc.mem,
|
|
87
|
+
ppid: orphanResult.ppid,
|
|
88
|
+
mem: proc.mem || 0,
|
|
88
89
|
age: proc.age,
|
|
89
90
|
startTime: proc.startTime,
|
|
90
91
|
reason: reasons.join(', '),
|
|
@@ -92,205 +93,60 @@ function scan(config, opts = {}) {
|
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
* Returns array of { pid, cmd, mem, age, startTime }
|
|
103
|
-
*/
|
|
104
|
-
function listProcesses() {
|
|
105
|
-
if (platform === 'win32') {
|
|
106
|
-
return listProcessesWindows();
|
|
107
|
-
}
|
|
108
|
-
return listProcessesUnix();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Unix process listing via `ps aux`.
|
|
113
|
-
*/
|
|
114
|
-
function listProcessesUnix() {
|
|
115
|
-
let output;
|
|
116
|
-
try {
|
|
117
|
-
// ps aux with etime for age calculation
|
|
118
|
-
// Columns: PID, RSS (KB), ELAPSED, STARTED, COMMAND
|
|
119
|
-
output = execSync('ps -eo pid=,rss=,etime=,lstart=,command=', {
|
|
120
|
-
encoding: 'utf-8',
|
|
121
|
-
timeout: 10000,
|
|
122
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
96
|
+
if (session && session.unattributed > 0) {
|
|
97
|
+
warnings.push({
|
|
98
|
+
code: 'session-attribution-gap',
|
|
99
|
+
sessionPid,
|
|
100
|
+
count: session.unattributed,
|
|
101
|
+
message: `${session.unattributed} candidate process${session.unattributed === 1 ? '' : 'es'} could not be proven related to session PID ${sessionPid}`,
|
|
123
102
|
});
|
|
124
|
-
} catch {
|
|
125
|
-
return [];
|
|
126
103
|
}
|
|
127
104
|
|
|
128
|
-
|
|
129
|
-
const lines = output.trim().split('\n');
|
|
130
|
-
const myPid = process.pid;
|
|
131
|
-
|
|
132
|
-
for (const line of lines) {
|
|
133
|
-
const trimmed = line.trim();
|
|
134
|
-
if (!trimmed) continue;
|
|
135
|
-
|
|
136
|
-
// Parse the fixed-width fields
|
|
137
|
-
// Format: " PID RSS ELAPSED LSTART COMMAND"
|
|
138
|
-
const match = trimmed.match(
|
|
139
|
-
/^\s*(\d+)\s+(\d+)\s+([\d:.-]+)\s+\w+\s+(\w+\s+\d+\s+[\d:]+\s+\d+)\s+(.+)$/
|
|
140
|
-
);
|
|
141
|
-
if (!match) continue;
|
|
142
|
-
|
|
143
|
-
const pid = parseInt(match[1], 10);
|
|
144
|
-
const rssKB = parseInt(match[2], 10);
|
|
145
|
-
const elapsed = match[3];
|
|
146
|
-
const lstart = match[4];
|
|
147
|
-
const cmd = match[5];
|
|
148
|
-
|
|
149
|
-
// Skip our own process
|
|
150
|
-
if (pid === myPid) continue;
|
|
151
|
-
|
|
152
|
-
// Parse elapsed time (format: [[DD-]HH:]MM:SS)
|
|
153
|
-
const ageMs = parseElapsed(elapsed);
|
|
154
|
-
|
|
155
|
-
// Parse start time
|
|
156
|
-
let startTime = null;
|
|
157
|
-
try {
|
|
158
|
-
startTime = new Date(lstart).toISOString();
|
|
159
|
-
} catch {
|
|
160
|
-
// Ignore parse errors
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
processes.push({
|
|
164
|
-
pid,
|
|
165
|
-
cmd,
|
|
166
|
-
mem: rssKB * 1024, // Convert KB to bytes
|
|
167
|
-
age: ageMs,
|
|
168
|
-
startTime,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return processes;
|
|
105
|
+
return attachDiagnostics(zombies, { warnings, errors, session });
|
|
173
106
|
}
|
|
174
107
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
function listProcessesWindows() {
|
|
179
|
-
let output;
|
|
180
|
-
try {
|
|
181
|
-
output = execSync(
|
|
182
|
-
'wmic process get ProcessId,CommandLine,WorkingSetSize,CreationDate /format:csv',
|
|
183
|
-
{ encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
|
|
184
|
-
);
|
|
185
|
-
} catch {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const processes = [];
|
|
190
|
-
const lines = output.trim().split('\n');
|
|
191
|
-
const myPid = process.pid;
|
|
192
|
-
|
|
193
|
-
for (const line of lines) {
|
|
194
|
-
const parts = line.trim().split(',');
|
|
195
|
-
if (parts.length < 5) continue;
|
|
196
|
-
|
|
197
|
-
// CSV format: Node, CommandLine, CreationDate, ProcessId, WorkingSetSize
|
|
198
|
-
const cmd = parts[1];
|
|
199
|
-
const creationDate = parts[2];
|
|
200
|
-
const pid = parseInt(parts[3], 10);
|
|
201
|
-
const workingSet = parseInt(parts[4], 10);
|
|
202
|
-
|
|
203
|
-
if (isNaN(pid) || pid === myPid || !cmd) continue;
|
|
204
|
-
|
|
205
|
-
// Parse WMI datetime: YYYYMMDDHHMMSS.MMMMMM+UUU
|
|
206
|
-
let ageMs = 0;
|
|
207
|
-
let startTime = null;
|
|
208
|
-
if (creationDate) {
|
|
209
|
-
try {
|
|
210
|
-
const year = creationDate.substring(0, 4);
|
|
211
|
-
const month = creationDate.substring(4, 6);
|
|
212
|
-
const day = creationDate.substring(6, 8);
|
|
213
|
-
const hours = creationDate.substring(8, 10);
|
|
214
|
-
const minutes = creationDate.substring(10, 12);
|
|
215
|
-
const seconds = creationDate.substring(12, 14);
|
|
216
|
-
const dt = new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
|
|
217
|
-
startTime = dt.toISOString();
|
|
218
|
-
ageMs = Date.now() - dt.getTime();
|
|
219
|
-
} catch {
|
|
220
|
-
// Ignore
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
processes.push({
|
|
225
|
-
pid,
|
|
226
|
-
cmd,
|
|
227
|
-
mem: workingSet || 0,
|
|
228
|
-
age: ageMs,
|
|
229
|
-
startTime,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return processes;
|
|
108
|
+
function normalizePid(value) {
|
|
109
|
+
const pid = Number(value);
|
|
110
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
234
111
|
}
|
|
235
112
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
let days = 0;
|
|
244
|
-
let rest = elapsed.trim();
|
|
245
|
-
|
|
246
|
-
// Check for "DD-" prefix
|
|
247
|
-
const dayMatch = rest.match(/^(\d+)-(.+)$/);
|
|
248
|
-
if (dayMatch) {
|
|
249
|
-
days = parseInt(dayMatch[1], 10);
|
|
250
|
-
rest = dayMatch[2];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const parts = rest.split(':').map((p) => parseInt(p, 10));
|
|
113
|
+
function evaluateThresholds(pattern, config, proc) {
|
|
114
|
+
const maxAgeValue = getPatternMaxAge(pattern, config);
|
|
115
|
+
const memoryValue = getPatternMemoryThreshold(pattern, config);
|
|
116
|
+
const maxAge = maxAgeValue ? parseDuration(maxAgeValue) : null;
|
|
117
|
+
const memoryThreshold = memoryValue ? parseMemory(memoryValue) : null;
|
|
118
|
+
const ageExceeded = Boolean(maxAge && proc.age >= maxAge);
|
|
119
|
+
const memoryExceeded = Boolean(memoryThreshold && proc.mem >= memoryThreshold);
|
|
254
120
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
[hours, minutes, seconds] = parts;
|
|
258
|
-
} else if (parts.length === 2) {
|
|
259
|
-
[minutes, seconds] = parts;
|
|
260
|
-
} else if (parts.length === 1) {
|
|
261
|
-
[seconds] = parts;
|
|
121
|
+
if (maxAge && memoryThreshold) {
|
|
122
|
+
return { passed: ageExceeded || memoryExceeded, ageExceeded, memoryExceeded };
|
|
262
123
|
}
|
|
263
|
-
|
|
264
|
-
|
|
124
|
+
if (maxAge) return { passed: ageExceeded, ageExceeded, memoryExceeded: false };
|
|
125
|
+
if (memoryThreshold) return { passed: memoryExceeded, ageExceeded: false, memoryExceeded };
|
|
126
|
+
return { passed: true, ageExceeded: false, memoryExceeded: false };
|
|
265
127
|
}
|
|
266
128
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (platform === 'win32') return false;
|
|
273
|
-
|
|
274
|
-
const visited = new Set();
|
|
275
|
-
let currentPid = pid;
|
|
129
|
+
function getPatternMaxAge(pattern, config) {
|
|
130
|
+
if (!pattern.maxOrphanAge) return null;
|
|
131
|
+
if (pattern.aiPathRequired && config.maxAge) return config.maxAge;
|
|
132
|
+
return pattern.maxOrphanAge;
|
|
133
|
+
}
|
|
276
134
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
135
|
+
function getPatternMemoryThreshold(pattern, config) {
|
|
136
|
+
if (!pattern.memThreshold) return null;
|
|
137
|
+
return config.memoryThreshold || pattern.memThreshold;
|
|
138
|
+
}
|
|
280
139
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
} catch {
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
140
|
+
function attachDiagnostics(zombies, diagnostics) {
|
|
141
|
+
zombies.warnings = diagnostics.warnings || [];
|
|
142
|
+
zombies.errors = diagnostics.errors || [];
|
|
143
|
+
zombies.enumerationFailed = hasScanErrors(zombies);
|
|
144
|
+
if (diagnostics.session) zombies.session = diagnostics.session;
|
|
145
|
+
return zombies;
|
|
146
|
+
}
|
|
292
147
|
|
|
293
|
-
|
|
148
|
+
function hasScanErrors(result) {
|
|
149
|
+
return Array.isArray(result?.errors) && result.errors.length > 0;
|
|
294
150
|
}
|
|
295
151
|
|
|
296
|
-
module.exports = { scan,
|
|
152
|
+
module.exports = { scan, hasScanErrors };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizePid(pid) {
|
|
4
|
+
const parsed = Number(pid);
|
|
5
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readWMICProcesses(runtime) {
|
|
9
|
+
let output;
|
|
10
|
+
try {
|
|
11
|
+
output = runtime.execSync(
|
|
12
|
+
'wmic process get ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate /format:csv',
|
|
13
|
+
{ encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
|
|
14
|
+
);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return {
|
|
17
|
+
processes: [],
|
|
18
|
+
warnings: [providerDiagnostic('wmic', 'process-enumeration-provider-failed', err)],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const processes = parseWMICOutput(String(output || ''), runtime);
|
|
23
|
+
return {
|
|
24
|
+
processes,
|
|
25
|
+
warnings: processes.length > 0 ? [] : [providerDiagnostic(
|
|
26
|
+
'wmic',
|
|
27
|
+
'process-enumeration-provider-empty',
|
|
28
|
+
'WMIC returned no process rows.'
|
|
29
|
+
)],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readCIMProcesses(runtime) {
|
|
34
|
+
let output;
|
|
35
|
+
try {
|
|
36
|
+
output = runtime.execSync(
|
|
37
|
+
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate | ConvertTo-Json -Compress"',
|
|
38
|
+
{ encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
|
|
39
|
+
);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
processes: [],
|
|
43
|
+
warnings: [providerDiagnostic('cim', 'process-enumeration-provider-failed', err)],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const processes = parseCIMOutput(String(output || ''), runtime);
|
|
48
|
+
return {
|
|
49
|
+
processes,
|
|
50
|
+
warnings: processes.length > 0 ? [] : [providerDiagnostic(
|
|
51
|
+
'cim',
|
|
52
|
+
'process-enumeration-provider-empty',
|
|
53
|
+
'CIM returned no process rows.'
|
|
54
|
+
)],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readWindowsProcess(pid, runtime) {
|
|
59
|
+
const safePid = normalizePid(pid);
|
|
60
|
+
if (!safePid) return null;
|
|
61
|
+
|
|
62
|
+
let output;
|
|
63
|
+
try {
|
|
64
|
+
output = runtime.execSync(
|
|
65
|
+
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress"`,
|
|
66
|
+
{ encoding: 'utf-8', timeout: 5000, maxBuffer: 1024 * 1024 }
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rows = parseJsonRows(String(output || ''));
|
|
73
|
+
const row = rows.find((item) => Number(readField(item, 'ProcessId')) === safePid);
|
|
74
|
+
if (!row) return null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
pid: safePid,
|
|
78
|
+
cmd: String(readField(row, 'CommandLine') || ''),
|
|
79
|
+
startTime: parseWindowsDate(readField(row, 'CreationDate')),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function windowsProcessExists(pid, runtime) {
|
|
84
|
+
const safePid = normalizePid(pid);
|
|
85
|
+
if (!safePid) return false;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const output = runtime.execSync(
|
|
89
|
+
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
90
|
+
{ encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }
|
|
91
|
+
);
|
|
92
|
+
return parseJsonRows(String(output || '')).some((row) => Number(readField(row, 'ProcessId')) === safePid);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseWMICOutput(output, runtime) {
|
|
99
|
+
const processes = [];
|
|
100
|
+
let headers = null;
|
|
101
|
+
|
|
102
|
+
for (const line of output.trim().split(/\r?\n/)) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed) continue;
|
|
105
|
+
|
|
106
|
+
const parts = parseCsvLine(trimmed);
|
|
107
|
+
if (!headers) {
|
|
108
|
+
headers = parts.map((h) => h.trim().toLowerCase());
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parts.length < headers.length) continue;
|
|
113
|
+
|
|
114
|
+
const idx = (name) => headers.indexOf(name);
|
|
115
|
+
const col = (name) => (idx(name) >= 0 ? parts[idx(name)] : '');
|
|
116
|
+
const pid = Number(col('processid'));
|
|
117
|
+
const cmd = String(col('commandline') || '').trim();
|
|
118
|
+
if (!Number.isFinite(pid) || pid === runtime.currentPid || !cmd) continue;
|
|
119
|
+
|
|
120
|
+
processes.push(processFromWindowsFields({
|
|
121
|
+
pid,
|
|
122
|
+
ppid: Number(col('parentprocessid')),
|
|
123
|
+
cmd,
|
|
124
|
+
workingSet: Number(col('workingsetsize')),
|
|
125
|
+
creationDate: col('creationdate'),
|
|
126
|
+
runtime,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return processes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseCIMOutput(output, runtime) {
|
|
134
|
+
const rows = parseJsonRows(output);
|
|
135
|
+
const processes = [];
|
|
136
|
+
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
const pid = Number(readField(row, 'ProcessId'));
|
|
139
|
+
const cmd = String(readField(row, 'CommandLine') || '').trim();
|
|
140
|
+
if (!Number.isFinite(pid) || pid === runtime.currentPid || !cmd) continue;
|
|
141
|
+
|
|
142
|
+
processes.push(processFromWindowsFields({
|
|
143
|
+
pid,
|
|
144
|
+
ppid: Number(readField(row, 'ParentProcessId')),
|
|
145
|
+
cmd,
|
|
146
|
+
workingSet: Number(readField(row, 'WorkingSetSize')),
|
|
147
|
+
creationDate: readField(row, 'CreationDate'),
|
|
148
|
+
runtime,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return processes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function processFromWindowsFields(fields) {
|
|
156
|
+
const startTime = parseWindowsDate(fields.creationDate);
|
|
157
|
+
const startMs = startTime ? new Date(startTime).getTime() : 0;
|
|
158
|
+
const now = typeof fields.runtime.now === 'function' ? fields.runtime.now() : fields.runtime.now;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
pid: fields.pid,
|
|
162
|
+
ppid: Number.isFinite(fields.ppid) ? fields.ppid : 0,
|
|
163
|
+
cmd: fields.cmd,
|
|
164
|
+
mem: Number.isFinite(fields.workingSet) ? fields.workingSet : 0,
|
|
165
|
+
age: startMs && Number.isFinite(now) ? now - startMs : 0,
|
|
166
|
+
startTime,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseJsonRows(output) {
|
|
171
|
+
if (!output.trim()) return [];
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(output);
|
|
174
|
+
if (Array.isArray(parsed)) return parsed;
|
|
175
|
+
if (parsed && typeof parsed === 'object') return [parsed];
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseWindowsDate(value) {
|
|
183
|
+
if (!value) return null;
|
|
184
|
+
const raw = String(value).trim();
|
|
185
|
+
if (!raw) return null;
|
|
186
|
+
|
|
187
|
+
if (/^\d{14}/.test(raw)) {
|
|
188
|
+
const year = raw.substring(0, 4);
|
|
189
|
+
const month = raw.substring(4, 6);
|
|
190
|
+
const day = raw.substring(6, 8);
|
|
191
|
+
const hours = raw.substring(8, 10);
|
|
192
|
+
const minutes = raw.substring(10, 12);
|
|
193
|
+
const seconds = raw.substring(12, 14);
|
|
194
|
+
const date = new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
|
|
195
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const date = new Date(raw);
|
|
199
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readField(row, name) {
|
|
203
|
+
if (!row || typeof row !== 'object') return undefined;
|
|
204
|
+
if (Object.prototype.hasOwnProperty.call(row, name)) return row[name];
|
|
205
|
+
|
|
206
|
+
const lower = name.toLowerCase();
|
|
207
|
+
const key = Object.keys(row).find((candidate) => candidate.toLowerCase() === lower);
|
|
208
|
+
return key ? row[key] : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseCsvLine(line) {
|
|
212
|
+
const parts = [];
|
|
213
|
+
let current = '';
|
|
214
|
+
let quoted = false;
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
217
|
+
const char = line[i];
|
|
218
|
+
if (char === '"') {
|
|
219
|
+
if (quoted && line[i + 1] === '"') {
|
|
220
|
+
current += '"';
|
|
221
|
+
i += 1;
|
|
222
|
+
} else {
|
|
223
|
+
quoted = !quoted;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (char === ',' && !quoted) {
|
|
229
|
+
parts.push(current);
|
|
230
|
+
current = '';
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
current += char;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parts.push(current);
|
|
238
|
+
return parts;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function providerDiagnostic(provider, code, err) {
|
|
242
|
+
return {
|
|
243
|
+
code,
|
|
244
|
+
provider,
|
|
245
|
+
message: err instanceof Error ? err.message : String(err || ''),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
normalizePid,
|
|
251
|
+
readCIMProcesses,
|
|
252
|
+
readWMICProcesses,
|
|
253
|
+
readWindowsProcess,
|
|
254
|
+
windowsProcessExists,
|
|
255
|
+
parseCIMOutput,
|
|
256
|
+
parseWindowsDate,
|
|
257
|
+
};
|