398 lines
10 KiB
PowerShell
398 lines
10 KiB
PowerShell
|
|
param(
|
||
|
|
[int]$Port = 0,
|
||
|
|
[string[]]$Command = @('node', './scripts/run-cdp-smoke.mjs')
|
||
|
|
)
|
||
|
|
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
if (-not $Command -or $Command.Count -eq 0) {
|
||
|
|
throw 'Command must not be empty'
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-UrlReady {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$Url
|
||
|
|
)
|
||
|
|
|
||
|
|
try {
|
||
|
|
$response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
|
||
|
|
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
|
||
|
|
} catch {
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Wait-UrlReady {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$Url,
|
||
|
|
[Parameter(Mandatory = $true)][string]$Label,
|
||
|
|
[int]$RetryCount = 60,
|
||
|
|
[int]$DelayMs = 500
|
||
|
|
)
|
||
|
|
|
||
|
|
for ($i = 0; $i -lt $RetryCount; $i++) {
|
||
|
|
if (Test-UrlReady -Url $Url) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
Start-Sleep -Milliseconds $DelayMs
|
||
|
|
}
|
||
|
|
|
||
|
|
throw "$Label did not become ready: $Url"
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-FreeTcpPort {
|
||
|
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||
|
|
$listener.Start()
|
||
|
|
try {
|
||
|
|
return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
|
||
|
|
} finally {
|
||
|
|
$listener.Stop()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Resolve-BrowserPath {
|
||
|
|
if ($env:E2E_BROWSER_PATH) {
|
||
|
|
return $env:E2E_BROWSER_PATH
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($env:CHROME_HEADLESS_SHELL_PATH) {
|
||
|
|
return $env:CHROME_HEADLESS_SHELL_PATH
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
|
||
|
|
return $env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||
|
|
}
|
||
|
|
|
||
|
|
$baseDir = Join-Path $env:LOCALAPPDATA 'ms-playwright'
|
||
|
|
$candidate = Get-ChildItem $baseDir -Directory -Filter 'chromium_headless_shell-*' |
|
||
|
|
Sort-Object Name -Descending |
|
||
|
|
Select-Object -First 1
|
||
|
|
|
||
|
|
if ($candidate) {
|
||
|
|
return (Join-Path $candidate.FullName 'chrome-headless-shell-win64\chrome-headless-shell.exe')
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($fallback in @(
|
||
|
|
'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||
|
|
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
|
||
|
|
'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
|
||
|
|
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
|
||
|
|
)) {
|
||
|
|
if (Test-Path $fallback) {
|
||
|
|
return $fallback
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw 'No compatible browser found; set E2E_BROWSER_PATH or CHROME_HEADLESS_SHELL_PATH explicitly if needed'
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-HeadlessShellBrowser {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$BrowserPath
|
||
|
|
)
|
||
|
|
|
||
|
|
return [System.IO.Path]::GetFileName($BrowserPath).ToLowerInvariant().Contains('headless-shell')
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-BrowserArguments {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$BrowserPath,
|
||
|
|
[Parameter(Mandatory = $true)][int]$Port,
|
||
|
|
[Parameter(Mandatory = $true)][string]$ProfileDir
|
||
|
|
)
|
||
|
|
|
||
|
|
$arguments = @(
|
||
|
|
"--remote-debugging-port=$Port",
|
||
|
|
"--user-data-dir=$ProfileDir",
|
||
|
|
'--no-sandbox'
|
||
|
|
)
|
||
|
|
|
||
|
|
if (Test-HeadlessShellBrowser -BrowserPath $BrowserPath) {
|
||
|
|
$arguments += '--single-process'
|
||
|
|
} else {
|
||
|
|
$arguments += @(
|
||
|
|
'--disable-dev-shm-usage',
|
||
|
|
'--disable-background-networking',
|
||
|
|
'--disable-background-timer-throttling',
|
||
|
|
'--disable-renderer-backgrounding',
|
||
|
|
'--disable-sync',
|
||
|
|
'--headless=new'
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
$arguments += 'about:blank'
|
||
|
|
return $arguments
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-BrowserProcessIds {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$BrowserPath
|
||
|
|
)
|
||
|
|
|
||
|
|
$processName = [System.IO.Path]::GetFileNameWithoutExtension($BrowserPath)
|
||
|
|
try {
|
||
|
|
return @(Get-Process -Name $processName -ErrorAction Stop | Select-Object -ExpandProperty Id)
|
||
|
|
} catch {
|
||
|
|
return @()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-BrowserProcessesByProfile {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$BrowserPath,
|
||
|
|
[Parameter(Mandatory = $true)][string]$ProfileDir
|
||
|
|
)
|
||
|
|
|
||
|
|
$processFileName = [System.IO.Path]::GetFileName($BrowserPath)
|
||
|
|
$profileFragment = $ProfileDir.ToLowerInvariant()
|
||
|
|
|
||
|
|
try {
|
||
|
|
return @(
|
||
|
|
Get-CimInstance Win32_Process -Filter ("Name = '{0}'" -f $processFileName) -ErrorAction Stop |
|
||
|
|
Where-Object {
|
||
|
|
$commandLine = $_.CommandLine
|
||
|
|
$commandLine -and $commandLine.ToLowerInvariant().Contains($profileFragment)
|
||
|
|
}
|
||
|
|
)
|
||
|
|
} catch {
|
||
|
|
return @()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-ChildProcessIds {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][int]$ParentId
|
||
|
|
)
|
||
|
|
|
||
|
|
$pending = [System.Collections.Generic.Queue[int]]::new()
|
||
|
|
$seen = [System.Collections.Generic.HashSet[int]]::new()
|
||
|
|
$pending.Enqueue($ParentId)
|
||
|
|
|
||
|
|
while ($pending.Count -gt 0) {
|
||
|
|
$currentParentId = $pending.Dequeue()
|
||
|
|
|
||
|
|
try {
|
||
|
|
$children = @(Get-CimInstance Win32_Process -Filter ("ParentProcessId = {0}" -f $currentParentId) -ErrorAction Stop)
|
||
|
|
} catch {
|
||
|
|
$children = @()
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($child in $children) {
|
||
|
|
if ($seen.Add([int]$child.ProcessId)) {
|
||
|
|
$pending.Enqueue([int]$child.ProcessId)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return @($seen)
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-BrowserCleanupIds {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)]$Handle
|
||
|
|
)
|
||
|
|
|
||
|
|
$ids = [System.Collections.Generic.HashSet[int]]::new()
|
||
|
|
|
||
|
|
if ($Handle.Process) {
|
||
|
|
$null = $ids.Add([int]$Handle.Process.Id)
|
||
|
|
foreach ($childId in Get-ChildProcessIds -ParentId $Handle.Process.Id) {
|
||
|
|
$null = $ids.Add([int]$childId)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($processInfo in Get-BrowserProcessesByProfile -BrowserPath $Handle.BrowserPath -ProfileDir $Handle.ProfileDir) {
|
||
|
|
$null = $ids.Add([int]$processInfo.ProcessId)
|
||
|
|
}
|
||
|
|
|
||
|
|
$liveIds = @()
|
||
|
|
foreach ($processId in $ids) {
|
||
|
|
try {
|
||
|
|
Get-Process -Id $processId -ErrorAction Stop | Out-Null
|
||
|
|
$liveIds += $processId
|
||
|
|
} catch {
|
||
|
|
# Process already exited.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return @($liveIds | Sort-Object -Unique)
|
||
|
|
}
|
||
|
|
|
||
|
|
function Start-BrowserProcess {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true)][string]$BrowserPath,
|
||
|
|
[Parameter(Mandatory = $true)][int]$Port,
|
||
|
|
[Parameter(Mandatory = $true)][string]$ProfileDir
|
||
|
|
)
|
||
|
|
|
||
|
|
$baselineIds = Get-BrowserProcessIds -BrowserPath $BrowserPath
|
||
|
|
$arguments = Get-BrowserArguments -BrowserPath $BrowserPath -Port $Port -ProfileDir $ProfileDir
|
||
|
|
$stdoutPath = Join-Path $ProfileDir 'browser-stdout.log'
|
||
|
|
$stderrPath = Join-Path $ProfileDir 'browser-stderr.log'
|
||
|
|
Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
$process = Start-Process `
|
||
|
|
-FilePath $BrowserPath `
|
||
|
|
-ArgumentList $arguments `
|
||
|
|
-PassThru `
|
||
|
|
-WindowStyle Hidden `
|
||
|
|
-RedirectStandardOutput $stdoutPath `
|
||
|
|
-RedirectStandardError $stderrPath
|
||
|
|
|
||
|
|
return [pscustomobject]@{
|
||
|
|
BrowserPath = $BrowserPath
|
||
|
|
BaselineIds = $baselineIds
|
||
|
|
ProfileDir = $ProfileDir
|
||
|
|
Process = $process
|
||
|
|
StdOut = $stdoutPath
|
||
|
|
StdErr = $stderrPath
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Show-BrowserLogs {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $false)]$Handle
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not $Handle) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($path in @($Handle.StdOut, $Handle.StdErr)) {
|
||
|
|
if (-not [string]::IsNullOrWhiteSpace($path) -and (Test-Path $path)) {
|
||
|
|
Get-Content $path -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Stop-BrowserProcess {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $false)]$Handle
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not $Handle) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($Handle.Process -and -not $Handle.Process.HasExited) {
|
||
|
|
foreach ($cleanupCommand in @(
|
||
|
|
{ param($id) taskkill /PID $id /T /F *> $null },
|
||
|
|
{ param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
|
||
|
|
)) {
|
||
|
|
try {
|
||
|
|
& $cleanupCommand $Handle.Process.Id
|
||
|
|
} catch {
|
||
|
|
# Ignore cleanup errors here; the residual PID check below is authoritative.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$residualIds = @()
|
||
|
|
|
||
|
|
for ($attempt = 0; $attempt -lt 12; $attempt++) {
|
||
|
|
$residualIds = @(Get-BrowserCleanupIds -Handle $Handle)
|
||
|
|
|
||
|
|
foreach ($processId in $residualIds) {
|
||
|
|
foreach ($cleanupCommand in @(
|
||
|
|
{ param($id) taskkill /PID $id /T /F *> $null },
|
||
|
|
{ param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
|
||
|
|
)) {
|
||
|
|
try {
|
||
|
|
& $cleanupCommand $processId
|
||
|
|
} catch {
|
||
|
|
# Ignore per-process cleanup errors during retry loop.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Start-Sleep -Milliseconds 500
|
||
|
|
$residualIds = @(Get-BrowserCleanupIds -Handle $Handle)
|
||
|
|
|
||
|
|
if ($residualIds.Count -eq 0) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($residualIds.Count -gt 0) {
|
||
|
|
throw "browser cleanup leaked PIDs: $($residualIds -join ', ')"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Remove-BrowserLogs {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $false)]$Handle
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not $Handle) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
$paths = @($Handle.StdOut, $Handle.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||
|
|
if ($paths.Count -gt 0) {
|
||
|
|
Remove-Item $paths -Force -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$browserPath = Resolve-BrowserPath
|
||
|
|
Write-Host "CDP browser: $browserPath"
|
||
|
|
$Port = if ($Port -gt 0) { $Port } else { Get-FreeTcpPort }
|
||
|
|
$profileRoot = Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..')).Path '.cache\cdp-profiles'
|
||
|
|
New-Item -ItemType Directory -Force $profileRoot | Out-Null
|
||
|
|
$profileDir = Join-Path $profileRoot "pw-profile-cdp-smoke-win-$Port"
|
||
|
|
$browserReadyUrl = "http://127.0.0.1:$Port/json/version"
|
||
|
|
$browserCdpBaseUrl = "http://127.0.0.1:$Port"
|
||
|
|
$browserHandle = $null
|
||
|
|
|
||
|
|
try {
|
||
|
|
for ($attempt = 1; $attempt -le 2; $attempt++) {
|
||
|
|
Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
|
||
|
|
$browserHandle = Start-BrowserProcess -BrowserPath $browserPath -Port $Port -ProfileDir $profileDir
|
||
|
|
|
||
|
|
try {
|
||
|
|
Wait-UrlReady -Url $browserReadyUrl -Label "browser CDP endpoint (attempt $attempt)"
|
||
|
|
Write-Host "CDP endpoint ready: $browserReadyUrl"
|
||
|
|
break
|
||
|
|
} catch {
|
||
|
|
Show-BrowserLogs $browserHandle
|
||
|
|
Stop-BrowserProcess $browserHandle
|
||
|
|
Remove-BrowserLogs $browserHandle
|
||
|
|
$browserHandle = $null
|
||
|
|
|
||
|
|
if ($attempt -eq 2) {
|
||
|
|
throw
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $env:E2E_COMMAND_TIMEOUT_MS) {
|
||
|
|
$env:E2E_COMMAND_TIMEOUT_MS = '120000'
|
||
|
|
}
|
||
|
|
|
||
|
|
$env:E2E_SKIP_BROWSER_LAUNCH = '1'
|
||
|
|
$env:E2E_CDP_PORT = "$Port"
|
||
|
|
$env:E2E_CDP_BASE_URL = $browserCdpBaseUrl
|
||
|
|
$env:E2E_PLAYWRIGHT_CDP_URL = $browserCdpBaseUrl
|
||
|
|
$env:E2E_EXTERNAL_CDP = '1'
|
||
|
|
|
||
|
|
$commandName = $Command[0]
|
||
|
|
$commandArgs = @()
|
||
|
|
if ($Command.Count -gt 1) {
|
||
|
|
$commandArgs = $Command[1..($Command.Count - 1)]
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host "Launching command: $commandName $($commandArgs -join ' ')"
|
||
|
|
& $commandName @commandArgs
|
||
|
|
if ($LASTEXITCODE -ne 0) {
|
||
|
|
throw "command failed with exit code $LASTEXITCODE"
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
Stop-BrowserProcess $browserHandle
|
||
|
|
Remove-BrowserLogs $browserHandle
|
||
|
|
Remove-Item Env:E2E_SKIP_BROWSER_LAUNCH -ErrorAction SilentlyContinue
|
||
|
|
Remove-Item Env:E2E_CDP_PORT -ErrorAction SilentlyContinue
|
||
|
|
Remove-Item Env:E2E_CDP_BASE_URL -ErrorAction SilentlyContinue
|
||
|
|
Remove-Item Env:E2E_PLAYWRIGHT_CDP_URL -ErrorAction SilentlyContinue
|
||
|
|
Remove-Item Env:E2E_EXTERNAL_CDP -ErrorAction SilentlyContinue
|
||
|
|
Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
|
||
|
|
}
|