Files
hello-agent/.gitea/workflows/build-windows.yml
T
mike f8ead215d8
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
Initial commit: hello-agent — headless RustDesk-protocol-compatible Windows agent
A single-binary, Flutter-free remote-support agent that speaks the stock
RustDesk wire protocol. Designed for one-line MDM deployment against a
self-hosted rustdesk-server: a supporter using the unmodified rustdesk.exe
client connects, the controlled-side user gets a native Win32 approval
prompt, click Yes / No.

CLI surface

    hello-agent.exe --install                # register + start service
    hello-agent.exe --uninstall              # stop, delete, clean up
    hello-agent.exe --config <BLOB>          # admin-UI deploy string
    hello-agent.exe --install --config <BLOB>   # MDM one-liner

--config accepts both forms emitted by the rustdesk-server admin UI: the
reversed-base64 deploy string and the host=,key=,api=,relay= filename
form. Decoded via the upstream custom_server module, persisted via
hbb_common::config::Config::set_option.

Architecture

    --service runs as a Session 0 LocalSystem service. It polls
    WTSGetActiveConsoleSessionId and (re)spawns hello-agent.exe --server
    into the active console session via librustdesk::platform::run_as_user,
    handling the Session 0 → user-session token impersonation.

    --server is the worker. It boots three concurrent components:
      1. cm_popup: an IPC listener on the rustdesk `_cm` named pipe
      2. librustdesk::start_server(true, false): the upstream protocol
         stack — rendezvous mediator, NAT punch, IPC server, screen
         capture, login validation, hbbs_http heartbeat / sysinfo sync
      3. (implicit) ApproveMode::Click is pinned in config, so every
         incoming connection routes through cm_popup

The popup mechanism reuses an existing upstream contract without any
patches to the protocol code: when a peer connects with no password,
Connection::start in the upstream code calls try_start_cm_ipc, which
ipc::connect-s the `_cm` pipe before falling back to spawning a Flutter
CM child. Since cm_popup is up first, step 1 succeeds; we read the
Data::Login{authorized:false} frame, show MessageBoxTimeoutW (Yes/No,
60s, top-most, system-modal), and reply Data::Authorize or Data::Close.

Source tree

    src/main.rs             CLI dispatcher + run_server() composition
    src/cli.rs              hand-rolled argv parser + unit tests
    src/service.rs          windows-service install/uninstall/dispatcher
    src/config_import.rs    --config blob decoding + persistence
    src/cm_popup.rs         _cm IPC listener + Win32 approval dialog

Vendoring

The upstream RustDesk crate is vendored under vendor/rustdesk/ — full
workspace including libs/{hbb_common, scrap, enigo, clipboard,
virtual_display, remote_printer}. This makes the build self-contained
(no submodules, no sibling-repo checkout in CI) and gives us freedom to
fork in a different direction later. Excluded from the vendor: .git,
target/, flutter/, appimage/, flatpak/, fastlane/, docs/, examples/,
ci/, build.py, Dockerfile, upstream README/CLAUDE/AGENTS/GEMINI.

One local divergence vs. upstream: vendor/rustdesk/src/lib.rs flips
`mod custom_server` → `pub mod custom_server` so config_import.rs can
call get_custom_server_from_string without going through the
ui_interface shim. Documented in README.md → "Re-syncing the vendored
copy".

CI

.gitea/workflows/build-windows.yml builds on a self-hosted Windows
runner with Rust 1.75, LLVM 15.0.6 (libclang for bindgen via libvpx-sys),
and a vcpkg cache. The vendored vcpkg.json drives x64-windows-static
deps. The workflow stages the resulting hello-agent.exe into
SignOutput\, reports authenticode signing status (warns on unsigned),
and uploads as artifact. ~15 min full build, faster on incremental.

