feat: enhance port management and server initialization process

- Added a new function to check if a port is in use without terminating processes, improving user experience during server startup.
- Updated the health check function to accept a dynamic port parameter, allowing for flexible server configurations.
- Implemented user prompts for handling port conflicts, enabling users to kill processes, choose different ports, or cancel the operation.
- Enhanced CORS configuration to support localhost and IPv6 addresses, ensuring compatibility across different development environments.
- Refactored the main function to utilize dynamic port assignments for both the web and server applications, improving overall flexibility.
This commit is contained in:
WebDevCody
2026-01-01 00:42:42 -05:00
parent abe272ef4d
commit f32f3e82b2
3 changed files with 173 additions and 16 deletions

View File

@@ -133,7 +133,11 @@ app.use(
}
// For local development, allow localhost origins
if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
if (
origin.startsWith('http://localhost:') ||
origin.startsWith('http://127.0.0.1:') ||
origin.startsWith('http://[::1]:')
) {
callback(null, origin);
return;
}

View File

@@ -53,9 +53,12 @@ let mainWindow: BrowserWindow | null = null;
let serverProcess: ChildProcess | null = null;
let staticServer: Server | null = null;
// Default ports - will be dynamically assigned if these are in use
const DEFAULT_SERVER_PORT = 3008;
const DEFAULT_STATIC_PORT = 3007;
// Default ports (can be overridden via env) - will be dynamically assigned if these are in use
// When launched via root init.mjs we pass:
// - PORT (backend)
// - TEST_PORT (vite dev server / static)
const DEFAULT_SERVER_PORT = parseInt(process.env.PORT || '3008', 10);
const DEFAULT_STATIC_PORT = parseInt(process.env.TEST_PORT || '3007', 10);
// Actual ports in use (set during startup)
let serverPort = DEFAULT_SERVER_PORT;
@@ -75,7 +78,9 @@ function isPortAvailable(port: number): Promise<boolean> {
resolve(true);
});
});
server.listen(port, '127.0.0.1');
// Use Node's default binding semantics (matches most dev servers)
// This avoids false-positives when a port is taken on IPv6/dual-stack.
server.listen(port);
});
}

170
init.mjs
View File

