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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { readCIMProcesses, readWMICProcesses } = require('./windows-processes');
|
|
6
|
+
|
|
7
|
+
const platform = os.platform();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory process tree for O(1) PID lookup and O(depth) ancestor traversal.
|
|
11
|
+
* Built from one platform enumeration call sequence — zero execSync during queries.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const tree = ProcessTree.build(); // platform auto-detect
|
|
15
|
+
* tree.isOrphan(pid) // no execSync
|
|
16
|
+
* tree.hasAncestorMatching(pid, testFn) // no execSync
|
|
17
|
+
*/
|
|
18
|
+
class ProcessTree {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Array<{pid: number, ppid: number, cmd: string, mem: number, age: number, startTime: string|null}>} processes
|
|
21
|
+
*/
|
|
22
|
+
constructor(processes, diagnostics = {}) {
|
|
23
|
+
this.warnings = Array.isArray(diagnostics.warnings) ? diagnostics.warnings : [];
|
|
24
|
+
this.errors = Array.isArray(diagnostics.errors) ? diagnostics.errors : [];
|
|
25
|
+
this.platform = diagnostics.platform || platform;
|
|
26
|
+
|
|
27
|
+
/** @type {Map<number, object>} */
|
|
28
|
+
this.byPid = new Map();
|
|
29
|
+
/** @type {Map<number, number[]>} pid → [childPids] */
|
|
30
|
+
this.childrenMap = new Map();
|
|
31
|
+
|
|
32
|
+
for (const proc of processes) {
|
|
33
|
+
this.byPid.set(proc.pid, proc);
|
|
34
|
+
if (!this.childrenMap.has(proc.ppid)) {
|
|
35
|
+
this.childrenMap.set(proc.ppid, []);
|
|
36
|
+
}
|
|
37
|
+
this.childrenMap.get(proc.ppid).push(proc.pid);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* O(1) lookup by PID.
|
|
43
|
+
* @returns {object|null}
|
|
44
|
+
*/
|
|
45
|
+
get(pid) {
|
|
46
|
+
return this.byPid.get(pid) || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parent process info, or null if PID not in tree or parent unknown.
|
|
51
|
+
* @returns {object|null}
|
|
52
|
+
*/
|
|
53
|
+
parent(pid) {
|
|
54
|
+
const proc = this.byPid.get(pid);
|
|
55
|
+
if (!proc) return null;
|
|
56
|
+
return this.byPid.get(proc.ppid) || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Array of direct child process infos.
|
|
61
|
+
* @returns {object[]}
|
|
62
|
+
*/
|
|
63
|
+
children(pid) {
|
|
64
|
+
const childPids = this.childrenMap.get(pid) || [];
|
|
65
|
+
return childPids.map((cpid) => this.byPid.get(cpid)).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ancestor chain starting from the direct parent, root last.
|
|
70
|
+
* Stops at PID 1 or when the parent is not in the tree.
|
|
71
|
+
* @returns {object[]}
|
|
72
|
+
*/
|
|
73
|
+
ancestors(pid) {
|
|
74
|
+
const result = [];
|
|
75
|
+
const visited = new Set();
|
|
76
|
+
let current = this.byPid.get(pid);
|
|
77
|
+
|
|
78
|
+
while (current) {
|
|
79
|
+
if (visited.has(current.pid)) break;
|
|
80
|
+
visited.add(current.pid);
|
|
81
|
+
const par = this.byPid.get(current.ppid);
|
|
82
|
+
if (!par) break;
|
|
83
|
+
result.push(par);
|
|
84
|
+
current = par;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determine orphan status from in-memory tree — no execSync.
|
|
92
|
+
*
|
|
93
|
+
* @returns {{ isOrphan: boolean, ppid: number|null, reason: string }}
|
|
94
|
+
*/
|
|
95
|
+
isOrphan(pid) {
|
|
96
|
+
const proc = this.byPid.get(pid);
|
|
97
|
+
if (!proc) return { isOrphan: false, ppid: null, reason: 'not-in-tree' };
|
|
98
|
+
|
|
99
|
+
const { ppid } = proc;
|
|
100
|
+
|
|
101
|
+
if (this.platform === 'win32') {
|
|
102
|
+
if (!this.byPid.has(ppid)) {
|
|
103
|
+
return { isOrphan: true, ppid, reason: 'parent-gone' };
|
|
104
|
+
}
|
|
105
|
+
return { isOrphan: false, ppid, reason: 'has-parent' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// macOS: PPID 1 = reparented to launchd
|
|
109
|
+
if (this.platform === 'darwin' && ppid === 1) {
|
|
110
|
+
return { isOrphan: true, ppid: 1, reason: 'reparented-to-launchd' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Linux: PPID 1 = reparented to init
|
|
114
|
+
if (this.platform === 'linux' && ppid === 1) {
|
|
115
|
+
return { isOrphan: true, ppid: 1, reason: 'reparented-to-init' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Linux: parent is systemd --user (user session slice)
|
|
119
|
+
if (this.platform === 'linux' && ppid > 1) {
|
|
120
|
+
const parentProc = this.byPid.get(ppid);
|
|
121
|
+
if (parentProc) {
|
|
122
|
+
// Extract process name (basename of first token)
|
|
123
|
+
const comm = parentProc.cmd.split(/\s+/)[0].split('/').pop();
|
|
124
|
+
if (comm === 'systemd') {
|
|
125
|
+
return { isOrphan: true, ppid, reason: 'reparented-to-systemd-user' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Has living parent in the tree
|
|
131
|
+
if (this.byPid.has(ppid)) {
|
|
132
|
+
return { isOrphan: false, ppid, reason: 'has-parent' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parent not in tree and not PID 1 — likely died
|
|
136
|
+
return { isOrphan: true, ppid, reason: 'parent-gone' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Walk ancestor chain, return true if any ancestor passes testFn.
|
|
141
|
+
* Cycle-safe via visited Set. Stops at PID 1 or when parent not in tree.
|
|
142
|
+
*
|
|
143
|
+
* @param {number} pid
|
|
144
|
+
* @param {(proc: object) => boolean} testFn
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
hasAncestorMatching(pid, testFn) {
|
|
148
|
+
const visited = new Set();
|
|
149
|
+
let current = this.byPid.get(pid);
|
|
150
|
+
|
|
151
|
+
while (current) {
|
|
152
|
+
if (visited.has(current.pid)) break; // cycle protection
|
|
153
|
+
visited.add(current.pid);
|
|
154
|
+
|
|
155
|
+
const parent = this.byPid.get(current.ppid);
|
|
156
|
+
if (!parent || parent.pid === 1) break;
|
|
157
|
+
|
|
158
|
+
if (testFn(parent)) return true;
|
|
159
|
+
current = parent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Unix: build ProcessTree from a single `ps` call.
|
|
167
|
+
* Format: pid= ppid= rss= etime= lstart= command=
|
|
168
|
+
*
|
|
169
|
+
* @returns {ProcessTree}
|
|
170
|
+
*/
|
|
171
|
+
static fromPS(options = {}) {
|
|
172
|
+
const runtime = runtimeOptions(options);
|
|
173
|
+
let output;
|
|
174
|
+
try {
|
|
175
|
+
// LC_ALL=C forces English date output in lstart= regardless of system locale
|
|
176
|
+
output = runtime.execSync('LC_ALL=C ps -eo pid=,ppid=,rss=,etime=,lstart=,command=', {
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
timeout: 10000,
|
|
179
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return new ProcessTree([], {
|
|
183
|
+
platform: runtime.platform,
|
|
184
|
+
errors: [providerDiagnostic('ps', 'process-enumeration-provider-failed', err)],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const processes = [];
|
|
189
|
+
const myPid = runtime.currentPid;
|
|
190
|
+
|
|
191
|
+
for (const line of output.trim().split('\n')) {
|
|
192
|
+
const trimmed = line.trim();
|
|
193
|
+
if (!trimmed) continue;
|
|
194
|
+
|
|
195
|
+
// Format: PID PPID RSS ELAPSED [DAY ]MON DD HH:MM:SS YEAR COMMAND
|
|
196
|
+
// Example: " 123 456 2048 01:23:45 Mon Jan 1 12:00:00 2024 node server.js"
|
|
197
|
+
const match = trimmed.match(
|
|
198
|
+
/^\s*(\d+)\s+(\d+)\s+(\d+)\s+([\d:.-]+)\s+\w+\s+(\w+\s+\d+\s+[\d:]+\s+\d+)\s+(.+)$/
|
|
199
|
+
);
|
|
200
|
+
if (!match) continue;
|
|
201
|
+
|
|
202
|
+
const pid = parseInt(match[1], 10);
|
|
203
|
+
if (pid === myPid) continue;
|
|
204
|
+
|
|
205
|
+
const ppid = parseInt(match[2], 10);
|
|
206
|
+
const rssKB = parseInt(match[3], 10);
|
|
207
|
+
const elapsed = match[4];
|
|
208
|
+
const lstart = match[5];
|
|
209
|
+
const cmd = match[6];
|
|
210
|
+
|
|
211
|
+
let startTime = null;
|
|
212
|
+
try { startTime = new Date(lstart).toISOString(); } catch { /* ignore */ }
|
|
213
|
+
|
|
214
|
+
processes.push({
|
|
215
|
+
pid,
|
|
216
|
+
ppid,
|
|
217
|
+
cmd,
|
|
218
|
+
mem: rssKB * 1024, // KB → bytes
|
|
219
|
+
age: parseElapsed(elapsed),
|
|
220
|
+
startTime,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return new ProcessTree(processes, { platform: runtime.platform });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static fromWMIC(options = {}) {
|
|
228
|
+
const runtime = runtimeOptions({ ...options, platform: 'win32' });
|
|
229
|
+
const result = readWMICProcesses(runtime);
|
|
230
|
+
return new ProcessTree(result.processes, {
|
|
231
|
+
platform: runtime.platform,
|
|
232
|
+
warnings: result.warnings,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static fromCIM(options = {}) {
|
|
237
|
+
const runtime = runtimeOptions({ ...options, platform: 'win32' });
|
|
238
|
+
const result = readCIMProcesses(runtime);
|
|
239
|
+
return new ProcessTree(result.processes, {
|
|
240
|
+
platform: runtime.platform,
|
|
241
|
+
warnings: result.warnings,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static fromWindows(options = {}) {
|
|
246
|
+
const runtime = runtimeOptions({ ...options, platform: 'win32' });
|
|
247
|
+
const wmic = readWMICProcesses(runtime);
|
|
248
|
+
if (wmic.processes.length > 0) {
|
|
249
|
+
return new ProcessTree(wmic.processes, { platform: runtime.platform });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const warnings = [...wmic.warnings];
|
|
253
|
+
const cim = readCIMProcesses(runtime);
|
|
254
|
+
if (cim.processes.length > 0) {
|
|
255
|
+
return new ProcessTree(cim.processes, {
|
|
256
|
+
platform: runtime.platform,
|
|
257
|
+
warnings: warnings.concat(cim.warnings),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
warnings.push(...cim.warnings);
|
|
262
|
+
return new ProcessTree([], {
|
|
263
|
+
platform: runtime.platform,
|
|
264
|
+
warnings,
|
|
265
|
+
errors: [{
|
|
266
|
+
code: 'process-enumeration-failed',
|
|
267
|
+
platform: 'win32',
|
|
268
|
+
providers: ['cim', 'wmic'],
|
|
269
|
+
message: 'Windows process enumeration failed for all providers.',
|
|
270
|
+
}],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Platform-aware factory. Use this from scanner/orphan/whitelist.
|
|
276
|
+
* Calls fromWindows() on Windows, fromPS() everywhere else.
|
|
277
|
+
*
|
|
278
|
+
* @returns {ProcessTree}
|
|
279
|
+
*/
|
|
280
|
+
static build(options = {}) {
|
|
281
|
+
const runtime = runtimeOptions(options);
|
|
282
|
+
return runtime.platform === 'win32'
|
|
283
|
+
? ProcessTree.fromWindows(runtime)
|
|
284
|
+
: ProcessTree.fromPS(runtime);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function runtimeOptions(options = {}) {
|
|
289
|
+
return {
|
|
290
|
+
execSync: options.execSync || execSync,
|
|
291
|
+
platform: options.platform || platform,
|
|
292
|
+
currentPid: options.currentPid || process.pid,
|
|
293
|
+
now: options.now || Date.now(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function providerDiagnostic(provider, code, err) {
|
|
298
|
+
return {
|
|
299
|
+
code,
|
|
300
|
+
provider,
|
|
301
|
+
message: err instanceof Error ? err.message : String(err || ''),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parse ps elapsed time format: [[DD-]HH:]MM:SS
|
|
307
|
+
* Moved here from scanner.js to co-locate with ProcessTree.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} elapsed
|
|
310
|
+
* @returns {number} milliseconds
|
|
311
|
+
*/
|
|
312
|
+
function parseElapsed(elapsed) {
|
|
313
|
+
if (!elapsed) return 0;
|
|
314
|
+
|
|
315
|
+
let days = 0;
|
|
316
|
+
let rest = elapsed.trim();
|
|
317
|
+
|
|
318
|
+
const dayMatch = rest.match(/^(\d+)-(.+)$/);
|
|
319
|
+
if (dayMatch) {
|
|
320
|
+
days = parseInt(dayMatch[1], 10);
|
|
321
|
+
rest = dayMatch[2];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const parts = rest.split(':').map((p) => parseInt(p, 10));
|
|
325
|
+
|
|
326
|
+
let hours = 0, minutes = 0, seconds = 0;
|
|
327
|
+
if (parts.length === 3) {
|
|
328
|
+
[hours, minutes, seconds] = parts;
|
|
329
|
+
} else if (parts.length === 2) {
|
|
330
|
+
[minutes, seconds] = parts;
|
|
331
|
+
} else if (parts.length === 1) {
|
|
332
|
+
[seconds] = parts;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { ProcessTree, parseElapsed };
|
package/src/reporter.js
CHANGED
|
@@ -65,10 +65,54 @@ function truncate(str, maxLen = 80) {
|
|
|
65
65
|
return str.substring(0, maxLen - 3) + '...';
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function getDiagnostics(result) {
|
|
69
|
+
return {
|
|
70
|
+
warnings: Array.isArray(result?.warnings) ? result.warnings : [],
|
|
71
|
+
errors: Array.isArray(result?.errors) ? result.errors : [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatDiagnostic(item) {
|
|
76
|
+
if (typeof item === 'string') return item;
|
|
77
|
+
const code = item.code ? `${item.code}: ` : '';
|
|
78
|
+
const providers = Array.isArray(item.providers) && item.providers.length > 0
|
|
79
|
+
? ` (providers: ${item.providers.join(', ')})`
|
|
80
|
+
: '';
|
|
81
|
+
return `${code}${item.message || 'process scan diagnostic'}${providers}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function reportScanDiagnostics(result) {
|
|
85
|
+
const { warnings, errors } = getDiagnostics(result);
|
|
86
|
+
|
|
87
|
+
if (warnings.length > 0) {
|
|
88
|
+
console.log(c('yellow', ' Scan warnings:'));
|
|
89
|
+
for (const warning of warnings) {
|
|
90
|
+
console.log(` ${formatDiagnostic(warning)}`);
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
console.log(c('red', ' Process enumeration failed. Scan results are incomplete.'));
|
|
97
|
+
for (const error of errors) {
|
|
98
|
+
console.log(` ${formatDiagnostic(error)}`);
|
|
99
|
+
}
|
|
100
|
+
console.log();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
/**
|
|
69
108
|
* Report dry-run scan results.
|
|
70
109
|
*/
|
|
71
110
|
function reportDryRun(zombies) {
|
|
111
|
+
if (reportScanDiagnostics(zombies)) {
|
|
112
|
+
console.log(c('yellow', ' Cleanup skipped because zclean could not inspect the process list safely.\n'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
if (zombies.length === 0) {
|
|
73
117
|
console.log(c('green', ' No zombie processes found. System is clean.'));
|
|
74
118
|
return;
|
|
@@ -93,7 +137,7 @@ function reportDryRun(zombies) {
|
|
|
93
137
|
* Report kill results.
|
|
94
138
|
*/
|
|
95
139
|
function reportKill(results) {
|
|
96
|
-
const { killed, failed, skipped } = results;
|
|
140
|
+
const { killed, failed, skipped, warning } = results;
|
|
97
141
|
|
|
98
142
|
if (killed.length === 0 && failed.length === 0 && skipped.length === 0) {
|
|
99
143
|
console.log(c('green', ' No zombie processes to clean.'));
|
|
@@ -102,6 +146,11 @@ function reportKill(results) {
|
|
|
102
146
|
|
|
103
147
|
console.log();
|
|
104
148
|
|
|
149
|
+
if (warning) {
|
|
150
|
+
console.log(c('yellow', ` ${warning}`));
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
|
|
105
154
|
if (killed.length > 0) {
|
|
106
155
|
const totalMem = killed.reduce((sum, p) => sum + p.mem, 0);
|
|
107
156
|
console.log(c('green', ` Killed ${killed.length} zombie process${killed.length === 1 ? '' : 'es'}:`));
|
|
@@ -109,6 +158,13 @@ function reportKill(results) {
|
|
|
109
158
|
console.log(` ${c('green', 'KILLED')} PID ${String(p.pid).padStart(6)} ${p.name.padEnd(16)} ${formatBytes(p.mem).padStart(8)}`);
|
|
110
159
|
}
|
|
111
160
|
console.log(c('green', `\n Memory freed: ${formatBytes(totalMem)}`));
|
|
161
|
+
|
|
162
|
+
// Show cumulative stats if available
|
|
163
|
+
if (results.cumulative) {
|
|
164
|
+
const s = results.cumulative;
|
|
165
|
+
console.log(c('gray', ` This week: ${s.weekKilled} cleaned, ${formatBytes(s.weekMemFreed)} freed`));
|
|
166
|
+
console.log(c('gray', ` All time: ${s.totalKilled} cleaned, ${formatBytes(s.totalMemFreed)} freed`));
|
|
167
|
+
}
|
|
112
168
|
}
|
|
113
169
|
|
|
114
170
|
if (skipped.length > 0) {
|
|
@@ -135,7 +191,9 @@ function reportStatus(zombies, logs) {
|
|
|
135
191
|
console.log(bold('\n zclean status\n'));
|
|
136
192
|
|
|
137
193
|
// Current zombies
|
|
138
|
-
if (zombies
|
|
194
|
+
if (reportScanDiagnostics(zombies)) {
|
|
195
|
+
console.log(c('red', ' Current zombies: unknown'));
|
|
196
|
+
} else if (zombies.length === 0) {
|
|
139
197
|
console.log(c('green', ' Current zombies: 0'));
|
|
140
198
|
} else {
|
|
141
199
|
console.log(c('yellow', ` Current zombies: ${zombies.length}`));
|
|
@@ -212,6 +270,7 @@ module.exports = {
|
|
|
212
270
|
reportStatus,
|
|
213
271
|
reportLogs,
|
|
214
272
|
reportConfig,
|
|
273
|
+
reportScanDiagnostics,
|
|
215
274
|
formatBytes,
|
|
216
275
|
formatDuration,
|
|
217
276
|
C,
|