Skip to content

Commit 8fc16bd

Browse files
committed
🐛 Fix code review issues for menubar support
Addresses feedback from PR #200 code review: **Critical fixes:** - Make saveUserPath() non-blocking to avoid CLI startup delays - Add comment documenting the race condition mitigation strategy **Bug fixes:** - Remove broken osascript notification (file watching is primary) - Clean up local .vizzly files when detecting stale registry entries **Code quality:** - Add input validation to register() (required fields, number checks) - Fix unregister() to use AND logic when both port and directory provided - Fix flaky test by using high port range (48500+) to avoid conflicts
1 parent 3497d08 commit 8fc16bd

4 files changed

Lines changed: 65 additions & 42 deletions

File tree

src/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,8 +763,8 @@ program
763763
await projectRemoveCommand(options, globalOptions);
764764
});
765765

766-
// Save user's PATH for menubar app before parsing commands
766+
// Save user's PATH for menubar app (non-blocking, runs in background)
767767
// This auto-configures the menubar app so it can find npx/node
768-
await saveUserPath().catch(() => {});
768+
saveUserPath().catch(() => {});
769769

770770
program.parse();

src/commands/tdd-daemon.js

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
5050
}
5151
return;
5252
} else {
53-
// Stale entry - clean it up
53+
// Stale entry - clean it up (registry and local files)
5454
registry.unregister({ directory: process.cwd() });
55+
56+
let vizzlyDir = join(process.cwd(), '.vizzly');
57+
let pidFile = join(vizzlyDir, 'server.pid');
58+
let serverFile = join(vizzlyDir, 'server.json');
59+
if (existsSync(pidFile)) unlinkSync(pidFile);
60+
if (existsSync(serverFile)) unlinkSync(serverFile);
5561
}
5662
}
5763

@@ -62,24 +68,27 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
6268
if (options.port) {
6369
// User specified a port - use it (will fail if busy)
6470
port = options.port;
71+
72+
// Check if user-specified port is in use
73+
if (await isServerRunning(port)) {
74+
output.header('tdd', 'local');
75+
output.print(
76+
` ${output.statusDot('error')} Port ${port} is already in use`
77+
);
78+
output.blank();
79+
output.hint('Try a different port: vizzly tdd start --port 47393');
80+
output.hint('Or let Vizzly auto-allocate: vizzly tdd start');
81+
return;
82+
}
6583
} else {
6684
// Auto-allocate an available port
85+
// Note: There's a small race window between finding a port and binding.
86+
// The registry acts as a soft reservation, and findAvailablePort does
87+
// an actual TCP bind test to minimize this window.
6788
port = await registry.findAvailablePort();
6889
autoAllocated = port !== 47392;
6990
}
7091

