use super::super::core::{
self, AppInfo, Capture, CaptureBudget, ElementAct, Impact, Locator, Observation, RawInput,
SurfaceGeometry, TargetSpec, WindowInfo, WindowListing,
};
use super::capture::CaptureContext;
use super::{input, seh, uia, window};
use crate::util::UnwrapPoison;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use windows::Win32::System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance};
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize};
use windows::Win32::UI::Accessibility::{CUIAutomation8, IUIAutomation, IUIAutomation2};
use windows::core::Interface as _;
const CONNECTION_TIMEOUT_MS: u32 = 3_000;
const TRANSACTION_TIMEOUT_MS: u32 = 10_000;
pub(super) enum Call {
ListApps,
ListWindows {
app: Option<AppInfo>,
},
FocusedWindow,
SurfaceGeometry {
target: TargetSpec,
},
Observe {
target: TargetSpec,
},
Act {
target: TargetSpec,
locator: Locator,
act: ElementAct,
},
RawInput {
target: TargetSpec,
input: RawInput,
},
CursorPosition,
Capture {
target: TargetSpec,
budget: CaptureBudget,
},
}
impl Call {
fn label(&self) -> String {
match self {
Call::ListApps => "the running applications".to_string(),
Call::ListWindows { app } => match app {
Some(app) => format!("the windows of {}", app.name),
None => "the open windows".to_string(),
},
Call::FocusedWindow => "the focused window".to_string(),
Call::SurfaceGeometry { target }
| Call::Observe { target }
| Call::Act { target, .. }
| Call::RawInput { target, .. }
| Call::Capture { target, .. } => target.describe(),
Call::CursorPosition => "the pointer".to_string(),
}
}
}
pub(super) enum Reply {
Apps(Vec<AppInfo>),
Windows(WindowListing),
Window(Box<WindowInfo>),
Geometry(SurfaceGeometry),
Observation(Box<Observation>),
Act(Impact),
Point(f64, f64),
Capture(Box<Capture>),
}
pub(super) trait FromReply: Sized {
fn from_reply(reply: Reply) -> anyhow::Result<Self>;
}
macro_rules! from_reply {
($($ty:ty: $pattern:pat => $value:expr),* $(,)?) => {$(
impl FromReply for $ty {
fn from_reply(reply: Reply) -> anyhow::Result<Self> {
match reply {
$pattern => Ok($value),
_ => Err(reply_mismatch()),
}
}
}
)*};
}
from_reply! {
Vec<AppInfo>: Reply::Apps(value) => value,
WindowListing: Reply::Windows(value) => value,
WindowInfo: Reply::Window(value) => *value,
SurfaceGeometry: Reply::Geometry(value) => value,
Observation: Reply::Observation(value) => *value,
Impact: Reply::Act(value) => value,
(f64, f64): Reply::Point(x, y) => (x, y),
Capture: Reply::Capture(value) => *value,
}
fn reply_mismatch() -> anyhow::Error {
core::taxonomy_error(
core::ERR_UNSUPPORTED,
"internal computer-use reply mismatch — a bug in this backend, not a retryable condition; \
report it",
)
}
struct Job {
call: Call,
reply: tokio::sync::oneshot::Sender<anyhow::Result<Reply>>,
}
struct Worker {
tx: Sender<Job>,
alive: Arc<AtomicBool>,
}
static WORKER: LazyLock<Mutex<Option<Worker>>> = LazyLock::new(|| Mutex::new(None));
static LEAKED_THREADS: AtomicU64 = AtomicU64::new(0);
enum JobFailure {
Call(anyhow::Error),
Stopped,
Deadline {
timeout: Duration,
label: String,
},
}
async fn job_reply(call: Call, timeout: Duration) -> Result<Reply, JobFailure> {
let label = call.label();
let (tx, alive) = sender().map_err(JobFailure::Call)?;
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
if tx
.send(Job {
call,
reply: reply_tx,
})
.is_err()
{
alive.store(false, Ordering::SeqCst);
return Err(JobFailure::Call(core::taxonomy_error(
core::ERR_DEGRADED,
"the computer-use thread stopped before it accepted the call — nothing was delivered; \
retry",
)));
}
match tokio::time::timeout(timeout, reply_rx).await {
Ok(Ok(Ok(reply))) => Ok(reply),
Ok(Ok(Err(e))) => Err(JobFailure::Call(e)),
Ok(Err(_)) => {
alive.store(false, Ordering::SeqCst);
Err(JobFailure::Stopped)
}
Err(_) => {
leak(&alive);
Err(JobFailure::Deadline { timeout, label })
}
}
}
fn leak(alive: &AtomicBool) {
alive.store(false, Ordering::SeqCst);
let count = LEAKED_THREADS.fetch_add(1, Ordering::SeqCst) + 1;
tracing::warn!(
leaked_threads = count,
"computer use: abandoned a hung worker thread — {count} left behind since this process \
started"
);
}
fn leaked_note() -> String {
let count = LEAKED_THREADS.load(Ordering::SeqCst);
let (threads, have) = if count == 1 {
("thread", "has")
} else {
("threads", "have")
};
format!(
"; {count} computer-use worker {threads} {have} been left behind by hangs since start — \
the next call starts a fresh one"
)
}
pub(super) async fn submit<T: FromReply>(call: Call, timeout: Duration) -> anyhow::Result<T> {
match job_reply(call, timeout).await {
Ok(reply) => T::from_reply(reply),
Err(JobFailure::Call(e)) => Err(e),
Err(JobFailure::Stopped) => Err(core::taxonomy_error(
core::ERR_DEGRADED,
"the computer-use thread stopped while running the call — retry",
)),
Err(JobFailure::Deadline { timeout, label }) => Err(core::taxonomy_error(
core::ERR_DEGRADED,
format!(
"{label} did not respond within {}s — the call was abandoned without a result; \
retry{}",
timeout.as_secs(),
leaked_note()
),
)),
}
}
pub(super) async fn submit_act(call: Call, timeout: Duration) -> anyhow::Result<Impact> {
match job_reply(call, timeout).await {
Ok(reply) => Impact::from_reply(reply),
Err(JobFailure::Call(e)) if core::is_framework_failed(&e) => {
Ok(Impact::Unconfirmed(format!(
"the system raised a structured exception while the call was running, so its input \
may be partly applied — {e}"
)))
}
Err(JobFailure::Call(e)) => Err(e),
Err(JobFailure::Stopped) => Ok(Impact::Unconfirmed(String::from(
"the computer-use thread stopped while running the call, so its input may or may not \
have been delivered",
))),
Err(JobFailure::Deadline { timeout, label }) => Ok(Impact::Unconfirmed(format!(
"{label} did not answer within {}s, so the call was abandoned while the job may still \
complete on the leaked thread — the input may land after this answer{}",
timeout.as_secs(),
leaked_note()
))),
}
}
fn sender() -> anyhow::Result<(Sender<Job>, Arc<AtomicBool>)> {
let mut guard = WORKER.lock().unwrap_poison();
if let Some(worker) = guard.as_ref()
&& worker.alive.load(Ordering::SeqCst)
{
return Ok((worker.tx.clone(), Arc::clone(&worker.alive)));
}
let alive = Arc::new(AtomicBool::new(true));
let (tx, rx) = std::sync::mpsc::channel();
let thread_alive = Arc::clone(&alive);
std::thread::Builder::new()
.name("mahbot-computer".to_string())
.spawn(move || run(&rx, &thread_alive))
.map_err(|e| {
core::taxonomy_error(
core::ERR_DEGRADED,
format!("failed to start the computer-use thread: {e}"),
)
})?;
*guard = Some(Worker {
tx: tx.clone(),
alive: Arc::clone(&alive),
});
Ok((tx, alive))
}
fn run(rx: &Receiver<Job>, alive: &AtomicBool) {
let mut ctx = match seh::guard(Ctx::open) {
Ok(Ok(ctx)) => ctx,
Ok(Err(e)) => return retire(rx, alive, &format!("{e:#}")),
Err(code) => {
let error = framework_failed("open the computer-use apartment", code);
let message = format!("{error:#}");
tracing::warn!("computer use: {message}");
return retire(rx, alive, &message);
}
};
while let Ok(job) = rx.recv() {
if !alive.load(Ordering::SeqCst) {
let error = unstarted();
let message = error.to_string();
let _ = job.reply.send(Err(error));
retire(rx, alive, &message);
break;
}
match seh::guard(|| ctx.execute(job.call)) {
Ok(result) => {
let _ = job.reply.send(result);
}
Err(code) => {
let error = framework_failed("run the computer-use call", code);
let message = format!("{error:#}");
tracing::warn!("computer use: {message}; retiring the thread");
let _ = job.reply.send(Err(error));
std::mem::forget(ctx);
return retire(rx, alive, &message);
}
}
}
if let Err(code) = seh::guard(move || drop(ctx)) {
let error = framework_failed("close the computer-use apartment", code);
tracing::warn!("computer use: {error:#}");
}
}
fn unstarted() -> anyhow::Error {
core::taxonomy_error(
core::ERR_DEGRADED,
"the computer-use thread was retired before this call started — nothing was delivered; \
retry",
)
}
fn retire(rx: &Receiver<Job>, alive: &AtomicBool, message: &str) {
alive.store(false, Ordering::SeqCst);
for job in rx {
let _ = job.reply.send(Err(anyhow::anyhow!(message.to_owned())));
}
}
fn framework_failed(what: &str, code: u32) -> anyhow::Error {
core::framework_failed(
what,
format!("the system raised structured exception 0x{code:08X}"),
)
}
struct Ctx {
uia: IUIAutomation,
capture: CaptureContext,
}
impl Ctx {
fn open() -> anyhow::Result<Self> {
unsafe { RoInitialize(RO_INIT_MULTITHREADED) }.map_err(|e| {
core::taxonomy_error(
core::ERR_DEGRADED,
format!("failed to initialize the computer-use apartment: {e}"),
)
})?;
window::ensure_dpi_aware();
let uia: IUIAutomation = unsafe {
CoCreateInstance(
&CUIAutomation8,
None::<&windows::core::IUnknown>,
CLSCTX_INPROC_SERVER,
)
}
.map_err(|e| {
core::taxonomy_error(
core::ERR_DEGRADED,
format!("UI Automation is unavailable: {e}"),
)
})?;
if let Ok(uia2) = uia.cast::<IUIAutomation2>() {
unsafe {
let _ = uia2.SetConnectionTimeout(CONNECTION_TIMEOUT_MS);
let _ = uia2.SetTransactionTimeout(TRANSACTION_TIMEOUT_MS);
}
}
Ok(Self {
uia,
capture: CaptureContext::default(),
})
}
fn execute(&mut self, call: Call) -> anyhow::Result<Reply> {
match call {
Call::ListApps => Ok(Reply::Apps(window::enumerate_apps())),
Call::ListWindows { app } => Ok(Reply::Windows(window::list_windows(app.as_ref()))),
Call::FocusedWindow => Ok(Reply::Window(Box::new(window::focused_window()?))),
Call::SurfaceGeometry { target } => {
Ok(Reply::Geometry(window::surface_geometry(&target)?))
}
Call::Observe { target } => Ok(Reply::Observation(Box::new(uia::observe(
&self.uia, &target,
)?))),
Call::Act {
target,
locator,
act,
} => Ok(Reply::Act(uia::act_on_element(
&self.uia, &target, &locator, &act,
)?)),
Call::RawInput { target, input } => Ok(Reply::Act(input::raw_input(&target, input)?)),
Call::CursorPosition => {
let (x, y) = input::cursor_position()?;
Ok(Reply::Point(x, y))
}
Call::Capture { target, budget } => Ok(Reply::Capture(Box::new(
self.capture.capture(&target, &budget)?,
))),
}
}
}