Modules/businessdev.ALbuild.Containers/Public/Install-BcCaptureBrowser.ps1
|
function Install-BcCaptureBrowser { <# .SYNOPSIS Installs the headless browser 'albuild screenshot' captures with: Playwright at the pinned version plus its Chromium. .DESCRIPTION This is the one expensive prerequisite of the capture command - a few hundred megabytes per agent - and the reason it is a separate, explicit step rather than something that happens on first use. A command that downloads half a gigabyte because someone asked for a screenshot has made a decision that was not theirs to make. Installed PER PLAYWRIGHT VERSION under the ALbuild base folder, with the browsers inside that same folder (PLAYWRIGHT_BROWSERS_PATH), NOT in the user profile. Two reasons, both paid for elsewhere in this toolchain already: * The MCP server commonly runs as a Windows service under LocalSystem. A browser under an interactive account's %LOCALAPPDATA% is invisible to it - the install reports success and every capture afterwards fails as though nothing had been installed. * On a build server the base folder is deliberately off the system drive; the default Playwright cache is not. Per-version isolation means a pin bump installs beside the old browser instead of over it, so a build that is still running against the previous version keeps working. Idempotent: an installation that already satisfies the pin is left alone unless -Force. Node.js is the one part this does NOT install. It is a machine-level runtime, and silently putting one on a build agent is a bigger decision than caching a browser - so a missing Node is an error that names the install command. .PARAMETER InstallPath Where to install. Default: <BaseFolder>\capture\<playwright-version>. .PARAMETER Force Reinstall even when the pinned version is already present. .PARAMETER PruneOtherVersions After the pinned version is in place, remove the OTHER version folders under the capture root. Each one is a few hundred megabytes, and without this an agent accumulates one per pin bump until somebody notices the disk. A folder whose files are locked - a capture running against the older version right now - is reported and left alone rather than half-deleted. .PARAMETER NodeExecutable The Node.js executable (default 'node'). .PARAMETER NpmExecutable The npm executable (default 'npm'). .OUTPUTS PSCustomObject with Installed (whether anything was downloaded this call) plus the fields of Get-BcCaptureBrowser. .EXAMPLE Install-BcCaptureBrowser .EXAMPLE Install-BcCaptureBrowser -Force # Reinstalls the pinned Playwright and re-downloads Chromium into the same per-version folder. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [string] $InstallPath, [switch] $Force, [switch] $PruneOtherVersions, [string] $NodeExecutable = 'node', [string] $NpmExecutable = 'npm' ) $resource = Get-BcCaptureResource $pinned = $resource.PlaywrightVersion if (-not $InstallPath) { $baseFolder = Get-ALbuildConfig -Name BaseFolder $InstallPath = Join-Path -Path (Join-Path -Path $baseFolder -ChildPath 'capture') -ChildPath $pinned } $browsersPath = Join-Path -Path $InstallPath -ChildPath 'browsers' $status = Get-BcCaptureBrowser -InstallPath $InstallPath -NodeExecutable $NodeExecutable # Node first, and as a hard error: every step below shells into it, and 'npm ERR! ... not found' is # a worse way to learn this than being told which command installs it. if ($status.Missing -contains 'node') { throw ("BROWSER_NOT_INSTALLED: Node.js 18 or newer is required to capture web client " + "screenshots and was not found$(if ($status.NodeVersion) { " (found $($status.NodeVersion))" }). " + "Install it and run this again: $($status.Remedy)") } if ($status.Ready -and -not $Force) { Write-ALbuildLog "Capture browser already present: Playwright $pinned in '$InstallPath' (cached)." $pruned = @(if ($PruneOtherVersions) { Remove-BcCaptureBrowserVersion -KeepPath $InstallPath -Confirm:$false }) return ($status | Add-Member -NotePropertyName Installed -NotePropertyValue $false -PassThru -Force | Add-Member -NotePropertyName Pruned -NotePropertyValue $pruned -PassThru -Force) } if (-not $PSCmdlet.ShouldProcess("Playwright $pinned + Chromium", "Install the capture browser into '$InstallPath'")) { return ($status | Add-Member -NotePropertyName Installed -NotePropertyValue $false -PassThru -Force) } if (-not (Test-Path -LiteralPath $InstallPath)) { New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null } # npm installs from the manifest in --prefix, so the module's pinned manifest is copied in. Copying # rather than pointing at the module folder keeps the install self-contained: the module can be # updated or removed underneath a warm agent without stranding node_modules. Copy-Item -LiteralPath $resource.ManifestPath -Destination (Join-Path -Path $InstallPath -ChildPath 'package.json') -Force # PLAYWRIGHT_BROWSERS_PATH steers BOTH steps: the package's own postinstall and the explicit # download below. Invoke-ALbuildProcess launches children with the current environment, so a # process-scoped variable is the transport - restored afterwards so a long-lived session (the MCP # server) does not keep a global pointing at one version's folder. $previousBrowsersPath = $env:PLAYWRIGHT_BROWSERS_PATH try { $env:PLAYWRIGHT_BROWSERS_PATH = $browsersPath Write-ALbuildLog "Installing Playwright $pinned into '$InstallPath'..." $npmArguments = @('install', '--prefix', $InstallPath, '--no-audit', '--no-fund') $npmResult = Invoke-ALbuildProcess -FilePath $NpmExecutable -Arguments $npmArguments -PassThru -SuccessExitCodes @(0) $npmOutput = "$($npmResult.StdOut)`n$($npmResult.StdErr)".Trim() if ($npmOutput) { Write-ALbuildLog "npm $($npmArguments -join ' '):`n$npmOutput" } if (-not $npmResult.Success) { throw "BROWSER_NOT_INSTALLED: npm could not install Playwright $pinned into '$InstallPath' [exit $($npmResult.ExitCode)]: $npmOutput" } # The package's postinstall usually fetches Chromium already; this makes it explicit and is a # no-op when it is there. Two candidate CLI entry points because the file moved between major # Playwright lines - refusing by name beats calling into something that does not exist. $playwrightRoot = Join-Path -Path $InstallPath -ChildPath 'node_modules' $cliCandidates = @( (Join-Path -Path (Join-Path -Path $playwrightRoot -ChildPath 'playwright') -ChildPath 'cli.js') (Join-Path -Path (Join-Path -Path $playwrightRoot -ChildPath 'playwright-core') -ChildPath 'cli.js') ) $cli = $cliCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $cli) { throw ("BROWSER_NOT_INSTALLED: Playwright $pinned installed but its CLI was not found " + "(looked for: $($cliCandidates -join ', ')). The install is incomplete; re-run with -Force.") } Write-ALbuildLog "Downloading Chromium into '$browsersPath' (a few hundred MB on a cold agent)..." $downloadResult = Invoke-ALbuildProcess -FilePath $NodeExecutable -Arguments @($cli, 'install', 'chromium') -PassThru -SuccessExitCodes @(0) $downloadOutput = "$($downloadResult.StdOut)`n$($downloadResult.StdErr)".Trim() if ($downloadOutput) { Write-ALbuildLog "playwright install chromium:`n$downloadOutput" } if (-not $downloadResult.Success) { throw "BROWSER_NOT_INSTALLED: Chromium could not be downloaded into '$browsersPath' [exit $($downloadResult.ExitCode)]: $downloadOutput" } } finally { $env:PLAYWRIGHT_BROWSERS_PATH = $previousBrowsersPath } # Verified against the same check the caller will use, not against the exit codes above: an npm that # exits 0 having installed the wrong version still leaves this host unable to capture. $status = Get-BcCaptureBrowser -InstallPath $InstallPath -NodeExecutable $NodeExecutable if (-not $status.Ready) { throw ("BROWSER_NOT_INSTALLED: the install finished but '$InstallPath' is still not usable " + "(missing: $($status.Missing -join ', ')). Re-run with -Force, or check the output above.") } Write-ALbuildLog "Capture browser ready: Playwright $pinned, Chromium at '$($status.ChromiumPath)'." # Pruned only AFTER the new version is verified usable: removing the old one first would leave an # agent with nothing at all if the download failed. $pruned = @(if ($PruneOtherVersions) { Remove-BcCaptureBrowserVersion -KeepPath $InstallPath -Confirm:$false }) return ($status | Add-Member -NotePropertyName Installed -NotePropertyValue $true -PassThru -Force | Add-Member -NotePropertyName Pruned -NotePropertyValue $pruned -PassThru -Force) } |