Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcRuntimeLogTail.ps1
|
function Get-BcRuntimeLogTail { <# .SYNOPSIS Reads the lines a worker has appended to its log since the last look, and keeps only the ones that describe a phase. .DESCRIPTION Until this existed, a running slice showed nothing at all for the ~20 minutes a container takes to build: the worker logged 'Creating container ...' into its own file, and the dispatcher only replayed anything once the whole platform version had finished. The run log went quiet for twenty minutes and then printed one line - which reads exactly like a hung agent. Tailing the whole file is the other failure, and it is the one the previous design was written to escape: a container's raw output is tens of thousands of lines of publish/sync progress. So only the worker's OWN phase lines are surfaced - the ones it writes through Write-ALbuildLog, which all carry a '[<platform version>]' tag. Everything else stays in the file and travels to the 'worker-logs' artifact. The offset is returned rather than remembered, so the caller owns the state and this stays a pure-enough function to test against a file on disk. A partial last line is deliberately NOT consumed: the returned offset stops at the last newline, so a line that is still being written is read whole on the next pass instead of being cut in two. .PARAMETER Path The worker's log file. A file that does not exist yet is not an error - a worker that has not written its first line is the normal state right after it starts. .PARAMETER Offset Byte offset returned by the previous call; 0 on the first. .PARAMETER PlatformVersion The version whose phase lines are wanted. Used to recognise the worker's own lines. .OUTPUTS PSCustomObject with Offset (long) and Line (string[]). #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path, [long] $Offset = 0, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $PlatformVersion ) $empty = [PSCustomObject]@{ Offset = $Offset; Line = @() } if (-not (Test-Path -LiteralPath $Path)) { return $empty } # FileShare.ReadWrite, because the worker holds this file open with AutoFlush on. Get-Content's # default share mode loses the race against a writer often enough to matter, and losing it here # would take down the dispatcher rather than skip a progress line. $stream = $null try { $stream = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) if ($stream.Length -le $Offset) { return $empty } [void]$stream.Seek($Offset, [System.IO.SeekOrigin]::Begin) # Cast: the difference is a [long], and Windows PowerShell 5.1 will not size a byte[] from one. $buffer = [byte[]]::new([int]($stream.Length - $Offset)) $read = $stream.Read($buffer, 0, $buffer.Length) $text = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $read) } catch { return $empty } finally { if ($stream) { $stream.Dispose() } } # Stop at the last complete line; the remainder is read again next time. $cut = $text.LastIndexOf("`n") if ($cut -lt 0) { return $empty } $complete = $text.Substring(0, $cut + 1) $newOffset = $Offset + [System.Text.Encoding]::UTF8.GetByteCount($complete) $tag = "[$PlatformVersion]" $lines = [System.Collections.Generic.List[string]]::new() foreach ($line in ($complete -split "`r?`n")) { # The BOM is stripped before anything is matched. Windows PowerShell 5.1 writes one with # '-Encoding UTF8' (PowerShell 7 does not), UTF8.GetString keeps it, and U+FEFF is not # whitespace - so the FIRST line of a 5.1-written worker log arrived as "##vso[..." and # slipped past the '##vso*' filter, replaying an Azure DevOps logging command that the # checkpoint owns. The serial path of the factory is exactly the 5.1 path, so this is a real # log and not just a test fixture. $t = "$line".TrimStart([char]0xFEFF).Trim() if ($t -eq '') { continue } # The worker's own phase lines, and nothing else. Azure DevOps logging commands are dropped: # replaying '##vso[task.logissue ...]' from here would raise the same issue twice, once now # and once from the checkpoint that owns it. if ($t -like '##vso*') { continue } if (-not $t.Contains($tag)) { continue } $lines.Add($t) } return [PSCustomObject]@{ Offset = $newOffset; Line = $lines.ToArray() } } |