Skip to content

Fix gestures#321

Open
Shashank-Mengani wants to merge 5 commits intoAOSSIE-Org:mainfrom
Shashank-Mengani:fix-gestures
Open

Fix gestures#321
Shashank-Mengani wants to merge 5 commits intoAOSSIE-Org:mainfrom
Shashank-Mengani:fix-gestures

Conversation

@Shashank-Mengani
Copy link

@Shashank-Mengani Shashank-Mengani commented Mar 19, 2026

Addressed Issues:

Fixes #312

Description

This PR fixes an issue where the Electron app could hang indefinitely if the Nitro server failed to start.

Previously, the waitForServer function retried indefinitely without any limit. If the Nitro server crashed or failed during startup, Electron would remain stuck waiting forever.

This PR introduces:

  • A retry limit for waitForServer to prevent infinite waiting
  • Early detection of Nitro process crashes using the exit event
  • Graceful shutdown handling via SIGINT and SIGTERM signals
  • Improved cleanup to prevent orphaned server processes

These changes ensure the app fails fast and exits cleanly instead of hanging.

Screenshots/Recordings:

N/A (no UI changes)

Functional Verification

Screen Mirror

  • Screen Mirror works.

Authentication

  • Connection doesn't work without a valid token.

Basic Gestures

  • One-finger tap: Verified as Left Click.
  • Two-finger tap: Verified as Right Click.
  • Click and drag: Verified selection behavior.
  • Pinch to zoom: Verified zoom functionality (if applicable).

Modes & Settings

  • Cursor mode: Cursor moves smoothly and accurately.
  • Scroll mode: Page scrolls as expected.
  • Sensitivity: Verified changes in cursor speed/sensitivity settings.
  • Copy and Paste: Verified both Copy and Paste functionality.
  • Invert Scrolling: Verified scroll direction toggles correctly.

Advanced Input

  • Key combinations: Verified "hold" behavior for modifiers (e.g., Ctrl+C) and held keys are shown in buffer.
  • Keyboard input: Verified Space, Backspace, and Enter keys work correctly.
  • Glide typing: Verified path drawing and text output.
  • Voice input: Verified speech-to-text functionality for full sentences.
  • Backspace doesn't send the previous input.

Any other gesture or input behavior introduced:

  • New Gestures: Verified any other gesture or input behavior introduced in this PR.

Additional Notes:

  • This change is limited to Electron main process startup and shutdown handling.
  • No impact on UI, gestures, or input systems.
  • Functional verification sections above are not applicable to this backend fix.

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the and I will share a link to this PR with the project maintainers there
  • I have read the contributing guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • Incase of UI change I've added a demo video.

⚠️ AI Notice - Important!
This PR was created with the assistance of AI tools. All changes have been reviewed, tested, and verified to ensure correctness and alignment with project requirements.

⚠️ AI Notice - Important!
We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

  • Bug Fixes
    • More reliable server startup with retries, request timeouts, and fail-fast detection when the server won't become ready.
    • Improved shutdown behavior: background server processes are stopped gracefully and the app exits cleanly on close or termination signals.
    • Fixed trackpad gesture state handling to prevent stuck gestures while preserving expected dragging timeout behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Walkthrough

Adds bounded server readiness polling, idempotent/centralized server shutdown, fail-fast server startup, SIGINT/SIGTERM shutdown handlers, startup try/catch with clean quits, and introduces a gesture reset helper that clears gesture refs and conditionally preserves the dragging timeout.

Changes

Cohort / File(s) Summary
Server startup & shutdown
electron/main.cjs
Added idempotent stopServer(); replaced waitForServer(url) with waitForServer(url, maxRetries = 20, delay = 500) that retries with per-request timeouts and rejects after max retries; made startServer() return a Promise that fails fast on spawn error or early exit and resolves after readiness; added shutdown() and registered SIGINT/SIGTERM; wrapped startup in try/catch; changed window-all-closed to call stopServer() instead of directly killing the process; logging/readiness gating adjusted to require HTTP 200.
Trackpad gesture state
src/hooks/useTrackpadGesture.ts
Introduced resetGestureState(preserveDraggingTimeout?: boolean) to clear gesture refs (ongoingTouches, moved, releasedCount, dragging, lastPinchDist, pinching) and optionally preserve draggingTimeout; updated handleTouchEnd to call this helper when all fingers lift; formatting/comment cleanup and minor emission refactor for pinch/zoom.

