161 lines
5.8 KiB
Rust
161 lines
5.8 KiB
Rust
//! Windows implementation of computer use actions using the Win32 SendInput
|
|
//! API for input and GDI for screenshots.
|
|
|
|
mod dpi;
|
|
mod keyboard;
|
|
mod mouse;
|
|
mod screenshot;
|
|
|
|
use async_trait::async_trait;
|
|
use galaxyui::r#async::Timer;
|
|
use windows::Win32::System::StationsAndDesktops::{
|
|
CloseDesktop, DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, HDESK, OpenInputDesktop,
|
|
};
|
|
|
|
use crate::{Action, ActionResult, Options};
|
|
|
|
/// Returns whether computer_use can drive input on this machine right now.
|
|
///
|
|
/// Reports `false` when there is no accessible input desktop (e.g., the process is running under
|
|
/// Session 0 as a Windows service, the workstation is locked, or the user has switched to a
|
|
/// different secure desktop). In those cases `SendInput` silently no-ops and GDI desktop capture
|
|
/// fails, so we'd rather fail fast here than surface the error mid-action.
|
|
pub fn is_supported_on_current_platform() -> bool {
|
|
probe_input_desktop_available()
|
|
}
|
|
|
|
/// Shared probe used by both [`is_supported_on_current_platform`] and [`Actor::new`] so the
|
|
/// "can we drive input right now?" logic lives in one place. This still runs the probe on each
|
|
/// call (it's a cheap `OpenInputDesktop` / `CloseDesktop` round-trip) — we don't cache it because
|
|
/// availability can change at runtime (workstation lock, secure desktop swap, Remote Desktop
|
|
/// reconnect).
|
|
fn probe_input_desktop_available() -> bool {
|
|
InputDesktop::acquire().is_some()
|
|
}
|
|
|
|
/// RAII wrapper for an `HDESK` returned by `OpenInputDesktop`. Guarantees the handle is closed
|
|
/// (or at least that a close attempt is made and logged on failure) even if the caller returns
|
|
/// early. Modeled after the GDI handle guards in `screenshot.rs`.
|
|
struct InputDesktop(HDESK);
|
|
|
|
impl InputDesktop {
|
|
fn acquire() -> Option<Self> {
|
|
// SAFETY: `OpenInputDesktop` has no preconditions. We pass `false` for inheritance and
|
|
// request no specific access (just probing for existence).
|
|
let handle =
|
|
unsafe { OpenInputDesktop(DESKTOP_CONTROL_FLAGS(0), false, DESKTOP_ACCESS_FLAGS(0)) };
|
|
handle.ok().map(Self)
|
|
}
|
|
}
|
|
|
|
impl Drop for InputDesktop {
|
|
fn drop(&mut self) {
|
|
// SAFETY: `self.0` is a valid HDESK returned by `OpenInputDesktop` and has not been
|
|
// closed yet.
|
|
unsafe {
|
|
if let Err(e) = CloseDesktop(self.0) {
|
|
log::warn!("CloseDesktop failed in InputDesktop::drop: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Actor holds Keyboard/Mouse state unconditionally — both are cheap to construct and have no
|
|
/// side effects — so a `perform_actions` call can recover as soon as an input desktop is
|
|
/// reachable again, even if `Actor::new` ran while the desktop was temporarily inaccessible
|
|
/// (workstation locked at startup, RDP disconnect, etc.). Supportability is decided per call by
|
|
/// [`probe_input_desktop_available`] rather than being cached in the actor's shape.
|
|
pub struct Actor {
|
|
keyboard: keyboard::Keyboard,
|
|
mouse: mouse::Mouse,
|
|
}
|
|
|
|
impl Actor {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
keyboard: keyboard::Keyboard::new(),
|
|
mouse: mouse::Mouse::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Actor {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Error returned by `perform_actions` when the input desktop is inaccessible at call time
|
|
/// (workstation lock, secure desktop swap, RDP reconnect, Session 0 service, …).
|
|
const NO_INPUT_DESKTOP_ERROR: &str = "Computer use is not available: no accessible input desktop";
|
|
|
|
#[async_trait]
|
|
impl super::Actor for Actor {
|
|
fn platform(&self) -> Option<super::Platform> {
|
|
// Live probe so callers can use `platform().is_some()` as a current "can drive input"
|
|
// signal. Matches the Linux `Unsupported`-returns-None convention.
|
|
if probe_input_desktop_available() {
|
|
Some(super::Platform::Windows)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
async fn perform_actions(
|
|
&mut self,
|
|
actions: &[Action],
|
|
options: Options,
|
|
) -> Result<ActionResult, String> {
|
|
// Probe at the top of every call so transient loss of the input desktop (workstation
|
|
// lock, secure desktop swap, RDP reconnect) surfaces as a descriptive error instead of
|
|
// letting `SendInput` silently no-op. Cheap `OpenInputDesktop`/`CloseDesktop` round-trip.
|
|
if !probe_input_desktop_available() {
|
|
return Err(NO_INPUT_DESKTOP_ERROR.to_string());
|
|
}
|
|
let keyboard = &mut self.keyboard;
|
|
let mouse = &mut self.mouse;
|
|
|
|
for action in actions {
|
|
match action {
|
|
Action::Wait(duration) => {
|
|
Timer::after(*duration).await;
|
|
}
|
|
Action::MouseDown { button, at } => {
|
|
mouse.move_to(*at)?;
|
|
mouse.button_down(button)?;
|
|
}
|
|
Action::MouseUp { button } => mouse.button_up(button)?,
|
|
Action::MouseMove { to } => mouse.move_to(*to)?,
|
|
Action::MouseWheel {
|
|
at,
|
|
direction,
|
|
distance,
|
|
} => {
|
|
mouse.move_to(*at)?;
|
|
mouse.scroll(direction, distance)?;
|
|
}
|
|
Action::TypeText { text } => {
|
|
keyboard.type_text(text)?;
|
|
}
|
|
Action::KeyDown { key } => {
|
|
keyboard.key_down(key)?;
|
|
}
|
|
Action::KeyUp { key } => {
|
|
keyboard.key_up(key)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
let screenshot = if let Some(params) = options.screenshot_params {
|
|
Some(screenshot::take(params)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(ActionResult {
|
|
screenshot,
|
|
cursor_position: Some(mouse.current_position()?),
|
|
})
|
|
}
|
|
}
|