Private/Export-RaidinessArtifact.ps1
|
<# The bridge: the report page, run headlessly, writing its own files. The page builds run.json, report.md, the LLM bundle and a standalone HTML report -- but only ever into a browser download, which lands in the operating system's download folder and which PowerShell can neither aim nor observe. With ?raidiness=export the page skips rendering, puts the same files base64 into one script element, and marks itself ready; this reads that element off stdout and writes the files where the operator asked. Nothing here throws. A missing browser, a timeout, a half-built page: all of them return what was produced and let the caller fall back to opening the report, which is exactly what happened before this existed. A checkup that fails because a browser flag changed would be a bad trade. #> # psrunner-lint allow: New-Item — creates the local llm-bundle directory in the operator's run folder; never a tenant object # psrunner-lint allow: Remove-Item — deletes the local temp profile and the unpacked zip; never a tenant object function Export-RaidinessArtifact { [CmdletBinding()] [OutputType([pscustomobject])] param( # The report.html that already carries the injected payload. [Parameter(Mandatory = $true)] [string] $ReportPath, # The run folder the files are written into. [Parameter(Mandatory = $true)] [string] $RunPath, [Parameter(Mandatory = $true)] [string] $Browser, [switch] $Pdf, [int] $TimeoutSeconds = 180 ) $ErrorActionPreference = 'Stop' $report = (Resolve-Path -Path $ReportPath).Path $profileDir = Join-Path ([System.IO.Path]::GetTempPath()) ("raidiness-browser-" + [guid]::NewGuid()) $written = [System.Collections.Generic.List[string]]::new() try { $arguments = @( '--headless=new' '--disable-gpu' '--no-first-run' '--no-default-browser-check' '--disable-extensions' "--user-data-dir=$profileDir" "--virtual-time-budget=$($TimeoutSeconds * 1000)" '--dump-dom' "file://$report`?raidiness=export" ) # An escape hatch for an unusual environment -- a CI container running # as root needs --no-sandbox, for instance. Not a default: those flags # weaken the browser, and a checkup should not do that silently. if ($env:RAIDINESS_BROWSER_ARGS) { $arguments += $env:RAIDINESS_BROWSER_ARGS.Split(' ', [StringSplitOptions]::RemoveEmptyEntries) } Write-RaidinessLog -Phase 'export' -Message "building the exports with $Browser" $result = Invoke-RaidinessBrowserProcess -Browser $Browser -ArgumentList $arguments -TimeoutSeconds ($TimeoutSeconds + 30) if ($result.TimedOut) { Write-RaidinessLog -Level warn -Phase 'export' -Message "the browser did not finish within $TimeoutSeconds s" return [pscustomobject]@{ Ok = $false; Files = @(); Reason = "the browser did not finish within $TimeoutSeconds seconds" } } $match = [regex]::Match($result.Output, '(?s)<script id="raidiness-artifacts" type="application/json">(.*?)</script>') if (-not $match.Success) { # Keep the dump: "the page never got there" and "the page broke" # look identical from here, and the difference is in that file. $dump = Join-Path $RunPath 'raidiness-browser-dump.txt' $result.Output | Out-File -FilePath $dump -Encoding utf8 if ($result.Error) { $result.Error | Out-File -FilePath $dump -Append -Encoding utf8 } # The browser usually says exactly what was wrong on stderr, and # burying that in a file while telling the operator "no artifacts" # sends them to read a dump for a sentence we already have. $why = ($result.Error -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -Last 1) $detail = $why ? " The browser said: $($why.Trim())" : '' Write-RaidinessLog -Level warn -Phase 'export' -Message "the report page produced no artifacts (exit $($result.ExitCode)).$detail" return [pscustomobject]@{ Ok = $false; Files = @(); Reason = "the report page produced no artifacts.$detail See $dump" } } $payload = $match.Groups[1].Value.Replace('<\/', '</') | ConvertFrom-Json if (-not $payload.ok) { $reason = $payload.PSObject.Properties['error'] ? $payload.error : 'the report page refused to build the exports' Write-RaidinessLog -Level warn -Phase 'export' -Message "the report page refused: $reason" return [pscustomobject]@{ Ok = $false; Files = @(); Reason = $reason } } foreach ($file in $payload.files) { # The page is our own build, but it names a path that is written on # this machine; refusing a traversal here costs one line. if ($file.path -match '(^[\\/])|(\.\.)|(^[A-Za-z]:)') { Write-RaidinessLog -Level warn -Phase 'export' -Message "refused an artifact path: $($file.path)" continue } $target = Join-Path $RunPath $file.path [System.IO.File]::WriteAllBytes($target, [Convert]::FromBase64String($file.base64)) $written.Add($file.path) } # The bundle travels as one zip and is unpacked here: a folder of # markdown is what a person wants to open, and System.IO.Compression is # in the box, so the page does not have to hand back twelve files. $zip = Join-Path $RunPath 'llm-bundle.zip' if (Test-Path -Path $zip) { $folder = Join-Path $RunPath 'llm-bundle' if (Test-Path -Path $folder) { Remove-Item -Path $folder -Recurse -Force } # The zip already carries the llm-bundle/ prefix, so it unpacks into # the run folder; extracting into llm-bundle/ nested it twice. [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $RunPath) Remove-Item -Path $zip -Force $written.Remove('llm-bundle.zip') | Out-Null $written.Add('llm-bundle/') } if ($Pdf) { $pdfPath = Join-Path $RunPath 'report.pdf' $printed = Join-Path $RunPath 'report-print.html' if (Test-Path -Path $printed) { $pdfArguments = @( '--headless=new' '--disable-gpu' '--no-first-run' "--user-data-dir=$profileDir" '--no-pdf-header-footer' "--print-to-pdf=$pdfPath" "file://$printed" ) if ($env:RAIDINESS_BROWSER_ARGS) { $pdfArguments += $env:RAIDINESS_BROWSER_ARGS.Split(' ', [StringSplitOptions]::RemoveEmptyEntries) } $pdfResult = Invoke-RaidinessBrowserProcess -Browser $Browser -ArgumentList $pdfArguments -TimeoutSeconds $TimeoutSeconds if ((-not $pdfResult.TimedOut) -and (Test-Path -Path $pdfPath)) { $written.Add('report.pdf') } else { # A missing PDF is a smaller loss than a failed run. Write-RaidinessLog -Level warn -Phase 'export' -Message 'the PDF could not be printed' } } } Write-RaidinessLog -Phase 'export' -Message "wrote $($written.Count) file(s) into $RunPath" [pscustomobject]@{ Ok = $true; Files = $written.ToArray(); Reason = $null } } catch { Write-RaidinessLog -Level error -Phase 'export' -Message "the export bridge failed: $($_.Exception.Message)" [pscustomobject]@{ Ok = $false; Files = $written.ToArray(); Reason = $_.Exception.Message } } finally { if (Test-Path -Path $profileDir) { Remove-Item -Path $profileDir -Recurse -Force -ErrorAction SilentlyContinue } } } |