Skip to content

fix(simulator): stop the emulator on Stop, and close its session on every stop path - #1009

Merged
thiagoralves merged 3 commits into
developmentfrom
bugfix/simulator-stop-and-session
Aug 12, 2026
Merged

fix(simulator): stop the emulator on Stop, and close its session on every stop path#1009
thiagoralves merged 3 commits into
developmentfrom
bugfix/simulator-stop-and-session

Conversation

@thiagoralves

@thiagoralves thiagoralves commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Paired with openplc-web #670 — must land together. workspace-activity-bar/default.tsx is in the Shared Surface Sync gate and changes identically in both. Verified locally: compare-surfaces.py reports match: true across 1029 files with both branches checked out.

Two fixes, one ownership rule: the emulator and the session it serves start and end together.

1. Stop actually stops the emulator — shared surface

The Stop button ended the debug session, logged "Simulator stopped." and left the emulator running.

stopSession() deliberately does not stop it — a debug session is a consumer of the emulator, not its owner, as its own docstring says — and this button, which owns that job, never made the call. So the avr8js loop kept re-scheduling itself and burning a core for the rest of the session, unreachable from the UI because the button had already flipped back to "Start".

Confirmed on web before the fix: simulator.isRunning() still returned true after pressing Stop. The file is byte-identical across repos, so the desktop editor had it too.

Order is now explicit: the debug session lets go of the transport first, then the emulator it was talking to goes away.

2. Every stop path closes the session — main process

Stops were scattered across six sites, and two of them closed nothing:

  • handleWindowReload stopped the emulator but left the session open, so main went on holding a simulator session the reloaded renderer knew nothing about. The reload resets the renderer's store to disconnected, so the two disagreed permanently.
  • handleSimulatorLoadFirmware's catch did neither. loadAndRun marks the emulator running before it finishes wiring, so a throw after that point leaked a running emulator with no session — and no button to reach it, because the renderer never learned it had started.

All six now route through one stopSimulator() choke point: close the session, then stop the emulator.

The load-failure path is the parity fix for openplc-web, where the worker already cleans up and posts 'stopped' when loadAndRun throws, which the web adapter turns into a closed session. The editor's shared SimulatorModule has no self-stop of its own, so the catch is where that signal lives here.

Verification

  • New: src/main/modules/ipc/__tests__/simulator-session.handler.test.ts — 5 cases covering close-then-stop ordering, a non-simulator link being left alone, the reload path, and the throw path. The two covering new behaviour fail without this change (verified by reverting main.ts and re-running).
  • Full suite: 303 suites / 6403 tests pass (was 302 / 6398).
  • tsc --noEmit clean; no new lint warnings; Prettier clean.
  • Shared-surface gate green.
  • The shared handler change was exercised end to end in the web app on a 316-variable project: Start → debugger live → Stop → isRunning() === false, session disconnected, and a subsequent Start works. Same code path, driven over IPC here.

Not covered

A crash inside the emulator's run loop (executeBatch) still has no handler in the editor — web catches the equivalent via worker.onerror. Closing that would mean a try/catch in the byte-identical SimulatorModule, so I left it out of this PR rather than widen the shared surface. Worth a follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved simulator shutdown to stop debug sessions and emulator processes cleanly.
    • Ensured simulator sessions are closed during app exit, window reloads, project changes, and failed startup attempts.
    • Added cleanup for partially initialized simulator state when firmware loading fails.
    • Preserved active non-simulator sessions during simulator shutdown.

…very stop path

Two fixes, one ownership rule: the emulator and the session it serves
start and end together.

1. Stop actually stops the emulator (shared surface)

The Stop button ended the debug session, logged "Simulator stopped." and
left the emulator running. `stopSession()` deliberately does not stop it
(a debug session is a consumer of the emulator, not its owner — see its
docstring), and this button, which owns that job, never made the call.
The avr8js loop kept re-scheduling itself and burning a core for the rest
of the session, unreachable from the UI because the button had already
flipped back to "Start".

`workspace-activity-bar/default.tsx` is byte-identical with openplc-web
and carries the same change there; the two must land together for the
Shared Surface Sync gate.

2. Every stop path closes the session (main process)

Stops were scattered, and two of them closed nothing:

- `handleWindowReload` stopped the emulator but left the session open,
  so main went on holding a simulator session the reloaded renderer knew
  nothing about.
- `handleSimulatorLoadFirmware`'s catch did neither. `loadAndRun` marks
  the emulator running before it finishes wiring, so a throw after that
  point leaked a running emulator with no session — and no button to
  reach it, because the renderer never learned it had started.

All six sites now route through one `stopSimulator()` choke point that
closes the session first, then stops the emulator. The load-failure path
is the parity fix for openplc-web, where the worker already cleans up and
reports 'stopped' when `loadAndRun` throws.

Adds `simulator-session.handler.test.ts`: 5 cases over ordering, the
non-simulator link left alone, the reload path and the throw path. The
two covering new behaviour fail without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thiagoralves

Copy link
Copy Markdown
Contributor Author

Paired with https://github.com/Autonomy-Logic/openplc-web/pull/670 — the shared workspace-activity-bar/default.tsx change is byte-identical in both, so the Shared Surface Sync gate needs them merged together.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Simulator shutdown is centralized in the main process. It closes simulator sessions before stopping emulators across stop, reload, quit, project transition, and startup-failure paths. The frontend and lifecycle tests use the updated behavior.

Changes

Simulator lifecycle cleanup

