Classes/PsGadgetLogger.ps1
|
#Requires -Version 5.1 # PsGadgetLogger Class # Unified session + protocol logger. All device instances share a single module-level # singleton (created in PSGadget.psm1) that appends to a fixed # ~/.psgadget/logs/psgadget.log file. When TraceEnabled = $true (set by # Start-PsGadgetTrace), WriteProto() entries are also written at [PROTO] level. # Rolls to psgadget.1.log when the file exceeds _maxSizeMb at open time. class PsGadgetLogger { [string] $LogFilePath [string] $SessionId [datetime]$StartTime [bool] $TraceEnabled # false until Start-PsGadgetTrace is called hidden [System.IO.StreamWriter]$_writer hidden [int]$_maxSizeMb PsGadgetLogger() { $this.StartTime = [datetime]::Now $this.SessionId = [System.Guid]::NewGuid().ToString().Substring(0, 8) $this.TraceEnabled = $false # Read max size from config if available; default 50 MB try { $cfg = $script:PsGadgetConfig if ($cfg -and $cfg.logging -and $cfg.logging.maxSizeMb) { $this._maxSizeMb = [int]$cfg.logging.maxSizeMb } else { $this._maxSizeMb = 50 } } catch { $this._maxSizeMb = 50 } $userHome = [Environment]::GetFolderPath('UserProfile') $logDir = Join-Path $userHome '.psgadget/logs' if (-not (Test-Path -LiteralPath $logDir)) { $null = New-Item -Path $logDir -ItemType Directory -Force } $this.LogFilePath = Join-Path $logDir 'psgadget.log' # Roll if the file already exceeds maxSizeMb try { if (Test-Path -LiteralPath $this.LogFilePath) { $sizeMb = (Get-Item -LiteralPath $this.LogFilePath).Length / 1MB if ($sizeMb -ge $this._maxSizeMb) { $backup = Join-Path $logDir 'psgadget.1.log' Copy-Item -LiteralPath $this.LogFilePath -Destination $backup -Force # Truncate by recreating [System.IO.File]::WriteAllText($this.LogFilePath, '') } } } catch {} # Open for append with ReadWrite share so Get-Content -Wait can tail it try { $fs = [System.IO.File]::Open( $this.LogFilePath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite ) $this._writer = [System.IO.StreamWriter]::new($fs, [System.Text.Encoding]::UTF8) $this._writer.AutoFlush = $true } catch { # Silent -- log failure must never break hardware operations return } $this.WriteHeader() } hidden [void] WriteHeader() { $ts = $this.StartTime.ToString('yyyy-MM-dd HH:mm:ss') $lines = @( "=== PsGadget Session $($this.SessionId) $ts ===", "OS: $([System.Environment]::OSVersion.VersionString)", "PowerShell: $($global:PSVersionTable.PSVersion)", "User: $([System.Environment]::UserName) Computer: $([System.Environment]::MachineName)" ) foreach ($line in $lines) { $this.WriteToFile('HEADER', $line) } } hidden [void] WriteToFile([string]$Level, [string]$Message) { if (-not $this._writer) { return } try { $ts = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff') $this._writer.WriteLine("[$ts] [$Level] $Message") } catch {} } # -- Standard log levels -------------------------------------------------- [void] WriteInfo([string]$Message) { $this.WriteToFile('INFO', $Message) Write-Verbose $Message } [void] WriteDebug([string]$Message) { $this.WriteToFile('DEBUG', $Message) Write-Debug $Message } [void] WriteTrace([string]$Message) { $this.WriteToFile('TRACE', $Message) } [void] WriteError([string]$Message) { $this.WriteToFile('ERROR', $Message) Write-Warning $Message } # -- Protocol-level entries (WriteProto) ---------------------------------- # Written only when TraceEnabled = $true (set by Start-PsGadgetTrace). # Format: # [timestamp] [PROTO] {Subsystem,-12} {Summary} # [timestamp] [PROTO] {' '*12} RAW {RawHex} [void] WriteProto([string]$Subsystem, [string]$Summary) { if (-not $this.TraceEnabled -or -not $this._writer) { return } try { $ts = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff') $this._writer.WriteLine("[$ts] [PROTO] $($Subsystem.PadRight(12)) $Summary") } catch {} } [void] WriteProto([string]$Subsystem, [string]$Summary, [string]$RawHex) { if (-not $this.TraceEnabled -or -not $this._writer) { return } try { $ts = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff') $pad = ' ' * 12 $this._writer.WriteLine("[$ts] [PROTO] $($Subsystem.PadRight(12)) $Summary") if ($RawHex) { $this._writer.WriteLine("[$ts] [PROTO] $pad RAW $RawHex") } } catch {} } # -- Hex formatting helper -------------------- [string] FormatHex([byte[]]$bytes) { return $this.FormatHex($bytes, 64) } [string] FormatHex([byte[]]$bytes, [int]$maxBytes) { if (-not $bytes -or $bytes.Length -eq 0) { return '' } $take = [System.Math]::Min($bytes.Length, $maxBytes) $hex = ($bytes[0..($take - 1)] | ForEach-Object { $_.ToString('X2') }) -join ' ' if ($bytes.Length -gt $maxBytes) { $hex += " [...+$($bytes.Length - $maxBytes)]" } return $hex } # Truncate the log file and reopen the writer so subsequent writes land at offset 0. # Used by Start-PsGadgetTrace -Clear -- calling Clear-Content externally leaves the # StreamWriter's position stale, causing writes to be preceded by a null-byte gap. [void] Clear() { try { if ($this._writer) { $this._writer.Flush() $this._writer.Close() $this._writer = $null } # Truncate using ReadWrite share so Get-Content -Wait in any open viewer # does not cause WriteAllText (exclusive access) to fail silently. $tfs = [System.IO.File]::Open( $this.LogFilePath, [System.IO.FileMode]::Truncate, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite ) $tfs.Close() $fs = [System.IO.File]::Open( $this.LogFilePath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite ) $this._writer = [System.IO.StreamWriter]::new($fs, [System.Text.Encoding]::UTF8) $this._writer.AutoFlush = $true $this.WriteHeader() } catch {} } [void] Dispose() { if ($this._writer) { try { $this._writer.Close() } catch {} $this._writer = $null } } } |