Skip to content

Commit f554d3b

Browse files
Add offline community script support via community/ directory (#814) (#822)
The installer now checks for pre-downloaded SQL files in a community/ directory before downloading from GitHub. Users in air-gapped environments can place the files there manually. If a file is missing, it falls back to the normal GitHub download. Expected files: - community/sp_WhoIsActive.sql - community/DarlingData.sql - community/Install-All-Scripts.sql Fixes #814 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f302205 commit f554d3b

6 files changed

Lines changed: 83 additions & 13 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ Lite/collection_schedule.json
5959

6060
# Plans directory
6161
plans/
62+
63+
# Community scripts (user-provided, not bundled)
64+
community/*.sql

Dashboard/AddServerDialog.xaml.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,8 @@ private async void InstallOrUpgrade_Click(object sender, RoutedEventArgs e)
618618
preValidationAction = async () =>
619619
{
620620
AppendInstallLog("Installing community dependencies...", "Info");
621-
using var depInstaller = new DependencyInstaller();
621+
string communityDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "community");
622+
using var depInstaller = new DependencyInstaller(communityDir);
622623
await depInstaller.InstallDependenciesAsync(installerConnStr, progress, cancellationToken);
623624
};
624625
}

Installer.Core/DependencyInstaller.cs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,56 @@ namespace Installer.Core;
1414

1515
/// <summary>
1616
/// Installs community dependencies (sp_WhoIsActive, DarlingData, First Responder Kit)
17-
/// from GitHub. Requires an HttpClient — create one instance and dispose when done.
17+
/// from a local community/ directory or GitHub. Local files are checked first — if
18+
/// present, the network is not used. This supports air-gapped installations.
1819
/// </summary>
1920
public sealed class DependencyInstaller : IDisposable
2021
{
2122
private readonly HttpClient _httpClient;
23+
private readonly string? _communityDirectory;
2224
private bool _disposed;
2325

24-
public DependencyInstaller()
26+
/// <param name="communityDirectory">
27+
/// Optional path to a community/ directory containing pre-downloaded SQL files.
28+
/// When provided and files exist, they are used instead of downloading from GitHub.
29+
/// </param>
30+
public DependencyInstaller(string? communityDirectory = null)
2531
{
2632
_httpClient = new HttpClient
2733
{
2834
Timeout = TimeSpan.FromSeconds(30)
2935
};
36+
_communityDirectory = communityDirectory;
3037
}
3138

3239
/// <summary>
33-
/// Install community dependencies from GitHub into the PerformanceMonitor database.
40+
/// Install community dependencies into the PerformanceMonitor database.
41+
/// Checks the community/ directory first, falls back to GitHub download.
3442
/// Returns the number of successfully installed dependencies.
3543
/// </summary>
3644
public async Task<int> InstallDependenciesAsync(
3745
string connectionString,
3846
IProgress<InstallationProgress>? progress = null,
3947
CancellationToken cancellationToken = default)
4048
{
41-
var dependencies = new List<(string Name, string Url, string Description)>
49+
var dependencies = new List<(string Name, string Url, string LocalFile, string Description)>
4250
{
4351
(
4452
"sp_WhoIsActive",
4553
"https://raw.githubusercontent.com/amachanic/sp_whoisactive/refs/heads/master/sp_WhoIsActive.sql",
54+
"sp_WhoIsActive.sql",
4655
"Query activity monitoring by Adam Machanic (GPLv3)"
4756
),
4857
(
4958
"DarlingData",
5059
"https://raw.githubusercontent.com/erikdarlingdata/DarlingData/main/Install-All/DarlingData.sql",
60+
"DarlingData.sql",
5161
"sp_HealthParser, sp_HumanEventsBlockViewer by Erik Darling (MIT)"
5262
),
5363
(
5464
"First Responder Kit",
5565
"https://raw.githubusercontent.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/refs/heads/main/Install-All-Scripts.sql",
66+
"Install-All-Scripts.sql",
5667
"sp_BlitzLock and diagnostic tools by Brent Ozar Unlimited (MIT)"
5768
)
5869
};
@@ -65,7 +76,7 @@ public async Task<int> InstallDependenciesAsync(
6576

6677
int successCount = 0;
6778

68-
foreach (var (name, url, description) in dependencies)
79+
foreach (var (name, url, localFile, description) in dependencies)
6980
{
7081
cancellationToken.ThrowIfCancellationRequested();
7182

@@ -78,15 +89,40 @@ public async Task<int> InstallDependenciesAsync(
7889
try
7990
{
8091
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" });
92+
string sql;
93+
94+
/* Check community/ directory first */
95+
string? localPath = ResolveLocalFile(localFile);
96+
if (localPath != null)
97+
{
98+
progress?.Report(new InstallationProgress
99+
{
100+
Message = $"[DEBUG] {name}: loading from {localPath}",
101+
Status = "Debug"
102+
});
103+
sql = await File.ReadAllTextAsync(localPath, cancellationToken).ConfigureAwait(false);
104+
}
105+
else
106+
{
107+
progress?.Report(new InstallationProgress
108+
{
109+
Message = $"[DEBUG] Downloading {name} from {url}",
110+
Status = "Debug"
111+
});
112+
sql = await DownloadWithRetryAsync(url, progress, cancellationToken: cancellationToken).ConfigureAwait(false);
113+
}
114+
115+
progress?.Report(new InstallationProgress
116+
{
117+
Message = $"[DEBUG] {name}: {(localPath != null ? "loaded" : "downloaded")} {sql.Length} chars in {depSw.ElapsedMilliseconds}ms",
118+
Status = "Debug"
119+
});
84120

85121
if (string.IsNullOrWhiteSpace(sql))
86122
{
87123
progress?.Report(new InstallationProgress
88124
{
89-
Message = $"{name} - FAILED (empty response)",
125+
Message = $"{name} - FAILED (empty {(localPath != null ? "file" : "response")})",
90126
Status = "Error"
91127
});
92128
continue;
@@ -115,9 +151,10 @@ public async Task<int> InstallDependenciesAsync(
115151
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
116152
}
117153

154+
string source = localPath != null ? "local" : "GitHub";
118155
progress?.Report(new InstallationProgress
119156
{
120-
Message = $"{name} - Success ({description})",
157+
Message = $"{name} - Success ({description}) [{source}]",
121158
Status = "Success"
122159
});
123160

@@ -158,6 +195,19 @@ public async Task<int> InstallDependenciesAsync(
158195
return successCount;
159196
}
160197

198+
/// <summary>
199+
/// Checks the community directory for a local copy of the dependency file.
200+
/// Returns the full path if found, null otherwise.
201+
/// </summary>
202+
private string? ResolveLocalFile(string fileName)
203+
{
204+
if (string.IsNullOrEmpty(_communityDirectory) || !Directory.Exists(_communityDirectory))
205+
return null;
206+
207+
string path = Path.Combine(_communityDirectory, fileName);
208+
return File.Exists(path) ? path : null;
209+
}
210+
161211
private async Task<string> DownloadWithRetryAsync(
162212
string url,
163213
IProgress<InstallationProgress>? progress = null,

Installer/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,8 @@ Execute SQL files in order
636636
Execute installation using Installer.Core
637637
Use DependencyInstaller for community dependencies before validation
638638
*/
639-
using var dependencyInstaller = new DependencyInstaller();
639+
string communityDir = Path.Combine(monitorRootDirectory, "community");
640+
using var dependencyInstaller = new DependencyInstaller(communityDir);
640641

641642
var installResult = await InstallationService.ExecuteInstallationAsync(
642643
connectionString,

InstallerGui/MainWindow.xaml.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public MainWindow()
6060
try
6161
{
6262
InitializeComponent();
63-
_dependencyInstaller = new DependencyInstaller();
63+
string communityDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "community");
64+
_dependencyInstaller = new DependencyInstaller(communityDir);
6465

6566
/*Set window title with version*/
6667
Title = $"Performance Monitor Installer v{AppVersion}";

community/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Community Scripts (Offline Installation)
2+
3+
Place pre-downloaded community SQL scripts in this directory for offline/air-gapped installations.
4+
When files are present here, the installer uses them instead of downloading from GitHub.
5+
6+
## Expected files
7+
8+
| File | Source | License |
9+
|------|--------|---------|
10+
| `sp_WhoIsActive.sql` | [amachanic/sp_whoisactive](https://github.com/amachanic/sp_whoisactive) | GPLv3 |
11+
| `DarlingData.sql` | [erikdarlingdata/DarlingData](https://github.com/erikdarlingdata/DarlingData/tree/main/Install-All) | MIT |
12+
| `Install-All-Scripts.sql` | [BrentOzarULTD/SQL-Server-First-Responder-Kit](https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit) | MIT |
13+
14+
Any file not found here will be downloaded from GitHub as usual.

0 commit comments

Comments
 (0)