Layer / File(s) Summary
Centralized simulator cleanup and validation
src/main/modules/ipc/main.ts, src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
The shared cleanup method closes simulator sessions before stopping emulators. Startup failures clean up partially initialized state. Tests cover ordering, reload cleanup, session preservation, and failure handling.
Lifecycle cleanup integration
src/main/modules/ipc/main.ts
App quit, window reload, and project transitions use the shared simulator cleanup method.
Frontend simulator stop handling
src/frontend/components/_organisms/workspace-activity-bar/default.tsx
The activity bar stops the debug session before simulator teardown. The callback clears the device-riding debug-session reference and includes simulator in its dependencies.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceActivityBar
  participant SimulatorSession
  participant Emulator
  WorkspaceActivityBar->>SimulatorSession: request simulator stop
  SimulatorSession->>SimulatorSession: close simulator session
  SimulatorSession->>Emulator: stop emulator
  Emulator-->>WorkspaceActivityBar: complete shutdown
Loading

Possibly related PRs

Poem

A rabbit closes sessions with care,
Then stops the emulator there.
Reloads and failures clear the way,
Debug state hops out of play.
Clean shutdown now rests in place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main simulator shutdown fixes, including stopping the emulator and closing its session on every stop path.
Description check ✅ Passed The description explains the changes, affected paths, testing, verification results, paired PR requirement, and known limitations in sufficient detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/simulator-stop-and-session

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
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/components/_organisms/workspace-activity-bar/default.tsx`:
- Around line 644-654: Wrap the debug cleanup sequence around
debugSession.stopSession() in a try/finally so simulator.stop() always executes,
including when the session cleanup rejects. Keep stopSession() as the first
cleanup attempt, and add a rejection test verifying simulator.stop() is still
called.

In `@src/main/modules/ipc/__tests__/simulator-session.handler.test.ts`:
- Around line 143-146: Update the test around bridge.handleSimulatorLoadFirmware
to mock firmware reading as successful, then configure
simulatorModule.loadAndRun() to throw after startup begins. Assert that the
partial-start cleanup closes the session and stops the emulator, exercising the
handler’s catch path rather than the pre-load failure path.
- Around line 47-71: Update
src/main/modules/ipc/__tests__/simulator-session.handler.test.ts at lines 47-71,
85-115, and 139-146: make createBridge satisfy MainIpcModuleConstructor, replace
the private deviceSession cast in holdLink with typed DeviceSessionManager
spies, and include the required descriptor field in the session fake. Pass a
typed IpcMainInvokeEvent fixture to every handler invocation at the sibling
sites.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65423d4d-0fc3-45f0-8e4d-9ea33972477c

📥 Commits

Reviewing files that changed from the base of the PR and between 32e46b8 and 2d97f47.

📒 Files selected for processing (3)
  • src/frontend/components/_organisms/workspace-activity-bar/default.tsx
  • src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
  • src/main/modules/ipc/main.ts

Comment thread src/frontend/components/_organisms/workspace-activity-bar/default.tsx Outdated
Comment thread src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
Comment thread src/main/modules/ipc/__tests__/simulator-session.handler.test.ts Outdated
Comment thread src/frontend/components/_organisms/workspace-activity-bar/default.tsx Outdated
Comment thread src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
…nt claims

Addresses the review on #1009 / #670.

Stop the emulator from a `finally` (shared surface, mirrors openplc-web).
Sequencing `simulator.stop()` after a plain `await debugSession.stopSession()`
left the emulator running whenever the teardown rejected — control jumped to the
catch, which only logs. Reproduced on web by injecting a rejecting
`debugger.disconnect`: emulator still running, debug panel up on frozen values,
`simulatorRunning` stuck true, every retry failing identically. Nothing settled,
which is worse than the bug this branch fixes. The nearest trigger is a throwing
`onDisconnected` subscriber, which this repo's debugger adapter re-invokes inside
its own catch — the second throw escapes.

Clear `debugSessionRidesDeviceRef` on the manual Stop path (shared surface).
`stopSession()` hides the debugger first, so the drop handler's
`isDebuggerVisible` gate returns before it reaches the reset.

Fix the load-firmware test so it exercises the leak its comment describes. It
threw on `fs.readFile`, which runs BEFORE `loadAndRun` — so the emulator was
never marked running and the assertion passed only because `stopSimulator()` is
unconditional. It would have stayed green if the real post-`loadAndRun` leak
regressed. `fs/promises` is now stubbed so the read succeeds and `loadAndRun`
throws, putting the throw where the leak was; the read-failure case is kept as a
separate test, since the catch cannot tell the two apart. Both fail with the
`stopSimulator()` call removed.

303 suites / 6404 tests pass. The modbus-rtu-client flake and the jest worker
teardown warning both reproduce on a clean `development`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/modules/ipc/__tests__/simulator-session.handler.test.ts`:
- Line 49: Update the readFile mock declaration to use Jest 30’s single
function-type generic, defining a callable signature that accepts the existing
arguments and returns Promise<string>; preserve the current mock behavior and
parameter optionality.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11536381-ffa9-4d14-852d-41a236bb49d0

📥 Commits

Reviewing files that changed from the base of the PR and between 2d97f47 and 885b7da.

📒 Files selected for processing (2)
  • src/frontend/components/_organisms/workspace-activity-bar/default.tsx
  • src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/frontend/components/_organisms/workspace-activity-bar/default.tsx

Comment thread src/main/modules/ipc/__tests__/simulator-session.handler.test.ts
@marconetsf
marconetsf self-requested a review August 12, 2026 19:13
@thiagoralves
thiagoralves merged commit af3da50 into development Aug 12, 2026
21 of 24 checks passed
@thiagoralves
thiagoralves deleted the bugfix/simulator-stop-and-session branch August 12, 2026 19:30
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.

3 participants