Initial commit: hello-agent — headless RustDesk-protocol-compatible Windows agent
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
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>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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
|
||||
Reference in New Issue
Block a user