diff --git a/src/logging.fallback.ps1 b/src/logging.fallback.ps1 new file mode 100644 index 0000000..62e5996 --- /dev/null +++ b/src/logging.fallback.ps1 @@ -0,0 +1,334 @@ +#region Globals + +if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() +} + +if (-not $Global:EventSinkCache -or -not ($Global:EventSinkCache -is [hashtable])) { + $Global:EventSinkCache = @{} +} + +#endregion Globals + +#region Helpers: Formatting + File + +function Get-LogColor { + param( + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level + ) + switch ($Level) { + "Info" { "Cyan" } + "Warning" { "Yellow" } + "Error" { "Red" } + "Success" { "Green" } + default { "White" } + } +} + +function Get-EventIdForLevel { + param( + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level, + [int]$CustomEventID + ) + if ($CustomEventID) { return $CustomEventID } + switch ($Level) { + "Info" { 1000 } + "Warning" { 2000 } + "Error" { 3000 } + "Success" { 4000 } + default { 1000 } + } +} + +function Get-EventEntryTypeForLevel { + param( + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level + ) + switch ($Level) { + "Info" { "Information" } + "Warning" { "Warning" } + "Error" { "Error" } + "Success" { "Information" } + default { "Information" } + } +} + +function Append-Utf8NoBomLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)] [string]$Path, + [Parameter(Mandatory)] [string]$Line + ) + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($Path, $Line + [Environment]::NewLine, $utf8NoBom) +} + +#endregion Helpers: Formatting + File + +#region Helpers: Event Log Binding + +function Test-IsAdmin { + try { + $current = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($current) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } catch { + return $false + } +} + +function Initialize-EventLogBinding { + [CmdletBinding()] + param( + [Parameter(Mandatory)] [string]$DesiredLog, + [Parameter(Mandatory)] [string]$DesiredSource, + [ValidateSet('Repair', 'Unique', 'Follow')] + [string]$ConflictPolicy = 'Repair' + ) + + $isAdmin = Test-IsAdmin + + $effectiveLog = $DesiredLog + $effectiveSource = $DesiredSource + + function Ensure-LogAndSource { + param([string]$LogName, [string]$SourceName) + + if (-not $isAdmin) { return $false } + + if (-not [System.Diagnostics.EventLog]::SourceExists($SourceName)) { + New-EventLog -LogName $LogName -Source $SourceName -ErrorAction Stop + } + elseif ([System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, '.') -ne $LogName) { + return $false + } + + return $true + } + + if ([System.Diagnostics.EventLog]::SourceExists($DesiredSource)) { + $boundLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($DesiredSource, '.') + + if ($boundLog -ne $DesiredLog) { + switch ($ConflictPolicy) { + 'Follow' { + $effectiveLog = $boundLog + $effectiveSource = $DesiredSource + } + + 'Unique' { + $candidate = "$DesiredSource.SAMY" + $i = 0 + while ([System.Diagnostics.EventLog]::SourceExists($candidate)) { + $i++ + $candidate = "$DesiredSource.SAMY$i" + } + + $effectiveLog = $DesiredLog + $effectiveSource = $candidate + + $ok = Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource + if (-not $ok) { throw "Unable to create unique Event Log source '$effectiveSource' under '$effectiveLog'." } + } + + 'Repair' { + if (-not $isAdmin) { + throw "Event source '$DesiredSource' is bound to '$boundLog' but repair requires elevation." + } + + if (Get-Command Remove-EventLog -ErrorAction SilentlyContinue) { + Remove-EventLog -Source $DesiredSource -ErrorAction Stop + } + else { + $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\$boundLog\$DesiredSource" + if (Test-Path $regPath) { + Remove-Item -Path $regPath -Recurse -Force -ErrorAction Stop + } + } + + $effectiveLog = $DesiredLog + $effectiveSource = $DesiredSource + + Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource | Out-Null + + $verify = [System.Diagnostics.EventLog]::LogNameFromSourceName($effectiveSource, '.') + if ($verify -ne $effectiveLog) { + throw "Repair failed: '$effectiveSource' is still bound to '$verify' (wanted '$effectiveLog')." + } + } + } + } + else { + # Bound correctly. Nothing else required. + $effectiveLog = $DesiredLog + $effectiveSource = $DesiredSource + } + } + else { + if ($isAdmin) { + $effectiveLog = $DesiredLog + $effectiveSource = $DesiredSource + Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource | Out-Null + } + else { + $effectiveLog = 'Application' + $effectiveSource = 'Windows PowerShell' + } + } + + [pscustomobject]@{ LogName = $effectiveLog; Source = $effectiveSource; IsAdmin = $isAdmin } +} + +#endregion Helpers: Event Log Binding + +#region Public: Write-LogHelper + +function global:Write-LogHelper { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Message, + + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level = "Info", + + [string]$TaskCategory = "GeneralTask", + + [switch]$LogToEvent = $false, + + [string]$EventSource = "SAMY", + + [string]$EventLog = "SVSMSP Events", + + [ValidateSet('Repair', 'Unique', 'Follow')] + [string]$EventLogConflictPolicy = 'Repair', + + [int]$CustomEventID, + + [string]$LogFile, + + [switch]$PassThru + ) + + $EventID = Get-EventIdForLevel -Level $Level -CustomEventID $CustomEventID + $Color = Get-LogColor -Level $Level + $EntryType = Get-EventEntryTypeForLevel -Level $Level + + $FormattedMessage = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + Write-Host $FormattedMessage -ForegroundColor $Color + + $logEntry = [PSCustomObject]@{ + Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + Level = $Level + Message = $FormattedMessage + } + [void]$Global:LogCache.Add($logEntry) + + if ($LogFile) { + try { + Append-Utf8NoBomLine -Path $LogFile -Line "$($logEntry.Timestamp) $FormattedMessage" + } + catch { + Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + if ($LogToEvent) { + $desiredKey = "$EventLog|$EventSource|$EventLogConflictPolicy" + + if (-not $Global:EventSinkCache.ContainsKey($desiredKey)) { + try { + $ev = Initialize-EventLogBinding -DesiredLog $EventLog -DesiredSource $EventSource -ConflictPolicy $EventLogConflictPolicy + + $Global:EventSinkCache[$desiredKey] = [pscustomobject]@{ + Ready = $true + LogName = $ev.LogName + Source = $ev.Source + } + } + catch { + $Global:EventSinkCache[$desiredKey] = [pscustomobject]@{ + Ready = $false + LogName = $EventLog + Source = $EventSource + Error = $_.Exception.Message + } + + Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + $sink = $Global:EventSinkCache[$desiredKey] + + if ($sink.Ready) { + try { + $EventMessage = "TaskCategory: $TaskCategory | Message: $Message" + Write-EventLog -LogName $sink.LogName -Source $sink.Source -EntryType $EntryType -EventId $EventID -Message $EventMessage + } + catch { + Write-Host "[Warning] Failed to write to Event Log '$($sink.LogName)' / source '$($sink.Source)': $($_.Exception.Message)" -ForegroundColor Yellow + } + } + else { + Write-Host "[Warning] Event Log not initialized for '$EventLog' / '$EventSource'. Skipping Event Log write." -ForegroundColor Yellow + } + } + + if ($PassThru) { return $logEntry } +} + +#endregion Public: Write-LogHelper + +#region Public: Write-LogHybrid + +function global:Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "SVSMSP_Module", + [string]$EventLog = "SVSMSP Events", + [ValidateSet('Repair', 'Unique', 'Follow')] + [string]$EventLogConflictPolicy = 'Repair', + [int]$CustomEventID, + [string]$LogFile, + [switch]$PassThru, + [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] + [string]$ForegroundColorOverride + ) + + $formatted = "[$Level] [$TaskCategory] $Message" + + $invokeParams = @{ + Message = $Message + Level = $Level + TaskCategory = $TaskCategory + LogToEvent = $LogToEvent + EventSource = $EventSource + EventLog = $EventLog + EventLogConflictPolicy = $EventLogConflictPolicy + } + + if ($PSBoundParameters.ContainsKey('CustomEventID')) { $invokeParams.CustomEventID = $CustomEventID } + if ($PSBoundParameters.ContainsKey('LogFile')) { $invokeParams.LogFile = $LogFile } + if ($PassThru) { $invokeParams.PassThru = $true } + + if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { + Write-Host $formatted -ForegroundColor $ForegroundColorOverride + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } + else { Write-LogHelper @invokeParams } + } + else { + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } + else { Write-LogHelper @invokeParams } + } +} + +#endregion Public: Write-LogHybrid