Skip to content

Conversation

@pacmancoder
Copy link
Contributor

@pacmancoder pacmancoder commented Nov 25, 2025

Adds window recording support to the Devolutions Agent. The implementation uses Windows event hooks to receive foreground window change notifications and optional polling to detect title changes within the same window.

Issue: ARC-353

@github-actions
Copy link

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

@pacmancoder pacmancoder changed the title feat(agent): add window recording support to now proto dvc feat(agent): add window recording support via now proto dvc Nov 25, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds window recording support to the devolutions-session agent by implementing a window monitoring system that tracks active window changes on Windows systems. The implementation uses Windows Event Hooks to receive foreground window change notifications and optional polling to detect title changes within the same window.

Key Changes

  • Added a new window_monitor module that uses Windows Event Hooks (SetWinEventHook) to monitor foreground window changes
  • Integrated window recording capabilities into the DVC task processing system with start/stop message handlers
  • Updated the dependency on now-proto-pdu to use a Git branch for new window recording protocol messages

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
devolutions-session/src/dvc/window_monitor.rs New module implementing window monitoring using Windows Event Hooks and optional polling for title changes, with async event handling through channels
devolutions-session/src/dvc/task.rs Added window recording start/stop handlers, updated capabilities to include WINDOW_RECORDING flag, and integrated window recording events into the message processing loop
devolutions-session/src/dvc/process.rs Extended ServerChannelEvent enum with WindowRecordingEvent variant for propagating window change events
devolutions-session/src/dvc/mod.rs Added window_monitor module declaration
devolutions-session/Cargo.toml Updated now-proto-pdu dependency to use Git branch and added Win32_UI_Accessibility feature for Windows API
Cargo.lock Updated now-proto-pdu source to Git repository

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pacmancoder
Copy link
Contributor Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Contributor

Copilot AI commented Dec 2, 2025

@pacmancoder I've opened a new pull request, #1600, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI added a commit that referenced this pull request Dec 2, 2025
pacmancoder added a commit that referenced this pull request Dec 2, 2025
…#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]>
@pacmancoder pacmancoder force-pushed the feat/window-title-recording branch from 8a56ad0 to 8ef6b83 Compare December 2, 2025 16:09
@pacmancoder
Copy link
Contributor Author

Testing

  • Install devolutions-agent to target machine
  • Build and copy devolutions-session.exe to target machine (%PROGRAMFILES%/Devolutions/Agent/DevolutionsSession.exe)
  • Use IronRDP to connect to the target machine with DVC proxying via named pipe:
    • cargo run -p ironrdp-client -- -u <LOGIN> -p <PASSWORD> --dvc-proxy Devolutions::Now::Agent=now-proto-GLOBAL <IP>
  • Within a few seconds from launching IronRDP launch now proto dvc test CLI:
    • cd <now-proto repo>\dotnet\Devolutions.NowClient.Cli
    • dotnet run --pipe=now-proto-GLOBAL
  • When now proto cli opens, execute winrec-start command and observe the received messages
[18:10:02] ACTIVE WINDOW: Program Manager
             Process ID: 7564
             Executable: C:\Windows\explorer.exe
[18:10:02] ACTIVE WINDOW:
             Process ID: 8060
             Executable: C:\Program Files\Mozilla Firefox\firefox.exe
[18:10:03] ACTIVE WINDOW: Mozilla Firefox
             Process ID: 8060
             Executable: C:\Program Files\Mozilla Firefox\firefox.exe
[18:10:04] TITLE CHANGED: What’s new with Firefox 144 — Mozilla Firefox
[18:10:19] TITLE CHANGED: New Tab — Mozilla Firefox

@pacmancoder pacmancoder requested a review from CBenoit December 5, 2025 16:22
@pacmancoder pacmancoder marked this pull request as ready for review December 5, 2025 16:22
@pacmancoder pacmancoder force-pushed the feat/window-title-recording branch from 305f497 to 92b4729 Compare December 5, 2025 16:22
@vnikonov-devolutions vnikonov-devolutions self-assigned this Dec 5, 2025
capabilities: NowChannelCapsetMsg,
sessions: HashMap<u32, WinApiProcess>,
/// Shutdown signal sender for window monitoring task.
window_monitor_shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Add inline comment explaining why you use a oneshot channel over other obvious synchronisation primitives such as Notify. From what I’ve seen in the code, you are after the "exactly once" semantic that Notify does not provide.

use std::path::PathBuf;
use std::time::SystemTime;

use anyhow::{Context, Result, bail};
Copy link
Member

@CBenoit CBenoit Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Do not use anyhow::Result, instead use anyhow::Result everywhere.

rationale: Makes it clear which Result it is at the callsite. Don’t shadow the default std::result::Result part of the prelude.

This is part of the IronRDP code style.

/// storage to communicate with the async runtime. This is safe because WINEVENT_OUTOFCONTEXT
/// ensures the callback runs on the same thread that installed the hook.
struct HookContext {
sender: mpsc::UnboundedSender<WindowSnapshot>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Unbounded channels are typically a yellow/red-ish flag. If you really need one, document why it’s really better than using a bounded channel with proper backpressure.

Comment on lines +237 to +238
/// This is only accessed from the hook thread, making it immutable after initialization.
static HOOK_CONTEXT: RefCell<Option<HookContext>> = const { RefCell::new(None) };
Copy link
Member

@CBenoit CBenoit Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You might be interested into OnceCell for the "initialize once, never change" semantics in a thread local: https://doc.rust-lang.org/std/cell/struct.OnceCell.html

EDIT: Reading further, I does not seem to be as immutable as the comment make it sound. I understand this is repeatedly updated each time an ActiveWindowHook is created, and cleaned when the hook is unregistered. The value stored inside the cell is indeed never changed during that span.

break;
}

// SAFETY: DispatchMessageW is safe to call with a valid MSG structure obtained from GetMessageW.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: More straightforward statements reads better: "msg is a valid MSG structure obtained from GetMessageW."

Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! Feel free to adjust the code and merge yourself!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants