From 02f84dfd136f5211cec3cc3159ad0a49e97334ac Mon Sep 17 00:00:00 2001 From: William Edwards Date: Mon, 19 Jan 2026 13:27:22 -0800 Subject: [PATCH 1/2] fix(Device Hiding): move hidden device nodes to /dev/inputplumber/sources --- src/main.rs | 6 +++++ src/udev/mod.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index cce20d88..9dd8da95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,12 @@ async fn main() -> Result<(), Box> { log::info!("Starting InputPlumber v{}", VERSION); + // Unhide any devices previously hidden by InputPlumber. This can happen + // if InputPlumber is killed before it can restore the devices. + if let Err(e) = unhide_all().await { + log::debug!("Failed to unhide devices at startup: {e}"); + } + // Configure the DBus connection let connection = Connection::system().await?; diff --git a/src/udev/mod.rs b/src/udev/mod.rs index 3ee56c80..4550596b 100644 --- a/src/udev/mod.rs +++ b/src/udev/mod.rs @@ -30,11 +30,33 @@ pub async fn hide_device(path: &str) -> Result<(), Box> { return Err("Unable to create match rule for device".into()); }; + // Create the directory to move devnodes to + tokio::fs::create_dir_all("/dev/inputplumber/sources").await?; + // Find the chmod command to use for hiding let chmod_cmd = if Path::new("/bin/chmod").exists() { - "/bin/chmod" + "/bin/chmod".to_string() + } else if Path::new("/usr/bin/chmod").exists() { + "/usr/bin/chmod".to_string() + } else { + let output = Command::new("which").arg("chmod").output().await?; + if !output.status.success() { + return Err("Unable to determine chmod command location".into()); + } + str::from_utf8(output.stdout.as_slice())?.trim().to_string() + }; + + // Find the mv command to use for hiding + let mv_cmd = if Path::new("/bin/mv").exists() { + "/bin/mv".to_string() + } else if Path::new("/usr/bin/mv").exists() { + "/usr/bin/mv".to_string() } else { - "/usr/bin/chmod" + let output = Command::new("which").arg("mv").output().await?; + if !output.status.success() { + return Err("Unable to determine mv command location".into()); + } + str::from_utf8(output.stdout.as_slice())?.trim().to_string() }; // Create an early udev rule to hide the device @@ -44,8 +66,10 @@ pub async fn hide_device(path: &str) -> Result<(), Box> { {match_rule}, GOTO="inputplumber_valid" GOTO="inputplumber_end" LABEL="inputplumber_valid" -KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="{subsystem}", MODE:="0000", GROUP:="root", RUN:="{chmod_cmd} 000 /dev/input/%k", SYMLINK+="inputplumber/by-hidden/%k" -KERNEL=="hidraw[0-9]*", SUBSYSTEM=="{subsystem}", MODE:="0000", GROUP:="root", RUN:="{chmod_cmd} 000 /dev/%k", SYMLINK+="inputplumber/by-hidden/%k" +KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="{subsystem}", MODE:="0000", GROUP:="root", RUN+="{chmod_cmd} 000 /dev/input/%k", SYMLINK+="inputplumber/by-hidden/%k" +KERNEL=="hidraw[0-9]*", SUBSYSTEM=="{subsystem}", MODE:="0000", GROUP:="root", RUN+="{chmod_cmd} 000 /dev/%k", SYMLINK+="inputplumber/by-hidden/%k" +KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="{subsystem}", RUN+="{mv_cmd} /dev/input/%k /dev/inputplumber/sources/%k" +KERNEL=="hidraw[0-9]*", SUBSYSTEM=="{subsystem}", RUN+="{mv_cmd} /dev/%k /dev/inputplumber/sources/%k" LABEL="inputplumber_end" "# ); @@ -65,6 +89,8 @@ GOTO="inputplumber_end" LABEL="inputplumber_valid" KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="{subsystem}", MODE="000", GROUP="root", TAG-="uaccess", RUN+="{chmod_cmd} 000 /dev/input/%k" KERNEL=="hidraw[0-9]*", SUBSYSTEM=="{subsystem}", MODE="000", GROUP="root", TAG-="uaccess", RUN+="{chmod_cmd} 000 /dev/%k" +KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="{subsystem}", RUN+="{mv_cmd} /dev/input/%k /dev/inputplumber/sources/%k" +KERNEL=="hidraw[0-9]*", SUBSYSTEM=="{subsystem}", RUN+="{mv_cmd} /dev/%k /dev/inputplumber/sources/%k" LABEL="inputplumber_end" "# ); @@ -83,19 +109,33 @@ LABEL="inputplumber_end" pub async fn unhide_device(path: String) -> Result<(), Box> { // Get the device to unhide let device = get_device(path.clone()).await?; - let name = device.name.clone(); + let name = device.name.as_str(); let Some(parent) = device.get_parent() else { return Err("Unable to determine parent for device".into()); }; let rule_path = format!( "{RULES_PREFIX}/{RULE_HIDE_DEVICE_EARLY_PRIORITY}-inputplumber-hide-{name}-early.rules" ); + log::debug!("Removing hide rule: {rule_path}"); fs::remove_file(rule_path)?; let rule_path = format!( "{RULES_PREFIX}/{RULE_HIDE_DEVICE_LATE_PRIORITY}-inputplumber-hide-{name}-late.rules" ); + log::debug!("Removing hide rule: {rule_path}"); fs::remove_file(rule_path)?; + // Move the device back + let src_path = format!("/dev/inputplumber/sources/{name}"); + let dst_path = if name.starts_with("event") || name.starts_with("js") { + format!("/dev/input/{name}") + } else { + format!("/dev/{name}") + }; + log::debug!("Restoring device node path '{src_path}' to '{dst_path}'"); + if let Err(e) = fs::rename(&src_path, &dst_path) { + log::warn!("Failed to move device node from {src_path} to {dst_path}: {e}"); + } + // Reload udev reload_children(parent).await?; @@ -104,6 +144,7 @@ pub async fn unhide_device(path: String) -> Result<(), Box> { /// Unhide all devices hidden by InputPlumber pub async fn unhide_all() -> Result<(), Box> { + // Remove all created udev rules let entries = fs::read_dir(RULES_PREFIX)?; for entry in entries { let Ok(entry) = entry else { @@ -114,9 +155,30 @@ pub async fn unhide_all() -> Result<(), Box> { continue; } let path = entry.path().to_string_lossy().to_string(); + log::debug!("Removing hide rule: {path}"); fs::remove_file(path)?; } + // Move all devices back + let entries = fs::read_dir("/dev/inputplumber/sources")?; + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + let path = entry.path(); + let name = entry.file_name().to_string_lossy().to_string(); + let name = name.as_str(); + let dst_path = if name.starts_with("event") || name.starts_with("js") { + format!("/dev/input/{name}") + } else { + format!("/dev/{name}") + }; + log::debug!("Restoring device node path {path:?} to '{dst_path}'"); + if let Err(e) = fs::rename(&path, &dst_path) { + log::warn!("Failed to move device node from {path:?} to {dst_path}: {e}"); + } + } + // Reload udev rules reload_all().await?; From c8ff60aa771867050d0f55bdee490dcf693176ac Mon Sep 17 00:00:00 2001 From: William Edwards Date: Mon, 19 Jan 2026 17:25:23 -0800 Subject: [PATCH 2/2] fix: add device unhiding on SIGINT and SIGTERM --- src/main.rs | 80 ++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9dd8da95..f203a8c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use std::env; use std::error::Error; use std::process; +use tokio::signal::unix::SignalKind; use zbus::fdo::ObjectManager; use zbus::Connection; @@ -60,57 +61,48 @@ async fn main() -> Result<(), Box> { .at(object_manager_path, object_manager) .await?; + // Request the named bus + if let Err(err) = connection.request_name(BUS_NAME).await { + log::error!("Error requesting dbus name: {err}"); + process::exit(-1); + } + // Create an InputManager instance let mut input_manager = Manager::new(connection.clone()); - let (ctrl_c_result, input_man_result, request_name_result) = tokio::join!( - // Setup CTRL+C handler - tokio::spawn(async move { - tokio::signal::ctrl_c().await.unwrap(); - log::info!("Un-hiding all devices"); - if let Err(e) = unhide_all().await { - log::error!("Unable to un-hide devices: {:?}", e); - } - log::info!("Shutting down"); - process::exit(0); - }), - // Start the input manager and listen on DBus - input_manager.run(), - // Request the named bus - connection.request_name(BUS_NAME) - ); + // Setup signal handlers + let mut sig_term = tokio::signal::unix::signal(SignalKind::terminate())?; + let mut sig_int = tokio::signal::unix::signal(SignalKind::interrupt())?; - match ctrl_c_result { - Ok(_) => { - log::info!("The input manager task has exited"); - } - Err(err) => { - log::error!("Error in joining ctrl+C watcher: {err}"); - return Err(Box::new(err) as Box); + // Start the main run loop + let mut exit_code = 0; + tokio::select! { + // Start the input manager and listen on DBus + result = input_manager.run() => { + if let Err(err) = result { + log::error!("Error running input manager: {err}"); + exit_code = -1; + } + }, + // Setup CTRL+C handler + _ = tokio::signal::ctrl_c() => { + log::info!("Received CTRL+C. Shutting down."); + }, + // Setup SIGINT handler + _ = sig_int.recv() => { + log::info!("Received SIGINT. Shutting down."); + }, + // Setup SIGTERM handler + _ = sig_term.recv() => { + log::info!("Received SIGTERM. Shutting down."); } } - match request_name_result { - Ok(_) => { - log::info!("The input manager task has exited"); - } - Err(err) => { - log::error!("Error in joining dbus request name operation: {err}"); - return Err(Box::new(err)); - } - }; - - match input_man_result { - Ok(_) => { - log::info!("The input manager task has exited"); - } - Err(err) => { - log::error!("Error in joining ctrl+C watcher: {err}"); - return Err(err); - } - }; + // Unhide all devices on shutdown + if let Err(e) = unhide_all().await { + log::error!("Unable to un-hide devices: {:?}", e); + } log::info!("InputPlumber stopped"); - - Ok(()) + process::exit(exit_code); }