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
96 changes: 66 additions & 30 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,20 @@ jobs:
"PKG_CONFIG=$pkgConfig" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"PKG_CONFIG_PATH=$pkgConfigPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"LIB=C:\Program Files\gstreamer\1.0\msvc_x86_64\lib;$env:LIB" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Prepare Windows update trust root
env:
BF_CLIENT_FIRMWARE_SIGNING_KEY: ${{ secrets.BF_CLIENT_FIRMWARE_SIGNING_KEY }}
run: |
if (-not $env:BF_CLIENT_FIRMWARE_SIGNING_KEY) { throw "Windows release signing key is required" }
$key = Join-Path $env:RUNNER_TEMP 'windows-update.key'
$pub = Join-Path $env:RUNNER_TEMP 'windows-update.pub.pem'
[IO.File]::WriteAllText($key, $env:BF_CLIENT_FIRMWARE_SIGNING_KEY)
openssl pkey -in $key -pubout -out $pub
if ($LASTEXITCODE -ne 0) { throw "Cannot derive Windows update trust root" }
"BF_FIRMWARE_SIGNING_PUBLIC_KEY<<BF_KEY" >> $env:GITHUB_ENV
Get-Content $pub >> $env:GITHUB_ENV
"BF_KEY" >> $env:GITHUB_ENV
"BF_WINDOWS_SIGNING_KEY=$key" >> $env:GITHUB_ENV
- name: Build and test Windows client
working-directory: client
run: cargo test --release --locked
Expand All @@ -395,41 +409,34 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
cargo install cargo-wix --version 0.3.9 --locked
$archive = Join-Path $env:RUNNER_TEMP "gstreamer-1.0-msvc-x86_64-$env:GSTREAMER_VERSION-merge-modules.zip"
gh release download $env:GSTREAMER_RELEASE --repo $env:GITHUB_REPOSITORY --dir $env:RUNNER_TEMP --pattern (Split-Path $archive -Leaf)
$actualHash = (Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne $env:GSTREAMER_MSM_SHA256) {
throw "GStreamer merge-module checksum mismatch: $actualHash"
}
$extractDir = Join-Path $env:RUNNER_TEMP "gstreamer-msm"
Expand-Archive -LiteralPath $archive -DestinationPath $extractDir
$sourceDir = Get-ChildItem $extractDir -Directory | Select-Object -First 1
$moduleDir = New-Item -ItemType Directory -Force -Path "target\gstreamer-msm"
@(
"base-system-1.0.msm",
"base-crypto.msm",
"gstreamer-1.0-core.msm",
"gstreamer-1.0-net.msm",
"gstreamer-1.0-playback.msm",
"gstreamer-1.0-codecs.msm",
"gstreamer-1.0-system.msm",
"gstreamer-1.0-libav.msm"
) | ForEach-Object { Copy-Item (Join-Path $sourceDir $_) $moduleDir }
$msiVersion = "0.1.${{ github.run_number }}"
cargo wix --package betterframe-client --nocapture --install-version $msiVersion -L -sice:ICE30 -L -sice:ICE80
../scripts/build-windows-msi.ps1 -InstallVersion "0.1.${{ github.run_number }}"
$msi = Get-ChildItem target\wix\*.msi | Select-Object -First 1
Copy-Item $msi.FullName "betterframe-windows-client-${{ inputs.version }}-x86_64.msi"
$artifact = "betterframe-windows-client-${{ inputs.version }}-x86_64.msi"
$sha = (Get-FileHash $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText("$artifact.sha256", $sha)
openssl pkeyutl -sign -rawin -in "$artifact.sha256" -inkey $env:BF_WINDOWS_SIGNING_KEY -out "$artifact.sig.raw"
if ($LASTEXITCODE -ne 0) { throw "Windows MSI signing failed" }
$signature = [Convert]::ToBase64String([IO.File]::ReadAllBytes("$artifact.sig.raw")).TrimEnd('=').Replace('+','-').Replace('/','_')
[IO.File]::WriteAllText("$artifact.sig", $signature)
Remove-Item $env:BF_WINDOWS_SIGNING_KEY, "$artifact.sig.raw"

- uses: actions/upload-artifact@v7
with:
name: betterframe-windows-client-x86_64
path: client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi
path: |
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi.sig
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi.sha256
retention-days: 14

windows-client-package-test:
needs: windows-client
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- uses: actions/download-artifact@v8
with:
name: betterframe-windows-client-x86_64
Expand All @@ -440,16 +447,45 @@ jobs:
if (Test-Path "$env:ProgramFiles\gstreamer") {
throw "clean-host test runner unexpectedly contains a global GStreamer installation"
}
$msi = Resolve-Path "betterframe-windows-client-${{ inputs.version }}-x86_64.msi"
$installer = Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /qn /norestart" -Wait -PassThru
if ($installer.ExitCode -notin 0, 3010) { throw "client install failed with exit code $($installer.ExitCode)" }
& "$env:ProgramFiles\betterframe-windows-client\bin\betterframe-windows-client.exe" self-test
if ($LASTEXITCODE -ne 0) { throw "installed client self-test failed with exit code $LASTEXITCODE" }
../scripts/test-windows-msi.ps1 -MsiPath "betterframe-windows-client-${{ inputs.version }}-x86_64.msi"
windows-client-publish:
needs: windows-client-package-test
runs-on: ubuntu-24.04
env:
BF_AUTOIMPORT_URL: ${{ secrets.BF_AUTOIMPORT_URL }}
BF_AUTOIMPORT_API_KEY: ${{ secrets.BF_AUTOIMPORT_API_KEY }}
steps:
- uses: actions/download-artifact@v8
with:
name: betterframe-windows-client-x86_64
path: client
- name: Publish signed Windows MSI to BF storage
working-directory: client
run: |
set -euo pipefail
test -n "$BF_AUTOIMPORT_URL" && test -n "$BF_AUTOIMPORT_API_KEY" || { echo "BF artifact publication is required"; exit 1; }
bin="betterframe-windows-client-${{ inputs.version }}-x86_64.msi"
base64 -w 0 "$bin" > "$bin.b64"
jq -nc --arg v "${{ inputs.version }}" --arg c "${{ inputs.channel }}" \
--rawfile b "$bin.b64" --rawfile s "$bin.sig" \
'{version:$v,channel:$c,target:"windows-x64",content_b64:$b,signature:$s}' > "$bin.import.json"
status=$(curl --fail-with-body --retry 3 --retry-all-errors --retry-delay 10 \
--connect-timeout 15 --max-time 600 \
-H "Authorization: Bearer $BF_AUTOIMPORT_API_KEY" -H 'Content-Type: application/json' \
--output "$bin.import-response.json" --write-out '%{http_code}' \
--data-binary @"$bin.import.json" "$BF_AUTOIMPORT_URL/api/admin/firmware/import")
[[ "$status" =~ ^2[0-9][0-9]$ ]] || { echo "BF publication did not return success: $status"; exit 1; }
jq -e --rawfile hash "$bin.sha256" \
'.ok == true and (.release_id | type == "string" and length > 0) and .sha256 == $hash' \
"$bin.import-response.json" > /dev/null
- name: Upload Windows installer to GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
files: client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi
files: |
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi.sig
client/betterframe-windows-client-${{ inputs.version }}-x86_64.msi.sha256

# Source/dependency SBOM is attached to every release for licensing and
# vulnerability inventory. Binary/image jobs remain traceable to this ref.
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ jobs:
- name: Build and test Windows client
working-directory: client
run: cargo test --workspace --locked
- name: Build and verify MSI startup lifecycle
working-directory: client
env:
GH_TOKEN: ${{ github.token }}
run: |
../scripts/build-windows-msi.ps1 -InstallVersion "0.1.${{ github.run_number }}"
$msi = Get-ChildItem target/wix/*.msi | Select-Object -First 1
../scripts/test-windows-msi.ps1 -MsiPath $msi.FullName
windows-update-recovery:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Recover Windows updates through BF after authentication and app failures
working-directory: client
run: ../scripts/test-windows-update-recovery.ps1


iobox:
runs-on: ubuntu-24.04
Expand Down
65 changes: 56 additions & 9 deletions client/Cargo.lock

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

3 changes: 2 additions & 1 deletion client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["core", "android-bridge"]
members = ["core", "android-bridge", "windows-updater"]
resolver = "2"

[package]
Expand Down Expand Up @@ -59,6 +59,7 @@ windows-sys = { version = "0.52", features = [
"Win32_Security_Cryptography",
"Win32_Storage_FileSystem",
"Win32_System_LibraryLoader",
"Win32_System_Console",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Input_KeyboardAndMouse",
Expand Down
33 changes: 33 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,36 @@ path. Camera and webpage navigation retain their own existing behavior.

BetterFrame uses `cloud.betterportal.frame` as its canonical application ID for
Android/Android TV and Linux GTK. See [application identity and domain conventions](../docs/application-identity.md).

### Windows installation and startup

Install the Windows MSI, then open **BetterFrame** from the Start menu to begin
pairing. The display shows the pairing code; no terminal or separate `install`
command is needed. Double-clicking the installed executable also starts the app.

The MSI enables BetterFrame at Windows sign-in through the machine-wide
`HKLM\Software\Microsoft\Windows\CurrentVersion\Run\BetterFrame` entry.
The agent and display run without a console window, in the signed-in user's
session. Repeated launches in that session reuse the running agent. MSI repair
restores the startup entry, upgrades update its executable path, and uninstall
removes it. Windows Settings / Task Manager **Startup apps** can disable startup.

Use one dedicated Windows account for the kiosk: enrollment is machine-wide,
with protected state restricted to the account that created it, administrators,
and SYSTEM. Sign in with that same account after reboot. The installer does not
configure automatic Windows sign-in, and the display cannot run before sign-in.
A Windows service runs in a noninteractive session and cannot display this UI.

The older CLI `install` / `uninstall` commands manage a separate, optional
scheduled task; they are not needed for MSI installations. If you previously
created that task, run the CLI `uninstall` command once as administrator to remove
it (this does not uninstall the MSI). Normal application removal uses Windows
**Installed apps**. Explicit CLI commands such as `agent` and `self-test` still
attach to an existing terminal; for scripts use PowerShell `Start-Process -Wait
-PassThru` to wait and inspect the exit code of the GUI executable.

The MSI also installs the independent **BetterFrameUpdater** service for automatic
app updates and rollback through BF-hosted, vendor-signed MSI packages. Updates
respect saved maintenance windows and can recover without working enrollment.
See [Windows updates and recovery](../docs/windows-updates.md) for first-deployment
requirements, retained installers, and recovery behavior.
2 changes: 2 additions & 0 deletions client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]

pub use betterframe_client_core as core;
pub use core::bundle;
mod network;
Expand Down
Loading
Loading