From 056ec6a034d15ec45df31b12a3d694c1b0d2ddf0 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 14 Dec 2023 17:36:20 +0200 Subject: [PATCH] feat(session): graceful disconnection support (#336) --- crates/ironrdp-client/src/gui.rs | 9 +- crates/ironrdp-client/src/rdp.rs | 21 ++--- crates/ironrdp-pdu/src/mcs.rs | 20 ++++ crates/ironrdp-pdu/src/rdp/headers.rs | 11 ++- crates/ironrdp-session/src/active_stage.rs | 94 +++++++++++++++---- crates/ironrdp-session/src/lib.rs | 2 +- crates/ironrdp-session/src/x224/mod.rs | 82 ++++++++++++++-- crates/ironrdp-web/src/session.rs | 41 ++++++-- crates/ironrdp/examples/screenshot.rs | 2 +- .../src/services/wasm-bridge.service.ts | 8 ++ xtask/src/wasm.rs | 3 +- 11 files changed, 236 insertions(+), 57 deletions(-) diff --git a/crates/ironrdp-client/src/gui.rs b/crates/ironrdp-client/src/gui.rs index 38be1e04..d3ba7b1d 100644 --- a/crates/ironrdp-client/src/gui.rs +++ b/crates/ironrdp-client/src/gui.rs @@ -69,7 +69,10 @@ impl GuiContext { }); } WindowEvent::CloseRequested => { - control_flow.set_exit(); + if input_event_sender.send(RdpInputEvent::Close).is_err() { + error!("Failed to send graceful shutdown event, closing the window"); + control_flow.set_exit(); + } } WindowEvent::DroppedFile(_) => { // TODO(#110): File upload @@ -222,8 +225,8 @@ impl GuiContext { } Event::UserEvent(RdpOutputEvent::Terminated(result)) => { let exit_code = match result { - Ok(()) => { - println!("Terminated gracefully"); + Ok(reason) => { + println!("Terminated gracefully: {reason}"); proc_exit::sysexits::OK } Err(error) => { diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 9f2c68c7..8cc0d587 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -3,7 +3,7 @@ use ironrdp::connector::{ConnectionResult, ConnectorResult}; use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; use ironrdp::session::image::DecodedImage; -use ironrdp::session::{ActiveStage, ActiveStageOutput, SessionResult}; +use ironrdp::session::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult}; use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session}; use rdpdr::NoopRdpdrBackend; use smallvec::SmallVec; @@ -20,7 +20,7 @@ pub enum RdpOutputEvent { PointerDefault, PointerHidden, PointerPosition { x: u16, y: u16 }, - Terminated(SessionResult<()>), + Terminated(SessionResult), } #[derive(Debug)] @@ -67,8 +67,8 @@ impl RdpClient { self.config.connector.desktop_size.width = width; self.config.connector.desktop_size.height = height; } - Ok(RdpControlFlow::TerminatedGracefully) => { - let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(()))); + Ok(RdpControlFlow::TerminatedGracefully(reason)) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(reason))); break; } Err(e) => { @@ -82,7 +82,7 @@ impl RdpClient { enum RdpControlFlow { ReconnectWithNewSize { width: u16, height: u16 }, - TerminatedGracefully, + TerminatedGracefully(GracefulDisconnectReason), } type UpgradedFramed = ironrdp_tokio::TokioFramed>; @@ -162,7 +162,7 @@ async fn active_session( let mut active_stage = ActiveStage::new(connection_result, None); - 'outer: loop { + let disconnect_reason = 'outer: loop { let outputs = tokio::select! { frame = framed.read_pdu() => { let (action, payload) = frame.map_err(|e| session::custom_err!("read frame", e))?; @@ -198,8 +198,7 @@ async fn active_session( active_stage.process_fastpath_input(&mut image, &events)? } RdpInputEvent::Close => { - // TODO(#115): https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 - break 'outer; + active_stage.graceful_shutdown()? } RdpInputEvent::Clipboard(event) => { if let Some(cliprdr) = active_stage.get_svc_processor::() { @@ -281,10 +280,10 @@ async fn active_session( ActiveStageOutput::PointerBitmap(_) => { // Not applicable, because we use the software cursor rendering. } - ActiveStageOutput::Terminate => break 'outer, + ActiveStageOutput::Terminate(reason) => break 'outer reason, } } - } + }; - Ok(RdpControlFlow::TerminatedGracefully) + Ok(RdpControlFlow::TerminatedGracefully(disconnect_reason)) } diff --git a/crates/ironrdp-pdu/src/mcs.rs b/crates/ironrdp-pdu/src/mcs.rs index e96e4fa1..c70cec97 100644 --- a/crates/ironrdp-pdu/src/mcs.rs +++ b/crates/ironrdp-pdu/src/mcs.rs @@ -736,6 +736,22 @@ impl DisconnectReason { _ => None, } } + + pub fn description(self) -> &'static str { + match self { + Self::DomainDisconnected => "domain disconnected", + Self::ProviderInitiated => "server-initiated disconnect", + Self::TokenPurged => "token purged", + Self::UserRequested => "user-requested disconnect", + Self::ChannelPurged => "channel purged", + } + } +} + +impl core::fmt::Display for DisconnectReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.description()) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -749,6 +765,10 @@ impl DisconnectProviderUltimatum { pub const NAME: &'static str = "DisconnectProviderUltimatum"; pub const FIXED_PART_SIZE: usize = 2; + + pub fn from_reason(reason: DisconnectReason) -> Self { + Self { reason } + } } impl<'de> McsPdu<'de> for DisconnectProviderUltimatum { diff --git a/crates/ironrdp-pdu/src/rdp/headers.rs b/crates/ironrdp-pdu/src/rdp/headers.rs index 28e659b0..130ab98c 100644 --- a/crates/ironrdp-pdu/src/rdp/headers.rs +++ b/crates/ironrdp-pdu/src/rdp/headers.rs @@ -265,6 +265,7 @@ pub enum ShareDataPdu { ServerSetErrorInfo(ServerSetErrorInfoPdu), Input(InputEventPdu), ShutdownRequest, + ShutdownDenied, SuppressOutput(SuppressOutputPdu), RefreshRectangle(RefreshRectanglePdu), } @@ -281,7 +282,8 @@ impl ShareDataPdu { ShareDataPdu::FrameAcknowledge(_) => "Frame Acknowledge PDU", ShareDataPdu::ServerSetErrorInfo(_) => "Server Set Error Info PDU", ShareDataPdu::Input(_) => "Server Input PDU", - ShareDataPdu::ShutdownRequest => "Shutdown Request", + ShareDataPdu::ShutdownRequest => "Shutdown Request PDU", + ShareDataPdu::ShutdownDenied => "Shutdown Denied PDU", ShareDataPdu::SuppressOutput(_) => "Suppress Output PDU", ShareDataPdu::RefreshRectangle(_) => "Refresh Rectangle PDU", } @@ -309,6 +311,7 @@ impl ShareDataPdu { )), ShareDataPduType::Input => Ok(ShareDataPdu::Input(InputEventPdu::from_buffer(&mut stream)?)), ShareDataPduType::ShutdownRequest => Ok(ShareDataPdu::ShutdownRequest), + ShareDataPduType::ShutdownDenied => Ok(ShareDataPdu::ShutdownDenied), ShareDataPduType::SuppressOutput => Ok(ShareDataPdu::SuppressOutput(SuppressOutputPdu::from_buffer( &mut stream, )?)), @@ -318,7 +321,6 @@ impl ShareDataPdu { ShareDataPduType::Update | ShareDataPduType::Pointer | ShareDataPduType::PlaySound - | ShareDataPduType::ShutdownDenied | ShareDataPduType::SetKeyboardIndicators | ShareDataPduType::BitmapCachePersistentList | ShareDataPduType::BitmapCacheErrorPdu @@ -343,7 +345,7 @@ impl ShareDataPdu { ShareDataPdu::FrameAcknowledge(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from), ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from), ShareDataPdu::Input(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from), - ShareDataPdu::ShutdownRequest => Ok(()), + ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => Ok(()), ShareDataPdu::SuppressOutput(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from), ShareDataPdu::RefreshRectangle(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from), } @@ -359,7 +361,7 @@ impl ShareDataPdu { ShareDataPdu::FrameAcknowledge(pdu) => pdu.buffer_length(), ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.buffer_length(), ShareDataPdu::Input(pdu) => pdu.buffer_length(), - ShareDataPdu::ShutdownRequest => 0, + ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => 0, ShareDataPdu::SuppressOutput(pdu) => pdu.buffer_length(), ShareDataPdu::RefreshRectangle(pdu) => pdu.buffer_length(), } @@ -376,6 +378,7 @@ impl ShareDataPdu { ShareDataPdu::ServerSetErrorInfo(_) => ShareDataPduType::SetErrorInfoPdu, ShareDataPdu::Input(_) => ShareDataPduType::Input, ShareDataPdu::ShutdownRequest => ShareDataPduType::ShutdownRequest, + ShareDataPdu::ShutdownDenied => ShareDataPduType::ShutdownDenied, ShareDataPdu::SuppressOutput(_) => ShareDataPduType::SuppressOutput, ShareDataPdu::RefreshRectangle(_) => ShareDataPduType::RefreshRectangle, } diff --git a/crates/ironrdp-session/src/active_stage.rs b/crates/ironrdp-session/src/active_stage.rs index bc88f46e..d96d2e5c 100644 --- a/crates/ironrdp-session/src/active_stage.rs +++ b/crates/ironrdp-session/src/active_stage.rs @@ -4,14 +4,15 @@ use ironrdp_connector::ConnectionResult; use ironrdp_graphics::pointer::DecodedPointer; use ironrdp_pdu::geometry::InclusiveRectangle; use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; +use ironrdp_pdu::rdp::headers::ShareDataPdu; use ironrdp_pdu::write_buf::WriteBuf; -use ironrdp_pdu::{Action, PduParsing}; +use ironrdp_pdu::{mcs, Action, PduParsing}; use ironrdp_svc::{StaticVirtualChannelProcessor, SvcProcessorMessages}; use crate::fast_path::UpdateKind; use crate::image::DecodedImage; use crate::x224::GfxHandler; -use crate::{fast_path, x224, SessionResult}; +use crate::{fast_path, x224, SessionError, SessionResult}; pub struct ActiveStage { x224_processor: x224::Processor, @@ -105,21 +106,26 @@ impl ActiveStage { action: Action, frame: &[u8], ) -> SessionResult> { - let (output, processor_updates) = match action { + let (mut stage_outputs, processor_updates) = match action { Action::FastPath => { let mut output = WriteBuf::new(); let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?; - (output.into_inner(), processor_updates) + ( + vec![ActiveStageOutput::ResponseFrame(output.into_inner())], + processor_updates, + ) + } + Action::X224 => { + let outputs = self + .x224_processor + .process(frame)? + .into_iter() + .map(TryFrom::try_from) + .collect::, _>>()?; + (outputs, Vec::new()) } - Action::X224 => (self.x224_processor.process(frame)?, Vec::new()), }; - let mut stage_outputs = Vec::new(); - - if !output.is_empty() { - stage_outputs.push(ActiveStageOutput::ResponseFrame(output)); - } - for update in processor_updates { match update { UpdateKind::None => {} @@ -144,17 +150,27 @@ impl ActiveStage { Ok(stage_outputs) } + /// Encodes client-side graceful shutdown request. Note that upon sending this request, + /// client should wait for server's ShutdownDenied PDU before closing the connection. + /// + /// Client-side graceful shutdown is defined in [MS-RDPBCGR] + /// + /// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + pub fn graceful_shutdown(&self) -> SessionResult> { + let mut frame = WriteBuf::new(); + self.x224_processor + .encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?; + + Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())]) + } + /// Sends a PDU on the dynamic channel. pub fn encode_dynamic(&self, output: &mut WriteBuf, channel_name: &str, dvc_data: &[u8]) -> SessionResult<()> { self.x224_processor.encode_dynamic(output, channel_name, dvc_data) } /// Send a pdu on the static global channel. Typically used to send input events - pub fn encode_static( - &self, - output: &mut WriteBuf, - pdu: ironrdp_pdu::rdp::headers::ShareDataPdu, - ) -> SessionResult { + pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult { self.x224_processor.encode_static(output, pdu) } @@ -184,5 +200,49 @@ pub enum ActiveStageOutput { PointerHidden, PointerPosition { x: u16, y: u16 }, PointerBitmap(Rc), - Terminate, + Terminate(GracefulDisconnectReason), +} + +impl TryFrom for ActiveStageOutput { + type Error = SessionError; + + fn try_from(value: x224::ProcessorOutput) -> Result { + match value { + x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)), + x224::ProcessorOutput::Disconnect(reason) => { + let reason = match reason { + mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated, + mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated, + other => GracefulDisconnectReason::Other(other.description()), + }; + + Ok(Self::Terminate(reason)) + } + } + } +} + +/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for +/// disconnect reasons. +#[derive(Debug, Clone, Copy)] +pub enum GracefulDisconnectReason { + UserInitiated, + ServerInitiated, + Other(&'static str), +} + +impl GracefulDisconnectReason { + pub fn description(&self) -> &'static str { + match self { + GracefulDisconnectReason::UserInitiated => "user initiated disconnect", + GracefulDisconnectReason::ServerInitiated => "server initiated disconnect", + GracefulDisconnectReason::Other(description) => description, + } + } +} + +impl core::fmt::Display for GracefulDisconnectReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.description()) + } } diff --git a/crates/ironrdp-session/src/lib.rs b/crates/ironrdp-session/src/lib.rs index 8eebd3da..2cb98e99 100644 --- a/crates/ironrdp-session/src/lib.rs +++ b/crates/ironrdp-session/src/lib.rs @@ -18,7 +18,7 @@ mod active_stage; use core::fmt; -pub use active_stage::{ActiveStage, ActiveStageOutput}; +pub use active_stage::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason}; pub type SessionResult = Result; diff --git a/crates/ironrdp-session/src/x224/mod.rs b/crates/ironrdp-session/src/x224/mod.rs index ef4252ed..b1dc6c94 100644 --- a/crates/ironrdp-session/src/x224/mod.rs +++ b/crates/ironrdp-session/src/x224/mod.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use ironrdp_connector::legacy::SendDataIndicationCtx; use ironrdp_connector::GraphicsConfig; use ironrdp_pdu::dvc::FieldType; +use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage}; use ironrdp_pdu::rdp::headers::ShareDataPdu; use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; use ironrdp_pdu::rdp::vc::dvc; @@ -17,7 +18,7 @@ use ironrdp_svc::{ StaticChannelSet, StaticVirtualChannel, StaticVirtualChannelProcessor, SvcMessage, SvcProcessorMessages, }; -use crate::{SessionErrorExt as _, SessionResult}; +use crate::{SessionError, SessionErrorExt as _, SessionResult}; #[rustfmt::skip] pub use self::gfx::GfxHandler; @@ -25,6 +26,15 @@ pub use self::gfx::GfxHandler; pub const RDP8_GRAPHICS_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::Graphics"; pub const RDP8_DISPLAY_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::DisplayControl"; +/// X224 Processor output +#[derive(Debug, Clone)] +pub enum ProcessorOutput { + /// A buffer with encoded data to send to the server. + ResponseFrame(Vec), + /// A graceful disconnect notification. Client should close the connection upon receiving this. + Disconnect(DisconnectReason), +} + pub struct Processor { channel_map: HashMap, static_channels: StaticChannelSet, @@ -90,26 +100,28 @@ impl Processor { process_svc_messages(messages.into(), channel_id, self.user_channel_id) } - /// Processes a received PDU. Returns a buffer with encoded data to send to the server, if any. - pub fn process(&mut self, frame: &[u8]) -> SessionResult> { + /// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed + /// in the returned order. + pub fn process(&mut self, frame: &[u8]) -> SessionResult> { let data_ctx: SendDataIndicationCtx<'_> = ironrdp_connector::legacy::decode_send_data_indication(frame).map_err(crate::legacy::map_error)?; let channel_id = data_ctx.channel_id; if channel_id == self.io_channel_id { - self.process_io_channel(data_ctx)?; - Ok(Vec::new()) + self.process_io_channel(data_ctx) } else if self.drdynvc_channel_id == Some(channel_id) { self.process_dyvc(data_ctx) + .map(|data| vec![ProcessorOutput::ResponseFrame(data)]) } else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) { let response_pdus = svc.process(data_ctx.user_data).map_err(crate::SessionError::pdu)?; process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id) + .map(|data| vec![ProcessorOutput::ResponseFrame(data)]) } else { Err(reason_err!("X224", "unexpected channel received: ID {channel_id}")) } } - fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<()> { + fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult> { debug_assert_eq!(data_ctx.channel_id, self.io_channel_id); let ctx = ironrdp_connector::legacy::decode_share_data(data_ctx).map_err(crate::legacy::map_error)?; @@ -117,16 +129,47 @@ impl Processor { match ctx.pdu { ShareDataPdu::SaveSessionInfo(session_info) => { debug!("Got Session Save Info PDU: {session_info:?}"); - Ok(()) + Ok(Vec::new()) } ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode( ProtocolIndependentCode::None, ))) => { debug!("Received None server error"); - Ok(()) + Ok(Vec::new()) } ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => { - Err(reason_err!("ServerSetErrorInfo", "{}", e.description())) + // This is a part of server-side graceful disconnect procedure defined + // in [MS-RDPBCGR]. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8 + let graceful_disconnect = error_info_to_graceful_disconnect_reason(&e); + + if let Some(reason) = graceful_disconnect { + debug!("Received server-side graceful disconnect request: {reason}"); + + Ok(vec![ProcessorOutput::Disconnect(reason)]) + } else { + Err(reason_err!("ServerSetErrorInfo", "{}", e.description())) + } + } + ShareDataPdu::ShutdownDenied => { + debug!("ShutdownDenied received, session will be closed"); + + // As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we + // need to send a disconnect ultimatum to the server if we want to proceed with the + // session shutdown. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + let ultimatum = McsMessage::DisconnectProviderUltimatum(DisconnectProviderUltimatum::from_reason( + DisconnectReason::UserRequested, + )); + + let encoded_pdu = ironrdp_pdu::encode_vec(&ultimatum).map_err(SessionError::pdu); + + Ok(vec![ + ProcessorOutput::ResponseFrame(encoded_pdu?), + ProcessorOutput::Disconnect(DisconnectReason::UserRequested), + ]) } _ => Err(reason_err!( "IO channel", @@ -507,3 +550,24 @@ impl CompleteData { } } } + +/// Converts an [`ErrorInfo`] into a [`DisconnectReason`]. +/// +/// Returns `None` if the error code is not a graceful disconnect code. +pub fn error_info_to_graceful_disconnect_reason(error_info: &ErrorInfo) -> Option { + let code = if let ErrorInfo::ProtocolIndependentCode(code) = error_info { + code + } else { + return None; + }; + + match code { + ProtocolIndependentCode::RpcInitiatedDisconnect + | ProtocolIndependentCode::RpcInitiatedLogoff + | ProtocolIndependentCode::DisconnectedByOtherconnection => Some(DisconnectReason::ProviderInitiated), + ProtocolIndependentCode::RpcInitiatedDisconnectByuser | ProtocolIndependentCode::LogoffByUser => { + Some(DisconnectReason::UserRequested) + } + _ => None, + } +} diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 04164354..5c33dfbf 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -18,7 +18,7 @@ use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; use ironrdp::pdu::write_buf::WriteBuf; use ironrdp::session::image::DecodedImage; -use ironrdp::session::{ActiveStage, ActiveStageOutput}; +use ironrdp::session::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason}; use rgb::AsPixels as _; use tap::prelude::*; use wasm_bindgen::prelude::*; @@ -342,6 +342,7 @@ pub(crate) enum RdpInputEvent { Cliprdr(ClipboardMessage), ClipboardBackend(WasmClipboardBackendMessage), FastPath(FastPathInputEvents), + TerminateSession, } enum CursorStyle { @@ -354,6 +355,18 @@ enum CursorStyle { }, } +#[wasm_bindgen] +pub struct SessionTerminationInfo { + reason: GracefulDisconnectReason, +} + +#[wasm_bindgen] +impl SessionTerminationInfo { + pub fn reason(&self) -> String { + self.reason.to_string() + } +} + #[wasm_bindgen] pub struct Session { desktop_size: connector::DesktopSize, @@ -374,7 +387,7 @@ pub struct Session { #[wasm_bindgen] impl Session { - pub async fn run(&self) -> Result<(), IronRdpError> { + pub async fn run(&self) -> Result { let rdp_reader = self .rdp_reader .borrow_mut() @@ -418,7 +431,7 @@ impl Session { let mut active_stage = ActiveStage::new(connection_result, None); - 'outer: loop { + let disconnect_reason = 'outer: loop { let outputs = select! { frame = framed.read_pdu().fuse() => { let (action, payload) = frame.context("read frame")?; @@ -471,7 +484,11 @@ impl Session { } RdpInputEvent::FastPath(events) => { active_stage.process_fastpath_input(&mut image, &events) - .context("Fast path input events processing")? + .context("fast path input events processing")? + } + RdpInputEvent::TerminateSession => { + active_stage.graceful_shutdown() + .context("graceful shutdown")? } } } @@ -589,14 +606,16 @@ impl Session { hotspot_y, })?; } - ActiveStageOutput::Terminate => break 'outer, + ActiveStageOutput::Terminate(reason) => break 'outer reason, } } - } + }; - info!("RPD session terminated"); + info!(%disconnect_reason, "RPD session terminated"); - Ok(()) + Ok(SessionTerminationInfo { + reason: disconnect_reason, + }) } pub fn desktop_size(&self) -> DesktopSize { @@ -651,9 +670,11 @@ impl Session { Ok(()) } - #[allow(clippy::unused_self)] // FIXME: not yet implemented pub fn shutdown(&self) -> Result<(), IronRdpError> { - // TODO(#115): https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + self.input_events_tx + .unbounded_send(RdpInputEvent::TerminateSession) + .context("failed to send terminate session event to writer task")?; + Ok(()) } diff --git a/crates/ironrdp/examples/screenshot.rs b/crates/ironrdp/examples/screenshot.rs index e2c7d163..0f204ead 100644 --- a/crates/ironrdp/examples/screenshot.rs +++ b/crates/ironrdp/examples/screenshot.rs @@ -291,7 +291,7 @@ fn active_stage( for out in outputs { match out { ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?, - ActiveStageOutput::Terminate => break 'outer, + ActiveStageOutput::Terminate(_) => break 'outer, _ => {} } } diff --git a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts index 26005684..a299436f 100644 --- a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts +++ b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts @@ -8,6 +8,7 @@ import init, { Session, SessionBuilder, ClipboardTransaction, + SessionTerminationInfo, } from '../../../../crates/ironrdp-web/pkg/ironrdp_web'; import { loggingService } from './logging.service'; import { catchError, filter, map } from 'rxjs/operators'; @@ -188,6 +189,13 @@ export class WasmBridgeService { }); return of(err); }), + map((termination_info: SessionTerminationInfo) => { + this.setVisibility(false); + this.raiseSessionEvent({ + type: SessionEventType.TERMINATED, + data: 'Session was terminated: ' + termination_info.reason() + '.', + }); + }), ) .subscribe(); return session; diff --git a/xtask/src/wasm.rs b/xtask/src/wasm.rs index 89a6938c..c1556d7b 100644 --- a/xtask/src/wasm.rs +++ b/xtask/src/wasm.rs @@ -1,4 +1,5 @@ -use crate::{local_bin, prelude::*}; +use crate::local_bin; +use crate::prelude::*; pub fn check(sh: &Shell) -> anyhow::Result<()> { let _s = Section::new("WASM-CHECK");