Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 76 additions & 18 deletions electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let serverProcess;
let serverHost = '0.0.0.0';
let serverPort = 3000;

// Load config if exists
try {
const configPath = './src/server-config.json';
if (fs.existsSync(configPath)) {
Expand All @@ -27,21 +28,44 @@ if (!gotLock) {
process.exit(0);
}

// Wait until server is ready
function waitForServer(url) {
return new Promise((resolve) => {
// Wait until server is ready (with retry limit)
function waitForServer(url, maxRetries = 20, delay = 500) {
return new Promise((resolve, reject) => {
let retries = 0;

const check = () => {
http
.get(url, () => resolve())
.on('error', () => setTimeout(check, 500));
.get(url, (res) => {
if (res.statusCode === 200) {
console.log('✅ Server is ready');
resolve();
} else {
retry();
}
})
.on('error', retry);
};
Comment on lines 47 to 65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against double settlement when request completes near timeout boundary.

If the response arrives just before the timeout fires, resolve() is called, but the timeout callback can still execute req.destroy(), which emits an 'error' event and invokes retry(). This won't break the Promise (already resolved), but it logs spurious retry messages and schedules unnecessary checks.

Suggested fix
     const check = () => {
+      let done = false;
       const req = http
         .get(url, (res) => {
           res.resume();
 
           if (res.statusCode === 200) {
             console.log('Server is ready');
+            done = true;
             resolve();
           } else {
             retry();
           }
         })
-        .on('error', retry);
+        .on('error', () => {
+          if (!done) retry();
+        });
 
       // Add timeout to prevent hanging
       req.setTimeout(delay, () => {
-        req.destroy(new Error('Request timeout'));
+        if (!done) req.destroy(new Error('Request timeout'));
       });
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.cjs` around lines 47 - 65, The check() function can
double-trigger retry() after resolve() if the timeout fires near completion; add
a local settled flag (e.g., let settled = false) in check(), set settled = true
immediately before calling resolve(), and early-return from the timeout callback
and the retry handler if settled is true; also remove or ignore the 'error'
listener (e.g., removeListener('error', retry) or check settled inside retry)
and clear the request timeout (req.setTimeout(0) or clear stored timer) when
settling to prevent spurious retry logs and extra checks.


const retry = () => {
retries++;
console.log(`Waiting for server... (${retries}/${maxRetries})`);

if (retries >= maxRetries) {
return reject(
new Error(`Server failed to start after ${maxRetries} attempts`)
);
}

setTimeout(check, delay);
};

check();
});
}

// Start Nitro server (production)
// Start Nitro server
function startServer() {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const serverPath = path.join(
process.resourcesPath,
'app.asar.unpacked',
Expand All @@ -50,19 +74,33 @@ function startServer() {
'index.mjs'
);

console.log("Starting server from:", serverPath);
console.log('Starting server from:', serverPath);

serverProcess = spawn('node', [serverPath], {
stdio: 'ignore', // no terminal
windowsHide: true, // hide CMD
stdio: 'ignore',
windowsHide: true,
env: {
...process.env,
HOST: serverHost,
PORT: serverPort.toString(),
},
Comment on lines 97 to 104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a derived connect URL instead of hardcoded localhost.

This startup path binds Nitro to serverHost, but the new probe still hits http://localhost:${serverPort}. Any config that binds Nitro to a specific non-loopback interface or hostname will look "down" here and trip the retry limit even though the server is healthy. Build a shared server URL from a connect host (127.0.0.1/[::1] for wildcard binds, otherwise serverHost) and reuse it here and in createWindow().

Suggested fix
+function getServerUrl() {
+  const connectHost =
+    serverHost === '0.0.0.0'
+      ? '127.0.0.1'
+      : serverHost === '::'
+        ? '[::1]'
+        : serverHost;
+
+  return `http://${connectHost}:${serverPort}`;
+}
+
 ...
-    waitForServer(`http://localhost:${serverPort}`)
+    waitForServer(getServerUrl())

Update mainWindow.loadURL(...) to reuse getServerUrl() as well.

Also applies to: 130-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.cjs` around lines 97 - 104, The probe is using a hardcoded
localhost URL while Nitro is bound to serverHost, causing false "down"
detections; create a single helper (e.g., getServerUrl(serverHost, serverPort))
that derives the connect host (use 127.0.0.1 or [::1] when serverHost is a
wildcard like 0.0.0.0/::, otherwise use serverHost) and returns the full URL,
then replace the probe URL and the mainWindow.loadURL(...) call to use
getServerUrl() and ensure spawn still sets env HOST and PORT from
serverHost/serverPort.

});

waitForServer(`http://localhost:${serverPort}`).then(resolve);
// Detect early crash
let resolved = false;

serverProcess.once('exit', (code) => {
if (!resolved) {
reject(new Error(`Nitro server exited early with code ${code}`));
}
});

waitForServer(`http://localhost:${serverPort}`)
.then(() => {
resolved = true;
resolve();
})
.catch(reject);
});
}

Expand All @@ -78,25 +116,45 @@ function createWindow() {

mainWindow.loadURL(`http://localhost:${serverPort}`);

// Show when ready
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});

// Debug only if needed
mainWindow.webContents.on('did-fail-load', (e, code, desc) => {
console.log("LOAD FAILED:", code, desc);
console.log('LOAD FAILED:', code, desc);
});
}

// Graceful shutdown
function shutdown() {
console.log('Shutting down...');

if (serverProcess) {
serverProcess.kill('SIGTERM');
}

app.quit();
}

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

// App start
app.whenReady().then(async () => {
await startServer();
createWindow();
try {
await startServer();
createWindow();
} catch (err) {
console.error('❌ Failed to start server:', err.message);
app.quit();
}
});

// Cleanup
// Cleanup on window close
app.on('window-all-closed', () => {
if (serverProcess) serverProcess.kill();
if (serverProcess) {
serverProcess.kill('SIGTERM');
}

if (process.platform !== 'darwin') app.quit();
});
Loading
Loading