Out of scope for this commit: Linux/macOS builds, code signing, MSI
packaging, coexistence with stock rustdesk on the same box (currently
shares the RustDesk APP_NAME and config dir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:29:31 +02:00

163 lines
6.4 KiB
YAML

name: build-windows
on:
push:
branches: [main, master]
workflow_dispatch:
inputs:
version_suffix:
description: "Version suffix (e.g. 'cst', 'beta1'). Empty = vanilla."
type: string
default: ""
env:
RUST_VERSION: "1.75"
LLVM_VERSION: "15.0.6"
# bindgen (pulled in via scrap → libvpx-sys) reads LIBCLANG_PATH; the runner
# provisioner installs LLVM here.
LLVM_HOME: 'C:\tools\llvm-15.0.6'
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
jobs:
build-x64:
name: build-hello-agent-x64
runs-on: [self-hosted, windows-10]
timeout-minutes: 90
env:
VCPKG_ROOT: C:\vcpkg
VCPKG_BINARY_SOURCES: "clear;files,C:\\vcpkg-cache,readwrite"
LIBCLANG_PATH: 'C:\tools\llvm-15.0.6\bin'
steps:
- name: Checkout hello-agent (with vendored rustdesk)
uses: actions/checkout@v4
# We vendor the rustdesk source under vendor/rustdesk/ so this
# checkout is fully self-contained — no sibling repo, no submodules.
- name: Verify host toolchain
shell: pwsh
run: |
$required = 'pwsh','git','bash','python','rustc','cargo','rustup','clang'
$missing = @()
foreach ($tool in $required) {
$cmd = Get-Command $tool -ErrorAction SilentlyContinue
if (-not $cmd) { $missing += $tool; continue }
$ver = & $tool --version 2>&1 | Select-Object -First 1
Write-Host ("{0,-10} {1} ({2})" -f $tool, $cmd.Source, $ver)
}
if ($missing.Count -gt 0) {
Write-Error ("Missing tools on runner: {0}" -f ($missing -join ', '))
exit 1
}
if (-not $env:VCPKG_ROOT -or -not (Test-Path "$env:VCPKG_ROOT\vcpkg.exe")) {
Write-Error "VCPKG_ROOT not set or vcpkg.exe missing at $env:VCPKG_ROOT"
exit 1
}
if (-not (Test-Path "$env:LIBCLANG_PATH\libclang.dll")) {
Write-Error "libclang.dll not found at $env:LIBCLANG_PATH"
exit 1
}
- name: Configure Rust toolchain
shell: pwsh
run: |
rustup toolchain install $env:RUST_VERSION --profile minimal --component rustfmt
if ($LASTEXITCODE -ne 0) { throw "rustup toolchain install failed ($LASTEXITCODE)" }
rustup default $env:RUST_VERSION
if ($LASTEXITCODE -ne 0) { throw "rustup default failed ($LASTEXITCODE)" }
rustup target add x86_64-pc-windows-msvc
rustc --version
cargo --version
- name: Configure git safe.directory
shell: pwsh
run: git config --global --add safe.directory '*'
- name: vcpkg install dependencies (x64-windows-static)
shell: bash
env:
VCPKG_DEFAULT_HOST_TRIPLET: x64-windows-static
# vcpkg.json sits at vendor/rustdesk/vcpkg.json (alongside the
# rustdesk Cargo.toml). Run from there so manifest mode picks it up.
run: |
mkdir -p /c/vcpkg-cache
cd vendor/rustdesk
if ! "$VCPKG_ROOT/vcpkg" install \
--triplet x64-windows-static \
--x-install-root="$VCPKG_ROOT/installed"; then
find "$VCPKG_ROOT/" -name "*.log" -exec sh -c 'echo "===== {} ====="; cat "{}"' \;
exit 1
fi
# Build hello-agent. We do NOT pre-build vendor/rustdesk/libs/virtual_display/dylib
# the way the upstream rustdesk workflow does. That dylib produces a
# standalone `dylib_virtual_display.dll` runtime artifact that the
# rustdesk Flutter exe ships side-by-side; hello-agent doesn't bundle
# it (no virtual-display feature in v0), and the `virtual_display`
# crate that librustdesk *does* link against has no compile-time dep
# on the dylib — it loads it by name at runtime if present.
#
# Pre-building it would also force a second cargo invocation inside
# the vendor/rustdesk/ workspace, which has no Cargo.lock of its own
# and would re-resolve git deps from HEAD (breaking the tray-icon
# 0.21.3 pin we keep at the hello-agent root).
- name: Cargo build hello-agent
shell: pwsh
run: |
cargo build --release --bin hello-agent --locked
if ($LASTEXITCODE -ne 0) { throw "hello-agent build failed" }
if (-not (Test-Path target\release\hello-agent.exe)) {
throw "target\release\hello-agent.exe missing after cargo build"
}
- name: Compute version suffix and stage artifact
shell: pwsh
run: |
$suffix = "${env:VERSION_SUFFIX}"
if ($suffix) { $tag = "0.1.0-$suffix" } else { $tag = "0.1.0" }
New-Item -ItemType Directory -Force -Path .\SignOutput | Out-Null
Copy-Item -Force `
target\release\hello-agent.exe `
".\SignOutput\hello-agent-$tag-x86_64.exe"
Write-Host "staged: SignOutput\hello-agent-$tag-x86_64.exe"
env:
VERSION_SUFFIX: ${{ inputs.version_suffix }}
- name: Report signing status of build artifacts
shell: pwsh
run: |
$artifacts = Get-ChildItem .\SignOutput -Include *.exe -File
if (-not $artifacts) {
Write-Warning "No artifacts found in SignOutput\"
return
}
$unsigned = @()
foreach ($f in $artifacts) {
$sig = Get-AuthenticodeSignature -FilePath $f.FullName
$size = '{0,8:N0}' -f $f.Length
switch ($sig.Status) {
'Valid' {
Write-Host ("[ SIGNED ] {0} ({1} bytes) signed by: {2}" -f $f.Name, $size, $sig.SignerCertificate.Subject)
}
'NotSigned' {
Write-Host ("[UNSIGNED] {0} ({1} bytes)" -f $f.Name, $size)
$unsigned += $f.Name
}
default {
Write-Host ("[ {0,-7} ] {1} ({2} bytes) -- {3}" -f $sig.Status, $f.Name, $size, $sig.StatusMessage)
$unsigned += $f.Name
}
}
}
if ($unsigned.Count -gt 0) {
$list = $unsigned -join ', '
Write-Host "::warning title=Unsigned artifacts::$list -- SmartScreen will warn end users. Wire up signing before distributing."
}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: hello-agent-windows-x64-${{ github.sha }}
path: SignOutput/hello-agent-*.exe
if-no-files-found: error
retention-days: 14