Skip to content

Commit 0875d84

Browse files
committed
feat(agent): add window recording support to now proto dvc
1 parent 1417db9 commit 0875d84

File tree

6 files changed

+703
-7
lines changed

6 files changed

+703
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ 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+
git = "https://github.com/Devolutions/now-proto.git"
48+
branch = "feat/window-monitoring"
4849
features = ["std"]
4950

5051
[target.'cfg(windows)'.build-dependencies]
@@ -56,6 +57,7 @@ optional = true
5657
features = [
5758
"Win32_Foundation",
5859
"Win32_System_Shutdown",
60+
"Win32_UI_Accessibility",
5961
"Win32_UI_WindowsAndMessaging",
6062
"Win32_UI_Shell",
6163
"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: 71 additions & 4 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);
@@ -73,7 +74,7 @@ impl Task for DvcIoTask {
7374

7475
// Spawning thread is relatively short operation, so it could be executed synchronously.
7576
let io_thread = std::thread::spawn(move || {
76-
let io_thread_result = run_dvc_io(write_rx, read_tx, cloned_shutdown_event);
77+
let io_thread_result: Result<(), anyhow::Error> = run_dvc_io(write_rx, read_tx, cloned_shutdown_event);
7778

7879
if let Err(error) = io_thread_result {
7980
error!(%error, "DVC IO thread failed");
@@ -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,8 @@ 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<()>>,
292301
}
293302

294303
impl MessageProcessor {
@@ -302,6 +311,7 @@ impl MessageProcessor {
302311
io_notification_tx,
303312
capabilities,
304313
sessions: HashMap::new(),
314+
window_monitor_shutdown_tx: None,
305315
}
306316
}
307317

@@ -466,6 +476,14 @@ impl MessageProcessor {
466476
error!(%error, "Failed to set keyboard layout");
467477
}
468478
}
479+
NowMessage::Session(NowSessionMessage::WindowRecStart(start_msg)) => {
480+
if let Err(error) = self.start_window_recording(start_msg).await {
481+
error!(%error, "Failed to start window recording");
482+
}
483+
}
484+
NowMessage::Session(NowSessionMessage::WindowRecStop(_stop_msg)) => {
485+
self.stop_window_recording();
486+
}
469487
NowMessage::System(NowSystemMessage::Shutdown(shutdown_msg)) => {
470488
let mut current_process_token = win_api_wrappers::process::Process::current_process()
471489
.token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?;
@@ -742,6 +760,46 @@ impl MessageProcessor {
742760

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

747805
fn append_ps_args(args: &mut Vec<String>, msg: &NowExecWinPsMsg<'_>) {
@@ -919,6 +977,15 @@ fn make_generic_error_failsafe(session_id: u32, code: u32, message: String) -> N
919977
})
920978
}
921979

980+
async fn handle_window_recording_event(
981+
dvc_tx: &WinapiSignaledSender<NowMessage<'static>>,
982+
message: NowSessionWindowRecEventMsg<'static>,
983+
) -> anyhow::Result<()> {
984+
dvc_tx.send(NowMessage::from(message.into_owned())).await?;
985+
986+
Ok(())
987+
}
988+
922989
async fn handle_exec_error(dvc_tx: &WinapiSignaledSender<NowMessage<'static>>, session_id: u32, error: ExecError) {
923990
let msg = match error {
924991
ExecError::NowStatus(status) => {

0 commit comments

Comments
 (0)