diff --git a/.gitignore b/.gitignore index e5507e8d..5af2ea72 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ Lite/collection_schedule.json # Plans directory plans/ + +# Community scripts (user-provided, not bundled) +community/*.sql diff --git a/Dashboard/AddServerDialog.xaml.cs b/Dashboard/AddServerDialog.xaml.cs index 1e626ea2..b3f262ba 100644 --- a/Dashboard/AddServerDialog.xaml.cs +++ b/Dashboard/AddServerDialog.xaml.cs @@ -618,7 +618,8 @@ private async void InstallOrUpgrade_Click(object sender, RoutedEventArgs e) preValidationAction = async () => { AppendInstallLog("Installing community dependencies...", "Info"); - using var depInstaller = new DependencyInstaller(); + string communityDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "community"); + using var depInstaller = new DependencyInstaller(communityDir); await depInstaller.InstallDependenciesAsync(installerConnStr, progress, cancellationToken); }; } diff --git a/Installer.Core/DependencyInstaller.cs b/Installer.Core/DependencyInstaller.cs index 13aad3cb..b2ac49e8 100644 --- a/Installer.Core/DependencyInstaller.cs +++ b/Installer.Core/DependencyInstaller.cs @@ -14,23 +14,31 @@ namespace Installer.Core; /// /// Installs community dependencies (sp_WhoIsActive, DarlingData, First Responder Kit) -/// from GitHub. Requires an HttpClient — create one instance and dispose when done. +/// from a local community/ directory or GitHub. Local files are checked first — if +/// present, the network is not used. This supports air-gapped installations. /// public sealed class DependencyInstaller : IDisposable { private readonly HttpClient _httpClient; + private readonly string? _communityDirectory; private bool _disposed; - public DependencyInstaller() + /// + /// Optional path to a community/ directory containing pre-downloaded SQL files. + /// When provided and files exist, they are used instead of downloading from GitHub. + /// + public DependencyInstaller(string? communityDirectory = null) { _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + _communityDirectory = communityDirectory; } /// - /// Install community dependencies from GitHub into the PerformanceMonitor database. + /// Install community dependencies into the PerformanceMonitor database. + /// Checks the community/ directory first, falls back to GitHub download. /// Returns the number of successfully installed dependencies. /// public async Task InstallDependenciesAsync( @@ -38,21 +46,24 @@ public async Task InstallDependenciesAsync( IProgress? progress = null, CancellationToken cancellationToken = default) { - var dependencies = new List<(string Name, string Url, string Description)> + var dependencies = new List<(string Name, string Url, string LocalFile, string Description)> { ( "sp_WhoIsActive", "https://raw.githubusercontent.com/amachanic/sp_whoisactive/refs/heads/master/sp_WhoIsActive.sql", + "sp_WhoIsActive.sql", "Query activity monitoring by Adam Machanic (GPLv3)" ), ( "DarlingData", "https://raw.githubusercontent.com/erikdarlingdata/DarlingData/main/Install-All/DarlingData.sql", + "DarlingData.sql", "sp_HealthParser, sp_HumanEventsBlockViewer by Erik Darling (MIT)" ), ( "First Responder Kit", "https://raw.githubusercontent.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/refs/heads/main/Install-All-Scripts.sql", + "Install-All-Scripts.sql", "sp_BlitzLock and diagnostic tools by Brent Ozar Unlimited (MIT)" ) }; @@ -65,7 +76,7 @@ public async Task InstallDependenciesAsync( int successCount = 0; - foreach (var (name, url, description) in dependencies) + foreach (var (name, url, localFile, description) in dependencies) { cancellationToken.ThrowIfCancellationRequested(); @@ -78,15 +89,40 @@ public async Task InstallDependenciesAsync( try { var depSw = Stopwatch.StartNew(); - progress?.Report(new InstallationProgress { Message = $"[DEBUG] Downloading {name} from {url}", Status = "Debug" }); - string sql = await DownloadWithRetryAsync(url, progress, cancellationToken: cancellationToken).ConfigureAwait(false); - progress?.Report(new InstallationProgress { Message = $"[DEBUG] {name}: downloaded {sql.Length} chars in {depSw.ElapsedMilliseconds}ms", Status = "Debug" }); + string sql; + + /* Check community/ directory first */ + string? localPath = ResolveLocalFile(localFile); + if (localPath != null) + { + progress?.Report(new InstallationProgress + { + Message = $"[DEBUG] {name}: loading from {localPath}", + Status = "Debug" + }); + sql = await File.ReadAllTextAsync(localPath, cancellationToken).ConfigureAwait(false); + } + else + { + progress?.Report(new InstallationProgress + { + Message = $"[DEBUG] Downloading {name} from {url}", + Status = "Debug" + }); + sql = await DownloadWithRetryAsync(url, progress, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + progress?.Report(new InstallationProgress + { + Message = $"[DEBUG] {name}: {(localPath != null ? "loaded" : "downloaded")} {sql.Length} chars in {depSw.ElapsedMilliseconds}ms", + Status = "Debug" + }); if (string.IsNullOrWhiteSpace(sql)) { progress?.Report(new InstallationProgress { - Message = $"{name} - FAILED (empty response)", + Message = $"{name} - FAILED (empty {(localPath != null ? "file" : "response")})", Status = "Error" }); continue; @@ -115,9 +151,10 @@ public async Task InstallDependenciesAsync( await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } + string source = localPath != null ? "local" : "GitHub"; progress?.Report(new InstallationProgress { - Message = $"{name} - Success ({description})", + Message = $"{name} - Success ({description}) [{source}]", Status = "Success" }); @@ -158,6 +195,19 @@ public async Task InstallDependenciesAsync( return successCount; } + /// + /// Checks the community directory for a local copy of the dependency file. + /// Returns the full path if found, null otherwise. + /// + private string? ResolveLocalFile(string fileName) + { + if (string.IsNullOrEmpty(_communityDirectory) || !Directory.Exists(_communityDirectory)) + return null; + + string path = Path.Combine(_communityDirectory, fileName); + return File.Exists(path) ? path : null; + } + private async Task DownloadWithRetryAsync( string url, IProgress? progress = null, diff --git a/Installer/Program.cs b/Installer/Program.cs index bbb487a5..dd89646b 100644 --- a/Installer/Program.cs +++ b/Installer/Program.cs @@ -636,7 +636,8 @@ Execute SQL files in order Execute installation using Installer.Core Use DependencyInstaller for community dependencies before validation */ - using var dependencyInstaller = new DependencyInstaller(); + string communityDir = Path.Combine(monitorRootDirectory, "community"); + using var dependencyInstaller = new DependencyInstaller(communityDir); var installResult = await InstallationService.ExecuteInstallationAsync( connectionString, diff --git a/InstallerGui/MainWindow.xaml.cs b/InstallerGui/MainWindow.xaml.cs index 6103e949..1f532c89 100644 --- a/InstallerGui/MainWindow.xaml.cs +++ b/InstallerGui/MainWindow.xaml.cs @@ -60,7 +60,8 @@ public MainWindow() try { InitializeComponent(); - _dependencyInstaller = new DependencyInstaller(); + string communityDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "community"); + _dependencyInstaller = new DependencyInstaller(communityDir); /*Set window title with version*/ Title = $"Performance Monitor Installer v{AppVersion}"; diff --git a/community/README.md b/community/README.md new file mode 100644 index 00000000..9bc2b264 --- /dev/null +++ b/community/README.md @@ -0,0 +1,14 @@ +# Community Scripts (Offline Installation) + +Place pre-downloaded community SQL scripts in this directory for offline/air-gapped installations. +When files are present here, the installer uses them instead of downloading from GitHub. + +## Expected files + +| File | Source | License | +|------|--------|---------| +| `sp_WhoIsActive.sql` | [amachanic/sp_whoisactive](https://github.com/amachanic/sp_whoisactive) | GPLv3 | +| `DarlingData.sql` | [erikdarlingdata/DarlingData](https://github.com/erikdarlingdata/DarlingData/tree/main/Install-All) | MIT | +| `Install-All-Scripts.sql` | [BrentOzarULTD/SQL-Server-First-Responder-Kit](https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit) | MIT | + +Any file not found here will be downloaded from GitHub as usual.