mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 02:10:59 +03:00
Customizing a Windows client recompiled the packer for every customer, because data.bin was baked in with include_bytes!. The generic payload is identical across customers, so only the small per-customer delta needs to vary: the branded runner exe, custom.txt and the icons. The packer now also reads an RDPKG RCDATA resource holding a second blob in the same format, and folds it over the compiled-in payload. A build can then inject that resource into a prebuilt template instead of running cargo. The executable to launch comes from the package trailer, and the extraction directory follows its stem, which replaces the sed of APP_PREFIX. Where the executable itself is not customized (sciter x86) it stays in the generic payload and is only renamed, so the merge covers both shapes. custom.txt keeps being written to disk next to the app: that is what the client reads at startup and what the updater stages so a customization survives an upgrade to a stock build. Also fixes generate.py restoring os.curdir (the literal ".") instead of the previous working directory, which left it inside the source folder. CI: ship windows-aarch64 in the unsigned tarball, so ARM custom clients have a template to build from. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
261 lines
8.0 KiB
Rust
261 lines
8.0 KiB
Rust
#![windows_subsystem = "windows"]
|
||
|
||
use std::{
|
||
path::{Path, PathBuf},
|
||
process::{Command, Stdio},
|
||
};
|
||
|
||
use bin_reader::BinaryReader;
|
||
|
||
pub mod bin_reader;
|
||
#[cfg(windows)]
|
||
mod ui;
|
||
|
||
#[cfg(windows)]
|
||
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
|
||
#[cfg(not(windows))]
|
||
const APP_METADATA: &[u8] = &[];
|
||
const APP_METADATA_CONFIG: &str = "meta.toml";
|
||
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
|
||
const APP_PREFIX: &str = "rustdesk";
|
||
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
||
#[cfg(windows)]
|
||
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
|
||
|
||
// The extraction directory follows whatever executable the payload asks for, so a
|
||
// custom client gets its own directory instead of sharing RustDesk's. Falls back to
|
||
// APP_PREFIX when no package is injected, which keeps stock builds unchanged.
|
||
fn app_dir_name(exe: &str) -> String {
|
||
Path::new(&exe.replace('\\', "/"))
|
||
.file_stem()
|
||
.and_then(|stem| stem.to_str())
|
||
.map(|stem| stem.trim().to_lowercase())
|
||
.filter(|stem| !stem.is_empty())
|
||
.unwrap_or_else(|| APP_PREFIX.to_owned())
|
||
}
|
||
|
||
fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool {
|
||
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||
return true;
|
||
};
|
||
for line in app_metadata.lines() {
|
||
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||
*ts = stored_ts;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if *ts == 0 {
|
||
return true;
|
||
}
|
||
|
||
if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) {
|
||
for line in content.lines() {
|
||
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||
return *ts == stored_ts;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
fn write_meta(dir: &Path, ts: u64) {
|
||
let meta_file = dir.join(APP_METADATA_CONFIG);
|
||
if ts != 0 {
|
||
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
|
||
// Ignore is ok here
|
||
let _ = std::fs::write(meta_file, content);
|
||
}
|
||
}
|
||
|
||
fn setup(
|
||
reader: BinaryReader,
|
||
dir: Option<PathBuf>,
|
||
clear: bool,
|
||
_args: &Vec<String>,
|
||
_ui: &mut bool,
|
||
) -> Option<PathBuf> {
|
||
let dir = if let Some(dir) = dir {
|
||
dir
|
||
} else {
|
||
// home dir
|
||
if let Some(dir) = dirs::data_local_dir() {
|
||
dir.join(app_dir_name(&reader.exe))
|
||
} else {
|
||
eprintln!("not found data local dir");
|
||
return None;
|
||
}
|
||
};
|
||
|
||
let mut ts = 0;
|
||
if clear || !is_timestamp_matches(&dir, &mut ts) {
|
||
#[cfg(windows)]
|
||
if _args.is_empty() {
|
||
*_ui = true;
|
||
ui::setup();
|
||
}
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
for file in reader.files.iter() {
|
||
file.write_to_file(&dir);
|
||
}
|
||
write_meta(&dir, ts);
|
||
#[cfg(windows)]
|
||
win::copy_runtime_broker(&dir);
|
||
#[cfg(linux)]
|
||
reader.configure_permission(&dir);
|
||
Some(dir.join(&reader.exe))
|
||
}
|
||
|
||
fn use_null_stdio() -> bool {
|
||
#[cfg(windows)]
|
||
{
|
||
// When running in CMD on Windows 7, using Stdio::inherit() with spawn returns an "invalid handle" error.
|
||
// Since using Stdio::null() didn’t cause any issues, and determining whether the program is launched from CMD or by double-clicking would require calling more APIs during startup, we also use Stdio::null() when launched by double-clicking on Windows 7.
|
||
let is_windows_7 = is_windows_7();
|
||
println!("is windows7: {}", is_windows_7);
|
||
return is_windows_7;
|
||
}
|
||
#[cfg(not(windows))]
|
||
false
|
||
}
|
||
|
||
#[cfg(windows)]
|
||
fn is_windows_7() -> bool {
|
||
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
|
||
|
||
unsafe {
|
||
let mut version_info = OSVERSIONINFOW::default();
|
||
version_info.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
|
||
|
||
if RtlGetVersion(&mut version_info).is_ok() {
|
||
// Windows 7 is version 6.1
|
||
println!(
|
||
"Windows version: {}.{}",
|
||
version_info.dwMajorVersion, version_info.dwMinorVersion
|
||
);
|
||
return version_info.dwMajorVersion == 6 && version_info.dwMinorVersion == 1;
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
|
||
println!("executing {}", path.display());
|
||
// setup env
|
||
let exe = std::env::current_exe().unwrap_or_default();
|
||
let exe_name = exe.file_name().unwrap_or_default();
|
||
// run executable
|
||
let mut cmd = Command::new(path);
|
||
cmd.args(args);
|
||
#[cfg(windows)]
|
||
{
|
||
use std::os::windows::process::CommandExt;
|
||
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||
if _ui {
|
||
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
|
||
}
|
||
}
|
||
|
||
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name);
|
||
if use_null_stdio() {
|
||
cmd.stdin(Stdio::null())
|
||
.stdout(Stdio::null())
|
||
.stderr(Stdio::null());
|
||
} else {
|
||
cmd.stdin(Stdio::inherit())
|
||
.stdout(Stdio::inherit())
|
||
.stderr(Stdio::inherit());
|
||
}
|
||
let _child = cmd.spawn();
|
||
|
||
#[cfg(windows)]
|
||
if _ui {
|
||
match _child {
|
||
Ok(child) => unsafe {
|
||
winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32);
|
||
},
|
||
Err(e) => {
|
||
eprintln!("{:?}", e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let mut args = Vec::new();
|
||
let mut arg_exe = Default::default();
|
||
let mut i = 0;
|
||
for arg in std::env::args() {
|
||
if i == 0 {
|
||
arg_exe = arg.clone();
|
||
} else {
|
||
args.push(arg);
|
||
}
|
||
i += 1;
|
||
}
|
||
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
|
||
#[cfg(windows)]
|
||
let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe);
|
||
#[cfg(not(windows))]
|
||
let quick_support = false;
|
||
|
||
let mut ui = false;
|
||
let reader = BinaryReader::default();
|
||
if let Some(exe) = setup(
|
||
reader,
|
||
None,
|
||
click_setup || args.contains(&"--silent-install".to_owned()),
|
||
&args,
|
||
&mut ui,
|
||
) {
|
||
if click_setup {
|
||
args = vec!["--install".to_owned()];
|
||
} else if quick_support {
|
||
args = vec!["--quick_support".to_owned()];
|
||
}
|
||
execute(exe, args, ui);
|
||
}
|
||
}
|
||
|
||
#[cfg(windows)]
|
||
mod win {
|
||
use std::{fs, os::windows::process::CommandExt, path::Path, process::Command};
|
||
|
||
// Used for privacy mode(magnifier impl).
|
||
pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
|
||
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
|
||
|
||
pub(super) fn copy_runtime_broker(dir: &Path) {
|
||
let src = RUNTIME_BROKER_EXE;
|
||
let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE;
|
||
let target_file = dir.join(tgt);
|
||
if target_file.exists() {
|
||
if let (Ok(src_file), Ok(tgt_file)) = (fs::read(src), fs::read(&target_file)) {
|
||
let src_md5 = format!("{:x}", md5::compute(&src_file));
|
||
let tgt_md5 = format!("{:x}", md5::compute(&tgt_file));
|
||
if src_md5 == tgt_md5 {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
let _allow_err = Command::new("taskkill")
|
||
.args(&["/F", "/IM", "RuntimeBroker_rustdesk.exe"])
|
||
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
|
||
.output();
|
||
let _allow_err = std::fs::copy(src, &format!("{}\\{}", dir.to_string_lossy(), tgt));
|
||
}
|
||
|
||
/// Check if the executable is a Quick Support version.
|
||
/// Note: This function must be kept in sync with `src/core_main.rs`.
|
||
#[inline]
|
||
pub(super) fn is_quick_support_exe(exe: &str) -> bool {
|
||
let exe = exe.to_lowercase();
|
||
exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe")
|
||
}
|
||
}
|