From 425a61f47c1c2557870d8c3c0a12e137786b1e15 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Tue, 7 Oct 2025 16:25:03 +0300 Subject: [PATCH 1/3] chore(agent): update now-proto-pdu crate --- Cargo.lock | 7 ++++--- devolutions-session/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2930d4fa1..9a186c979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.104", + "syn 1.0.109", ] [[package]] @@ -3898,13 +3898,14 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" [[package]] name = "now-proto-pdu" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023d146f7c4e7327e3343ae7ba3c716322ac2ef16e9405d1c6b84e3b66d40b3" +checksum = "22d98b852250eb2c50fa674a0e0c5d6fefecae44301969bfec03019769c083e5" dependencies = [ "bitflags 2.9.4", "ironrdp-core", "ironrdp-error 0.1.3", + "uuid", ] [[package]] diff --git a/devolutions-session/Cargo.toml b/devolutions-session/Cargo.toml index 0ab84168f..725f88a61 100644 --- a/devolutions-session/Cargo.toml +++ b/devolutions-session/Cargo.toml @@ -44,7 +44,7 @@ win-api-wrappers = { path = "../crates/win-api-wrappers", optional = true } [dependencies.now-proto-pdu] optional = true -version = "0.3.2" +version = "0.4.0" features = ["std"] [target.'cfg(windows)'.build-dependencies] From 61df907c52c5f109f49bc889c8c6994792c78d2c Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Fri, 17 Oct 2025 21:18:56 +0300 Subject: [PATCH 2/3] [WIP] feat(session): RDM messages bootstrap --- Cargo.lock | 2 + crates/devolutions-agent-shared/src/lib.rs | 1 + .../src/windows/registry.rs | 46 +- devolutions-session/Cargo.toml | 6 +- devolutions-session/src/dvc/task.rs | 640 +++++++++++++++++- devolutions-session/src/main.rs | 2 +- 6 files changed, 687 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a186c979..5faa1f22c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1436,6 +1436,7 @@ dependencies = [ "camino", "cfg-if", "ctrlc", + "devolutions-agent-shared", "devolutions-gateway-task", "devolutions-log", "embed-resource", @@ -1449,6 +1450,7 @@ dependencies = [ "thiserror 2.0.16", "tokio 1.46.1", "tracing", + "uuid", "win-api-wrappers", "windows 0.61.3", ] diff --git a/crates/devolutions-agent-shared/src/lib.rs b/crates/devolutions-agent-shared/src/lib.rs index 5aef1f74b..7d8098cf2 100644 --- a/crates/devolutions-agent-shared/src/lib.rs +++ b/crates/devolutions-agent-shared/src/lib.rs @@ -43,6 +43,7 @@ pub struct PackageInfoError { pub fn get_installed_agent_version() -> Result, PackageInfoError> { Ok(windows::registry::get_installed_product_version( windows::AGENT_UPDATE_CODE, + windows::registry::ProductVersionEncoding::Agent, )?) } diff --git a/crates/devolutions-agent-shared/src/windows/registry.rs b/crates/devolutions-agent-shared/src/windows/registry.rs index 041f3a72d..65611ae24 100644 --- a/crates/devolutions-agent-shared/src/windows/registry.rs +++ b/crates/devolutions-agent-shared/src/windows/registry.rs @@ -48,9 +48,14 @@ pub fn get_product_code(update_code: Uuid) -> Result, RegistryError Ok(Some(reversed_hex_to_uuid(&product_code)?)) } +pub enum ProductVersionEncoding { + Agent, + Rdm, +} + /// Get the installed version of a product using Windows registry. Returns `None` if the product /// is not installed. -pub fn get_installed_product_version(update_code: Uuid) -> Result, RegistryError> { +pub fn get_installed_product_version(update_code: Uuid, version_encoding: ProductVersionEncoding) -> Result, RegistryError> { let product_code_uuid = match get_product_code(update_code)? { Some(uuid) => uuid, None => return Ok(None), @@ -79,10 +84,14 @@ pub fn get_installed_product_version(update_code: Uuid) -> Result> 24) + 2000; let month = (product_version >> 16) & 0xFF; let day = product_version & 0xFFFF; + let short_year = match version_encoding { + ProductVersionEncoding::Agent => (product_version >> 24) + 2000, + ProductVersionEncoding::Rdm => (product_version >> 24) + 0x700, + }; + Ok(Some(DateVersion { year: short_year, month, @@ -91,3 +100,36 @@ pub fn get_installed_product_version(update_code: Uuid) -> Result Result, RegistryError> { + let product_code_uuid = match get_product_code(update_code)? { + Some(uuid) => uuid, + None => return Ok(None), + } + .braced(); + + let key_path = format!("{REG_CURRENT_VERSION}\\Uninstall\\{product_code_uuid}"); + + const INSTALL_LOCATION_VALUE_NAME: &str = "InstallLocation"; + + // Now we know the product code of installed MSI, we could read its install location. + let product_tree = windows_registry::LOCAL_MACHINE + .open(&key_path) + .map_err(|source| RegistryError::OpenKey { + key: key_path.clone(), + source, + })?; + + let install_location: String = product_tree + .get_value(INSTALL_LOCATION_VALUE_NAME) + .and_then(TryInto::try_into) + .map_err(|source| RegistryError::ReadValue { + value: INSTALL_LOCATION_VALUE_NAME.to_owned(), + key: key_path.clone(), + source, + })?; + + Ok(Some(install_location)) +} diff --git a/devolutions-session/Cargo.toml b/devolutions-session/Cargo.toml index 725f88a61..647754fe9 100644 --- a/devolutions-session/Cargo.toml +++ b/devolutions-session/Cargo.toml @@ -47,6 +47,10 @@ optional = true version = "0.4.0" features = ["std"] +[target.'cfg(windows)'.dependencies] +uuid = { version = "1.0", features = ["v4", "serde"] } +devolutions-agent-shared = { path = "../crates/devolutions-agent-shared" } + [target.'cfg(windows)'.build-dependencies] embed-resource = "3.0" @@ -60,4 +64,4 @@ features = [ "Win32_UI_Shell", "Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse", -] +] \ No newline at end of file diff --git a/devolutions-session/src/dvc/task.rs b/devolutions-session/src/dvc/task.rs index 6c3ab4411..2f295550e 100644 --- a/devolutions-session/src/dvc/task.rs +++ b/devolutions-session/src/dvc/task.rs @@ -1,23 +1,30 @@ use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::mem::size_of; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::{Context, bail}; use async_trait::async_trait; use tokio::select; use tokio::sync::mpsc::{self, Receiver, Sender}; -use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows::Win32::Foundation::{HWND, LPARAM, WPARAM, CloseHandle}; use windows::Win32::Security::{TOKEN_ADJUST_PRIVILEGES, TOKEN_QUERY}; use windows::Win32::System::Shutdown::{ EWX_FORCE, EWX_LOGOFF, EWX_POWEROFF, EWX_REBOOT, ExitWindowsEx, InitiateSystemShutdownW, LockWorkStation, SHUTDOWN_REASON, }; -use windows::Win32::System::Threading::{AttachThreadInput, GetCurrentThreadId}; +use windows::Win32::System::Threading::{AttachThreadInput, GetCurrentThreadId, CreateProcessW, WaitForSingleObject, PROCESS_INFORMATION, STARTUPINFOW, CREATE_UNICODE_ENVIRONMENT, INFINITE, PROCESS_QUERY_INFORMATION, TerminateProcess}; +use windows::Win32::Foundation::WAIT_OBJECT_0; use windows::Win32::UI::Input::KeyboardAndMouse::GetFocus; use windows::Win32::UI::Shell::ShellExecuteW; use windows::Win32::UI::WindowsAndMessaging::{ GetForegroundWindow, GetWindowThreadProcessId, HKL_NEXT, HKL_PREV, MESSAGEBOX_RESULT, MESSAGEBOX_STYLE, - MessageBoxW, PostMessageW, SW_RESTORE, WM_INPUTLANGCHANGEREQUEST, + MessageBoxW, PostMessageW, SW_RESTORE, SW_MAXIMIZE, SW_MINIMIZE, SW_SHOWMAXIMIZED, SW_SHOWNORMAL, + ShowWindow, WM_INPUTLANGCHANGEREQUEST, WM_CLOSE, EnumWindows, }; -use windows::core::PCWSTR; +use windows::core::{PCWSTR, PWSTR}; + use devolutions_gateway_task::Task; use now_proto_pdu::ironrdp_core::IntoOwned; @@ -25,15 +32,25 @@ use now_proto_pdu::{ ComApartmentStateKind, NowChannelCapsetMsg, NowChannelCloseMsg, NowChannelHeartbeatMsg, NowChannelMessage, NowExecBatchMsg, NowExecCancelRspMsg, NowExecCapsetFlags, NowExecDataMsg, NowExecDataStreamKind, NowExecMessage, NowExecProcessMsg, NowExecPwshMsg, NowExecResultMsg, NowExecRunMsg, NowExecStartedMsg, NowExecWinPsMsg, NowMessage, - NowMsgBoxResponse, NowProtoError, NowProtoVersion, NowSessionCapsetFlags, NowSessionMessage, + NowMsgBoxResponse, NowProtoError, NowProtoVersion, NowRdmAppActionMsg, NowRdmAppAction, NowRdmAppNotifyMsg, NowRdmAppStartMsg, NowRdmCapabilitiesMsg, NowRdmMessage, NowRdmAppState, NowRdmReason, NowSessionCapsetFlags, NowSessionMessage, NowSessionMsgBoxReqMsg, NowSessionMsgBoxRspMsg, NowStatusError, NowSystemCapsetFlags, NowSystemMessage, SetKbdLayoutOption, }; use win_api_wrappers::event::Event; use win_api_wrappers::security::privilege::ScopedPrivileges; use win_api_wrappers::utils::WideString; +use win_api_wrappers::process::{Process, ProcessEntry32Iterator}; +use win_api_wrappers::handle::HandleWrapper; + +use devolutions_agent_shared::windows::registry::{get_install_location, get_installed_product_version, ProductVersionEncoding}; +use uuid::Uuid; + +const RDM_UPDATE_CODE_UUID: &str = "2707F3BF-4D7B-40C2-882F-14B0ED869EE8"; + + use crate::dvc::channel::{WinapiSignaledSender, bounded_mpsc_channel, winapi_signaled_mpsc_channel}; + use crate::dvc::fs::TmpFileGuard; use crate::dvc::io::run_dvc_io; use crate::dvc::process::{ExecError, ServerChannelEvent, WinApiProcess, WinApiProcessBuilder}; @@ -282,12 +299,47 @@ enum ProcessMessageAction { Restart(NowChannelCapsetMsg), } +#[derive(Debug, Clone, Copy)] +enum WindowCommand { + Minimize, + Maximize, + Restore, +} + struct MessageProcessor { dvc_tx: WinapiSignaledSender>, io_notification_tx: Sender, #[allow(dead_code)] // Not yet used. capabilities: NowChannelCapsetMsg, sessions: HashMap, + rdm_process_spawned: Arc, +} + +/// RAII wrapper for RDM process handle +struct RdmProcessHandle { + handle: windows::Win32::Foundation::HANDLE, +} + +// SAFETY: HANDLE is just a pointer and can be safely sent between threads +unsafe impl Send for RdmProcessHandle {} +unsafe impl Sync for RdmProcessHandle {} + +impl RdmProcessHandle { + fn new(handle: windows::Win32::Foundation::HANDLE) -> Self { + Self { handle } + } + + fn handle(&self) -> windows::Win32::Foundation::HANDLE { + self.handle + } +} + +impl Drop for RdmProcessHandle { + fn drop(&mut self) { + unsafe { + let _ = CloseHandle(self.handle); + } + } } impl MessageProcessor { @@ -301,6 +353,7 @@ impl MessageProcessor { io_notification_tx, capabilities, sessions: HashMap::new(), + rdm_process_spawned: Arc::new(AtomicBool::new(false)), } } @@ -456,7 +509,7 @@ impl MessageProcessor { } } NowMessage::System(NowSystemMessage::Shutdown(shutdown_msg)) => { - let mut current_process_token = win_api_wrappers::process::Process::current_process() + let mut current_process_token = Process::current_process() .token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?; let mut _priv_tcb = ScopedPrivileges::enter( &mut current_process_token, @@ -502,6 +555,36 @@ impl MessageProcessor { // TODO: Adjust `NowSession` token privileges in NowAgent to make shutdown possible // from this process. } + NowMessage::Rdm(NowRdmMessage::Capabilities(rdm_caps_msg)) => { + match self.process_rdm_capabilities(rdm_caps_msg).await { + Ok(response_msg) => { + self.dvc_tx.send(response_msg.into()).await?; + } + Err(error) => { + error!(%error, "Failed to process RDM capabilities message"); + } + } + } + NowMessage::Rdm(NowRdmMessage::AppStart(rdm_app_start_msg)) => { + match self.process_rdm_app_start(rdm_app_start_msg).await { + Ok(()) => { + info!("RDM application started successfully"); + } + Err(error) => { + error!(%error, "Failed to start RDM application"); + } + } + } + NowMessage::Rdm(NowRdmMessage::AppAction(rdm_app_action_msg)) => { + match self.process_rdm_app_action(rdm_app_action_msg).await { + Ok(()) => { + info!("RDM application action processed successfully"); + } + Err(error) => { + error!(%error, "Failed to process RDM application action"); + } + } + } _ => { warn!("Unsupported message: {:?}", message); } @@ -703,6 +786,266 @@ impl MessageProcessor { self.sessions.clear(); } + + async fn process_rdm_capabilities(&self, rdm_caps_msg: NowRdmCapabilitiesMsg<'_>) -> anyhow::Result> { + let client_timestamp = rdm_caps_msg.timestamp(); + let server_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("Failed to get current timestamp")? + .as_secs(); + + info!(client_timestamp, server_timestamp, "Processing RDM capabilities message"); + + // Check if RDM is available by looking for the installation + let (is_rdm_available, rdm_version) = { + let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID) + .context("Failed to parse RDM update code UUID")?; + match get_installed_product_version(update_code_uuid, ProductVersionEncoding::Rdm) { + Ok(Some(date_version)) => { + info!(version = %date_version, "RDM installation found via MSI registry"); + (true, date_version.to_string()) + } + Ok(None) => { + info!("RDM not found in MSI registry"); + (false, String::new()) + } + Err(error) => { + warn!(%error, "Failed to check RDM via MSI registry"); + (false, String::new()) + } + } + }; + + // Create response message with server timestamp + let mut response = NowRdmCapabilitiesMsg::new(server_timestamp, rdm_version) + .context("Failed to create RDM capabilities response")?; + + if is_rdm_available { + response = response.with_app_available(); + info!("RDM application is available on system"); + } else { + info!("RDM application is not available"); + } + + Ok(NowMessage::Rdm(NowRdmMessage::Capabilities(response))) + } + + async fn process_rdm_app_start(&mut self, rdm_app_start_msg: NowRdmAppStartMsg) -> anyhow::Result<()> { + info!("Processing RDM app start message"); + + // Check if RDM is already running (either spawned by us or externally) + if self.rdm_process_spawned.load(Ordering::Acquire) || is_rdm_running() { + info!("RDM application is already running"); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::READY, NowRdmReason::NOT_SPECIFIED).await?; + return Ok(()); + } + + // Get RDM executable path with proper error handling + let rdm_exe_path = match get_rdm_exe_path() { + Ok(Some(path)) => path, + Ok(None) => { + error!("RDM is not installed - cannot start application"); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::NOT_INSTALLED).await?; + bail!("RDM is not installed"); + } + Err(error) => { + error!("Failed to get RDM executable path: {}", error); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; + return Err(error); + } + }; + + let install_location = rdm_exe_path.parent() + .context("Failed to get RDM installation directory")? + .to_string_lossy() + .to_string(); + + // Build environment variables for fullscreen and jump mode + let mut env_vars = HashMap::new(); + + if rdm_app_start_msg.is_fullscreen() { + env_vars.insert("RDM_OPT_FULLSCREEN".to_string(), "1".to_string()); + info!("Starting RDM in fullscreen mode"); + } + + if rdm_app_start_msg.is_jump_mode() { + env_vars.insert("RDM_OPT_JUMP".to_string(), "1".to_string()); + info!("Starting RDM in jump mode"); + } + + // Create environment block + let env_block = crate::dvc::env::make_environment_block(env_vars)?; + + // Convert command line to wide string + let current_dir = WideString::from(&install_location); + + info!( + exe_path = %rdm_exe_path.display(), + fullscreen = rdm_app_start_msg.is_fullscreen(), + maximized = rdm_app_start_msg.is_maximized(), + jump_mode = rdm_app_start_msg.is_jump_mode(), + "Starting RDM application with CreateProcess" + ); + + // Create process using CreateProcessW in a scoped block + let create_process_result = { + let startup_info = STARTUPINFOW { + cb: size_of::() as u32, + wShowWindow: if rdm_app_start_msg.is_maximized() { SW_MAXIMIZE.0 as u16 } else { SW_RESTORE.0 as u16 }, + dwFlags: windows::Win32::System::Threading::STARTF_USESHOWWINDOW, + ..Default::default() + }; + + let mut process_info = PROCESS_INFORMATION::default(); + + // Create a mutable copy of the command line for CreateProcessW + let mut command_line_buffer: Vec = format!("\"{}\"", rdm_exe_path.display()) + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + // SAFETY: All pointers are valid and properly initialized + let success = unsafe { + CreateProcessW( + None, // lpApplicationName + Some(PWSTR(command_line_buffer.as_mut_ptr())), // lpCommandLine + None, // lpProcessAttributes + None, // lpThreadAttributes + false.into(), // bInheritHandles + CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags + Some(env_block.as_ptr() as *const std::ffi::c_void), // lpEnvironment + PCWSTR(current_dir.as_pcwstr().as_ptr()), // lpCurrentDirectory + &startup_info, // lpStartupInfo + &mut process_info, // lpProcessInformation + ) + }; + + if success.is_err() { + let error = win_api_wrappers::Error::last_error(); + let error_msg = format!("Failed to start RDM application: {}", error); + Err(error_msg) + } else { + // Extract the handles and process ID immediately as raw values + Ok((process_info.hProcess.0 as usize, process_info.dwProcessId, process_info.hThread.0 as usize)) + } + }; + + let (process_handle_raw, process_id, thread_handle_raw) = match create_process_result { + Ok(result) => result, + Err(error_msg) => { + error!("{}", error_msg); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; + bail!(error_msg); + } + }; + + // Handle any errors from process creation + if process_handle_raw == 0 { + let error_msg = "Failed to start RDM application: Invalid process handle"; + error!("{}", error_msg); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; + bail!(error_msg); + } + + // Close thread handle as we don't need it + let thread_handle = windows::Win32::Foundation::HANDLE(thread_handle_raw as *mut std::ffi::c_void); + unsafe { let _ = CloseHandle(thread_handle); }; + + // Create RAII wrapper for process handle + let process_handle = windows::Win32::Foundation::HANDLE(process_handle_raw as *mut std::ffi::c_void); + let rdm_handle = RdmProcessHandle::new(process_handle); + + // Set process spawned status + self.rdm_process_spawned.store(true, Ordering::Release); + + info!("RDM application started successfully with PID: {}", process_id); + + // Send ready notification + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::READY, NowRdmReason::NOT_SPECIFIED).await?; + + // Spawn task to monitor the process + let dvc_tx = self.dvc_tx.clone(); + let spawned_status = self.rdm_process_spawned.clone(); + + tokio::task::spawn_blocking(move || { + monitor_rdm_process(dvc_tx, rdm_handle, process_id, spawned_status); + }); + + Ok(()) + } + + async fn process_rdm_app_action(&mut self, rdm_app_action_msg: NowRdmAppActionMsg<'_>) -> anyhow::Result<()> { + let action = rdm_app_action_msg.app_action(); + info!(?action, "Processing RDM app action message"); + + // Find the running RDM process + let process_id = match find_rdm_pid() { + Some(pid) => pid, + None => { + warn!("No running RDM process found for action"); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::NOT_SPECIFIED).await?; + bail!("RDM application is not running"); + } + }; + + match action { + NowRdmAppAction::CLOSE => { + info!(process_id, "Closing RDM application"); + + // Send WM_CLOSE message to all RDM windows + let window_count = send_message_to_all_windows(process_id, WM_CLOSE, WPARAM(0), LPARAM(0)); + + if window_count == 0 { + // If no windows found, try process termination + if let Ok(process) = Process::get_by_pid(process_id, PROCESS_QUERY_INFORMATION) { + let handle = process.handle(); + unsafe { + let _ = TerminateProcess(handle.raw(), 0); + } + info!("Terminated RDM process"); + } + } + + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::CLOSED, NowRdmReason::NOT_SPECIFIED).await?; + } + NowRdmAppAction::MINIMIZE => { + info!(process_id, "Minimizing RDM application"); + let window_count = exec_window_command(process_id, WindowCommand::Minimize); + + if window_count == 0 { + warn!("No windows found for minimize action"); + } + } + NowRdmAppAction::MAXIMIZE => { + info!(process_id, "Maximizing RDM application"); + let window_count = exec_window_command(process_id, WindowCommand::Maximize); + + if window_count == 0 { + warn!("No windows found for maximize action"); + } + } + NowRdmAppAction::RESTORE => { + info!(process_id, "Restoring RDM application"); + let window_count = exec_window_command(process_id, WindowCommand::Restore); + + if window_count == 0 { + warn!("No windows found for restore action"); + } + } + NowRdmAppAction::FULLSCREEN => { + info!(process_id, "Toggling RDM fullscreen mode"); + // For fullscreen toggle, we would need to send a specific message or key combination + // This depends on RDM's specific implementation - for now just log + warn!("Fullscreen toggle not yet implemented - requires RDM-specific message protocol"); + } + _ => { + warn!(?action, "Unsupported RDM app action"); + bail!("Unsupported RDM application action"); + } + } + + Ok(()) + } } fn append_ps_args(args: &mut Vec, msg: &NowExecWinPsMsg<'_>) { @@ -988,6 +1331,291 @@ fn get_focused_window() -> anyhow::Result { Ok(focused_window) } + + +/// Check if RDM process is already running by comparing executable paths +fn is_rdm_running() -> bool { + // Get RDM installation path internally + let rdm_exe_path = match get_rdm_executable_path() { + Some(path) => path, + None => { + warn!("Could not determine RDM executable path for process detection"); + return false; + } + }; + + match ProcessEntry32Iterator::new() { + Ok(process_iter) => { + for process_entry in process_iter { + let pid = process_entry.process_id(); + if let Ok(process) = Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { + if let Ok(exe_path) = process.exe_path() { + // Compare the full paths case-insensitively + if exe_path.to_string_lossy().to_lowercase() + == rdm_exe_path.to_string_lossy().to_lowercase() { + info!( + rdm_path = %rdm_exe_path.display(), + found_path = %exe_path.display(), + "Found already running RDM process" + ); + return true; + } + } + } + } + false + } + Err(error) => { + warn!(%error, "Failed to enumerate processes for RDM detection"); + false + } + } +} + +/// Get the RDM executable path from installation location +fn get_rdm_executable_path() -> Option { + let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID).ok()?; + + match get_install_location(update_code_uuid) { + Ok(Some(install_location)) => { + let rdm_exe_path = std::path::Path::new(&install_location).join("RemoteDesktopManager.exe"); + Some(rdm_exe_path) + } + Ok(None) => None, + Err(_) => None, + } +} + +/// Get RDM executable path with proper error handling for startup scenarios +fn get_rdm_exe_path() -> anyhow::Result> { + let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID) + .context("Failed to parse RDM update code UUID")?; + + let install_location = match get_install_location(update_code_uuid) { + Ok(Some(location)) => location, + Ok(None) => { + return Ok(None); // RDM is not installed + } + Err(error) => { + bail!("Failed to get RDM installation location: {}", error); + } + }; + + let rdm_exe_path = std::path::Path::new(&install_location).join("RemoteDesktopManager.exe"); + + if !rdm_exe_path.exists() { + bail!("RDM executable not found at: {}", rdm_exe_path.display()); + } + + Ok(Some(rdm_exe_path)) +}/// Send RDM app notification message +async fn send_rdm_app_notify( + dvc_tx: &WinapiSignaledSender>, + state: NowRdmAppState, + reason: NowRdmReason, +) -> anyhow::Result<()> { + info!(?state, ?reason, "Sending RDM app state notification"); + + let message = NowRdmAppNotifyMsg::new(state, reason); + dvc_tx.send(NowMessage::Rdm(NowRdmMessage::AppNotify(message))).await?; + Ok(()) +} + +/// Monitor RDM process and send notifications when state changes +fn monitor_rdm_process( + dvc_tx: WinapiSignaledSender>, + rdm_handle: RdmProcessHandle, + process_id: u32, + spawned_status: Arc, +) { + info!(process_id, "Starting RDM process monitor"); + + // Wait for process to exit + let wait_result = unsafe { WaitForSingleObject(rdm_handle.handle(), INFINITE) }; + + // Check if the wait was successful (process exited) + if wait_result == WAIT_OBJECT_0 { + info!(process_id, "RDM process has exited"); + // Send closed notification - we need to block on this since we're in sync context + let rt = tokio::runtime::Handle::current(); + if let Err(error) = rt.block_on(send_rdm_app_notify(&dvc_tx, NowRdmAppState::CLOSED, NowRdmReason::NOT_SPECIFIED)) { + error!(%error, "Failed to send RDM app closed notification"); + } + } else { + error!(process_id, wait_event = ?wait_result, "Failed to wait for RDM process"); + } + + // Clear the spawned status since our spawned process has exited + spawned_status.store(false, Ordering::Release); + + // The rdm_handle will be automatically closed when it goes out of scope +} + + + +/// Find running RDM process and return its process ID +fn find_rdm_pid() -> Option { + // Get RDM installation path internally + let rdm_exe_path = get_rdm_executable_path()?; + + match ProcessEntry32Iterator::new() { + Ok(process_iter) => { + for process_entry in process_iter { + let pid = process_entry.process_id(); + if let Ok(process) = Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { + if let Ok(exe_path) = process.exe_path() { + // Compare the full paths case-insensitively + if exe_path.to_string_lossy().to_lowercase() + == rdm_exe_path.to_string_lossy().to_lowercase() { + + info!( + rdm_path = %rdm_exe_path.display(), + found_path = %exe_path.display(), + process_id = pid, + "Found running RDM process" + ); + + return Some(pid); + } + } + } + } + None + } + Err(error) => { + warn!(%error, "Failed to enumerate processes for RDM detection"); + None + } + } +} + +/// Send a message to all windows belonging to a specific process +fn send_message_to_all_windows(process_id: u32, message: u32, wparam: WPARAM, lparam: LPARAM) -> u32 { + // Context for window enumeration callback + struct MessageSendContext { + target_process_id: u32, + message: u32, + wparam: WPARAM, + lparam: LPARAM, + window_count: u32, + } + + unsafe extern "system" fn enum_windows_proc( + hwnd: HWND, + lparam: LPARAM + ) -> windows::core::BOOL { + unsafe { + let context = &mut *(lparam.0 as *mut MessageSendContext); + + // Get the process ID of this window + let mut window_process_id = 0u32; + let _ = GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)); + + // Check if this window belongs to our target process + if window_process_id == context.target_process_id { + // Send message directly to this window + let _ = PostMessageW(Some(hwnd), context.message, context.wparam, context.lparam); + context.window_count += 1; + info!( + process_id = context.target_process_id, + window_handle = hwnd.0 as isize, + message = context.message, + "Sent message to RDM window" + ); + } + + windows::core::BOOL(1) // Continue enumeration to find all windows + } + } + + let mut context = MessageSendContext { + target_process_id: process_id, + message, + wparam, + lparam, + window_count: 0, + }; + + unsafe { + let _ = EnumWindows( + Some(enum_windows_proc), + LPARAM(&mut context as *mut _ as isize), + ); + } + + if context.window_count > 0 { + info!(process_id, window_count = context.window_count, "Sent message to RDM windows"); + } else { + warn!(process_id, "Could not find any windows for RDM process"); + } + + context.window_count +} + +/// Execute a window command on all windows belonging to a specific process +fn exec_window_command(process_id: u32, command: WindowCommand) -> u32 { + // Context for window enumeration callback + struct WindowCommandContext { + target_process_id: u32, + command: WindowCommand, + window_count: u32, + } + + unsafe extern "system" fn enum_windows_proc( + hwnd: HWND, + lparam: LPARAM + ) -> windows::core::BOOL { + unsafe { + let context = &mut *(lparam.0 as *mut WindowCommandContext); + + // Get the process ID of this window + let mut window_process_id = 0u32; + let _ = GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)); + + // Check if this window belongs to our target process + if window_process_id == context.target_process_id { + // Apply ShowWindow command directly to this window + let show_command = match context.command { + WindowCommand::Minimize => SW_MINIMIZE, + WindowCommand::Maximize => SW_SHOWMAXIMIZED, + WindowCommand::Restore => SW_SHOWNORMAL, + }; + let _ = ShowWindow(hwnd, show_command); + context.window_count += 1; + info!( + process_id = context.target_process_id, + window_handle = hwnd.0 as isize, + command = ?context.command, + "Applied window command to RDM window" + ); + } + + windows::core::BOOL(1) // Continue enumeration to find all windows + } + } + + let mut context = WindowCommandContext { + target_process_id: process_id, + command, + window_count: 0, + }; + + unsafe { + let _ = EnumWindows( + Some(enum_windows_proc), + LPARAM(&mut context as *mut _ as isize), + ); + } + + if context.window_count > 0 { + info!(process_id, window_count = context.window_count, command = ?command, "Applied window command to RDM windows"); + } else { + warn!(process_id, "Could not find any windows for RDM process"); + } + + context.window_count +} + #[link(name = "user32", kind = "dylib")] unsafe extern "C" { #[link_name = "MessageBoxTimeoutW"] diff --git a/devolutions-session/src/main.rs b/devolutions-session/src/main.rs index e8930864e..86c8d4773 100644 --- a/devolutions-session/src/main.rs +++ b/devolutions-session/src/main.rs @@ -8,7 +8,7 @@ use ::{ }; #[cfg(all(windows, feature = "dvc"))] -use ::{async_trait as _, now_proto_pdu as _, tempfile as _, thiserror as _, win_api_wrappers as _, windows as _}; +use ::{async_trait as _, now_proto_pdu as _, tempfile as _, thiserror as _, win_api_wrappers as _, windows as _, uuid as _, devolutions_agent_shared as _}; #[macro_use] extern crate tracing; From 9241bee9681bc911bb731511034f479645e44afc Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Tue, 4 Nov 2025 18:09:26 +0200 Subject: [PATCH 3/3] [WIP] refactoring --- Cargo.lock | 1 - .../src/windows/mod.rs | 4 + .../src/windows/registry.rs | 5 +- devolutions-session/Cargo.toml | 1 - devolutions-session/src/dvc/task.rs | 684 ++++++++---------- devolutions-session/src/main.rs | 5 +- 6 files changed, 314 insertions(+), 386 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5faa1f22c..863c5025a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1450,7 +1450,6 @@ dependencies = [ "thiserror 2.0.16", "tokio 1.46.1", "tracing", - "uuid", "win-api-wrappers", "windows 0.61.3", ] diff --git a/crates/devolutions-agent-shared/src/windows/mod.rs b/crates/devolutions-agent-shared/src/windows/mod.rs index e4c90d190..b3d3b3ab6 100644 --- a/crates/devolutions-agent-shared/src/windows/mod.rs +++ b/crates/devolutions-agent-shared/src/windows/mod.rs @@ -16,3 +16,7 @@ pub const GATEWAY_UPDATE_CODE: Uuid = uuid!("{db3903d6-c451-4393-bd80-eb9f45b902 /// /// See [`GATEWAY_UPDATE_CODE`] for more information on update codes. pub const AGENT_UPDATE_CODE: Uuid = uuid!("{82318d3c-811f-4d5d-9a82-b7c31b076755}"); +/// MSI upgrade code for the Remote Desktop Manager. +/// +/// See [`GATEWAY_UPDATE_CODE`] for more information on update codes. +pub const RDM_UPDATE_CODE: Uuid = uuid!("{2707F3BF-4D7B-40C2-882F-14B0ED869EE8}"); diff --git a/crates/devolutions-agent-shared/src/windows/registry.rs b/crates/devolutions-agent-shared/src/windows/registry.rs index 65611ae24..140867724 100644 --- a/crates/devolutions-agent-shared/src/windows/registry.rs +++ b/crates/devolutions-agent-shared/src/windows/registry.rs @@ -55,7 +55,10 @@ pub enum ProductVersionEncoding { /// Get the installed version of a product using Windows registry. Returns `None` if the product /// is not installed. -pub fn get_installed_product_version(update_code: Uuid, version_encoding: ProductVersionEncoding) -> Result, RegistryError> { +pub fn get_installed_product_version( + update_code: Uuid, + version_encoding: ProductVersionEncoding, +) -> Result, RegistryError> { let product_code_uuid = match get_product_code(update_code)? { Some(uuid) => uuid, None => return Ok(None), diff --git a/devolutions-session/Cargo.toml b/devolutions-session/Cargo.toml index 647754fe9..7d0568845 100644 --- a/devolutions-session/Cargo.toml +++ b/devolutions-session/Cargo.toml @@ -48,7 +48,6 @@ version = "0.4.0" features = ["std"] [target.'cfg(windows)'.dependencies] -uuid = { version = "1.0", features = ["v4", "serde"] } devolutions-agent-shared = { path = "../crates/devolutions-agent-shared" } [target.'cfg(windows)'.build-dependencies] diff --git a/devolutions-session/src/dvc/task.rs b/devolutions-session/src/dvc/task.rs index 2f295550e..1588a43b4 100644 --- a/devolutions-session/src/dvc/task.rs +++ b/devolutions-session/src/dvc/task.rs @@ -1,53 +1,54 @@ use std::collections::HashMap; -use std::time::{SystemTime, UNIX_EPOCH}; use std::mem::size_of; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{Context, bail}; use async_trait::async_trait; use tokio::select; use tokio::sync::mpsc::{self, Receiver, Sender}; -use windows::Win32::Foundation::{HWND, LPARAM, WPARAM, CloseHandle}; +use windows::Win32::Foundation::WAIT_OBJECT_0; +use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; use windows::Win32::Security::{TOKEN_ADJUST_PRIVILEGES, TOKEN_QUERY}; use windows::Win32::System::Shutdown::{ EWX_FORCE, EWX_LOGOFF, EWX_POWEROFF, EWX_REBOOT, ExitWindowsEx, InitiateSystemShutdownW, LockWorkStation, SHUTDOWN_REASON, }; -use windows::Win32::System::Threading::{AttachThreadInput, GetCurrentThreadId, CreateProcessW, WaitForSingleObject, PROCESS_INFORMATION, STARTUPINFOW, CREATE_UNICODE_ENVIRONMENT, INFINITE, PROCESS_QUERY_INFORMATION, TerminateProcess}; -use windows::Win32::Foundation::WAIT_OBJECT_0; +use windows::Win32::System::Threading::{ + AttachThreadInput, CREATE_UNICODE_ENVIRONMENT, CreateProcessW, GetCurrentThreadId, PROCESS_INFORMATION, + PROCESS_QUERY_INFORMATION, STARTUPINFOW, +}; use windows::Win32::UI::Input::KeyboardAndMouse::GetFocus; use windows::Win32::UI::Shell::ShellExecuteW; use windows::Win32::UI::WindowsAndMessaging::{ - GetForegroundWindow, GetWindowThreadProcessId, HKL_NEXT, HKL_PREV, MESSAGEBOX_RESULT, MESSAGEBOX_STYLE, - MessageBoxW, PostMessageW, SW_RESTORE, SW_MAXIMIZE, SW_MINIMIZE, SW_SHOWMAXIMIZED, SW_SHOWNORMAL, - ShowWindow, WM_INPUTLANGCHANGEREQUEST, WM_CLOSE, EnumWindows, + EnumWindows, GetForegroundWindow, GetWindowThreadProcessId, HKL_NEXT, HKL_PREV, MESSAGEBOX_RESULT, + MESSAGEBOX_STYLE, MessageBoxW, PostMessageW, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWMAXIMIZED, SW_SHOWNORMAL, + ShowWindow, WM_CLOSE, WM_INPUTLANGCHANGEREQUEST, }; use windows::core::{PCWSTR, PWSTR}; - use devolutions_gateway_task::Task; use now_proto_pdu::ironrdp_core::IntoOwned; use now_proto_pdu::{ ComApartmentStateKind, NowChannelCapsetMsg, NowChannelCloseMsg, NowChannelHeartbeatMsg, NowChannelMessage, NowExecBatchMsg, NowExecCancelRspMsg, NowExecCapsetFlags, NowExecDataMsg, NowExecDataStreamKind, NowExecMessage, NowExecProcessMsg, NowExecPwshMsg, NowExecResultMsg, NowExecRunMsg, NowExecStartedMsg, NowExecWinPsMsg, NowMessage, - NowMsgBoxResponse, NowProtoError, NowProtoVersion, NowRdmAppActionMsg, NowRdmAppAction, NowRdmAppNotifyMsg, NowRdmAppStartMsg, NowRdmCapabilitiesMsg, NowRdmMessage, NowRdmAppState, NowRdmReason, NowSessionCapsetFlags, NowSessionMessage, - NowSessionMsgBoxReqMsg, NowSessionMsgBoxRspMsg, NowStatusError, NowSystemCapsetFlags, NowSystemMessage, - SetKbdLayoutOption, + NowMsgBoxResponse, NowProtoError, NowProtoVersion, NowRdmAppAction, NowRdmAppActionMsg, NowRdmAppNotifyMsg, + NowRdmAppStartMsg, NowRdmAppState, NowRdmCapabilitiesMsg, NowRdmMessage, NowRdmReason, NowSessionCapsetFlags, + NowSessionMessage, NowSessionMsgBoxReqMsg, NowSessionMsgBoxRspMsg, NowStatusError, NowSystemCapsetFlags, + NowSystemMessage, SetKbdLayoutOption, }; use win_api_wrappers::event::Event; +use win_api_wrappers::handle::Handle; +use win_api_wrappers::process::{Process, ProcessEntry32Iterator}; use win_api_wrappers::security::privilege::ScopedPrivileges; use win_api_wrappers::utils::WideString; -use win_api_wrappers::process::{Process, ProcessEntry32Iterator}; -use win_api_wrappers::handle::HandleWrapper; - -use devolutions_agent_shared::windows::registry::{get_install_location, get_installed_product_version, ProductVersionEncoding}; -use uuid::Uuid; - -const RDM_UPDATE_CODE_UUID: &str = "2707F3BF-4D7B-40C2-882F-14B0ED869EE8"; - +use devolutions_agent_shared::windows::RDM_UPDATE_CODE; +use devolutions_agent_shared::windows::registry::{ + ProductVersionEncoding, get_install_location, get_installed_product_version, +}; use crate::dvc::channel::{WinapiSignaledSender, bounded_mpsc_channel, winapi_signaled_mpsc_channel}; @@ -315,33 +316,6 @@ struct MessageProcessor { rdm_process_spawned: Arc, } -/// RAII wrapper for RDM process handle -struct RdmProcessHandle { - handle: windows::Win32::Foundation::HANDLE, -} - -// SAFETY: HANDLE is just a pointer and can be safely sent between threads -unsafe impl Send for RdmProcessHandle {} -unsafe impl Sync for RdmProcessHandle {} - -impl RdmProcessHandle { - fn new(handle: windows::Win32::Foundation::HANDLE) -> Self { - Self { handle } - } - - fn handle(&self) -> windows::Win32::Foundation::HANDLE { - self.handle - } -} - -impl Drop for RdmProcessHandle { - fn drop(&mut self) { - unsafe { - let _ = CloseHandle(self.handle); - } - } -} - impl MessageProcessor { pub(crate) fn new( capabilities: NowChannelCapsetMsg, @@ -509,8 +483,8 @@ impl MessageProcessor { } } NowMessage::System(NowSystemMessage::Shutdown(shutdown_msg)) => { - let mut current_process_token = Process::current_process() - .token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?; + let mut current_process_token = + Process::current_process().token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?; let mut _priv_tcb = ScopedPrivileges::enter( &mut current_process_token, &[win_api_wrappers::security::privilege::SE_SHUTDOWN_NAME], @@ -787,20 +761,24 @@ impl MessageProcessor { self.sessions.clear(); } - async fn process_rdm_capabilities(&self, rdm_caps_msg: NowRdmCapabilitiesMsg<'_>) -> anyhow::Result> { + async fn process_rdm_capabilities( + &self, + rdm_caps_msg: NowRdmCapabilitiesMsg<'_>, + ) -> anyhow::Result> { let client_timestamp = rdm_caps_msg.timestamp(); let server_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .context("Failed to get current timestamp")? .as_secs(); - info!(client_timestamp, server_timestamp, "Processing RDM capabilities message"); + info!( + client_timestamp, + server_timestamp, "Processing RDM capabilities message" + ); // Check if RDM is available by looking for the installation let (is_rdm_available, rdm_version) = { - let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID) - .context("Failed to parse RDM update code UUID")?; - match get_installed_product_version(update_code_uuid, ProductVersionEncoding::Rdm) { + match get_installed_product_version(RDM_UPDATE_CODE, ProductVersionEncoding::Rdm) { Ok(Some(date_version)) => { info!(version = %date_version, "RDM installation found via MSI registry"); (true, date_version.to_string()) @@ -841,137 +819,127 @@ impl MessageProcessor { } // Get RDM executable path with proper error handling - let rdm_exe_path = match get_rdm_exe_path() { - Ok(Some(path)) => path, - Ok(None) => { + let rdm_exe_path = match get_rdm_executable_path() { + Some(path) => path, + None => { error!("RDM is not installed - cannot start application"); send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::NOT_INSTALLED).await?; bail!("RDM is not installed"); } - Err(error) => { - error!("Failed to get RDM executable path: {}", error); - send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; - return Err(error); - } }; - let install_location = rdm_exe_path.parent() + let install_location = rdm_exe_path + .parent() .context("Failed to get RDM installation directory")? .to_string_lossy() .to_string(); - // Build environment variables for fullscreen and jump mode - let mut env_vars = HashMap::new(); - - if rdm_app_start_msg.is_fullscreen() { - env_vars.insert("RDM_OPT_FULLSCREEN".to_string(), "1".to_string()); - info!("Starting RDM in fullscreen mode"); - } + // Build environment variables for fullscreen and jump mode + let mut env_vars = HashMap::new(); - if rdm_app_start_msg.is_jump_mode() { - env_vars.insert("RDM_OPT_JUMP".to_string(), "1".to_string()); - info!("Starting RDM in jump mode"); - } - - // Create environment block - let env_block = crate::dvc::env::make_environment_block(env_vars)?; + if rdm_app_start_msg.is_fullscreen() { + env_vars.insert("RDM_OPT_FULLSCREEN".to_string(), "1".to_string()); + info!("Starting RDM in fullscreen mode"); + } - // Convert command line to wide string - let current_dir = WideString::from(&install_location); + if rdm_app_start_msg.is_jump_mode() { + env_vars.insert("RDM_OPT_JUMP".to_string(), "1".to_string()); + info!("Starting RDM in jump mode"); + } - info!( - exe_path = %rdm_exe_path.display(), - fullscreen = rdm_app_start_msg.is_fullscreen(), - maximized = rdm_app_start_msg.is_maximized(), - jump_mode = rdm_app_start_msg.is_jump_mode(), - "Starting RDM application with CreateProcess" - ); + // Create environment block + let env_block = crate::dvc::env::make_environment_block(env_vars)?; - // Create process using CreateProcessW in a scoped block - let create_process_result = { - let startup_info = STARTUPINFOW { - cb: size_of::() as u32, - wShowWindow: if rdm_app_start_msg.is_maximized() { SW_MAXIMIZE.0 as u16 } else { SW_RESTORE.0 as u16 }, - dwFlags: windows::Win32::System::Threading::STARTF_USESHOWWINDOW, - ..Default::default() - }; + // Convert command line to wide string + let current_dir = WideString::from(&install_location); - let mut process_info = PROCESS_INFORMATION::default(); - - // Create a mutable copy of the command line for CreateProcessW - let mut command_line_buffer: Vec = format!("\"{}\"", rdm_exe_path.display()) - .encode_utf16() - .chain(std::iter::once(0)) - .collect(); - - // SAFETY: All pointers are valid and properly initialized - let success = unsafe { - CreateProcessW( - None, // lpApplicationName - Some(PWSTR(command_line_buffer.as_mut_ptr())), // lpCommandLine - None, // lpProcessAttributes - None, // lpThreadAttributes - false.into(), // bInheritHandles - CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags - Some(env_block.as_ptr() as *const std::ffi::c_void), // lpEnvironment - PCWSTR(current_dir.as_pcwstr().as_ptr()), // lpCurrentDirectory - &startup_info, // lpStartupInfo - &mut process_info, // lpProcessInformation - ) - }; + info!( + exe_path = %rdm_exe_path.display(), + fullscreen = rdm_app_start_msg.is_fullscreen(), + maximized = rdm_app_start_msg.is_maximized(), + jump_mode = rdm_app_start_msg.is_jump_mode(), + "Starting RDM application with CreateProcess" + ); - if success.is_err() { - let error = win_api_wrappers::Error::last_error(); - let error_msg = format!("Failed to start RDM application: {}", error); - Err(error_msg) + // Create process using CreateProcessW in a scoped block + let create_process_result = { + let startup_info = STARTUPINFOW { + cb: size_of::() as u32, + wShowWindow: if rdm_app_start_msg.is_maximized() { + SW_MAXIMIZE.0 as u16 } else { - // Extract the handles and process ID immediately as raw values - Ok((process_info.hProcess.0 as usize, process_info.dwProcessId, process_info.hThread.0 as usize)) - } + SW_RESTORE.0 as u16 + }, + dwFlags: windows::Win32::System::Threading::STARTF_USESHOWWINDOW, + ..Default::default() }; - let (process_handle_raw, process_id, thread_handle_raw) = match create_process_result { - Ok(result) => result, - Err(error_msg) => { - error!("{}", error_msg); - send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; - bail!(error_msg); - } + let mut process_info = PROCESS_INFORMATION::default(); + + // Create a mutable copy of the command line for CreateProcessW + let mut command_line_buffer: Vec = format!("\"{}\"", rdm_exe_path.display()) + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + // SAFETY: All pointers are valid and properly initialized + let success = unsafe { + CreateProcessW( + None, // lpApplicationName + Some(PWSTR(command_line_buffer.as_mut_ptr())), // lpCommandLine + None, // lpProcessAttributes + None, // lpThreadAttributes + false.into(), // bInheritHandles + CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags + Some(env_block.as_ptr() as *const std::ffi::c_void), // lpEnvironment + PCWSTR(current_dir.as_pcwstr().as_ptr()), // lpCurrentDirectory + &startup_info, // lpStartupInfo + &mut process_info, // lpProcessInformation + ) }; - // Handle any errors from process creation - if process_handle_raw == 0 { - let error_msg = "Failed to start RDM application: Invalid process handle"; - error!("{}", error_msg); - send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; - bail!(error_msg); - } + if success.is_err() || process_info.hProcess.is_invalid() { + Err(win_api_wrappers::Error::last_error()) + } else { + // Close thread handle as we don't need it - // Close thread handle as we don't need it - let thread_handle = windows::Win32::Foundation::HANDLE(thread_handle_raw as *mut std::ffi::c_void); - unsafe { let _ = CloseHandle(thread_handle); }; + // SAFETY: It is create owned handle wrapper from created process thread handle + let _ = unsafe { Handle::new(process_info.hThread, true) }; - // Create RAII wrapper for process handle - let process_handle = windows::Win32::Foundation::HANDLE(process_handle_raw as *mut std::ffi::c_void); - let rdm_handle = RdmProcessHandle::new(process_handle); + // SAFETY: It is safe to create owned handle wrapper from created process handle + let rdm_handle: Result = + unsafe { Handle::new(process_info.hProcess, true) }.map(Into::into); + + rdm_handle.map(|rdm_handle| (rdm_handle, process_info.dwProcessId)) + } + }; + + let (rdm_handle, process_id) = match create_process_result { + Ok(result) => result, + Err(error) => { + error!(%error, "Failed to start RDM application"); + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::FAILED, NowRdmReason::STARTUP_FAILURE).await?; + return Err(error.into()); + } + }; - // Set process spawned status - self.rdm_process_spawned.store(true, Ordering::Release); + // Set process spawned status + self.rdm_process_spawned.store(true, Ordering::Release); - info!("RDM application started successfully with PID: {}", process_id); + info!("RDM application started successfully with PID: {}", process_id); - // Send ready notification - send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::READY, NowRdmReason::NOT_SPECIFIED).await?; + // Send ready notification + send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::READY, NowRdmReason::NOT_SPECIFIED).await?; - // Spawn task to monitor the process - let dvc_tx = self.dvc_tx.clone(); - let spawned_status = self.rdm_process_spawned.clone(); + // Spawn task to monitor the process + let dvc_tx = self.dvc_tx.clone(); + let spawned_status = self.rdm_process_spawned.clone(); - tokio::task::spawn_blocking(move || { - monitor_rdm_process(dvc_tx, rdm_handle, process_id, spawned_status); - }); + tokio::task::spawn_blocking(move || { + monitor_rdm_process(dvc_tx, rdm_handle, process_id, spawned_status); + }); - Ok(()) + Ok(()) } async fn process_rdm_app_action(&mut self, rdm_app_action_msg: NowRdmAppActionMsg<'_>) -> anyhow::Result<()> { @@ -993,44 +961,22 @@ impl MessageProcessor { info!(process_id, "Closing RDM application"); // Send WM_CLOSE message to all RDM windows - let window_count = send_message_to_all_windows(process_id, WM_CLOSE, WPARAM(0), LPARAM(0)); - - if window_count == 0 { - // If no windows found, try process termination - if let Ok(process) = Process::get_by_pid(process_id, PROCESS_QUERY_INFORMATION) { - let handle = process.handle(); - unsafe { - let _ = TerminateProcess(handle.raw(), 0); - } - info!("Terminated RDM process"); - } - } + send_message_to_all_windows(process_id, WM_CLOSE, WPARAM(0), LPARAM(0)); + // TODO: task to monitor if RDM is running send_rdm_app_notify(&self.dvc_tx, NowRdmAppState::CLOSED, NowRdmReason::NOT_SPECIFIED).await?; } NowRdmAppAction::MINIMIZE => { info!(process_id, "Minimizing RDM application"); - let window_count = exec_window_command(process_id, WindowCommand::Minimize); - - if window_count == 0 { - warn!("No windows found for minimize action"); - } + exec_window_command(process_id, WindowCommand::Minimize) } NowRdmAppAction::MAXIMIZE => { info!(process_id, "Maximizing RDM application"); - let window_count = exec_window_command(process_id, WindowCommand::Maximize); - - if window_count == 0 { - warn!("No windows found for maximize action"); - } + exec_window_command(process_id, WindowCommand::Maximize); } NowRdmAppAction::RESTORE => { info!(process_id, "Restoring RDM application"); - let window_count = exec_window_command(process_id, WindowCommand::Restore); - - if window_count == 0 { - warn!("No windows found for restore action"); - } + exec_window_command(process_id, WindowCommand::Restore); } NowRdmAppAction::FULLSCREEN => { info!(process_id, "Toggling RDM fullscreen mode"); @@ -1331,8 +1277,6 @@ fn get_focused_window() -> anyhow::Result { Ok(focused_window) } - - /// Check if RDM process is already running by comparing executable paths fn is_rdm_running() -> bool { // Get RDM installation path internally @@ -1344,39 +1288,43 @@ fn is_rdm_running() -> bool { } }; - match ProcessEntry32Iterator::new() { - Ok(process_iter) => { - for process_entry in process_iter { - let pid = process_entry.process_id(); - if let Ok(process) = Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { - if let Ok(exe_path) = process.exe_path() { - // Compare the full paths case-insensitively - if exe_path.to_string_lossy().to_lowercase() - == rdm_exe_path.to_string_lossy().to_lowercase() { - info!( - rdm_path = %rdm_exe_path.display(), - found_path = %exe_path.display(), - "Found already running RDM process" - ); - return true; - } - } - } - } - false - } + let process_iterator = match ProcessEntry32Iterator::new() { + Ok(iter) => iter, Err(error) => { - warn!(%error, "Failed to enumerate processes for RDM detection"); - false + warn!(%error, "Failed to create process iterator for RDM detection"); + return false; + } + }; + + for process_entry in process_iterator { + let pid = process_entry.process_id(); + + let process = match Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { + Ok(proc) => proc, + Err(_) => continue, + }; + + let exe_path = match process.exe_path() { + Ok(path) => path, + Err(_) => continue, + }; + + // Compare the full paths case-insensitively + if exe_path == rdm_exe_path { + info!( + rdm_path = %rdm_exe_path.display(), + found_path = %exe_path.display(), + "Found already running RDM process" + ); + return true; } } + false } /// Get the RDM executable path from installation location fn get_rdm_executable_path() -> Option { - let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID).ok()?; - - match get_install_location(update_code_uuid) { + match get_install_location(RDM_UPDATE_CODE) { Ok(Some(install_location)) => { let rdm_exe_path = std::path::Path::new(&install_location).join("RemoteDesktopManager.exe"); Some(rdm_exe_path) @@ -1386,29 +1334,7 @@ fn get_rdm_executable_path() -> Option { } } -/// Get RDM executable path with proper error handling for startup scenarios -fn get_rdm_exe_path() -> anyhow::Result> { - let update_code_uuid = Uuid::parse_str(RDM_UPDATE_CODE_UUID) - .context("Failed to parse RDM update code UUID")?; - - let install_location = match get_install_location(update_code_uuid) { - Ok(Some(location)) => location, - Ok(None) => { - return Ok(None); // RDM is not installed - } - Err(error) => { - bail!("Failed to get RDM installation location: {}", error); - } - }; - - let rdm_exe_path = std::path::Path::new(&install_location).join("RemoteDesktopManager.exe"); - - if !rdm_exe_path.exists() { - bail!("RDM executable not found at: {}", rdm_exe_path.display()); - } - - Ok(Some(rdm_exe_path)) -}/// Send RDM app notification message +/// Send RDM app notification message async fn send_rdm_app_notify( dvc_tx: &WinapiSignaledSender>, state: NowRdmAppState, @@ -1421,199 +1347,193 @@ async fn send_rdm_app_notify( Ok(()) } +/// Send RDM app notification message +fn send_rdm_app_notify_blocking( + dvc_tx: &WinapiSignaledSender>, + state: NowRdmAppState, + reason: NowRdmReason, +) -> anyhow::Result<()> { + info!(?state, ?reason, "Sending RDM app state notification"); + + let message = NowRdmAppNotifyMsg::new(state, reason); + dvc_tx.blocking_send(NowMessage::Rdm(NowRdmMessage::AppNotify(message)))?; + Ok(()) +} + /// Monitor RDM process and send notifications when state changes fn monitor_rdm_process( dvc_tx: WinapiSignaledSender>, - rdm_handle: RdmProcessHandle, - process_id: u32, + process: Process, + pid: u32, spawned_status: Arc, ) { - info!(process_id, "Starting RDM process monitor"); - - // Wait for process to exit - let wait_result = unsafe { WaitForSingleObject(rdm_handle.handle(), INFINITE) }; - - // Check if the wait was successful (process exited) - if wait_result == WAIT_OBJECT_0 { - info!(process_id, "RDM process has exited"); - // Send closed notification - we need to block on this since we're in sync context - let rt = tokio::runtime::Handle::current(); - if let Err(error) = rt.block_on(send_rdm_app_notify(&dvc_tx, NowRdmAppState::CLOSED, NowRdmReason::NOT_SPECIFIED)) { - error!(%error, "Failed to send RDM app closed notification"); + trace!(pid, "Starting RDM process monitor"); + match process.wait(None) { + Ok(wait_result) if wait_result == WAIT_OBJECT_0 => { + info!(pid, "RDM process has exited"); + let _ = send_rdm_app_notify_blocking(&dvc_tx, NowRdmAppState::CLOSED, NowRdmReason::NOT_SPECIFIED); + } + Ok(wait_result) => { + error!(pid, "Unexpected wait result for RDM process: {}", wait_result.0); + } + Err(error) => { + error!(%error, pid, "Failed to wait for RDM process"); } - } else { - error!(process_id, wait_event = ?wait_result, "Failed to wait for RDM process"); } // Clear the spawned status since our spawned process has exited spawned_status.store(false, Ordering::Release); - - // The rdm_handle will be automatically closed when it goes out of scope } - - /// Find running RDM process and return its process ID fn find_rdm_pid() -> Option { // Get RDM installation path internally let rdm_exe_path = get_rdm_executable_path()?; - match ProcessEntry32Iterator::new() { - Ok(process_iter) => { - for process_entry in process_iter { - let pid = process_entry.process_id(); - if let Ok(process) = Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { - if let Ok(exe_path) = process.exe_path() { - // Compare the full paths case-insensitively - if exe_path.to_string_lossy().to_lowercase() - == rdm_exe_path.to_string_lossy().to_lowercase() { - - info!( - rdm_path = %rdm_exe_path.display(), - found_path = %exe_path.display(), - process_id = pid, - "Found running RDM process" - ); - - return Some(pid); - } - } - } - } - None - } + let process_iterator = match ProcessEntry32Iterator::new() { + Ok(iter) => iter, Err(error) => { - warn!(%error, "Failed to enumerate processes for RDM detection"); - None + warn!(%error, "Failed to create process iterator for RDM detection"); + return None; } - } -} + }; -/// Send a message to all windows belonging to a specific process -fn send_message_to_all_windows(process_id: u32, message: u32, wparam: WPARAM, lparam: LPARAM) -> u32 { - // Context for window enumeration callback - struct MessageSendContext { - target_process_id: u32, - message: u32, - wparam: WPARAM, - lparam: LPARAM, - window_count: u32, - } + for process_entry in process_iterator { + let pid = process_entry.process_id(); - unsafe extern "system" fn enum_windows_proc( - hwnd: HWND, - lparam: LPARAM - ) -> windows::core::BOOL { - unsafe { - let context = &mut *(lparam.0 as *mut MessageSendContext); - - // Get the process ID of this window - let mut window_process_id = 0u32; - let _ = GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)); - - // Check if this window belongs to our target process - if window_process_id == context.target_process_id { - // Send message directly to this window - let _ = PostMessageW(Some(hwnd), context.message, context.wparam, context.lparam); - context.window_count += 1; - info!( - process_id = context.target_process_id, - window_handle = hwnd.0 as isize, - message = context.message, - "Sent message to RDM window" - ); - } + let process = match Process::get_by_pid(pid, PROCESS_QUERY_INFORMATION) { + Ok(proc) => proc, + Err(_) => continue, + }; - windows::core::BOOL(1) // Continue enumeration to find all windows + let exe_path = match process.exe_path() { + Ok(path) => path, + Err(_) => continue, + }; + + if exe_path == rdm_exe_path { + info!( + rdm_path = %rdm_exe_path.display(), + found_path = %exe_path.display(), + process_id = pid, + "Found running RDM process" + ); + + return Some(pid); } } + None +} - let mut context = MessageSendContext { - target_process_id: process_id, +// Context for window enumeration callback +struct SendMessageEnumContext { + process_id: u32, + message: u32, + wparam: WPARAM, + lparam: LPARAM, +} + +/// Send a message to all windows belonging to a specific process +fn send_message_to_all_windows(process_id: u32, message: u32, wparam: WPARAM, lparam: LPARAM) { + let context = Box::new(SendMessageEnumContext { + process_id, message, wparam, lparam, - window_count: 0, - }; + }); + // SAFETY: Context is a valid pointer to heap memory, threfore call is safe. unsafe { - let _ = EnumWindows( - Some(enum_windows_proc), - LPARAM(&mut context as *mut _ as isize), - ); + let _ = EnumWindows(Some(enum_windows_send_message), LPARAM(Box::into_raw(context) as isize)); } +} - if context.window_count > 0 { - info!(process_id, window_count = context.window_count, "Sent message to RDM windows"); - } else { - warn!(process_id, "Could not find any windows for RDM process"); +extern "system" fn enum_windows_send_message(hwnd: HWND, lparam: LPARAM) -> windows::core::BOOL { + // SAFETY: lparam is a valid pointer to MessageSendContext + let context = unsafe { Box::from_raw(lparam.0 as *mut SendMessageEnumContext) }; + + // Get the process ID of this window + let mut window_process_id = 0u32; + // SAFETY: hwnd is a valid window handle, lpdwprocessid is valid throughout + // GetWindowThreadProcessId call as it points to valid stack memory. + let get_pid_result = unsafe { GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)) }; + if get_pid_result == 0 { + warn!(window_handle = hwnd.0 as isize, "Failed to get process ID for window"); + // Continue enumeration + return true.into(); } - context.window_count -} - -/// Execute a window command on all windows belonging to a specific process -fn exec_window_command(process_id: u32, command: WindowCommand) -> u32 { - // Context for window enumeration callback - struct WindowCommandContext { - target_process_id: u32, - command: WindowCommand, - window_count: u32, + // Continue enumeration if process IDs do not match + if window_process_id != context.process_id { + return true.into(); } - unsafe extern "system" fn enum_windows_proc( - hwnd: HWND, - lparam: LPARAM - ) -> windows::core::BOOL { - unsafe { - let context = &mut *(lparam.0 as *mut WindowCommandContext); - - // Get the process ID of this window - let mut window_process_id = 0u32; - let _ = GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)); - - // Check if this window belongs to our target process - if window_process_id == context.target_process_id { - // Apply ShowWindow command directly to this window - let show_command = match context.command { - WindowCommand::Minimize => SW_MINIMIZE, - WindowCommand::Maximize => SW_SHOWMAXIMIZED, - WindowCommand::Restore => SW_SHOWNORMAL, - }; - let _ = ShowWindow(hwnd, show_command); - context.window_count += 1; - info!( - process_id = context.target_process_id, - window_handle = hwnd.0 as isize, - command = ?context.command, - "Applied window command to RDM window" - ); - } + // SAFETY: hwnd is a valid window handle, posting message is safe operation. + let _ = unsafe { PostMessageW(Some(hwnd), context.message, context.wparam, context.lparam) }; - windows::core::BOOL(1) // Continue enumeration to find all windows - } + info!( + process_id = context.process_id, + window_handle = hwnd.0 as isize, + message = context.message, + "Sent message to RDM window" + ); + + // Continue enumeration + true.into() +} + +// Context for window enumeration callback +struct WindowCommandEnumContext { + process_id: u32, + command: WindowCommand, +} + +unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> windows::core::BOOL { + // SAFETY: lparam is a valid pointer to WindowCommandEnumContext + let context = unsafe { Box::from_raw(lparam.0 as *mut WindowCommandEnumContext) }; + + // Get the process ID of this window + let mut window_process_id = 0u32; + // SAFETY: hwnd is a valid window handle, lpdwprocessid is valid throughout + // GetWindowThreadProcessId call as it points to valid stack memory. + let get_pid_result = unsafe { GetWindowThreadProcessId(hwnd, Some(&mut window_process_id)) }; + if get_pid_result == 0 { + warn!(window_handle = hwnd.0 as isize, "Failed to get process ID for window"); + // Continue enumeration + return true.into(); } - let mut context = WindowCommandContext { - target_process_id: process_id, - command, - window_count: 0, - }; + // Check if this window belongs to our target process + if window_process_id == context.process_id { + // Apply ShowWindow command directly to this window + let show_command = match context.command { + WindowCommand::Minimize => SW_MINIMIZE, + WindowCommand::Maximize => SW_SHOWMAXIMIZED, + WindowCommand::Restore => SW_SHOWNORMAL, + }; + // SAFETY: FFI call with no outstanding preconditions. + let _ = unsafe { ShowWindow(hwnd, show_command) }; - unsafe { - let _ = EnumWindows( - Some(enum_windows_proc), - LPARAM(&mut context as *mut _ as isize), + info!( + process_id = context.process_id, + window_handle = hwnd.0 as isize, + command = ?context.command, + "Applied window command to RDM window" ); } - if context.window_count > 0 { - info!(process_id, window_count = context.window_count, command = ?command, "Applied window command to RDM windows"); - } else { - warn!(process_id, "Could not find any windows for RDM process"); - } + // Continue enumeration + true.into() +} + +/// Execute a window command on all windows belonging to a specific process +fn exec_window_command(process_id: u32, command: WindowCommand) { + let context = Box::new(WindowCommandEnumContext { process_id, command }); - context.window_count + // SAFETY: Context is a valid pointer to heap memory, threfore call is safe. + unsafe { + let _ = EnumWindows(Some(enum_windows_proc), LPARAM(Box::into_raw(context) as isize)); + } } #[link(name = "user32", kind = "dylib")] diff --git a/devolutions-session/src/main.rs b/devolutions-session/src/main.rs index 86c8d4773..816dfa370 100644 --- a/devolutions-session/src/main.rs +++ b/devolutions-session/src/main.rs @@ -8,7 +8,10 @@ use ::{ }; #[cfg(all(windows, feature = "dvc"))] -use ::{async_trait as _, now_proto_pdu as _, tempfile as _, thiserror as _, win_api_wrappers as _, windows as _, uuid as _, devolutions_agent_shared as _}; +use ::{ + async_trait as _, devolutions_agent_shared as _, now_proto_pdu as _, tempfile as _, thiserror as _, + win_api_wrappers as _, windows as _, +}; #[macro_use] extern crate tracing;