bgrun 3.12.2 → 3.12.3
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/dist/index.js +17 -13
- package/package.json +1 -1
- package/src/bgrun.test.ts +95 -62
- package/src/platform.ts +15 -8
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(exports_platform, {
|
|
|
25
25
|
reconcileProcessPids: () => reconcileProcessPids,
|
|
26
26
|
readFileTail: () => readFileTail,
|
|
27
27
|
psExec: () => psExec,
|
|
28
|
+
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
28
29
|
killProcessOnPort: () => killProcessOnPort,
|
|
29
30
|
isWindows: () => isWindows,
|
|
30
31
|
isProcessRunning: () => isProcessRunning,
|
|
@@ -457,6 +458,17 @@ async function getProcessBatchResources(pids) {
|
|
|
457
458
|
return resourceMap;
|
|
458
459
|
}) ?? new Map;
|
|
459
460
|
}
|
|
461
|
+
function parseUnixListeningPorts(output) {
|
|
462
|
+
const ports = new Set;
|
|
463
|
+
for (const line of output.split(`
|
|
464
|
+
`)) {
|
|
465
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
466
|
+
if (portMatch) {
|
|
467
|
+
ports.add(parseInt(portMatch[1]));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return Array.from(ports);
|
|
471
|
+
}
|
|
460
472
|
async function getProcessPorts(pid) {
|
|
461
473
|
try {
|
|
462
474
|
if (isWindows()) {
|
|
@@ -473,29 +485,21 @@ async function getProcessPorts(pid) {
|
|
|
473
485
|
} else {
|
|
474
486
|
try {
|
|
475
487
|
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
476
|
-
const
|
|
488
|
+
const ports = new Set;
|
|
477
489
|
for (const line of result2.split(`
|
|
478
490
|
`)) {
|
|
479
491
|
if (line.includes(`pid=${pid}`)) {
|
|
480
492
|
const portMatch = line.match(/:(\d+)\s/);
|
|
481
493
|
if (portMatch) {
|
|
482
|
-
|
|
494
|
+
ports.add(parseInt(portMatch[1]));
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
497
|
}
|
|
486
|
-
if (
|
|
487
|
-
return Array.from(
|
|
498
|
+
if (ports.size > 0)
|
|
499
|
+
return Array.from(ports);
|
|
488
500
|
} catch {}
|
|
489
501
|
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
490
|
-
|
|
491
|
-
for (const line of result.split(`
|
|
492
|
-
`)) {
|
|
493
|
-
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
494
|
-
if (portMatch) {
|
|
495
|
-
ports.add(parseInt(portMatch[1]));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return Array.from(ports);
|
|
502
|
+
return parseUnixListeningPorts(result);
|
|
499
503
|
}
|
|
500
504
|
} catch {
|
|
501
505
|
return [];
|
package/package.json
CHANGED
package/src/bgrun.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
10
10
|
import { parseEnvString, calculateRuntime } from './utils'
|
|
11
11
|
import { stripAnsi, truncateString, truncatePath } from './table'
|
|
12
12
|
import { detectPackageManager, formatDeployToolError } from './deploy'
|
|
13
|
-
import { isProcessRunning } from './platform'
|
|
13
|
+
import { isProcessRunning, parseUnixListeningPorts } from './platform'
|
|
14
14
|
import { mkdirSync, rmSync } from 'fs'
|
|
15
15
|
|
|
16
16
|
// Use a test-specific database to avoid polluting real data
|
|
@@ -141,6 +141,39 @@ describe('isProcessRunning', () => {
|
|
|
141
141
|
})
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
describe('parseUnixListeningPorts', () => {
|
|
145
|
+
test('extracts only LISTEN ports from lsof output', () => {
|
|
146
|
+
const output = [
|
|
147
|
+
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
148
|
+
'bun 12345 root 21u IPv4 123456 0t0 TCP *:3400 (LISTEN)',
|
|
149
|
+
'bun 12345 root 22u IPv4 123457 0t0 TCP 127.0.0.1:9222 (LISTEN)',
|
|
150
|
+
].join('\n')
|
|
151
|
+
|
|
152
|
+
expect(parseUnixListeningPorts(output)).toEqual([3400, 9222])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('ignores non-LISTEN sockets from broad lsof output', () => {
|
|
156
|
+
const output = [
|
|
157
|
+
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
158
|
+
'bun 12345 root 18u IPv4 111111 0t0 TCP 127.0.0.1:49440->127.0.0.1:3000 (ESTABLISHED)',
|
|
159
|
+
'bun 12345 root 19u IPv4 111112 0t0 TCP 127.0.0.1:49441->127.0.0.1:3737 (ESTABLISHED)',
|
|
160
|
+
'bun 12345 root 20u IPv4 111113 0t0 TCP *:3400 (LISTEN)',
|
|
161
|
+
].join('\n')
|
|
162
|
+
|
|
163
|
+
expect(parseUnixListeningPorts(output)).toEqual([3400])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('returns empty array for no-port worker output', () => {
|
|
167
|
+
const output = [
|
|
168
|
+
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
169
|
+
'bun 12345 root 18u unix 0xffff 0t0 /tmp/bun.sock',
|
|
170
|
+
'bun 12345 root 19u IPv4 111111 0t0 TCP 127.0.0.1:49440->127.0.0.1:3000 (ESTABLISHED)',
|
|
171
|
+
].join('\n')
|
|
172
|
+
|
|
173
|
+
expect(parseUnixListeningPorts(output)).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
144
177
|
// ─── detectPackageManager ───────────────────────────────
|
|
145
178
|
|
|
146
179
|
describe('formatDeployToolError', () => {
|
|
@@ -217,64 +250,64 @@ describe('detectPackageManager', () => {
|
|
|
217
250
|
}
|
|
218
251
|
})
|
|
219
252
|
})
|
|
220
|
-
|
|
221
|
-
// ─── Dependencies ───────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
describe('addDependency', () => {
|
|
224
|
-
test('adds a valid dependency', () => {
|
|
225
|
-
removeAllDependencies('web-server');
|
|
226
|
-
removeAllDependencies('database');
|
|
227
|
-
const ok = addDependency('web-server', 'database');
|
|
228
|
-
expect(ok).toBe(true);
|
|
229
|
-
expect(getDependencies('web-server')).toContain('database');
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('prevents self-dependency', () => {
|
|
233
|
-
expect(addDependency('api', 'api')).toBe(false);
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
test('prevents duplicate dependency', () => {
|
|
237
|
-
removeAllDependencies('app');
|
|
238
|
-
addDependency('app', 'db');
|
|
239
|
-
expect(addDependency('app', 'db')).toBe(false);
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test('prevents circular dependency', () => {
|
|
243
|
-
removeAllDependencies('a');
|
|
244
|
-
removeAllDependencies('b');
|
|
245
|
-
removeAllDependencies('c');
|
|
246
|
-
addDependency('a', 'b');
|
|
247
|
-
addDependency('b', 'c');
|
|
248
|
-
// c -> a would create a cycle
|
|
249
|
-
expect(addDependency('c', 'a')).toBe(false);
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
describe('getDependencyGraph', () => {
|
|
254
|
-
test('returns full graph', () => {
|
|
255
|
-
removeAllDependencies('svc-a');
|
|
256
|
-
removeAllDependencies('svc-b');
|
|
257
|
-
addDependency('svc-a', 'svc-b');
|
|
258
|
-
const graph = getDependencyGraph();
|
|
259
|
-
expect(graph['svc-a']).toContain('svc-b');
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
describe('getDependents', () => {
|
|
264
|
-
test('finds processes that depend on a target', () => {
|
|
265
|
-
removeAllDependencies('frontend');
|
|
266
|
-
removeAllDependencies('backend');
|
|
267
|
-
addDependency('frontend', 'backend');
|
|
268
|
-
expect(getDependents('backend')).toContain('frontend');
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
describe('removeDependency', () => {
|
|
273
|
-
test('removes an existing dependency', () => {
|
|
274
|
-
removeAllDependencies('x');
|
|
275
|
-
addDependency('x', 'y');
|
|
276
|
-
expect(getDependencies('x')).toContain('y');
|
|
277
|
-
removeDependency('x', 'y');
|
|
278
|
-
expect(getDependencies('x')).not.toContain('y');
|
|
279
|
-
})
|
|
280
|
-
})
|
|
253
|
+
|
|
254
|
+
// ─── Dependencies ───────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe('addDependency', () => {
|
|
257
|
+
test('adds a valid dependency', () => {
|
|
258
|
+
removeAllDependencies('web-server');
|
|
259
|
+
removeAllDependencies('database');
|
|
260
|
+
const ok = addDependency('web-server', 'database');
|
|
261
|
+
expect(ok).toBe(true);
|
|
262
|
+
expect(getDependencies('web-server')).toContain('database');
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('prevents self-dependency', () => {
|
|
266
|
+
expect(addDependency('api', 'api')).toBe(false);
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('prevents duplicate dependency', () => {
|
|
270
|
+
removeAllDependencies('app');
|
|
271
|
+
addDependency('app', 'db');
|
|
272
|
+
expect(addDependency('app', 'db')).toBe(false);
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('prevents circular dependency', () => {
|
|
276
|
+
removeAllDependencies('a');
|
|
277
|
+
removeAllDependencies('b');
|
|
278
|
+
removeAllDependencies('c');
|
|
279
|
+
addDependency('a', 'b');
|
|
280
|
+
addDependency('b', 'c');
|
|
281
|
+
// c -> a would create a cycle
|
|
282
|
+
expect(addDependency('c', 'a')).toBe(false);
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('getDependencyGraph', () => {
|
|
287
|
+
test('returns full graph', () => {
|
|
288
|
+
removeAllDependencies('svc-a');
|
|
289
|
+
removeAllDependencies('svc-b');
|
|
290
|
+
addDependency('svc-a', 'svc-b');
|
|
291
|
+
const graph = getDependencyGraph();
|
|
292
|
+
expect(graph['svc-a']).toContain('svc-b');
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('getDependents', () => {
|
|
297
|
+
test('finds processes that depend on a target', () => {
|
|
298
|
+
removeAllDependencies('frontend');
|
|
299
|
+
removeAllDependencies('backend');
|
|
300
|
+
addDependency('frontend', 'backend');
|
|
301
|
+
expect(getDependents('backend')).toContain('frontend');
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe('removeDependency', () => {
|
|
306
|
+
test('removes an existing dependency', () => {
|
|
307
|
+
removeAllDependencies('x');
|
|
308
|
+
addDependency('x', 'y');
|
|
309
|
+
expect(getDependencies('x')).toContain('y');
|
|
310
|
+
removeDependency('x', 'y');
|
|
311
|
+
expect(getDependencies('x')).not.toContain('y');
|
|
312
|
+
})
|
|
313
|
+
})
|
package/src/platform.ts
CHANGED
|
@@ -608,6 +608,20 @@ export async function getProcessBatchResources(pids: number[]): Promise<Map<numb
|
|
|
608
608
|
}) ?? new Map();
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Parse Unix lsof LISTEN output and return only true listening TCP ports.
|
|
613
|
+
*/
|
|
614
|
+
export function parseUnixListeningPorts(output: string): number[] {
|
|
615
|
+
const ports = new Set<number>();
|
|
616
|
+
for (const line of output.split('\n')) {
|
|
617
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
618
|
+
if (portMatch) {
|
|
619
|
+
ports.add(parseInt(portMatch[1]));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return Array.from(ports);
|
|
623
|
+
}
|
|
624
|
+
|
|
611
625
|
/**
|
|
612
626
|
* Get the TCP ports a process is currently listening on by querying the OS.
|
|
613
627
|
* Returns an array of port numbers (empty if none or process not found).
|
|
@@ -643,14 +657,7 @@ export async function getProcessPorts(pid: number): Promise<number[]> {
|
|
|
643
657
|
} catch { /* ss not available, try lsof */ }
|
|
644
658
|
|
|
645
659
|
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
646
|
-
|
|
647
|
-
for (const line of result.split('\n')) {
|
|
648
|
-
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
649
|
-
if (portMatch) {
|
|
650
|
-
ports.add(parseInt(portMatch[1]));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return Array.from(ports);
|
|
660
|
+
return parseUnixListeningPorts(result);
|
|
654
661
|
}
|
|
655
662
|
} catch {
|
|
656
663
|
return [];
|