Skip to content

Commit 8ef6b83

Browse files
committed
feat(agent): add window recording support to now proto dvc
1 parent 796abfa commit 8ef6b83

File tree

6 files changed

+717
-6
lines changed

6 files changed

+717
-6
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

devolutions-session/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ win-api-wrappers = { path = "../crates/win-api-wrappers", optional = true }
4444

4545
[dependencies.now-proto-pdu]
4646
optional = true
47-
version = "0.4.1"
47+
version = "0.4.2"
4848
features = ["std"]
4949

5050
[target.'cfg(windows)'.build-dependencies]
@@ -56,6 +56,7 @@ optional = true
5656
features = [
5757
"Win32_Foundation",
5858
"Win32_System_Shutdown",
59+
"Win32_UI_Accessibility",
5960
"Win32_UI_WindowsAndMessaging",
6061
"Win32_UI_Shell",
6162
"Win32_System_Console",

devolutions-session/src/dvc/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ pub mod io;
3939
pub mod now_message_dissector;
4040
pub mod process;
4141
pub mod task;
42+
pub mod window_monitor;
4243

4344
mod env;

devolutions-session/src/dvc/process.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ pub enum ServerChannelEvent {
9393
session_id: u32,
9494
error: ExecError,
9595
},
96+
WindowRecordingEvent {
97+
message: now_proto_pdu::NowSessionWindowRecEventMsg<'static>,
98+
},
9699
}
97100

