Skip to content

Commit 559d178

Browse files
committed
♻️ Split daemon process lifecycle helpers
Why: isolate PID discovery and process-exit waiting from command orchestration so daemon lifecycle behavior is easier to test and reason about without touching start/stop UX paths. What changed: moved pid-file parsing, lsof discovery, pid resolution, and wait-for-exit helpers into src/commands/tdd-daemon-process.js and re-exported them from tdd-daemon.js for compatibility.
1 parent 556b75e commit 559d178

2 files changed

Lines changed: 123 additions & 104 deletions

File tree

src/commands/tdd-daemon-process.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { spawn } from 'node:child_process';
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
5+
let defaultTimers = { setTimeout, clearTimeout };
6+
7+
function wait(ms, timers = defaultTimers) {
8+
return new Promise(resolve => {
9+
timers.setTimeout(resolve, ms);
10+
});
11+
}
12+
13+
function isProcessRunning(pid) {
14+
try {
15+
process.kill(pid, 0);
16+
return true;
17+
} catch {
18+
return false;
19+
}
20+
}
21+
22+
function parsePositiveInteger(value) {
23+
let text = String(value).trim();
24+
if (!/^\d+$/.test(text)) {
25+
return null;
26+
}
27+
28+
let number = Number(text);
29+
return Number.isSafeInteger(number) && number > 0 ? number : null;
30+
}
31+
32+
export function readDaemonPidFile(pidFile, deps = {}) {
33+
let fileExists = deps.existsSync || existsSync;
34+
let readFile = deps.readFileSync || readFileSync;
35+
36+
if (!fileExists(pidFile)) {
37+
return null;
38+
}
39+
40+
try {
41+
return parsePositiveInteger(readFile(pidFile, 'utf8'));
42+
} catch {
43+
return null;
44+
}
45+
}
46+
47+
export async function findDaemonPidByPort(port, { spawnProcess = spawn } = {}) {
48+
try {
49+
let lsofProcess = spawnProcess('lsof', ['-ti', `:${port}`], {
50+
stdio: 'pipe',
51+
});
52+
53+
let lsofOutput = '';
54+
lsofProcess.stdout.on('data', data => {
55+
lsofOutput += data.toString();
56+
});
57+
58+
return await new Promise(resolve => {
59+
lsofProcess.on('close', code => {
60+
if (code === 0 && lsofOutput.trim()) {
61+
let foundPid = parsePositiveInteger(lsofOutput.trim().split('\n')[0]);
62+
resolve(foundPid);
63+
return;
64+
}
65+
66+
resolve(null);
67+
});
68+
69+
lsofProcess.on('error', () => {
70+
resolve(null);
71+
});
72+
});
73+
} catch {
74+
return null;
75+
}
76+
}
77+
78+
export async function resolveDaemonPid({
79+
port,
80+
pidFile = join(process.cwd(), '.vizzly', 'server.pid'),
81+
readPid = readDaemonPidFile,
82+
findByPort = findDaemonPidByPort,
83+
fileDeps = {},
84+
} = {}) {
85+
let pid = readPid(pidFile, fileDeps);
86+
if (pid) {
87+
return pid;
88+
}
89+
90+
return await findByPort(port);
91+
}
92+
93+
export async function waitForProcessExit(
94+
pid,
95+
{
96+
timeoutMs = 2000,
97+
intervalMs = 100,
98+
processRunning = isProcessRunning,
99+
timers = defaultTimers,
100+
} = {}
101+
) {
102+
let elapsedMs = 0;
103+
104+
while (elapsedMs < timeoutMs) {
105+
if (!processRunning(pid)) {
106+
return true;
107+
}
108+
109+
let nextDelay = Math.min(intervalMs, timeoutMs - elapsedMs);
110+
await wait(nextDelay, timers);
111+
elapsedMs += nextDelay;
112+
}
113+
114+
return !processRunning(pid);
115+
}

src/commands/tdd-daemon.js

