z-clean 0.1.1 → 0.3.1
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/installer/launchd.js
CHANGED
|
@@ -4,48 +4,58 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
+
const { LOCAL_BIN_HINT, resolveZcleanBin } = require('./bin-path');
|
|
7
8
|
|
|
8
9
|
const PLIST_NAME = 'com.zclean.hourly';
|
|
9
10
|
const PLIST_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
10
11
|
const PLIST_PATH = path.join(PLIST_DIR, `${PLIST_NAME}.plist`);
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Resolve the
|
|
14
|
-
*
|
|
14
|
+
* Resolve the active nvm node bin path, if nvm is installed.
|
|
15
|
+
* Returns the path or null.
|
|
15
16
|
*/
|
|
16
|
-
function
|
|
17
|
-
|
|
17
|
+
function resolveNvmNodeBin() {
|
|
18
|
+
const nvmDir = path.join(os.homedir(), '.nvm', 'versions', 'node');
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
20
|
+
if (!fs.existsSync(nvmDir)) return null;
|
|
21
|
+
// Use the currently running node's path if it's under .nvm
|
|
22
|
+
const nodeBin = process.execPath;
|
|
23
|
+
if (nodeBin.includes('.nvm')) {
|
|
24
|
+
return path.dirname(nodeBin);
|
|
25
|
+
}
|
|
26
|
+
// Fallback: find the default version directory
|
|
27
|
+
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith('v')).sort().reverse();
|
|
28
|
+
if (versions.length > 0) {
|
|
29
|
+
return path.join(nvmDir, versions[0], 'bin');
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
// Fallback to npx
|
|
36
|
-
return 'npx zclean';
|
|
34
|
+
return null;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
38
|
* Generate the launchd plist XML.
|
|
41
39
|
*/
|
|
42
40
|
function generatePlist(binPath) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
.concat(['--yes'])
|
|
46
|
-
.map((arg) => ` <string>${arg}</string>`)
|
|
41
|
+
const programArgs = [binPath, '--yes']
|
|
42
|
+
.map((arg) => ` <string>${escapeXml(arg)}</string>`)
|
|
47
43
|
.join('\n');
|
|
48
44
|
|
|
45
|
+
// Build PATH with nvm node bin if available
|
|
46
|
+
const pathParts = [
|
|
47
|
+
'/usr/local/bin',
|
|
48
|
+
'/usr/bin',
|
|
49
|
+
'/bin',
|
|
50
|
+
'/opt/homebrew/bin',
|
|
51
|
+
path.join(os.homedir(), '.local', 'bin'),
|
|
52
|
+
];
|
|
53
|
+
const nvmBin = resolveNvmNodeBin();
|
|
54
|
+
if (nvmBin) {
|
|
55
|
+
pathParts.push(nvmBin);
|
|
56
|
+
}
|
|
57
|
+
const envPath = pathParts.join(':');
|
|
58
|
+
|
|
49
59
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
60
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
51
61
|
<plist version="1.0">
|
|
@@ -73,7 +83,7 @@ ${programArgs}
|
|
|
73
83
|
<key>EnvironmentVariables</key>
|
|
74
84
|
<dict>
|
|
75
85
|
<key>PATH</key>
|
|
76
|
-
<string
|
|
86
|
+
<string>${envPath}</string>
|
|
77
87
|
</dict>
|
|
78
88
|
</dict>
|
|
79
89
|
</plist>
|
|
@@ -90,12 +100,16 @@ function installLaunchd() {
|
|
|
90
100
|
return { installed: false, message: 'launchd is macOS only.' };
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
const binPath = resolveZcleanBin();
|
|
104
|
+
if (!binPath) {
|
|
105
|
+
return { installed: false, message: `Local zclean executable not found. ${LOCAL_BIN_HINT}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
// Ensure LaunchAgents directory exists
|
|
94
109
|
if (!fs.existsSync(PLIST_DIR)) {
|
|
95
110
|
fs.mkdirSync(PLIST_DIR, { recursive: true });
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
const binPath = resolveZcleanBin();
|
|
99
113
|
const plist = generatePlist(binPath);
|
|
100
114
|
|
|
101
115
|
// Unload existing if present
|
|
@@ -163,4 +177,19 @@ function removeLaunchd() {
|
|
|
163
177
|
};
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
|
|
180
|
+
function escapeXml(value) {
|
|
181
|
+
return String(value)
|
|
182
|
+
.replace(/&/g, '&')
|
|
183
|
+
.replace(/</g, '<')
|
|
184
|
+
.replace(/>/g, '>')
|
|
185
|
+
.replace(/"/g, '"')
|
|
186
|
+
.replace(/'/g, ''');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
installLaunchd,
|
|
191
|
+
removeLaunchd,
|
|
192
|
+
generatePlist,
|
|
193
|
+
resolveZcleanBin,
|
|
194
|
+
PLIST_PATH,
|
|
195
|
+
};
|
package/src/installer/systemd.js
CHANGED
|
@@ -4,46 +4,24 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
+
const { LOCAL_BIN_HINT, quoteSystemdArg, resolveZcleanBin } = require('./bin-path');
|
|
7
8
|
|
|
8
9
|
const SYSTEMD_USER_DIR = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
9
10
|
const SERVICE_NAME = 'zclean';
|
|
10
11
|
const SERVICE_PATH = path.join(SYSTEMD_USER_DIR, `${SERVICE_NAME}.service`);
|
|
11
12
|
const TIMER_PATH = path.join(SYSTEMD_USER_DIR, `${SERVICE_NAME}.timer`);
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Resolve the full path to the zclean binary.
|
|
15
|
-
*/
|
|
16
|
-
function resolveZcleanBin() {
|
|
17
|
-
try {
|
|
18
|
-
const npmBin = execSync('npm bin -g', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
19
|
-
const globalPath = path.join(npmBin, 'zclean');
|
|
20
|
-
if (fs.existsSync(globalPath)) return globalPath;
|
|
21
|
-
} catch { /* ignore */ }
|
|
22
|
-
|
|
23
|
-
const candidates = [
|
|
24
|
-
path.join(os.homedir(), '.local', 'bin', 'zclean'),
|
|
25
|
-
'/usr/local/bin/zclean',
|
|
26
|
-
path.join(os.homedir(), '.local', 'share', 'npm', 'bin', 'zclean'),
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
for (const candidate of candidates) {
|
|
30
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return path.join(os.homedir(), '.local', 'bin', 'zclean');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
14
|
/**
|
|
37
15
|
* Generate the systemd service unit file.
|
|
38
16
|
*/
|
|
39
17
|
function generateService(binPath) {
|
|
40
18
|
return `[Unit]
|
|
41
19
|
Description=zclean - AI coding tool zombie process cleaner
|
|
42
|
-
Documentation=https://github.com/
|
|
20
|
+
Documentation=https://github.com/TheStack-ai/zclean
|
|
43
21
|
|
|
44
22
|
[Service]
|
|
45
23
|
Type=oneshot
|
|
46
|
-
ExecStart=${binPath} --yes
|
|
24
|
+
ExecStart=${quoteSystemdArg(binPath)} --yes
|
|
47
25
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin:%h/.local/bin
|
|
48
26
|
StandardOutput=append:%h/.zclean/systemd.log
|
|
49
27
|
StandardError=append:%h/.zclean/systemd.log
|
|
@@ -56,7 +34,7 @@ StandardError=append:%h/.zclean/systemd.log
|
|
|
56
34
|
function generateTimer() {
|
|
57
35
|
return `[Unit]
|
|
58
36
|
Description=Run zclean hourly
|
|
59
|
-
Documentation=https://github.com/
|
|
37
|
+
Documentation=https://github.com/TheStack-ai/zclean
|
|
60
38
|
|
|
61
39
|
[Timer]
|
|
62
40
|
OnCalendar=hourly
|
|
@@ -78,13 +56,16 @@ function installSystemd() {
|
|
|
78
56
|
return { installed: false, message: 'systemd is Linux only.' };
|
|
79
57
|
}
|
|
80
58
|
|
|
59
|
+
const binPath = resolveZcleanBin();
|
|
60
|
+
if (!binPath) {
|
|
61
|
+
return { installed: false, message: `Local zclean executable not found. ${LOCAL_BIN_HINT}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
81
64
|
// Ensure systemd user directory exists
|
|
82
65
|
if (!fs.existsSync(SYSTEMD_USER_DIR)) {
|
|
83
66
|
fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
|
84
67
|
}
|
|
85
68
|
|
|
86
|
-
const binPath = resolveZcleanBin();
|
|
87
|
-
|
|
88
69
|
// Write service and timer files
|
|
89
70
|
fs.writeFileSync(SERVICE_PATH, generateService(binPath), 'utf-8');
|
|
90
71
|
fs.writeFileSync(TIMER_PATH, generateTimer(), 'utf-8');
|
|
@@ -159,4 +140,12 @@ function removeSystemd() {
|
|
|
159
140
|
};
|
|
160
141
|
}
|
|
161
142
|
|
|
162
|
-
module.exports = {
|
|
143
|
+
module.exports = {
|
|
144
|
+
installSystemd,
|
|
145
|
+
removeSystemd,
|
|
146
|
+
generateService,
|
|
147
|
+
generateTimer,
|
|
148
|
+
resolveZcleanBin,
|
|
149
|
+
SERVICE_PATH,
|
|
150
|
+
TIMER_PATH,
|
|
151
|
+
};
|
|
@@ -1,31 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
5
4
|
const os = require('os');
|
|
6
|
-
const
|
|
5
|
+
const { LOCAL_BIN_HINT, resolveZcleanBin } = require('./bin-path');
|
|
7
6
|
|
|
8
7
|
const TASK_NAME = 'zclean-hourly';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const candidate2 = path.join(npmPrefix, 'node_modules', '.bin', 'zclean.cmd');
|
|
20
|
-
if (fs.existsSync(candidate2)) return candidate2;
|
|
21
|
-
} catch { /* ignore */ }
|
|
22
|
-
|
|
23
|
-
// Check AppData local
|
|
24
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
25
|
-
const npmGlobal = path.join(appData, 'npm', 'zclean.cmd');
|
|
26
|
-
if (fs.existsSync(npmGlobal)) return npmGlobal;
|
|
9
|
+
function buildCreateTaskArgs(binPath) {
|
|
10
|
+
return [
|
|
11
|
+
'/create',
|
|
12
|
+
'/TN', TASK_NAME,
|
|
13
|
+
'/SC', 'HOURLY',
|
|
14
|
+
'/TR', formatTaskRunCommand(binPath),
|
|
15
|
+
'/F',
|
|
16
|
+
];
|
|
17
|
+
}
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
function formatTaskRunCommand(binPath) {
|
|
20
|
+
const text = String(binPath);
|
|
21
|
+
const command = /\s/.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
|
22
|
+
return `${command} --yes`;
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
/**
|
|
@@ -41,22 +35,14 @@ function installTaskScheduler() {
|
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
const binPath = resolveZcleanBin();
|
|
38
|
+
if (!binPath) {
|
|
39
|
+
return { installed: false, message: `Local zclean executable not found. ${LOCAL_BIN_HINT}` };
|
|
40
|
+
}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
// /SC HOURLY — run every hour
|
|
47
|
-
// /TN — task name
|
|
48
|
-
// /TR — task to run
|
|
49
|
-
// /F — force overwrite if exists
|
|
50
|
-
const command = [
|
|
51
|
-
'schtasks', '/create',
|
|
52
|
-
'/TN', `"${TASK_NAME}"`,
|
|
53
|
-
'/SC', 'HOURLY',
|
|
54
|
-
'/TR', `"${binPath} --yes"`,
|
|
55
|
-
'/F',
|
|
56
|
-
].join(' ');
|
|
42
|
+
const args = buildCreateTaskArgs(binPath);
|
|
57
43
|
|
|
58
44
|
try {
|
|
59
|
-
|
|
45
|
+
execFileSync('schtasks', args, { encoding: 'utf-8', timeout: 10000 });
|
|
60
46
|
return {
|
|
61
47
|
installed: true,
|
|
62
48
|
message: `Task Scheduler task created: ${TASK_NAME} (hourly)`,
|
|
@@ -80,7 +66,7 @@ function removeTaskScheduler() {
|
|
|
80
66
|
}
|
|
81
67
|
|
|
82
68
|
try {
|
|
83
|
-
|
|
69
|
+
execFileSync('schtasks', ['/delete', '/TN', TASK_NAME, '/F'], {
|
|
84
70
|
encoding: 'utf-8',
|
|
85
71
|
timeout: 10000,
|
|
86
72
|
});
|
|
@@ -99,4 +85,11 @@ function removeTaskScheduler() {
|
|
|
99
85
|
}
|
|
100
86
|
}
|
|
101
87
|
|
|
102
|
-
module.exports = {
|
|
88
|
+
module.exports = {
|
|
89
|
+
installTaskScheduler,
|
|
90
|
+
removeTaskScheduler,
|
|
91
|
+
resolveZcleanBin,
|
|
92
|
+
buildCreateTaskArgs,
|
|
93
|
+
formatTaskRunCommand,
|
|
94
|
+
TASK_NAME,
|
|
95
|
+
};
|
package/src/killer.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { appendLog } = require('./config');
|
|
6
|
+
const { normalizePid, readWindowsProcess, windowsProcessExists } = require('./windows-processes');
|
|
6
7
|
|
|
7
8
|
const platform = os.platform();
|
|
8
9
|
|
|
@@ -24,9 +25,17 @@ const platform = os.platform();
|
|
|
24
25
|
*/
|
|
25
26
|
function killZombies(zombies, config) {
|
|
26
27
|
const timeout = (config.sigterm_timeout || 10) * 1000;
|
|
27
|
-
const
|
|
28
|
+
const limit = config.maxKillBatch || 20;
|
|
29
|
+
const results = { killed: [], failed: [], skipped: [], warning: null };
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
// Rate limit: only kill up to maxKillBatch processes
|
|
32
|
+
let toKill = zombies;
|
|
33
|
+
if (zombies.length > limit) {
|
|
34
|
+
toKill = zombies.slice(0, limit);
|
|
35
|
+
results.warning = `Found ${zombies.length} zombies, killing ${limit}. Run again for remaining.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const proc of toKill) {
|
|
30
39
|
// Re-verify before killing
|
|
31
40
|
const verification = verifyProcess(proc);
|
|
32
41
|
if (!verification.valid) {
|
|
@@ -73,6 +82,7 @@ function killZombies(zombies, config) {
|
|
|
73
82
|
killed: results.killed.length,
|
|
74
83
|
failed: results.failed.length,
|
|
75
84
|
skipped: results.skipped.length,
|
|
85
|
+
limited: zombies.length > limit,
|
|
76
86
|
totalMemFreed: results.killed.reduce((sum, p) => sum + p.mem, 0),
|
|
77
87
|
});
|
|
78
88
|
|
|
@@ -83,17 +93,18 @@ function killZombies(zombies, config) {
|
|
|
83
93
|
* Re-verify a process before killing it.
|
|
84
94
|
* Ensures we don't kill a recycled PID or wrong process.
|
|
85
95
|
*/
|
|
86
|
-
function verifyProcess(proc) {
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
function verifyProcess(proc, options = {}) {
|
|
97
|
+
const runtime = runtimeOptions(options);
|
|
98
|
+
if (runtime.platform === 'win32') {
|
|
99
|
+
return verifyProcessWindows(proc, runtime);
|
|
89
100
|
}
|
|
90
|
-
return verifyProcessUnix(proc);
|
|
101
|
+
return verifyProcessUnix(proc, runtime);
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
function verifyProcessUnix(proc) {
|
|
104
|
+
function verifyProcessUnix(proc, runtime = runtimeOptions()) {
|
|
94
105
|
try {
|
|
95
106
|
// Check if PID still exists and get its command line
|
|
96
|
-
const cmd = execSync(`ps -o command= -p ${proc.pid}`, {
|
|
107
|
+
const cmd = runtime.execSync(`ps -o command= -p ${proc.pid}`, {
|
|
97
108
|
encoding: 'utf-8',
|
|
98
109
|
timeout: 5000,
|
|
99
110
|
}).trim();
|
|
@@ -113,7 +124,7 @@ function verifyProcessUnix(proc) {
|
|
|
113
124
|
// Verify start time if available
|
|
114
125
|
if (proc.startTime) {
|
|
115
126
|
try {
|
|
116
|
-
const lstart = execSync(`ps -o lstart= -p ${proc.pid}`, {
|
|
127
|
+
const lstart = runtime.execSync(`ps -o lstart= -p ${proc.pid}`, {
|
|
117
128
|
encoding: 'utf-8',
|
|
118
129
|
timeout: 5000,
|
|
119
130
|
}).trim();
|
|
@@ -132,30 +143,29 @@ function verifyProcessUnix(proc) {
|
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
function verifyProcessWindows(proc) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
).trim();
|
|
146
|
+
function verifyProcessWindows(proc, runtime = runtimeOptions({ platform: 'win32' })) {
|
|
147
|
+
const current = readWindowsProcess(proc.pid, runtime);
|
|
148
|
+
if (!current) {
|
|
149
|
+
return { valid: false, reason: 'process-gone' };
|
|
150
|
+
}
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
152
|
+
const currentCmd = String(current.cmd || '').trim();
|
|
153
|
+
const scanPrefix = proc.cmd.substring(0, 50);
|
|
154
|
+
const currentPrefix = currentCmd.substring(0, 50);
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
if (scanPrefix !== currentPrefix) {
|
|
157
|
+
return { valid: false, reason: 'cmd-mismatch' };
|
|
158
|
+
}
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
if (proc.startTime && current.startTime && current.startTime !== proc.startTime) {
|
|
161
|
+
return { valid: false, reason: 'start-time-mismatch' };
|
|
162
|
+
}
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return { valid: false, reason: 'process-gone' };
|
|
164
|
+
if (proc.startTime && !current.startTime) {
|
|
165
|
+
return { valid: false, reason: 'start-time-unverified' };
|
|
158
166
|
}
|
|
167
|
+
|
|
168
|
+
return { valid: true, reason: 'verified' };
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
/**
|
|
@@ -164,14 +174,15 @@ function verifyProcessWindows(proc) {
|
|
|
164
174
|
* macOS/Linux: SIGTERM → wait → SIGKILL
|
|
165
175
|
* Windows: taskkill → wait → taskkill /F
|
|
166
176
|
*/
|
|
167
|
-
function killProcess(pid, timeoutMs) {
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
function killProcess(pid, timeoutMs, options = {}) {
|
|
178
|
+
const runtime = runtimeOptions(options);
|
|
179
|
+
if (runtime.platform === 'win32') {
|
|
180
|
+
return killProcessWindows(pid, timeoutMs, runtime);
|
|
170
181
|
}
|
|
171
|
-
return killProcessUnix(pid, timeoutMs);
|
|
182
|
+
return killProcessUnix(pid, timeoutMs, runtime);
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
function killProcessUnix(pid, timeoutMs) {
|
|
185
|
+
function killProcessUnix(pid, timeoutMs, runtime = runtimeOptions()) {
|
|
175
186
|
try {
|
|
176
187
|
// Send SIGTERM
|
|
177
188
|
process.kill(pid, 'SIGTERM');
|
|
@@ -189,11 +200,8 @@ function killProcessUnix(pid, timeoutMs) {
|
|
|
189
200
|
try {
|
|
190
201
|
// process.kill(pid, 0) throws if process doesn't exist
|
|
191
202
|
process.kill(pid, 0);
|
|
192
|
-
// Still alive —
|
|
193
|
-
|
|
194
|
-
while (Date.now() < waitUntil) {
|
|
195
|
-
// Spin wait — we can't use setTimeout synchronously
|
|
196
|
-
}
|
|
203
|
+
// Still alive — blocking sleep to avoid CPU spin
|
|
204
|
+
try { runtime.execSync('sleep 0.5', { timeout: 2000 }); } catch { /* ignore */ }
|
|
197
205
|
} catch {
|
|
198
206
|
// Process is gone
|
|
199
207
|
return { success: true, method: 'sigterm' };
|
|
@@ -212,37 +220,45 @@ function killProcessUnix(pid, timeoutMs) {
|
|
|
212
220
|
}
|
|
213
221
|
}
|
|
214
222
|
|
|
215
|
-
function killProcessWindows(pid, timeoutMs) {
|
|
223
|
+
function killProcessWindows(pid, timeoutMs, runtime = runtimeOptions({ platform: 'win32' })) {
|
|
224
|
+
const safePid = normalizePid(pid);
|
|
225
|
+
if (!safePid) {
|
|
226
|
+
return { success: false, error: `Invalid PID: ${pid}` };
|
|
227
|
+
}
|
|
228
|
+
|
|
216
229
|
try {
|
|
217
230
|
// Graceful kill
|
|
218
|
-
execSync(`taskkill /PID ${
|
|
231
|
+
runtime.execSync(`taskkill /PID ${safePid}`, { encoding: 'utf-8', timeout: 5000 });
|
|
219
232
|
} catch {
|
|
220
233
|
// Might fail — try force kill directly
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
// Wait
|
|
224
|
-
const deadline =
|
|
225
|
-
while (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
encoding: 'utf-8',
|
|
229
|
-
timeout: 3000,
|
|
230
|
-
});
|
|
231
|
-
// Still alive — wait
|
|
232
|
-
const waitUntil = Date.now() + 500;
|
|
233
|
-
while (Date.now() < waitUntil) { /* spin */ }
|
|
234
|
-
} catch {
|
|
237
|
+
const deadline = runtime.now() + timeoutMs;
|
|
238
|
+
while (runtime.now() < deadline) {
|
|
239
|
+
const exists = windowsProcessExists(safePid, runtime);
|
|
240
|
+
if (exists === false) {
|
|
235
241
|
return { success: true, method: 'taskkill' };
|
|
236
242
|
}
|
|
243
|
+
if (exists === null) break;
|
|
244
|
+
try { runtime.execSync('timeout /T 1 /NOBREAK >nul', { timeout: 3000 }); } catch { /* ignore */ }
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
// Force kill
|
|
240
248
|
try {
|
|
241
|
-
execSync(`taskkill /F /PID ${
|
|
249
|
+
runtime.execSync(`taskkill /F /PID ${safePid}`, { encoding: 'utf-8', timeout: 5000 });
|
|
242
250
|
return { success: true, method: 'taskkill-force' };
|
|
243
251
|
} catch (err) {
|
|
244
252
|
return { success: false, error: `Force kill failed: ${err.message}` };
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
|
|
256
|
+
function runtimeOptions(options = {}) {
|
|
257
|
+
return {
|
|
258
|
+
execSync: options.execSync || execSync,
|
|
259
|
+
platform: options.platform || platform,
|
|
260
|
+
now: typeof options.now === 'function' ? options.now : () => Date.now(),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
module.exports = { killZombies, verifyProcess, killProcess };
|