From 728f657112a21f80d1273f117f35af20dac40aca Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 28 Jan 2026 19:32:28 +0100 Subject: [PATCH 1/3] (feature): hyper-v backup dryRun mode --- examples/HyperV-Backup/README.md | 28 ++++- examples/HyperV-Backup/hyper-v-backup.bat | 4 +- examples/HyperV-Backup/hyper-v-backup.ps1 | 117 ++++++++++++++++----- examples/HyperV-Backup/scriptsettings.json | 12 ++- 4 files changed, 127 insertions(+), 34 deletions(-) diff --git a/examples/HyperV-Backup/README.md b/examples/HyperV-Backup/README.md index b10daef..d460f35 100644 --- a/examples/HyperV-Backup/README.md +++ b/examples/HyperV-Backup/README.md @@ -1,7 +1,7 @@ # Hyper-V Backup Script -**Version:** 1.0.1 -**Last Updated:** 2026-01-26 +**Version:** 1.0.2 +**Last Updated:** 2026-01-28 ## Overview @@ -16,6 +16,7 @@ Production-ready automated backup solution for Hyper-V virtual machines with sch - ✅ **Checkpoint Management** - Automatic cleanup of backup checkpoints (keeps last 2 for rollback) - ✅ **Space Validation** - Dynamic space checks for temp (per VM) and destination before copy - ✅ **VM Exclusion** - Exclude specific VMs from backup +- ✅ **Dry Run Mode** - Test backup detection without actual export/copy operations - ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels - ✅ **Lock Files** - Prevents concurrent execution - ✅ **Error Handling** - Proper exit codes and error reporting @@ -26,7 +27,7 @@ Production-ready automated backup solution for Hyper-V virtual machines with sch - Windows Server with Hyper-V role installed - PowerShell 5.1 or later - Administrator privileges -- Hyper-V PowerShell module +- Hyper-V PowerShell module (auto-installed if missing) ### Dependencies - `SchedulerTemplate.psm1` module (located in parent directory) @@ -65,7 +66,10 @@ HyperV-Backup/ "credentialEnvVar": "YOUR_ENV_VAR_NAME", "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, - "excludeVMs": ["vm-to-exclude"] + "excludeVMs": ["vm-to-exclude"], + "options": { + "dryRun": false + } } ``` @@ -89,6 +93,8 @@ HyperV-Backup/ .\hyper-v-backup.bat # or .\hyper-v-backup.ps1 + + # Test with dry run first (set dryRun: true in scriptsettings.json) ``` ## Configuration Reference @@ -112,6 +118,12 @@ HyperV-Backup/ | `retentionCount` | number | Yes | Number of backup generations to keep (1-365) | | `excludeVMs` | array | No | VM names to exclude from backup | +### Options + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `dryRun` | bool | `false` | Simulate backup without exporting or copying VMs | + ### Version Tracking | Property | Type | Description | @@ -346,6 +358,14 @@ Run with verbose output: - Performance improvement: Skip unnecessary space checks - Refactored parameter splatting for Invoke-ScheduledExecution +### 1.0.2 (2026-01-28) +- Added auto-installation of Hyper-V PowerShell module if missing +- Removed `#Requires -Modules Hyper-V` directive in favor of runtime installation +- Module installation uses `Enable-WindowsOptionalFeature` for the Microsoft-Hyper-V-Management-PowerShell feature +- Handles restart requirement notification if Windows feature installation requires reboot +- Added dry run mode (`options.dryRun`) to simulate backup without actual export/copy operations +- Restructured settings to use `options` object for consistency with other scripts + ## Support For issues or questions: diff --git a/examples/HyperV-Backup/hyper-v-backup.bat b/examples/HyperV-Backup/hyper-v-backup.bat index 1832a24..b99ce91 100644 --- a/examples/HyperV-Backup/hyper-v-backup.bat +++ b/examples/HyperV-Backup/hyper-v-backup.bat @@ -3,8 +3,8 @@ setlocal EnableDelayedExpansion REM ============================================================================ REM Hyper-V Backup Launcher -REM VERSION: 1.0.1 -REM DATE: 2026-01-26 +REM VERSION: 1.0.2 +REM DATE: 2026-01-28 REM DESCRIPTION: Batch file launcher for hyper-v-backup.ps1 with admin check REM ============================================================================ diff --git a/examples/HyperV-Backup/hyper-v-backup.ps1 b/examples/HyperV-Backup/hyper-v-backup.ps1 index 812a848..9a4a974 100644 --- a/examples/HyperV-Backup/hyper-v-backup.ps1 +++ b/examples/HyperV-Backup/hyper-v-backup.ps1 @@ -5,7 +5,6 @@ param ( ) #Requires -RunAsAdministrator -#Requires -Modules Hyper-V <# .SYNOPSIS @@ -13,18 +12,18 @@ param ( .DESCRIPTION Production-ready Hyper-V backup solution with scheduling, checkpoints, and retention management. .VERSION - 1.0.1 + 1.0.2 .DATE - 2026-01-26 + 2026-01-28 .NOTES - Requires Administrator privileges - - Requires Hyper-V PowerShell module + - Requires Hyper-V PowerShell module (auto-installed if missing) - Requires SchedulerTemplate.psm1 module #> # Script Version -$ScriptVersion = "1.0.1" -$ScriptDate = "2026-01-26" +$ScriptVersion = "1.0.2" +$ScriptDate = "2026-01-28" try { Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop @@ -68,6 +67,10 @@ $CredentialEnvVar = $settings.credentialEnvVar $TempExportRoot = $settings.tempExportRoot $RetentionCount = $settings.retentionCount $BlacklistedVMs = $settings.excludeVMs +$Options = $settings.options + +# Get DryRun from settings +$DryRun = $Options.dryRun # Schedule Configuration $Config = @{ @@ -106,9 +109,47 @@ $script:BackupStats = @{ # Helper Functions ========================================================= +function Test-HyperVModule { + param([switch]$Automated) + + Write-Log "Checking Hyper-V PowerShell module..." -Level Info -Automated:$Automated + + if (-not (Get-Module -ListAvailable -Name Hyper-V)) { + Write-Log "Hyper-V PowerShell module not found. Installing..." -Level Warning -Automated:$Automated + + try { + # Install Hyper-V PowerShell module (Windows Feature) + $result = Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -All -NoRestart -ErrorAction Stop + + if ($result.RestartNeeded) { + Write-Log "Hyper-V PowerShell module installed but requires system restart" -Level Warning -Automated:$Automated + Write-Log "Please restart the system and run the script again" -Level Info -Automated:$Automated + return $false + } + + Write-Log "Hyper-V PowerShell module installed successfully" -Level Success -Automated:$Automated + } + catch { + Write-Log "Failed to install Hyper-V PowerShell module: $_" -Level Error -Automated:$Automated + Write-Log "Please install manually: Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -All" -Level Info -Automated:$Automated + return $false + } + } + + try { + Import-Module Hyper-V -ErrorAction Stop + Write-Log "Hyper-V PowerShell module loaded" -Level Success -Automated:$Automated + return $true + } + catch { + Write-Log "Failed to import Hyper-V module: $_" -Level Error -Automated:$Automated + return $false + } +} + function Test-Prerequisites { param([switch]$Automated) - + Write-Log "Checking prerequisites..." -Level Info -Automated:$Automated # Check if running as Administrator @@ -119,8 +160,7 @@ function Test-Prerequisites { } # Check Hyper-V module - if (-not (Get-Module -ListAvailable -Name Hyper-V)) { - Write-Log "Hyper-V PowerShell module is not installed!" -Level Error -Automated:$Automated + if (-not (Test-HyperVModule -Automated:$Automated)) { return $false } @@ -332,21 +372,30 @@ function Backup-VM { Write-Log "Temp drive ${tempDrive}: has $([math]::Round($freeSpace / 1GB, 2)) GB free" -Level Info -Automated:$Automated } + # Dry run mode - skip actual backup operations + if ($DryRun) { + Write-Log "DRY RUN: Would export VM '$VMName' to temp location" -Level Warning -Automated:$Automated + Write-Log "DRY RUN: Would copy export to backup location: $vmBackupPath" -Level Warning -Automated:$Automated + Write-Log "=== DRY RUN: Backup simulated for VM: $VMName ===" -Level Warning -Automated:$Automated + $script:BackupStats.SkippedVMs++ + return $true + } + # Export VM to temp location (Export-VM creates its own checkpoint internally) $tempExportPath = Join-Path -Path $TempExportRoot -ChildPath "$VMName-$DateSuffix" Write-Log "Exporting VM '$VMName' to temp location: $tempExportPath" -Level Info -Automated:$Automated - + try { Export-VM -Name $VMName -Path $tempExportPath -ErrorAction Stop } catch { Write-Log "Failed to export VM '$VMName': $_" -Level Error -Automated:$Automated - + # Cleanup temp if export failed if (Test-Path $tempExportPath) { Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction SilentlyContinue } - + $script:BackupStats.FailedVMs++ $script:BackupStats.FailureMessages += "Export failed for $VMName" return $false @@ -394,12 +443,12 @@ function Backup-VM { } catch { Write-Log "Failed to copy VM '$VMName' to backup location: $_" -Level Error -Automated:$Automated - + # Cleanup partial backup if (Test-Path $vmBackupPath) { Remove-Item -Path $vmBackupPath -Recurse -Force -ErrorAction SilentlyContinue } - + $script:BackupStats.FailedVMs++ $script:BackupStats.FailureMessages += "Copy to NAS failed for $VMName" return $false @@ -409,7 +458,7 @@ function Backup-VM { # Cleanup temp export Write-Log "Cleaning up temp export for VM '$VMName'..." -Level Info -Automated:$Automated - + try { if (Test-Path $tempExportPath) { Remove-Item -Path $tempExportPath -Recurse -Force -ErrorAction Stop @@ -558,6 +607,9 @@ function Start-BusinessLogic { Write-Log "Hyper-V Backup Process Started" -Level Info -Automated:$Automated Write-Log "Script Version: $ScriptVersion ($ScriptDate)" -Level Info -Automated:$Automated Write-Log "Host: $Hostname" -Level Info -Automated:$Automated + if ($DryRun) { + Write-Log "DRY RUN MODE - No changes will be made" -Level Warning -Automated:$Automated + } Write-Log "========================================" -Level Info -Automated:$Automated # Check prerequisites @@ -606,13 +658,18 @@ function Start-BusinessLogic { $dateSuffix = Get-Date -Format "yyyyMMddHHmmss" $backupFolder = Join-Path -Path $BackupPath -ChildPath $dateSuffix - try { - New-Item -Path $backupFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null - Write-Log "Created backup folder: $backupFolder" -Level Success -Automated:$Automated + if ($DryRun) { + Write-Log "DRY RUN: Would create backup folder: $backupFolder" -Level Warning -Automated:$Automated } - catch { - Write-Log "Failed to create backup folder '$backupFolder': $_" -Level Error -Automated:$Automated - exit 1 + else { + try { + New-Item -Path $backupFolder -ItemType Directory -Force -ErrorAction Stop | Out-Null + Write-Log "Created backup folder: $backupFolder" -Level Success -Automated:$Automated + } + catch { + Write-Log "Failed to create backup folder '$backupFolder': $_" -Level Error -Automated:$Automated + exit 1 + } } # Process each VM @@ -628,11 +685,21 @@ function Start-BusinessLogic { Backup-VM -VMName $vmName -BackupFolder $backupFolder -DateSuffix $dateSuffix -Automated:$Automated } - # Cleanup old checkpoints - Remove-OldCheckpoints -VMs $vms -Automated:$Automated + # Cleanup old checkpoints (skip in dry run mode) + if ($DryRun) { + Write-Log "DRY RUN: Skipping checkpoint cleanup" -Level Warning -Automated:$Automated + } + else { + Remove-OldCheckpoints -VMs $vms -Automated:$Automated + } - # Cleanup old backups - Remove-OldBackups -BackupPath $BackupPath -RetentionCount $RetentionCount -Automated:$Automated + # Cleanup old backups (skip in dry run mode) + if ($DryRun) { + Write-Log "DRY RUN: Skipping old backup cleanup" -Level Warning -Automated:$Automated + } + else { + Remove-OldBackups -BackupPath $BackupPath -RetentionCount $RetentionCount -Automated:$Automated + } # Print summary Write-BackupSummary -Automated:$Automated diff --git a/examples/HyperV-Backup/scriptsettings.json b/examples/HyperV-Backup/scriptsettings.json index 971bf14..249a611 100644 --- a/examples/HyperV-Backup/scriptsettings.json +++ b/examples/HyperV-Backup/scriptsettings.json @@ -2,8 +2,8 @@ "$schema": "https://json-schema.org/draft-07/schema", "title": "Hyper-V Backup Script Settings", "description": "Configuration file for hyper-v-backup.ps1 script", - "version": "1.0.1", - "lastModified": "2026-01-26", + "version": "1.0.2", + "lastModified": "2026-01-28", "schedule": { "runMonth": [], "runWeekday": ["Monday"], @@ -15,6 +15,9 @@ "tempExportRoot": "D:\\Temp\\HyperVExport", "retentionCount": 3, "excludeVMs": ["nassrv0002"], + "options": { + "dryRun": false + }, "_comments": { "version": "Configuration schema version", "lastModified": "Last modification date (YYYY-MM-DD)", @@ -28,6 +31,9 @@ "credentialEnvVar": "Name of Machine-level environment variable containing Base64-encoded 'username:password'", "tempExportRoot": "Local directory for temporary VM exports. Space is checked dynamically per VM (1.5x VM size).", "retentionCount": "Number of backup generations to keep (1-365). Older backups are automatically deleted.", - "excludeVMs": "Array of VM names to exclude from backup process" + "excludeVMs": "Array of VM names to exclude from backup process", + "options": { + "dryRun": "Simulate backup without actually exporting or copying VMs" + } } } From 986eea321e0ef8b8a4fa7e5bc64ab1c47359cdf6 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 28 Jan 2026 20:09:35 +0100 Subject: [PATCH 2/3] (feature): windows-update script example --- examples/SchedulerTemplate.psd1 | 11 +- examples/SchedulerTemplate.psm1 | 72 ++- examples/Windows-Update/README.md | 513 ++++++++++++++++++ .../Utilities/windows-update-policy.ps1 | 158 ++++++ examples/Windows-Update/scriptsettings.json | 83 +++ examples/Windows-Update/windows-update.bat | 74 +++ examples/Windows-Update/windows-update.ps1 | 480 ++++++++++++++++ 7 files changed, 1385 insertions(+), 6 deletions(-) create mode 100644 examples/Windows-Update/README.md create mode 100644 examples/Windows-Update/Utilities/windows-update-policy.ps1 create mode 100644 examples/Windows-Update/scriptsettings.json create mode 100644 examples/Windows-Update/windows-update.bat create mode 100644 examples/Windows-Update/windows-update.ps1 diff --git a/examples/SchedulerTemplate.psd1 b/examples/SchedulerTemplate.psd1 index b52e640..9c2395b 100644 --- a/examples/SchedulerTemplate.psd1 +++ b/examples/SchedulerTemplate.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'SchedulerTemplate.psm1' - ModuleVersion = '1.0.1' + ModuleVersion = '1.0.2' GUID = 'a3b2c1d0-e4f5-6a7b-8c9d-0e1f2a3b4c5d' Author = 'MaksIT' CompanyName = 'MaksIT' @@ -20,6 +20,7 @@ 'Test-ScheduledExecution', 'New-LockGuard', 'Remove-LockGuard', + 'Send-EmailNotification', 'Invoke-ScheduledExecution' ) CmdletsToExport = @() @@ -27,13 +28,19 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ - Tags = @('Scheduler', 'Automation', 'Lock', 'Logging', 'Credentials') + Tags = @('Scheduler', 'Automation', 'Lock', 'Logging', 'Credentials', 'Email') LicenseUri = '' ProjectUri = 'https://github.com/MaksIT/uscheduler' ReleaseNotes = @' +## 1.0.2 (2026-01-28) +- Added Send-EmailNotification function for SMTP email sending +- Supports SSL/TLS and credential-based authentication + ## 1.0.1 (2026-01-26) - Improved UNC path validation (Test-UNCPath function) - Enhanced credential management + +## 1.0.0 (2026-01-24) - Comprehensive logging with timestamp support - Scheduled execution with lock files and interval control - Schedule validation (month, weekday, time) diff --git a/examples/SchedulerTemplate.psm1 b/examples/SchedulerTemplate.psm1 index a1f43a5..638b02e 100644 --- a/examples/SchedulerTemplate.psm1 +++ b/examples/SchedulerTemplate.psm1 @@ -5,19 +5,20 @@ Reusable PowerShell module for scheduled script execution with lock files, interval control, and credential management. .VERSION - 1.0.1 + 1.0.2 .DATE - 2026-01-26 + 2026-01-28 .NOTES - Provides Write-Log function with timestamp and level support - Provides Get-CredentialFromEnvVar for secure credential retrieval - Provides Test-UNCPath for UNC path validation + - Provides Send-EmailNotification for SMTP email sending - Provides Invoke-ScheduledExecution for scheduled task management #> # Module Version (exported for external scripts to check version) -$script:ModuleVersion = "1.0.1" -$script:ModuleDate = "2026-01-26" +$script:ModuleVersion = "1.0.2" +$script:ModuleDate = "2026-01-28" # Module load confirmation Write-Verbose "SchedulerTemplate.psm1 v$ModuleVersion loaded ($ModuleDate)" @@ -217,6 +218,69 @@ function Remove-LockGuard { } } +# ====================================================================== +# Email Notification +# ====================================================================== +function Send-EmailNotification { + param( + [Parameter(Mandatory = $true)] + [hashtable]$EmailSettings, + [Parameter(Mandatory = $true)] + [string]$Subject, + [Parameter(Mandatory = $true)] + [string]$Body, + [switch]$Automated + ) + + # Validate required settings + $required = @('smtpServer', 'smtpPort', 'from', 'to') + foreach ($key in $required) { + if (-not $EmailSettings.$key) { + Write-Log "Email setting '$key' is required but missing" -Level Error -Automated:$Automated + return $false + } + } + + try { + $mailParams = @{ + SmtpServer = $EmailSettings.smtpServer + Port = $EmailSettings.smtpPort + From = $EmailSettings.from + To = $EmailSettings.to + Subject = $Subject + Body = $Body + BodyAsHtml = $false + } + + # Add SSL if configured + if ($EmailSettings.useSSL) { + $mailParams['UseSsl'] = $true + } + + # Add credentials if configured + if ($EmailSettings.credentialEnvVar) { + $creds = Get-CredentialFromEnvVar -EnvVarName $EmailSettings.credentialEnvVar -Automated:$Automated + if ($creds) { + $securePassword = ConvertTo-SecureString $creds.Password -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($creds.Username, $securePassword) + $mailParams['Credential'] = $credential + } + else { + Write-Log "Failed to retrieve email credentials from '$($EmailSettings.credentialEnvVar)'" -Level Error -Automated:$Automated + return $false + } + } + + Send-MailMessage @mailParams -ErrorAction Stop + Write-Log "Email sent successfully to $($EmailSettings.to -join ', ')" -Level Success -Automated:$Automated + return $true + } + catch { + Write-Log "Failed to send email: $_" -Level Error -Automated:$Automated + return $false + } +} + # ====================================================================== # Main unified executor (callback-based) # ====================================================================== diff --git a/examples/Windows-Update/README.md b/examples/Windows-Update/README.md new file mode 100644 index 0000000..1461c18 --- /dev/null +++ b/examples/Windows-Update/README.md @@ -0,0 +1,513 @@ +# Windows Update Script + +**Version:** 1.0.0 +**Last Updated:** 2026-01-28 + +## Overview + +Production-ready Windows Update automation solution using PowerShell and PSWindowsUpdate module. Supports scheduled updates, category filtering, exclusions, pre/post checks, and auto-reboot with maintenance window control. + +## Features + +- ✅ **PSWindowsUpdate Integration** - Uses reliable PSWindowsUpdate module +- ✅ **Multiple Categories** - Critical, Security, Definition, and more +- ✅ **Smart Exclusions** - Exclude by KB number or title patterns +- ✅ **Pre-flight Checks** - Disk space, pending reboot, service status +- ✅ **Reboot Control** - Optional automatic reboot with delay +- ✅ **Update Reports** - Generate post-installation reports with optional email notifications +- ✅ **Dry Run Mode** - Test update detection without installation +- ✅ **Detailed Logging** - Comprehensive logging with timestamps and severity levels +- ✅ **Lock Files** - Prevents concurrent execution +- ✅ **Flexible Scheduling** - Schedule updates by month, weekday, and time + +## Requirements + +### System Requirements +- Windows 10/11 or Windows Server 2016+ +- PowerShell 5.1 or later +- Administrator privileges +- Internet access for Windows Update + +### Dependencies +- `PSWindowsUpdate` module (auto-installed if missing) +- `SchedulerTemplate.psm1` module (located in parent directory) +- `scriptsettings.json` configuration file + +## File Structure + +``` +Windows-Update/ +├── windows-update.bat # Batch launcher with admin check +├── windows-update.ps1 # Main PowerShell script +├── scriptsettings.json # Configuration file +├── README.md # This file +└── Utilities/ + └── windows-update-policy.ps1 # Configure Windows Update behavior +``` + +## Installation + +1. **Copy Files** + ```powershell + # Copy the entire Windows-Update folder to your desired location + # Ensure SchedulerTemplate.psm1 is in the parent directory + ``` + +2. **Configure Settings** + + Edit `scriptsettings.json` with your preferences: + ```json + { + "schedule": { + "runWeekday": ["Wednesday"], + "runTime": ["02:00"] + }, + "updateCategories": [ + "Critical Updates", + "Security Updates" + ] + } + ``` + +3. **Test Manual Execution** + ```powershell + # Run as Administrator + .\windows-update.bat + # or + .\windows-update.ps1 + + # Test with dry run first (set dryRun: true in scriptsettings.json) + ``` + +## Configuration Reference + +### Schedule Settings + +| Property | Type | Description | Example | +|----------|------|-------------|---------| +| `runMonth` | array | Month names to run. Empty = every month | `["January", "July"]` or `[]` | +| `runWeekday` | array | Weekday names to run. Empty = every day | `["Wednesday"]` | +| `runTime` | array | UTC times to run (HH:mm format) | `["02:00", "14:00"]` | +| `minIntervalMinutes` | number | Minimum minutes between runs | `60` | + +### Update Categories + +Available categories to install: + +| Category | Description | +|----------|-------------| +| `Critical Updates` | Critical security and stability updates | +| `Security Updates` | Security-focused updates | +| `Definition Updates` | Antivirus and malware definition updates | +| `Update Rollups` | Cumulative update packages | +| `Feature Packs` | New feature additions | +| `Service Packs` | Major cumulative updates | +| `Tools` | System tools and utilities | +| `Drivers` | Hardware driver updates | + +**Example:** +```json +{ + "updateCategories": [ + "Critical Updates", + "Security Updates", + "Definition Updates" + ] +} +``` + +### Exclusions + +| Property | Type | Description | Example | +|----------|------|-------------|---------| +| `kbNumbers` | array | KB numbers to exclude | `["KB5034441", "KB5034123"]` | +| `titlePatterns` | array | Wildcard patterns to exclude | `["*Preview*", "*Beta*"]` | + +**Pattern Syntax:** +- Use wildcards with `-like` operator (e.g., `*Preview*`, `*Optional*`) +- KB numbers should include the `KB` prefix + +> **Note:** Filter matching uses PowerShell's `-like` operator. Test with `dryRun: true` first to verify expected behavior. + +### Pre-Checks + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `minDiskSpaceGB` | number | `10` | Minimum free disk space in GB | +| `checkPendingReboot` | bool | `true` | Check for pending reboot before updates | + +### Options + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `rebootBehavior` | string | `"manual"` | Reboot behavior: `"never"`, `"manual"`, or `"auto"` | +| `rebootDelayMinutes` | number | `5` | Minutes to wait before auto-reboot (when `rebootBehavior` is `"auto"`) | +| `dryRun` | bool | `false` | Simulate without installing updates | + +**Reboot Behavior Values:** +| Value | Description | +|-------|-------------| +| `"never"` | Abort if system has pending reboot or updates require reboot | +| `"manual"` | Continue with updates, log that reboot is needed, user reboots later | +| `"auto"` | Automatically reboot after configured delay when updates require it | + +### Reporting + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `generateReport` | bool | `true` | Generate text report after updates | +| `emailNotification` | bool | `false` | Send email notification after updates | +| `emailSettings` | object | - | SMTP configuration for email notifications | + +**Email Settings:** +| Property | Type | Description | +|----------|------|-------------| +| `smtpServer` | string | SMTP server hostname | +| `smtpPort` | number | SMTP port (587 for TLS, 465 for SSL, 25 for plain) | +| `from` | string | Sender email address | +| `to` | array | Recipient email addresses | +| `useSSL` | bool | Use SSL/TLS for connection | +| `credentialEnvVar` | string | Machine-level env var with Base64(`username:password`). Empty for no auth. | + +**Example:** +```json +{ + "reporting": { + "generateReport": true, + "emailNotification": true, + "emailSettings": { + "smtpServer": "smtp.office365.com", + "smtpPort": 587, + "from": "updates@example.com", + "to": ["admin@example.com"], + "useSSL": true, + "credentialEnvVar": "SMTP_CREDENTIALS" + } + } +} +``` + +## Usage + +### Manual Execution + +**Using Batch File (Recommended):** +```batch +REM Right-click and select "Run as administrator" +windows-update.bat +``` + +**Using PowerShell:** +```powershell +# Run as Administrator +.\windows-update.ps1 + +# With verbose output +.\windows-update.ps1 -Verbose +``` + +### Automated Execution + +The script supports automated execution through the UScheduler service: + +```powershell +# Called by scheduler with -Automated flag +.\windows-update.ps1 -Automated -CurrentDateTimeUtc "2026-01-28 02:00:00" +``` + +When `-Automated` is specified: +- Schedule is enforced (month, weekday, time) +- Lock files prevent concurrent execution +- Interval checking prevents duplicate runs +- Logs are formatted for service logger (no timestamps) + +## How It Works + +### Update Process Flow + +1. **Initialization** + - Load SchedulerTemplate.psm1 module + - Load and validate scriptsettings.json + - Check/install PSWindowsUpdate module + +2. **Pre-flight Checks** + - Verify disk space availability + - Check for pending reboot + - Verify Windows Update service is running + +3. **Scan Phase** + - Query available updates from Windows Update + - Filter by configured categories + - Apply exclusions (KB numbers, title patterns) + +4. **Installation Phase** + - Display list of updates to install + - Execute update installation + - Track installation results (success/failure) + - Monitor reboot requirements + +5. **Post-Update Actions** + - Handle reboot if required and configured + - Generate update report + +6. **Summary** + - Display installation statistics + - Report errors and warnings + +### Reboot Behavior + +| rebootBehavior | Behavior | +|----------------|----------| +| `"never"` | Abort if reboot pending/required | +| `"manual"` | Continue, log that reboot is needed | +| `"auto"` | Automatically reboot after delay | + +### Progress Output + +``` +[INFO] ========================================== +[INFO] Windows Update Process Started +[INFO] Script Version: 1.0.0 (2026-01-28) +[INFO] ========================================== +[SUCCESS] PSWindowsUpdate module loaded +[INFO] Running pre-update checks... +[INFO] Free space on C: : 125.50 GB +[SUCCESS] Pre-update checks passed +[INFO] Scanning for available updates... +[INFO] Found 5 update(s) to install +[INFO] ======================================== +[INFO] [KB5034441] 2024-01 Cumulative Update (15234.50 KB) +[INFO] [KB5034123] Security Intelligence Update (8765.25 KB) +[INFO] ======================================== +[INFO] Installing updates... +[SUCCESS] Installed: 2024-01 Cumulative Update for Windows 11 +[SUCCESS] Installed: Security Intelligence Update +``` + +### Update Summary + +``` +======================================== +UPDATE SUMMARY +======================================== +Start Time : 2026-01-28 02:00:00 +End Time : 2026-01-28 02:15:32 +Duration : 0h 15m 32s +Status : SUCCESS + +Installed : 5 +Failed : 0 +Skipped : 0 +Reboot Needed : True +======================================== +``` + +## Logging + +### Log Levels + +| Level | Description | Color (Manual) | +|-------|-------------|----------------| +| `Info` | Informational messages | White | +| `Success` | Successful operations | Green | +| `Warning` | Non-critical issues | Yellow | +| `Error` | Critical errors | Red | + +### Log Format + +**Manual Execution:** +``` +[2026-01-28 02:00:00] [Info] Windows Update Process Started +[2026-01-28 02:15:32] [Success] Updates completed successfully +``` + +**Automated Execution:** +``` +[Info] Windows Update Process Started +[Success] Updates completed successfully +``` + +## Exit Codes + +| Code | Description | +|------|-------------| +| `0` | Success (no errors) | +| `1` | Error occurred (config, checks, installation errors) | + +## Troubleshooting + +### Common Issues + +**1. PSWindowsUpdate Module Not Found** +``` +Error: Failed to import PSWindowsUpdate module +``` +**Solution:** +```powershell +# Install manually +Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers +``` + +**2. Insufficient Permissions** +``` +Error: This script must be run as Administrator +``` +**Solution:** Right-click the batch file and select "Run as administrator" + +**3. Insufficient Disk Space** +``` +Error: Insufficient disk space. Required: 10 GB, Available: 8.5 GB +``` +**Solution:** +- Free up disk space +- Reduce `minDiskSpaceGB` in configuration (not recommended) + +**4. Windows Update Service Not Running** +``` +Error: Failed to start Windows Update service +``` +**Solution:** +```powershell +# Start service manually +Start-Service -Name wuauserv + +# Check service status +Get-Service -Name wuauserv +``` + +**5. Updates Fail to Install** +``` +Error: Failed: 2024-01 Cumulative Update +``` +**Solution:** +- Check Windows Update logs: `C:\Windows\Logs\WindowsUpdate` +- Run Windows Update Troubleshooter +- Check for conflicting software (antivirus, etc.) +- Review exclusions - update might be excluded by pattern + +**6. Lock File Exists** +``` +Guard: Lock file exists. Skipping. +``` +**Solution:** +- Another instance is running, or previous run didn't complete +- Manually delete `.lock` file if stuck +- Check for hung PowerShell processes + +**7. Pending Reboot Blocks Updates** +``` +Error: Reboot required but rebootBehavior is 'never' +``` +**Solution:** +- Set `rebootBehavior: "manual"` or `"auto"` in configuration +- Manually reboot system before running updates + +### Debug Mode + +Run with verbose output: +```powershell +.\windows-update.ps1 -Verbose +``` + +Test without installing updates (set in scriptsettings.json): +```json +{ + "options": { + "dryRun": true + } +} +``` + +## Best Practices + +1. **Test First** - Always test with `dryRun: true` before actual execution +2. **Schedule Wisely** - Run during maintenance windows (nights, weekends) +3. **Start Conservative** - Begin with Critical/Security updates only +4. **Monitor Results** - Review update reports and logs regularly +5. **Backup First** - Ensure system backups before major updates +6. **Reboot Testing** - Test `rebootBehavior: "auto"` in non-production environment first +7. **Exclusion Management** - Keep exclusions list minimal and documented +8. **Review Failures** - Investigate and resolve failed updates promptly + +## Security Considerations + +- Script requires **Administrator privileges** for update installation +- Updates are downloaded from **Microsoft Update** servers only +- Consider using **dedicated service account** for automated execution +- **Lock files** prevent concurrent execution and potential conflicts +- **Audit logs** maintain record of all update activities + +## Performance Considerations + +- **Update scanning** typically takes 1-5 minutes +- **Download speed** depends on update size and internet connection +- **Installation time** varies by update type (minutes to hours) +- **Reboot time** adds 2-10 minutes to total duration +- **Definition updates** are typically fast (<1 minute) +- **Feature updates** can take 30+ minutes + +### Typical Update Times + +| Update Type | Download | Install | Reboot | +|-------------|----------|---------|--------| +| Definition Updates | <1 min | <1 min | No | +| Security Updates | 2-5 min | 5-10 min | Sometimes | +| Cumulative Updates | 5-15 min | 10-30 min | Yes | +| Feature Updates | 15-60 min | 30-120 min | Yes | + +## Version History + +### 1.0.0 (2026-01-28) +- Initial release +- PSWindowsUpdate integration +- Category-based filtering +- KB number and title pattern exclusions +- Pre-flight checks (disk space, pending reboot, service) +- Auto-reboot with configurable delay +- Update report generation +- Dry run mode +- Integration with SchedulerTemplate.psm1 + +## Support + +For issues or questions: +1. Check the [Troubleshooting](#troubleshooting) section +2. Review script logs for error details +3. Verify all [Requirements](#requirements) are met + +## License + +See [LICENSE](../../LICENSE.md) in the root directory. + +## Utilities + +### windows-update-policy.ps1 + +Located in `Utilities/`, this script configures Windows Update to use server-style manual updates (notify-only mode with no automatic reboots). + +**Apply server-style policy:** +```powershell +.\Utilities\windows-update-policy.ps1 +``` + +**Revert to Windows defaults:** +```powershell +.\Utilities\windows-update-policy.ps1 -Revert +``` + +**What it does:** +- Sets `AUOptions = 2` (notify for download and install) +- Disables automatic reboots with logged-on users +- Disables scheduled auto-reboot +- Disables automatic maintenance updates +- Uses Microsoft Update servers + +This is useful when you want full control over when updates are downloaded, installed, and when the system reboots - particularly for workstations that should behave like servers. + +## Related Files + +- `../SchedulerTemplate.psm1` - Shared scheduling and logging module +- `scriptsettings.json` - Configuration file +- `windows-update.bat` - Batch launcher +- `windows-update.ps1` - Main script +- `Utilities/windows-update-policy.ps1` - Windows Update policy configuration diff --git a/examples/Windows-Update/Utilities/windows-update-policy.ps1 b/examples/Windows-Update/Utilities/windows-update-policy.ps1 new file mode 100644 index 0000000..a52b0f3 --- /dev/null +++ b/examples/Windows-Update/Utilities/windows-update-policy.ps1 @@ -0,0 +1,158 @@ +[CmdletBinding()] +param ( + [switch]$Revert +) + +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Configure Windows Update to Server 2025-style manual updates. +.DESCRIPTION + Sets Windows Update behavior to notify-only mode with no automatic reboots. + This gives you full control over when updates are downloaded, installed, and rebooted. +.PARAMETER Revert + Remove the policy settings and restore Windows defaults. +.EXAMPLE + .\windows-update-policy.ps1 + Apply server-style update policy. +.EXAMPLE + .\windows-update-policy.ps1 -Revert + Remove policy and restore defaults. +.VERSION + 1.0.0 +.DATE + 2026-01-28 +.NOTES + - Requires Administrator privileges + - Changes take effect after gpupdate or reboot +#> + +# Registry paths +$WUBase = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' +$WU_AU = Join-Path $WUBase 'AU' + +# Helper Functions ========================================================= + +function Set-ServerStyleUpdates { + Write-Host "[INFO] Applying server-style Windows Update policy..." -ForegroundColor Cyan + + # Create policy keys + New-Item -Path $WUBase -Force | Out-Null + New-Item -Path $WU_AU -Force | Out-Null + + # AUOptions = 2: Notify for download and notify for install + New-ItemProperty -Path $WU_AU -Name 'AUOptions' -PropertyType DWord -Value 2 -Force | Out-Null + Write-Host " - AUOptions = 2 (Notify for download and install)" -ForegroundColor Gray + + # Do not auto reboot with logged-on users + New-ItemProperty -Path $WU_AU -Name 'NoAutoRebootWithLoggedOnUsers' -PropertyType DWord -Value 1 -Force | Out-Null + Write-Host " - NoAutoRebootWithLoggedOnUsers = 1" -ForegroundColor Gray + + # Prevent any scheduled auto reboot + New-ItemProperty -Path $WU_AU -Name 'AlwaysAutoRebootAtScheduledTime' -PropertyType DWord -Value 0 -Force | Out-Null + Write-Host " - AlwaysAutoRebootAtScheduledTime = 0" -ForegroundColor Gray + + # Disable automatic maintenance updates + New-ItemProperty -Path $WU_AU -Name 'AutomaticMaintenanceEnabled' -PropertyType DWord -Value 0 -Force | Out-Null + Write-Host " - AutomaticMaintenanceEnabled = 0" -ForegroundColor Gray + + # Disable auto restart reminders + New-ItemProperty -Path $WU_AU -Name 'RebootWarningTimeoutEnabled' -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $WU_AU -Name 'RebootRelaunchTimeoutEnabled' -PropertyType DWord -Value 0 -Force | Out-Null + Write-Host " - RebootWarningTimeoutEnabled = 0" -ForegroundColor Gray + Write-Host " - RebootRelaunchTimeoutEnabled = 0" -ForegroundColor Gray + + # Set empty WUServer/WUStatusServer (use Microsoft Update) + New-ItemProperty -Path $WUBase -Name 'WUServer' -PropertyType String -Value '' -Force | Out-Null + New-ItemProperty -Path $WUBase -Name 'WUStatusServer' -PropertyType String -Value '' -Force | Out-Null + Write-Host " - WUServer/WUStatusServer = '' (Microsoft Update)" -ForegroundColor Gray + + Write-Host "[SUCCESS] Server-style Windows Update policy applied." -ForegroundColor Green +} + +function Remove-ServerStyleUpdates { + Write-Host "[INFO] Removing server-style Windows Update policy..." -ForegroundColor Cyan + + # Remove WUBase properties + foreach ($name in @('WUServer', 'WUStatusServer')) { + Remove-ItemProperty -Path $WUBase -Name $name -ErrorAction SilentlyContinue + } + + # Remove AU properties + foreach ($name in @( + 'AUOptions', + 'NoAutoRebootWithLoggedOnUsers', + 'AlwaysAutoRebootAtScheduledTime', + 'AutomaticMaintenanceEnabled', + 'RebootWarningTimeoutEnabled', + 'RebootRelaunchTimeoutEnabled' + )) { + Remove-ItemProperty -Path $WU_AU -Name $name -ErrorAction SilentlyContinue + } + + # Clean up empty keys + try { + $auProps = Get-ItemProperty -Path $WU_AU -ErrorAction SilentlyContinue + if ($auProps) { + $members = $auProps | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -notlike 'PS*' } + if (-not $members) { + Remove-Item $WU_AU -Force -ErrorAction SilentlyContinue + } + } + } + catch { } + + Write-Host "[SUCCESS] Server-style Windows Update policy removed." -ForegroundColor Green +} + +function Invoke-PolicyRefresh { + Write-Host "[INFO] Refreshing group policy..." -ForegroundColor Cyan + + try { + gpupdate /force 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host "[SUCCESS] Group policy refreshed." -ForegroundColor Green + } + else { + throw "gpupdate returned exit code $LASTEXITCODE" + } + } + catch { + Write-Host "[WARNING] gpupdate failed. Restarting Windows Update service..." -ForegroundColor Yellow + Stop-Service wuauserv -Force -ErrorAction SilentlyContinue + Start-Service wuauserv -ErrorAction SilentlyContinue + Write-Host "[INFO] Windows Update service restarted." -ForegroundColor Cyan + } +} + +# Main ===================================================================== + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Windows Update Policy Configuration" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +if ($Revert) { + Remove-ServerStyleUpdates + Write-Host "" + Invoke-PolicyRefresh + Write-Host "" + Write-Host "[INFO] Windows Update will now use default behavior." -ForegroundColor Cyan + Write-Host "[INFO] You may need to reboot for all changes to take effect." -ForegroundColor Yellow +} +else { + Set-ServerStyleUpdates + Write-Host "" + Invoke-PolicyRefresh + Write-Host "" + Write-Host "[INFO] Windows Update is now configured for manual control:" -ForegroundColor Cyan + Write-Host " - Updates will notify but not auto-download" -ForegroundColor Gray + Write-Host " - No automatic reboots will occur" -ForegroundColor Gray + Write-Host " - Use windows-update.ps1 to manually install updates" -ForegroundColor Gray + Write-Host "" + Write-Host "[INFO] You may need to reboot for all changes to take effect." -ForegroundColor Yellow +} + +Write-Host "" diff --git a/examples/Windows-Update/scriptsettings.json b/examples/Windows-Update/scriptsettings.json new file mode 100644 index 0000000..b09d06f --- /dev/null +++ b/examples/Windows-Update/scriptsettings.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Windows Update Script Settings", + "description": "Configuration file for windows-update.ps1 script (automated Windows Update management)", + "version": "1.0.0", + "lastModified": "2026-01-28", + "schedule": { + "runMonth": [], + "runWeekday": ["Wednesday"], + "runTime": ["02:00"], + "minIntervalMinutes": 60 + }, + "updateCategories": [ + "Critical Updates", + "Security Updates", + "Definition Updates", + "Update Rollups" + ], + "exclusions": { + "kbNumbers": [], + "titlePatterns": [ + "*Preview*", + "*Beta*" + ] + }, + "preChecks": { + "minDiskSpaceGB": 10, + "checkPendingReboot": true + }, + "options": { + "rebootBehavior": "manual", + "rebootDelayMinutes": 5, + "dryRun": false + }, + "reporting": { + "generateReport": true, + "emailNotification": false, + "emailSettings": { + "smtpServer": "smtp.example.com", + "smtpPort": 587, + "from": "windows-update@example.com", + "to": ["admin@example.com"], + "useSSL": true, + "credentialEnvVar": "" + } + }, + "_comments": { + "version": "Configuration schema version", + "lastModified": "Last modification date (YYYY-MM-DD)", + "schedule": { + "runMonth": "Array of month names (e.g. 'January', 'June') to run updates. Empty array = every month.", + "runWeekday": "Array of weekday names (e.g. 'Wednesday', 'Sunday') to run updates. Empty array = every day.", + "runTime": "Array of UTC times in HH:mm format when updates should run.", + "minIntervalMinutes": "Minimum minutes between update runs to prevent duplicate executions." + }, + "updateCategories": "Array of update categories to install. Available: 'Critical Updates', 'Security Updates', 'Definition Updates', 'Update Rollups', 'Feature Packs', 'Service Packs', 'Tools', 'Drivers'", + "exclusions": { + "kbNumbers": "Array of KB numbers to exclude (e.g. 'KB5034441')", + "titlePatterns": "Array of wildcard patterns to exclude by title (e.g. '*Preview*', '*Optional*')" + }, + "preChecks": { + "minDiskSpaceGB": "Minimum free disk space in GB required before installing updates", + "checkPendingReboot": "Check if system has pending reboot from previous updates" + }, + "options": { + "rebootBehavior": "Reboot behavior: 'never' (abort if reboot needed), 'manual' (continue, user reboots later), 'auto' (reboot automatically after delay)", + "rebootDelayMinutes": "Minutes to wait before auto-reboot when rebootBehavior is 'auto' (0 = immediate)", + "dryRun": "Simulate update installation without making changes" + }, + "reporting": { + "generateReport": "Generate text report after update installation", + "emailNotification": "Send email notification after updates (requires emailSettings)", + "emailSettings": { + "smtpServer": "SMTP server hostname", + "smtpPort": "SMTP port (typically 587 for TLS, 465 for SSL, 25 for plain)", + "from": "Sender email address", + "to": "Array of recipient email addresses", + "useSSL": "Use SSL/TLS for connection", + "credentialEnvVar": "Machine-level environment variable containing Base64('username:password'). Empty for no auth." + } + } + } +} \ No newline at end of file diff --git a/examples/Windows-Update/windows-update.bat b/examples/Windows-Update/windows-update.bat new file mode 100644 index 0000000..8f81344 --- /dev/null +++ b/examples/Windows-Update/windows-update.bat @@ -0,0 +1,74 @@ +@echo off +setlocal EnableDelayedExpansion + +REM ============================================================================ +REM Windows Update Launcher +REM VERSION: 1.0.0 +REM DATE: 2026-01-28 +REM DESCRIPTION: Batch file launcher for windows-update.ps1 with admin check +REM ============================================================================ + +echo. +echo ============================================ +echo Windows Update Automation Launcher +echo ============================================ +echo. + +REM Check for Administrator privileges +net session >nul 2>&1 +if %errorLevel% NEQ 0 ( + echo [ERROR] This script must be run as Administrator! + echo. + echo Please right-click and select "Run as administrator" + echo. + pause + exit /b 1 +) + +echo [OK] Running with Administrator privileges +echo. + +REM Get script directory +set "SCRIPT_DIR=%~dp0" +set "PS_SCRIPT=%SCRIPT_DIR%windows-update.ps1" + +REM Check if PowerShell script exists +if not exist "%PS_SCRIPT%" ( + echo [ERROR] PowerShell script not found: %PS_SCRIPT% + echo. + pause + exit /b 1 +) + +echo [OK] Found PowerShell script: %PS_SCRIPT% +echo. +echo ============================================ +echo Starting Windows Update process... +echo ============================================ +echo. + +REM Execute PowerShell script +REM Note: Logging is handled by UScheduler service +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS_SCRIPT%" + +REM Capture exit code +set "EXIT_CODE=%ERRORLEVEL%" + +echo. +echo ============================================ +echo Windows Update process completed +echo Exit Code: %EXIT_CODE% +echo ============================================ +echo. + +if %EXIT_CODE% EQU 0 ( + echo [SUCCESS] Updates completed successfully +) else ( + echo [ERROR] Updates completed with errors +) + +echo. +pause + +endlocal +exit /b %EXIT_CODE% \ No newline at end of file diff --git a/examples/Windows-Update/windows-update.ps1 b/examples/Windows-Update/windows-update.ps1 new file mode 100644 index 0000000..0a986bb --- /dev/null +++ b/examples/Windows-Update/windows-update.ps1 @@ -0,0 +1,480 @@ +[CmdletBinding()] +param ( + [switch]$Automated, + [string]$CurrentDateTimeUtc +) + +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Automated Windows Update management with scheduling and reporting. +.DESCRIPTION + Production-ready Windows Update automation using PSWindowsUpdate module. + Supports scheduled updates, category filtering, exclusions, pre/post checks, + and auto-reboot with maintenance window control. +.VERSION + 1.0.0 +.DATE + 2026-01-28 +.NOTES + - Requires PSWindowsUpdate module (auto-installed if missing) + - Requires SchedulerTemplate.psm1 module +#> + +# Script Version +$ScriptVersion = "1.0.0" +$ScriptDate = "2026-01-28" + +try { + Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop +} +catch { + Write-Error "Failed to load SchedulerTemplate.psm1: $_" + exit 1 +} + +# Load Settings ============================================================ + +$settingsFile = Join-Path $PSScriptRoot "scriptsettings.json" + +if (-not (Test-Path $settingsFile)) { + Write-Error "Settings file not found: $settingsFile" + exit 1 +} + +try { + $settings = Get-Content $settingsFile -Raw | ConvertFrom-Json + Write-Verbose "Loaded settings from $settingsFile" +} +catch { + Write-Error "Failed to load settings from $settingsFile : $_" + exit 1 +} + +# Process Settings ========================================================= + +# Validate required settings +$requiredSettings = @('updateCategories', 'preChecks', 'options') +foreach ($setting in $requiredSettings) { + if (-not $settings.$setting) { + Write-Error "Required setting '$setting' is missing or empty in $settingsFile" + exit 1 + } +} + +# Extract settings +$UpdateCategories = $settings.updateCategories +$Exclusions = $settings.exclusions +$PreChecks = $settings.preChecks +$Options = $settings.options +$Reporting = $settings.reporting + +# Get DryRun from settings +$DryRun = $Options.dryRun + +# Schedule Configuration +$Config = @{ + RunMonth = $settings.schedule.runMonth + RunWeekday = $settings.schedule.runWeekday + RunTime = $settings.schedule.runTime + MinIntervalMinutes = $settings.schedule.minIntervalMinutes +} + +# End Settings ============================================================= + +# Global variables +$script:UpdateStats = @{ + StartTime = Get-Date + EndTime = $null + Success = $false + Installed = 0 + Failed = 0 + Skipped = 0 + RebootRequired = $false + ErrorMessage = $null +} + +# Helper Functions ========================================================= + +function Test-PSWindowsUpdate { + param([switch]$Automated) + + Write-Log "Checking PSWindowsUpdate module..." -Level Info -Automated:$Automated + + if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) { + Write-Log "PSWindowsUpdate module not found. Installing..." -Level Warning -Automated:$Automated + + try { + # Try to install from PSGallery + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue | Out-Null + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue + Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -ErrorAction Stop + Write-Log "PSWindowsUpdate module installed successfully" -Level Success -Automated:$Automated + } + catch { + Write-Log "Failed to install PSWindowsUpdate module: $_" -Level Error -Automated:$Automated + Write-Log "Please install manually: Install-Module PSWindowsUpdate -Force" -Level Info -Automated:$Automated + return $false + } + } + + try { + Import-Module PSWindowsUpdate -ErrorAction Stop + Write-Log "PSWindowsUpdate module loaded" -Level Success -Automated:$Automated + return $true + } + catch { + Write-Log "Failed to import PSWindowsUpdate module: $_" -Level Error -Automated:$Automated + return $false + } +} + +function Test-PreUpdateChecks { + param([switch]$Automated) + + Write-Log "Running pre-update checks..." -Level Info -Automated:$Automated + + # Check disk space + $systemDrive = $env:SystemDrive + $drive = Get-PSDrive -Name $systemDrive.TrimEnd(':') + $freeSpaceGB = [math]::Round($drive.Free / 1GB, 2) + $minSpaceGB = $PreChecks.minDiskSpaceGB + + Write-Log "Free space on $systemDrive : $freeSpaceGB GB" -Level Info -Automated:$Automated + + if ($freeSpaceGB -lt $minSpaceGB) { + Write-Log "Insufficient disk space. Required: $minSpaceGB GB, Available: $freeSpaceGB GB" -Level Error -Automated:$Automated + return $false + } + + # Check for pending reboot + if ($PreChecks.checkPendingReboot) { + $rebootRequired = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" + if ($rebootRequired) { + Write-Log "System has pending reboot from previous updates" -Level Warning -Automated:$Automated + if ($Options.rebootBehavior -eq 'never') { + Write-Log "Reboot required but rebootBehavior is 'never'" -Level Error -Automated:$Automated + return $false + } + } + } + + # Check Windows Update service + $wuService = Get-Service -Name wuauserv + if ($wuService.Status -ne 'Running') { + Write-Log "Starting Windows Update service..." -Level Info -Automated:$Automated + try { + Start-Service -Name wuauserv -ErrorAction Stop + Write-Log "Windows Update service started" -Level Success -Automated:$Automated + } + catch { + Write-Log "Failed to start Windows Update service: $_" -Level Error -Automated:$Automated + return $false + } + } + + Write-Log "Pre-update checks passed" -Level Success -Automated:$Automated + return $true +} + +function Get-AvailableUpdates { + param([switch]$Automated) + + Write-Log "Scanning for available updates..." -Level Info -Automated:$Automated + + try { + # Get updates + $updates = Get-WindowsUpdate -MicrosoftUpdate -Verbose:$false | Where-Object { + $update = $_ + $included = $false + + # Check categories + foreach ($cat in $UpdateCategories) { + if ($update.Categories -match $cat) { + $included = $true + break + } + } + + # Apply KB exclusions + if ($included -and $Exclusions.kbNumbers.Count -gt 0) { + foreach ($kb in $Exclusions.kbNumbers) { + if ($update.KBArticleIDs -contains $kb) { + Write-Log "Excluded by KB: $($update.Title) [$kb]" -Level Info -Automated:$Automated + $included = $false + break + } + } + } + + # Apply title exclusions + if ($included -and $Exclusions.titlePatterns.Count -gt 0) { + foreach ($pattern in $Exclusions.titlePatterns) { + if ($update.Title -like $pattern) { + Write-Log "Excluded by pattern: $($update.Title) [$pattern]" -Level Info -Automated:$Automated + $included = $false + break + } + } + } + + return $included + } + + return $updates + } + catch { + Write-Log "Failed to scan for updates: $_" -Level Error -Automated:$Automated + return @() + } +} + +function Install-AvailableUpdates { + param( + $Updates, + [switch]$Automated + ) + + if ($Updates.Count -eq 0) { + Write-Log "No updates to install" -Level Info -Automated:$Automated + return + } + + Write-Log "Found $($Updates.Count) update(s) to install" -Level Info -Automated:$Automated + Write-Log "========================================" -Level Info -Automated:$Automated + + foreach ($update in $Updates) { + $sizeKB = [math]::Round($update.Size / 1KB, 2) + Write-Log " [$($update.KBArticleIDs -join ',')] $($update.Title) ($sizeKB KB)" -Level Info -Automated:$Automated + } + + Write-Log "========================================" -Level Info -Automated:$Automated + + if ($DryRun) { + Write-Log "DRY RUN MODE - No updates will be installed" -Level Warning -Automated:$Automated + $script:UpdateStats.Skipped = $Updates.Count + return + } + + # Install updates + Write-Log "Installing updates..." -Level Info -Automated:$Automated + + try { + $installParams = @{ + MicrosoftUpdate = $true + AcceptAll = $true + IgnoreReboot = ($Options.rebootBehavior -ne 'auto') + Verbose = $false + } + + # Use KBArticleID filter if available + $kbList = $Updates | ForEach-Object { $_.KBArticleIDs } | Where-Object { $_ } + if ($kbList.Count -gt 0) { + $installParams['KBArticleID'] = $kbList + } + + $result = Install-WindowsUpdate @installParams + + # Process results + foreach ($item in $result) { + if ($item.Result -eq "Installed" -or $item.Result -eq "Downloaded") { + $script:UpdateStats.Installed++ + Write-Log "Installed: $($item.Title)" -Level Success -Automated:$Automated + } + elseif ($item.Result -eq "Failed") { + $script:UpdateStats.Failed++ + Write-Log "Failed: $($item.Title)" -Level Error -Automated:$Automated + } + else { + $script:UpdateStats.Skipped++ + Write-Log "Skipped: $($item.Title) [Result: $($item.Result)]" -Level Warning -Automated:$Automated + } + + if ($item.RebootRequired) { + $script:UpdateStats.RebootRequired = $true + } + } + } + catch { + Write-Log "Update installation failed: $_" -Level Error -Automated:$Automated + $script:UpdateStats.Failed = $Updates.Count + $script:UpdateStats.ErrorMessage = "Installation failed: $_" + } +} + +function Invoke-PostUpdateActions { + param([switch]$Automated) + + Write-Log "Running post-update actions..." -Level Info -Automated:$Automated + + # Check reboot requirement + if ($script:UpdateStats.RebootRequired) { + Write-Log "System reboot required" -Level Warning -Automated:$Automated + + if ($Options.rebootBehavior -eq 'auto') { + $delayMinutes = $Options.rebootDelayMinutes + Write-Log "System will reboot in $delayMinutes minutes..." -Level Warning -Automated:$Automated + + if ($delayMinutes -gt 0) { + Start-Sleep -Seconds ($delayMinutes * 60) + } + + # Remove lock file before reboot to prevent future runs from being blocked + $lockFile = [IO.Path]::ChangeExtension($PSCommandPath, ".lock") + if (Test-Path $lockFile) { + Remove-Item $lockFile -Force + Write-Log "Lock file removed before reboot" -Level Info -Automated:$Automated + } + + Write-Log "Initiating system reboot..." -Level Warning -Automated:$Automated + Restart-Computer -Force + } + else { + Write-Log "Manual reboot required" -Level Warning -Automated:$Automated + } + } + + # Generate update report + if ($Reporting.generateReport) { + $reportPath = Join-Path $PSScriptRoot "update-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + + $reportContent = @" +Windows Update Report +===================== +Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + +Statistics: +----------- +Updates Installed: $($script:UpdateStats.Installed) +Updates Failed: $($script:UpdateStats.Failed) +Updates Skipped: $($script:UpdateStats.Skipped) +Reboot Required: $($script:UpdateStats.RebootRequired) + +Recent Update History: +---------------------- +"@ + + try { + $history = Get-WindowsUpdate -Last 10 -Verbose:$false + foreach ($item in $history) { + $reportContent += "`n[$($item.Date)] $($item.Title) - $($item.Result)" + } + } + catch { + $reportContent += "`nFailed to retrieve update history" + } + + $reportContent | Out-File -FilePath $reportPath -Encoding UTF8 + Write-Log "Report saved: $reportPath" -Level Success -Automated:$Automated + + # Send email notification if enabled + if ($Reporting.emailNotification -and $Reporting.emailSettings) { + $hostname = $env:COMPUTERNAME + $status = if ($script:UpdateStats.Failed -eq 0) { "SUCCESS" } else { "COMPLETED WITH ERRORS" } + $subject = "[$hostname] Windows Update Report - $status" + + Send-EmailNotification -EmailSettings $Reporting.emailSettings -Subject $subject -Body $reportContent -Automated:$Automated + } + } +} + +function Write-UpdateSummary { + param([switch]$Automated) + + $script:UpdateStats.EndTime = Get-Date + $duration = $script:UpdateStats.EndTime - $script:UpdateStats.StartTime + + Write-Log "" -Level Info -Automated:$Automated + Write-Log "========================================" -Level Info -Automated:$Automated + Write-Log "UPDATE SUMMARY" -Level Info -Automated:$Automated + Write-Log "========================================" -Level Info -Automated:$Automated + Write-Log "Start Time : $($script:UpdateStats.StartTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated + Write-Log "End Time : $($script:UpdateStats.EndTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated + Write-Log "Duration : $($duration.Hours)h $($duration.Minutes)m $($duration.Seconds)s" -Level Info -Automated:$Automated + Write-Log "Status : $(if ($script:UpdateStats.Failed -eq 0) { 'SUCCESS' } else { 'COMPLETED WITH ERRORS' })" -Level $(if ($script:UpdateStats.Failed -eq 0) { 'Success' } else { 'Warning' }) -Automated:$Automated + Write-Log "" -Level Info -Automated:$Automated + Write-Log "Installed : $($script:UpdateStats.Installed)" -Level Info -Automated:$Automated + Write-Log "Failed : $($script:UpdateStats.Failed)" -Level $(if ($script:UpdateStats.Failed -eq 0) { 'Info' } else { 'Error' }) -Automated:$Automated + Write-Log "Skipped : $($script:UpdateStats.Skipped)" -Level Info -Automated:$Automated + Write-Log "Reboot Needed : $($script:UpdateStats.RebootRequired)" -Level Info -Automated:$Automated + + if ($script:UpdateStats.ErrorMessage) { + Write-Log "" -Level Info -Automated:$Automated + Write-Log "Error: $($script:UpdateStats.ErrorMessage)" -Level Error -Automated:$Automated + } + + Write-Log "========================================" -Level Info -Automated:$Automated + + $script:UpdateStats.Success = ($script:UpdateStats.Failed -eq 0) +} + +# Main Business Logic ====================================================== + +function Start-BusinessLogic { + param([switch]$Automated) + + Write-Log "========================================" -Level Info -Automated:$Automated + Write-Log "Windows Update Process Started" -Level Info -Automated:$Automated + Write-Log "Script Version: $ScriptVersion ($ScriptDate)" -Level Info -Automated:$Automated + if ($DryRun) { + Write-Log "DRY RUN MODE - No changes will be made" -Level Warning -Automated:$Automated + } + Write-Log "========================================" -Level Info -Automated:$Automated + + # Check PSWindowsUpdate module + if (-not (Test-PSWindowsUpdate -Automated:$Automated)) { + Write-Log "PSWindowsUpdate module check failed. Aborting." -Level Error -Automated:$Automated + exit 1 + } + + # Run pre-update checks + if (-not (Test-PreUpdateChecks -Automated:$Automated)) { + Write-Log "Pre-update checks failed. Aborting." -Level Error -Automated:$Automated + exit 1 + } + + # Scan for updates + $updates = Get-AvailableUpdates -Automated:$Automated + + if ($updates.Count -eq 0) { + Write-Log "System is up to date. No updates available." -Level Success -Automated:$Automated + } + else { + # Install updates + Install-AvailableUpdates -Updates $updates -Automated:$Automated + + # Post-update actions + Invoke-PostUpdateActions -Automated:$Automated + } + + # Print summary + Write-UpdateSummary -Automated:$Automated + + # Exit with appropriate code + if ($script:UpdateStats.Failed -gt 0) { + exit 1 + } +} + +# Entry Point ============================================================== + +if ($Automated) { + if (Get-Command Invoke-ScheduledExecution -ErrorAction SilentlyContinue) { + $params = @{ + Config = $Config + Automated = $Automated + CurrentDateTimeUtc = $CurrentDateTimeUtc + ScriptBlock = { Start-BusinessLogic -Automated:$Automated } + } + Invoke-ScheduledExecution @params + } + else { + Write-Log "Invoke-ScheduledExecution not available. Execution aborted." -Level Error -Automated:$Automated + exit 1 + } +} +else { + Write-Log "Manual execution started" -Level Info -Automated:$Automated + Start-BusinessLogic -Automated:$Automated +} From 81da9ad2997b6114b9d5419058be7d3c36983d60 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sun, 8 Feb 2026 12:06:13 +0100 Subject: [PATCH 3/3] (feature): release 1.0.1. Improved release with bundled scripts. UI to register service, change scheduling and log viewer --- CHANGELOG.md | 70 +- CONTRIBUTING.md | 236 +++++ LICENSE.md | 2 +- README.md | 258 ++++- badges/coverage-branches.svg | 21 + badges/coverage-lines.svg | 21 + badges/coverage-methods.svg | 21 + .../App.xaml | 8 + .../App.xaml.cs | 7 + .../MainWindow.xaml | 445 ++++++++ .../MainWindow.xaml.cs | 20 + .../MaksIT.UScheduler.ScheduleManager.csproj | 27 + .../Models/ScriptSchedule.cs | 59 ++ .../Models/ScriptStatus.cs | 49 + .../Services/AppSettingsService.cs | 206 ++++ .../Services/LogViewerService.cs | 259 +++++ .../Services/ScriptSettingsService.cs | 139 +++ .../Services/ScriptStatusService.cs | 94 ++ .../Services/UISettingsService.cs | 110 ++ .../Services/WindowsServiceManager.cs | 239 +++++ .../ViewModels/MainViewModel.cs | 953 ++++++++++++++++++ .../app.manifest | 19 + .../appsettings.json | 5 + src/MaksIT.UScheduler.Shared/Configuration.cs | 78 ++ .../Extensions/LoggerFactoryExtensions.cs | 20 + .../Helpers/PathHelper.cs | 37 + .../MaksIT.UScheduler.Shared.csproj | 15 + .../PSScriptBackgroundServiceTests.cs | 211 ++++ .../ProcessBackgroundServiceTests.cs | 180 ++++ .../ConfigurationTests.cs | 155 +++ .../MaksIT.UScheduler.Tests.csproj | 34 + src/MaksIT.UScheduler.sln | 21 +- .../ConfigurationReloadBackgroundService.cs | 32 + .../PSScriptBackgroundService.cs | 62 +- .../ProcessBackgroundService.cs | 62 +- src/MaksIT.UScheduler/Configuration.cs | 20 - src/MaksIT.UScheduler/Install.cmd | 3 - src/MaksIT.UScheduler/LICENSE.md | 21 - .../MaksIT.UScheduler.csproj | 30 +- src/MaksIT.UScheduler/MaksIT.UScheduler.sln | 24 - src/MaksIT.UScheduler/Program.cs | 217 +++- .../Services/IPSScriptService.cs | 32 + .../Services/IProcessService.cs | 36 + .../Services/PSScriptService.cs | 162 ++- .../Services/ProcessService.cs | 94 +- src/MaksIT.UScheduler/Uninstall.cmd | 2 - src/MaksIT.UScheduler/appsettings.json | 25 + src/Release-ToGitHub.ps1 | 173 ---- {examples => src/Scripts}/File-Sync/README.md | 0 .../Scripts}/File-Sync/file-sync.bat | 0 .../Scripts}/File-Sync/file-sync.ps1 | 0 .../Scripts}/File-Sync/scriptsettings.json | 10 +- .../Scripts}/File-Sync/sync.ffs_batch | 0 .../Scripts}/HyperV-Backup/README.md | 0 .../Scripts}/HyperV-Backup/hyper-v-backup.bat | 0 .../Scripts}/HyperV-Backup/hyper-v-backup.ps1 | 0 .../HyperV-Backup/scriptsettings.json | 0 .../Scripts}/Native-Sync/README.md | 0 .../Scripts}/Native-Sync/native-sync.bat | 0 .../Scripts}/Native-Sync/native-sync.ps1 | 0 .../Scripts}/Native-Sync/scriptsettings.json | 0 .../Scripts}/SchedulerTemplate.psd1 | 0 .../Scripts}/SchedulerTemplate.psm1 | 0 .../Scripts}/Windows-Update/README.md | 66 +- .../Utilities/windows-update-policy.ps1 | 0 .../Windows-Update/scriptsettings.json | 11 +- .../Windows-Update/windows-update.bat | 0 .../Windows-Update/windows-update.ps1 | 213 +++- .../Force-AmendTaggedCommit.bat | 9 + .../Force-AmendTaggedCommit.ps1 | 242 +++++ .../scriptsettings.json | 10 + .../Generate-CoverageBadges.bat | 9 + .../Generate-CoverageBadges.ps1 | 198 ++++ .../scriptsettings.json | 34 + .../Release-ToGitHub}/Release-ToGitHub.bat | 4 +- utils/Release-ToGitHub/Release-ToGitHub.ps1 | 695 +++++++++++++ utils/Release-ToGitHub/scriptsettings.json | 57 ++ utils/TestRunner.psm1 | 158 +++ 78 files changed, 6258 insertions(+), 472 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 badges/coverage-branches.svg create mode 100644 badges/coverage-lines.svg create mode 100644 badges/coverage-methods.svg create mode 100644 src/MaksIT.UScheduler.ScheduleManager/App.xaml create mode 100644 src/MaksIT.UScheduler.ScheduleManager/App.xaml.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/MainWindow.xaml create mode 100644 src/MaksIT.UScheduler.ScheduleManager/MainWindow.xaml.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/MaksIT.UScheduler.ScheduleManager.csproj create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Models/ScriptSchedule.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Models/ScriptStatus.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/AppSettingsService.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/LogViewerService.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/ScriptSettingsService.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/ScriptStatusService.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/UISettingsService.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/Services/WindowsServiceManager.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/ViewModels/MainViewModel.cs create mode 100644 src/MaksIT.UScheduler.ScheduleManager/app.manifest create mode 100644 src/MaksIT.UScheduler.ScheduleManager/appsettings.json create mode 100644 src/MaksIT.UScheduler.Shared/Configuration.cs create mode 100644 src/MaksIT.UScheduler.Shared/Extensions/LoggerFactoryExtensions.cs create mode 100644 src/MaksIT.UScheduler.Shared/Helpers/PathHelper.cs create mode 100644 src/MaksIT.UScheduler.Shared/MaksIT.UScheduler.Shared.csproj create mode 100644 src/MaksIT.UScheduler.Tests/BackgroundServices/PSScriptBackgroundServiceTests.cs create mode 100644 src/MaksIT.UScheduler.Tests/BackgroundServices/ProcessBackgroundServiceTests.cs create mode 100644 src/MaksIT.UScheduler.Tests/ConfigurationTests.cs create mode 100644 src/MaksIT.UScheduler.Tests/MaksIT.UScheduler.Tests.csproj create mode 100644 src/MaksIT.UScheduler/BackgroundServices/ConfigurationReloadBackgroundService.cs delete mode 100644 src/MaksIT.UScheduler/Configuration.cs delete mode 100644 src/MaksIT.UScheduler/Install.cmd delete mode 100644 src/MaksIT.UScheduler/LICENSE.md delete mode 100644 src/MaksIT.UScheduler/MaksIT.UScheduler.sln create mode 100644 src/MaksIT.UScheduler/Services/IPSScriptService.cs create mode 100644 src/MaksIT.UScheduler/Services/IProcessService.cs delete mode 100644 src/MaksIT.UScheduler/Uninstall.cmd delete mode 100644 src/Release-ToGitHub.ps1 rename {examples => src/Scripts}/File-Sync/README.md (100%) rename {examples => src/Scripts}/File-Sync/file-sync.bat (100%) rename {examples => src/Scripts}/File-Sync/file-sync.ps1 (100%) rename {examples => src/Scripts}/File-Sync/scriptsettings.json (95%) rename {examples => src/Scripts}/File-Sync/sync.ffs_batch (100%) rename {examples => src/Scripts}/HyperV-Backup/README.md (100%) rename {examples => src/Scripts}/HyperV-Backup/hyper-v-backup.bat (100%) rename {examples => src/Scripts}/HyperV-Backup/hyper-v-backup.ps1 (100%) rename {examples => src/Scripts}/HyperV-Backup/scriptsettings.json (100%) rename {examples => src/Scripts}/Native-Sync/README.md (100%) rename {examples => src/Scripts}/Native-Sync/native-sync.bat (100%) rename {examples => src/Scripts}/Native-Sync/native-sync.ps1 (100%) rename {examples => src/Scripts}/Native-Sync/scriptsettings.json (100%) rename {examples => src/Scripts}/SchedulerTemplate.psd1 (100%) rename {examples => src/Scripts}/SchedulerTemplate.psm1 (100%) rename {examples => src/Scripts}/Windows-Update/README.md (89%) rename {examples => src/Scripts}/Windows-Update/Utilities/windows-update-policy.ps1 (100%) rename {examples => src/Scripts}/Windows-Update/scriptsettings.json (88%) rename {examples => src/Scripts}/Windows-Update/windows-update.bat (100%) rename {examples => src/Scripts}/Windows-Update/windows-update.ps1 (67%) create mode 100644 utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.bat create mode 100644 utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 create mode 100644 utils/Force-AmendTaggedCommit/scriptsettings.json create mode 100644 utils/Generate-CoverageBadges/Generate-CoverageBadges.bat create mode 100644 utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1 create mode 100644 utils/Generate-CoverageBadges/scriptsettings.json rename {src => utils/Release-ToGitHub}/Release-ToGitHub.bat (63%) create mode 100644 utils/Release-ToGitHub/Release-ToGitHub.ps1 create mode 100644 utils/Release-ToGitHub/scriptsettings.json create mode 100644 utils/TestRunner.psm1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 93213d7..a7a4b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # MaksIT.UScheduler Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## v1.0.1 - 2026-02-15 + +### Added +- **CLI service management**: Added command-line arguments for service installation and management + - `--install`, `-i`: Install the Windows service + - `--uninstall`, `-u`: Uninstall the Windows service + - `--start`: Start the service + - `--stop`: Stop the service + - `--status`: Query service status + - `--help`, `-h`: Show help message +- **Relative path support**: Script and process paths can now be relative to the application directory +- **OS guard**: Application now checks for Windows at startup and exits with error on unsupported platforms +- **Release script enhancement**: Example scripts are now automatically added to `appsettings.json` in disabled state during release build +- **Unit tests**: Added comprehensive test project `MaksIT.UScheduler.Tests` with tests for background services and configuration +- **Uscheduler Manager UI**: Introduced a graphical user interface (WPF) for managing script schedules, viewing script status, and editing configuration in real time +- **Live configuration reload**: Added `ConfigurationReloadBackgroundService` to log configuration changes at runtime +- **Hot-reload for scripts/processes**: Background services now use `IOptionsMonitor` to pick up changes in `appsettings.json` without restart + +### Changed +- Default value for `IsSigned` in PowerShell script configuration changed from `false` to `true` for improved security +- PSScriptService now implements `IDisposable` for proper RunspacePool cleanup +- Method signatures updated: `RunScript` → `RunScriptAsync`, `RunProcess` → `RunProcessAsync` +- Service is now installed with `start=auto` for automatic startup on boot +- Updated package dependencies: + - MaksIT.Core: 1.6.0 → 1.6.3 + - Microsoft.Extensions.Hosting: 10.0.0 → 10.0.3 + - Microsoft.Extensions.Hosting.WindowsServices: 10.0.0 → 10.0.3 + - System.Diagnostics.PerformanceCounter: 10.0.0 → 10.0.3 + - Microsoft.Extensions.Logging.Abstractions: 10.0.2 → 10.0.3 + +### Removed +- `Install.cmd` and `Uninstall.cmd` files (replaced by CLI arguments) + +### Fixed +- **Parallel execution**: Restored parallel execution of PowerShell scripts and processes (broken during .NET Framework to .NET migration) + - PSScriptService now uses RunspacePool (up to CPU core count) for concurrent script execution + - Background services use `Task.WhenAll` to launch all tasks simultaneously +- **Unit test improvements**: Refactored tests to use `IOptionsMonitor` for better coverage and reliability + ## v1.0.0 - 2025-12-06 ### Major Changes @@ -13,7 +57,7 @@ - `ProcessBackgroundService` for process management. - Enhanced PowerShell script execution with signature validation and script unblocking. - Improved process management with restart-on-failure logic. -- Updated install/uninstall scripts (`Install.cmd`, `Uninstall.cmd`) for service management. +- Added install/uninstall scripts (`Install.cmd`, `Uninstall.cmd`) for service management (removed in v1.0.1). - Added comprehensive README with usage, configuration, and scheduling examples. - MIT License included. @@ -21,3 +65,27 @@ - Old solution, project, and service files removed. - Configuration format and service naming conventions updated. - Scheduling logic for console applications is not yet implemented (runs every 10 seconds). + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..19fac68 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,236 @@ +# Contributing to MaksIT.UScheduler + +Thank you for your interest in contributing to MaksIT.UScheduler! + +## Table of Contents + +- [Contributing to MaksIT.UScheduler](#contributing-to-maksituscheduler) + - [Table of Contents](#table-of-contents) + - [Development Setup](#development-setup) + - [Branch Strategy](#branch-strategy) + - [Making Changes](#making-changes) + - [Versioning](#versioning) + - [Release Process](#release-process) + - [Prerequisites](#prerequisites) + - [Version Files](#version-files) + - [Branch-Based Release Behavior](#branch-based-release-behavior) + - [Development Build Workflow](#development-build-workflow) + - [Production Release Workflow](#production-release-workflow) + - [Release Script Details](#release-script-details) + - [Changelog Guidelines](#changelog-guidelines) + - [AI-Powered Changelog Generation (Optional)](#ai-powered-changelog-generation-optional) + +--- + +## Development Setup + +1. Clone the repository: + ```bash + git clone https://github.com/MaksIT/uscheduler.git + cd uscheduler + ``` + +2. Open the solution in Visual Studio or your preferred IDE: + ``` + src/MaksIT.UScheduler/MaksIT.UScheduler.sln + ``` + +3. Build the project: + ```bash + dotnet build src/MaksIT.UScheduler/MaksIT.UScheduler.csproj + ``` + +--- + +## Branch Strategy + +- `main` - Production-ready code +- `dev` - Active development branch +- Feature branches - Created from `dev` for specific features + +--- + +## Making Changes + +1. Create a feature branch from `dev` +2. Make your changes +3. Update `CHANGELOG.md` with your changes +4. Update version in `.csproj` if needed +5. Test your changes locally using dev tags +6. Submit a pull request to `dev` + +--- + +## Versioning + +This project follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** - Incompatible API changes +- **MINOR** - New functionality (backwards compatible) +- **PATCH** - Bug fixes (backwards compatible) + +Version format: `X.Y.Z` (e.g., `1.0.1`) + +--- + +## Release Process + +### Prerequisites + +- .NET SDK installed +- Git CLI +- GitHub CLI (`gh`) - required only for production releases +- GitHub token set in environment variable (configured in `scriptsettings.json`) + +### Version Files + +Before creating a release, ensure version consistency across: + +1. **`.csproj`** - Update `` element: + ```xml + 1.0.1 + ``` + +2. **`CHANGELOG.md`** - Add version entry at the top: + ```markdown + ## v1.0.1 + + ### Added + - New feature description + + ### Fixed + - Bug fix description + ``` + +### Branch-Based Release Behavior + +The release script behavior is controlled by the current branch (configurable in `scriptsettings.json`): + +| Branch | Tag Required | Uncommitted Changes | Behavior | +|--------|--------------|---------------------|----------| +| Dev (`dev`) | No | Allowed | Local build only (version from .csproj) | +| Release (`main`) | Yes | Not allowed | Full release to GitHub | +| Other | - | - | Blocked | + +Branch names can be customized in `scriptsettings.json`: +```json +"branches": { + "release": "main", + "dev": "dev" +} +``` + +### Development Build Workflow + +Test builds on the `dev` branch - no tag needed: + +```bash +# 1. On dev branch: Update version in .csproj and CHANGELOG.md +git checkout dev + +# 2. Commit your changes +git add . +git commit -m "Prepare v1.0.1 release" + +# 3. Run the release script (no tag needed!) +cd src/scripts/Release-ToGitHub +.\Release-ToGitHub.ps1 + +# Output: DEV BUILD COMPLETE +# Creates: release/maksit.uscheduler-1.0.1.zip (local only) +``` + +### Production Release Workflow + +When ready to publish, merge to `main`, create tag, and run: + +```bash +# 1. Merge to main +git checkout main +git merge dev + +# 2. Create tag (required on main) +git tag v1.0.1 + +# 3. Run the release script +cd src/scripts/Release-ToGitHub +.\Release-ToGitHub.ps1 + +# Output: RELEASE COMPLETE +# Creates: release/maksit.uscheduler-1.0.1.zip +# Pushes tag to GitHub +# Creates GitHub release with assets +``` + +### Release Script Details + +The `Release-ToGitHub.ps1` script performs these steps: + +**Pre-flight checks:** +- Detects current branch (`main` or `dev`) +- On `main`: requires clean working directory; on `dev`: uncommitted changes allowed +- Reads version from `.csproj` (source of truth) +- On `main`: requires tag matching the version +- Ensures `CHANGELOG.md` has matching version entry +- Checks GitHub CLI authentication (main branch only) + +**Build process:** +- Publishes .NET project in Release configuration +- Copies `Scripts` folder into the release +- Creates versioned ZIP archive +- Extracts release notes from `CHANGELOG.md` + +**GitHub release (main branch only):** +- Pushes tag to remote if not present +- Creates (or recreates) GitHub release with assets + +**Configuration:** + +The script reads settings from `scriptsettings.json`: + +```json +{ + "github": { + "tokenEnvVar": "GITHUB_MAKS_IT_COM" + }, + "paths": { + "csprojPath": "..\\..\\MaksIT.UScheduler\\MaksIT.UScheduler.csproj", + "changelogPath": "..\\..\\..\\CHANGELOG.md", + "releaseDir": "..\\..\\release" + }, + "release": { + "zipNamePattern": "maksit.uscheduler-{version}.zip", + "releaseTitlePattern": "Release {version}" + } +} +``` + +--- + +## Changelog Guidelines + +Follow [Keep a Changelog](https://keepachangelog.com/) format: + +```markdown +## v1.0.1 + +### Added +- New features + +### Changed +- Changes to existing functionality + +### Deprecated +- Features to be removed in future + +### Removed +- Removed features + +### Fixed +- Bug fixes + +### Security +- Security-related changes +``` + +--- diff --git a/LICENSE.md b/LICENSE.md index c43e08a..f5aa10f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Maksym Sadovnychyy (Maks-IT) +Copyright (c) 2017 - 2026 Maksym Sadovnychyy (Maks-IT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 35a56f4..9813a7f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ - - # MaksIT Unified Scheduler Service +![Line Coverage](badges/coverage-lines.svg) ![Branch Coverage](badges/coverage-branches.svg) ![Method Coverage](badges/coverage-methods.svg) + A modern, fully rewritten Windows service built on **.NET 10** for scheduling and running PowerShell scripts and console applications. Designed for system administrators — and also for those who *feel like* system administrators — who need a predictable, resilient, and secure background execution environment. @@ -14,38 +14,52 @@ Designed for system administrators — and also for those who *feel like* system - [Scripts Examples](#scripts-examples) - [Features at a Glance](#features-at-a-glance) - [Installation](#installation) - - [Recommended (using bundled scripts)](#recommended-using-bundled-scripts) - - [Manual Installation](#manual-installation) + - [Using CLI Commands](#using-cli-commands) + - [Using sc.exe](#using-scexe) - [Configuration (`appsettings.json`)](#configuration-appsettingsjson) + - [Path Resolution](#path-resolution) + - [Log Levels](#log-levels) - [PowerShell Scripts](#powershell-scripts) - [Processes](#processes) - [How It Works](#how-it-works) - [PowerShell Execution Parameters](#powershell-execution-parameters) - - [Thread Layout](#thread-layout) + - [Execution Model](#execution-model) - [Reusable Scheduler Module (`SchedulerTemplate.psm1`)](#reusable-scheduler-module-schedulertemplatepsm1) + - [Exported Functions](#exported-functions) + - [Module Version](#module-version) - [Example usage](#example-usage) - [Security](#security) - [Logging](#logging) + - [Testing](#testing) + - [Running Tests](#running-tests) + - [Code Coverage](#code-coverage) + - [Test Structure](#test-structure) - [Contact](#contact) - [License](#license) ## Scripts Examples -- [Hyper-V Backup](./examples/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management -- [Native-Sync](./examples/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies -- [File-Sync](./examples/File-Sync/README.md) - [FreeFileSync](https://freefilesync.org/) batch job execution -- [Scheduler Template Module](./examples/SchedulerTemplate.psm1) + +> **Note:** These examples are **bundled with the release** and included in the default `appsettings.json`, but are **disabled by default**. To enable an example, set `"Disabled": false` in the configuration. + +- [Hyper-V Backup](./src/Scripts/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management +- [Native-Sync](./src/Scripts/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies +- [File-Sync](./src/Scripts/File-Sync/README.md) - [FreeFileSync](https://freefilesync.org/) batch job execution +- [Windows-Update](./src/Scripts/Windows-Update/README.md) - Production-ready Windows Update automation solution using pure PowerShell +- [Scheduler Template Module](./src/Scripts/SchedulerTemplate.psm1) --- ## Features at a Glance * **.NET 10 Worker Service** – clean, robust, stable. +* **Windows only** – designed specifically for Windows services. * **Strongly typed configuration** via `appsettings.json`. -* **Run PowerShell scripts & executables concurrently** (each in its own thread). +* **Parallel execution** – PowerShell scripts & executables run concurrently using RunspacePool and Task.WhenAll. +* **Relative path support** – script and process paths can be relative to the application directory. * **Signature enforcement** (AllSigned by default). * **Automatic restart-on-failure** for supervised processes. -* **Extensible logging** (file + console). -* **Simple Install.cmd / Uninstall.cmd**. +* **Extensible logging** (file + console + Windows EventLog). +* **Built-in CLI** for service management (`--install`, `--uninstall`, `--start`, `--stop`, `--status`). * **Reusable scheduling module**: `SchedulerTemplate.psm1`. * **Thread-isolated architecture** — individual failures do not affect others. @@ -53,30 +67,55 @@ Designed for system administrators — and also for those who *feel like* system ## Installation -### Recommended (using bundled scripts) +### Using CLI Commands -```bat -cd /d path\to\src\MaksIT.UScheduler -Install.cmd +The executable includes built-in service management commands. Run as Administrator: + +```powershell +# Install the service (auto-start enabled) +MaksIT.UScheduler.exe --install + +# Start the service +MaksIT.UScheduler.exe --start + +# Check service status +MaksIT.UScheduler.exe --status + +# Stop the service +MaksIT.UScheduler.exe --stop + +# Uninstall the service +MaksIT.UScheduler.exe --uninstall + +# Show help +MaksIT.UScheduler.exe --help +``` + +| Command | Short | Description | +|---------|-------|-------------| +| `--install` | `-i` | Install the Windows service (auto-start) | +| `--uninstall` | `-u` | Stop and remove the Windows service | +| `--start` | | Start the service | +| `--stop` | | Stop the service | +| `--status` | | Query service status | +| `--help` | `-h` | Show help message | + +> **Note:** Service management commands require administrator privileges. + +### Using sc.exe + +Alternatively, use Windows Service Control Manager directly: + +```powershell +sc.exe create "MaksIT.UScheduler" binpath="C:\Path\To\MaksIT.UScheduler.exe" start=auto +sc.exe start "MaksIT.UScheduler" ``` To uninstall: -```bat -Uninstall.cmd -``` - -### Manual Installation - ```powershell -sc.exe create "MaksIT.UScheduler Service" binpath="C:\Path\To\MaksIT.UScheduler.exe" -sc.exe start "MaksIT.UScheduler Service" -``` - -Manual uninstall: - -```powershell -sc.exe delete "MaksIT.UScheduler Service" +sc.exe stop "MaksIT.UScheduler" +sc.exe delete "MaksIT.UScheduler" ``` --- @@ -85,31 +124,79 @@ sc.exe delete "MaksIT.UScheduler Service" ```json { + "Logging": { + "LogLevel": { + "Default": "Information" + }, + "EventLog": { + "SourceName": "MaksIT.UScheduler", + "LogName": "Application", + "LogLevel": { + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, "Configuration": { "ServiceName": "MaksIT.UScheduler", "LogDir": "C:\\Logs", "Powershell": [ - { "Path": "C:\\Scripts\\MyScript.ps1", "IsSigned": true } + { "Path": "../Scripts/MyScript.ps1", "IsSigned": true, "Disabled": false }, + { "Path": "C:\\Scripts\\AnotherScript.ps1", "IsSigned": false, "Disabled": true } ], "Processes": [ - { "Path": "C:\\Programs\\MyApp.exe", "Args": ["--option"], "RestartOnFailure": true } + { "Path": "../Tools/MyApp.exe", "Args": ["--option"], "RestartOnFailure": true, "Disabled": false } ] } } ``` +> **Note:** `ServiceName` and `LogDir` are optional. Defaults: `"MaksIT.UScheduler"` and `Logs` folder in app directory. + +### Path Resolution + +Paths can be either absolute or relative: + +| Path Type | Example | Resolved To | +|-----------|---------|-------------| +| Absolute | `C:\Scripts\backup.ps1` | `C:\Scripts\backup.ps1` | +| Relative | `../Scripts/backup.ps1` | `{AppDirectory}\..\Scripts\backup.ps1` | +| Relative | `scripts/backup.ps1` | `{AppDirectory}\scripts\backup.ps1` | + +Relative paths are resolved against the application's base directory (where `MaksIT.UScheduler.exe` is located). + +### Log Levels + +The `"Default": "Information"` setting controls the minimum severity of messages that get logged. Available levels (from most to least verbose): + +| Level | Description | +|-------|-------------| +| `Trace` | Most detailed, for debugging internals | +| `Debug` | Debugging information | +| `Information` | General operational events (recommended default) | +| `Warning` | Abnormal or unexpected events | +| `Error` | Errors and exceptions | +| `Critical` | Critical failures requiring immediate attention | +| `None` | Disables logging | + ### PowerShell Scripts -* `Path` — full `.ps1` file path -* `IsSigned` — `true` enforces AllSigned, `false` runs unrestricted +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `Path` | string | required | Path to `.ps1` file (absolute or relative) | +| `IsSigned` | bool | `true` | `true` enforces AllSigned, `false` runs unrestricted | +| `Disabled` | bool | `false` | `true` skips this script during execution | ### Processes -* `Path` — executable -* `Args` — command-line arguments -* `RestartOnFailure` — restart logic handled by service +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `Path` | string | required | Path to executable (absolute or relative) | +| `Args` | string[] | `null` | Command-line arguments | +| `RestartOnFailure` | bool | `false` | Restart process if it exits with non-zero code | +| `Disabled` | bool | `false` | `true` skips this process during execution | --- @@ -133,21 +220,28 @@ param ( ) ``` -### Thread Layout +### Execution Model + +Scripts and processes run **in parallel** using: + +- **PowerShell**: `RunspacePool` (up to CPU core count concurrent runspaces) +- **Processes**: `Task.WhenAll` for concurrent process execution ``` Unified Scheduler Service -├── PowerShell -│ ├── ScriptA.ps1 Thread -│ ├── ScriptB.ps1 Thread -│ └── ... -└── Processes - ├── ProgramA.exe Thread - ├── ProgramB.exe Thread - └── ... +├── PSScriptBackgroundService (RunspacePool) +│ ├── ScriptA.ps1 ─┐ +│ ├── ScriptB.ps1 ─┼─ Parallel execution +│ └── ScriptC.ps1 ─┘ +└── ProcessBackgroundService (Task.WhenAll) + ├── ProgramA.exe ─┐ + ├── ProgramB.exe ─┼─ Parallel execution + └── ProgramC.exe ─┘ ``` -A crash in one thread **never stops the service** or other components. +- A failure in one script/process **never stops the service** or other components. +- The same script/process won't run twice concurrently (protected by "already running" check). +- Execution cycle repeats every 10 seconds. --- @@ -164,7 +258,20 @@ This module provides: * Automatic lock file (no concurrent execution) * Last-run file tracking * Unified callback execution pattern -* Logging helpers (Write-Log) + +### Exported Functions + +| Function | Description | +|----------|-------------| +| `Write-Log` | Logging with timestamp, level (Info/Success/Warning/Error), and color support | +| `Invoke-ScheduledExecution` | Main scheduler — checks schedule, manages locks, runs callback | +| `Get-CredentialFromEnvVar` | Retrieves credentials from Base64-encoded machine environment variables | +| `Test-UNCPath` | Validates whether a path is a UNC path | +| `Send-EmailNotification` | Sends SMTP email with optional SSL and credential support | + +### Module Version + +The module exports `$ModuleVersion` and `$ModuleDate` for version tracking. ### Example usage @@ -205,20 +312,65 @@ That’s it — the full scheduling engine is reused automatically. ## Security -* Signed scripts required by default. -* Scripts are auto-unblocked before execution. -* Unrestricted execution can be enabled if needed (not recommended on production systems). +* Scripts run with **AllSigned** execution policy by default. +* Set `IsSigned: false` to use **Unrestricted** policy (not recommended for production). +* Scripts are auto-unblocked before execution (Zone.Identifier removed). +* Signature validation ensures only trusted scripts execute. --- ## Logging -* Console logging -* File logging under the directory specified by `LogDir` +* **Console logging** — standard output +* **File logging** — written to `LogDir` (default: `Logs` folder in app directory) +* **Windows EventLog** — events logged to Application log under `MaksIT.UScheduler` source * All events (start, stop, crash, restart, error, skip) are logged --- +## Testing + +The project includes a comprehensive test suite using **xUnit** and **Moq** for unit testing. + +### Running Tests + +```powershell +# Run all tests +dotnet test src/MaksIT.UScheduler.Tests + +# Run with verbose output +dotnet test src/MaksIT.UScheduler.Tests --verbosity normal +``` + +### Code Coverage + +Coverage badges are generated locally using [ReportGenerator](https://github.com/danielpalme/ReportGenerator). + +**Prerequisites:** + +```powershell +dotnet tool install --global dotnet-reportgenerator-globaltool +``` + +**Generate coverage report and badges:** + +```powershell +.\src\scripts\Run-Coverage\Run-Coverage.ps1 + +# With HTML report opened in browser +.\src\scripts\Run-Coverage\Run-Coverage.ps1 -OpenReport +``` + +### Test Structure + +| Test Class | Coverage | +|------------|----------| +| `ConfigurationTests` | Configuration POCOs and default values | +| `ProcessBackgroundServiceTests` | Process execution lifecycle and error handling | +| `PSScriptBackgroundServiceTests` | PowerShell script execution and signature validation | + +--- + ## Contact Maksym Sadovnychyy – MAKS-IT, 2025 diff --git a/badges/coverage-branches.svg b/badges/coverage-branches.svg new file mode 100644 index 0000000..f8e8de3 --- /dev/null +++ b/badges/coverage-branches.svg @@ -0,0 +1,21 @@ + + Branch Coverage: 9.6% + + + + + + + + + + + + + + + Branch Coverage + + 9.6% + + diff --git a/badges/coverage-lines.svg b/badges/coverage-lines.svg new file mode 100644 index 0000000..00b2fee --- /dev/null +++ b/badges/coverage-lines.svg @@ -0,0 +1,21 @@ + + Line Coverage: 18.6% + + + + + + + + + + + + + + + Line Coverage + + 18.6% + + diff --git a/badges/coverage-methods.svg b/badges/coverage-methods.svg new file mode 100644 index 0000000..2f8480e --- /dev/null +++ b/badges/coverage-methods.svg @@ -0,0 +1,21 @@ + + Method Coverage: 43.2% + + + + + + + + + + + + + + + Method Coverage + + 43.2% + + diff --git a/src/MaksIT.UScheduler.ScheduleManager/App.xaml b/src/MaksIT.UScheduler.ScheduleManager/App.xaml new file mode 100644 index 0000000..eded4c6 --- /dev/null +++ b/src/MaksIT.UScheduler.ScheduleManager/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/src/MaksIT.UScheduler.ScheduleManager/App.xaml.cs b/src/MaksIT.UScheduler.ScheduleManager/App.xaml.cs new file mode 100644 index 0000000..89fbef8 --- /dev/null +++ b/src/MaksIT.UScheduler.ScheduleManager/App.xaml.cs @@ -0,0 +1,7 @@ +using System.Windows; + +namespace MaksIT.UScheduler.ScheduleManager; + +public partial class App : Application +{ +} diff --git a/src/MaksIT.UScheduler.ScheduleManager/MainWindow.xaml b/src/MaksIT.UScheduler.ScheduleManager/MainWindow.xaml new file mode 100644 index 0000000..63ce6d3 --- /dev/null +++ b/src/MaksIT.UScheduler.ScheduleManager/MainWindow.xaml @@ -0,0 +1,445 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +