Files
winutil/functions/private/Write-WinUtilLog.ps1
T
Malin Fossum d0d39d6478 Warn instead of logging "tweak completed" after a tweak step error (#5090)
* Warn instead of logging tweak completed after a step error

Invoke-WinUtilTweaks wrote "Apply tweak completed" unconditionally, even
when Invoke-WinUtilScript or another helper had just logged an ERROR for
that tweak. The job layer already counts those errors, so snapshot the
count after the header line and compare at the end: log a WARN line with
the error count when it grew, otherwise the existing completed line.

Two Pester cases run the real logger and script runner against a temp
log file to pin both outcomes.

* Count tweak step errors from the shared log list

The job error counter only increments inside a Start-WinUtilJob worker.
Toggle switches call Invoke-WinUtilTweaks directly on the UI thread, so
a failing toggle still logged "tweak completed" after its ERROR line.

Every ERROR line is added to $sync.LoggedErrors from any runspace, and
Invoke-WinUtilAutoRun already reads it as a before/after delta, so the
tweak runner now does the same. The completion-status tests run without
the worker flag and seed an earlier unrelated error, and a new case
covers a failing UndoScript.

* Count tweak errors on the logging runspace, not the shared list

Diffing $sync.LoggedErrors charged a toggle with errors a concurrent
job logged from its own runspace. Global scope is per runspace, so the
logger now bumps the runspace counter for every headline error and the
tweak status diffs that. Job workers still reset the counter at start
and end, so job results are unchanged.
2026-09-29 10:58:32 -05:00

126 lines
4.4 KiB
PowerShell

function Write-WinUtilLog {
<#
.SYNOPSIS
Writes a timestamped WinUtil log entry to the active session log.
.DESCRIPTION
Called from the interface thread and from every job body. When Start-Transcript owns the
active session log, entries go through the host so the transcript records them without a
competing file write. Standalone callers use a named mutex to serialize direct appends.
.PARAMETER Message
The message to write.
.PARAMETER Level
The severity level for the log entry.
.PARAMETER Component
The WinUtil component producing the log entry.
#>
param (
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")]
[string]$Level = "INFO",
[string]$Component = "WinUtil",
# Continuation of an error already counted, such as a stack frame
[switch]$Detail
)
# UI performance diagnostics are useful to developers but are too noisy for the release
# transcript. Compile.ps1 stamps local builds so DEBUG output cannot leak into CI artifacts.
if ($Level -eq "DEBUG" -and ($null -eq $sync -or -not $sync.IsLocalCompile)) {
return
}
if ($Level -eq "ERROR" -and -not $Detail -and $null -ne $sync.LoggedErrors) {
$null = $sync.LoggedErrors.Add("[$Component] $Message")
}
# Global scope is per runspace, so this counter only ever sees errors logged by the
# runspace that owns it: a job worker reads its own, and a tweak on the UI thread reads the
# UI thread's
if ($Level -eq "ERROR" -and -not $Detail) {
$global:WinUtilJobErrorCount++
}
try {
$logPath = $null
$transcriptPath = $null
if ($null -ne $sync -and $sync.ContainsKey("logPath")) {
$logPath = $sync.logPath
}
if ($null -ne $sync -and $sync.ContainsKey("transcriptPath")) {
$transcriptPath = $sync.transcriptPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($transcriptPath)) {
$logPath = $transcriptPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and $null -ne $sync -and $sync.ContainsKey("winutildir")) {
$logDirectory = Join-Path $sync.winutildir "logs"
$logPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
$sync.logPath = $logPath
}
if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($env:LocalAppData)) {
if ([string]::IsNullOrWhiteSpace($script:WinUtilLogPath)) {
$logDirectory = Join-Path (Join-Path $env:LocalAppData "winutil") "logs"
$script:WinUtilLogPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log"
}
$logPath = $script:WinUtilLogPath
}
if ([string]::IsNullOrWhiteSpace($logPath)) {
return
}
$logDirectory = Split-Path -Path $logPath -Parent
if (-not (Test-Path $logDirectory)) {
New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$line = "[$timestamp] [$Level] [$Component] $Message"
if (-not [string]::IsNullOrWhiteSpace($transcriptPath) -and $logPath -eq $transcriptPath) {
Write-Host $line
return
}
$mutex = [System.Threading.Mutex]::new($false, "WinUtilSessionLog")
$held = $false
try {
try {
$held = $mutex.WaitOne(2000)
} catch [System.Threading.AbandonedMutexException] {
# A thread died holding the mutex; ownership transfers to us either way
$held = $true
}
if (-not $held) {
# Writing anyway is what interleaves lines, and the wait only times out when
# contention is at its worst
Write-Host $line
return
}
Add-Content -Path $logPath -Value $line -Encoding UTF8 -ErrorAction Stop
} catch [System.IO.IOException] {
Write-Host $line
} finally {
if ($held) { $mutex.ReleaseMutex() }
$mutex.Dispose()
}
} catch {
Write-Warning "Unable to write WinUtil log entry: $($_.Exception.Message)"
}
}