diff --git a/build.rs b/build.rs index 28a7a5e45..30e486a4b 100644 --- a/build.rs +++ b/build.rs @@ -2,7 +2,12 @@ fn build_windows() { let file = "src/platform/windows.cc"; let file2 = "src/platform/windows_delete_test_cert.cc"; - cc::Build::new().file(file).file(file2).compile("windows"); + let mut build = cc::Build::new(); + if !cfg!(feature = "flutter") { + build.cpp(true).std("c++17").define("RUSTDESK_SCITER", None); + println!("cargo:rerun-if-changed=flutter/windows/runner/service_control.h"); + } + build.file(file).file(file2).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); println!("cargo:rerun-if-changed={}", file); println!("cargo:rerun-if-changed={}", file2); diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 747d6f586..4ac6b9983 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -10,6 +10,7 @@ #include "win32_desktop.h" #include "flutter_window.h" #include "utils.h" +#include "service_control.h" typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); @@ -23,6 +24,9 @@ const wchar_t* getWindowClassName(); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + if (auto result = service_control::HandleCommand(command_line)) { + return static_cast(*result); + } HINSTANCE hInstance = LoadLibraryA("librustdesk.dll"); if (!hInstance) { diff --git a/flutter/windows/runner/service_control.h b/flutter/windows/runner/service_control.h new file mode 100644 index 000000000..787c36c7e --- /dev/null +++ b/flutter/windows/runner/service_control.h @@ -0,0 +1,183 @@ +#ifndef RUSTDESK_RUNNER_SERVICE_CONTROL_H_ +#define RUSTDESK_RUNNER_SERVICE_CONTROL_H_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace service_control { + +using ServiceHandle = std::unique_ptr, + decltype(&CloseServiceHandle)>; +constexpr DWORD kStopTimeoutMilliseconds = 30000; +constexpr DWORD kPollIntervalMilliseconds = 100; + +inline DWORD StopService(SC_HANDLE service) { + SERVICE_STATUS_PROCESS status{}; + DWORD bytes_needed = 0; + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, + reinterpret_cast(&status), sizeof(status), + &bytes_needed)) { + return GetLastError(); + } + if (status.dwCurrentState == SERVICE_STOPPED) { + return ERROR_SUCCESS; + } + if (status.dwCurrentState != SERVICE_STOP_PENDING) { + SERVICE_STATUS stopped{}; + if (!ControlService(service, SERVICE_CONTROL_STOP, &stopped)) { + const DWORD error = GetLastError(); + return error == ERROR_SERVICE_NOT_ACTIVE ? ERROR_SUCCESS : error; + } + } + // The installer may remove files next; accepting STOP alone does not mean + // the service has reached SERVICE_STOPPED. + const ULONGLONG started = GetTickCount64(); + while (status.dwCurrentState != SERVICE_STOPPED) { + if (GetTickCount64() - started >= kStopTimeoutMilliseconds) { + return ERROR_TIMEOUT; + } + Sleep(kPollIntervalMilliseconds); + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, + reinterpret_cast(&status), sizeof(status), + &bytes_needed)) { + return GetLastError(); + } + } + return ERROR_SUCCESS; +} + +inline DWORD RemoveService(const wchar_t* name) { + ServiceHandle manager(OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT), + CloseServiceHandle); + if (!manager) { + return GetLastError(); + } + ServiceHandle service( + OpenServiceW(manager.get(), name, + SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE), + CloseServiceHandle); + if (!service) { + const DWORD error = GetLastError(); + return error == ERROR_SERVICE_DOES_NOT_EXIST ? ERROR_SUCCESS : error; + } + const DWORD stopped = StopService(service.get()); + if (stopped != ERROR_SUCCESS) { + // The uninstall script must still reach its taskkill cleanup. + std::fwprintf(stderr, + L"Service did not stop cleanly: Windows error %lu\n", stopped); + } + if (DeleteService(service.get())) { + return ERROR_SUCCESS; + } + const DWORD error = GetLastError(); + return error == ERROR_SERVICE_MARKED_FOR_DELETE ? ERROR_SUCCESS : error; +} + +inline DWORD InstallService(const wchar_t* name, const wchar_t* executable, + bool rollback_on_failure) { + if (!*executable || wcschr(executable, L'"')) { + return ERROR_INVALID_PARAMETER; + } + const std::wstring binary_path = + L"\"" + std::wstring(executable) + L"\" --service"; + const std::wstring display_name = std::wstring(name) + L" Service"; + ServiceHandle manager( + OpenSCManagerW(nullptr, nullptr, + SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE), + CloseServiceHandle); + if (!manager) { + return GetLastError(); + } + ServiceHandle service( + CreateServiceW(manager.get(), name, display_name.c_str(), + SERVICE_START | DELETE, SERVICE_WIN32_OWN_PROCESS, + SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, + binary_path.c_str(), nullptr, + nullptr, nullptr, nullptr, nullptr), + CloseServiceHandle); + const bool created = service != nullptr; + // --after-install can reuse a registration. Preserve its executable and + // account, as the previous create-then-start script did. + if (!service && GetLastError() == ERROR_SERVICE_EXISTS) { + service.reset(OpenServiceW(manager.get(), name, SERVICE_START)); + } + if (!service) { + return GetLastError(); + } + if (StartServiceW(service.get(), 0, nullptr)) { + return ERROR_SUCCESS; + } + const DWORD error = GetLastError(); + // Only manual Start Service restores stop-service=Y on failure. Other + // installation paths retain the registration for a later startup attempt. + if (rollback_on_failure && created && error != ERROR_SERVICE_ALREADY_RUNNING && + !DeleteService(service.get())) { + const DWORD rollback_error = GetLastError(); + std::fwprintf(stderr, + L"Failed to roll back newly created service: Windows error %lu\n", + rollback_error); + } + return error == ERROR_SERVICE_ALREADY_RUNNING ? ERROR_SUCCESS : error; +} + +inline DWORD RunCommand(int count, wchar_t** args) { + constexpr int kRemoveArgumentCount = 4; + constexpr int kInstallArgumentCount = 5; + if (count < kRemoveArgumentCount) { + return ERROR_BAD_ARGUMENTS; + } + if (!*args[3]) { + return ERROR_INVALID_PARAMETER; + } + const std::wstring_view operation(args[2]); + const bool rollback_on_failure = operation == L"create-service-with-rollback"; + if ((operation == L"create-service" || rollback_on_failure) && + count == kInstallArgumentCount) { + return InstallService(args[3], args[4], rollback_on_failure); + } + if (operation == L"remove" && count == kRemoveArgumentCount) { + return RemoveService(args[3]); + } + return ERROR_BAD_ARGUMENTS; +} + +// Keep these commands in the existing runner so installation/removal can control +// services before loading librustdesk.dll or initializing the UI. A Rust-side +// handler would first require that DLL to load. The header-only implementation +// keeps main.cpp as a dispatch hook without adding a CMake source entry; it is +// compiled into the existing EXE, not a separately packaged helper. +inline std::optional HandleCommand(const wchar_t* command_line) { + constexpr std::wstring_view option = L"--service-control"; + const std::wstring_view arguments(command_line); + if (arguments.compare(0, option.size(), option) != 0 || + (arguments.size() > option.size() && arguments[option.size()] != L' ' && + arguments[option.size()] != L'\t')) { + return std::nullopt; + } + int count = 0; + wchar_t** args = CommandLineToArgvW(GetCommandLineW(), &count); + if (!args) { + return GetLastError(); + } + const DWORD result = RunCommand(count, args); + LocalFree(args); + if (result != ERROR_SUCCESS) { + std::fwprintf(stderr, + L"RustDesk service control failed with Windows error %lu\n", + result); + } + return result; +} + +} // namespace service_control + +#endif // RUSTDESK_RUNNER_SERVICE_CONTROL_H_ diff --git a/src/main.rs b/src/main.rs index 336d729b0..7cdbcb79e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,10 @@ fn main() { #[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))] fn main() -> std::process::ExitCode { + #[cfg(windows)] + if let Some(exit_code) = platform::handle_service_control() { + return exit_code; + } #[cfg(all(windows, not(feature = "inline")))] unsafe { winapi::um::shellscalingapi::SetProcessDpiAwareness(2); diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 03e171838..f23f1c2c8 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -15,6 +15,23 @@ #include #include +#ifdef RUSTDESK_SCITER +// Share the runner's service operations without loading either UI framework. +#include "../../flutter/windows/runner/service_control.h" + +extern "C" DWORD run_service_control() +{ + int count = 0; + wchar_t** args = CommandLineToArgvW(GetCommandLineW(), &count); + if (!args) { + return GetLastError(); + } + const DWORD result = service_control::RunCommand(count, args); + LocalFree(args); + return result; +} +#endif + extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); void flog(char const *fmt, ...) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index ff0f5cf98..00457c811 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -97,6 +97,7 @@ use windows_service::{ use winreg::{enums::*, RegKey}; mod acl; +pub(crate) mod installer_files; mod installer_handoff; mod installer_shell; mod verified_update; @@ -107,10 +108,10 @@ pub use acl::{ set_path_permission_for_portable_service_shmem_file, validate_path_for_portable_service_shmem_dir, }; -use installer_handoff::run_cmds; +use installer_handoff::{run_cmds, InstallCommands}; use installer_shell::{ - embedded_shortcut_commands, embedded_tray_shortcut_commands, escape_nested_cmd_ampersands, - shortcut_bytes, validate_install_value, + escape_nested_cmd_ampersands, shortcut_bytes, staged_shortcut_commands, + staged_tray_shortcut_commands, validate_install_value, }; pub use verified_update::{ copy_and_verify_update_file_sha256, schedule_current_verified_update_file_cleanup, @@ -1675,18 +1676,16 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res // The elevated runner expands this to `%~f0.dir`, beside its protected copy. // Do not stage privileged shortcut artifacts in the user-writable `%TEMP%`. let tmp_path = "%RUSTDESK_OUTPUT_DIR%".to_owned(); - let mk_shortcut_commands = embedded_shortcut_commands( + let (mk_shortcut_commands, mk_shortcut_file) = staged_shortcut_commands( shortcut_bytes(&exe, None, shortcut_icon_location.as_deref())?, &format!("{app_name}.lnk"), - "mk_shortcut", - ); - let uninstall_shortcut_commands = embedded_shortcut_commands( + )?; + let (uninstall_shortcut_commands, uninstall_shortcut_file) = staged_shortcut_commands( shortcut_bytes(&exe, Some("--uninstall"), Some("msiexec.exe"))?, &format!("Uninstall {app_name}.lnk"), - "uninstall_shortcut", - ); - let tray_shortcut_commands = - embedded_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?; + )?; + let (tray_shortcut_commands, tray_shortcut_file) = + staged_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?; let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_start_menu_shortcuts = "0".to_owned(); let mut reg_value_printer = "0".to_owned(); @@ -1760,6 +1759,17 @@ copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\ "".to_owned() }; + let after_install = get_after_install( + &exe, + Some(reg_value_start_menu_shortcuts), + Some(reg_value_desktop_shortcuts), + Some(reg_value_printer), + ); + let after_install = native_after_install_commands( + format!("{after_install}\r\n{install_remote_printer}"), + &exe, + true, + )?; // Remember to check if `update_me` need to be changed if changing the `cmds`. // No need to merge the existing dup code, because the code in these two functions are too critical. // New code should be written in a common function. @@ -1791,25 +1801,25 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {dels} {import_config} {after_install} -{install_remote_printer} {sleep} ", display_icon = shortcut_icon_location.as_deref().unwrap_or(exe.as_str()), nested_exe = escape_nested_cmd_ampersands(&exe), version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, - after_install = get_after_install( - &exe, - Some(reg_value_start_menu_shortcuts), - Some(reg_value_desktop_shortcuts), - Some(reg_value_printer) - ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, import_config = get_import_config(&exe), ); - run_cmds(cmds, debug, "install")?; + run_cmds( + InstallCommands { + script: cmds, + files: vec![mk_shortcut_file, uninstall_shortcut_file, tray_shortcut_file], + }, + debug, + "install", + )?; run_after_run_cmds(silent); Ok(()) } @@ -1817,29 +1827,30 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" pub fn run_after_install() -> ResultType<()> { let (_, _, _, exe) = get_install_info(); run_cmds( - get_after_install(&exe, None, None, None), + native_after_install_commands(get_after_install(&exe, None, None, None), &exe, false)?, true, "after_install", ) } pub fn run_before_uninstall() -> ResultType<()> { - run_cmds(get_before_uninstall(true), true, "before_install") + run_cmds(get_before_uninstall(true)?, true, "before_install") } -fn get_before_uninstall(kill_self: bool) -> String { +fn get_before_uninstall(kill_self: bool) -> ResultType { let app_name = crate::get_app_name(); + let (_, _, _, exe) = get_install_info(); + let remove_service = service_control_command("remove", &exe, false)?; let ext = app_name.to_lowercase(); let filter = if kill_self { "".to_string() } else { format!(" /FI \"PID ne {}\"", get_current_pid()) }; - format!( + Ok(format!( " chcp 65001 - sc stop {app_name} - sc delete {app_name} + {remove_service} taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe{filter} reg delete HKEY_CLASSES_ROOT\\.{ext} /f @@ -1847,7 +1858,7 @@ fn get_before_uninstall(kill_self: bool) -> String { netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", broker_exe = WIN_TOPMOST_INJECTED_PROCESS_EXE, - ) + )) } /// Constructs the uninstall command string for the application. @@ -1894,7 +1905,7 @@ fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> ResultType if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\" if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" ", - before_uninstall=get_before_uninstall(kill_self), + before_uninstall=get_before_uninstall(kill_self)?, uninstall_amyuni_idd=get_uninstall_amyuni_idd(), app_name = crate::get_app_name(), )) @@ -3323,15 +3334,24 @@ pub fn try_lock_tray_single_instance() -> bool { } } +/// Returns `true` on failure or cancellation so the caller keeps running. +/// Successful removal exits this process instead of returning. pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { log::info!("Uninstalling service..."); let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); + let (_, _, _, exe) = get_install_info(); + let remove_service = match service_control_command("remove", &exe, false) { + Ok(commands) => commands, + Err(err) => { + log::error!("Failed to prepare service removal: {err}"); + return true; + } + }; Config::set_option("stop-service".into(), "Y".into()); let cmds = format!( " chcp 65001 - sc stop {app_name} - sc delete {app_name} + {remove_service} if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe{filter} @@ -3348,7 +3368,7 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { std::process::exit(0); } -fn get_install_service_commands(path: &str, exe: &str) -> ResultType { +fn get_install_service_commands(path: &str, exe: &str) -> ResultType { let app_name = crate::get_app_name(); for value in [path, exe] { validate_install_value(value)?; @@ -3363,10 +3383,10 @@ fn get_install_service_commands(path: &str, exe: &str) -> ResultType { if let Some(icon) = shortcut_icon_location.as_deref() { validate_install_value(icon)?; } - let tray_shortcut_commands = - embedded_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?; + let (tray_shortcut_commands, tray_shortcut_file) = + staged_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?; let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); - Ok(format!( + let script = format!( " chcp 65001 taskkill /F /IM {app_name}.exe{filter} @@ -3376,8 +3396,12 @@ copy /Y \"%RUSTDESK_OUTPUT_DIR%\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsof {create_service} ", import_config = get_import_config(exe), - create_service = get_create_service(exe), - )) + create_service = service_control_command("create-service-with-rollback", exe, false)?, + ); + Ok(InstallCommands { + script, + files: vec![tray_shortcut_file], + }) } pub fn install_service() -> bool { @@ -3928,7 +3952,86 @@ pub fn update_me_msi(msi: &str, expected_sha256: &str, quiet: bool) -> ResultTyp verified_update::install_verified_msi(msi, expected_sha256, quiet) } +fn native_after_install_commands( + commands: String, + exe: &str, + installed_now: bool, +) -> ResultType { + let service = get_create_service(exe); + if service.is_empty() { + return Ok(commands); + } + let native_service = service_control_command("create-service", exe, installed_now)?; + if native_service == service { + return Ok(commands); + } + // Complete independent setup before service-start errors can exit the BAT. + Ok(format!( + "{}\r\n{native_service}", + commands.replacen(&service, "", 1) + )) +} + +#[cfg(not(feature = "flutter"))] +pub fn handle_service_control() -> Option { + if std::env::args_os().nth(1).as_deref() != Some(std::ffi::OsStr::new("--service-control")) { + return None; + } + extern "C" { + fn run_service_control() -> u32; + } + let error = unsafe { run_service_control() }; + Some(if error == ERROR_SUCCESS { + std::process::ExitCode::SUCCESS + } else { + eprintln!( + "RustDesk service control failed: {}", + std::io::Error::from_raw_os_error(error as i32) + ); + std::process::ExitCode::FAILURE + }) +} + +fn service_control_command(operation: &str, exe: &str, installed_now: bool) -> ResultType { + let app_name = crate::get_app_name(); + let is_create = matches!(operation, "create-service" | "create-service-with-rollback"); + let legacy = match operation { + "create-service" | "create-service-with-rollback" => get_create_service(exe), + "remove" => format!("sc stop {app_name}\r\nsc delete {app_name}"), + _ => bail!("Unsupported service operation: {operation}"), + }; + if legacy.is_empty() || (is_create && Config::get_option("stop-service") == "Y") { + return Ok(legacy); + } + // A fresh installation has just copied this runner. Otherwise only use the + // new command when the caller is the installed executable; other versions + // may not recognize it. + if !installed_now { + let current = std::env::current_exe()?; + let current = current + .to_str() + .ok_or_else(|| anyhow!("Current executable path is not valid Unicode"))?; + if !current.eq_ignore_ascii_case(exe) { + log::debug!("Keeping legacy service commands for a different installed executable"); + return Ok(legacy); + } + } + validate_install_value(exe)?; + let executable_argument = if is_create { + format!(" \"{exe}\"") + } else { + String::new() + }; + Ok(format!( + "start \"\" /b /wait \"{exe}\" --service-control {operation} \"{app_name}\"{executable_argument}\r\n\ + if not \"%errorlevel%\"==\"0\" exit /b %errorlevel%" + )) +} + fn get_import_config(exe: &str) -> String { + // --import-config does not register a service dispatcher, so SCM can report + // a start failure even when import runs. Preserve this legacy contract for + // older installed executables. if config::is_outgoing_only() { return "".to_string(); } diff --git a/src/platform/windows/installer_files.rs b/src/platform/windows/installer_files.rs new file mode 100644 index 000000000..a4eaaaf08 --- /dev/null +++ b/src/platform/windows/installer_files.rs @@ -0,0 +1,373 @@ +use super::{installer_handoff::directory, wide_string}; +use hbb_common::{anyhow::anyhow, log, ResultType}; +use std::{ + fs::{self, File}, + io::{self, Read}, + os::windows::io::{AsRawHandle, FromRawHandle}, + path::{Path, PathBuf}, +}; +use windows::{ + core::{w, Owned, PCWSTR}, + Win32::{ + Foundation::{DuplicateHandle, GENERIC_READ, GENERIC_WRITE, HANDLE, HLOCAL}, + Security::{ + Authorization::{ + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }, + PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, + }, + Storage::FileSystem::{ + CreateFileW, FileDispositionInfo, SetFileInformationByHandle, CREATE_NEW, DELETE, + FILE_ATTRIBUTE_NORMAL, FILE_DISPOSITION_INFO, FILE_SHARE_READ, + }, + System::Threading::GetCurrentProcess, + }, +}; + +// Stage data before elevation instead of relaunching a verifier from a portable +// directory whose executable or DLLs could be replaced while UAC is pending. +pub(super) struct ProtectedInstallFile { + path: PathBuf, + cleanup_handle: Option>, +} + +impl ProtectedInstallFile { + pub(super) fn new(path: &Path, bytes: &[u8]) -> ResultType { + Self::create(path, &mut io::Cursor::new(bytes), None) + } + + pub(super) fn copy_verified(source: &Path, expected_sha256: &str) -> ResultType { + let mut source = File::open(source)?; + let path = directory::path()?.with_extension("tmp"); + Self::create(&path, &mut source, Some(expected_sha256)) + } + + fn create( + path: &Path, + source: &mut impl Read, + expected_sha256: Option<&str>, + ) -> ResultType { + let mut file = create_protected_file(path)?; + let result = (|| -> ResultType> { + io::copy(source, &mut file)?; + file.sync_all()?; + if let Some(expected) = expected_sha256 { + crate::update_hash::verify_sha256_reader(&mut file, expected).map_err(|error| { + anyhow!( + "Failed to verify staged installer file {}: {error:?}", + path.display() + ) + })?; + } + duplicate_cleanup_handle(&file) + })(); + let cleanup_handle = result.map_err(|error| { + // The initial handle can remove an incomplete file without reopening its protected path. + if let Err(cleanup_error) = delete_staged_file(HANDLE(file.as_raw_handle())) { + log::warn!( + "Failed to remove incomplete installer file {}: {cleanup_error}", + path.display() + ); + } + error + })?; + Ok(Self { + path: path.to_owned(), + cleanup_handle: Some(cleanup_handle), + }) + } + + pub(super) fn path(&self) -> &Path { + &self.path + } + + pub(super) fn release(&mut self) { + // cmd.exe cannot read/copy these files while a DELETE handle is open. + // The handoff gate keeps it waiting until every staged handle is closed. + drop(self.cleanup_handle.take()); + } +} + +impl Drop for ProtectedInstallFile { + fn drop(&mut self) { + // Cancellation cannot reopen the protected path with DELETE permission. + let result = match &self.cleanup_handle { + Some(handle) => delete_staged_file(**handle), + None => match fs::remove_file(&self.path) { + Err(error) if error.kind() != io::ErrorKind::NotFound => Err(error.into()), + _ => Ok(()), + }, + }; + if let Err(error) = result { + log::warn!( + "Protected installer file may remain at {}: {error}", + self.path.display() + ); + } + } +} + +fn create_protected_file(path: &Path) -> ResultType { + let mut descriptor = PSECURITY_DESCRIPTOR::default(); + // Restrict OWNER RIGHTS at creation so the unelevated creator cannot reopen + // the file for writing or grant itself that permission after staging. + unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + w!("D:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FRFX;;;OW)"), + SDDL_REVISION_1, + &mut descriptor, + None, + )?; + } + let _descriptor = unsafe { Owned::new(HLOCAL(descriptor.0)) }; + let attributes = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: descriptor.0, + bInheritHandle: false.into(), + }; + let path_string = path + .to_str() + .ok_or_else(|| anyhow!("Invalid protected installer path"))?; + let path_string = wide_string(path_string); + let handle = unsafe { + CreateFileW( + PCWSTR(path_string.as_ptr()), + GENERIC_READ.0 | GENERIC_WRITE.0 | DELETE.0, + FILE_SHARE_READ, + Some(&attributes), + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL, + None, + ) + } + .map_err(|error| { + anyhow!( + "Failed to create protected installer file {}: {error}", + path.display() + ) + })?; + Ok(unsafe { File::from_raw_handle(handle.0) }) +} + +fn duplicate_cleanup_handle(file: &File) -> ResultType> { + let mut handle = HANDLE::default(); + // Retain deletion capability, not a writable handle, while waiting for UAC. + unsafe { + let process = GetCurrentProcess(); + DuplicateHandle( + process, + HANDLE(file.as_raw_handle()), + process, + &mut handle, + DELETE.0, + false, + Default::default(), + )?; + Ok(Owned::new(handle)) + } +} + +fn delete_staged_file(handle: HANDLE) -> ResultType<()> { + let disposition = FILE_DISPOSITION_INFO { + DeleteFile: true.into(), + }; + unsafe { + SetFileInformationByHandle( + handle, + FileDispositionInfo, + &disposition as *const _ as *const _, + std::mem::size_of_val(&disposition) as u32, + )?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use windows::{ + core::BOOL, + Win32::{ + Foundation::HANDLE, + Security::{ + AccessCheck, + Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, + CreateRestrictedToken, CreateWellKnownSid, DuplicateToken, SecurityImpersonation, + WinBuiltinAdministratorsSid, DACL_SECURITY_INFORMATION, DISABLE_MAX_PRIVILEGE, + GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, + PRIVILEGE_SET, PSID, SECURITY_MAX_SID_SIZE, SID_AND_ATTRIBUTES, TOKEN_DUPLICATE, + TOKEN_QUERY, + }, + Storage::FileSystem::{ + FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, + }, + System::Threading::{GetCurrentProcess, OpenProcessToken}, + }, + }; + + #[test] + fn cancelled_staging_removes_files_without_parent_delete_permission() { + use windows::Win32::Security::{ImpersonateLoggedOnUser, RevertToSelf}; + let root = std::env::temp_dir().join(format!("rustdesk-cancel-{}", uuid::Uuid::new_v4())); + fs::create_dir(&root).unwrap(); + let path = root.join("package.tmp"); + let file = ProtectedInstallFile::new(&path, b"cancelled update").unwrap(); + // Omit DELETE_CHILD as in Windows Temp, but retain fixture ACL control. + set_parent_acl(&root, w!("D:P(A;;0x1600af;;;OW)")); + unsafe { + let token = ordinary_owner_token(); + ImpersonateLoggedOnUser(*token).unwrap(); + drop(file); + RevertToSelf().unwrap(); + } + set_parent_acl(&root, w!("D:P(A;;FA;;;OW)")); + let remaining = fs::symlink_metadata(&path); + fs::remove_dir_all(&root).unwrap(); + assert!( + matches!(remaining, Err(error) if error.kind() == io::ErrorKind::NotFound), + "Cancelled staging must remove the protected package" + ); + } + + fn set_parent_acl(path: &Path, sddl: PCWSTR) { + use windows::Win32::Security::{SetFileSecurityW, PROTECTED_DACL_SECURITY_INFORMATION}; + let path = wide_string(path.to_str().unwrap()); + let mut descriptor = PSECURITY_DESCRIPTOR::default(); + unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl, + SDDL_REVISION_1, + &mut descriptor, + None, + ) + .unwrap(); + let _descriptor = Owned::new(HLOCAL(descriptor.0)); + assert!(SetFileSecurityW( + PCWSTR(path.as_ptr()), + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + descriptor + ) + .as_bool()); + } + } + + #[test] + fn staged_files_protect_contents_and_remove_unverified_data() { + const HASH: &str = "304ca1638c5effa6832e0e15b958a8f74847efe4df9c3f3187216e921c168fed"; + let root = std::env::temp_dir().join(format!("rustdesk-staging-{}", uuid::Uuid::new_v4())); + fs::create_dir(&root).unwrap(); + let path = root.join("input.tmp"); + let file = + ProtectedInstallFile::create(&path, &mut io::Cursor::new(b"rustdesk"), Some(HASH)) + .unwrap(); + assert_eq!( + ordinary_owner_access(file.path()), + (FILE_GENERIC_READ | FILE_GENERIC_EXECUTE).0 + ); + assert_eq!(fs::read(file.path()).unwrap(), b"rustdesk"); + assert!(ProtectedInstallFile::new(&path, b"replacement").is_err()); + let rejected = root.join("rejected.tmp"); + assert!(ProtectedInstallFile::create( + &rejected, + &mut io::Cursor::new(b"changed"), + Some(HASH) + ) + .is_err()); + assert!(!rejected.exists()); + drop(file); + fs::remove_dir(root).unwrap(); + } + + fn ordinary_owner_access(path: &Path) -> u32 { + let path = wide_string(path.to_str().unwrap()); + let mut descriptor = PSECURITY_DESCRIPTOR::default(); + unsafe { + GetNamedSecurityInfoW( + PCWSTR(path.as_ptr()), + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + None, + None, + None, + None, + &mut descriptor, + ) + .ok() + .unwrap(); + } + let _descriptor = unsafe { Owned::new(HLOCAL(descriptor.0)) }; + let token = ordinary_owner_token(); + let mapping = GENERIC_MAPPING { + GenericRead: FILE_GENERIC_READ.0, + GenericWrite: FILE_GENERIC_WRITE.0, + GenericExecute: FILE_GENERIC_EXECUTE.0, + GenericAll: FILE_ALL_ACCESS.0, + }; + let mut privileges = PRIVILEGE_SET::default(); + let mut size = std::mem::size_of::() as u32; + let mut granted = 0; + let mut allowed = BOOL::default(); + unsafe { + AccessCheck( + descriptor, + *token, + winapi::um::winnt::MAXIMUM_ALLOWED, + &mapping, + Some(&mut privileges), + &mut size, + &mut granted, + &mut allowed, + ) + .unwrap(); + } + assert!(allowed.as_bool()); + granted + } + + fn ordinary_owner_token() -> Owned { + let mut original = HANDLE::default(); + unsafe { + OpenProcessToken( + GetCurrentProcess(), + TOKEN_QUERY | TOKEN_DUPLICATE, + &mut original, + ) + .unwrap(); + } + let original = unsafe { Owned::new(original) }; + let mut sid = [0; SECURITY_MAX_SID_SIZE as usize]; + let sid_pointer = PSID(sid.as_mut_ptr().cast()); + let mut size = sid.len() as u32; + unsafe { + CreateWellKnownSid( + WinBuiltinAdministratorsSid, + None, + Some(sid_pointer), + &mut size, + ) + .unwrap(); + } + let disabled = [SID_AND_ATTRIBUTES { + Sid: sid_pointer, + Attributes: 0, + }]; + let mut restricted = HANDLE::default(); + unsafe { + CreateRestrictedToken( + *original, + DISABLE_MAX_PRIVILEGE, + Some(&disabled), + None, + None, + &mut restricted, + ) + .unwrap(); + } + let restricted = unsafe { Owned::new(restricted) }; + let mut impersonation = HANDLE::default(); + unsafe { + DuplicateToken(*restricted, SecurityImpersonation, &mut impersonation).unwrap(); + } + unsafe { Owned::new(impersonation) } + } +} diff --git a/src/platform/windows/installer_handoff.rs b/src/platform/windows/installer_handoff.rs index 4b66e3ca5..3fc384c2c 100644 --- a/src/platform/windows/installer_handoff.rs +++ b/src/platform/windows/installer_handoff.rs @@ -1,147 +1,95 @@ -mod directory; +pub(super) mod directory; +mod gate; use super::{ + installer_files::ProtectedInstallFile, installer_shell::{ - get_system_executable, path_for_cmd_assignment, path_for_cmd_environment, - run_elevated_and_wait, trusted_install_environment, - BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH, + get_system_executable, launch_elevated, path_for_cmd_environment, + trusted_install_environment, BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH, WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS, }, validate_install_app_name, ResultType, }; -use hbb_common::{ - anyhow::anyhow, - bail, log, - sha2::{Digest, Sha256}, -}; -use std::{ - fs, - io::{self, Write}, - path::{Path, PathBuf}, -}; +use gate::InstallGate; +use hbb_common::{bail, log}; +use std::{fs, io, path::Path}; -const CERTUTIL_RELATIVE_PATH: &str = "certutil.exe"; const CHCP_RELATIVE_PATH: &str = "chcp.com"; -const FINDSTR_RELATIVE_PATH: &str = "findstr.exe"; const UTF8_CODE_PAGE: u32 = 65001; -const INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE: u32 = 0x5253_0001; -const INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0002; -const INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_0003; -const INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_0004; const BATCH_CODE_PAGE_FAILURE_EXIT_CODE: u32 = 0x5253_0005; -const BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE: u32 = 0x5253_0006; -const BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE: u32 = 0x5253_0007; -const SHA256_HASH_LENGTH: usize = 32; -type BatchHash = [u8; SHA256_HASH_LENGTH]; - -struct InstallCommandScript { - path: PathBuf, - expected_hash: BatchHash, +pub(super) struct InstallCommands { + pub script: String, + pub files: Vec, } -impl Drop for InstallCommandScript { - fn drop(&mut self) { - if let Err(err) = fs::remove_file(&self.path) { - if err.kind() != io::ErrorKind::NotFound { - log::warn!( - "Failed to remove temporary installer file {:?}: {err}", - self.path - ); - } +impl From for InstallCommands { + fn from(script: String) -> Self { + Self { + script, + files: Vec::new(), } } } -fn prepare_install_commands(commands: &str) -> ResultType { - let commands = commands.replace("\r\n", "\n").replace('\n', "\r\n"); +struct InstallCommandScript { + file: ProtectedInstallFile, + _files: Vec, +} + +impl InstallCommandScript { + fn release_files(&mut self) { + self.file.release(); + for file in &mut self._files { + file.release(); + } + } +} + +fn prepare_install_commands(cmds: &InstallCommands) -> ResultType { + let commands = cmds.script.replace("\r\n", "\n").replace('\n', "\r\n"); let chcp_path = get_system_executable(CHCP_RELATIVE_PATH)?; let chcp = path_for_cmd_environment(&chcp_path)?; + let files = cmds + .files + .iter() + .map(|file| path_for_cmd_environment(file.path()).map(|path| format!("\"{path}\""))) + .collect::>>()? + .join(" "); + // Call a fixed label so early EXIT /B still reaches cleanup, without reparsing a path. + // Exit the dedicated cmd after self-deletion; EXIT /B would reopen the missing BAT. Ok(format!( "@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n\ + set \"ERRORLEVEL=\"\r\n\ + call :rustdesk_install\r\n\ + set \"RUSTDESK_INSTALL_EXIT_CODE=%ERRORLEVEL%\"\r\n\ + rd /s /q \"%~f0.dir\" > nul 2>&1\r\n\ + (del /q \"%~f0\" {files} > nul 2>&1 & exit %RUSTDESK_INSTALL_EXIT_CODE%)\r\n\ + :rustdesk_install\r\n\ \"{chcp}\" {UTF8_CODE_PAGE} > nul || exit /b \ {BATCH_CODE_PAGE_FAILURE_EXIT_CODE}\r\n\ {}\r\n\ - if exist \"%~f0.dir\" exit /b {BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE}\r\n\ - md \"%~f0.dir\" || exit /b {BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE}\r\n\ set \"RUSTDESK_OUTPUT_DIR=%~f0.dir\"\r\n{commands}\r\nexit /b 0\r\n", trusted_install_environment()? )) } -fn write_install_script(cmds: String) -> ResultType { - let directory = std::env::temp_dir(); - path_for_cmd_environment(&directory)?; +fn write_install_script(cmds: InstallCommands, path: &Path) -> ResultType { let commands = prepare_install_commands(&cmds)?; - let expected_hash = Sha256::digest(commands.as_bytes()).into(); - let path = directory.join(format!( - "rustdesk_install_{}.bat", - uuid::Uuid::new_v4().simple() - )); - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&path)?; - let script = InstallCommandScript { - path, - expected_hash, - }; - file.write_all(commands.as_bytes())?; - file.sync_all()?; - Ok(script) + Ok(InstallCommandScript { + file: ProtectedInstallFile::new(path, commands.as_bytes())?, + _files: cmds.files, + }) } -fn install_hash_pattern(hash: &BatchHash) -> String { - hash.iter() - .map(|byte| format!("{byte:02x}")) - .collect::>() - .join(" *") -} - -fn verified_install_bootstrap( +fn protected_install_parameters( script: &InstallCommandScript, - runner_directory: &Path, + gate: &InstallGate, ) -> ResultType { - let source = path_for_cmd_assignment(&script.path)?; - let directory = path_for_cmd_assignment(runner_directory)?; - let runner = runner_directory.join("install.bat"); - let runner = path_for_cmd_assignment(&runner)?; - let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?; - let certutil_path = get_system_executable(CERTUTIL_RELATIVE_PATH)?; - let findstr_path = get_system_executable(FINDSTR_RELATIVE_PATH)?; - let cmd = path_for_cmd_assignment(&cmd_path)?; - let certutil = path_for_cmd_assignment(&certutil_path)?; - let findstr = path_for_cmd_assignment(&findstr_path)?; - // Short names preserve headroom under the Windows 7 ShellExecuteExW 2,048 - // UTF-16-character parameter limit. Paths are stored with delayed expansion - // disabled, then expanded indirectly so literal `!` survives: S=source, - // R=runner, Q=cmd.exe, H=certutil.exe, F=findstr.exe, D=directory, E=exit code. - Ok(format!( - "setlocal DisableDelayedExpansion & set \"S={source}\" & set \"R={runner}\" & \ - set \"Q={cmd}\" & set \"H={certutil}\" & set \"F={findstr}\" & \ - set \"ERRORLEVEL=\" & set \"D={directory}\" & set \"E=0\" & setlocal EnableDelayedExpansion & \ - if exist \"!R!\" (set \"E={INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE}\") else (\ - copy /Y \"!S!\" \"!R!\" > nul || \ - (set \"E={INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE}\") & \ - if \"!E!\"==\"0\" (\"!H!\" -hashfile \"!R!\" SHA256 > \"!R!.hash\" || \ - set \"E={INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE}\") & \ - if \"!E!\"==\"0\" (\"!F!\" /R /I /X /C:\"{}\" \"!R!.hash\" > nul || \ - set \"E={INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE}\") & \ - if \"!E!\"==\"0\" (\"!Q!\" /D /E:ON /V:OFF /C \"\"!R!\"\" & \ - set \"E=!errorlevel!\")) & \ - rd /s /q \"!D!\" > nul 2>&1 & exit /b !E!", - install_hash_pattern(&script.expected_hash), - )) -} - -fn verified_install_parameters( - script: &InstallCommandScript, - directory: &Path, -) -> ResultType { - let parameters = format!( - "/D /E:ON /V:ON /C {}", - verified_install_bootstrap(script, directory)? - ); + let path = path_for_cmd_environment(script.file.path())?; + let pipe = gate.name(); + let parameters = + format!("/D /E:ON /V:OFF /C \"set /p RUSTDESK_HANDOFF=<\"{pipe}\" >nul && \"{path}\"\""); let length = parameters.encode_utf16().count(); if length >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS { bail!("Elevated command is too long: {length} UTF-16 characters"); @@ -149,19 +97,28 @@ fn verified_install_parameters( Ok(parameters) } -pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> { +pub(super) fn run_cmds(cmds: impl Into, show: bool, tip: &str) -> ResultType<()> { validate_install_app_name(&crate::get_app_name())?; - let script = write_install_script(cmds)?; + let path = directory::path()?.with_extension("bat"); + let directory = path.with_extension("bat.dir"); + let mut script = write_install_script(cmds.into(), &path)?; let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?; - let directory = directory::path()?; - let parameters = verified_install_parameters(&script, &directory)?; + let gate = InstallGate::new()?; + let parameters = protected_install_parameters(&script, &gate)?; directory::create(&directory)?; - let exit_code = run_elevated_and_wait(&cmd_path, ¶meters, show).map_err(|err| { - anyhow!( - "Installer handoff failed; protected directory may remain at {}: {err}", + let process = launch_elevated(&cmd_path, ¶meters, show).map_err(|err| { + log::debug!("Failed to launch elevated installer: {err:#}"); + err.context(format!( + "Installer handoff failed; protected directory may remain at {}", directory.display() - ) + )) })?; + gate.wait_for_client(process.handle())?; + // Keep DELETE-capable handles through UAC cancellation, but close them before + // cmd opens the BAT or copies its inputs. The protected DACL stays unchanged. + script.release_files(); + gate.release(process.handle())?; + let exit_code = process.wait()?; // Cleanup must not change the result of an installation that already ran. match fs::symlink_metadata(&directory) { Ok(_) => log::warn!( @@ -185,141 +142,111 @@ pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> { fn elevated_install_failure_reason(exit_code: u32) -> &'static str { match exit_code { - INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE => "protected runner already exists", - INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE => "failed to copy protected runner", - INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE => "failed to hash protected runner", - INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE => "protected runner hash mismatch", BATCH_CODE_PAGE_FAILURE_EXIT_CODE => "failed to set the installer code page", - BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE => "installer output directory already exists", - BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE => { - "failed to create the installer output directory" - } - BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE => "failed to decode an embedded shortcut", + BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE => "failed to copy a prepared shortcut", _ => "installer command failed", } } #[cfg(test)] mod tests { - use super::super::installer_shell::{ - embedded_shortcut_commands, shortcut_bytes, WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS, - }; + use super::super::installer_shell::shortcut_bytes; use super::*; - use ::windows::Win32::System::Threading; - use std::os::windows::process::CommandExt; + use std::{ + os::windows::{io::AsRawHandle, process::CommandExt}, + process::Stdio, + }; + use windows::Win32::Foundation::HANDLE; #[test] - fn native_install_handoff_verifies_before_execution() { - let marker = std::env::temp_dir().join(format!( - "rustdesk_install_marker_{}", - uuid::Uuid::new_v4().simple() - )); - let runner_dir = std::env::temp_dir().join(format!( - "rustdesk_install_!RUSTDESK_HANDOFF_EXPAND!&^@()runner_{}", - uuid::Uuid::new_v4().simple() - )); - std::fs::create_dir(&runner_dir).expect("runner directory should be created"); - let shortcut_commands = embedded_shortcut_commands( - shortcut_bytes(r"C:\RustDesk.exe", None, None) - .expect("native shortcut should be generated"), - "test.lnk", - "test", - ); - let script = write_install_script(format!( - "if \"%PROGRAMDATA%\"==\"rustdesk_untrusted\" exit /b 77\r\n\ - if \"%PUBLIC%\"==\"rustdesk_untrusted\" exit /b 77\r\n\ - {shortcut_commands}\r\n\ - > \"{}\" echo verified", - marker.display() - )) - .expect("install script should be created"); - let bootstrap = verified_install_bootstrap(&script, &runner_dir) - .expect("native verifier bootstrap should be generated"); - assert_native_handoff_structure(&script, &shortcut_commands, &runner_dir); - - let output = run_install_bootstrap_for_test(&bootstrap); - assert!( - output.status.success(), - "unchanged script failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - assert!(marker.exists(), "verified install script must execute"); - std::fs::remove_file(&marker).expect("test marker should be removed"); - assert!( - !runner_dir.exists(), - "protected runner directory must be removed" - ); - std::fs::create_dir(&runner_dir).expect("runner directory should be created"); - assert_replaced_install_script_is_rejected(&script, &runner_dir, &marker); - assert!( - !runner_dir.exists(), - "failed handoff directory must be removed" - ); + fn protected_handoff_preserves_shortcuts_failures_and_cleanup() { + const COMMAND_FAILURE: u32 = 77; + for exit_code in [0, COMMAND_FAILURE] { + let root = + std::env::temp_dir().join(format!("rustdesk-handoff-{}", uuid::Uuid::new_v4())); + let source = root.join("source !&^@().bat"); + let runner = source.with_extension("bat.dir"); + fs::create_dir_all(&runner).unwrap(); + let bytes = shortcut_bytes(r"C:\RustDesk.exe", Some("--tray"), None).unwrap(); + let shortcut = + ProtectedInstallFile::new(&root.join("shortcut !&^@().tmp"), &bytes).unwrap(); + let marker = root.join("copied.lnk"); + let commands = InstallCommands { + script: format!( + "copy /B /Y \"{}\" \"{}\" > nul\r\nexit /b {exit_code}", + shortcut.path().display(), + marker.display() + ), + files: vec![shortcut], + }; + let mut script = write_install_script(commands, &source).unwrap(); + if !super::super::is_elevated(None).unwrap() { + assert!(fs::write(script.file.path(), "exit /b 0\r\n").is_err()); + } + let output = run_script(&mut script, true); + let copied = fs::read(&marker).unwrap(); + let cleaned = !runner.exists() + && !script.file.path().exists() + && script._files.iter().all(|file| !file.path().exists()); + fs::remove_dir_all(&root).unwrap(); + assert_eq!( + output.status.code().map(|code| code as u32), + Some(exit_code), + "{output:?}" + ); + assert_eq!(copied, bytes); + assert!(cleaned, "The runner and protected inputs must be removed"); + } } - fn assert_native_handoff_structure( - script: &InstallCommandScript, - shortcut_commands: &str, - runner_dir: &Path, - ) { - let bootstrap = verified_install_bootstrap(script, runner_dir) - .expect("native verifier bootstrap should be generated"); - assert!(shortcut_commands.contains("certutil")); - assert!(shortcut_commands.contains("-decode")); - assert!(!shortcut_commands.to_ascii_lowercase().contains("cscript")); - assert!(!shortcut_commands - .to_ascii_lowercase() - .contains("powershell")); - let win7_hash_pattern = script - .expected_hash - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::>() - .join(" *"); - assert!(bootstrap.contains(&format!("/R /I /X /C:\"{win7_hash_pattern}\""))); - let parameters = verified_install_parameters(script, runner_dir) - .expect("elevated parameters should be generated"); - assert!(bootstrap.contains("certutil.exe")); - assert!(bootstrap.contains("findstr.exe")); - assert!(!bootstrap.to_ascii_lowercase().contains("powershell")); - assert!(parameters.encode_utf16().count() < WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS); + #[test] + fn interrupted_handoff_does_not_execute_the_batch() { + let root = + std::env::temp_dir().join(format!("rustdesk-interrupted-{}", uuid::Uuid::new_v4())); + fs::create_dir(&root).unwrap(); + let source = root.join("source.bat"); + let marker = root.join("executed"); + let commands = format!("echo unexpected > \"{}\"", marker.display()); + let mut script = write_install_script(commands.into(), &source).unwrap(); + let output = run_script(&mut script, false); + assert!(!output.status.success(), "{output:?}"); + assert!(!marker.exists()); + drop(script); + assert!(!source.exists()); + fs::remove_dir(&root).unwrap(); + + // A child exiting without connecting must also retire the pending pipe I/O. + let gate = InstallGate::new().unwrap(); + let mut child = + std::process::Command::new(get_system_executable(CMD_RELATIVE_PATH).unwrap()) + .args(["/D", "/C", "exit", "0"]) + .creation_flags(::windows::Win32::System::Threading::CREATE_NO_WINDOW.0) + .spawn() + .unwrap(); + assert!(gate.wait_for_client(HANDLE(child.as_raw_handle())).is_err()); + assert!(child.wait().unwrap().success()); } - fn assert_replaced_install_script_is_rejected( - script: &InstallCommandScript, - runner_dir: &Path, - marker: &Path, - ) { - std::fs::write( - &script.path, - format!("> \"{}\" echo hijacked\r\n", marker.display()), - ) - .expect("install script should be replaceable"); - let replaced = verified_install_bootstrap(&script, &runner_dir) - .expect("replacement verifier should be generated"); - let output = run_install_bootstrap_for_test(&replaced); - assert!( - !output.status.success(), - "replaced script unexpectedly passed verification" - ); - assert_eq!( - output.status.code(), - Some(INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE as i32) - ); - assert!(!marker.exists(), "replaced script must not execute"); - } - - fn run_install_bootstrap_for_test(bootstrap: &str) -> std::process::Output { - let cmd = get_system_executable(CMD_RELATIVE_PATH).expect("system cmd.exe should resolve"); - let mut command = std::process::Command::new(cmd); - command - .env("PROGRAMDATA", "rustdesk_untrusted") - .env("PUBLIC", "rustdesk_untrusted") - .env("RUSTDESK_HANDOFF_EXPAND", "expanded"); - command.raw_arg(format!("/D /E:ON /V:ON /C {bootstrap}")); - command - .creation_flags(Threading::CREATE_NO_WINDOW.0) - .output() - .expect("native verifier should run") + fn run_script(script: &mut InstallCommandScript, release: bool) -> std::process::Output { + let gate = InstallGate::new().unwrap(); + let parameters = protected_install_parameters(script, &gate).unwrap(); + let mut child = + std::process::Command::new(get_system_executable(CMD_RELATIVE_PATH).unwrap()) + .raw_arg(parameters) + .creation_flags(::windows::Win32::System::Threading::CREATE_NO_WINDOW.0) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let process = HANDLE(child.as_raw_handle()); + gate.wait_for_client(process).unwrap(); + assert!(child.try_wait().unwrap().is_none()); + if release { + script.release_files(); + gate.release(process).unwrap(); + } else { + drop(gate); + } + child.wait_with_output().unwrap() } } diff --git a/src/platform/windows/installer_handoff/directory.rs b/src/platform/windows/installer_handoff/directory.rs index aa2c152f8..7c23c9cee 100644 --- a/src/platform/windows/installer_handoff/directory.rs +++ b/src/platform/windows/installer_handoff/directory.rs @@ -22,7 +22,7 @@ use windows::{ }, }; -pub(super) fn path() -> ResultType { +pub(in crate::platform::windows) fn path() -> ResultType { let system = get_system_executable("")?; let windows = system .parent() diff --git a/src/platform/windows/installer_handoff/gate.rs b/src/platform/windows/installer_handoff/gate.rs new file mode 100644 index 000000000..4a3cee1c7 --- /dev/null +++ b/src/platform/windows/installer_handoff/gate.rs @@ -0,0 +1,135 @@ +use super::super::wide_string; +use hbb_common::{bail, log, ResultType}; +use std::io; +use windows::{ + core::{Owned, PCWSTR}, + Win32::{ + Foundation::{ + ERROR_IO_PENDING, ERROR_NOT_FOUND, ERROR_PIPE_CONNECTED, HANDLE, WAIT_OBJECT_0, + }, + Storage::FileSystem::{ + WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, PIPE_ACCESS_OUTBOUND, + }, + System::{ + Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, + PIPE_REJECT_REMOTE_CLIENTS, + }, + Threading::{CreateEventW, GetProcessId, WaitForMultipleObjects, INFINITE}, + IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED}, + }, + }, +}; + +const READY: &[u8] = b"1\r\n"; +const PIPE_INSTANCES: u32 = 1; + +pub(super) struct InstallGate { + pipe: Owned, + name: String, +} + +impl InstallGate { + pub(super) fn new() -> ResultType { + let name = format!( + r"\\.\pipe\rustdesk-install-{}", + uuid::Uuid::new_v4().simple() + ); + let wide_name = wide_string(&name); + let pipe = unsafe { + CreateNamedPipeW( + PCWSTR(wide_name.as_ptr()), + PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, + PIPE_REJECT_REMOTE_CLIENTS, + PIPE_INSTANCES, + READY.len() as u32, + 0, + 0, + None, + ) + }; + if pipe.is_invalid() { + return Err(io::Error::last_os_error().into()); + } + Ok(Self { + pipe: unsafe { Owned::new(pipe) }, + name, + }) + } + + pub(super) fn name(&self) -> &str { + &self.name + } + + pub(super) fn wait_for_client(&self, process: HANDLE) -> ResultType<()> { + let event = unsafe { Owned::new(CreateEventW(None, true, false, None)?) }; + let mut overlapped = OVERLAPPED { + hEvent: *event, + ..Default::default() + }; + if let Err(error) = unsafe { ConnectNamedPipe(*self.pipe, Some(&mut overlapped)) } { + if error.code() == ERROR_IO_PENDING.to_hresult() { + self.wait_for_io(&overlapped, process)?; + } else if error.code() != ERROR_PIPE_CONNECTED.to_hresult() { + return Err(error.into()); + } + } + let mut client = 0; + unsafe { GetNamedPipeClientProcessId(*self.pipe, &mut client)? }; + if client != unsafe { GetProcessId(process) } { + bail!("Unexpected installer handoff pipe client"); + } + Ok(()) + } + + // cmd's SET /P must read a line successfully before it opens the BAT. Closing + // the pipe without this signal fails the command instead of running the BAT. + pub(super) fn release(&self, process: HANDLE) -> ResultType<()> { + let event = unsafe { Owned::new(CreateEventW(None, true, false, None)?) }; + let mut overlapped = OVERLAPPED { + hEvent: *event, + ..Default::default() + }; + // Windows 7 requires a byte-count pointer even for overlapped writes. + let mut written = 0; + if let Err(error) = unsafe { + WriteFile( + *self.pipe, + Some(READY), + Some(&mut written), + Some(&mut overlapped), + ) + } { + if error.code() != ERROR_IO_PENDING.to_hresult() { + return Err(error.into()); + } + self.wait_for_io(&overlapped, process)?; + } + // Keep the server handle alive until cmd exits so the signal stays buffered. + Ok(()) + } + + fn wait_for_io(&self, overlapped: &OVERLAPPED, process: HANDLE) -> ResultType<()> { + let wait = + unsafe { WaitForMultipleObjects(&[overlapped.hEvent, process], false, INFINITE) }; + let mut transferred = 0; + if wait == WAIT_OBJECT_0 { + unsafe { GetOverlappedResult(*self.pipe, overlapped, &mut transferred, false)? }; + return Ok(()); + } + let error = io::Error::last_os_error(); + // Retire pending I/O before the stack-allocated OVERLAPPED and event go away. + if let Err(error) = unsafe { CancelIoEx(*self.pipe, Some(overlapped)) } { + if error.code() != ERROR_NOT_FOUND.to_hresult() { + log::warn!("Failed to cancel installer handoff I/O: {error}"); + } + } + // Cancellation normally completes with ERROR_OPERATION_ABORTED; preserve + // the original wait failure after observing that completion. + let _ = unsafe { GetOverlappedResult(*self.pipe, overlapped, &mut transferred, true) }; + if wait.0 == WAIT_OBJECT_0.0 + 1 { + bail!("Elevated installer exited before the file handoff completed"); + } + bail!("Installer handoff wait failed ({}): {error}", wait.0) + } +} diff --git a/src/platform/windows/installer_shell.rs b/src/platform/windows/installer_shell.rs index 7cbd80608..7efc2503b 100644 --- a/src/platform/windows/installer_shell.rs +++ b/src/platform/windows/installer_shell.rs @@ -1,10 +1,7 @@ -use super::{wide_string, ResultType}; -use hbb_common::{ - anyhow::anyhow, - bail, - base64::{engine::general_purpose::STANDARD, Engine as _}, - log, +use super::{ + installer_files::ProtectedInstallFile, installer_handoff::directory, wide_string, ResultType, }; +use hbb_common::{anyhow::anyhow, bail, log}; use std::{ ffi::OsString, io, mem, @@ -26,7 +23,7 @@ use windows::{ }; pub(super) const CMD_RELATIVE_PATH: &str = "cmd.exe"; -pub(super) const BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE: u32 = 0x5253_0008; +pub(super) const BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0008; pub(super) const WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS: usize = 2048; const SHORTCUT_ICON_INDEX: i32 = 0; @@ -71,26 +68,29 @@ pub(super) fn shortcut_bytes( Ok(bytes) } -pub(super) fn embedded_shortcut_commands(bytes: Vec, filename: &str, name: &str) -> String { - let encoded = STANDARD.encode(bytes); - let encoded_path = format!("%~f0.{name}.b64"); - format!( - "> \"{encoded_path}\" echo {encoded}\r\n\ - certutil -f -decode \"{encoded_path}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE}" - ) +pub(super) fn staged_shortcut_commands( + bytes: Vec, + filename: &str, +) -> ResultType<(String, ProtectedInstallFile)> { + validate_install_value(filename)?; + let file = ProtectedInstallFile::new(&directory::path()?.with_extension("lnk"), &bytes)?; + let source = path_for_cmd_environment(file.path())?; + let commands = format!( + "copy /B /Y \"{source}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_COPY_FAILURE_EXIT_CODE}" + ); + Ok((commands, file)) } -pub(super) fn embedded_tray_shortcut_commands( +pub(super) fn staged_tray_shortcut_commands( app_name: &str, exe: &str, icon_location: Option<&str>, -) -> ResultType { +) -> ResultType<(String, ProtectedInstallFile)> { let filename = format!("{app_name} Tray.lnk"); - Ok(embedded_shortcut_commands( + staged_shortcut_commands( shortcut_bytes(exe, Some("--tray"), icon_location)?, &filename, - "tray_shortcut", - )) + ) } pub(super) fn validate_install_value(value: &str) -> ResultType<()> { @@ -135,14 +135,6 @@ pub(super) fn path_for_cmd_environment(path: &Path) -> ResultType<&str> { Ok(value) } -// Bootstrap assignments are parsed before DisableDelayedExpansion takes effect. -// Escape carets first so `^!` preserves each literal exclamation mark. -pub(super) fn path_for_cmd_assignment(path: &Path) -> ResultType { - Ok(path_for_cmd_environment(path)? - .replace('^', "^^") - .replace('!', "^!")) -} - pub(super) fn trusted_install_environment() -> ResultType { let system = get_system_executable("")?; let program_data = get_known_folder(&FOLDERID_ProgramData)?; @@ -201,7 +193,25 @@ fn initialize_shell_com() -> ResultType> { Ok(Some(ShellComGuard)) } -struct ProcessHandle(HANDLE); +pub(super) struct ProcessHandle(HANDLE); +impl ProcessHandle { + pub(super) fn handle(&self) -> HANDLE { + self.0 + } + + pub(super) fn wait(self) -> ResultType { + let wait_result = unsafe { Threading::WaitForSingleObject(self.0, Threading::INFINITE) }; + if wait_result == Foundation::WAIT_FAILED { + return Err(io::Error::last_os_error().into()); + } + if wait_result != Foundation::WAIT_OBJECT_0 { + bail!("Unexpected elevated process wait result: {}", wait_result.0); + } + let mut exit_code = 0; + unsafe { Threading::GetExitCodeProcess(self.0, &mut exit_code) }?; + Ok(exit_code) + } +} impl Drop for ProcessHandle { fn drop(&mut self) { @@ -217,11 +227,11 @@ fn elevated_working_directory(executable: &Path) -> ResultType<&Path> { .ok_or_else(|| anyhow!("Elevated executable has no parent directory")) } -pub(super) fn run_elevated_and_wait( +pub(super) fn launch_elevated( executable: &Path, parameters: &str, show: bool, -) -> ResultType { +) -> ResultType { let parameter_chars = parameters.encode_utf16().count(); if parameter_chars >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS { bail!("Elevated command is too long: {parameter_chars} UTF-16 characters"); @@ -249,17 +259,7 @@ pub(super) fn run_elevated_and_wait( if info.hProcess.0.is_null() { bail!("Windows did not return an elevated process handle"); } - let process = ProcessHandle(info.hProcess); - let wait_result = unsafe { Threading::WaitForSingleObject(process.0, Threading::INFINITE) }; - if wait_result == Foundation::WAIT_FAILED { - return Err(io::Error::last_os_error().into()); - } - if wait_result != Foundation::WAIT_OBJECT_0 { - bail!("Unexpected elevated process wait result: {}", wait_result.0); - } - let mut exit_code = 0; - unsafe { Threading::GetExitCodeProcess(process.0, &mut exit_code) }?; - Ok(exit_code) + Ok(ProcessHandle(info.hProcess)) } // Escape `^` before using it to escape `&` so both survive nested cmd.exe parsing. diff --git a/src/platform/windows/verified_update.rs b/src/platform/windows/verified_update.rs index 634ff1548..64dae1a86 100644 --- a/src/platform/windows/verified_update.rs +++ b/src/platform/windows/verified_update.rs @@ -1,10 +1,11 @@ mod file; use super::{ - get_custom_client_staging_dir, get_reg_of_hkcr, - handle_custom_client_staging_dir_before_update, installer_handoff::run_cmds, - installer_shell::path_for_cmd_environment, remove_custom_client_staging_dir, ResultType, - REG_NAME_INSTALL_PRINTER, + get_custom_client_staging_dir, get_reg_of_hkcr, handle_custom_client_staging_dir_before_update, + installer_files::ProtectedInstallFile, + installer_handoff::{run_cmds, InstallCommands}, + installer_shell::path_for_cmd_environment, + remove_custom_client_staging_dir, ResultType, REG_NAME_INSTALL_PRINTER, }; use hbb_common::{allow_err, anyhow::anyhow, bail, log}; use std::path::Path; @@ -15,10 +16,7 @@ const VERIFIED_UPDATE_FILE_PREFIX: &str = "rustdesk-verified-"; const VERIFIED_EXE_FILENAME: &str = "update.exe"; const VERIFIED_MSI_FILENAME: &str = "update.msi"; const VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0009; -const VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_000A; -const VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_000B; const VERIFIED_EXE_LAUNCH_FAILURE_EXIT_CODE: u32 = 0x5253_000C; -const SHA256_HASH_LENGTH: usize = 32; pub fn update_to_verified(file: &str, expected_sha256: &str, expected_size: u64) -> ResultType<()> { let extension = file::update_file_extension(file).unwrap_or_default(); @@ -77,50 +75,33 @@ fn launch_verified_update( Ok(()) } -fn verified_update_hash_pattern(expected_sha256: &str) -> ResultType { - let hash = hex::decode(expected_sha256.trim()) - .map_err(|err| anyhow!("Invalid expected update SHA-256: {err}"))?; - if hash.len() != SHA256_HASH_LENGTH { - bail!("Invalid expected update SHA-256 length"); - } - // FINDSTR /R /I /X /C matches the complete certutil hash line; ` *` allows - // optional spaces between byte pairs. - Ok(hash - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::>() - .join(" *")) -} - fn launch_verified_exe(exe: &str, expected_sha256: &str) -> ResultType<()> { - let source = path_for_cmd_environment(Path::new(exe))?; - let hash_pattern = verified_update_hash_pattern(expected_sha256)?; - // Keep the hash outside the EXE directory because update_me() copies that - // entire directory into the installation. + let update = ProtectedInstallFile::copy_verified(Path::new(exe), expected_sha256)?; + let source = path_for_cmd_environment(update.path())?; let commands = format!( "set \"RUSTDESK_VERIFIED_EXE_SOURCE={source}\"\r\n\ set \"RUSTDESK_VERIFIED_EXE=%RUSTDESK_OUTPUT_DIR%\\{VERIFIED_EXE_FILENAME}\"\r\n\ copy /B /Y \"%RUSTDESK_VERIFIED_EXE_SOURCE%\" \"%RUSTDESK_VERIFIED_EXE%\" > nul || exit /b {VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE}\r\n\ - certutil.exe -hashfile \"%RUSTDESK_VERIFIED_EXE%\" SHA256 > \"%~f0.exe.sha256\" || exit /b {VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE}\r\n\ - findstr.exe /R /I /X /C:\"{hash_pattern}\" \"%~f0.exe.sha256\" > nul || exit /b {VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE}\r\n\ start \"\" /wait \"%RUSTDESK_VERIFIED_EXE%\" --update || exit /b {VERIFIED_EXE_LAUNCH_FAILURE_EXIT_CODE}" ); - run_cmds(commands, false, "update-exe") + run_cmds( + InstallCommands { + script: commands, + files: vec![update], + }, + false, + "update-exe", + ) } -fn verified_msi_install_commands( - msi: &Path, - expected_sha256: &str, - quiet: bool, -) -> ResultType { +fn verified_msi_install_commands(msi: &Path, quiet: bool) -> ResultType { use windows::Win32::Foundation::{ ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, ERROR_SUCCESS_REBOOT_REQUIRED, }; - // The open source handle prevents replacing that file, but not retargeting an - // ancestor junction. Copy and reverify it in the protected runner directory. + // The input is already verified and protected, including its parent path. + // Execute the copy from the protected runner to exclude adjacent untrusted files. let source = path_for_cmd_environment(msi)?; - let hash_pattern = verified_update_hash_pattern(expected_sha256)?; let quiet_args = if quiet { " /qn LAUNCH_TRAY_APP=N" } else { "" }; let app_name = crate::get_app_name(); let subkey = format!(".{}", app_name.to_lowercase()); @@ -141,8 +122,6 @@ fn verified_msi_install_commands( "set \"RUSTDESK_VERIFIED_MSI_SOURCE={source}\"\r\n\ set \"RUSTDESK_VERIFIED_MSI=%RUSTDESK_OUTPUT_DIR%\\{VERIFIED_MSI_FILENAME}\"\r\n\ copy /B /Y \"%RUSTDESK_VERIFIED_MSI_SOURCE%\" \"%RUSTDESK_VERIFIED_MSI%\" > nul || exit /b {VERIFIED_UPDATE_COPY_FAILURE_EXIT_CODE}\r\n\ - certutil.exe -hashfile \"%RUSTDESK_VERIFIED_MSI%\" SHA256 > \"%RUSTDESK_VERIFIED_MSI%.sha256\" || exit /b {VERIFIED_UPDATE_HASH_FAILURE_EXIT_CODE}\r\n\ - findstr.exe /R /I /X /C:\"{hash_pattern}\" \"%RUSTDESK_VERIFIED_MSI%.sha256\" > nul || exit /b {VERIFIED_UPDATE_HASH_MISMATCH_EXIT_CODE}\r\n\ msiexec.exe /i \"%RUSTDESK_VERIFIED_MSI%\"{quiet_args}{printer_args} REBOOT=ReallySuppress /norestart\r\n\ set \"RUSTDESK_MSI_EXIT_CODE=%ERRORLEVEL%\"\r\n\ if \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{reboot_initiated}\" exit /b {success}\r\n\ @@ -159,8 +138,12 @@ pub(super) fn install_verified_msi( expected_sha256: &str, quiet: bool, ) -> ResultType<()> { + let update = ProtectedInstallFile::copy_verified(Path::new(msi), expected_sha256)?; run_cmds( - verified_msi_install_commands(Path::new(msi), expected_sha256, quiet)?, + InstallCommands { + script: verified_msi_install_commands(update.path(), quiet)?, + files: vec![update], + }, false, "update-msi", ) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d67e3941b..bf0295a5f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -107,9 +107,18 @@ pub fn goto_install() { pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) { #[cfg(windows)] std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me( - &_options, _path, _silent, _debug - )); + if let Err(err) = crate::platform::windows::install_me(&_options, _path, _silent, _debug) { + log::error!("Installation failed: {err:#}"); + if !_silent + && !err + .downcast_ref::<::windows::core::Error>() + .is_some_and(|err| { + err.code() == ::windows::Win32::Foundation::ERROR_CANCELLED.to_hresult() + }) + { + crate::platform::windows::message_box(&format!("Installation failed:\n{err:#}!")); + } + } std::process::exit(0); }); }