feat(session): graceful disconnection support (#336)

This commit is contained in:
Vladyslav Nikonov 2023-12-14 17:36:20 +02:00 committed by GitHub
parent d24000a2ae
commit 056ec6a034
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 236 additions and 57 deletions

View file

@ -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) => {

View file

@ -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<GracefulDisconnectReason>),
}
#[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<ironrdp_tls::TlsStream<TcpStream>>;
@ -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::<ironrdp::cliprdr::Cliprdr>() {
@ -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))
}

View file

@ -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 {

View file

@ -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,
}

View file

@ -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<Vec<ActiveStageOutput>> {
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::<Result<Vec<_>, _>>()?;
(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<Vec<ActiveStageOutput>> {
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<usize> {
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
self.x224_processor.encode_static(output, pdu)
}
@ -184,5 +200,49 @@ pub enum ActiveStageOutput {
PointerHidden,
PointerPosition { x: u16, y: u16 },
PointerBitmap(Rc<DecodedPointer>),
Terminate,
Terminate(GracefulDisconnectReason),
}
impl TryFrom<x224::ProcessorOutput> for ActiveStageOutput {
type Error = SessionError;
fn try_from(value: x224::ProcessorOutput) -> Result<Self, Self::Error> {
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())
}
}

View file

@ -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<T> = Result<T, SessionError>;

View file

@ -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<u8>),
/// A graceful disconnect notification. Client should close the connection upon receiving this.
Disconnect(DisconnectReason),
}
pub struct Processor {
channel_map: HashMap<String, u32>,
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<Vec<u8>> {
/// 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<Vec<ProcessorOutput>> {
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<Vec<ProcessorOutput>> {
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<DisconnectReason> {
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,
}
}

View file

@ -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<SessionTerminationInfo, IronRdpError> {
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(())
}

View file

@ -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,
_ => {}
}
}

View file

@ -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;

View file

@ -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");