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.