From a8f33729ae1a847ffac99fac9039378a92d60ef4 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:22:06 -0600 Subject: [PATCH 1/4] Fix Linux crash on startup and 60s plan timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #50: Linux threw PlatformNotSupportedException at launch because no credential service existed for Linux. Added InMemoryCredentialService as a fallback — credentials work within the session but don't persist across restarts. Issue #49: Actual and estimated plan capture had a hardcoded 60-second timeout. Bumped to 3600s (1 hour) so long-running queries can complete. Co-Authored-By: Claude Opus 4.6 --- .../Controls/QuerySessionControl.axaml.cs | 6 +-- src/PlanViewer.App/MainWindow.axaml.cs | 2 +- .../Services/CredentialServiceFactory.cs | 5 +-- .../Services/InMemoryCredentialService.cs | 39 +++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 src/PlanViewer.Core/Services/InMemoryCredentialService.cs diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 7311a70..e0fd5f9 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -466,14 +466,14 @@ private async Task CaptureAndShowPlan(bool estimated) if (estimated) { planXml = await EstimatedPlanExecutor.GetEstimatedPlanAsync( - _connectionString, _selectedDatabase, queryText, timeoutSeconds: 60, ct); + _connectionString, _selectedDatabase, queryText, timeoutSeconds: 3600, ct); } else { planXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( _connectionString, _selectedDatabase, queryText, planXml: null, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 60, ct); + isAzureSqlDb: isAzure, timeoutSeconds: 3600, ct); } sw.Stop(); @@ -1185,7 +1185,7 @@ private async void GetActualPlan_Click(object? sender, RoutedEventArgs e) var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( _connectionString, _selectedDatabase, queryText, planXml, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 60, ct); + isAzureSqlDb: isAzure, timeoutSeconds: 3600, ct); sw.Stop(); diff --git a/src/PlanViewer.App/MainWindow.axaml.cs b/src/PlanViewer.App/MainWindow.axaml.cs index a834a5a..c77d670 100644 --- a/src/PlanViewer.App/MainWindow.axaml.cs +++ b/src/PlanViewer.App/MainWindow.axaml.cs @@ -1070,7 +1070,7 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer) var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( connectionString, database, queryText, viewer.RawXml, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 60, cts.Token); + isAzureSqlDb: isAzure, timeoutSeconds: 3600, cts.Token); sw.Stop(); diff --git a/src/PlanViewer.Core/Services/CredentialServiceFactory.cs b/src/PlanViewer.Core/Services/CredentialServiceFactory.cs index e99a654..648ff53 100644 --- a/src/PlanViewer.Core/Services/CredentialServiceFactory.cs +++ b/src/PlanViewer.Core/Services/CredentialServiceFactory.cs @@ -13,8 +13,7 @@ public static ICredentialService Create() if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return new KeychainCredentialService(); - throw new PlatformNotSupportedException( - "Credential storage is not yet supported on this platform. " + - "Windows and macOS are supported."); + // Linux and other platforms: use in-memory storage (credentials not persisted across sessions) + return new InMemoryCredentialService(); } } diff --git a/src/PlanViewer.Core/Services/InMemoryCredentialService.cs b/src/PlanViewer.Core/Services/InMemoryCredentialService.cs new file mode 100644 index 0000000..733dc54 --- /dev/null +++ b/src/PlanViewer.Core/Services/InMemoryCredentialService.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using PlanViewer.Core.Interfaces; + +namespace PlanViewer.Core.Services; + +/// +/// In-memory credential service for platforms without native credential storage (e.g. Linux). +/// Credentials are held for the lifetime of the app but not persisted to disk. +/// +public class InMemoryCredentialService : ICredentialService +{ + private readonly ConcurrentDictionary _store = new(); + + public bool SaveCredential(string serverId, string username, string password) + { + _store[serverId] = (username, password); + return true; + } + + public (string Username, string Password)? GetCredential(string serverId) + { + return _store.TryGetValue(serverId, out var cred) ? cred : null; + } + + public bool DeleteCredential(string serverId) + { + return _store.TryRemove(serverId, out _); + } + + public bool CredentialExists(string serverId) + { + return _store.ContainsKey(serverId); + } + + public bool UpdateCredential(string serverId, string username, string password) + { + return SaveCredential(serverId, username, password); + } +} From e5b6eb057fd76edf84578bb540926ac06bf6c1ac Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:25:31 -0600 Subject: [PATCH 2/4] Remove plan execution timeout entirely CommandTimeout=0 means no timeout in ADO.NET. A performance tuning tool should never kill a query the user is deliberately running. Co-Authored-By: Claude Opus 4.6 --- src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs | 6 +++--- src/PlanViewer.App/MainWindow.axaml.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index e0fd5f9..69a8c8c 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -466,14 +466,14 @@ private async Task CaptureAndShowPlan(bool estimated) if (estimated) { planXml = await EstimatedPlanExecutor.GetEstimatedPlanAsync( - _connectionString, _selectedDatabase, queryText, timeoutSeconds: 3600, ct); + _connectionString, _selectedDatabase, queryText, timeoutSeconds: 0, ct); } else { planXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( _connectionString, _selectedDatabase, queryText, planXml: null, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 3600, ct); + isAzureSqlDb: isAzure, timeoutSeconds: 0, ct); } sw.Stop(); @@ -1185,7 +1185,7 @@ private async void GetActualPlan_Click(object? sender, RoutedEventArgs e) var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( _connectionString, _selectedDatabase, queryText, planXml, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 3600, ct); + isAzureSqlDb: isAzure, timeoutSeconds: 0, ct); sw.Stop(); diff --git a/src/PlanViewer.App/MainWindow.axaml.cs b/src/PlanViewer.App/MainWindow.axaml.cs index c77d670..d395fd9 100644 --- a/src/PlanViewer.App/MainWindow.axaml.cs +++ b/src/PlanViewer.App/MainWindow.axaml.cs @@ -1070,7 +1070,7 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer) var actualPlanXml = await ActualPlanExecutor.ExecuteForActualPlanAsync( connectionString, database, queryText, viewer.RawXml, isolationLevel: null, - isAzureSqlDb: isAzure, timeoutSeconds: 3600, cts.Token); + isAzureSqlDb: isAzure, timeoutSeconds: 0, cts.Token); sw.Stop(); From 807dd23a4b6a1c88e126a12f0beef16a38da02b8 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:27:09 -0600 Subject: [PATCH 3/4] Add Cancel button for running queries (Escape shortcut) Shows a Cancel button in the toolbar while a plan capture is running. Also bound to Escape key. Without this, removing the timeout left no way to stop a long-running query short of closing the app. Co-Authored-By: Claude Opus 4.6 --- .../Controls/QuerySessionControl.axaml | 4 ++++ .../Controls/QuerySessionControl.axaml.cs | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml b/src/PlanViewer.App/Controls/QuerySessionControl.axaml index 7262b7e..a57aa6a 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml @@ -31,6 +31,10 @@ Height="28" Padding="10,0" FontSize="12" IsEnabled="False" Theme="{StaticResource AppButton}" ToolTip.Tip="Capture estimated plan without executing (Ctrl+L)"/> +