Sequence Diagram(s)

sequenceDiagram
    participant ElectronApp as Electron App
    participant Starter as startServer()
    participant ServerProc as Server Process
    participant Poll as waitForServer()
    participant WebServer as Nitro HTTP Server
    participant OS as OS Signals

    ElectronApp->>Starter: invoke startServer()
    Starter->>ServerProc: spawn server process
    ServerProc-->>Starter: spawn error / early exit (optional)
    alt spawn error / early exit
        Starter->>Starter: reject Promise / call stopServer()
        Starter-->>ElectronApp: startup fails
    else spawned
        Starter->>Poll: begin polling (maxRetries, per-request timeout)
        Poll->>WebServer: HTTP GET /health
        alt 200 OK
            WebServer-->>Poll: 200
            Poll-->>Starter: resolve
            Starter-->>ElectronApp: startServer resolves
            ElectronApp->>ElectronApp: createWindow()
        else non-200 / timeout (retries)
            WebServer-->>Poll: non-200 / no response
            Poll-->>Poll: retry until maxRetries
            Poll-->>Starter: reject after maxRetries
            Starter->>Starter: call stopServer()
            Starter-->>ElectronApp: startup fails
        end
    end

    OS-->>ElectronApp: SIGINT / SIGTERM
    ElectronApp->>Starter: shutdown()
    Starter->>ServerProc: stopServer() (SIGTERM, kill fallback)
    Starter->>ElectronApp: app.quit()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Typescript Lang

Poem

🐰 I hopped in quick to mend the wait,
Retries now count and exits abate,
Signals heard, the server sleeps neat,
Gestures reset with tidy feet,
Hooray — the app and server meet!

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fix gestures' is vague and does not clearly convey the actual changes, which focus on Electron server startup/shutdown handling rather than gesture fixes. Revise the title to reflect the actual primary change, such as 'Fix Electron app hanging on Nitro server startup failure' or 'Add retry limit and graceful shutdown to Electron startup'.
Out of Scope Changes check ❓ Inconclusive The PR summary shows changes to useTrackpadGesture.ts, which appears unrelated to the stated objective of fixing Electron server startup issues in electron/main.cjs.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The PR description addresses the linked issue #312, explains the problem and fixes clearly, includes a checklist, and notes that functional verification is not applicable to this backend fix.
Linked Issues check ✅ Passed All objectives from issue #312 are met: waitForServer now has a maxRetries limit, SIGINT/SIGTERM handlers are added, and Nitro process exit events are detected for early crash handling [#312].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/main.cjs`:
- Around line 129-137: Create a single helper (e.g., ensureServerStopped or
killServerProcess) that encapsulates the child-teardown logic currently in
shutdown (checking serverProcess and calling serverProcess.kill('SIGTERM') and
clearing/setting serverProcess to null), then replace the inline kill logic in
shutdown(), the 'window-all-closed' handler, and the catch block after
waitForServer() so each path calls this helper before calling app.quit(); also
ensure the helper is idempotent and safe to call if serverProcess is already
null or has exited.
- Around line 89-103: The startup Promise only listens for
serverProcess.once('exit') but not serverProcess.once('error'), so spawn
failures bypass the Promise and crash the main process; add a
serverProcess.once('error', (err) => { if (!resolved) reject(new Error(...
include err.message or err))); } handler (paired with the existing exit handler
and the resolved flag) so errors from spawn/fork/signup are routed to reject,
and ensure any existing waitForServer().catch(reject) remains unchanged; target
the serverProcess variable and the resolved/resolve/reject logic around
waitForServer to implement this.
- Around line 36-46: The readiness probe in function check uses http.get(url,
...) without a timeout, so a hung TCP connection can block retries; update the
check logic that calls http.get to set a per-request timeout (e.g., via
request.setTimeout or passing a timeout option), listen for the 'timeout' event
and abort/destroy the request/response, then call retry() (and emit an error
path if needed) so the retry cap advances; ensure you also wire the 'error' and
'response' callbacks to properly clear any timers and avoid leaks when using the
http.get/request object and keep references to url, retry, and check to locate
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 30cfd4ba-7c52-4bc8-863a-4c78956ab775

📥 Commits

