93 lines
2.7 KiB
Markdown
93 lines
2.7 KiB
Markdown
## Terminal unmount race cleanup
|
|
|
|
Prevent Ghostty Terminal/WebSocket leaks when unmounting mid-init
|
|
|
|
---
|
|
|
|
### Summary
|
|
|
|
`packages/app/src/components/terminal.tsx` initializes Ghostty in `onMount` via async steps (`import("ghostty-web")`, `Ghostty.load()`, WebSocket creation, terminal creation, listeners). If the component unmounts while awaits are pending, `onCleanup` runs before `ws`/`term` exist. The async init can then continue and create resources that never get disposed.
|
|
|
|
This spec makes initialization abortable and ensures resources created after unmount are immediately cleaned up.
|
|
|
|
---
|
|
|
|
### Scoped files (parallel-safe)
|
|
|
|
- `packages/app/src/components/terminal.tsx`
|
|
|
|
---
|
|
|
|
### Goals
|
|
|
|
- Never leave a WebSocket open after the terminal component unmounts
|
|
- Never leave window/container/textarea event listeners attached after unmount
|
|
- Avoid creating terminal resources if `disposed` is already true
|
|
|
|
---
|
|
|
|
### Non-goals
|
|
|
|
- Reworking terminal buffering/persistence format
|
|
- Changing PTY server protocol
|
|
|
|
---
|
|
|
|
### Current state
|
|
|
|
- `disposed` is checked in some WebSocket event handlers, but not during async init.
|
|
- `onCleanup` closes/disposes only the resources already assigned at cleanup time.
|
|
|
|
---
|
|
|
|
### Proposed approach
|
|
|
|
1. Guard async init steps
|
|
|
|
- After each `await`, check `disposed` and return early.
|
|
|
|
2. Register cleanups as resources are created
|
|
|
|
- Maintain an array of cleanup callbacks (`cleanups: VoidFunction[]`).
|
|
- When creating `socket`, `term`, adding event listeners, etc., push the corresponding cleanup.
|
|
- In `onCleanup`, run all registered cleanups exactly once.
|
|
|
|
3. Avoid mutating shared vars until safe
|
|
|
|
- Prefer local variables inside `run()` and assign to outer `ws`/`term` only after confirming not disposed.
|
|
|
|
---
|
|
|
|
### Implementation steps
|
|
|
|
1. Add `const cleanups: VoidFunction[] = []` and `const cleanup = () => { ... }` in component scope
|
|
|
|
2. In `onCleanup`, set `disposed = true` and call `cleanup()`
|
|
|
|
3. In `run()`:
|
|
|
|
- `await import(...)` -> if disposed return
|
|
- `await Ghostty.load()` -> if disposed return
|
|
- create WebSocket -> if disposed, close it and return
|
|
- create Terminal -> if disposed, dispose + close socket and return
|
|
- when adding listeners, register removers in `cleanups`
|
|
|
|
4. Ensure `cleanup()` is idempotent
|
|
|
|
---
|
|
|
|
### Acceptance criteria
|
|
|
|
- Rapidly mounting/unmounting terminal components does not leave open WebSockets
|
|
- No `resize` listeners remain after unmount
|
|
- No errors are thrown if unmount occurs mid-initialization
|
|
|
|
---
|
|
|
|
### Validation plan
|
|
|
|
- Manual:
|
|
- Open a session and rapidly switch sessions/tabs to force terminal unmount/mount
|
|
- Verify via devtools that no orphan WebSocket connections remain
|
|
- Verify that terminal continues to work normally when kept mounted
|