Skip to content

Harden Desktop error telemetry and diagnostics#1271

Open
Kosinkadink wants to merge 4 commits into
mainfrom
fix/error-telemetry-hardening
Open

Harden Desktop error telemetry and diagnostics#1271
Kosinkadink wants to merge 4 commits into
mainfrom
fix/error-telemetry-hardening

Conversation

@Kosinkadink

Copy link
Copy Markdown
Member

Summary

  • preserve scrubbed, size-bounded traceback and boot context for execution failures, including correct chained and adjacent traceback handling
  • sanitize and bound exception messages, stacks, and flat metadata at the final PostHog and Datadog sinks
  • apply shared process/session event budgets across PostHog and Datadog error forwarding
  • deduplicate nested migration/adoption failures while retaining the canonical failed stage and installation context
  • improve stable error classification for Chromium, Node, Python, missing-module, and JavaScript errors
  • make process termination handling prefer drained close events without hanging on inherited pipes
  • make Desktop self-update failures explicit about operation (check, download, or apply_restart), running/target versions, provider, error source, stack, and Chinese-mirror cohort

Validation

  • pnpm run typecheck
  • pnpm run lint
  • pnpm run build
  • pnpm run test (188 files, 2826 tests)

Closes #1268
Closes #1269

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR standardizes telemetry sanitization and failure tracking, preserves bounded Python traceback context, adds operation-specific updater errors, instruments adoption and migration phases, and improves process termination and port-conflict handling during launches.

Changes

Telemetry and diagnostics