Reviewing files that changed from the base of the PR and between 25b2382 and 3823d2d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • electron/main.cjs
  • src/hooks/useTrackpadGesture.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useTrackpadGesture.ts (1)

30-31: 🧹 Nitpick | 🔵 Trivial

Consider using explicit typing for the send callback.

The send parameter is typed as (msg: unknown) => void. For better type safety, consider importing and using the InputMessage interface from src/server/InputHandler.ts.

♻️ Suggested improvement
+import type { InputMessage } from "../server/InputHandler"
+
 export const useTrackpadGesture = (
-	send: (msg: unknown) => void,
+	send: (msg: InputMessage) => void,
 	scrollMode: boolean,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useTrackpadGesture.ts` around lines 30 - 31, The send parameter in
useTrackpadGesture is currently typed as (msg: unknown) => void; update it to
use the concrete InputMessage type for stronger typing: import the InputMessage
interface and change the signature to send: (msg: InputMessage) => void (and
update any call sites or generics accordingly) so useTrackpadGesture and its
callers compile with the stricter type; reference: useTrackpadGesture, send,
InputMessage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/main.cjs`:
- Around line 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.
- Around line 31-39: stopServer currently sends SIGTERM and immediately sets
serverProcess = null, losing the handle to observe exit or escalate; change
stopServer to keep serverProcess until it emits 'exit' or is force-killed: call
serverProcess.kill('SIGTERM'), attach an 'exit' and 'error' listener on
serverProcess that clears a 5-second timeout, removes listeners and then sets
serverProcess = null, and start a timer (e.g., 5000ms) that if reached will call
serverProcess.kill('SIGKILL') and then cleanup; ensure listeners are removed in
both normal and forced paths and that errors from kill() are caught/logged to
avoid throwing.

---

Outside diff comments:
In `@src/hooks/useTrackpadGesture.ts`:
- Around line 30-31: The send parameter in useTrackpadGesture is currently typed
as (msg: unknown) => void; update it to use the concrete InputMessage type for
stronger typing: import the InputMessage interface and change the signature to
send: (msg: InputMessage) => void (and update any call sites or generics
accordingly) so useTrackpadGesture and its callers compile with the stricter
type; reference: useTrackpadGesture, send, InputMessage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78e20436-b59a-4453-a0bf-018db0a7eaf9

📥 Commits

Reviewing files that changed from the base of the PR and between 3823d2d and 9e4e778.

📒 Files selected for processing (2)
  • electron/main.cjs
  • src/hooks/useTrackpadGesture.ts

Comment on lines +31 to +39
function stopServer() {
if (serverProcess) {
try {
serverProcess.kill('SIGTERM');
} catch (e) {
console.warn('Error stopping server:', e.message);
}
serverProcess = null;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 19, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In current Node.js, does child_process.ChildProcess.kill('SIGTERM') wait for the child to exit, or does it only send the signal and return immediately? Also, what does a false return value from kill() mean?

💡 Result:

In current Node.js, child_process.ChildProcess.kill('SIGTERM') sends the SIGTERM signal to the child process and returns immediately without waiting for the child to exit. The child process may handle the signal, exit gracefully, or ignore it depending on its implementation. A false return value from kill means the underlying kill(2) system call failed (e.g., the process no longer exists, invalid signal on the platform, or permission denied). It does not throw an error synchronously but may emit an 'error' event asynchronously if the signal could not be delivered.

Citations:


Wait for child process exit before clearing the reference.

stopServer() sends SIGTERM and immediately nulls serverProcess, but kill('SIGTERM') returns instantly without waiting for the child to exit. If Nitro ignores the signal or takes time to shut down, the reference is lost and cannot be used to observe exit or escalate to SIGKILL. This allows the child process to be orphaned even though you intend to terminate it.

Keep the reference, listen for the 'exit' event, then apply a timeout-based force-kill if the child doesn't terminate within a reasonable window (e.g., 5 seconds).

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

In `@electron/main.cjs` around lines 31 - 39, stopServer currently sends SIGTERM
and immediately sets serverProcess = null, losing the handle to observe exit or
escalate; change stopServer to keep serverProcess until it emits 'exit' or is
force-killed: call serverProcess.kill('SIGTERM'), attach an 'exit' and 'error'
listener on serverProcess that clears a 5-second timeout, removes listeners and
then sets serverProcess = null, and start a timer (e.g., 5000ms) that if reached
will call serverProcess.kill('SIGKILL') and then cleanup; ensure listeners are
removed in both normal and forced paths and that errors from kill() are
caught/logged to avoid throwing.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the detailed review!

I've addressed all the reported issues:

  • Added a timeout to the HTTP readiness probe in waitForServer to prevent hanging requests when the server accepts a connection but does not respond.
  • Handled serverProcess.once('error') to properly catch spawn failures and avoid unhandled crashes.
  • Introduced a centralized stopServer() helper to encapsulate teardown logic and ensure idempotent cleanup.
  • Replaced duplicate serverProcess.kill calls across shutdown, window-all-closed, and startup failure paths with the new helper.
  • Ensured the server process is properly terminated on startup failure to avoid orphan processes.

These changes make the startup flow more robust and prevent edge-case hangs and resource leaks.

Please let me know if anything else needs improvement

Copy link
Contributor

Choose a reason for hiding this comment

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

@Shashank-Mengani, thank you for the detailed summary — the improvements you've made are solid. However, the core concern from the original review comment is still unaddressed in the current stopServer():

function stopServer() {
  if (serverProcess) {
    try {
      serverProcess.kill('SIGTERM');
    } catch (e) {
      console.warn('Error stopping server:', e.message);
    }
    serverProcess = null;  // ← reference lost immediately
  }
}

kill('SIGTERM') returns instantly — it only sends the signal. If Nitro takes time to exit (or ignores SIGTERM), the reference is gone and you can no longer observe the exit or escalate to SIGKILL, risking an orphaned process.

Here is the recommended fix — wait for 'exit', and force-kill after a timeout:

function stopServer() {
  if (!serverProcess) return;

  const proc = serverProcess;
  serverProcess = null; // prevent re-entry

  let cleanedUp = false;
  const cleanup = () => {
    if (cleanedUp) return;
    cleanedUp = true;
    proc.removeAllListeners('exit');
    proc.removeAllListeners('error');
    clearTimeout(forceKillTimer);
  };

  const forceKillTimer = setTimeout(() => {
    if (!cleanedUp) {
      console.warn('Server did not exit in time, sending SIGKILL');
      try {
        proc.kill('SIGKILL');
      } catch (e) {
        console.warn('Error force-killing server:', e.message);
      }
      cleanup();
    }
  }, 5000);

  proc.once('exit', () => {
    console.log('Server process exited cleanly');
    cleanup();
  });

  proc.once('error', (e) => {
    console.warn('Error from server process during shutdown:', e.message);
    cleanup();
  });

  try {
    proc.kill('SIGTERM');
  } catch (e) {
    console.warn('Error sending SIGTERM:', e.message);
    cleanup();
  }
}

Key points:

  • serverProcess is nulled at entry to prevent re-entrant calls (idempotency preserved).
  • A local proc holds the reference so it can be used in listeners and the force-kill timer.
  • If the process doesn't exit within 5 seconds, SIGKILL is sent.
  • All listeners and the timer are cleaned up in both normal and forced exit paths.

Comment on lines 97 to 104
serverProcess = spawn('node', [serverPath], {
stdio: 'ignore', // no terminal
windowsHide: true, // hide CMD
stdio: 'ignore',
windowsHide: true,
env: {
...process.env,
HOST: serverHost,
PORT: serverPort.toString(),
},
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
electron/main.cjs (2)

31-39: ⚠️ Potential issue | 🟠 Major

Reference cleared before confirming child exit—process may be orphaned.

kill('SIGTERM') sends the signal and returns immediately without waiting for the child to exit. Setting serverProcess = null right after loses the handle needed to observe the 'exit' event or escalate to SIGKILL if Nitro doesn't terminate promptly.

Consider keeping the reference until exit is confirmed or a timeout elapses:

Suggested improvement
+const KILL_TIMEOUT_MS = 5000;
+
 function stopServer() {
-  if (serverProcess) {
+  if (!serverProcess) return;
+
+  const proc = serverProcess;
+  serverProcess = null; // Prevent re-entry
+
+  const forceKill = setTimeout(() => {
     try {
-      serverProcess.kill('SIGTERM');
+      proc.kill('SIGKILL');
+      console.warn('Force-killed server after timeout');
     } catch (e) {
-      console.warn('Error stopping server:', e.message);
+      // Already dead or inaccessible
     }
-    serverProcess = null;
-  }
+  }, KILL_TIMEOUT_MS);
+
+  proc.once('exit', () => clearTimeout(forceKill));
+
+  try {
+    proc.kill('SIGTERM');
+  } catch (e) {
+    clearTimeout(forceKill);
+    console.warn('Error stopping server:', e.message);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.cjs` around lines 31 - 39, stopServer currently sends SIGTERM
to serverProcess and immediately clears serverProcess, which can orphan the
child; update stopServer to retain the serverProcess reference until the child
emits the 'exit' event or a configurable timeout elapses, attach an 'exit'
listener to serverProcess that clears the reference and optionally escalates to
kill('SIGKILL') if the timeout passes, and ensure any errors from kill are
handled in the existing try/catch; reference functions/variables: stopServer,
serverProcess, the 'exit' event, SIGTERM, SIGKILL, and the timeout/escalation
logic.

130-137: ⚠️ Potential issue | 🟠 Major

Hardcoded localhost may not match the configured server host.

The probe uses http://localhost:${serverPort}, but the server binds to serverHost. If serverHost is a specific non-loopback interface (e.g., 192.168.1.10), the probe will never reach the server and exhaust retries even though Nitro is healthy.

Derive the connect URL based on the bind address:

Suggested fix
+function getConnectUrl() {
+  // Map wildcard binds to loopback for probing
+  const connectHost =
+    serverHost === '0.0.0.0' ? '127.0.0.1' :
+    serverHost === '::' ? '[::1]' :
+    serverHost;
+  return `http://${connectHost}:${serverPort}`;
+}
+
 ...
-    waitForServer(`http://localhost:${serverPort}`)
+    waitForServer(getConnectUrl())

Also update mainWindow.loadURL() at Line 151 to use the same helper.

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

In `@electron/main.cjs` around lines 130 - 137, The probe currently calls
waitForServer(`http://localhost:${serverPort}`) which fails when the server
binds to serverHost; change the code to derive the connect URL from serverHost
and serverPort (e.g., use a helper getConnectUrl(serverHost, serverPort) that
maps '0.0.0.0'/'::' to 'localhost' but otherwise uses the actual bind address)
and pass that URL into waitForServer instead of the hardcoded localhost; also
update the mainWindow.loadURL(...) call that currently uses localhost to use the
same getConnectUrl(serverHost, serverPort) helper so both probe and window
target the correct host.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/main.cjs`:
- Around line 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.

---

Duplicate comments:
In `@electron/main.cjs`:
- Around line 31-39: stopServer currently sends SIGTERM to serverProcess and
immediately clears serverProcess, which can orphan the child; update stopServer
to retain the serverProcess reference until the child emits the 'exit' event or
a configurable timeout elapses, attach an 'exit' listener to serverProcess that
clears the reference and optionally escalates to kill('SIGKILL') if the timeout
passes, and ensure any errors from kill are handled in the existing try/catch;
reference functions/variables: stopServer, serverProcess, the 'exit' event,
SIGTERM, SIGKILL, and the timeout/escalation logic.
- Around line 130-137: The probe currently calls
waitForServer(`http://localhost:${serverPort}`) which fails when the server
binds to serverHost; change the code to derive the connect URL from serverHost
and serverPort (e.g., use a helper getConnectUrl(serverHost, serverPort) that
maps '0.0.0.0'/'::' to 'localhost' but otherwise uses the actual bind address)
and pass that URL into waitForServer instead of the hardcoded localhost; also
update the mainWindow.loadURL(...) call that currently uses localhost to use the
same getConnectUrl(serverHost, serverPort) helper so both probe and window
target the correct host.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 55a9c298-1191-4c29-9f0b-b868fa20a64e

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4e778 and b7d2dbb.

📒 Files selected for processing (1)
  • electron/main.cjs

Comment on lines 47 to 65
const check = () => {
http
.get(url, () => resolve())
.on('error', () => setTimeout(check, 500));
const req = http
.get(url, (res) => {
res.resume();

if (res.statusCode === 200) {
console.log('Server is ready');
resolve();
} else {
retry();
}
})
.on('error', retry);

// Add timeout to prevent hanging
req.setTimeout(delay, () => {
req.destroy(new Error('Request timeout'));
});
};
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Electron hangs forever if Nitro server fails to start

1 participant