@@ -170,6 +170,14 @@ function killProcess(pid) {
}
}
/**
* Check if a port is in use (without killing)
*/
function isPortInUse(port) {
const pids = getProcessesOnPort(port);
return pids.length > 0;
}
/**
* Kill processes on a port and wait for it to be freed
*/
@@ -211,9 +219,9 @@ function sleep(ms) {
/**
* Check if the server health endpoint is responding
*/
function checkHealth() {
function checkHealth(port = 3008) {
return new Promise((resolve) => {
const req = http.get('http://localhost:3008/api/health', (res) => {
const req = http.get(`http://localhost:${port}/api/health`, (res) => {
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
@@ -245,10 +253,16 @@ function prompt(question) {
* Run npm command using cross-spawn for Windows compatibility
*/
function runNpm(args, options = {}) {
const { env, ...restOptions } = options;
const spawnOptions = {
stdio: 'inherit',
cwd: __dirname,
...options,
...restOptions,
// Ensure environment variables are properly merged with process.env
env: {
...process.env,
...(env || {}),
},
};
// cross-spawn handles Windows .cmd files automatically
return crossSpawn('npm', args, spawnOptions);
@@ -352,10 +366,123 @@ async function main() {
log('Playwright installation skipped', 'yellow');
}
// Kill any existing processes on required ports
// Check for processes on required ports and prompt user
log('Checking for processes on ports 3007 and 3008...', 'yellow');
await killPort(3007);
await killPort(3008);
const webPortInUse = isPortInUse(3007);
const serverPortInUse = isPortInUse(3008);
let webPort = 3007;
let serverPort = 3008;
let corsOriginEnv = process.env.CORS_ORIGIN || '';
if (webPortInUse || serverPortInUse) {
console.log('');
if (webPortInUse) {
const pids = getProcessesOnPort(3007);
log(`⚠ Port 3007 is in use by process(es): ${pids.join(', ')}`, 'yellow');
}
if (serverPortInUse) {
const pids = getProcessesOnPort(3008);
log(`⚠ Port 3008 is in use by process(es): ${pids.join(', ')}`, 'yellow');
}
console.log('');
while (true) {
const choice = await prompt('What would you like to do? (k)ill processes, (u)se different ports, or (c)ancel: ');
const lowerChoice = choice.toLowerCase();
if (lowerChoice === 'k' || lowerChoice === 'kill') {
if (webPortInUse) {
await killPort(3007);
} else {
log(`✓ Port 3007 is available`, 'green');
}
if (serverPortInUse) {
await killPort(3008);
} else {
log(`✓ Port 3008 is available`, 'green');
}
break;
} else if (lowerChoice === 'u' || lowerChoice === 'use') {
// Prompt for new ports
while (true) {
const newWebPort = await prompt('Enter web port (default 3007): ');
const parsedWebPort = newWebPort.trim() ? parseInt(newWebPort.trim(), 10) : 3007;
if (isNaN(parsedWebPort) || parsedWebPort < 1024 || parsedWebPort > 65535) {
log('Invalid port. Please enter a number between 1024 and 65535.', 'red');
continue;
}
if (isPortInUse(parsedWebPort)) {
const pids = getProcessesOnPort(parsedWebPort);
log(`Port ${parsedWebPort} is already in use by process(es): ${pids.join(', ')}`, 'red');
const useAnyway = await prompt('Use this port anyway? (y/n): ');
if (useAnyway.toLowerCase() !== 'y' && useAnyway.toLowerCase() !== 'yes') {
continue;
}
}
webPort = parsedWebPort;
break;
}
while (true) {
const newServerPort = await prompt('Enter server port (default 3008): ');
const parsedServerPort = newServerPort.trim() ? parseInt(newServerPort.trim(), 10) : 3008;
if (isNaN(parsedServerPort) || parsedServerPort < 1024 || parsedServerPort > 65535) {
log('Invalid port. Please enter a number between 1024 and 65535.', 'red');
continue;
}
if (parsedServerPort === webPort) {
log('Server port cannot be the same as web port.', 'red');
continue;
}
if (isPortInUse(parsedServerPort)) {
const pids = getProcessesOnPort(parsedServerPort);
log(`Port ${parsedServerPort} is already in use by process(es): ${pids.join(', ')}`, 'red');
const useAnyway = await prompt('Use this port anyway? (y/n): ');
if (useAnyway.toLowerCase() !== 'y' && useAnyway.toLowerCase() !== 'yes') {
continue;
}
}
serverPort = parsedServerPort;
break;
}
log(`Using ports: Web=${webPort}, Server=${serverPort}`, 'blue');
break;
} else if (lowerChoice === 'c' || lowerChoice === 'cancel') {
log('Cancelled.', 'yellow');
process.exit(0);
} else {
log('Invalid choice. Please enter k (kill), u (use different ports), or c (cancel).', 'red');
}
}
} else {
log(`✓ Port 3007 is available`, 'green');
log(`✓ Port 3008 is available`, 'green');
}
// Ensure backend CORS allows whichever UI port we ended up using.
// If CORS_ORIGIN is set, server enforces it strictly (see apps/server/src/index.ts),
// so we must include the selected web origin(s) in that list.
{
const existing = (process.env.CORS_ORIGIN || '')
.split(',')
.map((o) => o.trim())
.filter(Boolean)
.filter((o) => o !== '*');
const origins = new Set(existing);
origins.add(`http://localhost:${webPort}`);
origins.add(`http://127.0.0.1:${webPort}`);
corsOriginEnv = Array.from(origins).join(',');
}
console.log('');
// Show menu
@@ -388,7 +515,7 @@ async function main() {
log('Launching Web Application...', 'blue');
// Start the backend server
log('Starting backend server on port 3008...', 'blue');
log(`Starting backend server on port ${serverPort}...`, 'blue');
// Create logs directory
if (!fs.existsSync(path.join(__dirname, 'logs'))) {
@@ -399,6 +526,10 @@ async function main() {
const logStream = fs.createWriteStream(path.join(__dirname, 'logs', 'server.log'));
serverProcess = runNpm(['run', 'dev:server'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
PORT: String(serverPort),
CORS_ORIGIN: corsOriginEnv,
},
});
// Pipe to both log file and console so user can see API key
@@ -418,7 +549,7 @@ async function main() {
let serverReady = false;
for (let i = 0; i < maxRetries; i++) {
if (await checkHealth()) {
if (await checkHealth(serverPort)) {
serverReady = true;
break;
}
@@ -436,11 +567,17 @@ async function main() {
}
log('✓ Server is ready!', 'green');
log(`The application will be available at: http://localhost:3007`, 'green');
log(`The application will be available at: http://localhost:${webPort}`, 'green');
console.log('');
// Start web app
webProcess = runNpm(['run', 'dev:web'], { stdio: 'inherit' });
webProcess = runNpm(['run', 'dev:web'], {
stdio: 'inherit',
env: {
TEST_PORT: String(webPort),
VITE_SERVER_URL: `http://localhost:${serverPort}`,
},
});
await new Promise((resolve) => {
webProcess.on('close', resolve);
});
@@ -452,7 +589,18 @@ async function main() {
log('(Electron will start its own backend server)', 'yellow');
console.log('');
electronProcess = runNpm(['run', 'dev:electron'], { stdio: 'inherit' });
// Pass selected ports through to Vite + Electron backend
// - TEST_PORT controls Vite dev server port (see apps/ui/vite.config.mts)
// - PORT controls backend server port (see apps/server/src/index.ts)
electronProcess = runNpm(['run', 'dev:electron'], {
stdio: 'inherit',
env: {
TEST_PORT: String(webPort),
PORT: String(serverPort),
VITE_SERVER_URL: `http://localhost:${serverPort}`,
CORS_ORIGIN: corsOriginEnv,
},
});
await new Promise((resolve) => {
electronProcess.on('close', resolve);
});