Lines changed: 8 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import { getServerRegistry } from '../tdd/server-registry.js';
1212
import { withTimeout } from '../utils/async-utils.js';
1313
import * as output from '../utils/output.js';
1414
import { tddCommand } from './tdd.js';
15+
import { resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
16+
17+
export {
18+
findDaemonPidByPort,
19+
readDaemonPidFile,
20+
resolveDaemonPid,
21+
waitForProcessExit,
22+
} from './tdd-daemon-process.js';
1523

1624
let defaultTimers = { setTimeout, clearTimeout };
1725

@@ -143,86 +151,6 @@ function wait(ms, timers = defaultTimers) {
143151
});
144152
}
145153

146-
function isProcessRunning(pid) {
147-
try {
148-
process.kill(pid, 0);
149-
return true;
150-
} catch {
151-
return false;
152-
}
153-
}
154-
155-
function parsePositiveInteger(value) {
156-
let text = String(value).trim();
157-
if (!/^\d+$/.test(text)) {
158-
return null;
159-
}
160-
161-
let number = Number(text);
162-
return Number.isSafeInteger(number) && number > 0 ? number : null;
163-
}
164-
165-
export function readDaemonPidFile(pidFile, deps = {}) {
166-
let fileExists = deps.existsSync || existsSync;
167-
let readFile = deps.readFileSync || readFileSync;
168-
169-
if (!fileExists(pidFile)) {
170-
return null;
171-
}
172-
173-
try {
174-
return parsePositiveInteger(readFile(pidFile, 'utf8'));
175-
} catch {
176-
return null;
177-
}
178-
}
179-
180-
export async function findDaemonPidByPort(port, { spawnProcess = spawn } = {}) {
181-
try {
182-
let lsofProcess = spawnProcess('lsof', ['-ti', `:${port}`], {
183-
stdio: 'pipe',
184-
});
185-
186-
let lsofOutput = '';
187-
lsofProcess.stdout.on('data', data => {
188-
lsofOutput += data.toString();
189-
});
190-
191-
return await new Promise(resolve => {
192-
lsofProcess.on('close', code => {
193-
if (code === 0 && lsofOutput.trim()) {
194-
let foundPid = parsePositiveInteger(lsofOutput.trim().split('\n')[0]);
195-
resolve(foundPid);
196-
return;
197-
}
198-
199-
resolve(null);
200-
});
201-
202-
lsofProcess.on('error', () => {
203-
resolve(null);
204-
});
205-
});
206-
} catch {
207-
return null;
208-
}
209-
}
210-
211-
export async function resolveDaemonPid({
212-
port,
213-
pidFile = getLocalDaemonFiles().pidFile,
214-
readPid = readDaemonPidFile,
215-
findByPort = findDaemonPidByPort,
216-
fileDeps = {},
217-
} = {}) {
218-
let pid = readPid(pidFile, fileDeps);
219-
if (pid) {
220-
return pid;
221-
}
222-
223-
return await findByPort(port);
224-
}
225-
226154
export function buildDaemonChildArgs({
227155
entrypoint = process.argv[1],
228156
port,
@@ -355,30 +283,6 @@ export async function waitForServerRunning(
355283
return false;
356284
}
357285

358-
export async function waitForProcessExit(
359-
pid,
360-
{
361-
timeoutMs = 2000,
362-
intervalMs = 100,
363-
processRunning = isProcessRunning,
364-
timers = defaultTimers,
365-
} = {}
366-
) {
367-
let elapsedMs = 0;
368-
369-
while (elapsedMs < timeoutMs) {
370-
if (!processRunning(pid)) {
371-
return true;
372-
}
373-
374-
let nextDelay = Math.min(intervalMs, timeoutMs - elapsedMs);
375-
await wait(nextDelay, timers);
376-
elapsedMs += nextDelay;
377-
}
378-
379-
return !processRunning(pid);
380-
}
381-
382286
/**
383287
* Start TDD server in daemon mode
384288
* @param {Object} options - Command options

0 commit comments

Comments
 (0)