From 28768d9eaa82e09e84ae9e2ef33bcc91be445ba5 Mon Sep 17 00:00:00 2001 From: Sparsh Date: Sat, 23 May 2026 21:45:56 +0530 Subject: [PATCH] feat(desktop): Dynamic port selection via --port 0 + stdout signal Spawn the GitNexus backend server with --port 0 so the OS assigns a free port instead of hardcoding 4747. The server announces the chosen port via a GITNEXUS_PORT=N stdout line, which the desktop captures and uses for health checks and identity verification. Also fixes server/api.ts to log the actual bound port (server.address().port) instead of the port parameter, which was wrong when using --port 0. Removes GITNEXUS_PORT, GITNEXUS_SERVER_URL, GITNEXUS_SERVER_HEALTH_URLS constants. Adds identity verification via /api/info version field. Passes server URL to web UI via ?server= query param (already supported by App.tsx auto-connect). Refs GH-985 --- gitnexus-desktop/src/main/main.ts | 81 ++++++++++++++++++++----------- gitnexus/src/server/api.ts | 5 +- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/gitnexus-desktop/src/main/main.ts b/gitnexus-desktop/src/main/main.ts index ec546d1cc..f478caa50 100644 --- a/gitnexus-desktop/src/main/main.ts +++ b/gitnexus-desktop/src/main/main.ts @@ -25,14 +25,8 @@ const DESKTOP_APP_ICON_PATH = app.isPackaged const DESKTOP_GET_SHELL_STATE_CHANNEL = 'gitnexus-desktop:get-shell-state'; const DESKTOP_WINDOW_ACTION_CHANNEL = 'gitnexus-desktop:window-action'; const DESKTOP_WINDOW_STATE_CHANGED_CHANNEL = 'gitnexus-desktop:window-state-changed'; -const GITNEXUS_PORT = 4747; const GITNEXUS_HOST = 'localhost'; -const GITNEXUS_SERVER_URL = `http://${GITNEXUS_HOST}:${GITNEXUS_PORT}`; -const GITNEXUS_SERVER_HEALTH_URLS = [ - `${GITNEXUS_SERVER_URL}/api/info`, - `http://127.0.0.1:${GITNEXUS_PORT}/api/info`, - `http://[::1]:${GITNEXUS_PORT}/api/info`, -]; +let gitNexusPort = 0; const GITNEXUS_DEV_RUNTIME_DIR = path.resolve(__dirname, '../../../gitnexus'); const GITNEXUS_WEB_DEV_HOST = 'localhost'; const GITNEXUS_WEB_DEV_PORT = 5173; @@ -72,7 +66,7 @@ const MIME_TYPES: Record = { '.woff2': 'font/woff2', }; -let serverReadyPromise: Promise | null = null; +let serverReadyPromise: Promise | null = null; let gitNexusServerProcess: ChildProcess | null = null; let gitNexusServerOutput = ''; let webDevServerProcess: ChildProcess | null = null; @@ -284,7 +278,7 @@ const getNodeProcessEnvironment = (overrides: NodeJS.ProcessEnv = {}): NodeJS.Pr const spawnGitNexusServer = (): ChildProcess => { const childProcess = spawn( getNodeCommand(), - [getGitNexusCliEntry(), 'serve', '--host', GITNEXUS_HOST], + [getGitNexusCliEntry(), 'serve', '--host', GITNEXUS_HOST, '--port', '0'], { cwd: getGitNexusRuntimeDir(), env: getNodeProcessEnvironment(app.isPackaged ? { GITNEXUS_DISABLE_MCP_HTTP: '1' } : {}), @@ -295,7 +289,12 @@ const spawnGitNexusServer = (): ChildProcess => { childProcess.stdout?.on('data', (chunk) => { appendGitNexusServerOutput(chunk); - process.stdout.write(`[gitnexus-server] ${chunk.toString()}`); + const str = chunk.toString(); + const match = str.match(/GITNEXUS_PORT=(\d+)/); + if (match) { + gitNexusPort = parseInt(match[1], 10); + } + process.stdout.write(`[gitnexus-server] ${str}`); }); childProcess.stderr?.on('data', (chunk) => { @@ -314,12 +313,25 @@ const spawnGitNexusServer = (): ChildProcess => { return childProcess; }; -const waitForGitNexusServerReady = async (): Promise => { +const waitForGitNexusServerReady = async (): Promise => { const deadline = Date.now() + GITNEXUS_SERVER_READY_TIMEOUT_MS; while (Date.now() < deadline) { - if (await isAnyHttpUrlReady(GITNEXUS_SERVER_HEALTH_URLS)) { - return; + if (gitNexusPort > 0) { + const healthUrl = `http://${GITNEXUS_HOST}:${gitNexusPort}/api/info`; + const responseText = await fetchUrlText(healthUrl, 2_000); + + if (responseText) { + try { + const data = JSON.parse(responseText); + // Identity check: /api/info must return { version: string } + if (typeof data.version === 'string') { + return gitNexusPort; + } + } catch { + // Retry — response may be incomplete or not yet the API server + } + } } if (gitNexusServerProcess?.exitCode !== null && gitNexusServerProcess?.exitCode !== undefined) { @@ -337,8 +349,8 @@ const waitForGitNexusServerReady = async (): Promise => { const output = gitNexusServerOutput.trim(); throw new Error( output - ? `Timed out waiting for GitNexus backend at ${GITNEXUS_SERVER_HEALTH_URLS.join(', ')}.\n\n${output}` - : `Timed out waiting for GitNexus backend at ${GITNEXUS_SERVER_HEALTH_URLS.join(', ')}.`, + ? `Timed out waiting for GitNexus backend.\n\n${output}` + : 'Timed out waiting for GitNexus backend.', ); }; @@ -687,7 +699,7 @@ const showStartupError = (error: unknown): void => { if (isAddressInUseError(error)) { dialog.showErrorBox( 'GitNexus Desktop Startup Failed', - `Port ${GITNEXUS_PORT} is already in use.\n\nStop the other process using port ${GITNEXUS_PORT} and try again.`, + 'Another process is already using the port GitNexus needs.\n\nClose the other application and try again.', ); return; } @@ -698,29 +710,40 @@ const showStartupError = (error: unknown): void => { ); }; -const ensureGitNexusServerStarted = async (): Promise => { +const ensureGitNexusServerStarted = async (): Promise => { if (!serverReadyPromise) { serverReadyPromise = (async () => { - if (await isAnyHttpUrlReady(GITNEXUS_SERVER_HEALTH_URLS)) { - return; + if (gitNexusPort > 0) { + const healthUrl = `http://${GITNEXUS_HOST}:${gitNexusPort}/api/info`; + const responseText = await fetchUrlText(healthUrl, 2_000); + if (responseText) { + try { + const data = JSON.parse(responseText); + if (typeof data.version === 'string') { + return gitNexusPort; + } + } catch {} + } } if (!gitNexusServerProcess || gitNexusServerProcess.exitCode !== null) { + gitNexusPort = 0; gitNexusServerOutput = ''; gitNexusServerProcess = spawnGitNexusServer(); } - await waitForGitNexusServerReady(); + return await waitForGitNexusServerReady(); })().catch((error) => { serverReadyPromise = null; throw error; }); } - await serverReadyPromise; + return serverReadyPromise; }; async function createWindow(): Promise { + const serverPort = await ensureGitNexusServerStarted(); let embeddedAppUrl: string; if (app.isPackaged) { @@ -730,6 +753,11 @@ async function createWindow(): Promise { embeddedAppUrl = GITNEXUS_WEB_DEV_URL; } + // Pass server URL to web UI via query param so it auto-connects (App.tsx reads ?server=) + const serverUrl = `http://${GITNEXUS_HOST}:${serverPort}`; + const separator = embeddedAppUrl.includes('?') ? '&' : '?'; + embeddedAppUrl += `${separator}server=${encodeURIComponent(serverUrl)}`; + const window = new BrowserWindow({ width: 1280, height: 800, @@ -803,7 +831,6 @@ async function createWindow(): Promise { app.whenReady().then(async () => { try { registerWindowIpcHandlers(); - await ensureGitNexusServerStarted(); await createWindow(); } catch (error) { showStartupError(error); @@ -812,12 +839,10 @@ app.whenReady().then(async () => { app.on('activate', () => { if (openWindows.size === 0) { - void ensureGitNexusServerStarted() - .then(() => createWindow()) - .catch((error) => { - showStartupError(error); - exitStartupFailure(); - }); + void createWindow().catch((error) => { + showStartupError(error); + exitStartupFailure(); + }); } }); }); diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index d90e63e62..3cbe3e455 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -1865,8 +1865,11 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => // to the caller instead of crashing with an unhandled 'error' event. await new Promise((resolve, reject) => { const server = app.listen(port, host, () => { + const addr = server.address(); + const actualPort: number = addr && typeof addr !== 'string' ? addr.port : port; const displayHost = host === '::' || host === '0.0.0.0' ? 'localhost' : host; - console.log(`GitNexus server running on http://${displayHost}:${port}`); + console.log(`GITNEXUS_PORT=${actualPort}`); + console.log(`GitNexus server running on http://${displayHost}:${actualPort}`); resolve(); }); server.on('error', (err) => reject(err));