Implement auto-update routine
This commit is contained in:
+45
-3
@@ -31,6 +31,13 @@ pub enum Action {
|
|||||||
/// — every `Data::FS(...)` frame the server sends is executed here, in
|
/// — every `Data::FS(...)` frame the server sends is executed here, in
|
||||||
/// the user's security context.
|
/// the user's security context.
|
||||||
Cm,
|
Cm,
|
||||||
|
/// `--update`. Self-replacement entry point launched as an elevated child
|
||||||
|
/// by the running service's updater (see `librustdesk::updater`) after it
|
||||||
|
/// has downloaded and SHA256-verified a new hello-agent.exe from the
|
||||||
|
/// Gitea releases page. `current_exe()` here points at the staged new
|
||||||
|
/// binary in `%TEMP%`; it copies itself over the installed location and
|
||||||
|
/// restarts the service via `librustdesk::platform::update_me`.
|
||||||
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -47,6 +54,7 @@ impl ParsedArgs {
|
|||||||
let mut service = false;
|
let mut service = false;
|
||||||
let mut server = false;
|
let mut server = false;
|
||||||
let mut cm = false;
|
let mut cm = false;
|
||||||
|
let mut update = false;
|
||||||
let mut config_blob: Option<String> = None;
|
let mut config_blob: Option<String> = None;
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -56,6 +64,7 @@ impl ParsedArgs {
|
|||||||
"--uninstall" => uninstall = true,
|
"--uninstall" => uninstall = true,
|
||||||
"--service" => service = true,
|
"--service" => service = true,
|
||||||
"--server" => server = true,
|
"--server" => server = true,
|
||||||
|
"--update" => update = true,
|
||||||
// Connection-manager popup mode. Treat `--cm-no-ui` (the
|
// Connection-manager popup mode. Treat `--cm-no-ui` (the
|
||||||
// Linux-headless variant librustdesk also tries) as a
|
// Linux-headless variant librustdesk also tries) as a
|
||||||
// synonym; either way we run cm_popup.
|
// synonym; either way we run cm_popup.
|
||||||
@@ -81,14 +90,21 @@ impl ParsedArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mutual-exclusion rules. --install + --config is the MDM one-liner;
|
// Mutual-exclusion rules. --install + --config is the MDM one-liner;
|
||||||
// everything else is one-action-at-a-time.
|
// everything else is one-action-at-a-time. --update is launched by
|
||||||
let exclusive = [uninstall, service, server, cm].iter().filter(|x| **x).count();
|
// the updater as a standalone elevated child, never combined.
|
||||||
|
let exclusive = [uninstall, service, server, cm, update]
|
||||||
|
.iter()
|
||||||
|
.filter(|x| **x)
|
||||||
|
.count();
|
||||||
if exclusive > 1 {
|
if exclusive > 1 {
|
||||||
bail!("--uninstall, --service, --server, --cm are mutually exclusive");
|
bail!("--uninstall, --service, --server, --cm, --update are mutually exclusive");
|
||||||
}
|
}
|
||||||
if uninstall && (install || config_blob.is_some()) {
|
if uninstall && (install || config_blob.is_some()) {
|
||||||
bail!("--uninstall cannot be combined with other flags");
|
bail!("--uninstall cannot be combined with other flags");
|
||||||
}
|
}
|
||||||
|
if update && (install || config_blob.is_some()) {
|
||||||
|
bail!("--update cannot be combined with other flags");
|
||||||
|
}
|
||||||
|
|
||||||
let action = if uninstall {
|
let action = if uninstall {
|
||||||
Action::Uninstall
|
Action::Uninstall
|
||||||
@@ -100,6 +116,8 @@ impl ParsedArgs {
|
|||||||
Action::Server
|
Action::Server
|
||||||
} else if cm {
|
} else if cm {
|
||||||
Action::Cm
|
Action::Cm
|
||||||
|
} else if update {
|
||||||
|
Action::Update
|
||||||
} else if config_blob.is_some() {
|
} else if config_blob.is_some() {
|
||||||
Action::ConfigOnly
|
Action::ConfigOnly
|
||||||
} else {
|
} else {
|
||||||
@@ -131,6 +149,10 @@ OPTIONS:
|
|||||||
--service SCM entry point. Do not invoke manually.
|
--service SCM entry point. Do not invoke manually.
|
||||||
--server Worker mode (launched by the service shell into
|
--server Worker mode (launched by the service shell into
|
||||||
the active console session).
|
the active console session).
|
||||||
|
--update Self-replacement entry point. Launched by the
|
||||||
|
running service's updater after downloading and
|
||||||
|
SHA256-verifying a new release from Gitea. Do
|
||||||
|
not invoke manually.
|
||||||
-h, --help Show this help.
|
-h, --help Show this help.
|
||||||
-V, --version Show version.
|
-V, --version Show version.
|
||||||
|
|
||||||
@@ -191,4 +213,24 @@ mod tests {
|
|||||||
fn unknown_arg() {
|
fn unknown_arg() {
|
||||||
assert!(parse(&["--no-such-flag"]).is_err());
|
assert!(parse(&["--no-such-flag"]).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_alone() {
|
||||||
|
assert_eq!(parse(&["--update"]).unwrap().action, Action::Update);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_install_conflict() {
|
||||||
|
assert!(parse(&["--update", "--install"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_service_conflict() {
|
||||||
|
assert!(parse(&["--update", "--service"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_config_conflict() {
|
||||||
|
assert!(parse(&["--update", "--config", "BLOB"]).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
@@ -90,10 +90,40 @@ fn main() {
|
|||||||
Action::Service => "service",
|
Action::Service => "service",
|
||||||
Action::Server => "server",
|
Action::Server => "server",
|
||||||
Action::Cm => "cm",
|
Action::Cm => "cm",
|
||||||
|
Action::Update => "update",
|
||||||
Action::ConfigOnly | Action::None => "hello-agent",
|
Action::ConfigOnly | Action::None => "hello-agent",
|
||||||
};
|
};
|
||||||
init_logging(mode);
|
init_logging(mode);
|
||||||
|
|
||||||
|
// --update is the self-replacement re-entry: the running service's
|
||||||
|
// updater downloads a new hello-agent.exe to %TEMP%, verifies its
|
||||||
|
// SHA256, then launches `<temp>\hello-agent.exe --update` as an
|
||||||
|
// elevated child. We are that child — `current_exe()` is the staged
|
||||||
|
// new binary, and our only job is to copy ourselves over the
|
||||||
|
// installed location and restart the service. Do it before the
|
||||||
|
// config-import dance below so a corrupt-on-disk config can't block
|
||||||
|
// an update from going through.
|
||||||
|
if parsed.action == Action::Update {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
match librustdesk::platform::update_me(false) {
|
||||||
|
Ok(()) => {
|
||||||
|
log::info!("hello-agent: --update completed");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("hello-agent: --update failed: {e:#}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
eprintln!("hello-agent: --update is Windows-only.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --config is allowed to combine with --install (one-line MDM deploy)
|
// --config is allowed to combine with --install (one-line MDM deploy)
|
||||||
// but on its own is a separate operation. Apply it first so --install
|
// but on its own is a separate operation. Apply it first so --install
|
||||||
// sees the populated config.
|
// sees the populated config.
|
||||||
@@ -172,6 +202,11 @@ fn main() {
|
|||||||
// can watch logs. Production deployments use --install + --service.
|
// can watch logs. Production deployments use --install + --service.
|
||||||
run_server();
|
run_server();
|
||||||
}
|
}
|
||||||
|
Action::Update => {
|
||||||
|
// Handled in the early-return block above (before config-import).
|
||||||
|
// The match has to cover this variant for exhaustiveness.
|
||||||
|
unreachable!("Action::Update is dispatched before this match");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+100
-22
@@ -94,11 +94,24 @@ pub mod input {
|
|||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref SOFTWARE_UPDATE_URL: Arc<Mutex<String>> = Default::default();
|
pub static ref SOFTWARE_UPDATE_URL: Arc<Mutex<String>> = Default::default();
|
||||||
|
// hello-agent local patch: assets resolved by the Gitea-backed
|
||||||
|
// `do_check_software_update`. `SOFTWARE_UPDATE_URL` is kept holding the
|
||||||
|
// human-facing tag URL (so `ui_interface::get_new_version` still works
|
||||||
|
// by rsplit('/')) while the actual binary + sha256 download URLs live
|
||||||
|
// here and are consumed by `updater::check_update`. None = no update.
|
||||||
|
pub static ref SOFTWARE_UPDATE_ASSETS: Arc<Mutex<Option<UpdateAssets>>> = Default::default();
|
||||||
pub static ref DEVICE_ID: Arc<Mutex<String>> = Default::default();
|
pub static ref DEVICE_ID: Arc<Mutex<String>> = Default::default();
|
||||||
pub static ref DEVICE_NAME: Arc<Mutex<String>> = Default::default();
|
pub static ref DEVICE_NAME: Arc<Mutex<String>> = Default::default();
|
||||||
static ref PUBLIC_IPV6_ADDR: Arc<Mutex<(Option<SocketAddr>, Option<Instant>)>> = Default::default();
|
static ref PUBLIC_IPV6_ADDR: Arc<Mutex<(Option<SocketAddr>, Option<Instant>)>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpdateAssets {
|
||||||
|
pub binary_url: String,
|
||||||
|
pub binary_name: String,
|
||||||
|
pub sha256_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// Is server process, with "--server" args
|
// Is server process, with "--server" args
|
||||||
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
|
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
|
||||||
@@ -949,19 +962,45 @@ pub fn check_software_update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to check `danger_accept_invalid_cert` for now.
|
// hello-agent local patch: instead of POSTing to api.rustdesk.com (the
|
||||||
// Because the url is always `https://api.rustdesk.com/version/latest`.
|
// upstream endpoint that resolves the latest stock-RustDesk release), this
|
||||||
|
// queries the Gitea Releases API on the hello-agent repo and resolves
|
||||||
|
// the binary + sha256 asset URLs of the latest release. The original
|
||||||
|
// rustdesk-api code path is intentionally gone: a stock-RustDesk auto-update
|
||||||
|
// would happily replace hello-agent's installation with vanilla rustdesk.
|
||||||
|
// The product version compared against the release tag is
|
||||||
|
// `hbb_common::config::AGENT_VERSION` (populated from `CARGO_PKG_VERSION`
|
||||||
|
// in hello-agent's `main`) — not `crate::VERSION`, which is the embedded
|
||||||
|
// rustdesk core version and would always be much higher than hello-agent's
|
||||||
|
// release tags, making the updater think no update is ever available.
|
||||||
|
const HELLO_AGENT_RELEASES_API: &str =
|
||||||
|
"https://gitea.cstudio.ch/api/v1/repos/mike/hello-agent/releases/latest";
|
||||||
|
const HELLO_AGENT_TAG_URL_PREFIX: &str =
|
||||||
|
"https://gitea.cstudio.ch/mike/hello-agent/releases/tag";
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct GiteaRelease {
|
||||||
|
tag_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
assets: Vec<GiteaAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct GiteaAsset {
|
||||||
|
name: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
||||||
let (request, url) =
|
let url = HELLO_AGENT_RELEASES_API;
|
||||||
hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string());
|
|
||||||
let proxy_conf = Config::get_socks();
|
let proxy_conf = Config::get_socks();
|
||||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||||
let tls_type = get_cached_tls_type(tls_url);
|
let tls_type = get_cached_tls_type(tls_url);
|
||||||
let is_tls_not_cached = tls_type.is_none();
|
let is_tls_not_cached = tls_type.is_none();
|
||||||
let tls_type = tls_type.unwrap_or(TlsType::Rustls);
|
let tls_type = tls_type.unwrap_or(TlsType::Rustls);
|
||||||
let client = create_http_client_async(tls_type, false);
|
let client = create_http_client_async(tls_type, false);
|
||||||
let latest_release_response = match client.post(&url).json(&request).send().await {
|
let response = match client.get(url).send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
upsert_tls_cache(tls_url, tls_type, false);
|
upsert_tls_cache(tls_url, tls_type, false);
|
||||||
resp
|
resp
|
||||||
@@ -970,7 +1009,7 @@ pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
|||||||
if is_tls_not_cached && err.is_request() {
|
if is_tls_not_cached && err.is_request() {
|
||||||
let tls_type = TlsType::NativeTls;
|
let tls_type = TlsType::NativeTls;
|
||||||
let client = create_http_client_async(tls_type, false);
|
let client = create_http_client_async(tls_type, false);
|
||||||
let resp = client.post(&url).json(&request).send().await?;
|
let resp = client.get(url).send().await?;
|
||||||
upsert_tls_cache(tls_url, tls_type, false);
|
upsert_tls_cache(tls_url, tls_type, false);
|
||||||
resp
|
resp
|
||||||
} else {
|
} else {
|
||||||
@@ -978,24 +1017,63 @@ pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let bytes = latest_release_response.bytes().await?;
|
if !response.status().is_success() {
|
||||||
let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?;
|
bail!("Gitea releases API returned HTTP {}", response.status());
|
||||||
let response_url = resp.url;
|
}
|
||||||
let latest_release_version = response_url.rsplit('/').next().unwrap_or_default();
|
let bytes = response.bytes().await?;
|
||||||
|
let release: GiteaRelease = serde_json::from_slice(&bytes)?;
|
||||||
|
|
||||||
if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
|
let latest_version = release.tag_name.trim_start_matches('v');
|
||||||
#[cfg(feature = "flutter")]
|
let current_version = hbb_common::config::AGENT_VERSION.read().unwrap().clone();
|
||||||
{
|
let current_version = if current_version.is_empty() {
|
||||||
let mut m = HashMap::new();
|
crate::VERSION.to_owned()
|
||||||
m.insert("name", "check_software_update_finish");
|
|
||||||
m.insert("url", &response_url);
|
|
||||||
if let Ok(data) = serde_json::to_string(&m) {
|
|
||||||
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
|
||||||
} else {
|
} else {
|
||||||
|
current_version
|
||||||
|
};
|
||||||
|
if get_version_number(latest_version) <= get_version_number(¤t_version) {
|
||||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
|
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
|
||||||
|
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = None;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the Windows binary asset and its SHA256 companion. The release
|
||||||
|
// is expected to carry a `*.exe` (or `*.exe.signed`) and a matching
|
||||||
|
// `*.sha256` file. We don't pair them by name — there should only be
|
||||||
|
// one of each per release.
|
||||||
|
let binary = release.assets.iter().find(|a| {
|
||||||
|
let n = a.name.to_lowercase();
|
||||||
|
(n.ends_with(".exe") || n.ends_with(".exe.signed")) && !n.ends_with(".sha256")
|
||||||
|
});
|
||||||
|
let sha256 = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.to_lowercase().ends_with(".sha256"));
|
||||||
|
let (Some(binary), Some(sha256)) = (binary, sha256) else {
|
||||||
|
log::warn!(
|
||||||
|
"hello-agent release {} is missing a binary and/or .sha256 asset",
|
||||||
|
release.tag_name
|
||||||
|
);
|
||||||
|
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
|
||||||
|
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = None;
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let tag_url = format!("{}/{}", HELLO_AGENT_TAG_URL_PREFIX, release.tag_name);
|
||||||
|
*SOFTWARE_UPDATE_URL.lock().unwrap() = tag_url.clone();
|
||||||
|
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = Some(UpdateAssets {
|
||||||
|
binary_url: binary.browser_download_url.clone(),
|
||||||
|
binary_name: binary.name.clone(),
|
||||||
|
sha256_url: sha256.browser_download_url.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
{
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
m.insert("name", "check_software_update_finish");
|
||||||
|
m.insert("url", &tag_url);
|
||||||
|
if let Ok(data) = serde_json::to_string(&m) {
|
||||||
|
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-13
@@ -3110,21 +3110,27 @@ reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hello-agent local patch: only refresh the Add/Remove Programs entry if
|
||||||
|
// the install path actually created one. Hello-agent's `--install`
|
||||||
|
// (`src/service.rs::install`) does not write to
|
||||||
|
// `HKLM\...\Uninstall\<APP_NAME>` — the agent is a headless service
|
||||||
|
// that intentionally doesn't appear in Add/Remove Programs (there's
|
||||||
|
// nothing meaningful to uninstall through the shell-integrated UI;
|
||||||
|
// operators run `hello-agent.exe --uninstall`). Without this guard,
|
||||||
|
// the upstream `update_me` would *create* the uninstall key on every
|
||||||
|
// update — and then leave it behind as an orphan, since
|
||||||
|
// `service::uninstall()` doesn't remove it either. Upstream rustdesk's
|
||||||
|
// `install_me` does write the key, so stock-rustdesk installs continue
|
||||||
|
// to get their version display refreshed as before.
|
||||||
|
let subkey_exists = |sk: &str| {
|
||||||
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
|
hklm.open_subkey(sk.replace("HKEY_LOCAL_MACHINE\\", ""))
|
||||||
|
.is_ok()
|
||||||
|
};
|
||||||
let reg_cmd = {
|
let reg_cmd = {
|
||||||
let reg_cmd_main = get_reg_cmd(
|
let reg_cmd_main = if subkey_exists(&subkey) {
|
||||||
&subkey,
|
|
||||||
is_msi,
|
|
||||||
&display_icon,
|
|
||||||
&version,
|
|
||||||
&build_date,
|
|
||||||
&version_major,
|
|
||||||
&version_minor,
|
|
||||||
&version_build,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
|
|
||||||
get_reg_cmd(
|
get_reg_cmd(
|
||||||
®_msi_key,
|
&subkey,
|
||||||
is_msi,
|
is_msi,
|
||||||
&display_icon,
|
&display_icon,
|
||||||
&version,
|
&version,
|
||||||
@@ -3137,6 +3143,25 @@ reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
|||||||
} else {
|
} else {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
};
|
};
|
||||||
|
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
|
||||||
|
if subkey_exists(®_msi_key) {
|
||||||
|
get_reg_cmd(
|
||||||
|
®_msi_key,
|
||||||
|
is_msi,
|
||||||
|
&display_icon,
|
||||||
|
&version,
|
||||||
|
&build_date,
|
||||||
|
&version_major,
|
||||||
|
&version_minor,
|
||||||
|
&version_build,
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
};
|
||||||
format!("{}{}", reg_cmd_main, reg_cmd_msi)
|
format!("{}{}", reg_cmd_main, reg_cmd_msi)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
+101
-61
@@ -118,8 +118,16 @@ fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_update(manually: bool) -> ResultType<()> {
|
fn check_update(manually: bool) -> ResultType<()> {
|
||||||
|
// hello-agent local patch: `is_msi_installed()` reads HKLM\...\Uninstall\HelloAgent
|
||||||
|
// and errors when the uninstall key (or `WindowsInstaller` value) is
|
||||||
|
// absent, which is the common case for a hello-agent install. The
|
||||||
|
// upstream code used `?` here, propagating the registry error and
|
||||||
|
// killing the update before our Gitea check ever ran. Swallow the
|
||||||
|
// error: `update_msi` will be `false` (correct — hello-agent is a
|
||||||
|
// custom client so the MSI branch is never the right one anyway).
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client();
|
let update_msi =
|
||||||
|
crate::platform::is_msi_installed().unwrap_or(false) && !crate::is_custom_client();
|
||||||
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
|
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -128,70 +136,102 @@ fn check_update(manually: bool) -> ResultType<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let update_url = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone();
|
// hello-agent local patch: the upstream code reconstructed the download
|
||||||
if update_url.is_empty() {
|
// URL from a GitHub "tag" URL and a hard-coded filename pattern. Both
|
||||||
|
// are now resolved by `do_check_software_update` against the Gitea
|
||||||
|
// Releases API and exposed via `SOFTWARE_UPDATE_ASSETS`.
|
||||||
|
let assets = crate::common::SOFTWARE_UPDATE_ASSETS.lock().unwrap().clone();
|
||||||
|
let Some(assets) = assets else {
|
||||||
log::debug!("No update available.");
|
log::debug!("No update available.");
|
||||||
} else {
|
return Ok(());
|
||||||
let download_url = update_url.replace("tag", "download");
|
};
|
||||||
let version = download_url.split('/').last().unwrap_or_default();
|
let download_url = assets.binary_url;
|
||||||
#[cfg(target_os = "windows")]
|
let sha256_url = assets.sha256_url;
|
||||||
let download_url = if cfg!(feature = "flutter") {
|
let version = assets.binary_name;
|
||||||
format!(
|
log::debug!("New version available: {}", &version);
|
||||||
"{}/rustdesk-{}-x86_64.{}",
|
let client = create_http_client_with_url(&download_url);
|
||||||
download_url,
|
let Some(file_path) = get_download_file_from_url(&download_url) else {
|
||||||
version,
|
bail!("Failed to get the file path from the URL: {}", download_url);
|
||||||
if update_msi { "msi" } else { "exe" }
|
};
|
||||||
)
|
let mut is_file_exists = false;
|
||||||
|
if file_path.exists() {
|
||||||
|
// Check if the file size is the same as the server file size
|
||||||
|
// If the file size is the same, we don't need to download it again.
|
||||||
|
let file_size = std::fs::metadata(&file_path)?.len();
|
||||||
|
let response = client.head(&download_url).send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
bail!("Failed to get the file size: {}", response.status());
|
||||||
|
}
|
||||||
|
let total_size = response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_LENGTH)
|
||||||
|
.and_then(|ct_len| ct_len.to_str().ok())
|
||||||
|
.and_then(|ct_len| ct_len.parse::<u64>().ok());
|
||||||
|
let Some(total_size) = total_size else {
|
||||||
|
bail!("Failed to get content length");
|
||||||
|
};
|
||||||
|
if file_size == total_size {
|
||||||
|
is_file_exists = true;
|
||||||
} else {
|
} else {
|
||||||
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
|
std::fs::remove_file(&file_path)?;
|
||||||
};
|
|
||||||
log::debug!("New version available: {}", &version);
|
|
||||||
let client = create_http_client_with_url(&download_url);
|
|
||||||
let Some(file_path) = get_download_file_from_url(&download_url) else {
|
|
||||||
bail!("Failed to get the file path from the URL: {}", download_url);
|
|
||||||
};
|
|
||||||
let mut is_file_exists = false;
|
|
||||||
if file_path.exists() {
|
|
||||||
// Check if the file size is the same as the server file size
|
|
||||||
// If the file size is the same, we don't need to download it again.
|
|
||||||
let file_size = std::fs::metadata(&file_path)?.len();
|
|
||||||
let response = client.head(&download_url).send()?;
|
|
||||||
if !response.status().is_success() {
|
|
||||||
bail!("Failed to get the file size: {}", response.status());
|
|
||||||
}
|
|
||||||
let total_size = response
|
|
||||||
.headers()
|
|
||||||
.get(reqwest::header::CONTENT_LENGTH)
|
|
||||||
.and_then(|ct_len| ct_len.to_str().ok())
|
|
||||||
.and_then(|ct_len| ct_len.parse::<u64>().ok());
|
|
||||||
let Some(total_size) = total_size else {
|
|
||||||
bail!("Failed to get content length");
|
|
||||||
};
|
|
||||||
if file_size == total_size {
|
|
||||||
is_file_exists = true;
|
|
||||||
} else {
|
|
||||||
std::fs::remove_file(&file_path)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !is_file_exists {
|
}
|
||||||
let response = client.get(&download_url).send()?;
|
if !is_file_exists {
|
||||||
if !response.status().is_success() {
|
let response = client.get(&download_url).send()?;
|
||||||
bail!(
|
if !response.status().is_success() {
|
||||||
"Failed to download the new version file: {}",
|
bail!(
|
||||||
response.status()
|
"Failed to download the new version file: {}",
|
||||||
);
|
response.status()
|
||||||
}
|
);
|
||||||
let file_data = response.bytes()?;
|
|
||||||
let mut file = std::fs::File::create(&file_path)?;
|
|
||||||
file.write_all(&file_data)?;
|
|
||||||
}
|
|
||||||
// We have checked if the `conns` is empty before, but we need to check again.
|
|
||||||
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
|
|
||||||
// before the download, but not empty after the download.
|
|
||||||
if has_no_active_conns() {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
update_new_version(update_msi, &version, &file_path);
|
|
||||||
}
|
}
|
||||||
|
let file_data = response.bytes()?;
|
||||||
|
let mut file = std::fs::File::create(&file_path)?;
|
||||||
|
file.write_all(&file_data)?;
|
||||||
|
}
|
||||||
|
// hello-agent local patch: verify the downloaded binary's SHA256 against
|
||||||
|
// the `.sha256` companion asset published by the same Gitea release
|
||||||
|
// before launching. We're about to run this file with elevated rights —
|
||||||
|
// a mismatch means something went wrong in transit or the release was
|
||||||
|
// tampered with, and we must NOT launch it. The expected-hash file is a
|
||||||
|
// standard `sha256sum` output (`<hex> <filename>`) or just `<hex>`.
|
||||||
|
let sha_client = create_http_client_with_url(&sha256_url);
|
||||||
|
let sha_resp = sha_client.get(&sha256_url).send()?;
|
||||||
|
if !sha_resp.status().is_success() {
|
||||||
|
let _ = std::fs::remove_file(&file_path);
|
||||||
|
bail!("Failed to download SHA256 file: {}", sha_resp.status());
|
||||||
|
}
|
||||||
|
let sha_text = sha_resp.text()?;
|
||||||
|
let expected = sha_text
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
let _ = std::fs::remove_file(&file_path);
|
||||||
|
bail!("Malformed SHA256 file: {:?}", sha_text);
|
||||||
|
}
|
||||||
|
let file_bytes = std::fs::read(&file_path)?;
|
||||||
|
use sha2::Digest as _;
|
||||||
|
let mut hasher = sha2::Sha256::new();
|
||||||
|
hasher.update(&file_bytes);
|
||||||
|
let actual = hex::encode(hasher.finalize());
|
||||||
|
if actual != expected {
|
||||||
|
log::error!(
|
||||||
|
"SHA256 mismatch for {}: expected {}, got {}",
|
||||||
|
version,
|
||||||
|
expected,
|
||||||
|
actual
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&file_path);
|
||||||
|
bail!("SHA256 verification failed");
|
||||||
|
}
|
||||||
|
log::info!("SHA256 verified for {}", version);
|
||||||
|
// We have checked if the `conns` is empty before, but we need to check again.
|
||||||
|
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
|
||||||
|
// before the download, but not empty after the download.
|
||||||
|
if has_no_active_conns() {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
update_new_version(update_msi, &version, &file_path);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
pub const VERSION: &str = "1.4.6";
|
pub const VERSION: &str = "1.4.6";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const BUILD_DATE: &str = "2026-05-09 10:43";
|
pub const BUILD_DATE: &str = "2026-05-21 13:02";
|
||||||
|
|||||||
Reference in New Issue
Block a user