71-
// If user specified a port, check if it's in use
72-
if (options.port && (await isServerRunning(port))) {
73-
output.header('tdd', 'local');
74-
output.print(
75-
` ${output.statusDot('error')} Port ${port} is already in use`
76-
);
77-
output.blank();
78-
output.hint('Try a different port: vizzly tdd start --port 47393');
79-
output.hint('Or let Vizzly auto-allocate: vizzly tdd start');
80-
return;
81-
}
82-
8392
try {
8493
// Ensure .vizzly directory exists
8594
let vizzlyDir = join(process.cwd(), '.vizzly');

src/tdd/server-registry.js

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { execSync } from 'node:child_process';
21
import { randomBytes } from 'node:crypto';
32
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
43
import { createServer } from 'node:net';
@@ -55,18 +54,30 @@ export class ServerRegistry {
5554
* Register a new server in the registry
5655
*/
5756
register(serverInfo) {
57+
// Validate required fields
58+
if (!serverInfo.pid || !serverInfo.port || !serverInfo.directory) {
59+
throw new Error('Missing required fields: pid, port, directory');
60+
}
61+
62+
let port = Number(serverInfo.port);
63+
let pid = Number(serverInfo.pid);
64+
65+
if (Number.isNaN(port) || Number.isNaN(pid)) {
66+
throw new Error('Invalid port or pid - must be numbers');
67+
}
68+
5869
let registry = this.read();
5970

6071
// Remove any existing entry for this port or directory (shouldn't happen, but be safe)
6172
registry.servers = registry.servers.filter(
62-
s => s.port !== serverInfo.port && s.directory !== serverInfo.directory
73+
s => s.port !== port && s.directory !== serverInfo.directory
6374
);
6475

6576
// Add the new server
6677
registry.servers.push({
6778
id: serverInfo.id || randomBytes(8).toString('hex'),
68-
port: Number(serverInfo.port),
69-
pid: Number(serverInfo.pid),
79+
port,
80+
pid,
7081
directory: serverInfo.directory,
7182
startedAt: serverInfo.startedAt || new Date().toISOString(),
7283
configPath: serverInfo.configPath || null,
@@ -81,16 +92,22 @@ export class ServerRegistry {
8192
}
8293

8394
/**
84-
* Unregister a server by port or directory
95+
* Unregister a server by port and/or directory
96+
* When both are provided, matches servers with BOTH criteria (AND logic)
97+
* When only one is provided, matches servers with that criteria
8598
*/
8699
unregister({ port, directory }) {
87100
let registry = this.read();
88101
let initialCount = registry.servers.length;
89102

90-
if (port) {
103+
if (port && directory) {
104+
// Both specified - match servers with both port AND directory
105+
registry.servers = registry.servers.filter(
106+
s => !(s.port === port && s.directory === directory)
107+
);
108+
} else if (port) {
91109
registry.servers = registry.servers.filter(s => s.port !== port);
92-
}
93-
if (directory) {
110+
} else if (directory) {
94111
registry.servers = registry.servers.filter(
95112
s => s.directory !== directory
96113
);
@@ -155,20 +172,14 @@ export class ServerRegistry {
155172

156173
/**
157174
* Notify the menubar app that the registry changed
158-
* Uses macOS DistributedNotificationCenter via osascript
175+
*
176+
* NOTE: The menubar app primarily uses FSEvents file watching on servers.json.
177+
* This method is a placeholder for future notification mechanisms (e.g., XPC).
178+
* For now, file watching provides reliable, immediate updates.
159179
*/
160180
notifyMenubar() {
161-
if (process.platform !== 'darwin') return;
162-
163-
try {
164-
// Post a distributed notification that the menubar app listens for
165-
execSync(
166-
`osascript -e 'tell application "System Events" to post notification with name "dev.vizzly.serverChanged"'`,
167-
{ stdio: 'ignore', timeout: 1000 }
168-
);
169-
} catch (_err) {
170-
// Non-fatal - menubar might not be running or osascript might fail
171-
}
181+
// File watching on servers.json is the primary notification mechanism.
182+
// This method exists for future enhancements (XPC, etc.) but is currently a no-op.
172183
}
173184

174185
/**

tests/tdd/server-registry.test.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,21 @@ describe('tdd/server-registry', () => {
184184
});
185185

186186
describe('findAvailablePort', () => {
187+
// Use high port range (48500+) to avoid conflicts with running TDD servers
188+
let testBasePort = 48500;
189+
187190
it('returns default port when nothing is using it', async () => {
188-
let port = await registry.findAvailablePort(47392);
189-
assert.strictEqual(port, 47392);
191+
let port = await registry.findAvailablePort(testBasePort);
192+
assert.strictEqual(port, testBasePort);
190193
});
191194

192195
it('skips ports registered in the registry', async () => {
193-
// Register servers on 47392 and 47393 with current PID so they're not cleaned up
194-
registry.register({ pid: process.pid, port: 47392, directory: '/a' });
195-
registry.register({ pid: process.pid, port: 47393, directory: '/b' });
196+
// Register servers on testBasePort and testBasePort+1 with current PID so they're not cleaned up
197+
registry.register({ pid: process.pid, port: testBasePort, directory: '/a' });
198+
registry.register({ pid: process.pid, port: testBasePort + 1, directory: '/b' });
196199

197-
let port = await registry.findAvailablePort(47392);
198-
assert.strictEqual(port, 47394);
200+
let port = await registry.findAvailablePort(testBasePort);
201+
assert.strictEqual(port, testBasePort + 2);
199202
});
200203

201204
it('skips ports actually in use by other processes', async () => {

0 commit comments

Comments
 (0)