use crate::{common::do_check_software_update, hbbs_http::create_http_client}; use hbb_common::{bail, config, log, ResultType}; use std::{ io::{self, Write}, path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, mpsc::{channel, Receiver, Sender}, Mutex, }, time::{Duration, Instant}, }; enum UpdateMsg { CheckUpdate, Exit, } lazy_static::lazy_static! { static ref TX_MSG : Mutex> = Mutex::new(start_auto_update_check()); } static CONTROLLING_SESSION_COUNT: AtomicUsize = AtomicUsize::new(0); const DUR_ONE_DAY: Duration = Duration::from_secs(60 * 60 * 24); pub fn update_controlling_session_count(count: usize) { CONTROLLING_SESSION_COUNT.store(count, Ordering::SeqCst); } pub fn start_auto_update() { let _sender = TX_MSG.lock().unwrap(); } #[allow(dead_code)] pub fn manually_check_update() -> ResultType<()> { let sender = TX_MSG.lock().unwrap(); sender.send(UpdateMsg::CheckUpdate)?; Ok(()) } #[allow(dead_code)] pub fn stop_auto_update() { let sender = TX_MSG.lock().unwrap(); sender.send(UpdateMsg::Exit).unwrap_or_default(); } #[inline] fn has_no_active_conns() -> bool { let conns = crate::Connection::alive_conns(); conns.is_empty() && has_no_controlling_conns() } #[cfg(any(not(target_os = "windows"), feature = "flutter"))] fn has_no_controlling_conns() -> bool { CONTROLLING_SESSION_COUNT.load(Ordering::SeqCst) == 0 } #[cfg(not(any(not(target_os = "windows"), feature = "flutter")))] fn has_no_controlling_conns() -> bool { let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase()); for arg in [ "--connect", "--play", "--file-transfer", "--view-camera", "--port-forward", "--rdp", ] { if !crate::platform::get_pids_of_process_with_first_arg(&app_exe, arg).is_empty() { return false; } } true } fn start_auto_update_check() -> Sender { let (tx, rx) = channel(); std::thread::spawn(move || start_auto_update_check_(rx)); return tx; } fn start_auto_update_check_(rx_msg: Receiver) { std::thread::sleep(Duration::from_secs(30)); if let Err(e) = check_update(false) { log::error!("Error checking for updates: {}", e); } const MIN_INTERVAL: Duration = Duration::from_secs(60 * 10); const RETRY_INTERVAL: Duration = Duration::from_secs(60 * 30); let mut last_check_time = Instant::now(); let mut check_interval = DUR_ONE_DAY; loop { let recv_res = rx_msg.recv_timeout(check_interval); match &recv_res { Ok(UpdateMsg::CheckUpdate) | Err(_) => { if last_check_time.elapsed() < MIN_INTERVAL { // log::debug!("Update check skipped due to minimum interval."); continue; } // Don't check update if there are alive connections. if !has_no_active_conns() { check_interval = RETRY_INTERVAL; continue; } if let Err(e) = check_update(matches!(recv_res, Ok(UpdateMsg::CheckUpdate))) { log::error!("Error checking for updates: {}", e); check_interval = RETRY_INTERVAL; } else { last_check_time = Instant::now(); check_interval = DUR_ONE_DAY; } } Ok(UpdateMsg::Exit) => break, } } } fn check_update(manually: bool) -> ResultType<()> { #[cfg(target_os = "windows")] let is_msi = crate::platform::is_msi_installed()?; if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) { return Ok(()); } if !do_check_software_update().is_ok() { // ignore return Ok(()); } let update_url = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone(); if update_url.is_empty() { log::debug!("No update available."); } else { let download_url = update_url.replace("tag", "download"); let version = download_url.split('/').last().unwrap_or_default(); #[cfg(target_os = "windows")] let download_url = if cfg!(feature = "flutter") { format!( "{}/rustdesk-{}-x86_64.{}", download_url, version, if is_msi { "msi" } else { "exe" } ) } else { format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version) }; log::debug!("New version available: {}", &version); let client = create_http_client(); 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::().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 !response.status().is_success() { bail!( "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(is_msi, &version, &file_path); } } Ok(()) } #[cfg(target_os = "windows")] fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) { log::debug!("New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}", file_path.to_str()); if let Some(p) = file_path.to_str() { if let Some(session_id) = crate::platform::get_current_process_session_id() { if is_msi { match crate::platform::update_me_msi(p, true) { Ok(_) => { log::debug!("New version \"{}\" updated.", version); } Err(e) => { log::error!( "Failed to install the new msi version \"{}\": {}", version, e ); } } } else { match crate::platform::launch_privileged_process( session_id, &format!("{} --update", p), ) { Ok(h) => { if h.is_null() { log::error!("Failed to update to the new version: {}", version); } } Err(e) => { log::error!("Failed to run the new version: {}", e); } } } } else { log::error!( "Failed to get the current process session id, Error {}", io::Error::last_os_error() ); } } else { // unreachable!() log::error!( "Failed to convert the file path to string: {}", file_path.display() ); } } pub fn get_download_file_from_url(url: &str) -> Option { let filename = url.split('/').last()?; Some(std::env::temp_dir().join(filename)) }