f8ead215d8
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>
268 lines
9.3 KiB
Rust
268 lines
9.3 KiB
Rust
use std::{
|
|
env, fs,
|
|
path::{Path, PathBuf},
|
|
println,
|
|
};
|
|
|
|
#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))]
|
|
fn link_pkg_config(name: &str) -> Vec<PathBuf> {
|
|
// sometimes an override is needed
|
|
let pc_name = match name {
|
|
"libvpx" => "vpx",
|
|
_ => name,
|
|
};
|
|
let lib = pkg_config::probe_library(pc_name)
|
|
.expect(format!(
|
|
"unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled).
|
|
try installing '{pc_name}-dev' from your system package manager.").as_str());
|
|
|
|
lib.include_paths
|
|
}
|
|
#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))]
|
|
fn link_pkg_config(_name: &str) -> Vec<PathBuf> {
|
|
unimplemented!()
|
|
}
|
|
|
|
/// Link vcpkg package.
|
|
fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
|
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
|
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
|
if target_arch == "x86_64" {
|
|
target_arch = "x64".to_owned();
|
|
} else if target_arch == "x86" {
|
|
target_arch = "x86".to_owned();
|
|
} else if target_arch == "loongarch64" {
|
|
target_arch = "loongarch64".to_owned();
|
|
} else if target_arch == "aarch64" {
|
|
target_arch = "arm64".to_owned();
|
|
} else {
|
|
target_arch = "arm".to_owned();
|
|
}
|
|
let mut target = if target_os == "macos" {
|
|
if target_arch == "x64" {
|
|
"x64-osx".to_owned()
|
|
} else if target_arch == "arm64" {
|
|
"arm64-osx".to_owned()
|
|
} else {
|
|
format!("{}-{}", target_arch, target_os)
|
|
}
|
|
} else if target_os == "windows" {
|
|
"x64-windows-static".to_owned()
|
|
} else {
|
|
format!("{}-{}", target_arch, target_os)
|
|
};
|
|
if target_arch == "x86" {
|
|
target = target.replace("x64", "x86");
|
|
}
|
|
println!("cargo:info={}", target);
|
|
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
|
|
path = vcpkg_root.into();
|
|
} else {
|
|
path.push("installed");
|
|
}
|
|
path.push(target);
|
|
println!(
|
|
"cargo:rustc-link-lib=static={}",
|
|
name.trim_start_matches("lib")
|
|
);
|
|
println!(
|
|
"cargo:rustc-link-search={}",
|
|
path.join("lib").to_str().unwrap()
|
|
);
|
|
let include = path.join("include");
|
|
println!("cargo:include={}", include.to_str().unwrap());
|
|
include
|
|
}
|
|
|
|
/// Link homebrew package(for Mac M1).
|
|
fn link_homebrew_m1(name: &str) -> PathBuf {
|
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
|
if target_os != "macos" || target_arch != "aarch64" {
|
|
panic!("Couldn't find VCPKG_ROOT, also can't fallback to homebrew because it's only for macos aarch64.");
|
|
}
|
|
let mut path = PathBuf::from("/opt/homebrew/Cellar");
|
|
path.push(name);
|
|
let entries = if let Ok(dir) = std::fs::read_dir(&path) {
|
|
dir
|
|
} else {
|
|
panic!("Could not find package in {}. Make sure your homebrew and package {} are all installed.", path.to_str().unwrap(),&name);
|
|
};
|
|
let mut directories = entries
|
|
.into_iter()
|
|
.filter(|x| x.is_ok())
|
|
.map(|x| x.unwrap().path())
|
|
.filter(|x| x.is_dir())
|
|
.collect::<Vec<_>>();
|
|
// Find the newest version.
|
|
directories.sort_unstable();
|
|
if directories.is_empty() {
|
|
panic!(
|
|
"There's no installed version of {} in /opt/homebrew/Cellar",
|
|
name
|
|
);
|
|
}
|
|
path.push(directories.pop().unwrap());
|
|
// Link the library.
|
|
println!(
|
|
"cargo:rustc-link-lib=static={}",
|
|
name.trim_start_matches("lib")
|
|
);
|
|
// Add the library path.
|
|
println!(
|
|
"cargo:rustc-link-search={}",
|
|
path.join("lib").to_str().unwrap()
|
|
);
|
|
// Add the include path.
|
|
let include = path.join("include");
|
|
println!("cargo:include={}", include.to_str().unwrap());
|
|
include
|
|
}
|
|
|
|
/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1).
|
|
/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config
|
|
/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1)
|
|
fn find_package(name: &str) -> Vec<PathBuf> {
|
|
let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}");
|
|
println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}");
|
|
if cfg!(all(target_os = "linux", feature = "linux-pkg-config"))
|
|
&& std::env::var(no_pkg_config_var_name).as_deref() != Ok("1")
|
|
{
|
|
link_pkg_config(name)
|
|
} else if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") {
|
|
vec![link_vcpkg(vcpkg_root.into(), name)]
|
|
} else {
|
|
// Try using homebrew
|
|
vec![link_homebrew_m1(name)]
|
|
}
|
|
}
|
|
|
|
fn generate_bindings(
|
|
ffi_header: &Path,
|
|
include_paths: &[PathBuf],
|
|
ffi_rs: &Path,
|
|
exact_file: &Path,
|
|
regex: &str,
|
|
) {
|
|
let mut b = bindgen::builder()
|
|
.header(ffi_header.to_str().unwrap())
|
|
.allowlist_type(regex)
|
|
.allowlist_var(regex)
|
|
.allowlist_function(regex)
|
|
.rustified_enum(regex)
|
|
.trust_clang_mangling(false)
|
|
.layout_tests(false) // breaks 32/64-bit compat
|
|
.generate_comments(false); // comments have prefix /*!\
|
|
|
|
for dir in include_paths {
|
|
b = b.clang_arg(format!("-I{}", dir.display()));
|
|
}
|
|
|
|
b.generate().unwrap().write_to_file(ffi_rs).unwrap();
|
|
fs::copy(ffi_rs, exact_file).ok(); // ignore failure
|
|
}
|
|
|
|
fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &str) {
|
|
let includes = find_package(package);
|
|
let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
|
let src_dir = Path::new(&src_dir);
|
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
|
let out_dir = Path::new(&out_dir);
|
|
|
|
let ffi_header = src_dir.join("src").join("bindings").join(ffi_header);
|
|
println!("rerun-if-changed={}", ffi_header.display());
|
|
for dir in &includes {
|
|
println!("rerun-if-changed={}", dir.display());
|
|
}
|
|
|
|
let ffi_rs = out_dir.join(generated);
|
|
let exact_file = src_dir.join("generated").join(generated);
|
|
generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex);
|
|
}
|
|
|
|
// If you have problems installing ffmpeg, you can download $VCPKG_ROOT/installed from ci
|
|
// Linux require link in hwcodec
|
|
/*
|
|
fn ffmpeg() {
|
|
// ffmpeg
|
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
|
let static_libs = vec!["avcodec", "avutil", "avformat"];
|
|
static_libs.iter().for_each(|lib| {
|
|
find_package(lib);
|
|
});
|
|
if target_os == "windows" {
|
|
println!("cargo:rustc-link-lib=static=libmfx");
|
|
}
|
|
|
|
// os
|
|
let dyn_libs: Vec<&str> = if target_os == "windows" {
|
|
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
|
|
} else if target_os == "linux" {
|
|
let mut v = ["va", "va-drm", "va-x11", "vdpau", "X11", "stdc++"].to_vec();
|
|
if target_arch == "x86_64" {
|
|
v.push("z");
|
|
}
|
|
v
|
|
} else if target_os == "macos" || target_os == "ios" {
|
|
["c++", "m"].to_vec()
|
|
} else if target_os == "android" {
|
|
["z", "m", "android", "atomic"].to_vec()
|
|
} else {
|
|
panic!("unsupported os");
|
|
};
|
|
dyn_libs
|
|
.iter()
|
|
.map(|lib| println!("cargo:rustc-link-lib={}", lib))
|
|
.count();
|
|
|
|
if target_os == "macos" || target_os == "ios" {
|
|
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
|
println!("cargo:rustc-link-lib=framework=CoreVideo");
|
|
println!("cargo:rustc-link-lib=framework=CoreMedia");
|
|
println!("cargo:rustc-link-lib=framework=VideoToolbox");
|
|
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
|
}
|
|
}
|
|
*/
|
|
|
|
fn main() {
|
|
// in this crate, these are also valid configurations
|
|
println!("cargo:rustc-check-cfg=cfg(dxgi,quartz,x11)");
|
|
|
|
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
|
|
|
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
|
|
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
|
|
// please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc",
|
|
// then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc"
|
|
let target = target_build_utils::TargetInfo::new();
|
|
if target.unwrap().target_pointer_width() != "64" {
|
|
// panic!("Only support 64bit system");
|
|
}
|
|
env::remove_var("CARGO_CFG_TARGET_FEATURE");
|
|
env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
|
|
|
|
find_package("libyuv");
|
|
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
|
|
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
|
|
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
|
|
// ffmpeg();
|
|
|
|
if target_os == "ios" {
|
|
// nothing
|
|
} else if target_os == "android" {
|
|
println!("cargo:rustc-cfg=android");
|
|
} else if cfg!(windows) {
|
|
// The first choice is Windows because DXGI is amazing.
|
|
println!("cargo:rustc-cfg=dxgi");
|
|
} else if cfg!(target_os = "macos") {
|
|
// Quartz is second because macOS is the (annoying) exception.
|
|
println!("cargo:rustc-cfg=quartz");
|
|
} else if cfg!(unix) {
|
|
// On UNIX we pray that X11 (with XCB) is available.
|
|
println!("cargo:rustc-cfg=x11");
|
|
}
|
|
}
|