Repository navigation
Plan viewer: guard clipboard writes, and hand back the captured query on a truncated single-statement copy - #4593
Merged
Conversation
… on a truncated single-statement copy The Windows clipboard is a shared resource; another process holding it briefly (a clipboard manager, Office, RDP) makes an unguarded write throw and crash the app, the same failure #2833 already fixed for reads. ClipboardText gains a guarded TrySetText, mirroring TryRead's bounded retry, and every Clipboard.SetText/SetDataObject call in the shared plan viewer, blocking chain, deadlock graph, and DataGrid export helpers now routes through it. Copy Query Text on a truncated single-statement plan now hands back the full query the plan was captured from instead of the plan's own 4,000-character showplan-capped copy, matching PerformanceStudio's fix. The guard counts statements the way the Statements grid counts them (descending into stored procedure and UDF bodies), not by outer batch, so a plan captured around EXEC dbo.SomeProc is correctly treated as multi-statement and never hands back the outer EXEC for a truncated body statement. Fixes #4582
…and-truncated-copy # Conflicts: # PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
erikdarlingdata
marked this pull request as ready for review
September 28, 2026 16:05
erikdarlingdata
deleted the
viewer/4582-clipboard-and-truncated-copy
branch
September 28, 2026 16:05
erikdarlingdata
added a commit
that referenced
this pull request
Sep 28, 2026
…dText.TrySetText Merging dev brought in a properties-panel copy path with its own bare-try guard around Clipboard.SetText, added after #4593/#4600 branched. It didn't route through the shared ClipboardText helper, so ClipboardWriteCensusTests failed post-merge. Delegate to ClipboardText.TrySetText instead of duplicating the guard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4582. Part of #4511.
Why
erikdarlingdata/PerformanceStudio@7cd9218 hardened every viewer clipboard write against a busy
Windows clipboard: another process holding it briefly (a clipboard manager, Office, RDP) makes
SetTextAsync/SetTextthrow, and an unguarded call crashes the app. PM already had a guardedclipboard READ (
ClipboardText.TryRead, #2833) but no guarded write, and the plan viewer's CopyQuery Text / Copy Operator Name / Copy Predicate / Copy Seek Predicate entries, plus the blocking
chain, deadlock graph, and DataGrid export copy helpers, all called
Clipboard.SetText/Clipboard.SetDataObjectunguarded.Separately, erikdarlingdata/PerformanceStudio@35249e2 (corrected by @fdd3d22) fixed Copy Query Text
to fall back to the query a plan was captured from when the plan's own copy hit SQL Server's
4,000-character showplan cap — but only for single-statement plans, counted the way the Statements
grid counts them (flattened into stored procedure/UDF bodies), not
Batches.Sum(which stops atthe outer batch and would mis-treat
EXEC dbo.SomeProcas single-statement).What changes
PerformanceMonitor.Ui/ClipboardText.cs: newTrySetText(string), mirroringTryRead'sbounded retry (8 attempts, 25 ms apart) over a swappable
WriteOnceseam so a pin can force theclipboard-open failure without a real busy clipboard.
Clipboard.SetText/SetDataObjectcall inPerformanceMonitor.Uinow routes throughClipboardText.TrySetText:PlanViewerControl.xaml.cs(Copy Query Text),PlanViewerControl.Interaction.cs(Copy Operator Name / Object Name / Predicate / SeekPredicate),
BlockingChainControl.xaml.csandDeadlockGraphControl.xaml.cs(Copy SQL Text),and
DataGridExport.cs(Copy Cell / Copy Row / Copy All Rows — used by ~29 grids across Lite andthe Darling Viewer).
PerformanceMonitor.PlanAnalysis/PlanDisplayText.CopyQueryText(statement, statementCount, capturedQueryText): the pure fallback rule. Returns the captured text only when the statementis truncated (
IsTextTruncated), the plan is single-statement by the caller's already-flattenedcount, and captured text is available; otherwise the plan's own text.
PlanViewerControl: addedCapturedQueryText(set byLoadPlan's existingqueryTextparameter — no host wiring needed, both hosts already pass it where they have it) and
_allStatementsCount(the same flattened countPopulateStatementsGridalready builds), andwired
CopyStatementText_Clickthrough the new helper.Lite: gets this too (shared control)
Yes —
PlanViewerControl,DataGridExport, and both context-menu controls live inPerformanceMonitor.Uiand are shared by Lite and the Darling Viewer. Both hosts' plan-opening callsites (
Lite/Controls/ServerTab.Plans.cs,Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Plans.cs)already pass a
queryTextargument toLoadPlanat most call sites (Query Store stored plans,active-query snapshots), so
CapturedQueryTextis populated there for free. Call sites that open aplan with no stored query text (e.g. a plan opened from a saved
.sqlplanfile, or a cache-fetchedplan with no query text available) pass
null, and the fallback simply doesn't fire for those — theplan's own (possibly truncated) text is returned, same as today, with Rule 39 still flagging the
truncation.
Not ported / follow-up
Lite/Helpers/ContextMenuHelper.cs,Lite/Controls/ServerTab.CopyExport.cs,Lite/Controls/RecommendationsTab.xaml.cs,Lite/Windows/SettingsWindow.xaml.cs,Lite/Windows/AlertDetailWindow.xaml.cs,Lite/Windows/EntraDeviceCodeWindow.xaml.cs,Darling/PerformanceMonitor.Darling.Viewer/MainWindow.xaml.cs,ViewerServerTab.CopyExport.cs,ViewerServerTab.ChartContextMenu.cs,SettingsWindow.xaml.cs,AlertDetailWindow.xaml.cs(18 unguardedClipboard.SetText/SetDataObjectcall sites total)have the same unguarded shape but sit outside the shared
PerformanceMonitor.Uiplan-viewersurface this issue scoped to. They're filed as Lite and the Darling Viewer: 18 unguarded clipboard writes can crash the app when the clipboard is busy #4594.
deprecated/Dashboard/**gets no work per standing policy.CopyParameterizedStatementText_Clickfallback exclusion (@fdd3d22's other half) has no PMequivalent yet — PM has no parameterized-copy menu entry to guard.
Test plan
New pure pins in
Darling/Darling.Tests/Viewer4582Tests.cs(macOS-runnable, no WPF):statementCount: 2) truncated → returns theplan's own text, NOT the captured outer batch (the
@fdd3d22bug pinned directly)RED: on
origin/dev(461d503), the same test file fails to compile —PlanDisplayTexthas noCopyQueryTextmember (CS0117, all 4 tests). Confirmed via a detachedworktree at that commit.
Mutation: flipping
statementCount == 1tostatementCount >= 1inCopyQueryTextturns themulti-statement pin (case 4 above)
[FAIL]; reverted, and a clean rebuild confirmed 4/4 GREENagain.
Run (macOS, in-process,
Microsoft.WindowsDesktop.Appstripped from the runtimeconfig):(
Viewer4582Testsplus the full required list:ShowPlanParserCondAndMultiplePlanTests,ActualPlanRequestTests,ActualPlanDispatchTests,ActualPlanResultParseTests,QueryModificationDetectorTests,ActualPlanCaptureLoopTests,ActualPlanGatingTests,ReproScriptBuilderHardeningTests,DarlingAnalysisPipelineTests,DarlingMcpPlanToolsSurfaceAndSqlTests,DarlingMcpPlanToolsLivePostgresTests,McpPlanAnalysisEnvelopeTests,SerialLoopStoreSizeSourceTests,TsqlConventionGuardTests,DocCommentHygieneTests.)Builds, Release,
-p:EnableWindowsTargeting=true, 0 warnings each:Darling.Tests,PerformanceMonitor.PlanAnalysis,PerformanceMonitor.Ui,Lite/PerformanceMonitorLite.csproj,Lite.Tests,Darling/PerformanceMonitor.Darling.Viewer.Not run — Windows-only, CI decides them:
Darling.TestsandLite.Teststhemselves build onmacOS but their WPF-dependent
Viewer*classes can't discover/run here forPresentationFrameworkreasons; none of them reference the files this PR touches, so they're unaffected.
Lite.Testshasno new/changed test in this PR (the guarded-write pin lives in
Darling.Tests, which does runin-process on macOS).
Screenshot plan (for a human on Windows, since the WPF wiring itself has no macOS-runnable
pin): open a plan with a single statement whose text is at or beyond the showplan cap (any plan
whose
StatementTextis ≥3,990 characters) via a Query Store "View Plan" path that passes storedquery text (so a full, untruncated query is available); right-click the row in the Statements
panel and choose "Copy Query Text"; paste — the pasted text should be the full stored query, not a
string ending mid-token. Then hold the Windows clipboard open in another app (e.g. an active
Office/Word paste-preview) and repeat any Copy action in the plan viewer, the blocking chain
control, or a DataGrid — none should crash; a transient failure should simply not copy.
CHANGELOG
SECTION: Fixed
ENTRY: - The plan viewer no longer crashes when another program is holding the clipboard, and "Copy Query Text" now hands back the full query on a truncated single-statement plan ([#4593]) - Copying anything from the plan viewer, blocking chain, deadlock graph, or a results grid used to crash the app if the Windows clipboard was briefly locked by another program; copies now fail quietly instead. Separately, "Copy Query Text" on a single-statement plan whose text hit SQL Server's 4,000-character cap now returns the full query the plan was captured from, instead of a copy cut off mid-word — for the Query Store and Active Queries plan sources that pass the original query text.
REF: [#4593]: #4593