Guard terminal resize/dispose race against xterm.js dimension getters#314795
Guard terminal resize/dispose race against xterm.js dimension getters#314795bryanchen-d merged 1 commit intomainfrom
Conversation
After bumping xterm.js to beta.213 (#313817), three error buckets keep firing the same Cannot read properties of undefined (reading 'dimensions') TypeError on the latest insider builds (1.120.0-insider 79d6cea / 0aed0a9): - aaa283a2 - resize -> _resizeYCallback -> _updatePtyDimensions, triggered by config-change -> setVisible -> _resize. - 4826565b - debounced _debounceResizeX timer firing after the renderer is gone. - 1e83a096 - _onProcessExit -> dispose -> setVisible(false) -> _resize -> _resizeBothCallback. All three reach RenderService.get dimensions() in xterm.js, which dereferences this._renderer.value (undefined post-dispose) and throws synchronously. The optional chaining on rawXterm.dimensions doesn't help because the getter itself throws. The upstream beta.213 fix added isDisposed guards at the OverviewRuler call sites, not inside the getter, so the correct symmetrical fix here is to guard the call sites that VS Code owns: * terminalInstance.ts: bail out of the resize closures and _updatePtyDimensions when the instance is already disposed; defensively wrap the dimensions read in try/catch so a future renderer-dispose race fails silently instead of bubbling as an unhandled error. * terminalResizeDebouncer.ts: bail out of resize/flush and the runWhenWindowIdle / @debounce-scheduled callbacks when the debouncer's store is disposed - the @debounce decorator schedules a setTimeout that isn't tied to the disposable store. Refs #303546, #313817
| // that here so the optional chaining short-circuits to undefined. | ||
| let pixelWidth: number | undefined; | ||
| let pixelHeight: number | undefined; | ||
| try { |
There was a problem hiding this comment.
This just looks like we want to swallow known error instead of actually addressing root cause.
Seems like actual throw is from xterm not vscode.
@bryanchen-d What is the repro step for this dimension error? Is it causing breakage in terminal behavior?
There was a problem hiding this comment.
It is hard to find the repro step. Essentially the error comes from calling term.dimensions after term.dispose(), so user shouldn't see visible breakage since the terminal has been disposed.
I think the try-catch here is no-harm here, but I'd agree we can remove it since we have already check isDisposed above.
There was a problem hiding this comment.
It will be harm if the root cause is somewhere else and this would prevent us from root-causing the issue.
There was a problem hiding this comment.
That's fair, I made a follow up PR to revert that part, can you review?
Follow-up to #314795. The `isDisposed` early-return at the top of `_updatePtyDimensions` (plus the matching guards on the resize closures and `TerminalResizeDebouncer`) already covers all three telemetry buckets that motivated #314795: `aaa283a2`, `4826565b`, `1e83a096`. The disposable store flips `isDisposed` to true before disposing children, so any debounced/idle callback that fires after `TerminalInstance.dispose()` returns early before reaching `rawXterm.dimensions`. The try/catch was added defensively against a hypothetical future race where xterm.js' `RenderService` is disposed independently of the `TerminalInstance` lifecycle. That scenario is not represented in the current telemetry, and swallowing the throw would suppress the signal we'd need to diagnose it. Prefer to surface such failures via telemetry and address them at the source — xterm.js' `Terminal.dimensions` is typed `IRenderDimensions | undefined`, so the getter should honor that contract rather than throw post-dispose. Refs #314795
What
Guards three terminal resize/dispose race paths that throw
Cannot read properties of undefined (reading 'dimensions')from xterm.js'RenderService.get dimensions()after the renderer is disposed.Why
PR #313817 bumped
@xterm/*tobeta.213and resolved theOverviewRulerRendererdispose race (bucket8612c55d, ✅ zero hits on post-fix insider builds). However three sibling buckets keep firing on the latest insiders (1.120.0-insider79d6cea3/0aed0a9b):aaa283a2_resizeYCallbackfrom configChange →setVisible→_resize4826565b_debounceResizeXtimer1e83a096_resizeBothCallbackfrom_onProcessExit→dispose→setVisible(false)→_resizeAll three reach
RenderService.get dimensions()inside xterm.js, which dereferencesthis._renderer.value(undefined post-dispose) and throws synchronously. The optional chaining onrawXterm.dimensions?.css.canvas.widthdoesn't help because the getter itself throws before the?.short-circuits.The upstream
08ae141patch addedisDisposedguards at theOverviewRulercall sites — not inside the getter — so the symmetrical fix here is to guard the call sites VS Code owns.Changes
terminalInstance.tsTerminalResizeDebouncerwhenthis.isDisposed._updatePtyDimensions: early-return when disposed; defensivelytry/catcharoundrawXterm.dimensionsso a future renderer-dispose race fails silently withundefineddimensions instead of bubbling as an unhandled error.terminalResizeDebouncer.tsresize/flush: bail out whenthis._store.isDisposed.runWhenWindowIdlecallbacks: re-check disposed state before invoking the resize callback (the idle callback can fire afterMutableDisposable.clearfor a different timing reason)._debounceResizeX: re-check disposed state. The@debounce(100)decorator schedules asetTimeoutthat is not tied to the disposable store, so it can fire after the terminal/xterm is gone — this is the direct cause of bucket4826565b.How to test
Ctrl+Shift+W) a terminal while resizing the window.terminal.integrated.fontFamily) immediately after killing a terminal.exit) while the panel is mid-resize.dimensionsTypeErrors in DevTools console.aaa283a2,4826565b,1e83a096over the next two insider builds — expect zero new occurrences.Risk
try/catchin_updatePtyDimensionsaccepts a single missed dimension update if the renderer is mid-dispose; this is preferable to throwing and losing the entire reconnect/restore flow.Refs #303546, #313817