refactor(e2e-agent): split the protocol from the verbs of a platform

The agent of the Windows guest speaks a line protocol (one JSON request, one JSON answer, raw bytes
after the line for put_file and get_file) that has nothing to do with Windows. Move it into
protocol.rs, with a trait for the verbs a platform adds, so that another platform answers the same
protocol instead of copying it, and the host has one client.

The Windows verbs (os_windows, displays, activate, minimize, close) and the code that reads them
from the session are moved to windows.rs, not changed; the agent still serves each connection
on a thread of its own. What the protocol gains for every platform: an environment and an output
file for a spawned program, a `status` of one, and a `send` to its standard input, which the
Windows agent does not use yet. The port is one for every platform, as the Windows one already was.
This commit is contained in:
LGUG2Z committed 2026-11-06 14:40:49 -08:00
1 parent 0f5cfc8b99
commit 71a9018e4b
4 files changed
+441 -301

No files matched your search

+2
View File
@@ -8,4 +8,6 @@ publish = false
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
+17 -301
View File
@@ -1,307 +1,23 @@
//! The guest half of the e2e suite on Windows.
//! The guest half of the e2e suite.
//!
//! It must run inside the interactive session (a Run entry of the autologon
//! user), because session 0 has no usable desktop. The host talks to it over
//! a TCP port that QEMU forwards from the host's loopback. One request is one
//! line of JSON, `{"id": 1, "verb": "...", ...}`, answered by one line,
//! `{"id": 1, "ok": ...}` or `{"id": 1, "error": "..."}`.
//!
//! `put_file` and `get_file` move bytes after the JSON line, as the `size`
//! raw bytes that the request or the answer announces, so that a daemon of
//! tens of megabytes is not wrapped in JSON.
//!
//! The agent injects no input: keys, pointer and screenshots come from the
//! host (QMP), which also works at the lock screen.
//! It runs inside the interactive session of the guest, and the host talks to it over TCP: the
//! protocol is the same on every platform (`protocol.rs`), and what a platform adds to it is the
//! windows and the displays of its own desktop (`windows.rs`, `macos.rs`).
#![cfg(windows)]
// no console window: it would be a window for the daemon to tile
#![windows_subsystem = "windows"]
// no console window on Windows: it would be a window for the daemon to tile
#![cfg_attr(windows, windows_subsystem = "windows")]
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::TcpListener;
use std::net::TcpStream;
use std::os::windows::process::CommandExt;
use std::process::Command;
use serde::Deserialize;
use serde_json::Value;
use serde_json::json;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::MONITORINFO;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetClassNameW;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::core::BOOL;
const DEFAULT_PORT: u16 = 7878;
/// A windowless program has nowhere to print a panic, so it goes here.
const LOG: &str = "C:\\e2e\\agent.log";
fn log(message: &str) {
use std::io::Write as _;
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(LOG)
{
let _ = writeln!(file, "{message}");
}
}
/// `CREATE_NO_WINDOW`: a child must not open a console of its own either.
const NO_WINDOW: u32 = 0x0800_0000;
#[derive(Deserialize)]
struct Request {
id: Value,
verb: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
path: String,
#[serde(default)]
size: u64,
#[serde(default)]
hwnd: isize,
}
fn rect_json(r: &RECT) -> Value {
// the shape komorebi itself prints: right and bottom are width and height
json!({
"left": r.left, "top": r.top,
"right": r.right - r.left, "bottom": r.bottom - r.top,
})
}
fn hwnd_of(raw: isize) -> HWND {
HWND(raw as *mut _)
}
/// One top-level window as the operating system describes it.
fn describe(hwnd: HWND) -> Option<Value> {
let mut title = [0u16; 512];
// SAFETY: the buffer is valid for the length it is given.
let n = unsafe { GetWindowTextW(hwnd, &mut title) };
if n <= 0 {
return None;
}
let mut class = [0u16; 256];
// SAFETY: as above.
let c = unsafe { GetClassNameW(hwnd, &mut class) };
let mut frame = RECT::default();
// SAFETY: `frame` is a RECT and its size is passed.
let _ = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
(&mut frame as *mut RECT).cast(),
std::mem::size_of::<RECT>() as u32,
)
};
let mut cloaked = 0u32;
// SAFETY: `cloaked` is a u32 and its size is passed.
let _ = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
(&mut cloaked as *mut u32).cast(),
std::mem::size_of::<u32>() as u32,
)
};
// SAFETY: plain queries on a window handle.
let (visible, minimized, focused) = unsafe {
(
IsWindowVisible(hwnd).as_bool(),
IsIconic(hwnd).as_bool(),
GetForegroundWindow() == hwnd,
)
};
Some(json!({
"hwnd": hwnd.0 as isize,
"title": String::from_utf16_lossy(&title[..n as usize]),
"class": String::from_utf16_lossy(&class[..c.max(0) as usize]),
"frame": rect_json(&frame),
"visible": visible,
"minimized": minimized,
"cloaked": cloaked != 0,
"focused": focused,
}))
}
fn os_windows() -> Vec<Value> {
unsafe extern "system" fn each(hwnd: HWND, out: LPARAM) -> BOOL {
// SAFETY: `out` is the `Vec` passed below, alive for the whole call.
let out = unsafe { &mut *(out.0 as *mut Vec<Value>) };
out.extend(describe(hwnd));
BOOL(1)
}
let mut out = Vec::new();
// SAFETY: the callback only touches the `Vec` passed as the parameter.
let _ = unsafe { EnumWindows(Some(each), LPARAM(&mut out as *mut _ as isize)) };
out
}
fn displays() -> Vec<Value> {
unsafe extern "system" fn each(monitor: HMONITOR, _: HDC, _: *mut RECT, out: LPARAM) -> BOOL {
// SAFETY: `out` is the `Vec` passed below, alive for the whole call.
let out = unsafe { &mut *(out.0 as *mut Vec<Value>) };
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
// SAFETY: `info` has its size set, as the call requires.
if unsafe { GetMonitorInfoW(monitor, &mut info) }.as_bool() {
out.push(json!({
"monitor": rect_json(&info.rcMonitor),
"work_area": rect_json(&info.rcWork),
"primary": info.dwFlags & 1 != 0,
}));
}
BOOL(1)
}
let mut out = Vec::new();
// SAFETY: the callback only touches the `Vec` passed as the parameter.
let _ =
unsafe { EnumDisplayMonitors(None, None, Some(each), LPARAM(&mut out as *mut _ as isize)) };
out
}
fn answer(stream: &mut TcpStream, id: &Value, result: Result<Value, String>) {
let line = match result {
Ok(ok) => json!({"id": id, "ok": ok}),
Err(error) => json!({"id": id, "error": error}),
};
let _ = writeln!(stream, "{line}");
}
fn exec(args: &[String]) -> Result<Value, String> {
let (program, rest) = args.split_first().ok_or("exec wants a program")?;
let out = Command::new(program)
.args(rest)
.creation_flags(NO_WINDOW)
.output()
.map_err(|e| format!("{program}: {e}"))?;
Ok(json!({
"status": out.status.code(),
"stdout": String::from_utf8_lossy(&out.stdout),
"stderr": String::from_utf8_lossy(&out.stderr),
}))
}
/// Starts a program that keeps running, in this session, and returns at once.
fn spawn(args: &[String]) -> Result<Value, String> {
let (program, rest) = args.split_first().ok_or("spawn wants a program")?;
let child = Command::new(program)
.args(rest)
.creation_flags(NO_WINDOW)
.spawn()
.map_err(|e| format!("{program}: {e}"))?;
Ok(json!({"pid": child.id()}))
}
fn serve(stream: TcpStream) -> std::io::Result<()> {
let mut writer = stream.try_clone()?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line)? == 0 {
return Ok(());
}
let req: Request = match serde_json::from_str(&line) {
Ok(req) => req,
Err(e) => {
answer(&mut writer, &Value::Null, Err(format!("bad request: {e}")));
continue;
}
};
let hwnd = hwnd_of(req.hwnd);
let result = match req.verb.as_str() {
"ping" => Ok(json!("pong")),
"exec" => exec(&req.args),
"spawn" => spawn(&req.args),
"os_windows" => Ok(Value::Array(os_windows())),
"displays" => Ok(Value::Array(displays())),
"activate" => {
// SAFETY: plain calls on a window handle given by the host.
unsafe {
let _ = ShowWindow(hwnd, SW_RESTORE);
Ok(json!(SetForegroundWindow(hwnd).as_bool()))
}
}
"minimize" => {
// SAFETY: as above.
Ok(json!(unsafe { ShowWindow(hwnd, SW_MINIMIZE) }.as_bool()))
}
"close" => {
// SAFETY: as above.
unsafe { PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0)) }
.map(|()| json!(true))
.map_err(|e| e.to_string())
}
"put_file" => {
let mut bytes = vec![0u8; req.size as usize];
reader.read_exact(&mut bytes)?;
std::fs::write(&req.path, bytes)
.map(|()| json!(req.size))
.map_err(|e| format!("{}: {e}", req.path))
}
"get_file" => match std::fs::read(&req.path) {
Ok(bytes) => {
answer(&mut writer, &req.id, Ok(json!({"size": bytes.len()})));
writer.write_all(&bytes)?;
continue;
}
Err(e) => Err(format!("{}: {e}", req.path)),
},
other => Err(format!("unknown verb {other}")),
};
answer(&mut writer, &req.id, result);
}
}
mod protocol;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
fn main() -> std::io::Result<()> {
std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
let port = std::env::var("KOMOREBI_E2E_AGENT_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(DEFAULT_PORT);
// QEMU's user-mode network forwards the host port to the guest's own
// address, so loopback would not see it. The guest is on a private
// network of its own and has its firewall off (see `setup.ps1`).
let listener = TcpListener::bind(("0.0.0.0", port))?;
for stream in listener.incoming() {
let stream = stream?;
std::thread::spawn(move || {
if let Err(e) = serve(stream) {
log(&format!("connection ended: {e}"));
}
});
}
Ok(())
windows::run()
}
#[cfg(not(windows))]
fn main() {
eprintln!("komorebi-e2e-agent runs on Windows only");
std::process::exit(1);
}
+208
View File
@@ -0,0 +1,208 @@
//! What the agent says to the host, on every platform: one request is one line of JSON,
//! `{"id": 1, "verb": "...", ...}`, answered by one line, `{"id": 1, "ok": ...}` or
//! `{"id": 1, "error": "..."}`. `put_file` and `get_file` move bytes after the line, as the `size`
//! raw bytes that the request or the answer announces, so that a daemon of tens of megabytes is
//! not wrapped in JSON.
//!
//! The verbs every platform has are here: `ping`, `exec`, `spawn`, `status`, `send`, `put_file`
//! and `get_file`. A platform adds its own (the windows of its desktop, its displays) by
//! implementing [`Verbs`].
//!
//! A frame is `{left, top, right, bottom}` where `right` and `bottom` are the width and the
//! height, the shape komorebi itself prints.
use std::collections::HashMap;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::TcpStream;
use std::process::Child;
use std::process::Command;
use std::sync::Mutex;
use serde::Deserialize;
use serde_json::Value;
use serde_json::json;
/// The port the agent listens on, on every platform.
pub const DEFAULT_PORT: u16 = 7878;
/// The programs `spawn` started and that have not been looked at since they ended: the agent keeps
/// them so that `status` can say how one ended, and `send` can write to its standard input.
static CHILDREN: Mutex<Option<HashMap<u32, Child>>> = Mutex::new(None);
fn children<T>(use_them: impl FnOnce(&mut HashMap<u32, Child>) -> T) -> T {
let mut guard = CHILDREN.lock().unwrap();
use_them(guard.get_or_insert_with(HashMap::new))
}
/// A request. A verb reads the fields it needs; the others are left at their defaults.
#[derive(Deserialize)]
pub struct Request {
pub id: Value,
pub verb: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub path: String,
#[serde(default)]
pub size: u64,
/// The environment a program is started with, on top of the agent's own.
#[serde(default)]
pub env: HashMap<String, String>,
/// A file that gets what a spawned program prints, standard output and error.
#[serde(default)]
pub out: Option<String>,
/// A process id of a program `spawn` started.
#[serde(default)]
pub pid: u32,
/// A line for the standard input of that program.
#[serde(default)]
pub line: String,
/// A window handle, on a platform that has them (Windows).
#[serde(default)]
pub hwnd: isize,
}
/// Why a command is being prepared: it runs to its end, or it keeps running.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Exec,
Spawn,
}
/// What a platform adds to the protocol.
pub trait Verbs {
/// Answers a verb of this platform, or `None` if it is not one.
fn handle(&mut self, request: &Request) -> Option<Result<Value, String>>;
/// Settings of the platform for a program the agent starts (no console window on Windows, a
/// standard input that stays open on macOS).
fn prepare(&self, _command: &mut Command, _mode: Mode) {}
}
pub fn frame_json(left: i64, top: i64, width: i64, height: i64) -> Value {
json!({"left": left, "top": top, "right": width, "bottom": height})
}
fn answer(stream: &mut TcpStream, id: &Value, result: Result<Value, String>) {
let line = match result {
Ok(ok) => json!({"id": id, "ok": ok}),
Err(error) => json!({"id": id, "error": error}),
};
let _ = writeln!(stream, "{line}");
}
fn exec(request: &Request, verbs: &dyn Verbs) -> Result<Value, String> {
let (program, rest) = request.args.split_first().ok_or("exec wants a program")?;
let mut command = Command::new(program);
command.args(rest).envs(&request.env);
verbs.prepare(&mut command, Mode::Exec);
let out = command.output().map_err(|e| format!("{program}: {e}"))?;
Ok(json!({
"status": out.status.code(),
"stdout": String::from_utf8_lossy(&out.stdout),
"stderr": String::from_utf8_lossy(&out.stderr),
}))
}
/// Starts a program that keeps running, in this session, and returns at once.
fn spawn(request: &Request, verbs: &dyn Verbs) -> Result<Value, String> {
let (program, rest) = request.args.split_first().ok_or("spawn wants a program")?;
let mut command = Command::new(program);
command.args(rest).envs(&request.env);
verbs.prepare(&mut command, Mode::Spawn);
if let Some(path) = &request.out {
let file = std::fs::File::create(path).map_err(|e| format!("{path}: {e}"))?;
let again = file.try_clone().map_err(|e| format!("{path}: {e}"))?;
command.stdout(file).stderr(again);
}
let child = command.spawn().map_err(|e| format!("{program}: {e}"))?;
let pid = child.id();
children(|children| children.insert(pid, child));
Ok(json!({"pid": pid}))
}
fn status(pid: u32) -> Result<Value, String> {
children(|children| {
let child = children
.get_mut(&pid)
.ok_or_else(|| format!("{pid} was not started by this agent"))?;
match child.try_wait() {
Ok(Some(status)) => Ok(json!({"running": false, "code": status.code()})),
Ok(None) => Ok(json!({"running": true})),
Err(e) => Err(e.to_string()),
}
})
}
/// Writes a line to the standard input of a program `spawn` started.
fn send(pid: u32, line: &str) -> Result<Value, String> {
children(|children| {
let input = children
.get_mut(&pid)
.ok_or_else(|| format!("{pid} was not started by this agent"))?
.stdin
.as_mut()
.ok_or_else(|| format!("{pid} has no standard input"))?;
writeln!(input, "{line}")
.and_then(|()| input.flush())
.map(|()| json!(true))
.map_err(|e| format!("{pid}: {e}"))
})
}
/// Serves one connection until the host closes it.
pub fn serve(stream: TcpStream, verbs: &mut dyn Verbs) -> std::io::Result<()> {
let mut writer = stream.try_clone()?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line)? == 0 {
return Ok(());
}
let request: Request = match serde_json::from_str(&line) {
Ok(request) => request,
Err(e) => {
answer(&mut writer, &Value::Null, Err(format!("bad request: {e}")));
continue;
}
};
let result = match request.verb.as_str() {
"ping" => Ok(json!("pong")),
"exec" => exec(&request, verbs),
"spawn" => spawn(&request, verbs),
"status" => status(request.pid),
"send" => send(request.pid, &request.line),
"put_file" => {
let mut bytes = vec![0u8; request.size as usize];
reader.read_exact(&mut bytes)?;
std::fs::write(&request.path, bytes)
.map(|()| json!(request.size))
.map_err(|e| format!("{}: {e}", request.path))
}
"get_file" => match std::fs::read(&request.path) {
Ok(bytes) => {
answer(&mut writer, &request.id, Ok(json!({"size": bytes.len()})));
writer.write_all(&bytes)?;
continue;
}
Err(e) => Err(format!("{}: {e}", request.path)),
},
other => verbs
.handle(&request)
.unwrap_or_else(|| Err(format!("unknown verb {other}"))),
};
answer(&mut writer, &request.id, result);
}
}
/// The port from the environment, or the default.
pub fn port() -> u16 {
std::env::var("KOMOREBI_E2E_AGENT_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(DEFAULT_PORT)
}
+214
View File
@@ -0,0 +1,214 @@
//! The verbs of the Windows agent: the windows and displays of the interactive session.
//!
//! It must run inside the interactive session (a Run entry of the autologon user), because
//! session 0 has no usable desktop. The agent injects no input: keys, pointer and screenshots
//! come from the host (QMP), which also works at the lock screen.
use std::net::TcpListener;
use std::os::windows::process::CommandExt;
use std::process::Command;
use serde_json::Value;
use serde_json::json;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::MONITORINFO;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetClassNameW;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::core::BOOL;
use crate::protocol::Mode;
use crate::protocol::Request;
use crate::protocol::Verbs;
use crate::protocol::frame_json;
/// A windowless program has nowhere to print a panic, so it goes here.
const LOG: &str = "C:\\e2e\\agent.log";
fn log(message: &str) {
use std::io::Write as _;
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(LOG)
{
let _ = writeln!(file, "{message}");
}
}
/// `CREATE_NO_WINDOW`: a child must not open a console of its own either.
const NO_WINDOW: u32 = 0x0800_0000;
fn rect_json(r: &RECT) -> Value {
frame_json(
r.left.into(),
r.top.into(),
(r.right - r.left).into(),
(r.bottom - r.top).into(),
)
}
fn hwnd_of(raw: isize) -> HWND {
HWND(raw as *mut _)
}
/// One top-level window as the operating system describes it.
fn describe(hwnd: HWND) -> Option<Value> {
let mut title = [0u16; 512];
// SAFETY: the buffer is valid for the length it is given.
let n = unsafe { GetWindowTextW(hwnd, &mut title) };
if n <= 0 {
return None;
}
let mut class = [0u16; 256];
// SAFETY: as above.
let c = unsafe { GetClassNameW(hwnd, &mut class) };
let mut frame = RECT::default();
// SAFETY: `frame` is a RECT and its size is passed.
let _ = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
(&mut frame as *mut RECT).cast(),
std::mem::size_of::<RECT>() as u32,
)
};
let mut cloaked = 0u32;
// SAFETY: `cloaked` is a u32 and its size is passed.
let _ = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
(&mut cloaked as *mut u32).cast(),
std::mem::size_of::<u32>() as u32,
)
};
// SAFETY: plain queries on a window handle.
let (visible, minimized, focused) = unsafe {
(
IsWindowVisible(hwnd).as_bool(),
IsIconic(hwnd).as_bool(),
GetForegroundWindow() == hwnd,
)
};
Some(json!({
"hwnd": hwnd.0 as isize,
"title": String::from_utf16_lossy(&title[..n as usize]),
"class": String::from_utf16_lossy(&class[..c.max(0) as usize]),
"frame": rect_json(&frame),
"visible": visible,
"minimized": minimized,
"cloaked": cloaked != 0,
"focused": focused,
}))
}
fn os_windows() -> Vec<Value> {
unsafe extern "system" fn each(hwnd: HWND, out: LPARAM) -> BOOL {
// SAFETY: `out` is the `Vec` passed below, alive for the whole call.
let out = unsafe { &mut *(out.0 as *mut Vec<Value>) };
out.extend(describe(hwnd));
BOOL(1)
}
let mut out = Vec::new();
// SAFETY: the callback only touches the `Vec` passed as the parameter.
let _ = unsafe { EnumWindows(Some(each), LPARAM(&mut out as *mut _ as isize)) };
out
}
fn displays() -> Vec<Value> {
unsafe extern "system" fn each(monitor: HMONITOR, _: HDC, _: *mut RECT, out: LPARAM) -> BOOL {
// SAFETY: `out` is the `Vec` passed below, alive for the whole call.
let out = unsafe { &mut *(out.0 as *mut Vec<Value>) };
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
// SAFETY: `info` has its size set, as the call requires.
if unsafe { GetMonitorInfoW(monitor, &mut info) }.as_bool() {
out.push(json!({
"monitor": rect_json(&info.rcMonitor),
"work_area": rect_json(&info.rcWork),
"primary": info.dwFlags & 1 != 0,
}));
}
BOOL(1)
}
let mut out = Vec::new();
// SAFETY: the callback only touches the `Vec` passed as the parameter.
let _ =
unsafe { EnumDisplayMonitors(None, None, Some(each), LPARAM(&mut out as *mut _ as isize)) };
out
}
struct WindowsVerbs;
impl Verbs for WindowsVerbs {
fn prepare(&self, command: &mut Command, _mode: Mode) {
command.creation_flags(NO_WINDOW);
}
fn handle(&mut self, request: &Request) -> Option<Result<Value, String>> {
let hwnd = hwnd_of(request.hwnd);
Some(match request.verb.as_str() {
"os_windows" => Ok(Value::Array(os_windows())),
"displays" => Ok(Value::Array(displays())),
"activate" => {
// SAFETY: plain calls on a window handle given by the host.
unsafe {
let _ = ShowWindow(hwnd, SW_RESTORE);
Ok(json!(SetForegroundWindow(hwnd).as_bool()))
}
}
"minimize" => {
// SAFETY: as above.
Ok(json!(unsafe { ShowWindow(hwnd, SW_MINIMIZE) }.as_bool()))
}
"close" => {
// SAFETY: as above.
unsafe { PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0)) }
.map(|()| json!(true))
.map_err(|e| e.to_string())
}
_ => return None,
})
}
}
pub fn run() -> std::io::Result<()> {
std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
// QEMU's user-mode network forwards the host port to the guest's own
// address, so loopback would not see it. The guest is on a private
// network of its own and has its firewall off (see `setup.ps1`).
let listener = TcpListener::bind(("0.0.0.0", crate::protocol::port()))?;
for stream in listener.incoming() {
let stream = stream?;
std::thread::spawn(move || {
if let Err(e) = crate::protocol::serve(stream, &mut WindowsVerbs) {
log(&format!("connection ended: {e}"));
}
});
}
Ok(())
}