bgrun 3.4.0 → 3.7.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.
@@ -1,4 +1,4 @@
1
- import { CommandOptions } from "../types";
1
+ import type { CommandOptions } from "../types";
2
2
  import { getProcess, removeProcessByName, retryDatabaseOperation, insertProcess } from "../db";
3
3
  import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree } from "../platform";
4
4
  import { error, announce } from "../logger";
@@ -7,8 +7,10 @@ import { parseConfigFile } from "../config";
7
7
  import { $ } from "bun";
8
8
  import { sleep } from "bun";
9
9
  import { join } from "path";
10
+ import { createMeasure } from "measure-fn";
10
11
 
11
12
  const homePath = getHomeDir();
13
+ const run = createMeasure('run');
12
14
 
13
15
  export async function handleRun(options: CommandOptions) {
14
16
  const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
@@ -24,18 +26,20 @@ export async function handleRun(options: CommandOptions) {
24
26
  if (!require('fs').existsSync(require('path').join(finalDirectory, '.git'))) {
25
27
  error(`Cannot --fetch: '${finalDirectory}' is not a Git repository.`);
26
28
  }
27
- try {
28
- await $`git fetch origin`;
29
- const localHash = (await $`git rev-parse HEAD`.text()).trim();
30
- const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
31
-
32
- if (localHash !== remoteHash) {
33
- await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
34
- announce("📥 Pulled latest changes", "Git Update");
29
+ await run.measure(`Git fetch "${name}"`, async () => {
30
+ try {
31
+ await $`git fetch origin`;
32
+ const localHash = (await $`git rev-parse HEAD`.text()).trim();
33
+ const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
34
+
35
+ if (localHash !== remoteHash) {
36
+ await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
37
+ announce("📥 Pulled latest changes", "Git Update");
38
+ }
39
+ } catch (err) {
40
+ error(`Failed to pull latest changes: ${err}`);
35
41
  }
36
- } catch (err) {
37
- error(`Failed to pull latest changes: ${err}`);
38
- }
42
+ });
39
43
  }
40
44
 
41
45
  const isRunning = await isProcessRunning(existingProcess.pid);
@@ -50,23 +54,26 @@ export async function handleRun(options: CommandOptions) {
50
54
  }
51
55
 
52
56
  if (isRunning) {
53
- await terminateProcess(existingProcess.pid);
54
- announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
57
+ await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
58
+ await terminateProcess(existingProcess.pid);
59
+ announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
60
+ });
55
61
  }
56
62
 
57
63
  // Kill anything still on the ports the old process was using
58
- for (const port of detectedPorts) {
59
- await killProcessOnPort(port);
60
- }
61
-
62
- // Wait for all detected ports to free up
63
- for (const port of detectedPorts) {
64
- const freed = await waitForPortFree(port, 5000);
65
- if (!freed) {
66
- // Retry kill and wait once more
67
- await killProcessOnPort(port);
68
- await waitForPortFree(port, 3000);
69
- }
64
+ if (detectedPorts.length > 0) {
65
+ await run.measure(`Port cleanup [${detectedPorts.join(', ')}]`, async () => {
66
+ for (const port of detectedPorts) {
67
+ await killProcessOnPort(port);
68
+ }
69
+ for (const port of detectedPorts) {
70
+ const freed = await waitForPortFree(port, 5000);
71
+ if (!freed) {
72
+ await killProcessOnPort(port);
73
+ await waitForPortFree(port, 3000);
74
+ }
75
+ }
76
+ });
70
77
  }
71
78
 
72
79
  await retryDatabaseOperation(() =>
@@ -97,12 +104,17 @@ export async function handleRun(options: CommandOptions) {
97
104
  const fullConfigPath = join(finalDirectory, finalConfigPath);
98
105
 
99
106
  if (await Bun.file(fullConfigPath).exists()) {
100
- try {
101
- const newConfigEnv = await parseConfigFile(fullConfigPath);
102
- finalEnv = { ...finalEnv, ...newConfigEnv };
107
+ const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
108
+ try {
109
+ return await parseConfigFile(fullConfigPath);
110
+ } catch (err: any) {
111
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
112
+ return null;
113
+ }
114
+ });
115
+ if (configEnv) {
116
+ finalEnv = { ...finalEnv, ...configEnv };
103
117
  console.log(`Loaded config from ${finalConfigPath}`);
104
- } catch (err: any) {
105
- console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
106
118
  }
107
119
  } else {
108
120
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
@@ -114,20 +126,23 @@ export async function handleRun(options: CommandOptions) {
114
126
  const stderrPath = stderr || existingProcess?.stderr_path || join(homePath, ".bgr", `${name}-err.txt`);
115
127
  Bun.write(stderrPath, '');
116
128
 
117
- const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
118
- env: { ...Bun.env, ...finalEnv },
119
- cwd: finalDirectory,
120
- stdout: Bun.file(stdoutPath),
121
- stderr: Bun.file(stderrPath),
122
- });
123
-
124
- newProcess.unref();
125
- // Give shell a moment to spawn child, then find PID before shell exits
126
- await sleep(100);
127
- // Find the actual child PID (shell wrapper exits immediately after spawning)
128
- const actualPid = await findChildPid(newProcess.pid);
129
- // Wait more for subprocess to initialize
130
- await sleep(400);
129
+ const actualPid = await run.measure(`Spawn "${name}" → ${finalCommand}`, async () => {
130
+ const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
131
+ env: { ...Bun.env, ...finalEnv },
132
+ cwd: finalDirectory,
133
+ stdout: Bun.file(stdoutPath),
134
+ stderr: Bun.file(stderrPath),
135
+ });
136
+
137
+ newProcess.unref();
138
+ // Give shell a moment to spawn child, then find PID before shell exits
139
+ await sleep(100);
140
+ // Find the actual child PID (shell wrapper exits immediately after spawning)
141
+ const pid = await findChildPid(newProcess.pid);
142
+ // Wait more for subprocess to initialize
143
+ await sleep(400);
144
+ return pid;
145
+ }) ?? 0;
131
146
 
132
147
  await retryDatabaseOperation(() =>
133
148
  insertProcess({
@@ -147,5 +162,3 @@ export async function handleRun(options: CommandOptions) {
147
162
  "Process Started"
148
163
  );
149
164
  }
150
-
151
-
package/src/db.ts CHANGED
@@ -2,6 +2,7 @@ import { Database, z } from "sqlite-zod-orm";
2
2
  import { getHomeDir, ensureDir } from "./platform";
3
3
  import { join } from "path";
4
4
  import { sleep } from "bun";
5
+ import { existsSync, copyFileSync } from "fs";
5
6
 
6
7
  // =============================================================================
7
8
  // SCHEMA (inline — single table, no need for a separate file)
@@ -27,11 +28,24 @@ export type Process = z.infer<typeof ProcessSchema> & { id: number };
27
28
 
28
29
  const homePath = getHomeDir();
29
30
  const bgrDir = join(homePath, ".bgr");
30
- const dbName = process.env.DB_NAME ?? "bgr";
31
- export const dbPath = join(bgrDir, `${dbName}_v2.sqlite`);
32
- export const bgrHome = bgrDir;
33
31
  ensureDir(bgrDir);
34
32
 
33
+ // DB filename: configurable via BGRUN_DB env, default "bgrun.sqlite"
34
+ const dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
35
+ export const dbPath = join(bgrDir, dbFilename);
36
+ export const bgrHome = bgrDir;
37
+
38
+ // Auto-migration: if new DB doesn't exist but old one does, copy it over
39
+ const legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
40
+ if (!existsSync(dbPath) && existsSync(legacyDbPath)) {
41
+ try {
42
+ copyFileSync(legacyDbPath, dbPath);
43
+ console.log(`[bgrun] Migrated database: ${legacyDbPath} → ${dbPath}`);
44
+ } catch (e) {
45
+ // Migration failed — start fresh
46
+ }
47
+ }
48
+
35
49
  export const db = new Database(dbPath, {
36
50
  process: ProcessSchema,
37
51
  }, {
@@ -90,6 +104,14 @@ export function removeProcessByName(name: string) {
90
104
  }
91
105
  }
92
106
 
107
+ /** Update the stored PID for a process (used by PID reconciliation) */
108
+ export function updateProcessPid(name: string, newPid: number) {
109
+ const proc = db.process.select().where({ name }).limit(1).get();
110
+ if (proc) {
111
+ db.process.update(proc.id, { pid: newPid });
112
+ }
113
+ }
114
+
93
115
  export function removeAllProcesses() {
94
116
  const all = db.process.select().all();
95
117
  for (const p of all) {
@@ -105,8 +127,8 @@ export function getDbInfo() {
105
127
  return {
106
128
  dbPath,
107
129
  bgrHome,
108
- dbName,
109
- exists: require('fs').existsSync(dbPath),
130
+ dbFilename,
131
+ exists: existsSync(dbPath),
110
132
  };
111
133
  }
112
134
 
package/src/index.ts CHANGED
@@ -17,6 +17,13 @@ import dedent from "dedent";
17
17
  import chalk from "chalk";
18
18
  import { join } from "path";
19
19
  import { sleep } from "bun";
20
+ import { configure } from "measure-fn";
21
+
22
+ if (!Bun.argv.includes("--_serve")) {
23
+ if (!Bun.env.MEASURE_SILENT) {
24
+ configure({ silent: true });
25
+ }
26
+ }
20
27
 
21
28
  async function showHelp() {
22
29
  const usage = dedent`
@@ -244,7 +251,7 @@ async function run() {
244
251
  Version: ${chalk.cyan(version)}
245
252
  BGR Home: ${chalk.yellow(info.bgrHome)}
246
253
  DB Path: ${chalk.yellow(info.dbPath)}
247
- DB Name: ${info.dbName}
254
+ DB File: ${info.dbFilename}
248
255
  DB Exists: ${info.exists ? chalk.green('✓') : chalk.red('✗')}
249
256
  Platform: ${process.platform}
250
257
  Bun: ${Bun.version}
package/src/platform.ts CHANGED
@@ -5,7 +5,11 @@
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as os from "os";
8
+ import { join } from "path";
8
9
  import { $ } from "bun";
10
+ import { measure, createMeasure } from "measure-fn";
11
+
12
+ const plat = createMeasure('platform');
9
13
 
10
14
  /** Detect if running on Windows - use function to prevent bundler tree-shaking */
11
15
  export function isWindows(): boolean {
@@ -24,24 +28,24 @@ export function getHomeDir(): string {
24
28
  * For Docker containers, checks container status instead of PID
25
29
  */
26
30
  export async function isProcessRunning(pid: number, command?: string): Promise<boolean> {
27
- try {
28
- // Docker container detection
29
- if (command && (command.includes('docker run') || command.includes('docker-compose up') || command.includes('docker compose up'))) {
30
- return await isDockerContainerRunning(command);
31
- }
31
+ return plat.measure(`PID ${pid} alive?`, async () => {
32
+ try {
33
+ // Docker container detection
34
+ if (command && (command.includes('docker run') || command.includes('docker-compose up') || command.includes('docker compose up'))) {
35
+ return await isDockerContainerRunning(command);
36
+ }
32
37
 
33
- if (isWindows()) {
34
- // On Windows, use tasklist to check for PID
35
- const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
36
- return result.includes(`${pid}`);
37
- } else {
38
- // On Unix, use ps -p
39
- const result = await $`ps -p ${pid}`.nothrow().text();
40
- return result.includes(`${pid}`);
38
+ if (isWindows()) {
39
+ const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
40
+ return result.includes(`${pid}`);
41
+ } else {
42
+ const result = await $`ps -p ${pid}`.nothrow().text();
43
+ return result.includes(`${pid}`);
44
+ }
45
+ } catch {
46
+ return false;
41
47
  }
42
- } catch {
43
- return false;
44
- }
48
+ });
45
49
  }
46
50
 
47
51
  /**
@@ -104,46 +108,48 @@ async function getChildPids(pid: number): Promise<number[]> {
104
108
  * Terminate a process and its children
105
109
  */
106
110
  export async function terminateProcess(pid: number, force: boolean = false): Promise<void> {
107
- // First, kill children
108
- const children = await getChildPids(pid);
111
+ await plat.measure(`Terminate PID ${pid}`, async (m) => {
112
+ // First, kill children
113
+ const children = await m('Get children', () => getChildPids(pid)) ?? [];
109
114
 
110
- for (const childPid of children) {
111
- try {
112
- if (isWindows()) {
113
- if (force) {
114
- await $`taskkill /F /PID ${childPid}`.nothrow().quiet();
115
+ for (const childPid of children) {
116
+ try {
117
+ if (isWindows()) {
118
+ if (force) {
119
+ await $`taskkill /F /PID ${childPid}`.nothrow().quiet();
120
+ } else {
121
+ await $`taskkill /PID ${childPid}`.nothrow().quiet();
122
+ }
115
123
  } else {
116
- await $`taskkill /PID ${childPid}`.nothrow().quiet();
124
+ const signal = force ? 'KILL' : 'TERM';
125
+ await $`kill -${signal} ${childPid}`.nothrow();
117
126
  }
118
- } else {
119
- const signal = force ? 'KILL' : 'TERM';
120
- await $`kill -${signal} ${childPid}`.nothrow();
127
+ } catch {
128
+ // Ignore errors for already-dead processes
121
129
  }
122
- } catch {
123
- // Ignore errors for already-dead processes
124
130
  }
125
- }
126
131
 
127
- // Wait a bit for graceful shutdown
128
- await Bun.sleep(500);
132
+ // Wait a bit for graceful shutdown
133
+ await Bun.sleep(500);
129
134
 
130
- // Then kill the parent if still running
131
- if (await isProcessRunning(pid)) {
132
- try {
133
- if (isWindows()) {
134
- if (force) {
135
- await $`taskkill /F /PID ${pid}`.nothrow().quiet();
135
+ // Then kill the parent if still running
136
+ if (await isProcessRunning(pid)) {
137
+ try {
138
+ if (isWindows()) {
139
+ if (force) {
140
+ await $`taskkill /F /PID ${pid}`.nothrow().quiet();
141
+ } else {
142
+ await $`taskkill /PID ${pid}`.nothrow().quiet();
143
+ }
136
144
  } else {
137
- await $`taskkill /PID ${pid}`.nothrow().quiet();
145
+ const signal = force ? 'KILL' : 'TERM';
146
+ await $`kill -${signal} ${pid}`.nothrow();
138
147
  }
139
- } else {
140
- const signal = force ? 'KILL' : 'TERM';
141
- await $`kill -${signal} ${pid}`.nothrow();
148
+ } catch {
149
+ // Ignore errors
142
150
  }
143
- } catch {
144
- // Ignore errors
145
151
  }
146
- }
152
+ });
147
153
  }
148
154
 
149
155
  /**
@@ -297,6 +303,108 @@ export async function findChildPid(parentPid: number): Promise<number> {
297
303
  return currentPid;
298
304
  }
299
305
 
306
+ /**
307
+ * Reconcile stale PIDs: when a stored PID is dead, search for a live process
308
+ * matching the same command line and update the DB with the correct PID.
309
+ *
310
+ * This handles the case where cmd.exe wrapper PIDs die after spawning the
311
+ * actual bun.exe child process, or after a system reboot where PIDs change.
312
+ *
313
+ * Returns a map of process name → reconciled PID for all matched processes.
314
+ */
315
+ export async function reconcileProcessPids(
316
+ processes: Array<{ name: string; pid: number; command: string; workdir: string }>,
317
+ deadPids: Set<number>,
318
+ ): Promise<Map<string, number>> {
319
+ return await plat.measure('Reconcile PIDs', async () => {
320
+ const result = new Map<string, number>();
321
+ const needsReconciliation = processes.filter(p => deadPids.has(p.pid));
322
+ if (needsReconciliation.length === 0) return result;
323
+
324
+ try {
325
+ // Get all running processes with their command lines
326
+ let runningProcs: Array<{ pid: number; cmdLine: string }> = [];
327
+
328
+ if (isWindows()) {
329
+ // Write a temp PS1 script to avoid quoting issues with $() in Bun's shell
330
+ const tmpScript = join(os.tmpdir(), 'bgr-reconcile.ps1');
331
+ const psCode = `Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'bun.exe' } | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`;
332
+ await Bun.write(tmpScript, psCode);
333
+
334
+ const ps = Bun.spawnSync(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', tmpScript]);
335
+ const output = ps.stdout.toString();
336
+
337
+ for (const line of output.split('\n')) {
338
+ const sepIdx = line.indexOf('|');
339
+ if (sepIdx === -1) continue;
340
+ const pid = parseInt(line.substring(0, sepIdx).trim());
341
+ const cmdLine = line.substring(sepIdx + 1).trim();
342
+ if (!isNaN(pid) && pid > 0 && cmdLine) {
343
+ runningProcs.push({ pid, cmdLine });
344
+ }
345
+ }
346
+ } else {
347
+ const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
348
+ for (const line of psOutput.trim().split('\n')) {
349
+ const match = line.trim().match(/^(\d+)\s+(.+)/);
350
+ if (match) {
351
+ runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
352
+ }
353
+ }
354
+ }
355
+
356
+ // For each dead process, try to find a matching live process
357
+ // Uses multi-criteria scoring to avoid false matches when multiple
358
+ // processes share similar commands (e.g. "bun run server.ts")
359
+ for (const proc of needsReconciliation) {
360
+ const cmdParts = proc.command.split(/\s+/);
361
+ // Extract meaningful parts: full command and workdir path segments
362
+ const workdirParts = proc.workdir.replace(/\\/g, '/').split('/').filter(Boolean);
363
+ const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || '';
364
+
365
+ let bestMatch: { pid: number; score: number } | null = null;
366
+ let ambiguous = false;
367
+
368
+ for (const running of runningProcs) {
369
+ const cmdLower = running.cmdLine.toLowerCase();
370
+ let score = 0;
371
+
372
+ // Score 1: command parts match (e.g. "run", "server.ts")
373
+ for (const part of cmdParts) {
374
+ if (part.length > 2 && cmdLower.includes(part.toLowerCase())) score++;
375
+ }
376
+
377
+ // Score 2: workdir folder name appears in command line path
378
+ // This distinguishes "bun run server.ts" in different directories
379
+ if (workdirLast && cmdLower.includes(workdirLast)) score += 3;
380
+
381
+ // Score 3: full workdir path match (strongest signal)
382
+ if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, '/'))) score += 5;
383
+ if (cmdLower.includes(proc.workdir.toLowerCase())) score += 5;
384
+
385
+ if (score < 4) continue; // Require workdir evidence — generic cmd matches alone aren't enough
386
+
387
+ if (!bestMatch || score > bestMatch.score) {
388
+ ambiguous = false;
389
+ bestMatch = { pid: running.pid, score };
390
+ } else if (score === bestMatch.score) {
391
+ ambiguous = true; // Multiple equally good matches — skip
392
+ }
393
+ }
394
+
395
+ if (bestMatch && !ambiguous) {
396
+ result.set(proc.name, bestMatch.pid);
397
+ runningProcs = runningProcs.filter(p => p.pid !== bestMatch!.pid);
398
+ }
399
+ }
400
+ } catch {
401
+ // Reconciliation is best-effort — return partial results
402
+ }
403
+
404
+ return result;
405
+ }) ?? new Map();
406
+ }
407
+
300
408
  /**
301
409
  * Wait for a port to become active and return the PID listening on it.
302
410
  * More reliable than findChildPid since it waits for the actual server
@@ -341,19 +449,21 @@ export async function findPidByPort(port: number, maxWaitMs = 8000): Promise<num
341
449
  }
342
450
 
343
451
  export async function readFileTail(filePath: string, lines?: number): Promise<string> {
344
- try {
345
- const content = await Bun.file(filePath).text();
452
+ return plat.measure(`Read tail ${lines ?? 'all'}L`, async () => {
453
+ try {
454
+ const content = await Bun.file(filePath).text();
346
455
 
347
- if (!lines) {
348
- return content;
349
- }
456
+ if (!lines) {
457
+ return content;
458
+ }
350
459
 
351
- const allLines = content.split(/\r?\n/);
352
- const tailLines = allLines.slice(-lines);
353
- return tailLines.join('\n');
354
- } catch (error) {
355
- throw new Error(`Error reading file: ${error}`);
356
- }
460
+ const allLines = content.split(/\r?\n/);
461
+ const tailLines = allLines.slice(-lines);
462
+ return tailLines.join('\n');
463
+ } catch (error) {
464
+ throw new Error(`Error reading file: ${error}`);
465
+ }
466
+ });
357
467
  }
358
468
 
359
469
  /**
@@ -379,57 +489,53 @@ export async function getProcessMemory(pid: number): Promise<number> {
379
489
  * to avoid spawning N subprocesses.
380
490
  */
381
491
  export async function getProcessBatchMemory(pids: number[]): Promise<Map<number, number>> {
382
- const memoryMap = new Map<number, number>();
383
- if (pids.length === 0) return memoryMap;
492
+ if (pids.length === 0) return new Map();
384
493
 
385
- // Create a quick lookup set
386
- const pidSet = new Set(pids);
494
+ return await plat.measure(`Batch memory (${pids.length} PIDs)`, async () => {
495
+ const memoryMap = new Map<number, number>();
496
+ const pidSet = new Set(pids);
387
497
 
388
- try {
389
- if (isWindows()) {
390
- // Get-Process | Select-Object Id, WorkingSet
391
- const result = await $`powershell -Command "Get-Process | Select-Object Id, WorkingSet"`.nothrow().quiet().text();
392
- const lines = result.trim().split('\n');
498
+ try {
499
+ if (isWindows()) {
500
+ const result = await $`powershell -Command "Get-Process | Select-Object Id, WorkingSet"`.nothrow().quiet().text();
501
+ const lines = result.trim().split('\n');
393
502
 
394
- for (const line of lines) {
395
- const trimmed = line.trim();
396
- if (!trimmed || trimmed.startsWith('Id') || trimmed.startsWith('--')) continue;
503
+ for (const line of lines) {
504
+ const trimmed = line.trim();
505
+ if (!trimmed || trimmed.startsWith('Id') || trimmed.startsWith('--')) continue;
397
506
 
398
- const parts = trimmed.split(/\s+/);
399
- if (parts.length >= 2) {
400
- const val1 = parseInt(parts[0]);
401
- const val2 = parseInt(parts[parts.length - 1]);
507
+ const parts = trimmed.split(/\s+/);
508
+ if (parts.length >= 2) {
509
+ const val1 = parseInt(parts[0]);
510
+ const val2 = parseInt(parts[parts.length - 1]);
402
511
 
403
- if (!isNaN(val1) && !isNaN(val2)) {
404
- if (pidSet.has(val1)) memoryMap.set(val1, val2);
512
+ if (!isNaN(val1) && !isNaN(val2)) {
513
+ if (pidSet.has(val1)) memoryMap.set(val1, val2);
514
+ }
405
515
  }
406
516
  }
407
- }
408
- } else {
409
- // Unix: ps -o pid,rss
410
- // PID RSS
411
- // 123 456
412
- const result = await $`ps -eo pid,rss`.nothrow().quiet().text();
413
- const lines = result.trim().split('\n');
414
-
415
- // Skip header
416
- for (let i = 1; i < lines.length; i++) {
417
- const line = lines[i].trim();
418
- if (!line) continue;
419
- const [pidStr, rssStr] = line.split(/\s+/);
420
- const pid = parseInt(pidStr);
421
- const rss = parseInt(rssStr); // KB
422
-
423
- if (pidSet.has(pid)) {
424
- memoryMap.set(pid, rss * 1024); // Convert KB to Bytes
517
+ } else {
518
+ const result = await $`ps -eo pid,rss`.nothrow().quiet().text();
519
+ const lines = result.trim().split('\n');
520
+
521
+ for (let i = 1; i < lines.length; i++) {
522
+ const line = lines[i].trim();
523
+ if (!line) continue;
524
+ const [pidStr, rssStr] = line.split(/\s+/);
525
+ const pid = parseInt(pidStr);
526
+ const rss = parseInt(rssStr);
527
+
528
+ if (pidSet.has(pid)) {
529
+ memoryMap.set(pid, rss * 1024);
530
+ }
425
531
  }
426
532
  }
533
+ } catch (e) {
534
+ // silently fail
427
535
  }
428
- } catch (e) {
429
- // console.error("Error fetching batch memory:", e);
430
- }
431
536
 
432
- return memoryMap;
537
+ return memoryMap;
538
+ }) ?? new Map();
433
539
  }
434
540
 
435
541
  /**