Skip to content

Harden all clipboard paths against a busy Windows clipboard - #416

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/clipboard-crash-hardening
Jul 28, 2026
Merged

Harden all clipboard paths against a busy Windows clipboard#416
erikdarlingdata merged 1 commit into
devfrom
fix/clipboard-crash-hardening

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

Fixes the crash in #415: clicking Copy to Clipboard in the Robot Advice window while the Windows clipboard was locked by another process (clipboard managers, RDP, Office clipboard sync) threw a COMException (CLIPBRD_E_CANT_OPEN) out of an async void handler and killed the app.

The reported button was one of 15 write sites with the identical unguarded pattern, and two more instances of the same crash class live inside Avalonia itself (DataGrid built-in Ctrl+C and TextBox Copy/Cut/Paste, both verified unguarded against the decompiled 11.3.x binaries). This PR closes all of them.

  • New ClipboardHelper: every clipboard read/write resolves the TopLevel, retries up to 3 times with a 100ms pause while the clipboard is locked, and returns false/null instead of throwing. All 15 write sites and both paste sites in PlanViewer.App now go through it; nothing outside the helper touches the raw clipboard API (grep-verified).
  • DataGridBehaviors.AttachCopyGuard: tunnel-phase Ctrl+C/Ctrl+Insert interception on all four DataGrids (Query Store results, statements, history, settings format grid), mirroring the built-in copy's exact key/modifier gate (platform command modifier, no Shift/Alt) so behavior is identical minus the crash. In-cell editors keep their own Ctrl+C.
  • TextBoxClipboardGuard: app-wide class handlers for TextBox CopyingToClipboard/CuttingToClipboard/PastingFromClipboard, which Avalonia raises before touching the clipboard. Covers every TextBox in the app including ones created later. CanCopy/CanCut/CanPaste gates preserve password-box and read-only semantics.
  • Cut (query editor and all TextBoxes) now only deletes the selection after the text actually reached the clipboard - previously a failed write during Cut would have destroyed the selected text.
  • Failure feedback where there is an affordance: Advice window button and Copy Repro button flash "Clipboard busy - try again", About window MCP copy shows the same, query session status bar keeps its message.
  • CrashLogger + AppDomain.UnhandledException/TaskScheduler.UnobservedTaskException wiring in Program.cs: any future crash-to-desktop leaves a stack trace in %LOCALAPPDATA%\PerformanceStudio\crash.log (1MB cap) instead of requiring Event Viewer archaeology like this report did.

Changes

  • Services/ClipboardHelper.cs (new): never-throwing TrySetTextAsync/TryGetTextAsync with retry
  • Services/TextBoxClipboardGuard.cs (new): global TextBox copy/cut/paste interception, registered in App.axaml.cs
  • Services/CrashLogger.cs (new) + Program.cs: last-resort exception logging
  • Helpers/DataGridBehaviors.cs: new AttachCopyGuard, attached in QueryStoreGridControl, PlanViewerControl, QueryStoreHistoryControl, SettingsWindow
  • Services/AdviceWindowHelper.cs, AboutWindow, MainWindow.*, PlanViewerControl.*, QuerySessionControl.*, QueryStoreGridControl.Selection, QueryStoreHistoryControl: all call sites converted to the helper
  • QueryStoreGridControl.Selection.cs: Copy Row format extracted to FormatRowForClipboard, shared by the context menu and Ctrl+C

Test Plan

  • dotnet build PlanViewer.App: 0 warnings, 0 errors
  • All 288 PlanViewer.Core tests pass
  • Launch smoke test: app starts clean with the global guards registered and stays up
  • Reviewed against decompiled Avalonia 11.3.x: tunnel handler preempts DataGrid's bubble-phase copy; TextBox skips its internal clipboard path when the routed events are handled
  • Manual repro: run the clipboard-holder script below, then click Copy to Clipboard in Robot Advice - expect the button to flash "Clipboard busy - try again" (or a successful copy via retry if the lock clears), not an APPCRASH
  • Same while locked: Ctrl+C on a Query Store grid row, Ctrl+C/X/V in any TextBox, Copy Repro button
  • Normal operation unchanged: copies, cuts, and pastes work everywhere with the clipboard free
hold-clipboard.ps1 (locks the clipboard like RDP/clipboard managers do)
param([int]$Seconds = 30)
$sig = @'
[DllImport("user32.dll", SetLastError = true)]
public static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool CloseClipboard();
'@
$clip = Add-Type -MemberDefinition $sig -Name ClipboardHolder -Namespace Win32 -PassThru
if (-not $clip::OpenClipboard([IntPtr]::Zero)) { Write-Host "Clipboard already held, try again."; exit 1 }
Write-Host "Clipboard LOCKED for $Seconds seconds. Click Copy in the app now."
Start-Sleep -Seconds $Seconds
$clip::CloseClipboard() | Out-Null
Write-Host "Clipboard released."

Closes #415

Generated with Claude Code

The Windows clipboard is a shared resource; when another process holds it,
SetTextAsync throws CLIPBRD_E_CANT_OPEN, and an unguarded await in an async
void handler crashes the app (issue #415, Robot Advice Copy to Clipboard).

- New ClipboardHelper: all reads/writes retry briefly and never throw
- Convert all 15 write sites and both paste sites in PlanViewer.App
- Replace DataGrid's built-in Ctrl+C (unguarded inside Avalonia) with a
  guarded copy on all four grids, mirroring its exact key/modifier gate
- Intercept TextBox Copy/Cut/Paste app-wide via class handlers; Avalonia's
  own implementations are unguarded async void / TimeoutException-only
- Cut (editor and TextBoxes) only deletes the selection after the text
  actually reached the clipboard
- Clipboard-busy feedback on Advice window, Copy Repro, and MCP copy button
- Log AppDomain/TaskScheduler unhandled exceptions to
  LocalApplicationData\PerformanceStudio\crash.log

Fixes #415

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 35d8c4b into dev Jul 28, 2026
3 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/clipboard-crash-hardening branch July 28, 2026 21:33
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.

1 participant