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;
}
}