diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 0c331f0..78ab33a 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -78,11 +78,15 @@ public QuerySessionControl(ICredentialService credentialService, ConnectionStore QueryEditor.TextArea.Focus(); }; - // Dispose TextMate when detached (e.g. tab switch) to release renderers/transformers + // Dispose TextMate when detached (e.g. tab switch) to release renderers/transformers. + // Also cancel any in-flight status-clear dispatch so it doesn't fire on a dead control. DetachedFromVisualTree += (_, _) => { _textMateInstallation?.Dispose(); _textMateInstallation = null; + _statusClearCts?.Cancel(); + _statusClearCts?.Dispose(); + _statusClearCts = null; }; // Focus the editor when the Editor tab is selected; toggle plan-dependent buttons diff --git a/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml.cs b/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml.cs index ca5d9b1..b39825a 100644 --- a/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml.cs +++ b/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml.cs @@ -169,6 +169,16 @@ public static string MapOrderByToMetricTag(string orderBy) : "AvgCpuMs"; } + protected override void OnClosed(EventArgs e) + { + // Cancel any in-flight history fetch so the SqlConnection doesn't sit open on the + // server after the dialog is dismissed. + _fetchCts?.Cancel(); + _fetchCts?.Dispose(); + _fetchCts = null; + base.OnClosed(e); + } + private async System.Threading.Tasks.Task LoadHistoryAsync() { _fetchCts?.Cancel(); diff --git a/src/PlanViewer.Core/Services/KeychainCredentialService.cs b/src/PlanViewer.Core/Services/KeychainCredentialService.cs index 4e1f9cb..772eceb 100644 --- a/src/PlanViewer.Core/Services/KeychainCredentialService.cs +++ b/src/PlanViewer.Core/Services/KeychainCredentialService.cs @@ -103,11 +103,14 @@ private static (int ExitCode, string Output) RunSecurity(params string[] args) using var process = Process.Start(psi); if (process == null) return (-1, string.Empty); + // Read both streams concurrently. Doing stderr synchronously while stdout is + // async can deadlock if stderr fills its pipe buffer before the process exits + // (reproduces on `security dump-keychain` with large keychains). var stdoutTask = process.StandardOutput.ReadToEndAsync(); - var stderr = process.StandardError.ReadToEnd(); + var stderrTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(); - var stdout = stdoutTask.Result; + Task.WaitAll(stdoutTask, stderrTask); - return (process.ExitCode, stdout + stderr); + return (process.ExitCode, stdoutTask.Result + stderrTask.Result); } } diff --git a/src/PlanViewer.Core/Services/ReproScriptBuilder.cs b/src/PlanViewer.Core/Services/ReproScriptBuilder.cs index aa1136b..8b94f09 100644 --- a/src/PlanViewer.Core/Services/ReproScriptBuilder.cs +++ b/src/PlanViewer.Core/Services/ReproScriptBuilder.cs @@ -20,6 +20,20 @@ namespace PlanViewer.Core.Services; /// public static class ReproScriptBuilder { + /// + /// Valid T-SQL isolation level names (uppercase) that may appear in a SET TRANSACTION + /// ISOLATION LEVEL statement. Used to gate interpolation so arbitrary upstream strings + /// can't land in the generated script. + /// + private static readonly HashSet IsolationLevels = new(StringComparer.Ordinal) + { + "READ UNCOMMITTED", + "READ COMMITTED", + "REPEATABLE READ", + "SNAPSHOT", + "SERIALIZABLE", + }; + /// /// Builds a complete reproduction script from available query data. /// @@ -94,10 +108,11 @@ public static string BuildReproScript( sb.AppendLine("*/"); sb.AppendLine(); - /* USE database (skip for Azure SQL DB — USE is invalid there) */ + /* USE database (skip for Azure SQL DB — USE is invalid there). + Double any ']' in the identifier so names like 'cool]stuff' still parse. */ if (!string.IsNullOrEmpty(databaseName) && !isAzureSqlDb) { - sb.AppendLine($"USE [{databaseName}];"); + sb.AppendLine($"USE [{databaseName.Replace("]", "]]")}];"); sb.AppendLine(); } @@ -116,7 +131,9 @@ public static string BuildReproScript( if (!string.IsNullOrEmpty(isolationLevel)) { - sb.AppendLine($"SET TRANSACTION ISOLATION LEVEL {isolationLevel.ToUpperInvariant()};"); + var upper = isolationLevel.ToUpperInvariant(); + if (IsolationLevels.Contains(upper)) + sb.AppendLine($"SET TRANSACTION ISOLATION LEVEL {upper};"); } sb.AppendLine("SET NOCOUNT ON;"); sb.AppendLine(); diff --git a/src/PlanViewer.Core/Services/ShowPlanParser.cs b/src/PlanViewer.Core/Services/ShowPlanParser.cs index 31c93d6..db1a374 100644 --- a/src/PlanViewer.Core/Services/ShowPlanParser.cs +++ b/src/PlanViewer.Core/Services/ShowPlanParser.cs @@ -1832,6 +1832,7 @@ private static double ParseDouble(string? value) private static long ParseLong(string? value) { if (string.IsNullOrEmpty(value)) return 0; - return long.TryParse(value, out var result) ? result : 0; + return long.TryParse(value, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var result) ? result : 0; } }