ci(install-e2e): one screen-recording mechanism on every runner, Xvfb for headless linux
The composite action .github/actions/e2e-screen-record owns setup and lifecycle on all three OSes: ffmpeg via apt/brew-verify/winget+cache, capture via x11grab/gdigrab/avfoundation, mkv at 15fps stopped by 'q' on live stdin with kill fallback. Linux runners have no display, so start brings up a dedicated Xvfb :99 and exports DISPLAY - one display serves both the recorder and any app a later step launches. Recording moves out of the GUI driver into workflow infrastructure - that is what makes it uniform - and a missing ffmpeg or a zero-frame file now FAILS the leg instead of skipping silently: the graceful-skip path is how the windows leg shipped no recording.mkv while green. Lifecycle proven locally: start against lavfi testsrc, q-stop, ffprobe duration check (record-start.sh/record-stop.sh under nix ffmpeg).
This commit is contained in:
102
.github/actions/e2e-screen-record/action.yml
vendored
Normal file
102
.github/actions/e2e-screen-record/action.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: E2E screen recording
|
||||
description: >
|
||||
One screen-recording mechanism for every install-e2e runner. mode=start
|
||||
installs what the OS needs (ffmpeg everywhere; Xvfb on headless linux),
|
||||
starts the recorder, and exports DISPLAY for later steps. mode=stop
|
||||
finalizes the recording and fails on a zero-frame file. A missing ffmpeg
|
||||
is a hard error - a graceful skip makes the missing tool invisible and
|
||||
the artifact silently loses its recording.
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: start | stop
|
||||
required: true
|
||||
output:
|
||||
description: Path of the recording (mkv).
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# ---- setup + start ------------------------------------------------------
|
||||
- name: Install ffmpeg + Xvfb (linux)
|
||||
if: inputs.mode == 'start' && runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v Xvfb >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq --no-install-recommends ffmpeg xvfb
|
||||
fi
|
||||
|
||||
- name: Start Xvfb (linux, headless)
|
||||
if: inputs.mode == 'start' && runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# A dedicated display rather than xvfb-run-wrapping each command, so
|
||||
# ONE display serves both the app under test and the recorder.
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
Xvfb :99 -screen 0 1920x1080x24 &
|
||||
echo "$!" > "$RUNNER_TEMP/xvfb.pid"
|
||||
echo "DISPLAY=:99" >> "$GITHUB_ENV"
|
||||
export DISPLAY=:99
|
||||
fi
|
||||
# Wait until the display accepts connections; xdpyinfo may not be
|
||||
# installed, so probe with the X socket.
|
||||
for _ in $(seq 1 50); do
|
||||
[ -S "/tmp/.X11-unix/X99" ] && break
|
||||
sleep 0.2
|
||||
done
|
||||
[ -S "/tmp/.X11-unix/X99" ] || { echo "Xvfb :99 did not come up" >&2; exit 1; }
|
||||
|
||||
- name: Verify ffmpeg (macos)
|
||||
if: inputs.mode == 'start' && runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Do not trust "preinstalled" claims - verify, install on miss.
|
||||
command -v ffmpeg >/dev/null 2>&1 || brew install --quiet ffmpeg
|
||||
|
||||
- name: Restore cached ffmpeg (windows)
|
||||
if: inputs.mode == 'start' && runner.os == 'Windows'
|
||||
id: ffmpeg-cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ runner.temp }}\test-bins\ffmpeg
|
||||
key: e2e-ffmpeg-${{ runner.os }}-v1
|
||||
|
||||
- name: Install ffmpeg (windows)
|
||||
if: inputs.mode == 'start' && runner.os == 'Windows' && steps.ffmpeg-cache.outputs.cache-hit != 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bins = "$env:RUNNER_TEMP\test-bins\ffmpeg"
|
||||
New-Item -ItemType Directory -Path $bins -Force | Out-Null
|
||||
winget install -e --id Gyan.FFmpeg --silent --accept-source-agreements --accept-package-agreements --disable-interactivity --location "$env:RUNNER_TEMP\ffmpeg_dir"
|
||||
Copy-Item -Path "$env:RUNNER_TEMP\ffmpeg_dir\*\*" -Destination $bins -Recurse -Force
|
||||
|
||||
- name: Add ffmpeg to PATH (windows)
|
||||
if: inputs.mode == 'start' && runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\test-bins\ffmpeg\bin"
|
||||
|
||||
- name: Start recording (posix)
|
||||
if: inputs.mode == 'start' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: bash "$GITHUB_ACTION_PATH/../../../tests/install/e2e-assets/record-start.sh" '${{ inputs.output }}'
|
||||
|
||||
- name: Start recording (windows)
|
||||
if: inputs.mode == 'start' && runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File "$env:GITHUB_ACTION_PATH\..\..\..\tests\install\e2e-assets\record-start.ps1" -OutFile "${{ inputs.output }}"
|
||||
|
||||
# ---- stop ---------------------------------------------------------------
|
||||
- name: Stop recording (posix)
|
||||
if: inputs.mode == 'stop' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: bash "$GITHUB_ACTION_PATH/../../../tests/install/e2e-assets/record-stop.sh" '${{ inputs.output }}'
|
||||
|
||||
- name: Stop recording (windows)
|
||||
if: inputs.mode == 'stop' && runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File "$env:GITHUB_ACTION_PATH\..\..\..\tests\install\e2e-assets\record-stop.ps1" -OutFile "${{ inputs.output }}"
|
||||
15
.github/workflows/install-e2e-run.yml
vendored
15
.github/workflows/install-e2e-run.yml
vendored
@@ -91,6 +91,14 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# One recording mechanism on every OS (Xvfb gives headless linux a
|
||||
# display; the same display serves any app the driver launches).
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
mode: start
|
||||
output: ${{ runner.temp }}/e2e-logs/recording.mkv
|
||||
|
||||
- name: Run install + update E2E
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -103,6 +111,13 @@ jobs:
|
||||
# would trip the driver's own dirty-tree guard.
|
||||
HERMES_E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs
|
||||
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
mode: stop
|
||||
output: ${{ runner.temp }}/e2e-logs/recording.mkv
|
||||
|
||||
# Artifact names cannot contain '/', and install-ref may be a full ref
|
||||
# like refs/heads/main. GitHub Actions expressions have no string-replace
|
||||
# function, so build the safe name here. Runs even on failure -- that is
|
||||
|
||||
52
.github/workflows/install-e2e-windows-run.yml
vendored
52
.github/workflows/install-e2e-windows-run.yml
vendored
@@ -119,31 +119,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# ffmpeg records the screen for the whole run so a failed click is
|
||||
# diagnosable from the artifact instead of by guesswork. NOT
|
||||
# preinstalled on windows-latest; the driver skips recording
|
||||
# gracefully when it's absent, so this step is what makes the
|
||||
# recording actually happen. Cached — winget's ffmpeg download is
|
||||
# the slow part.
|
||||
- name: Restore cached ffmpeg
|
||||
id: ffmpeg-cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
# One recording mechanism on every OS: the composite action installs
|
||||
# ffmpeg (cached - winget's download is the slow part), starts the
|
||||
# capture, and record-stop fails on a zero-frame file so a silently
|
||||
# missing recording cannot go green.
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
path: ${{ runner.temp }}\test-bins\ffmpeg
|
||||
key: e2e-ffmpeg-${{ runner.os }}-v1
|
||||
|
||||
- name: Install ffmpeg
|
||||
if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bins = "$env:RUNNER_TEMP\test-bins\ffmpeg"
|
||||
New-Item -ItemType Directory -Path $bins -Force | Out-Null
|
||||
winget install -e --id Gyan.FFmpeg --silent --accept-source-agreements --accept-package-agreements --disable-interactivity --location "$env:RUNNER_TEMP\ffmpeg_dir"
|
||||
Copy-Item -Path "$env:RUNNER_TEMP\ffmpeg_dir\*\*" -Destination $bins -Recurse -Force
|
||||
|
||||
- name: Add ffmpeg to PATH
|
||||
shell: pwsh
|
||||
run: Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\test-bins\ffmpeg\bin"
|
||||
mode: start
|
||||
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
|
||||
|
||||
- name: Stage serve repo (main -> ${{ inputs.install-ref }})
|
||||
shell: powershell
|
||||
@@ -157,6 +141,13 @@ jobs:
|
||||
shell: powershell
|
||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-desktop-gui-e2e.ps1 -Phase update-gui -Route "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}" -SetupExeUrl ${{ inputs.setup-exe-url }}
|
||||
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
mode: stop
|
||||
output: ${{ github.workspace }}\gui-e2e-proof\recording.mkv
|
||||
|
||||
- name: Collect proof + logs
|
||||
if: always()
|
||||
shell: powershell
|
||||
@@ -208,12 +199,25 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Start screen recording
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
mode: start
|
||||
output: ${{ runner.temp }}\e2e-logs\recording.mkv
|
||||
|
||||
- name: Run install + update E2E
|
||||
shell: powershell
|
||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File tests\install\windows-installer-script-e2e.ps1 -InstallMethod "${{ inputs.install-method }}" -UpdateMethod "${{ inputs.update-method }}" -InstallRef "${{ inputs.install-ref }}"
|
||||
env:
|
||||
HERMES_E2E_LOG_DIR: ${{ runner.temp }}\e2e-logs
|
||||
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
uses: ./.github/actions/e2e-screen-record
|
||||
with:
|
||||
mode: stop
|
||||
output: ${{ runner.temp }}\e2e-logs\recording.mkv
|
||||
|
||||
- name: Upload installer logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
|
||||
@@ -79,4 +79,4 @@ gh workflow run install-e2e.yml --ref <branch> -f route=both -f tag-count=2
|
||||
|
||||
## Artifacts
|
||||
|
||||
Each leg uploads its logs as an artifact. The windows GUI leg also uploads screenshots, a screen recording, and the update result file. Get them with `gh run download <run-id>`.
|
||||
Each leg uploads its logs as an artifact. Every leg also records the screen for its whole run: the composite action `.github/actions/e2e-screen-record` installs ffmpeg, records with the OS's capture backend (x11grab on linux, gdigrab on windows, avfoundation on macos), and fails the leg if the recording is missing or has zero frames. Linux runners have no display, so the action starts `Xvfb :99` first and exports `DISPLAY` for every later step — the app under test and the recorder share that display. The windows GUI leg also uploads screenshots and the update result file. Get them with `gh run download <run-id>`.
|
||||
|
||||
62
tests/install/e2e-assets/record-start.ps1
Normal file
62
tests/install/e2e-assets/record-start.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
# Start a continuous ffmpeg screen recording in the background (windows).
|
||||
#
|
||||
# Usage: powershell -File record-start.ps1 -OutFile recording.mkv
|
||||
#
|
||||
# The graceful stop is the character 'q' on ffmpeg's LIVE stdin, which only
|
||||
# System.Diagnostics.Process exposes (Start-Process -RedirectStandardInput
|
||||
# hands ffmpeg a file handle already at EOF). This script therefore spawns a
|
||||
# detached HOLDER powershell that owns the ffmpeg process and its stdin pipe,
|
||||
# and stops it when a STOP marker file appears; record-stop.ps1 writes the
|
||||
# marker. mkv on purpose: it stays playable even unfinalized. A missing
|
||||
# ffmpeg is a HARD error - a graceful skip makes the missing tool invisible
|
||||
# and the artifact silently loses its recording.
|
||||
|
||||
#Requires -Version 5.1
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OutFile
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "record-start: ffmpeg not on PATH (the workflow must install it)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$outDir = Split-Path -Parent $OutFile
|
||||
if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
$stopMarker = "$OutFile.stop"
|
||||
$stateFile = "$OutFile.state"
|
||||
Remove-Item -LiteralPath $stopMarker, $stateFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$holder = "$OutFile.holder.ps1"
|
||||
@'
|
||||
param([string]$OutFile, [string]$StopMarker)
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = "ffmpeg"
|
||||
$psi.Arguments = "-y -f gdigrab -framerate 15 -i desktop " +
|
||||
"-hide_banner -loglevel error " +
|
||||
"-c:v libx264 -preset ultrafast -pix_fmt yuv420p `"$OutFile`""
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
Set-Content -LiteralPath "$OutFile.ffpid" -Value $proc.Id
|
||||
while (-not $proc.HasExited) {
|
||||
if (Test-Path -LiteralPath $StopMarker) {
|
||||
try {
|
||||
$proc.StandardInput.Write("q")
|
||||
$proc.StandardInput.Close()
|
||||
} catch {}
|
||||
if (-not $proc.WaitForExit(15000)) { try { $proc.Kill() } catch {} }
|
||||
break
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
'@ | Set-Content -LiteralPath $holder -Encoding UTF8
|
||||
|
||||
$holderProc = Start-Process -FilePath "powershell.exe" `
|
||||
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $holder, "-OutFile", $OutFile, "-StopMarker", $stopMarker `
|
||||
-WindowStyle Hidden -PassThru
|
||||
Set-Content -LiteralPath $stateFile -Value "$($holderProc.Id) $stopMarker"
|
||||
Write-Host "record-start: recording to $OutFile (holder pid $($holderProc.Id))"
|
||||
71
tests/install/e2e-assets/record-start.sh
Executable file
71
tests/install/e2e-assets/record-start.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start a continuous ffmpeg screen recording in the background.
|
||||
#
|
||||
# Usage: record-start.sh OUTPUT.mkv [INPUT_ARGS...]
|
||||
# OUTPUT.mkv where to record (mkv: stays playable even unfinalized)
|
||||
# INPUT_ARGS optional ffmpeg input override; default picks per-OS:
|
||||
# linux -f x11grab -i "$DISPLAY" (Xvfb or real)
|
||||
# macos -f avfoundation -i <first screen index>
|
||||
#
|
||||
# Writes state (pid + control fifo path) next to OUTPUT as OUTPUT.state so
|
||||
# record-stop.sh can finalize gracefully: ffmpeg stops cleanly on the
|
||||
# character 'q' on live stdin, so stdin is a fifo we hold open. A missing
|
||||
# ffmpeg is a HARD error - a graceful skip makes the missing tool invisible
|
||||
# and the artifact silently loses its recording.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${1:?usage: record-start.sh OUTPUT.mkv [input args...]}"
|
||||
shift || true
|
||||
|
||||
command -v ffmpeg >/dev/null 2>&1 || {
|
||||
echo "record-start: ffmpeg not on PATH (the workflow must install it)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
INPUT=("$@")
|
||||
if [ "${#INPUT[@]}" -eq 0 ]; then
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
: "${DISPLAY:?record-start: DISPLAY not set (start Xvfb first on headless runners)}"
|
||||
INPUT=(-f x11grab -framerate 15 -i "$DISPLAY")
|
||||
;;
|
||||
Darwin)
|
||||
# avfoundation lists devices on stderr; the first "Capture screen"
|
||||
# index is the whole display. Parse it rather than hardcoding: the
|
||||
# index shifts with attached cameras.
|
||||
screen_idx="$(ffmpeg -f avfoundation -list_devices true -i "" 2>&1 \
|
||||
| sed -n 's/^\[AVFoundation[^]]*\] \[\([0-9]*\)\] Capture screen.*/\1/p' | head -1)"
|
||||
[ -n "$screen_idx" ] || { echo "record-start: no capture screen device found" >&2; exit 1; }
|
||||
INPUT=(-f avfoundation -framerate 15 -capture_cursor 1 -i "${screen_idx}:none")
|
||||
;;
|
||||
*)
|
||||
echo "record-start: unsupported OS $(uname -s) (windows uses record-start.ps1)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
FIFO="$OUT.ctl"
|
||||
STATE="$OUT.state"
|
||||
rm -f "$FIFO" "$STATE"
|
||||
mkfifo "$FIFO"
|
||||
|
||||
# Hold the fifo's write end open in a shepherd process; ffmpeg reads its
|
||||
# stdin from the fifo. record-stop.sh writes 'q' into the fifo.
|
||||
ffmpeg -hide_banner -loglevel error "${INPUT[@]}" \
|
||||
-pix_fmt yuv420p -c:v libx264 -preset ultrafast "$OUT" < "$FIFO" &
|
||||
FFMPEG_PID=$!
|
||||
# Open a persistent write fd so the fifo doesn't EOF before stop.
|
||||
exec 9> "$FIFO"
|
||||
# Hand the fd to a shepherd that outlives this script.
|
||||
(
|
||||
exec 9>&9
|
||||
while kill -0 "$FFMPEG_PID" 2>/dev/null; do sleep 1; done
|
||||
) &
|
||||
SHEPHERD_PID=$!
|
||||
disown "$SHEPHERD_PID" 2>/dev/null || true
|
||||
|
||||
printf '%s %s %s\n' "$FFMPEG_PID" "$FIFO" "$SHEPHERD_PID" > "$STATE"
|
||||
echo "record-start: recording to $OUT (ffmpeg pid $FFMPEG_PID)"
|
||||
53
tests/install/e2e-assets/record-stop.ps1
Normal file
53
tests/install/e2e-assets/record-stop.ps1
Normal file
@@ -0,0 +1,53 @@
|
||||
# Stop a recording started by record-start.ps1 and verify the file is real.
|
||||
#
|
||||
# Usage: powershell -File record-stop.ps1 -OutFile recording.mkv
|
||||
#
|
||||
# Drops the STOP marker the holder watches for (it writes 'q' to ffmpeg's
|
||||
# live stdin), waits for the holder to exit, then asserts the output exists
|
||||
# and has a decodable duration - a zero-frame recording is the classic
|
||||
# silent failure.
|
||||
|
||||
#Requires -Version 5.1
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OutFile
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$stateFile = "$OutFile.state"
|
||||
if (-not (Test-Path -LiteralPath $stateFile)) {
|
||||
Write-Host "record-stop: no state at $stateFile (was record-start run?)"
|
||||
exit 1
|
||||
}
|
||||
$state = (Get-Content -LiteralPath $stateFile -Raw).Trim() -split " ", 2
|
||||
$holderPid = [int]$state[0]
|
||||
$stopMarker = $state[1]
|
||||
|
||||
Set-Content -LiteralPath $stopMarker -Value "stop"
|
||||
try {
|
||||
$holder = Get-Process -Id $holderPid -ErrorAction SilentlyContinue
|
||||
if ($holder) { $holder.WaitForExit(20000) | Out-Null }
|
||||
} catch {}
|
||||
# Belt and braces: if ffmpeg outlived the holder, kill it directly.
|
||||
$ffpidFile = "$OutFile.ffpid"
|
||||
if (Test-Path -LiteralPath $ffpidFile) {
|
||||
$ffpid = [int](Get-Content -LiteralPath $ffpidFile -Raw).Trim()
|
||||
try { Stop-Process -Id $ffpid -Force -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
Remove-Item -LiteralPath $stateFile, $stopMarker, $ffpidFile, "$OutFile.holder.ps1" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not (Test-Path -LiteralPath $OutFile) -or (Get-Item -LiteralPath $OutFile).Length -eq 0) {
|
||||
Write-Host "record-stop: $OutFile missing or empty"
|
||||
exit 1
|
||||
}
|
||||
if (Get-Command ffprobe -ErrorAction SilentlyContinue) {
|
||||
$prevEap = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||
$dur = (& ffprobe -v error -show_entries format=duration -of csv=p=0 $OutFile 2>&1 | Out-String).Trim()
|
||||
$ErrorActionPreference = $prevEap
|
||||
if (-not $dur -or $dur -eq "0" -or $dur -like "0.0*") {
|
||||
Write-Host "record-stop: $OutFile has no duration (zero-frame recording)"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "record-stop: $OutFile finalized (${dur}s)"
|
||||
} else {
|
||||
Write-Host "record-stop: $OutFile finalized (ffprobe absent; size $((Get-Item -LiteralPath $OutFile).Length) bytes)"
|
||||
}
|
||||
49
tests/install/e2e-assets/record-stop.sh
Executable file
49
tests/install/e2e-assets/record-stop.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop a recording started by record-start.sh and verify the file is real.
|
||||
#
|
||||
# Usage: record-stop.sh OUTPUT.mkv
|
||||
#
|
||||
# Graceful stop: the character 'q' on ffmpeg's live stdin (the control
|
||||
# fifo). Falls back to SIGINT (also a clean finalize for ffmpeg), then
|
||||
# SIGKILL. Fails if the output is missing or has no decodable duration -
|
||||
# a zero-frame recording is the classic silent failure.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${1:?usage: record-stop.sh OUTPUT.mkv}"
|
||||
STATE="$OUT.state"
|
||||
|
||||
[ -f "$STATE" ] || { echo "record-stop: no state at $STATE (was record-start run?)" >&2; exit 1; }
|
||||
read -r FFMPEG_PID FIFO _SHEPHERD < "$STATE"
|
||||
|
||||
if kill -0 "$FFMPEG_PID" 2>/dev/null; then
|
||||
# Write the quit key; don't hang if the reader is already gone.
|
||||
{ printf 'q' > "$FIFO"; } 2>/dev/null &
|
||||
WRITER=$!
|
||||
for _ in $(seq 1 50); do
|
||||
kill -0 "$FFMPEG_PID" 2>/dev/null || break
|
||||
sleep 0.2
|
||||
done
|
||||
kill "$WRITER" 2>/dev/null || true
|
||||
if kill -0 "$FFMPEG_PID" 2>/dev/null; then
|
||||
echo "record-stop: q did not stop ffmpeg; SIGINT" >&2
|
||||
kill -INT "$FFMPEG_PID" 2>/dev/null || true
|
||||
for _ in $(seq 1 25); do
|
||||
kill -0 "$FFMPEG_PID" 2>/dev/null || break
|
||||
sleep 0.2
|
||||
done
|
||||
kill -9 "$FFMPEG_PID" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -f "$FIFO" "$STATE"
|
||||
|
||||
[ -s "$OUT" ] || { echo "record-stop: $OUT missing or empty" >&2; exit 1; }
|
||||
if command -v ffprobe >/dev/null 2>&1; then
|
||||
dur="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUT" || echo 0)"
|
||||
case "$dur" in
|
||||
''|0|0.*) echo "record-stop: $OUT has no duration (zero-frame recording)" >&2; exit 1 ;;
|
||||
esac
|
||||
echo "record-stop: $OUT finalized (${dur}s)"
|
||||
else
|
||||
echo "record-stop: $OUT finalized (ffprobe absent; size $(wc -c < "$OUT") bytes)"
|
||||
fi
|
||||
@@ -225,41 +225,6 @@ function Save-DesktopScreenshot([string]$OutFile) {
|
||||
}
|
||||
}
|
||||
|
||||
function Start-ScreenRecording([string]$OutFile) {
|
||||
# Continuous ffmpeg screen capture (gdigrab, 15fps). ffmpeg ships on the
|
||||
# windows-latest runner image; skip gracefully elsewhere. mkv on purpose:
|
||||
# it stays playable even if the process dies without finalizing.
|
||||
#
|
||||
# ffmpeg must be started, fed, and stopped from THIS process: the
|
||||
# graceful stop is the character 'q' on its LIVE stdin pipe, which only
|
||||
# System.Diagnostics.Process exposes (Start-Process
|
||||
# -RedirectStandardInput hands it a file handle already at EOF).
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
||||
Write-Host " (ffmpeg not on PATH; skipping screen recording)"
|
||||
return $null
|
||||
}
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = "ffmpeg"
|
||||
$psi.Arguments = "-y -f gdigrab -framerate 15 -i desktop " +
|
||||
"-hide_banner -loglevel error " +
|
||||
"-c:v libx264 -preset ultrafast -pix_fmt yuv420p `"$OutFile`""
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
Write-Host " screen recording started (pid $($proc.Id)) -> $OutFile"
|
||||
return $proc
|
||||
}
|
||||
|
||||
function Stop-ScreenRecording($proc) {
|
||||
if ($proc -and -not $proc.HasExited) {
|
||||
try {
|
||||
$proc.StandardInput.Write("q")
|
||||
$proc.StandardInput.Close()
|
||||
} catch {}
|
||||
if (-not $proc.WaitForExit(15000)) { try { $proc.Kill() } catch {} }
|
||||
}
|
||||
}
|
||||
|
||||
function Start-DesktopRecorder([string]$OutDir) {
|
||||
# Rolling desktop capture: one PNG every 3s from a detached PowerShell,
|
||||
# capped at 800 frames (~40 min). Proof that survives any step failure.
|
||||
@@ -419,7 +384,6 @@ function Invoke-PhaseInstallGui {
|
||||
New-Item -ItemType Directory -Path $HermesHome -Force | Out-Null
|
||||
|
||||
$recorder = Start-DesktopRecorder (Join-Path $proof "desktop-frames")
|
||||
$recording = Start-ScreenRecording (Join-Path $proof "recording.mkv")
|
||||
$ahkLog = Join-Path $proof "ahk.log"
|
||||
try {
|
||||
Save-DesktopScreenshot (Join-Path $proof "00-before-installer.png")
|
||||
@@ -458,7 +422,6 @@ function Invoke-PhaseInstallGui {
|
||||
Assert-True $installer.HasExited "Hermes-Setup.exe exited after Launch"
|
||||
}
|
||||
finally {
|
||||
Stop-ScreenRecording $recording
|
||||
Stop-DesktopRecorder $recorder (Join-Path $proof "desktop-frames")
|
||||
# Surface the installer's own log win or lose, full and folded.
|
||||
$bootLog = Join-Path $HermesHome "logs\bootstrap-installer.log"
|
||||
@@ -549,7 +512,6 @@ function Invoke-GuiUpdateDesktopRoute([string]$TargetSha) {
|
||||
Assert-True ($npmExit -eq 0) "npm install @playwright/test@$PlaywrightVersion into the driver dir"
|
||||
|
||||
$recorder = Start-DesktopRecorder (Join-Path $proof "desktop-frames")
|
||||
$recording = Start-ScreenRecording (Join-Path $proof "recording.mkv")
|
||||
try {
|
||||
# Launch the installed app and click through Settings -> About ->
|
||||
# Update now. Exit 0 = the app quit for the updater hand-off.
|
||||
@@ -658,7 +620,6 @@ function Invoke-GuiUpdateDesktopRoute([string]$TargetSha) {
|
||||
Save-DesktopScreenshot (Join-Path $proof "99-relaunched-desktop.png")
|
||||
}
|
||||
finally {
|
||||
Stop-ScreenRecording $recording
|
||||
Stop-DesktopRecorder $recorder (Join-Path $proof "desktop-frames")
|
||||
$handoffLog = Join-Path $HermesHome "logs\desktop-update-handoff.log"
|
||||
if (Test-Path -LiteralPath $handoffLog) {
|
||||
|
||||
Reference in New Issue
Block a user