use crate::common::{
fetch_events, get_random_device_name, get_virtual_device, key_press, key_release, wait_for_device,
wait_for_grabbed, VirtualDeviceInfo,
};
use anyhow::{bail, Result};
use evdev::{Device, EventType, FetchEventsSynced, InputEvent, KeyCode as Key};
use std::cell::Cell;
use std::iter::repeat_with;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use xremap::device::SEPARATOR;
use xremap::util::until;
pub enum InputDeviceFilter {
NoFilter,
RandomName,
CustomFilter { filter: String },
}
pub struct XremapBuilder {
nocapture_: bool,
log_level_: String,
allow_stdio_errors_: bool,
custom_input_device_: InputDeviceFilter,
ignore_device_: Option<String>,
open_for_fetch_: bool,
watch_: bool,
watch_config_: bool,
config_file: Option<String>,
}
impl XremapBuilder {
fn new() -> Self {
Self {
nocapture_: false,
log_level_: "debug".into(),
allow_stdio_errors_: false,
custom_input_device_: InputDeviceFilter::RandomName,
ignore_device_: None,
open_for_fetch_: true,
watch_: false,
watch_config_: false,
config_file: None,
}
}
pub fn nocapture(&mut self) -> &mut Self {
self.nocapture_ = true;
self
}
pub fn log_level(&mut self, log_level: impl Into<String>) -> &mut Self {
self.log_level_ = log_level.into();
self
}
pub fn allow_stdio_errors(&mut self, allow_stdio_errors: bool) -> &mut Self {
self.allow_stdio_errors_ = allow_stdio_errors;
self
}
pub fn input_device(&mut self, filter: InputDeviceFilter) -> &mut Self {
self.custom_input_device_ = filter;
self
}
pub fn ignore_device(&mut self, name: impl Into<String>) -> &mut Self {
self.ignore_device_ = Some(name.into());
self
}
pub fn not_open_for_fetch(&mut self) -> &mut Self {
self.open_for_fetch_ = false;
self
}
pub fn watch(&mut self, value: bool) -> &mut Self {
self.watch_ = value;
self
}
pub fn watch_config(&mut self, config: &str) -> Result<&mut Self> {
self.config(config)?;
self.watch_config_ = true;
Ok(self)
}
pub fn config(&mut self, config: &str) -> Result<&mut Self> {
let filename =
format!("xremap_config_{}.yml", repeat_with(fastrand::alphanumeric).take(10).collect::<String>());
let config_file = std::env::temp_dir().join(filename);
self.config_file = Some(config_file.to_string_lossy().into());
std::fs::write(&config_file, config)?;
Ok(self)
}
pub fn build(&mut self) -> anyhow::Result<XremapController> {
XremapController::from_builder(self)
}
}
#[derive(Debug)]
pub struct Output {
pub stdout: String,
pub stderr: String,
}
pub struct XremapController {
child: Cell<Option<Child>>,
nocapture: bool,
allow_stdio_errors: bool,
input_device: Option<VirtualDeviceInfo>,
output_device_name: String,
output_device: Option<Device>,
device_filter: Option<String>,
config_file: String,
}
impl XremapController {
pub fn builder() -> XremapBuilder {
XremapBuilder::new()
}
pub fn new() -> anyhow::Result<Self> {
XremapController::builder().build()
}
fn from_builder(def: &XremapBuilder) -> anyhow::Result<Self> {
let path = match std::env::var("CARGO_TARGET_DIR") {
Ok(path) => PathBuf::from(path).join("debug/xremap"),
Err(_) => PathBuf::from("target/debug/xremap"),
};
let mut command = Command::new(path);
let output_device_name =
format!("test output device {}", repeat_with(fastrand::alphanumeric).take(10).collect::<String>());
let builder = command
.env("RUST_LOG", &def.log_level_)
.args(vec!["--output-device-name", &output_device_name]);
let config_file = match &def.config_file {
Some(config) => {
println!("Using custom config: {:?}", config);
builder.arg(config);
config
}
None => {
let config = "tests/common/config-test.yml";
builder.arg(config);
config
}
};
if !def.nocapture_ {
builder.stdout(Stdio::piped()).stderr(Stdio::piped());
}
let mut input_device: Option<VirtualDeviceInfo> = None;
if def.watch_ {
builder.arg("--watch");
}
if def.watch_config_ {
builder.arg("--watch=config");
}
let device_filter = match &def.custom_input_device_ {
InputDeviceFilter::NoFilter => {
None
}
InputDeviceFilter::RandomName => {
let name = get_random_device_name();
input_device = Some(get_virtual_device(&name)?);
Some(name)
}
InputDeviceFilter::CustomFilter { filter } => Some(filter.clone()),
};
if let Some(device_filter) = &device_filter {
builder.arg("--device").arg(device_filter);
}
if let Some(ignore_device) = &def.ignore_device_ {
builder.arg("--ignore").arg(ignore_device);
}
let child = builder.spawn()?;
let mut ctrl = Self {
child: Cell::new(Some(child)),
nocapture: def.nocapture_,
allow_stdio_errors: def.allow_stdio_errors_,
input_device,
output_device_name,
output_device: None,
device_filter,
config_file: config_file.to_string(),
};
match &ctrl.input_device {
None => {
println!("No input device configured for xremap.");
}
Some(input_device) => {
wait_for_grabbed(&input_device.path)?;
println!("Input device grabbed by xremap");
}
}
if def.open_for_fetch_ {
ctrl.open_output_device()?;
}
Ok(ctrl)
}
pub fn get_input_device_name<'a>(&'a mut self) -> &'a Option<String> {
&self.device_filter
}
pub fn get_config_file<'a>(&'a mut self) -> &'a str {
&self.config_file
}
pub fn open_input_device(&mut self, name: impl Into<String>) -> anyhow::Result<()> {
if self.input_device.is_some() {
bail!("Input device already opened.")
}
println!("Preparing new input device for xremap, that is already running.");
let dev_info = get_virtual_device(name)?;
wait_for_grabbed(&dev_info.path)?;
println!("Input device grabbed by xremap");
self.input_device = Some(dev_info);
Ok(())
}
pub fn close_input_device(&mut self) -> anyhow::Result<()> {
if self.input_device.is_none() {
bail!("Input device not opened.")
}
self.input_device = None;
println!("Input device closed");
Ok(())
}
pub fn open_output_device(&mut self) -> anyhow::Result<()> {
if self.output_device.is_some() {
bail!("Output device already opened.")
}
let (_, mut device) = wait_for_device(&self.output_device_name)?;
device.grab()?;
println!("Output device from xremap grabbed");
self.output_device = Some(device);
Ok(())
}
pub fn emit_events(&mut self, events: &[InputEvent]) -> anyhow::Result<()> {
let input_device = self.input_device.as_mut().expect("Input device is not opened");
let mut probe_device = Device::open(&input_device.path)?;
if probe_device.grab().is_ok() {
probe_device.ungrab()?;
bail!("Input device not grabbed.");
};
Ok(input_device.device.emit(events)?)
}
pub fn fetch_events(&mut self) -> anyhow::Result<FetchEventsSynced<'_>> {
fetch_events(self.output_device.as_mut().expect("Output device is not opened"))
}
pub fn fetch_until_key(&mut self, key: Key) -> anyhow::Result<Vec<InputEvent>> {
let start = Instant::now();
let mut done = false;
let mut result: Vec<InputEvent> = vec![];
loop {
if done {
break;
}
if Instant::now().duration_since(start) > Duration::from_secs(1) {
break;
}
let events = self.fetch_events()?;
for event in events {
if event.event_type() == EventType::KEY && event.code() == key.0 && event.value() == 0 {
done = true;
}
result.push(event);
}
}
Ok(result)
}
pub fn fetch(&mut self) -> anyhow::Result<Vec<InputEvent>> {
self.emit_events(&vec![key_press(Key::KEY_MOVE), key_release(Key::KEY_MOVE)])?;
Ok(self.fetch_until_key(Key::KEY_MOVE)?)
}
pub fn kill_for_output(&mut self) -> anyhow::Result<Output> {
self.raw_kill()?;
self.wait_for_output()
}
pub fn wait_for_output(&self) -> anyhow::Result<Output> {
if self.nocapture {
bail!("Can't get output when configured for nocapture.");
}
let child = self.child.take().expect("Output is already fetched.");
self.wait_for_output_inner(child)
}
fn wait_for_output_inner(&self, mut child: Child) -> anyhow::Result<Output> {
println!("Waiting for xremap to exit");
let is_stopped = until(
|| child.try_wait().is_ok_and(|val| !val.is_none()),
Duration::from_secs(1),
"Timed out waiting for xremap exit",
);
if is_stopped.is_err() {
child.kill()?;
println!("Xremap killed");
};
let res = child.wait_with_output()?;
println!("Xremap stopped");
let stdout = String::from_utf8(res.stdout)?;
let stderr = String::from_utf8(res.stderr)?;
if self.nocapture {
assert_eq!("", stdout);
assert_eq!("", stderr);
} else {
println!("{SEPARATOR}");
println!("stdout:\n{stdout}");
println!("{SEPARATOR}");
println!("stderr:\n{stderr}");
println!("{SEPARATOR}");
}
if !self.allow_stdio_errors {
Self::check_stdio(&stdout)?;
Self::check_stdio(&stderr)?;
}
match is_stopped {
Ok(_) => Ok(Output { stdout, stderr }),
Err(e) => Err(e),
}
}
fn check_stdio(stdio: &str) -> anyhow::Result<()> {
let stdio = stdio.replace("Failed to ungrab device: No such device (os error 19)", "");
let stdio = stdio.to_ascii_lowercase();
if stdio.contains("fail")
|| stdio.contains("fatal")
|| stdio.contains("error")
|| stdio.contains("panic")
|| stdio.contains("warn")
{
bail!("Stdio contained an error message")
}
Ok(())
}
fn raw_kill(&self) -> anyhow::Result<()> {
let mut child = self.child.take().expect("Output is already fetched.");
let result = child.kill();
println!("Xremap killed");
self.child.set(Some(child));
Ok(result?)
}
pub fn kill(&self) -> anyhow::Result<()> {
if let Some(mut child) = self.child.take() {
if child.try_wait()?.is_none() {
child.kill()?;
println!("Xremap killed");
} else {
println!("Xremap already stopped when attempting to kill.");
}
let _ = self.wait_for_output_inner(child)?;
} else {
println!("Some sort of shutdown has already been requested.");
}
Ok(())
}
}
impl Drop for XremapController {
fn drop(&mut self) {
println!("XremapController dropped");
self.child.take().map(|child| {
match self.wait_for_output_inner(child) {
Ok(_) => {
}
Err(err) => {
println!("While dropping xremap command: {err:?}");
}
}
});
}
}