Layer / File(s) Summary
Shared error and scrubbing contracts
src/shared/errorEvent.ts, src/shared/errorBucket.ts, src/shared/piiScrub.ts, src/shared/datadogMirroredEvents.ts, src/shared/*test.ts
Error classification, secret/PII scrubbing, exception-context normalization, traceback truncation, and Datadog diagnostic filtering are expanded.
Sanitized telemetry emission and forwarding
src/main/lib/telemetry.ts, src/main/lib/ipc/registerTelemetryHandlers.ts, src/main/lib/processErrorHandlers.ts, src/renderer/src/lib/rendererBootstrap.ts, src/main/lib/*test.ts
Telemetry capture now reports acceptance, sanitizes exception data, supports nested canonical tracked steps, applies caps, and gates Datadog forwarding on accepted events.
Traceback collection and bounded execution diagnostics
src/main/lib/executionTap.ts, src/main/lib/executionTap.test.ts
Chained traceback parsing preserves one outer error with scrubbed, size-bounded traceback context while distinguishing independent tracebacks.
Operation-specific updater error telemetry
src/main/lib/updater.ts, src/main/lib/updater.test.ts
Updater failures use active operation tracking, stage attribution, deduplication, source context, target versions, and bounded error details.

Adoption, migration, and launch flows

Layer / File(s) Summary
Adoption and migration step instrumentation
src/main/lib/desktopAdopt.ts, src/main/lib/desktopAdopt.test.ts, src/main/lib/ipc/sessionActions/migrate.ts, src/main/lib/standaloneMigration.ts
Adoption and standalone migration phases are wrapped in tracked steps with canonical or contextual error handling.
Process termination and port-conflict handling
src/main/lib/ipc/sessionActions/launch.ts, src/main/lib/ipc/sessionActions/launch.test.ts, src/main/lib/process.ts
Launch monitoring unifies close and exit, propagates cancellation, retries available ports, improves process cleanup and polling, and returns structured port-conflict details.

Sequence Diagram(s)

sequenceDiagram
  participant Launch
  participant ChildProcess
  participant PortManager
  participant ExecutionTap
  Launch->>ChildProcess: spawn ComfyUI process
  Launch->>PortManager: wait for or reserve launch port
  ChildProcess->>ExecutionTap: stream stderr and stdout
  PortManager-->>Launch: report listener or port conflict
  ChildProcess-->>Launch: emit close or exit
  Launch->>Launch: retry port or return structured failure
Loading

Possibly related PRs

Suggested reviewers: christian-byrne, austinmroz, deepme987

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also includes unrelated launch, process, migration, and renderer telemetry refactors beyond #1268/#1269. Split those unrelated refactors into separate PRs or document them as required scope before merging.
✅ Passed checks (1 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #1268 and #1269 with explicit updater operation tracking, scrubbed bounded traceback context, and the requested test coverage.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/error-telemetry-hardening
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/error-telemetry-hardening

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
src/main/lib/updater.ts (1)

408-417: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Track the active updater operation explicitly.

This still derives the operation from cached state, so a revalidation check performed while state is ready is reported as an installer failure; carry the initiating check, download, or apply_restart operation into the event handler instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/updater.ts` around lines 408 - 417, Update the updater event
handling around the stage calculation and emitDesktopUpdateError call to use the
initiating operation captured when the updater action starts, rather than
deriving it from _appUpdateState.kind, _autoDownloadTriggeredFor, or
wasUserInitiated. Propagate the active check, download, or apply_restart
operation into this handler and emit that operation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/lib/executionTap.ts`:
- Around line 68-69: Update EXCEPTION_LINE and the traceback detection logic in
executionTap to recognize final exception lines structurally rather than
requiring class names to end with Error, Exception, Warning, or Interrupt.
Ensure names such as Boom, StopIteration, and ExceptionGroup trigger traceback
flushing through the existing some(...) checks, and add a regression test
covering a non-suffixed exception.

In `@src/main/lib/ipc/sessionActions/launch.test.ts`:
- Around line 81-86: Update the process event test around proc.emit and the
callback assertions to use distinct payloads for the exit and close events, then
assert that the callback is invoked once with the close event’s payload.
Preserve the multiple close emissions to verify the first close result is
preferred over exit and subsequent close events.

In `@src/main/lib/ipc/sessionActions/launch.ts`:
- Around line 823-830: Compute the explicit-port policy once in the shared
launch flow, treating a present portOverride as explicit alongside the --port
launch argument. Reuse that shared value in both conflict checks, including the
branches around the existing late-conflict logic, and remove their duplicate
local declarations while preserving the current portConflict behavior.
- Around line 129-134: Update the finish callback in the launch termination flow
to handle rejected promises returned by callback instead of discarding them with
void callback(...). Preserve the existing single-execution guard and timer
cleanup, and ensure callback failures are caught so they do not produce
unhandled rejections or interrupt lifecycle cleanup.
- Around line 1069-1083: In the port-conflict retry flow surrounding tryLaunch,
await killProcessTree(spawned.proc) before returning tryLaunch(). Keep the
existing port release, reassignment, reservation, and retry behavior unchanged,
ensuring the previous process tree is fully terminated before launching the
replacement.

In `@src/main/lib/processErrorHandlers.ts`:
- Around line 48-52: Update the options passed to mainTelemetry.captureException
in the process error handler to merge scrubbed.context alongside origin, source,
and level. Preserve the existing fields and allow captureException to normalize
the merged context so child/render process details are retained.

In `@src/main/lib/updater.ts`:
- Around line 302-317: Update the standard error-field construction in the
telemetry payload to use the original error value rather than only message.
Reuse errorObject when available, preserving Error.name and structured code for
updater failures, while retaining the existing message fallback when no Error
instance exists.

In `@src/renderer/src/lib/rendererBootstrap.ts`:
- Around line 167-174: Replace the renderer-local claimRendererTelemetryBudget
counter with a shared main-process IPC budget, ensuring each telemetry event is
emitted to Datadog only after main acknowledges acceptance. Update the related
renderer telemetry call sites, including the error path, so all
renderer-originated events use this centralized acceptance flow and cannot
bypass the global cap.

In `@src/shared/errorBucket.ts`:
- Around line 26-40: Update the detail extraction in the error-bucketing logic
around the raw input normalization so plain objects with a string record.message
use that value as their detail. Preserve existing Error and string handling,
allowing serialized errors such as { message: 'ECONNRESET' } to contribute to
raw instead of being classified as unknown.

In `@src/shared/errorEvent.ts`:
- Line 176: Update the missing-module replacement regex in error signature
normalization to preserve only explicitly allowlisted safe module identifiers;
replace non-allowlisted identifiers, including private package names, with <str>
to prevent unbounded grouping cardinality.
- Around line 83-93: Restrict structuredCode in src/shared/errorEvent.ts:83-93
to return only recognized Node, Chromium, or errno codes rather than arbitrary
uppercase tokens, including the nested cause path. In
src/shared/errorEvent.test.ts:102-105, add an uppercase private or
high-cardinality code case and assert it resolves to unknown.

---

Outside diff comments:
In `@src/main/lib/updater.ts`:
- Around line 408-417: Update the updater event handling around the stage
calculation and emitDesktopUpdateError call to use the initiating operation
captured when the updater action starts, rather than deriving it from
_appUpdateState.kind, _autoDownloadTriggeredFor, or wasUserInitiated. Propagate
the active check, download, or apply_restart operation into this handler and
emit that operation unchanged.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 10f0495a-0b35-4d22-b440-fa42c2e85238

📥 Commits

Reviewing files that changed from the base of the PR and between bd1e234 and d297eb0.

📒 Files selected for processing (23)
  • src/main/lib/desktopAdopt.test.ts
  • src/main/lib/desktopAdopt.ts
  • src/main/lib/executionTap.test.ts
  • src/main/lib/executionTap.ts
  • src/main/lib/ipc/registerTelemetryHandlers.test.ts
  • src/main/lib/ipc/registerTelemetryHandlers.ts
  • src/main/lib/ipc/sessionActions/launch.test.ts
  • src/main/lib/ipc/sessionActions/launch.ts
  • src/main/lib/ipc/sessionActions/migrate.ts
  • src/main/lib/processErrorHandlers.ts
  • src/main/lib/standaloneMigration.ts
  • src/main/lib/telemetry.test.ts
  • src/main/lib/telemetry.ts
  • src/main/lib/updater.test.ts
  • src/main/lib/updater.ts
  • src/renderer/src/lib/rendererBootstrap.ts
  • src/shared/datadogMirroredEvents.test.ts
  • src/shared/datadogMirroredEvents.ts
  • src/shared/errorBucket.ts
  • src/shared/errorEvent.test.ts
  • src/shared/errorEvent.ts
  • src/shared/piiScrub.test.ts
  • src/shared/piiScrub.ts

Comment thread src/main/lib/executionTap.ts Outdated
Comment thread src/main/lib/ipc/sessionActions/launch.test.ts Outdated
Comment thread src/main/lib/ipc/sessionActions/launch.ts
Comment thread src/main/lib/ipc/sessionActions/launch.ts Outdated
Comment thread src/main/lib/ipc/sessionActions/launch.ts
Comment thread src/main/lib/updater.ts
Comment thread src/renderer/src/lib/rendererBootstrap.ts
Comment thread src/shared/errorBucket.ts
Comment thread src/shared/errorEvent.ts
Comment thread src/shared/errorEvent.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
src/main/lib/updater.ts (1)

657-713: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the duplicated source logic into a shared variable.

Repeated ternary logic makes maintenance a fright, so share that variable and keep it tight! As per coding guidelines, check for duplicated logic and extract it into shared variables to prevent divergence.

♻️ Proposed refactor
 export function installUpdate(userInitiated = true): void {
+  const updateSource = userInitiated ? 'install_call' : 'startup_install'
   if (isSessionEnding()) {
     // The OS is shutting down / logging off. Spawning the installer now risks
     // it being force-killed mid-write, corrupting the install — the exact
     // failure this whole flow exists to avoid. The staged update stays put and
     // applies on the next launch instead.
     return
   }
   const updater = getAutoUpdater()
   if (!updater) {
     emitDesktopUpdateError('apply_restart', UPDATER_UNAVAILABLE_MESSAGE, {
       userInitiated,
-      source: userInitiated ? 'install_call' : 'startup_install'
+      source: updateSource
     })
     _broadcastToRenderer('app-update:user-action-failed', { message: UPDATER_UNAVAILABLE_MESSAGE })
     return
   }
   emitTelemetry('comfy.desktop.app_update.install_triggered', {
     version: _appUpdateState.version,
     auto_update_setting: isAutoInstallEnabled() ? 'on' : 'off'
   })
   try {
     _activeUpdateOperation = {
       operation: 'apply_restart',
-      source: userInitiated ? 'install_call' : 'startup_install',
+      source: updateSource,
       targetVersion: _appUpdateState.version,
       userInitiated,
       startedAt: Date.now()
     }
     setQuitReason('update-install')
     // macOS Squirrel quirk: if requestSingleInstanceLock is still held by
     // the quitting process, ShipIt swaps the .app bundle correctly but
     // the new Squirrel.Mac process cannot acquire the lock and exits
     // silently — the user sees the app close and nothing relaunches.
     // Electron's own docs call this out: quitAndInstall + single-instance
     // lock is a known footgun on darwin. Releasing the lock immediately
     // before restartAndInstall lets the next process come up cleanly.
     // Windows / Linux update paths don't have this contention and don't
     // need the release.
     if (process.platform === 'darwin') {
       app.releaseSingleInstanceLock()
     }
     // `isSilent: false` shows the NSIS progress window during the install (see
     // `isInstallerUIEnabled` — Windows-only, default on). Forced silent on
     // macOS/Linux, where `isSilent` has no effect anyway.
     updater.restartAndInstall({ isSilent: !isInstallerUIEnabled() })
   } catch (err) {
     _activeUpdateOperation = null
     clearQuitReason()
     emitDesktopUpdateError('apply_restart', err, {
       userInitiated,
-      source: userInitiated ? 'install_call' : 'startup_install'
+      source: updateSource
     })
     _broadcastToRenderer('app-update:user-action-failed', {
       message: err instanceof Error ? err.message : String(err)
     })
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/updater.ts` around lines 657 - 713, In installUpdate, extract
the repeated userInitiated ? 'install_call' : 'startup_install' expression into
one local source variable near the start of the function, then reuse it for
_activeUpdateOperation.source and both emitDesktopUpdateError calls. Preserve
the existing source values and behavior.

Source: Coding guidelines

src/main/lib/telemetry.ts (1)

1013-1016: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated stack-length cap — extract a shared constant. Both sinks truncate error.stack to the same magic 16 * 1024, instead of reusing a shared constant the way ERROR_MESSAGE_MAX is already shared between main and renderer. One divergence away from the two caps quietly drifting.

  • src/main/lib/telemetry.ts#L1013-L1016: replace the inline 16 * 1024 in captureException with a shared ERROR_STACK_MAX constant (defined alongside ERROR_MESSAGE_MAX).
  • src/renderer/src/lib/rendererBootstrap.ts#L536-L539: replace the inline 16 * 1024 in reportRendererError with the same shared ERROR_STACK_MAX constant.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/telemetry.ts` around lines 1013 - 1016, Define a shared
ERROR_STACK_MAX constant alongside ERROR_MESSAGE_MAX, then update
captureException in src/main/lib/telemetry.ts:1013-1016 and reportRendererError
in src/renderer/src/lib/rendererBootstrap.ts:536-539 to use it instead of the
inline 16 * 1024 cap.

Source: Coding guidelines

src/renderer/src/lib/rendererBootstrap.ts (1)

201-212: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

scrubTelemetryContext silently skips array values — Datadog-bound array fields bypass PII scrubbing.

Unlike main-process scrubProperties (which scrubs each string entry of array values too), this renderer-side helper does if (typeof value !== 'string') continue, so any array-shaped context value sails through unscrubbed straight into datadogRum.addAction/addError. Pre-existing gap, but this PR just widened what flows through this exact function (main-process events now round-trip here via onTelemetryActionFromMain), so the blast radius grew. Bring it in line with scrubProperties to keep the two sinks' scrubbing symmetric — no need to let one scrub imp nap while the other one works overtime.

🔒 Proposed fix
 function scrubTelemetryContext(context: TelemetryContext): TelemetryContext {
   let mutated: TelemetryContext | null = null
   for (const key of Object.keys(context)) {
     const value = context[key]
-    if (typeof value !== 'string') continue
-    const cleaned = scrubAll(value)
-    if (cleaned === value) continue
-    if (!mutated) mutated = { ...context }
-    mutated[key] = cleaned
+    if (typeof value === 'string') {
+      const cleaned = scrubAll(value)
+      if (cleaned === value) continue
+      if (!mutated) mutated = { ...context }
+      mutated[key] = cleaned
+    } else if (Array.isArray(value)) {
+      const cleaned = value.map((entry) => (typeof entry === 'string' ? scrubAll(entry) : entry))
+      if (cleaned.every((entry, index) => entry === value[index])) continue
+      if (!mutated) mutated = { ...context }
+      mutated[key] = cleaned
+    }
   }
   return mutated ?? context
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/lib/rendererBootstrap.ts` around lines 201 - 212, Update
scrubTelemetryContext to scrub array-valued fields as well as direct string
values, applying scrubAll to each string element and preserving non-string
elements. Keep the existing copy-on-change behavior and return the original
context when no values require modification, matching scrubProperties’ handling.
♻️ Duplicate comments (1)
src/main/lib/processErrorHandlers.ts (1)

47-59: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

scrubbed.context still doesn't reach PostHog — and now bypasses it entirely for Datadog.

Same gap flagged previously: mainTelemetry.captureException(...) only receives origin/source/level, so exitCode, reason, name, serviceName from child-process-gone/render-process-gone never land in PostHog. Worse, this diff now spreads scrubbed.context into forwardExceptionToRenderer instead — so the same fields that are missing from the "rich" sink partially leak toward the intentionally-thin Datadog sink, inverting the intended sink priority.

🩹 Proposed fix
     const accepted = mainTelemetry.captureException(err, {
+      ...(scrubbed.context || {}),
       origin: 'main-process',
       source: scrubbed.source,
       level: scrubbed.level ?? null
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/processErrorHandlers.ts` around lines 47 - 59, Update the error
handling flow around mainTelemetry.captureException and
forwardExceptionToRenderer so scrubbed.context is included in the PostHog
capture payload, while the renderer/Datadog forwarding payload remains
intentionally limited to origin, source, and level. Remove the context spread
from forwardExceptionToRenderer and preserve the existing scrubbed field
handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/lib/telemetry.ts`:
- Around line 1013-1016: Define a shared ERROR_STACK_MAX constant alongside
ERROR_MESSAGE_MAX, then update captureException in
src/main/lib/telemetry.ts:1013-1016 and reportRendererError in
src/renderer/src/lib/rendererBootstrap.ts:536-539 to use it instead of the
inline 16 * 1024 cap.

In `@src/main/lib/updater.ts`:
- Around line 657-713: In installUpdate, extract the repeated userInitiated ?
'install_call' : 'startup_install' expression into one local source variable
near the start of the function, then reuse it for _activeUpdateOperation.source
and both emitDesktopUpdateError calls. Preserve the existing source values and
behavior.

In `@src/renderer/src/lib/rendererBootstrap.ts`:
- Around line 201-212: Update scrubTelemetryContext to scrub array-valued fields
as well as direct string values, applying scrubAll to each string element and
preserving non-string elements. Keep the existing copy-on-change behavior and
return the original context when no values require modification, matching
scrubProperties’ handling.

---

Duplicate comments:
In `@src/main/lib/processErrorHandlers.ts`:
- Around line 47-59: Update the error handling flow around
mainTelemetry.captureException and forwardExceptionToRenderer so
scrubbed.context is included in the PostHog capture payload, while the
renderer/Datadog forwarding payload remains intentionally limited to origin,
source, and level. Remove the context spread from forwardExceptionToRenderer and
preserve the existing scrubbed field handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d7842d6-ef51-4301-9643-dca862c29c56

📥 Commits

Reviewing files that changed from the base of the PR and between d297eb0 and 12e9726.

📒 Files selected for processing (11)
  • src/main/lib/ipc/registerTelemetryHandlers.test.ts
  • src/main/lib/ipc/registerTelemetryHandlers.ts
  • src/main/lib/ipc/sessionActions/launch.ts
  • src/main/lib/processErrorHandlers.ts
  • src/main/lib/telemetry.test.ts
  • src/main/lib/telemetry.ts
  • src/main/lib/updater.test.ts
  • src/main/lib/updater.ts
  • src/renderer/src/lib/rendererBootstrap.ts
  • src/shared/datadogMirroredEvents.test.ts
  • src/shared/datadogMirroredEvents.ts

@Kosinkadink

Copy link
Copy Markdown
Member Author

Addressed the actionable outside-diff review findings:

  • Active updater operation attribution is authoritative (check, download, or apply_restart) rather than inferred from cached state (12e9726).
  • installUpdate now shares one source value across operation state and both error paths (23502f5).
  • Main and renderer exception stacks use the same shared size cap (23502f5).
  • Renderer telemetry scrubs string elements inside array-valued context while preserving copy-on-change behavior (23502f5).
  • Process context is retained in PostHog, while the intentionally thin Datadog payload excludes free-text diagnostics (23502f5).

Validation: full typecheck, lint, build, and test suites pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/main/lib/ipc/sessionActions/launch.ts (1)

771-809: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract duplicated process exit logic.

The termination handling logic here is identical to the logic in attachExitHandler at lines 1359-1395 (aside from getStderr() vs currentGetStderr()). As per coding guidelines, extract this duplicated logic into a shared helper function to prevent divergence. Don't be a goblin hoarding duplicate code! Extracting it makes the codebase gleam and puts less stress on the team.

♻️ Proposed helper extraction

Define a helper function in handleLaunch (above the skipPortWait block):

  async function handleProcessExited(
    code: number | null, 
    signal: NodeJS.Signals | null, 
    getStderrFn: () => string,
    logStream: WriteStream,
    execTap: ReturnType<typeof createExecutionTap>,
    hwTap: ReturnType<typeof createHardwareTap>
  ) {
    logStream.end()
    const crashed = _runningSessions.has(installationId) && isCrashedExit(code, signal)
    const lastStderr = lastNLines(getStderrFn(), 100)
    execTap.flushSummary()
    hwTap.flushSummary()
    const crashDiagnosis = crashed ? await diagnoseCrash(code) : {}
    _removeSession(installationId)
    const exitedPayload = {
      installationId,
      crashed,
      exitCode: code ?? undefined,
      signal: signal ?? undefined,
      installationName: inst.name,
      lastStderr,
      ...crashDiagnosis
    }
    telemetry.emit('comfy.desktop.comfyui.exited', {
      installation_id: installationId,
      crashed,
      exit_code: code ?? null,
      last_stderr: lastStderr ?? null
    })
    if (crashed) {
      recordCrash(exitedPayload)
      _broadcastToRenderer('instance-crashed', exitedPayload)
    }
    if (!sender.isDestroyed()) {
      sender.send('comfy-exited', exitedPayload)
    }
    if (_onComfyExited) _onComfyExited({ installationId, crashed })
  }

Then replace this block (and the matching block in attachExitHandler at lines 1359-1395) with a single call:

    onProcessTerminated(proc, async (code, signal) => {
      await handleProcessExited(code, signal, getStderr, logStream, execTap, hwTap)
    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/ipc/sessionActions/launch.ts` around lines 771 - 809, Extract
the shared termination handling from the current exit callback and
attachExitHandler into a local async handleProcessExited helper within
handleLaunch, accepting the exit details, stderr getter, log stream, and
execution/hardware taps. Move the common cleanup, crash diagnosis, payload
creation, telemetry, crash recording, renderer notification, and _onComfyExited
calls into that helper, preserving getStderr versus currentGetStderr and ending
the corresponding log stream. Replace both duplicated blocks with calls to
handleProcessExited through onProcessTerminated.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/lib/executionTap.ts`:
- Around line 68-69: Update the EXCEPTION_LINE regular expression in
executionTap.ts to allow uppercase letters in dotted module-name prefixes, while
preserving the existing matching behavior for exception class names and trailing
colon/whitespace.

In `@src/main/lib/process.ts`:
- Around line 91-103: Bound the retry loop in waitForGroupExit by tracking poll
attempts or elapsed time and stopping after a defined maximum, including
persistent EPERM cases. When the limit is reached, call done() so the launch
lifecycle cannot hang indefinitely; preserve the existing polling behavior
before the cap.
- Around line 193-197: In src/main/lib/process.ts lines 193-197, update
waitForPort to use a per-attempt boolean guard so the error and timeout handlers
schedule poll only once, including when req.destroy() emits error synchronously,
and ignore late errors after success. Apply the identical guard in waitForUrl at
src/main/lib/process.ts lines 232-236.

In `@src/main/lib/processErrorHandlers.ts`:
- Around line 54-58: Update the mainTelemetry.forwardExceptionToRenderer call in
process error handling to include scrubbed.context alongside origin, source, and
level, preserving the same merged context already used by captureException so
Datadog receives details such as exitCode and reason.

---

Outside diff comments:
In `@src/main/lib/ipc/sessionActions/launch.ts`:
- Around line 771-809: Extract the shared termination handling from the current
exit callback and attachExitHandler into a local async handleProcessExited
helper within handleLaunch, accepting the exit details, stderr getter, log
stream, and execution/hardware taps. Move the common cleanup, crash diagnosis,
payload creation, telemetry, crash recording, renderer notification, and
_onComfyExited calls into that helper, preserving getStderr versus
currentGetStderr and ending the corresponding log stream. Replace both
duplicated blocks with calls to handleProcessExited through onProcessTerminated.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c223db9c-0f00-45eb-b06e-12400b645bd7

📥 Commits

Reviewing files that changed from the base of the PR and between 12e9726 and 23502f5.

📒 Files selected for processing (15)
  • src/main/lib/executionTap.test.ts
  • src/main/lib/executionTap.ts
  • src/main/lib/ipc/sessionActions/launch.test.ts
  • src/main/lib/ipc/sessionActions/launch.ts
  • src/main/lib/process.ts
  • src/main/lib/processErrorHandlers.ts
  • src/main/lib/telemetry.test.ts
  • src/main/lib/telemetry.ts
  • src/main/lib/updater.test.ts
  • src/main/lib/updater.ts
  • src/renderer/src/lib/rendererBootstrap.ts
  • src/renderer/src/lib/telemetry.ts
  • src/shared/errorBucket.ts
  • src/shared/errorEvent.test.ts
  • src/shared/errorEvent.ts

Comment on lines +68 to +69
const EXCEPTION_LINE =
/^(?:[A-Z_][A-Za-z0-9_]*|(?:[a-z_][A-Za-z0-9_]*\.)+[A-Z_][A-Za-z0-9_]*)(?::(?:\s|$)|$)/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Allow uppercase module names in exception traceback matching.

Like a shy imp, uppercase modules hide from this regex! 😈

Restricting the module prefix matching to [a-z_] means valid exceptions originating from capitalized packages (like PIL.UnidentifiedImageError) will be silently ignored as exception lines. Consider relaxing the module prefix check to allow both upper and lowercase letters.

🛠️ Proposed fix to catch all the exceptions
-  /^(?:[A-Z_][A-Za-z0-9_]*|(?:[a-z_][A-Za-z0-9_]*\.)+[A-Z_][A-Za-z0-9_]*)(?::(?:\s|$)|$)/
+  /^(?:[A-Za-z_][A-Za-z0-9_]*\.)*[A-Z_][A-Za-z0-9_]*(?::(?:\s|$)|$)/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const EXCEPTION_LINE =
/^(?:[A-Z_][A-Za-z0-9_]*|(?:[a-z_][A-Za-z0-9_]*\.)+[A-Z_][A-Za-z0-9_]*)(?::(?:\s|$)|$)/
const EXCEPTION_LINE =
/^(?:[A-Za-z_][A-Za-z0-9_]*\.)*[A-Z_][A-Za-z0-9_]*(?::(?:\s|$)|$)/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/executionTap.ts` around lines 68 - 69, Update the EXCEPTION_LINE
regular expression in executionTap.ts to allow uppercase letters in dotted
module-name prefixes, while preserving the existing matching behavior for
exception class names and trailing colon/whitespace.

Comment thread src/main/lib/process.ts
Comment on lines +91 to +103
const waitForGroupExit = (): void => {
try {
process.kill(processGroup, 0)
setTimeout(waitForGroupExit, 25).unref()
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EPERM') {
setTimeout(waitForGroupExit, 25).unref()
} else {
done()
}
}
}
waitForGroupExit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Bound the process group exit poll.

An unkillable process (e.g., stuck in uninterruptible sleep or returning persistent EPERM) will trap this poll in an infinite loop, permanently hanging the launch lifecycle. You don't want this loop haunting you forever! Cap the poll, take control, and let the launch achieve its goal.

🛡️ Proposed fix to add a retry cap
-    const waitForGroupExit = (): void => {
+    let attempts = 0
+    const waitForGroupExit = (): void => {
+      if (++attempts > 40) return done() // Cap at ~1s
       try {
         process.kill(processGroup, 0)
         setTimeout(waitForGroupExit, 25).unref()
       } catch (err) {
         if ((err as NodeJS.ErrnoException).code === 'EPERM') {
           setTimeout(waitForGroupExit, 25).unref()
         } else {
           done()
         }
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const waitForGroupExit = (): void => {
try {
process.kill(processGroup, 0)
setTimeout(waitForGroupExit, 25).unref()
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EPERM') {
setTimeout(waitForGroupExit, 25).unref()
} else {
done()
}
}
}
waitForGroupExit()
let attempts = 0
const waitForGroupExit = (): void => {
if (++attempts > 40) return done() // Cap at ~1s
try {
process.kill(processGroup, 0)
setTimeout(waitForGroupExit, 25).unref()
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EPERM') {
setTimeout(waitForGroupExit, 25).unref()
} else {
done()
}
}
}
waitForGroupExit()
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, execFile, type ChildProcess } from 'child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/process.ts` around lines 91 - 103, Bound the retry loop in
waitForGroupExit by tracking poll attempts or elapsed time and stopping after a
defined maximum, including persistent EPERM cases. When the limit is reached,
call done() so the launch lifecycle cannot hang indefinitely; preserve the
existing polling behavior before the cap.

Comment thread src/main/lib/process.ts
Comment on lines +193 to 197
req.on('error', () => setTimeout(poll, intervalMs))
req.on('timeout', () => {
req.destroy()
setTimeout(poll, intervalMs)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard polling against double-fire on timeout.

When req.destroy() is called to abort a timed-out request, it synchronously emits an error event. Without an idempotency guard, both the timeout and error handlers schedule poll independently, creating an exponentially multiplying fork bomb of background polling requests. Let's cage that mischievous multiplying beast, before it turns your network traffic into a feast!

  • src/main/lib/process.ts#L193-L197: Use a boolean guard in waitForPort to ensure poll is only scheduled once per attempt and blocks out late errors after success.
  • src/main/lib/process.ts#L232-L236: Apply the identical guard in waitForUrl.
🐛 Proposed fix for both locations
+      let handled = false
+      const retry = () => {
+        if (handled) return
+        handled = true
+        setTimeout(poll, intervalMs)
+      }
       const req = http.get({ host, port, path: '/', timeout: 2000 }, (res) => {
         res.resume()
+        if (handled) return
+        handled = true
         resolve()
       })
 
-      req.on('error', () => setTimeout(poll, intervalMs))
-      req.on('timeout', () => {
-        req.destroy()
-        setTimeout(poll, intervalMs)
-      })
+      req.on('error', retry)
+      req.on('timeout', () => {
+        req.destroy()
+        retry()
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
req.on('error', () => setTimeout(poll, intervalMs))
req.on('timeout', () => {
req.destroy()
setTimeout(poll, intervalMs)
})
let handled = false
const retry = () => {
if (handled) return
handled = true
setTimeout(poll, intervalMs)
}
req.on('error', retry)
req.on('timeout', () => {
req.destroy()
retry()
})
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, execFile, type ChildProcess } from 'child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

📍 Affects 1 file
  • src/main/lib/process.ts#L193-L197 (this comment)
  • src/main/lib/process.ts#L232-L236
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/process.ts` around lines 193 - 197, In src/main/lib/process.ts
lines 193-197, update waitForPort to use a per-attempt boolean guard so the
error and timeout handlers schedule poll only once, including when req.destroy()
emits error synchronously, and ignore late errors after success. Apply the
identical guard in waitForUrl at src/main/lib/process.ts lines 232-236.

Comment on lines +54 to +58
mainTelemetry.forwardExceptionToRenderer({
origin: 'main-process',
source: scrubbed.source,
level: scrubbed.level ?? null
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve scrubbed.context when forwarding to Datadog.

Poof! The context vanishes like a ghost in the night! 👻

While you successfully merged scrubbed.context for PostHog in captureException, Datadog relies on forwardExceptionToRenderer to receive the same details. Omitting it here means Datadog loses crucial context like exitCode and reason for child/renderer process crashes.

✨ Proposed fix to keep the context intact
     if (accepted) {
       mainTelemetry.forwardExceptionToRenderer({
+        ...(scrubbed.context || {}),
         origin: 'main-process',
         source: scrubbed.source,
         level: scrubbed.level ?? null
       })
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mainTelemetry.forwardExceptionToRenderer({
origin: 'main-process',
source: scrubbed.source,
level: scrubbed.level ?? null
})
mainTelemetry.forwardExceptionToRenderer({
...(scrubbed.context || {}),
origin: 'main-process',
source: scrubbed.source,
level: scrubbed.level ?? null
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/lib/processErrorHandlers.ts` around lines 54 - 58, Update the
mainTelemetry.forwardExceptionToRenderer call in process error handling to
include scrubbed.context alongside origin, source, and level, preserving the
same merged context already used by captureException so Datadog receives details
such as exitCode and reason.

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

Labels

None yet

Projects

None yet

1 participant