portdog 1.0.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/README.md +43 -0
- package/bin/portdog.js +41 -0
- package/package.json +21 -0
- package/src/commands.js +195 -0
- package/src/portUtils.js +229 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# portdog
|
|
2
|
+
|
|
3
|
+
Your port watchdog. Stop googling `lsof -i :3000`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g portdog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# See all ports in use
|
|
15
|
+
portdog list
|
|
16
|
+
|
|
17
|
+
# Kill whatever is on port 3000
|
|
18
|
+
portdog kill 3000
|
|
19
|
+
|
|
20
|
+
# Force kill (SIGKILL)
|
|
21
|
+
portdog kill 3000 --force
|
|
22
|
+
|
|
23
|
+
# Check if ports are free
|
|
24
|
+
portdog scan 3000 8080 5432
|
|
25
|
+
|
|
26
|
+
# Get a free port
|
|
27
|
+
portdog free
|
|
28
|
+
|
|
29
|
+
# Get 5 free ports
|
|
30
|
+
portdog free -n 5
|
|
31
|
+
|
|
32
|
+
# Detailed info about what's on a port
|
|
33
|
+
portdog who 3000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Aliases
|
|
37
|
+
|
|
38
|
+
- `portdog ls` = `portdog list`
|
|
39
|
+
- `portdog k <port>` = `portdog kill <port>`
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/bin/portdog.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { listPorts, killPort, scanPorts, freePort, whoisPort } = require('../src/commands');
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('portdog')
|
|
8
|
+
.description('🐕 Your port watchdog. Stop googling lsof.')
|
|
9
|
+
.version('1.0.0');
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command('list')
|
|
13
|
+
.alias('ls')
|
|
14
|
+
.description('Show all ports in use with process info')
|
|
15
|
+
.option('-p, --port <port>', 'Filter by specific port')
|
|
16
|
+
.action(listPorts);
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('kill <port>')
|
|
20
|
+
.alias('k')
|
|
21
|
+
.description('Kill whatever is running on a port')
|
|
22
|
+
.option('-f, --force', 'Force kill (SIGKILL)', false)
|
|
23
|
+
.action(killPort);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('scan <ports...>')
|
|
27
|
+
.description('Check if ports are free (e.g. portdog scan 3000 8080 5432)')
|
|
28
|
+
.action(scanPorts);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('free')
|
|
32
|
+
.description('Find and print a random free port')
|
|
33
|
+
.option('-n, --count <number>', 'Number of free ports to find', '1')
|
|
34
|
+
.action(freePort);
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('who <port>')
|
|
38
|
+
.description('Detailed info about what is using a port')
|
|
39
|
+
.action(whoisPort);
|
|
40
|
+
|
|
41
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "portdog",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Your port watchdog. Stop googling lsof.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"portdog": "./bin/portdog.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["port", "kill", "process", "cli", "devtools", "lsof", "watchdog"],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^4.1.2",
|
|
18
|
+
"cli-table3": "^0.6.5",
|
|
19
|
+
"commander": "^14.0.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const Table = require('cli-table3');
|
|
3
|
+
const {
|
|
4
|
+
getProcessesOnPorts,
|
|
5
|
+
isPortFree,
|
|
6
|
+
findFreePorts,
|
|
7
|
+
killProcess,
|
|
8
|
+
getDetailedProcessInfo,
|
|
9
|
+
timeSince,
|
|
10
|
+
} = require('./portUtils');
|
|
11
|
+
|
|
12
|
+
async function listPorts(options) {
|
|
13
|
+
const processes = getProcessesOnPorts();
|
|
14
|
+
|
|
15
|
+
if (processes.length === 0) {
|
|
16
|
+
console.log(chalk.green('\n No ports in use. Your machine is squeaky clean.\n'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let filtered = processes;
|
|
21
|
+
if (options.port) {
|
|
22
|
+
const targetPort = parseInt(options.port, 10);
|
|
23
|
+
filtered = processes.filter((p) => p.port === targetPort);
|
|
24
|
+
if (filtered.length === 0) {
|
|
25
|
+
console.log(chalk.green(`\n Port ${targetPort} is free. Nothing there.\n`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Sort by port number
|
|
31
|
+
filtered.sort((a, b) => a.port - b.port);
|
|
32
|
+
|
|
33
|
+
const table = new Table({
|
|
34
|
+
head: [
|
|
35
|
+
chalk.cyan('PORT'),
|
|
36
|
+
chalk.cyan('PID'),
|
|
37
|
+
chalk.cyan('PROCESS'),
|
|
38
|
+
chalk.cyan('USER'),
|
|
39
|
+
chalk.cyan('MEMORY'),
|
|
40
|
+
chalk.cyan('RUNNING SINCE'),
|
|
41
|
+
],
|
|
42
|
+
style: { head: [], border: ['gray'] },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const p of filtered) {
|
|
46
|
+
const portColor = p.port < 1024 ? chalk.red : chalk.green;
|
|
47
|
+
table.push([
|
|
48
|
+
portColor(p.port),
|
|
49
|
+
chalk.yellow(p.pid),
|
|
50
|
+
chalk.white(p.command),
|
|
51
|
+
chalk.gray(p.user),
|
|
52
|
+
chalk.magenta(p.memory || '-'),
|
|
53
|
+
chalk.gray(timeSince(p.startTime) || '-'),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n ${chalk.bold(`${filtered.length} port(s) in use`)}\n`);
|
|
58
|
+
console.log(table.toString());
|
|
59
|
+
console.log(chalk.gray(`\n Kill with: ${chalk.white('portdog kill <port>')}\n`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function killPort(port, options) {
|
|
63
|
+
const portNum = parseInt(port, 10);
|
|
64
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
65
|
+
console.log(chalk.red(`\n Invalid port: ${port}\n`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const processes = getProcessesOnPorts().filter((p) => p.port === portNum);
|
|
70
|
+
|
|
71
|
+
if (processes.length === 0) {
|
|
72
|
+
console.log(chalk.green(`\n Port ${portNum} is already free. Nothing to kill.\n`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const p of processes) {
|
|
77
|
+
const method = options.force ? 'SIGKILL' : 'SIGTERM';
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.yellow(
|
|
80
|
+
`\n Killing ${chalk.white(p.command)} (PID ${p.pid}) on port ${portNum} with ${method}...`
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const success = killProcess(p.pid, options.force);
|
|
85
|
+
|
|
86
|
+
if (success) {
|
|
87
|
+
// Verify it's actually dead
|
|
88
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
89
|
+
const stillAlive = getProcessesOnPorts().some(
|
|
90
|
+
(proc) => proc.pid === p.pid && proc.port === portNum
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (stillAlive && !options.force) {
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.yellow(
|
|
96
|
+
` Process didn't die. Try: ${chalk.white(`portdog kill ${portNum} --force`)}`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
} else if (stillAlive) {
|
|
100
|
+
console.log(chalk.red(` Process refused to die even with SIGKILL. Check permissions.`));
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.green(` Done. Port ${portNum} is now free.\n`));
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.log(
|
|
106
|
+
chalk.red(` Failed to kill PID ${p.pid}. You might need sudo:\n`)
|
|
107
|
+
);
|
|
108
|
+
console.log(chalk.gray(` sudo portdog kill ${portNum}\n`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function scanPorts(ports) {
|
|
114
|
+
console.log('');
|
|
115
|
+
const results = await Promise.all(
|
|
116
|
+
ports.map(async (p) => {
|
|
117
|
+
const portNum = parseInt(p, 10);
|
|
118
|
+
if (isNaN(portNum)) return { port: p, status: 'invalid' };
|
|
119
|
+
const free = await isPortFree(portNum);
|
|
120
|
+
return { port: portNum, status: free ? 'free' : 'in-use' };
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
if (r.status === 'invalid') {
|
|
126
|
+
console.log(chalk.red(` ${r.port} invalid port number`));
|
|
127
|
+
} else if (r.status === 'free') {
|
|
128
|
+
console.log(chalk.green(` :${r.port} free`));
|
|
129
|
+
} else {
|
|
130
|
+
const proc = getProcessesOnPorts().find((p) => p.port === r.port);
|
|
131
|
+
const info = proc ? chalk.gray(` (${proc.command}, PID ${proc.pid})`) : '';
|
|
132
|
+
console.log(chalk.red(` :${r.port} in use${info}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function freePort(options) {
|
|
139
|
+
const count = parseInt(options.count, 10) || 1;
|
|
140
|
+
const ports = await findFreePorts(count);
|
|
141
|
+
|
|
142
|
+
if (count === 1) {
|
|
143
|
+
console.log(`\n ${chalk.green(ports[0])}\n`);
|
|
144
|
+
} else {
|
|
145
|
+
console.log('');
|
|
146
|
+
for (const p of ports) {
|
|
147
|
+
console.log(` ${chalk.green(p)}`);
|
|
148
|
+
}
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function whoisPort(port) {
|
|
154
|
+
const portNum = parseInt(port, 10);
|
|
155
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
156
|
+
console.log(chalk.red(`\n Invalid port: ${port}\n`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const processes = getProcessesOnPorts().filter((p) => p.port === portNum);
|
|
161
|
+
|
|
162
|
+
if (processes.length === 0) {
|
|
163
|
+
console.log(chalk.green(`\n Port ${portNum} is free. Nobody home.\n`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const p of processes) {
|
|
168
|
+
const details = getDetailedProcessInfo(p.pid);
|
|
169
|
+
|
|
170
|
+
console.log(`\n ${chalk.bold.cyan(`Port ${portNum}`)}`);
|
|
171
|
+
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
172
|
+
console.log(` ${chalk.gray('Process:')} ${chalk.white(p.command)}`);
|
|
173
|
+
console.log(` ${chalk.gray('PID:')} ${chalk.yellow(p.pid)}`);
|
|
174
|
+
console.log(` ${chalk.gray('User:')} ${p.user}`);
|
|
175
|
+
console.log(` ${chalk.gray('Memory:')} ${chalk.magenta(p.memory || '-')}`);
|
|
176
|
+
console.log(` ${chalk.gray('Started:')} ${p.startTime || '-'} ${chalk.gray(`(${timeSince(p.startTime)})`)}`);
|
|
177
|
+
|
|
178
|
+
if (details) {
|
|
179
|
+
console.log(` ${chalk.gray('Full cmd:')} ${chalk.dim(details.fullCommand)}`);
|
|
180
|
+
console.log(` ${chalk.gray('Open files:')} ${details.openFiles}`);
|
|
181
|
+
console.log(
|
|
182
|
+
` ${chalk.gray('Children:')} ${details.childCount}${
|
|
183
|
+
details.childPids.length > 0
|
|
184
|
+
? chalk.gray(` (PIDs: ${details.childPids.join(', ')})`)
|
|
185
|
+
: ''
|
|
186
|
+
}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
191
|
+
console.log(chalk.gray(` Kill it: ${chalk.white(`portdog kill ${portNum}`)}\n`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { listPorts, killPort, scanPorts, freePort, whoisPort };
|
package/src/portUtils.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
function getProcessesOnPorts() {
|
|
6
|
+
const platform = os.platform();
|
|
7
|
+
const results = [];
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
let output;
|
|
11
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
12
|
+
// Get listening TCP ports
|
|
13
|
+
output = execSync('lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null || true', {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
timeout: 5000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const lines = output.trim().split('\n');
|
|
19
|
+
if (lines.length <= 1) return results;
|
|
20
|
+
|
|
21
|
+
// Skip header
|
|
22
|
+
for (let i = 1; i < lines.length; i++) {
|
|
23
|
+
const parts = lines[i].split(/\s+/);
|
|
24
|
+
if (parts.length < 9) continue;
|
|
25
|
+
|
|
26
|
+
const command = parts[0];
|
|
27
|
+
const pid = parseInt(parts[1], 10);
|
|
28
|
+
const user = parts[2];
|
|
29
|
+
const nameField = parts[8]; // e.g. *:3000 or 127.0.0.1:8080
|
|
30
|
+
|
|
31
|
+
const portMatch = nameField.match(/:(\d+)$/);
|
|
32
|
+
if (!portMatch) continue;
|
|
33
|
+
|
|
34
|
+
const port = parseInt(portMatch[1], 10);
|
|
35
|
+
|
|
36
|
+
// Get process start time and memory
|
|
37
|
+
let startTime = '';
|
|
38
|
+
let memory = '';
|
|
39
|
+
try {
|
|
40
|
+
const psOut = execSync(`ps -p ${pid} -o lstart=,rss= 2>/dev/null || true`, {
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
timeout: 3000,
|
|
43
|
+
}).trim();
|
|
44
|
+
if (psOut) {
|
|
45
|
+
const rssMatch = psOut.match(/(\d+)\s*$/);
|
|
46
|
+
if (rssMatch) {
|
|
47
|
+
memory = formatMemory(parseInt(rssMatch[1], 10));
|
|
48
|
+
startTime = psOut.replace(/\s*\d+\s*$/, '').trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
results.push({ port, pid, command, user, startTime, memory });
|
|
54
|
+
}
|
|
55
|
+
} else if (platform === 'win32') {
|
|
56
|
+
output = execSync('netstat -ano -p TCP | findstr LISTENING', {
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: 5000,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const lines = output.trim().split('\n');
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const parts = line.trim().split(/\s+/);
|
|
64
|
+
if (parts.length < 5) continue;
|
|
65
|
+
|
|
66
|
+
const localAddr = parts[1];
|
|
67
|
+
const pid = parseInt(parts[4], 10);
|
|
68
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
69
|
+
if (!portMatch) continue;
|
|
70
|
+
|
|
71
|
+
const port = parseInt(portMatch[1], 10);
|
|
72
|
+
|
|
73
|
+
let command = '';
|
|
74
|
+
try {
|
|
75
|
+
command = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH 2>nul`, {
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
timeout: 3000,
|
|
78
|
+
}).trim().split(',')[0]?.replace(/"/g, '') || '';
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
results.push({ port, pid, command, user: '', startTime: '', memory: '' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Silently handle - will return empty results
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Deduplicate by port+pid
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
return results.filter((r) => {
|
|
91
|
+
const key = `${r.port}:${r.pid}`;
|
|
92
|
+
if (seen.has(key)) return false;
|
|
93
|
+
seen.add(key);
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPortFree(port) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const server = net.createServer();
|
|
101
|
+
server.once('error', () => resolve(false));
|
|
102
|
+
server.once('listening', () => {
|
|
103
|
+
server.close(() => resolve(true));
|
|
104
|
+
});
|
|
105
|
+
server.listen(port, '0.0.0.0');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findFreePorts(count = 1) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const ports = [];
|
|
112
|
+
let found = 0;
|
|
113
|
+
|
|
114
|
+
function findNext() {
|
|
115
|
+
if (found >= count) return resolve(ports);
|
|
116
|
+
|
|
117
|
+
const server = net.createServer();
|
|
118
|
+
server.listen(0, '0.0.0.0', () => {
|
|
119
|
+
const port = server.address().port;
|
|
120
|
+
server.close(() => {
|
|
121
|
+
ports.push(port);
|
|
122
|
+
found++;
|
|
123
|
+
findNext();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
findNext();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function killProcess(pid, force = false) {
|
|
132
|
+
const platform = os.platform();
|
|
133
|
+
const signal = force ? 'SIGKILL' : 'SIGTERM';
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (platform === 'win32') {
|
|
137
|
+
execSync(`taskkill ${force ? '/F' : ''} /PID ${pid} 2>nul`, { encoding: 'utf-8' });
|
|
138
|
+
} else {
|
|
139
|
+
process.kill(pid, signal);
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getDetailedProcessInfo(pid) {
|
|
148
|
+
const platform = os.platform();
|
|
149
|
+
if (platform === 'win32') return null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const psOut = execSync(
|
|
153
|
+
`ps -p ${pid} -o pid=,ppid=,user=,lstart=,%cpu=,%mem=,rss=,command= 2>/dev/null`,
|
|
154
|
+
{ encoding: 'utf-8', timeout: 3000 }
|
|
155
|
+
).trim();
|
|
156
|
+
|
|
157
|
+
if (!psOut) return null;
|
|
158
|
+
|
|
159
|
+
// Also get the full command with args
|
|
160
|
+
const cmdOut = execSync(`ps -p ${pid} -o args= 2>/dev/null`, {
|
|
161
|
+
encoding: 'utf-8',
|
|
162
|
+
timeout: 3000,
|
|
163
|
+
}).trim();
|
|
164
|
+
|
|
165
|
+
// Get open file count
|
|
166
|
+
let openFiles = 0;
|
|
167
|
+
try {
|
|
168
|
+
const lsofOut = execSync(`lsof -p ${pid} 2>/dev/null | wc -l`, {
|
|
169
|
+
encoding: 'utf-8',
|
|
170
|
+
timeout: 3000,
|
|
171
|
+
}).trim();
|
|
172
|
+
openFiles = parseInt(lsofOut, 10) || 0;
|
|
173
|
+
} catch {}
|
|
174
|
+
|
|
175
|
+
// Get child process count
|
|
176
|
+
let children = [];
|
|
177
|
+
try {
|
|
178
|
+
const childOut = execSync(`pgrep -P ${pid} 2>/dev/null || true`, {
|
|
179
|
+
encoding: 'utf-8',
|
|
180
|
+
timeout: 3000,
|
|
181
|
+
}).trim();
|
|
182
|
+
if (childOut) children = childOut.split('\n').filter(Boolean);
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
fullCommand: cmdOut,
|
|
187
|
+
openFiles,
|
|
188
|
+
childCount: children.length,
|
|
189
|
+
childPids: children.map(Number),
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatMemory(rssKb) {
|
|
197
|
+
if (rssKb > 1024 * 1024) return `${(rssKb / (1024 * 1024)).toFixed(1)} GB`;
|
|
198
|
+
if (rssKb > 1024) return `${(rssKb / 1024).toFixed(1)} MB`;
|
|
199
|
+
return `${rssKb} KB`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function timeSince(dateStr) {
|
|
203
|
+
if (!dateStr) return '';
|
|
204
|
+
try {
|
|
205
|
+
const start = new Date(dateStr);
|
|
206
|
+
const now = new Date();
|
|
207
|
+
const diffMs = now - start;
|
|
208
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
209
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
210
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
211
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
212
|
+
|
|
213
|
+
if (diffDay > 0) return `${diffDay}d ${diffHr % 24}h ago`;
|
|
214
|
+
if (diffHr > 0) return `${diffHr}h ${diffMin % 60}m ago`;
|
|
215
|
+
if (diffMin > 0) return `${diffMin}m ago`;
|
|
216
|
+
return `${diffSec}s ago`;
|
|
217
|
+
} catch {
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
getProcessesOnPorts,
|
|
224
|
+
isPortFree,
|
|
225
|
+
findFreePorts,
|
|
226
|
+
killProcess,
|
|
227
|
+
getDetailedProcessInfo,
|
|
228
|
+
timeSince,
|
|
229
|
+
};
|