Skip to content

Commit 8c9e8a0

Browse files
Add XACT_STATE check after third-party proc calls (#695) (#759)
* Add Installer.Core shared library and retarget InstallerGui + Tests (#755) Phase 1 extraction of shared installation logic into Installer.Core class library. InstallerGui and Installer.Tests now consume the shared library instead of duplicating code. CLI Installer refactor is next. New files: - Installer.Core/InstallationService.cs — all static install/upgrade methods - Installer.Core/DependencyInstaller.cs — community dependency downloads - Installer.Core/ScriptProvider.cs — filesystem + embedded resource abstraction - Installer.Core/Patterns.cs — shared regex patterns ([GeneratedRegex]) - Installer.Core/Models/ — InstallationProgress, ServerInfo, InstallationResult, UpgradeInfo, InstallationResultCode (enum mapping CLI exit codes 0-8) Key changes: - SQL scripts embedded as assembly resources for future Dashboard integration - ScriptProvider.FromDirectory() for CLI/GUI, FromEmbeddedResources() for Dashboard - AutoDiscover() searches filesystem then falls back to embedded - Comprehensive [DEBUG] logging throughout all methods for GUI diagnostics - upgrade.txt missing warning (was silently skipped, now logged) - GenerateSummaryReport gains optional outputDirectory parameter Retargeted: - InstallerGui references Installer.Core, old InstallationService.cs deleted - Installer.Tests targets net8.0 (was net8.0-windows), no WPF dependency - Tests use ScriptProvider.FromDirectory() instead of raw file paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Refactor CLI Installer to thin wrapper over Installer.Core (#755) Program.cs rewritten from 2,122 lines to 1,172 lines (45% reduction). All duplicated logic removed — CLI now delegates to Installer.Core for: - Connection string building (InstallationService.BuildConnectionString) - Connection testing (InstallationService.TestConnectionAsync) - Script discovery (ScriptProvider.FromDirectory) - SQL file execution (InstallationService.ExecuteInstallationAsync) - Upgrade detection and execution (ExecuteAllUpgradesAsync) - Uninstall (InstallationService.ExecuteUninstallAsync) - Version detection (InstallationService.GetInstalledVersionAsync) - Community dependencies (DependencyInstaller) - Validation (InstallationService.RunValidationAsync) - Installation history (InstallationService.LogInstallationHistoryAsync) - Summary reports (InstallationService.GenerateSummaryReport) What stays in Program.cs (console-specific): - Argument parsing, interactive prompts, retry loop - Console output helpers (WriteSuccess/WriteError/WriteWarning) - ReadPassword, WaitForExit, CheckForInstallerUpdateAsync - Error log generation (WriteErrorLog) Fixes during review: - Added missing exit code 8 (UpgradesFailed) to --help text - Fixed encryption default label in error message (was "optional", is "mandatory") Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix doomed transaction error handling in delta framework and ensure_collection_table (#756) When calculate_deltas was called inside a collector's transaction and failed, the CATCH block tried to INSERT into collection_log while the transaction was doomed (XACT_STATE = -1), swallowing the real error with "The current transaction cannot be committed." Same pattern in ensure_collection_table where INSERT happened before ROLLBACK. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add XACT_STATE check after third-party proc calls in XML processors (#695) sp_BlitzLock and sp_HumanEventsBlockViewer can fail internally and doom the caller's transaction. Without checking XACT_STATE after the call, the next write attempt produces "cannot be committed" which swallows the real error. Now we detect the doomed state, rollback, and surface a meaningful error message. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 91f46f7 commit 8c9e8a0

21 files changed

Lines changed: 3206 additions & 3726 deletions
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System.Diagnostics;
10+
using Installer.Core.Models;
11+
using Microsoft.Data.SqlClient;
12+
13+
namespace Installer.Core;
14+
15+
/// <summary>
16+
/// Installs community dependencies (sp_WhoIsActive, DarlingData, First Responder Kit)
17+
/// from GitHub. Requires an HttpClient — create one instance and dispose when done.
18+
/// </summary>
19+
public sealed class DependencyInstaller : IDisposable
20+
{
21+
private readonly HttpClient _httpClient;
22+
private bool _disposed;
23+
24+
public DependencyInstaller()
25+
{
26+
_httpClient = new HttpClient
27+
{
28+
Timeout = TimeSpan.FromSeconds(30)
29+
};
30+
}
31+
32+
/// <summary>
33+
/// Install community dependencies from GitHub into the PerformanceMonitor database.
34+
/// Returns the number of successfully installed dependencies.
35+
/// </summary>
36+
public async Task<int> InstallDependenciesAsync(
37+
string connectionString,
38+
IProgress<InstallationProgress>? progress = null,
39+
CancellationToken cancellationToken = default)
40+
{
41+
var dependencies = new List<(string Name, string Url, string Description)>
42+
{
43+
(
44+
"sp_WhoIsActive",
45+
"https://raw.githubusercontent.com/amachanic/sp_whoisactive/refs/heads/master/sp_WhoIsActive.sql",
46+
"Query activity monitoring by Adam Machanic (GPLv3)"
47+
),
48+
(
49+
"DarlingData",
50+
"https://raw.githubusercontent.com/erikdarlingdata/DarlingData/main/Install-All/DarlingData.sql",
51+
"sp_HealthParser, sp_HumanEventsBlockViewer by Erik Darling (MIT)"
52+
),
53+
(
54+
"First Responder Kit",
55+
"https://raw.githubusercontent.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/refs/heads/main/Install-All-Scripts.sql",
56+
"sp_BlitzLock and diagnostic tools by Brent Ozar Unlimited (MIT)"
57+
)
58+
};
59+
60+
progress?.Report(new InstallationProgress
61+
{
62+
Message = "Installing community dependencies...",
63+
Status = "Info"
64+
});
65+
66+
int successCount = 0;
67+
68+
foreach (var (name, url, description) in dependencies)
69+
{
70+
cancellationToken.ThrowIfCancellationRequested();
71+
72+
progress?.Report(new InstallationProgress
73+
{
74+
Message = $"Installing {name}...",
75+
Status = "Info"
76+
});
77+
78+
try
79+
{
80+
var depSw = Stopwatch.StartNew();
81+
progress?.Report(new InstallationProgress { Message = $"[DEBUG] Downloading {name} from {url}", Status = "Debug" });
82+
string sql = await DownloadWithRetryAsync(url, progress, cancellationToken: cancellationToken).ConfigureAwait(false);
83+
progress?.Report(new InstallationProgress { Message = $"[DEBUG] {name}: downloaded {sql.Length} chars in {depSw.ElapsedMilliseconds}ms", Status = "Debug" });
84+
85+
if (string.IsNullOrWhiteSpace(sql))
86+
{
87+
progress?.Report(new InstallationProgress
88+
{
89+
Message = $"{name} - FAILED (empty response)",
90+
Status = "Error"
91+
});
92+
continue;
93+
}
94+
95+
using var connection = new SqlConnection(connectionString);
96+
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
97+
98+
using (var useDbCommand = new SqlCommand("USE PerformanceMonitor;", connection))
99+
{
100+
await useDbCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
101+
}
102+
103+
string[] batches = Patterns.GoBatchSplitter.Split(sql);
104+
int nonEmpty = batches.Count(b => !string.IsNullOrWhiteSpace(b));
105+
progress?.Report(new InstallationProgress { Message = $"[DEBUG] {name}: executing {nonEmpty} batches", Status = "Debug" });
106+
107+
foreach (string batch in batches)
108+
{
109+
string trimmedBatch = batch.Trim();
110+
if (string.IsNullOrWhiteSpace(trimmedBatch))
111+
continue;
112+
113+
using var command = new SqlCommand(trimmedBatch, connection);
114+
command.CommandTimeout = InstallationService.DependencyTimeoutSeconds;
115+
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
116+
}
117+
118+
progress?.Report(new InstallationProgress
119+
{
120+
Message = $"{name} - Success ({description})",
121+
Status = "Success"
122+
});
123+
124+
successCount++;
125+
}
126+
catch (HttpRequestException ex)
127+
{
128+
progress?.Report(new InstallationProgress
129+
{
130+
Message = $"{name} - Download failed: {ex.Message}",
131+
Status = "Error"
132+
});
133+
}
134+
catch (SqlException ex)
135+
{
136+
progress?.Report(new InstallationProgress
137+
{
138+
Message = $"{name} - SQL execution failed: {ex.Message}",
139+
Status = "Error"
140+
});
141+
}
142+
catch (Exception ex)
143+
{
144+
progress?.Report(new InstallationProgress
145+
{
146+
Message = $"{name} - Failed: {ex.Message}",
147+
Status = "Error"
148+
});
149+
}
150+
}
151+
152+
progress?.Report(new InstallationProgress
153+
{
154+
Message = $"Dependencies installed: {successCount}/{dependencies.Count}",
155+
Status = successCount == dependencies.Count ? "Success" : "Warning"
156+
});
157+
158+
return successCount;
159+
}
160+
161+
private async Task<string> DownloadWithRetryAsync(
162+
string url,
163+
IProgress<InstallationProgress>? progress = null,
164+
int maxRetries = 3,
165+
CancellationToken cancellationToken = default)
166+
{
167+
for (int attempt = 1; attempt <= maxRetries; attempt++)
168+
{
169+
try
170+
{
171+
return await _httpClient.GetStringAsync(url, cancellationToken).ConfigureAwait(false);
172+
}
173+
catch (HttpRequestException) when (attempt < maxRetries)
174+
{
175+
int delaySeconds = (int)Math.Pow(2, attempt);
176+
progress?.Report(new InstallationProgress
177+
{
178+
Message = $"Network error, retrying in {delaySeconds}s ({attempt}/{maxRetries})...",
179+
Status = "Warning"
180+
});
181+
await Task.Delay(delaySeconds * 1000, cancellationToken).ConfigureAwait(false);
182+
}
183+
}
184+
return await _httpClient.GetStringAsync(url, cancellationToken).ConfigureAwait(false);
185+
}
186+
187+
public void Dispose()
188+
{
189+
if (!_disposed)
190+
{
191+
_httpClient?.Dispose();
192+
_disposed = true;
193+
}
194+
GC.SuppressFinalize(this);
195+
}
196+
}

0 commit comments

Comments
 (0)