Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 93 additions & 10 deletions .github/installers/cell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ if (-not $gatewayVersion) { Fail "$pin names no Gateway version" }
$root = Join-Path $env:LOCALAPPDATA 'Runvil'
$profileDir = Join-Path $env:USERPROFILE '.runvil'
$workshopExe = Join-Path $root 'runvil-workshop.exe'
# The std Plugin's library installs with Workshop, in its Plugin directory.
$stdPlugin = Join-Path $root 'plugins\plugin_std.dll'
$gatewayExe = Join-Path $root 'runvil-gateway.exe'
$config = Join-Path $profileDir 'gateway.toml'
$discovery = Join-Path $profileDir 'run\gateway.json'
Expand Down Expand Up @@ -80,13 +82,41 @@ function Invoke-Setup([string[]]$Arguments, [switch]$DeadProxy) {
}
}

function Get-Started {
@(Get-Process -Name 'runvil-workshop', 'runvil-gateway' -ErrorAction SilentlyContinue)
}

# Stops Workshop and every Gateway, and returns only when a fresh listing
# finds neither. Windows keeps a stopped process in the list, with its
# files still open, while it tears the process down, and `Wait-Process`
# returns before that ends. A listing of the model files taken in that
# time is stale. Workshop goes first, because it supervises its Gateway
# and starts another when it finds none.
function Stop-Started {
$running = @(Get-Process -Name 'runvil-workshop', 'runvil-gateway' -ErrorAction SilentlyContinue)
foreach ($process in $running) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
foreach ($process in $running) {
Wait-Process -Id $process.Id -Timeout 30 -ErrorAction SilentlyContinue
$clock = [Diagnostics.Stopwatch]::StartNew()
while ($true) {
foreach ($name in 'runvil-workshop', 'runvil-gateway') {
$running = @(Get-Process -Name $name -ErrorAction SilentlyContinue)
foreach ($process in $running) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
foreach ($process in $running) {
Wait-Process -Id $process.Id -Timeout 30 -ErrorAction SilentlyContinue
}
}
$left = @(Get-Started)
if ($left.Count -eq 0) {
# Worth a line when it was slow: the next failure may be this.
if ($clock.Elapsed.TotalSeconds -ge 1) {
Write-Host ('cell: Workshop and the Gateway took {0:N1} seconds to stop' -f $clock.Elapsed.TotalSeconds)
}
return
}
if ($clock.Elapsed.TotalSeconds -ge 120) {
$names = ($left | ForEach-Object { "$($_.ProcessName) (pid $($_.Id))" }) -join ', '
Fail "Workshop and the Gateway must stop, but after 120 seconds these still run: $names"
}
Start-Sleep -Milliseconds 100
}
}

Expand All @@ -100,6 +130,42 @@ function Get-ModelFiles {
@(Get-ChildItem -LiteralPath (Join-Path $profileDir 'models') -Recurse -File -ErrorAction SilentlyContinue)
}

# A file's length as the file reports it, or -1 when it cannot be opened.
# A directory listing reports the length from the last time the file was
# closed, which is 0 for a download that is still being written.
function Get-Length([string]$Path) {
try {
$stream = [IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite, Delete')
} catch {
return -1
}
try {
return $stream.Length
} finally {
$stream.Dispose()
}
}

function Get-ModelState {
(@(Get-ModelFiles) | Sort-Object FullName | ForEach-Object { "$($_.FullName):$(Get-Length $_.FullName)" }) -join ','
}

# The model files and their lengths once nothing writes to them: two
# listings half a second apart that agree. The Gateway's first start
# begins the Speech to Text download, so a listing taken while a transfer
# still runs would differ from a later one through no act of the setup.
function Wait-ModelState {
$last = Get-ModelState
foreach ($attempt in 1..240) {
Start-Sleep -Milliseconds 500
$now = Get-ModelState
if ($now -eq $last -and $now -notmatch ':-1(,|$)') { return $now }
$last = $now
}
$running = (Get-Started | ForEach-Object { "$($_.ProcessName) (pid $($_.Id))" }) -join ', '
Fail "the model files must stop changing within 120 seconds, but they still change: [$last]; running: [$running]"
}

# The Speech to Text gate the setup writes for the Gateway's first start:
# $null when absent (speech on), else the DWORD (0 = speech off).
function Get-InstallStt {
Expand Down Expand Up @@ -169,17 +235,19 @@ function Get-InstallState {
$keys = @(Get-Item -Path 'HKCU:\Software\*\Runvil\Components' -ErrorAction SilentlyContinue)
if ($keys.Count -ne 1) { Fail "exactly one HKCU:\Software\*\Runvil\Components key must hold the persisted selection; found $($keys.Count)" }
$components = ($keys[0].GetValueNames() | Sort-Object | ForEach-Object { "$_=$($keys[0].GetValue($_))" }) -join ','
$models = (@(Get-ModelFiles) | Sort-Object FullName | ForEach-Object { "$($_.FullName):$($_.Length)" }) -join ','
[pscustomobject]@{
Config = [Convert]::ToBase64String([IO.File]::ReadAllBytes($config))
Models = $models
Models = Wait-ModelState
Components = $components
}
}

function Assert-SameState($Before, $After, [string]$What) {
if ($After.Config -ne $Before.Config) { Fail "$What changed $config" }
if ($After.Models -ne $Before.Models) { Fail "$What changed the model files from [$($Before.Models)] to [$($After.Models)]" }
if ($After.Models -ne $Before.Models) {
$running = (Get-Started | ForEach-Object { "$($_.ProcessName) (pid $($_.Id))" }) -join ', '
Fail "$What changed the model files from [$($Before.Models)] to [$($After.Models)]; running: [$running]"
}
if ($After.Components -ne $Before.Components) { Fail "$What changed the persisted selection from [$($Before.Components)] to [$($After.Components)]" }
}

Expand All @@ -192,6 +260,7 @@ function Test-Selection($Want) {
# nothing into the profile, and the Speech to Text gate records the
# choice for the Gateway's first start.
Test-Presence 'Workshop' $Want.Workshop $workshopExe
Test-Presence "Workshop's std Plugin" $Want.Workshop $stdPlugin
Test-Presence 'Gateway' $Want.Gateway $gatewayExe
if (Test-Path -LiteralPath $config) { Fail "the install must leave the Gateway config to the Gateway's first start, but $config exists" }
$downloaded = @(Get-ModelFiles)
Expand All @@ -209,6 +278,19 @@ function Test-Selection($Want) {
if ($output -ne $check.Expected) { Fail "$($check.Exe) --version printed '$output', required '$($check.Expected)'" }
}

# Workshop loads the std Plugin it installed, which holds the file tools.
# The pipe matters: Workshop is a windowed program, and PowerShell waits
# for one, and reads its output, only when that output is piped.
if ($Want.Workshop) {
$stderr = Join-Path $logs 'plugins.stderr.log'
$listed = (& $workshopExe --plugins 2>$stderr | Out-String).Trim()
if ($listed -notmatch '(?m)^std \d') {
$reason = if (Test-Path -LiteralPath $stderr) { (Get-Content -LiteralPath $stderr -Raw) } else { '' }
Fail "$workshopExe --plugins must list the std Plugin; it printed '$listed' and exited $LASTEXITCODE. stderr: $reason"
}
Write-Host "cell: Workshop loads its std Plugin ($listed)"
}

# 4. Workshop serves its page, attached to the installed Gateway when
# there is one and to the fixture endpoint otherwise.
$origin = $null
Expand Down Expand Up @@ -350,7 +432,8 @@ try {
}
Write-Host "cell: $Selection passed"
} finally {
Stop-Started
# A failure to stop here must not hide the failure that came first.
try { Stop-Started } catch { Write-Host $_.Exception.Message }
$profileLogs = Join-Path $profileDir 'logs'
if (Test-Path -LiteralPath $profileLogs) {
Copy-Item -LiteralPath $profileLogs -Destination (Join-Path $logs 'runvil-logs') -Recurse -Force -ErrorAction SilentlyContinue
Expand Down
32 changes: 31 additions & 1 deletion .github/installers/cell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,19 @@ case "$system" in
root=${CELL_ROOT:-/Applications/Runvil}
workshop_item="$root/Runvil.app"
workshop_exe="$workshop_item/Contents/MacOS/runvil-workshop"
# The std Plugin's library rides inside Workshop's bundle, so the
# updater, which replaces the bundle, replaces it too.
std_plugin="$workshop_item/Contents/Resources/plugins/libplugin_std.dylib"
gateway_item="$root/Runvil Gateway.app"
gateway_exe="$gateway_item/Contents/MacOS/runvil-gateway"
;;
Linux)
root=${CELL_ROOT:-$HOME/Runvil}
workshop_item="$root/Runvil.AppImage"
workshop_exe=$workshop_item
# The std Plugin's library rides inside the AppImage; step 6 looks
# for it there.
std_plugin=""
gateway_item="$root/runvil-gateway"
gateway_exe=$gateway_item
;;
Expand Down Expand Up @@ -174,6 +180,10 @@ check_presence() {
fi
}
check_presence Workshop "$want_workshop" "$workshop_item"
# The std Plugin's library installs with Workshop, inside its bundle, and
# nothing installs a Plugin directory beside it.
[ -z "$std_plugin" ] || check_presence "Workshop's std Plugin" "$want_workshop" "$std_plugin"
check_presence "a Plugin directory beside Workshop" 0 "$root/plugins"
check_presence Gateway "$want_gateway" "$gateway_item"
if [ "$system" = Linux ]; then
check_presence "Workshop's desktop entry" "$want_workshop" "$applications/com.runvil.workshop.desktop"
Expand Down Expand Up @@ -206,6 +216,21 @@ check_version() {
[ "$want_workshop" = 0 ] || check_version "$workshop_exe" "runvil-workshop $version"
[ "$want_gateway" = 0 ] || check_version "$gateway_exe" "runvil-gateway $gateway_version"

# Workshop loads the std Plugin it ships with, which holds the file tools.
check_plugins() {
local output
if ! output=$("$workshop_exe" --plugins 2>"$logs/plugins.stderr.log"); then
tail -n 20 "$logs/plugins.stderr.log" >&2
fail "$workshop_exe --plugins failed; full output in $logs/plugins.stderr.log"
fi
if ! echo "$output" | grep -Eq '^std [0-9]'; then
tail -n 20 "$logs/plugins.stderr.log" >&2
fail "$workshop_exe --plugins must list the std Plugin; it printed '$output'"
fi
echo "cell: Workshop loads its std Plugin ($output)"
}
[ "$want_workshop" = 0 ] || check_plugins

# 4. Workshop serves its page, attached to the installed Gateway when there
# is one and to the fixture endpoint otherwise.
gateway_origin=""
Expand Down Expand Up @@ -336,8 +361,13 @@ if [ "$system" = Linux ] && [ "$want_workshop" = 1 ]; then
done
grep -rqs GST_PLUGIN_SYSTEM_PATH_1_0 "$extracted/squashfs-root/apprun-hooks" ||
fail "no AppImage AppRun hook sets GST_PLUGIN_SYSTEM_PATH_1_0"
# Workshop looks for its Plugin directory from usr/bin, where it runs.
[ -f "$extracted/squashfs-root/usr/bin/runvil-workshop" ] ||
fail "the AppImage lacks usr/bin/runvil-workshop, which the Plugin directory lookup starts from"
[ -f "$extracted/squashfs-root/usr/lib/runvil/plugins/libplugin_std.so" ] ||
fail "the AppImage lacks usr/lib/runvil/plugins/libplugin_std.so"
rm -rf "$extracted"
echo "cell: the AppImage carries its GStreamer audio plugins"
echo "cell: the AppImage carries its GStreamer audio plugins and the std Plugin"
fi

echo "cell: $selection passed"
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ jobs:
- name: Cache cargo
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

# A mirror that does not answer makes apt-get wait for a long time, so
# the step fails after 20 minutes. It normally takes under one.
- name: Install Tauri system packages
timeout-minutes: 20
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libssl-dev librsvg2-dev
Expand Down Expand Up @@ -338,7 +341,10 @@ jobs:
- name: Cache cargo
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

# A mirror that does not answer makes apt-get wait for a long time, so
# the step fails after 20 minutes. It normally takes under one.
- name: Install Tauri system packages
timeout-minutes: 20
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libssl-dev librsvg2-dev
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/installers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ on:
- crates/workshop/desktop/*.plist
- crates/workshop/desktop/tauri*.conf.json
- crates/workshop/desktop/icons/**
- crates/workshop/plugin-std/**
- crates/workshop/package.json
- crates/workshop/package-lock.json
- crates/workshop/gateway-discovery/src/**
Expand Down Expand Up @@ -259,7 +260,10 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: node tools/fetch-gateway-sidecar.mjs --target ${{ matrix.triple }}

# A mirror that does not answer makes apt-get wait for a long time, so
# the step fails after 20 minutes. It normally takes under one.
- name: Install the Linux build packages
timeout-minutes: 20
if: startsWith(matrix.platform, 'linux')
run: |
sudo apt-get update
Expand Down Expand Up @@ -405,7 +409,10 @@ jobs:
# (https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist),
# and Workshop opens GLES at run time; every GTK desktop has all five.
# The runner images lack EGL, and a bare Ubuntu image the others.
# A mirror that does not answer makes apt-get wait for a long time, so
# the step fails after 20 minutes. It normally takes under one.
- name: Install the Linux test packages
timeout-minutes: 20
if: runner.os == 'Linux'
run: |
set -euo pipefail
Expand Down
30 changes: 25 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading