Add src/logging.fallback.ps1

This commit is contained in:
2026-01-31 17:48:45 -05:00
parent e9b210ad33
commit 32678483f7

334
src/logging.fallback.ps1 Normal file
View File

@@ -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