98101
pub struct WinApiProcessCtx {

devolutions-session/src/dvc/task.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ use now_proto_pdu::{
2727
NowExecBatchMsg, NowExecCancelRspMsg, NowExecCapsetFlags, NowExecDataMsg, NowExecDataStreamKind, NowExecMessage,
2828
NowExecProcessMsg, NowExecPwshMsg, NowExecResultMsg, NowExecRunMsg, NowExecStartedMsg, NowExecWinPsMsg, NowMessage,
2929
NowMsgBoxResponse, NowProtoError, NowProtoVersion, NowSessionCapsetFlags, NowSessionMessage,
30-
NowSessionMsgBoxReqMsg, NowSessionMsgBoxRspMsg, NowStatusError, NowSystemCapsetFlags, NowSystemMessage,
31-
SetKbdLayoutOption,
30+
NowSessionMsgBoxReqMsg, NowSessionMsgBoxRspMsg, NowSessionWindowRecEventMsg, NowSessionWindowRecStartMsg,
31+
NowStatusError, NowSystemCapsetFlags, NowSystemMessage, SetKbdLayoutOption, WindowRecStartFlags,
3232
};
3333
use win_api_wrappers::event::Event;
3434
use win_api_wrappers::security::privilege::ScopedPrivileges;
@@ -38,6 +38,7 @@ use crate::dvc::channel::{WinapiSignaledSender, bounded_mpsc_channel, winapi_sig
3838
use crate::dvc::fs::TmpFileGuard;
3939
use crate::dvc::io::run_dvc_io;
4040
use crate::dvc::process::{ExecError, ServerChannelEvent, WinApiProcess, WinApiProcessBuilder};
41+
use crate::dvc::window_monitor::{WindowMonitorConfig, run_window_monitor};
4142

4243
// One minute heartbeat interval by default
4344
const DEFAULT_HEARTBEAT_INTERVAL: core::time::Duration = core::time::Duration::from_secs(60);
@@ -229,6 +230,11 @@ async fn process_messages(
229230

230231
handle_exec_error(&dvc_tx, session_id, error).await;
231232
}
233+
ServerChannelEvent::WindowRecordingEvent { message } => {
234+
if let Err(error) = handle_window_recording_event(&dvc_tx, message).await {
235+
error!(%error, "Failed to handle window recording event");
236+
}
237+
}
232238
ServerChannelEvent::CloseChannel => {
233239
info!("Received close channel notification, shutting down...");
234240

@@ -265,7 +271,8 @@ fn default_server_caps() -> NowChannelCapsetMsg {
265271
NowSessionCapsetFlags::LOCK
266272
| NowSessionCapsetFlags::LOGOFF
267273
| NowSessionCapsetFlags::MSGBOX
268-
| NowSessionCapsetFlags::SET_KBD_LAYOUT,
274+
| NowSessionCapsetFlags::SET_KBD_LAYOUT
275+
| NowSessionCapsetFlags::WINDOW_RECORDING,
269276
)
270277
.with_exec_capset(
271278
NowExecCapsetFlags::STYLE_RUN
@@ -289,6 +296,10 @@ struct MessageProcessor {
289296
#[allow(dead_code)] // Not yet used.
290297
capabilities: NowChannelCapsetMsg,
291298
sessions: HashMap<u32, WinApiProcess>,
299+
/// Shutdown signal sender for window monitoring task.
300+
window_monitor_shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
301+
/// Handle for the window monitor task.
302+
window_monitor_handle: Option<tokio::task::JoinHandle<()>>,
292303
}
293304

294305
impl MessageProcessor {
@@ -302,6 +313,8 @@ impl MessageProcessor {
302313
io_notification_tx,
303314
capabilities,
304315
sessions: HashMap::new(),
316+
window_monitor_shutdown_tx: None,
317+
window_monitor_handle: None,
305318
}
306319
}
307320

@@ -466,6 +479,14 @@ impl MessageProcessor {
466479
error!(%error, "Failed to set keyboard layout");
467480
}
468481
}
482+
NowMessage::Session(NowSessionMessage::WindowRecStart(start_msg)) => {
483+
if let Err(error) = self.start_window_recording(start_msg).await {
484+
error!(%error, "Failed to start window recording");
485+
}
486+
}
487+
NowMessage::Session(NowSessionMessage::WindowRecStop(_stop_msg)) => {
488+
self.stop_window_recording().await;
489+
}
469490
NowMessage::System(NowSystemMessage::Shutdown(shutdown_msg)) => {
470491
let mut current_process_token = win_api_wrappers::process::Process::current_process()
471492
.token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?;
@@ -742,6 +763,55 @@ impl MessageProcessor {
742763

743764
self.sessions.clear();
744765
}
766+
767+
async fn start_window_recording(&mut self, start_msg: NowSessionWindowRecStartMsg) -> anyhow::Result<()> {
768+
// Stop any existing window recording first.
769+
self.stop_window_recording().await;
770+
771+
info!("Starting window recording");
772+
773+
let poll_interval_ms = if start_msg.poll_interval > 0 {
774+
u64::from(start_msg.poll_interval)
775+
} else {
776+
1000 // Default to 1000ms (1 second) if not specified.
777+
};
778+
779+
let track_title_changes = start_msg.flags.contains(WindowRecStartFlags::TRACK_TITLE_CHANGE);
780+
781+
// Create shutdown channel for window monitor.
782+
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
783+
784+
// Store shutdown sender so we can stop monitoring later.
785+
self.window_monitor_shutdown_tx = Some(shutdown_tx);
786+
787+
// Spawn window monitor task.
788+
let event_tx = self.io_notification_tx.clone();
789+
let window_monitor_handle = tokio::task::spawn(async move {
790+
let config = WindowMonitorConfig::new(event_tx, track_title_changes, shutdown_rx)
791+
.with_poll_interval_ms(poll_interval_ms);
792+
793+
run_window_monitor(config).await;
794+
});
795+
796+
self.window_monitor_handle = Some(window_monitor_handle);
797+
798+
Ok(())
799+
}
800+
801+
async fn stop_window_recording(&mut self) {
802+
if let Some(shutdown_tx) = self.window_monitor_shutdown_tx.take() {
803+
info!("Stopping window recording");
804+
// Send shutdown signal (ignore errors if receiver was already dropped).
805+
let _ = shutdown_tx.send(());
806+
807+
// Wait for the task to finish.
808+
if let Some(handle) = self.window_monitor_handle.take()
809+
&& let Err(error) = handle.await
810+
{
811+
error!(%error, "Window monitor task panicked");
812+
}
813+
}
814+
}
745815
}
746816

747817
fn append_ps_args(args: &mut Vec<String>, msg: &NowExecWinPsMsg<'_>) {
@@ -919,6 +989,15 @@ fn make_generic_error_failsafe(session_id: u32, code: u32, message: String) -> N
919989
})
920990
}
921991

992+
async fn handle_window_recording_event(
993+
dvc_tx: &WinapiSignaledSender<NowMessage<'static>>,
994+
message: NowSessionWindowRecEventMsg<'static>,
995+
) -> anyhow::Result<()> {
996+
dvc_tx.send(NowMessage::from(message.into_owned())).await?;
997+
998+
Ok(())
999+
}
1000+
9221001
async fn handle_exec_error(dvc_tx: &WinapiSignaledSender<NowMessage<'static>>, session_id: u32, error: ExecError) {
9231002
let msg = match error {
9241003
ExecError::NowStatus(status) => {

0 commit comments

Comments
 (0)