Skip to content

Commit 8a56ad0

Browse files
Copilotpacmancoder
andauthored
refactor(agent): address review comments for window recording support (#1600)
Addresses review feedback from PR #1583 on the window recording feature implementation. ### Dependency Management - Updated `now-proto-pdu` to use v0.4.2 from crates.io for stable, reproducible builds ### Task Lifecycle - Track window monitor task with `JoinHandle` and await completion on shutdown to prevent race conditions - Made `stop_window_recording` async to properly await task termination ### Code Quality - Removed redundant type annotation on `run_dvc_io` result - Extracted duplicate timestamp logic into `get_current_timestamp()` helper - Fixed module docs to clarify polling is used for title change detection ### Bug Fix - When title tracking is disabled, preserve previous title in snapshot to avoid missing actual window changes: ```rust // Before: updating full snapshot could mask process/exe_path changes last_snapshot = Some(snapshot); // After: preserve previous title when tracking disabled let prev_title = last_snapshot.as_ref().map_or_else(String::new, |s| s.title.clone()); last_snapshot = Some(WindowSnapshot { process_id: snapshot.process_id, exe_path: snapshot.exe_path.clone(), title: prev_title, }); ``` <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: pacmancoder <[email protected]>
1 parent 0875d84 commit 8a56ad0

File tree

4 files changed

+47
-32
lines changed

4 files changed

+47
-32
lines changed

Cargo.lock

Lines changed: 3 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: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ win-api-wrappers = { path = "../crates/win-api-wrappers", optional = true }
4444

4545
[dependencies.now-proto-pdu]
4646
optional = true
47-
git = "https://github.com/Devolutions/now-proto.git"
48-
branch = "feat/window-monitoring"
47+
version = "0.4.2"
4948
features = ["std"]
5049

5150
[target.'cfg(windows)'.build-dependencies]

devolutions-session/src/dvc/task.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl Task for DvcIoTask {
7474

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

7979
if let Err(error) = io_thread_result {
8080
error!(%error, "DVC IO thread failed");
@@ -298,6 +298,8 @@ struct MessageProcessor {
298298
sessions: HashMap<u32, WinApiProcess>,
299299
/// Shutdown signal sender for window monitoring task.
300300
window_monitor_shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
301+
/// Handle for the window monitor task.
302+
window_monitor_handle: Option<tokio::task::JoinHandle<()>>,
301303
}
302304

303305
impl MessageProcessor {
@@ -312,6 +314,7 @@ impl MessageProcessor {
312314
capabilities,
313315
sessions: HashMap::new(),
314316
window_monitor_shutdown_tx: None,
317+
window_monitor_handle: None,
315318
}
316319
}
317320

@@ -482,7 +485,7 @@ impl MessageProcessor {
482485
}
483486
}
484487
NowMessage::Session(NowSessionMessage::WindowRecStop(_stop_msg)) => {
485-
self.stop_window_recording();
488+
self.stop_window_recording().await;
486489
}
487490
NowMessage::System(NowSystemMessage::Shutdown(shutdown_msg)) => {
488491
let mut current_process_token = win_api_wrappers::process::Process::current_process()
@@ -763,7 +766,7 @@ impl MessageProcessor {
763766

764767
async fn start_window_recording(&mut self, start_msg: NowSessionWindowRecStartMsg) -> anyhow::Result<()> {
765768
// Stop any existing window recording first.
766-
self.stop_window_recording();
769+
self.stop_window_recording().await;
767770

768771
info!("Starting window recording");
769772

@@ -783,21 +786,30 @@ impl MessageProcessor {
783786

784787
// Spawn window monitor task.
785788
let event_tx = self.io_notification_tx.clone();
786-
tokio::task::spawn(async move {
789+
let window_monitor_handle = tokio::task::spawn(async move {
787790
let config = WindowMonitorConfig::new(event_tx, track_title_changes, shutdown_rx)
788791
.with_poll_interval_ms(poll_interval_ms);
789792

790793
run_window_monitor(config).await;
791794
});
792795

796+
self.window_monitor_handle = Some(window_monitor_handle);
797+
793798
Ok(())
794799
}
795800

796-
fn stop_window_recording(&mut self) {
801+
async fn stop_window_recording(&mut self) {
797802
if let Some(shutdown_tx) = self.window_monitor_shutdown_tx.take() {
798803
info!("Stopping window recording");
799804
// Send shutdown signal (ignore errors if receiver was already dropped).
800805
let _ = shutdown_tx.send(());
806+
807+
// Wait for the task to finish.
808+
if let Some(handle) = self.window_monitor_handle.take() {
809+
if let Err(error) = handle.await {
810+
error!(%error, "Window monitor task panicked");
811+
}
812+
}
801813
}
802814
}
803815
}

devolutions-session/src/dvc/window_monitor.rs

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
//! and timestamp (UTC).
66
//!
77
//! Uses Windows Event Hooks (SetWinEventHook) to receive EVENT_SYSTEM_FOREGROUND
8-
//! notifications whenever the foreground window changes, avoiding the need for polling.
8+
//! notifications whenever the foreground window changes. Additionally supports optional
9+
//! polling for detecting title changes within the same window.
910
//!
1011
//! The module provides a callback-based interface for integrating with other systems
1112
//! (e.g., DVC protocol for transmitting window change events).
@@ -194,6 +195,14 @@ fn capture_foreground_window() -> Result<WindowSnapshot> {
194195
capture_window_snapshot(foreground_window)
195196
}
196197

198+
/// Gets the current timestamp as seconds since Unix epoch.
199+
fn get_current_timestamp() -> u64 {
200+
SystemTime::now()
201+
.duration_since(std::time::UNIX_EPOCH)
202+
.map(|d| d.as_secs())
203+
.unwrap_or(0)
204+
}
205+
197206
/// Thread-local context for the event hook callback.
198207
///
199208
/// Windows event hook callbacks must be plain C functions, so we use thread-local
@@ -394,18 +403,15 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
394403
});
395404

396405
// Wait for hook thread to send its thread ID.
397-
let hook_thread_id = thread_id_rx.await.expect("Hook thread should send thread ID");
406+
let hook_thread_id = thread_id_rx.await.expect("Failed to receive thread ID from hook thread; the thread may have panicked or exited unexpectedly during initialization");
398407

399408
// Track last known window state to detect changes.
400409
let mut last_snapshot: Option<WindowSnapshot> = None;
401410

402411
// Capture and notify about initial foreground window.
403412
match capture_foreground_window() {
404413
Ok(snapshot) => {
405-
let timestamp = SystemTime::now()
406-
.duration_since(std::time::UNIX_EPOCH)
407-
.map(|d| d.as_secs())
408-
.unwrap_or(0);
414+
let timestamp = get_current_timestamp();
409415

410416
info!(
411417
process_id = snapshot.process_id,
@@ -441,10 +447,7 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
441447
Err(error) => {
442448
debug!(%error, "No initial active window");
443449

444-
let timestamp = SystemTime::now()
445-
.duration_since(std::time::UNIX_EPOCH)
446-
.map(|d| d.as_secs())
447-
.unwrap_or(0);
450+
let timestamp = get_current_timestamp();
448451

449452
// Send "no active window" event.
450453
let message = NowSessionWindowRecEventMsg::no_active_window(timestamp);
@@ -486,10 +489,7 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
486489

487490
// Check if this is actually a change.
488491
if last_snapshot.as_ref() != Some(&snapshot) {
489-
let timestamp = SystemTime::now()
490-
.duration_since(std::time::UNIX_EPOCH)
491-
.map(|d| d.as_secs())
492-
.unwrap_or(0);
492+
let timestamp = get_current_timestamp();
493493

494494
info!(
495495
process_id = snapshot.process_id,
@@ -532,10 +532,7 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
532532
Ok(snapshot) => {
533533
// Check if title or window changed.
534534
if last_snapshot.as_ref() != Some(&snapshot) {
535-
let timestamp = SystemTime::now()
536-
.duration_since(std::time::UNIX_EPOCH)
537-
.map(|d| d.as_secs())
538-
.unwrap_or(0);
535+
let timestamp = get_current_timestamp();
539536

540537
// Determine if only the title changed for the same process.
541538
let is_title_change = last_snapshot.as_ref()
@@ -544,7 +541,16 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
544541

545542
// Skip title changes if tracking is disabled.
546543
if is_title_change && !config.track_title_changes {
547-
last_snapshot = Some(snapshot);
544+
// Only update process_id and exe_path, keep the previous title
545+
// to avoid missing process/exe_path changes.
546+
let prev_title = last_snapshot
547+
.as_ref()
548+
.map_or_else(String::new, |s| s.title.clone());
549+
last_snapshot = Some(WindowSnapshot {
550+
process_id: snapshot.process_id,
551+
exe_path: snapshot.exe_path.clone(),
552+
title: prev_title,
553+
});
548554
} else {
549555
let message_result = if is_title_change {
550556
debug!(
@@ -590,10 +596,7 @@ pub async fn run_window_monitor(config: WindowMonitorConfig) {
590596

591597
// If we previously had an active window, send "no active window" event.
592598
if last_snapshot.is_some() {
593-
let timestamp = SystemTime::now()
594-
.duration_since(std::time::UNIX_EPOCH)
595-
.map(|d| d.as_secs())
596-
.unwrap_or(0);
599+
let timestamp = get_current_timestamp();
597600

598601
let message = NowSessionWindowRecEventMsg::no_active_window(timestamp);
599602
if config.event_tx.send(ServerChannelEvent::WindowRecordingEvent { message }).await.is_err() {

0 commit comments

Comments
 (0)