From bd0569b406efa2b40caa6b58b05ad541d142099a Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:50:00 -0500 Subject: [PATCH] Add --preserve-jobs flag to CLI and GUI installers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing SQL Agent jobs (owner, schedule, notifications) are now preserved during upgrades when --preserve-jobs is specified (CLI) or the checkbox is checked (GUI). Missing jobs are still created. Uses a @preserve_jobs variable in the T-SQL — the C# installer flips it from 0 to 1 via string replace when the flag is set. Fixes #324 Co-Authored-By: Claude Opus 4.6 --- Installer/Program.cs | 13 + InstallerGui/MainWindow.xaml | 6 + InstallerGui/MainWindow.xaml.cs | 2 + InstallerGui/Services/InstallationService.cs | 12 + README.md | 1 + install/45_create_agent_jobs.sql | 247 +++++++++++-------- 6 files changed, 174 insertions(+), 107 deletions(-) diff --git a/Installer/Program.cs b/Installer/Program.cs index a47ed83..f00279b 100644 --- a/Installer/Program.cs +++ b/Installer/Program.cs @@ -95,6 +95,7 @@ static async Task Main(string[] args) Console.WriteLine(" -h, --help Show this help message"); Console.WriteLine(" --reinstall Drop existing database and perform clean install"); Console.WriteLine(" --reset-schedule Reset collection schedule to recommended defaults"); + Console.WriteLine(" --preserve-jobs Keep existing SQL Agent jobs (owner, schedule, notifications)"); Console.WriteLine(" --encrypt= Connection encryption: mandatory (default), optional, strict"); Console.WriteLine(" --trust-cert Trust server certificate without validation"); Console.WriteLine(); @@ -115,6 +116,7 @@ static async Task Main(string[] args) bool automatedMode = args.Length > 0; bool reinstallMode = args.Any(a => a.Equals("--reinstall", StringComparison.OrdinalIgnoreCase)); bool resetSchedule = args.Any(a => a.Equals("--reset-schedule", StringComparison.OrdinalIgnoreCase)); + bool preserveJobs = args.Any(a => a.Equals("--preserve-jobs", StringComparison.OrdinalIgnoreCase)); bool trustCert = args.Any(a => a.Equals("--trust-cert", StringComparison.OrdinalIgnoreCase)); /*Parse encryption option (default: Mandatory)*/ @@ -135,6 +137,7 @@ static async Task Main(string[] args) var filteredArgs = args .Where(a => !a.Equals("--reinstall", StringComparison.OrdinalIgnoreCase)) .Where(a => !a.Equals("--reset-schedule", StringComparison.OrdinalIgnoreCase)) + .Where(a => !a.Equals("--preserve-jobs", StringComparison.OrdinalIgnoreCase)) .Where(a => !a.Equals("--trust-cert", StringComparison.OrdinalIgnoreCase)) .Where(a => !a.StartsWith("--encrypt=", StringComparison.OrdinalIgnoreCase)) .ToArray(); @@ -653,6 +656,16 @@ INSERT...WHERE NOT EXISTS re-populates with current recommended values Console.Write("(resetting schedule) "); } + /* + Preserve existing SQL Agent jobs if requested — flip the T-SQL + variable so existing jobs are left untouched during upgrade + */ + if (preserveJobs && fileName.StartsWith("45_", StringComparison.Ordinal)) + { + sqlContent = sqlContent.Replace("@preserve_jobs bit = 0", "@preserve_jobs bit = 1"); + Console.Write("(preserving existing jobs) "); + } + /* Remove SQLCMD directives (:r includes) as we're executing files directly */ diff --git a/InstallerGui/MainWindow.xaml b/InstallerGui/MainWindow.xaml index 61b4046..76e650d 100644 --- a/InstallerGui/MainWindow.xaml +++ b/InstallerGui/MainWindow.xaml @@ -186,6 +186,12 @@ Margin="0,0,0,10" Foreground="{DynamicResource ForegroundBrush}"/> + + + { diff --git a/InstallerGui/Services/InstallationService.cs b/InstallerGui/Services/InstallationService.cs index 512a6ab..dd6705b 100644 --- a/InstallerGui/Services/InstallationService.cs +++ b/InstallerGui/Services/InstallationService.cs @@ -325,6 +325,7 @@ public static async Task ExecuteInstallationAsync( List sqlFiles, bool cleanInstall, bool resetSchedule = false, + bool preserveJobs = false, IProgress? progress = null, Func? preValidationAction = null, CancellationToken cancellationToken = default) @@ -422,6 +423,17 @@ Execute SQL files }); } + /*Preserve existing SQL Agent jobs if requested*/ + if (preserveJobs && fileName.StartsWith("45_", StringComparison.Ordinal)) + { + sqlContent = sqlContent.Replace("@preserve_jobs bit = 0", "@preserve_jobs bit = 1"); + progress?.Report(new InstallationProgress + { + Message = "Preserving existing SQL Agent jobs...", + Status = "Info" + }); + } + /*Remove SQLCMD directives*/ sqlContent = SqlCmdDirectivePattern.Replace(sqlContent, ""); diff --git a/README.md b/README.md index 9e37523..7318ae9 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ A GUI installer (`PerformanceMonitorInstallerGui.exe`) is also included in the r | `USERNAME PASSWORD` | SQL Authentication credentials (positional, optional) | | `--reinstall` | Drop existing database and perform clean install | | `--reset-schedule` | Reset collection schedule to recommended defaults during upgrade | +| `--preserve-jobs` | Keep existing SQL Agent jobs unchanged (owner, schedule, notifications) | | `--encrypt=optional\|mandatory\|strict` | Connection encryption level (default: mandatory) | | `--trust-cert` | Trust server certificate without validation (default: require valid cert) | diff --git a/install/45_create_agent_jobs.sql b/install/45_create_agent_jobs.sql index e7ce07c..c9fe080 100644 --- a/install/45_create_agent_jobs.sql +++ b/install/45_create_agent_jobs.sql @@ -21,9 +21,21 @@ GO /* Create SQL Server Agent Jobs for Performance Monitor These jobs automate data collection and retention + +When @preserve_jobs = 1, existing jobs are left untouched (owner, +schedule, notifications, etc.) and only missing jobs are created. +The installer sets this to 1 when --preserve-jobs is specified. */ +DECLARE + @preserve_jobs bit = 0; + PRINT 'Creating SQL Server Agent jobs for Performance Monitor'; + +IF @preserve_jobs = 1 +BEGIN + PRINT '(preserve mode — existing jobs will not be modified)'; +END; PRINT ''; /* @@ -31,11 +43,8 @@ Job 1: PerformanceMonitor - Collection Runs scheduled master collector every 1 minute The collector checks config.collection_schedule to determine which collectors should run */ - -/* -Drop existing job if it exists -*/ -IF EXISTS +IF @preserve_jobs = 0 +AND EXISTS ( SELECT 1/0 @@ -72,48 +81,55 @@ BEGIN PRINT 'Dropped existing PerformanceMonitor - Collection job'; END; -/* -Create the collection job -*/ -EXECUTE msdb.dbo.sp_add_job - @job_name = N'PerformanceMonitor - Collection', - @enabled = 1, - @description = N'Runs scheduled master collector to execute collectors based on config.collection_schedule', - @category_name = N'Data Collector'; - -EXECUTE msdb.dbo.sp_add_jobstep - @job_name = N'PerformanceMonitor - Collection', - @step_name = N'Run Scheduled Master Collector', - @subsystem = N'TSQL', - @database_name = N'PerformanceMonitor', - @command = N'EXECUTE collect.scheduled_master_collector @debug = 0;', - @retry_attempts = 0, - @on_success_action = 1; /*Quit with success*/ - -EXECUTE msdb.dbo.sp_add_jobschedule - @job_name = N'PerformanceMonitor - Collection', - @name = N'Every 1 Minute', - @freq_type = 4, /*Daily*/ - @freq_interval = 1, - @freq_subday_type = 4, /*Minutes*/ - @freq_subday_interval = 1; /*Every 1 minute*/ - -EXECUTE msdb.dbo.sp_add_jobserver - @job_name = N'PerformanceMonitor - Collection', - @server_name = N'(local)'; - -PRINT 'Created PerformanceMonitor - Collection job (runs every 1 minute)'; +IF NOT EXISTS +( + SELECT + 1/0 + FROM msdb.dbo.sysjobs AS j + WHERE j.name = N'PerformanceMonitor - Collection' +) +BEGIN + EXECUTE msdb.dbo.sp_add_job + @job_name = N'PerformanceMonitor - Collection', + @enabled = 1, + @description = N'Runs scheduled master collector to execute collectors based on config.collection_schedule', + @category_name = N'Data Collector'; + + EXECUTE msdb.dbo.sp_add_jobstep + @job_name = N'PerformanceMonitor - Collection', + @step_name = N'Run Scheduled Master Collector', + @subsystem = N'TSQL', + @database_name = N'PerformanceMonitor', + @command = N'EXECUTE collect.scheduled_master_collector @debug = 0;', + @retry_attempts = 0, + @on_success_action = 1; /*Quit with success*/ + + EXECUTE msdb.dbo.sp_add_jobschedule + @job_name = N'PerformanceMonitor - Collection', + @name = N'Every 1 Minute', + @freq_type = 4, /*Daily*/ + @freq_interval = 1, + @freq_subday_type = 4, /*Minutes*/ + @freq_subday_interval = 1; /*Every 1 minute*/ + + EXECUTE msdb.dbo.sp_add_jobserver + @job_name = N'PerformanceMonitor - Collection', + @server_name = N'(local)'; + + PRINT 'Created PerformanceMonitor - Collection job (runs every 1 minute)'; +END; +ELSE IF @preserve_jobs = 1 +BEGIN + PRINT 'PerformanceMonitor - Collection job already exists — preserving current settings'; +END; PRINT ''; /* Job 2: PerformanceMonitor - Data Retention Purges old performance monitoring data daily at 2am */ - -/* -Drop existing job if it exists -*/ -IF EXISTS +IF @preserve_jobs = 0 +AND EXISTS ( SELECT 1/0 @@ -150,47 +166,54 @@ BEGIN PRINT 'Dropped existing PerformanceMonitor - Data Retention job'; END; -/* -Create the data retention job -*/ -EXECUTE msdb.dbo.sp_add_job - @job_name = N'PerformanceMonitor - Data Retention', - @enabled = 1, - @description = N'Purges old performance monitoring data', - @category_name = N'Data Collector'; - -EXECUTE msdb.dbo.sp_add_jobstep - @job_name = N'PerformanceMonitor - Data Retention', - @step_name = N'Run Data Retention', - @subsystem = N'TSQL', - @database_name = N'PerformanceMonitor', - @command = N'EXECUTE config.data_retention @debug = 1;', - @retry_attempts = 0, - @on_success_action = 1; /*Quit with success*/ - -EXECUTE msdb.dbo.sp_add_jobschedule - @job_name = N'PerformanceMonitor - Data Retention', - @name = N'Daily at 2am', - @freq_type = 4, /*Daily*/ - @freq_interval = 1, - @active_start_time = 20000; /*2:00 AM*/ - -EXECUTE msdb.dbo.sp_add_jobserver - @job_name = N'PerformanceMonitor - Data Retention', - @server_name = N'(local)'; - -PRINT 'Created PerformanceMonitor - Data Retention job (runs daily at 2:00 AM)'; +IF NOT EXISTS +( + SELECT + 1/0 + FROM msdb.dbo.sysjobs AS j + WHERE j.name = N'PerformanceMonitor - Data Retention' +) +BEGIN + EXECUTE msdb.dbo.sp_add_job + @job_name = N'PerformanceMonitor - Data Retention', + @enabled = 1, + @description = N'Purges old performance monitoring data', + @category_name = N'Data Collector'; + + EXECUTE msdb.dbo.sp_add_jobstep + @job_name = N'PerformanceMonitor - Data Retention', + @step_name = N'Run Data Retention', + @subsystem = N'TSQL', + @database_name = N'PerformanceMonitor', + @command = N'EXECUTE config.data_retention @debug = 1;', + @retry_attempts = 0, + @on_success_action = 1; /*Quit with success*/ + + EXECUTE msdb.dbo.sp_add_jobschedule + @job_name = N'PerformanceMonitor - Data Retention', + @name = N'Daily at 2am', + @freq_type = 4, /*Daily*/ + @freq_interval = 1, + @active_start_time = 20000; /*2:00 AM*/ + + EXECUTE msdb.dbo.sp_add_jobserver + @job_name = N'PerformanceMonitor - Data Retention', + @server_name = N'(local)'; + + PRINT 'Created PerformanceMonitor - Data Retention job (runs daily at 2:00 AM)'; +END; +ELSE IF @preserve_jobs = 1 +BEGIN + PRINT 'PerformanceMonitor - Data Retention job already exists — preserving current settings'; +END; PRINT ''; /* Job 3: PerformanceMonitor - Hung Job Monitor Monitors the collection job for hung state every 5 minutes */ - -/* -Drop existing job if it exists -*/ -IF EXISTS +IF @preserve_jobs = 0 +AND EXISTS ( SELECT 1/0 @@ -227,42 +250,52 @@ BEGIN PRINT 'Dropped existing PerformanceMonitor - Hung Job Monitor job'; END; -/* -Create the hung job monitor job -*/ -EXECUTE msdb.dbo.sp_add_job - @job_name = N'PerformanceMonitor - Hung Job Monitor', - @enabled = 1, - @description = N'Monitors collection job for hung state and stops it if needed', - @category_name = N'Data Collector'; - -EXECUTE msdb.dbo.sp_add_jobstep - @job_name = N'PerformanceMonitor - Hung Job Monitor', - @step_name = N'Check for Hung Collection Job', - @subsystem = N'TSQL', - @database_name = N'PerformanceMonitor', - @command = N'EXECUTE config.check_hung_collector_job +IF NOT EXISTS +( + SELECT + 1/0 + FROM msdb.dbo.sysjobs AS j + WHERE j.name = N'PerformanceMonitor - Hung Job Monitor' +) +BEGIN + EXECUTE msdb.dbo.sp_add_job + @job_name = N'PerformanceMonitor - Hung Job Monitor', + @enabled = 1, + @description = N'Monitors collection job for hung state and stops it if needed', + @category_name = N'Data Collector'; + + EXECUTE msdb.dbo.sp_add_jobstep + @job_name = N'PerformanceMonitor - Hung Job Monitor', + @step_name = N'Check for Hung Collection Job', + @subsystem = N'TSQL', + @database_name = N'PerformanceMonitor', + @command = N'EXECUTE config.check_hung_collector_job @job_name = N''PerformanceMonitor - Collection'', @normal_max_duration_minutes = 5, @first_run_max_duration_minutes = 30, @stop_hung_job = 1, @debug = 0;', - @retry_attempts = 0, - @on_success_action = 1; /*Quit with success*/ - -EXECUTE msdb.dbo.sp_add_jobschedule - @job_name = N'PerformanceMonitor - Hung Job Monitor', - @name = N'Every 5 Minutes', - @freq_type = 4, /*Daily*/ - @freq_interval = 1, - @freq_subday_type = 4, /*Minutes*/ - @freq_subday_interval = 5; /*Every 5 minutes*/ - -EXECUTE msdb.dbo.sp_add_jobserver - @job_name = N'PerformanceMonitor - Hung Job Monitor', - @server_name = N'(local)'; - -PRINT 'Created PerformanceMonitor - Hung Job Monitor job (runs every 5 minutes)'; + @retry_attempts = 0, + @on_success_action = 1; /*Quit with success*/ + + EXECUTE msdb.dbo.sp_add_jobschedule + @job_name = N'PerformanceMonitor - Hung Job Monitor', + @name = N'Every 5 Minutes', + @freq_type = 4, /*Daily*/ + @freq_interval = 1, + @freq_subday_type = 4, /*Minutes*/ + @freq_subday_interval = 5; /*Every 5 minutes*/ + + EXECUTE msdb.dbo.sp_add_jobserver + @job_name = N'PerformanceMonitor - Hung Job Monitor', + @server_name = N'(local)'; + + PRINT 'Created PerformanceMonitor - Hung Job Monitor job (runs every 5 minutes)'; +END; +ELSE IF @preserve_jobs = 1 +BEGIN + PRINT 'PerformanceMonitor - Hung Job Monitor job already exists — preserving current settings'; +END; PRINT ''; /*