From d42b6fd8167588f0f88b8e82252663b2a591e90c Mon Sep 17 00:00:00 2001 From: William Edwards Date: Mon, 10 Feb 2025 20:01:29 -0800 Subject: [PATCH 1/2] feat(Manager): add gamepad reordering --- src/cli/device.rs | 41 ++++ src/dbus/interface/manager.rs | 31 ++- src/input/composite_device/client.rs | 11 + src/input/composite_device/command.rs | 1 + src/input/composite_device/mod.rs | 34 +-- src/input/manager.rs | 331 ++++++++++++++++++++++---- src/input/target/mod.rs | 8 + 7 files changed, 387 insertions(+), 70 deletions(-) diff --git a/src/cli/device.rs b/src/cli/device.rs index 000e7bdd..fa79f08e 100644 --- a/src/cli/device.rs +++ b/src/cli/device.rs @@ -76,6 +76,8 @@ impl Display for InterceptMode { pub enum DevicesCommand { /// List all running composite devices List, + /// List or set the player order of composite devices + Order { device_ids: Option> }, /// Enable/disable managing all supported input devices ManageAll { #[arg(long, action)] @@ -223,6 +225,45 @@ pub async fn handle_devices(conn: Connection, cmd: DevicesCommand) -> Result<(), let verb = if enable { "Enabled" } else { "Disabled" }; println!("{verb} management of all supported devices"); } + DevicesCommand::Order { device_ids } => { + let manager = ManagerInterfaceProxy::builder(&conn).build().await?; + if let Some(ids) = device_ids { + let paths: Vec = ids + .into_iter() + .map(|id| format!("/org/shadowblip/InputPlumber/CompositeDevice{id}")) + .collect(); + manager.set_gamepad_order(paths).await?; + } + + // Fetch the current gamepad order + let order = manager.gamepad_order().await?; + + // Query information about each device + let mut devices = Vec::with_capacity(order.len()); + for path in order { + let device = CompositeDeviceInterfaceProxy::builder(&conn) + .path(path.clone()) + .unwrap() + .build() + .await; + let Some(device) = device.ok() else { + continue; + }; + + let number = path.replace("/org/shadowblip/InputPlumber/CompositeDevice", ""); + let name = device.name().await.unwrap_or_default(); + + let row = DeviceRow { id: number, name }; + + devices.push(row); + } + + let mut table = Table::new(devices); + table + .with(Style::modern_rounded()) + .with(Panel::header("Composite Devices")); + println!("{table}"); + } } Ok(()) diff --git a/src/dbus/interface/manager.rs b/src/dbus/interface/manager.rs index d3601d2d..e644fa6a 100644 --- a/src/dbus/interface/manager.rs +++ b/src/dbus/interface/manager.rs @@ -37,8 +37,35 @@ impl ManagerInterface { } #[zbus(property)] - async fn intercept_mode(&self) -> fdo::Result { - Ok("InputPlumber".to_string()) + async fn gamepad_order(&self) -> fdo::Result> { + let (sender, mut receiver) = mpsc::channel(1); + self.tx + .send_timeout( + ManagerCommand::GetGamepadOrder { sender }, + Duration::from_millis(500), + ) + .await + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + + // Read the response from the manager + let Some(response) = receiver.recv().await else { + return Err(fdo::Error::Failed("No response from manager".to_string())); + }; + + Ok(response) + } + + #[zbus(property)] + async fn set_gamepad_order(&self, order: Vec) -> zbus::Result<()> { + self.tx + .send_timeout( + ManagerCommand::SetGamepadOrder { dbus_paths: order }, + Duration::from_millis(500), + ) + .await + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + + Ok(()) } /// If set to 'true', InputPlumber will try to manage all input devices diff --git a/src/input/composite_device/client.rs b/src/input/composite_device/client.rs index a04e0999..69d1ec71 100644 --- a/src/input/composite_device/client.rs +++ b/src/input/composite_device/client.rs @@ -360,4 +360,15 @@ impl CompositeDeviceClient { } Err(ClientError::ChannelClosed) } + + /// Returns true if the target devices are suspended + pub async fn is_suspended(&self) -> Result { + let (tx, mut rx) = channel(1); + self.tx.send(CompositeCommand::IsSuspended(tx)).await?; + + if let Some(result) = rx.recv().await { + return Ok(result); + } + Err(ClientError::ChannelClosed) + } } diff --git a/src/input/composite_device/command.rs b/src/input/composite_device/command.rs index 4b416633..2e49cd4f 100644 --- a/src/input/composite_device/command.rs +++ b/src/input/composite_device/command.rs @@ -48,4 +48,5 @@ pub enum CompositeCommand { Stop, Suspend(mpsc::Sender<()>), Resume(mpsc::Sender<()>), + IsSuspended(mpsc::Sender), } diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index bb77ae71..ae1538ea 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -512,6 +512,13 @@ impl CompositeDevice { log::error!("Failed to send resume response: {e:?}"); } } + CompositeCommand::IsSuspended(sender) => { + let is_suspended = !self.target_devices_suspended.is_empty(); + log::debug!("Checking if device is suspended: {is_suspended}"); + if let Err(e) = sender.send(is_suspended).await { + log::error!("Failed to send suspended response: {e:?}"); + } + } } } @@ -2064,9 +2071,6 @@ impl CompositeDevice { // Clear the list of suspended target devices self.target_devices_suspended.clear(); - // Create a list of target devices that should be stopped on suspend - let mut targets_to_stop = HashMap::new(); - // Record what target devices are currently used so they can be restored // when the system is resumed. for (path, target) in self.target_devices.clone().into_iter() { @@ -2078,21 +2082,7 @@ impl CompositeDevice { } }; - // The "deck" target device does not support suspend - if target_type.as_str() == "deck" { - targets_to_stop.insert(path, target); - } - self.target_devices_suspended.push(target_type); - } - log::info!( - "Target devices before suspend: {:?}", - self.target_devices_suspended - ); - - // Tear down any target devices that do not support suspend - for (path, target) in targets_to_stop.into_iter() { - log::info!("Stopping target device: {path}"); self.target_devices.remove(&path); for (_, target_devices) in self.target_devices_by_capability.iter_mut() { target_devices.remove(&path); @@ -2104,6 +2094,10 @@ impl CompositeDevice { // Wait a few beats to ensure that the target device is really gone tokio::time::sleep(Duration::from_millis(200)).await; } + log::info!( + "Target devices before suspend: {:?}", + self.target_devices_suspended + ); } /// Called when notified by the input manager that system resume is about @@ -2114,12 +2108,6 @@ impl CompositeDevice { self.target_devices_suspended ); - // Only handle resume if a deck controller target device was used - if !self.target_devices_suspended.contains(&"deck".to_string()) { - self.target_devices_suspended.clear(); - return; - } - // Set the target devices back to the ones used before suspend if let Err(err) = self .set_target_devices(self.target_devices_suspended.clone()) diff --git a/src/input/manager.rs b/src/input/manager.rs index b2b1166b..7b842398 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -1,5 +1,6 @@ use core::panic; use std::collections::HashMap; +use std::collections::HashSet; use std::error::Error; use std::fs; use std::time::Duration; @@ -101,6 +102,12 @@ pub enum ManagerCommand { SystemWake { sender: mpsc::Sender<()>, }, + GetGamepadOrder { + sender: mpsc::Sender>, + }, + SetGamepadOrder { + dbus_paths: Vec, + }, } /// Manages input devices @@ -147,13 +154,16 @@ pub struct Manager { composite_device_sources: HashMap>, /// Map of target devices being used by a [CompositeDevice]. /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": Vec<"/org/shadowblip/InputPlumber/devices/target/dbus0">} - composite_device_targets: HashMap>, + composite_device_targets: HashMap>, /// Mapping of DBus path to its corresponding [CompositeDeviceConfig] /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": } used_configs: HashMap, /// Mapping of target devices to their respective handles /// E.g. {"/org/shadowblip/InputPlumber/devices/target/dbus0": } target_devices: HashMap, + /// List of composite device dbus paths with gamepad devices in player order. + /// E.g. ["/org/shadowblip/InputPlumber/CompositeDevice0"] + target_gamepad_order: Vec, /// Defines whether or not InputPlumber should try to automatically manage all /// input devices that have a [CompositeDeviceConfig] definition manage_all_devices: bool, @@ -193,6 +203,7 @@ impl Manager { composite_device_sources: HashMap::new(), composite_device_targets: HashMap::new(), manage_all_devices: false, + target_gamepad_order: vec![], } } @@ -286,34 +297,19 @@ impl Manager { sender, } => { log::debug!("Got request to attach target device {target_path} to device: {composite_path}"); - let Some(target) = self.target_devices.get(&target_path) else { - let err = ManagerError::AttachTargetDeviceFailed( - "Failed to find target device".into(), - ); - log::error!("{err}"); - if let Err(e) = sender.send(Err(err)).await { - log::error!("Failed to send response: {e:?}"); - } - continue; - }; - let Some(device) = self.composite_devices.get(&composite_path) else { - let err = ManagerError::AttachTargetDeviceFailed( - "Failed to find composite device".into(), - ); - log::error!("{err}"); + if let Err(err) = self + .attach_target_device(target_path.as_str(), composite_path.as_str()) + .await + { + log::error!("Failed to attach {target_path} to {composite_path}: {err:?}"); if let Err(e) = sender.send(Err(err)).await { log::error!("Failed to send response: {e:?}"); } continue; - }; - - // Send the attach command to the composite device - let mut targets = HashMap::new(); - targets.insert(target_path.clone(), target.clone()); - if let Err(e) = device.attach_target_devices(targets).await { - log::error!("Failed to send attach command: {e:?}"); } - log::debug!("Finished handling attach request for: {target_path}"); + if let Err(e) = sender.send(Ok(())).await { + log::error!("Failed to send response: {e:?}"); + } } ManagerCommand::StopTargetDevice { path } => { log::debug!("Got request to stop target device: {path}"); @@ -329,6 +325,53 @@ impl Manager { ManagerCommand::TargetDeviceStopped { path } => { log::debug!("Target device stopped: {path}"); self.target_devices.remove(&path); + + // Lookup the compoiste device and see if it is suspended? + // TODO: Use a different hashmap to map target device to composite device + let mut device_path = None; + for (composite_path, target_device_paths) in + self.composite_device_targets.iter() + { + if target_device_paths.contains(&path) { + device_path = Some(composite_path.clone()); + break; + } + } + let Some(device_path) = device_path else { + continue; + }; + + log::debug!("Found composite device for target device: {device_path}"); + let Some(device) = self.composite_devices.get(&device_path) else { + continue; + }; + + let is_suspended = match device.is_suspended().await { + Ok(suspended) => suspended, + Err(e) => { + log::error!("Failed to check if device is suspended: {e:?}"); + continue; + } + }; + + // If the composite device is suspended, do not remove the + // target device from the gamepad ordering. + if is_suspended { + continue; + } + + self.target_gamepad_order = self + .target_gamepad_order + .drain(..) + .filter(|paf| paf.as_str() != device_path.as_str()) + .collect(); + log::info!("Gamepad order: {:?}", self.target_gamepad_order); + + self.composite_device_targets + .entry(device_path) + .and_modify(|paths| { + paths.remove(&path); + }); } ManagerCommand::DeviceAdded { device } => { let dev_name = device.name(); @@ -413,8 +456,30 @@ impl Manager { // Call the resume handler on each composite device and wait // for a response. let composite_devices = self.composite_devices.clone(); + let gamepad_order = self.target_gamepad_order.clone(); tokio::task::spawn(async move { + // Resume any composite devices in gamepad order first + for path in gamepad_order { + let Some(device) = composite_devices.get(&path) else { + continue; + }; + if let Err(e) = device.resume().await { + log::error!("Failed to call resume handler on device: {e:?}"); + } + } + + // Resume any remaining composite devices for device in composite_devices.values() { + let is_suspended = match device.is_suspended().await { + Ok(suspended) => suspended, + Err(e) => { + log::error!("Failed to check if device is suspended: {e:?}"); + continue; + } + }; + if !is_suspended { + continue; + } if let Err(e) = device.resume().await { log::error!("Failed to call resume handler on device: {e:?}"); } @@ -429,6 +494,16 @@ impl Manager { log::info!("Finished preparing for system resume"); }); } + ManagerCommand::GetGamepadOrder { sender } => { + log::debug!("Request for gamepad order"); + let order = self.target_gamepad_order.clone(); + if let Err(e) = sender.send(order).await { + log::error!("Failed to send gamepad order: {e:?}"); + } + } + ManagerCommand::SetGamepadOrder { dbus_paths } => { + self.set_gamepad_order(dbus_paths).await; + } } } @@ -546,6 +621,78 @@ impl Manager { Ok(target_devices) } + /// Attach the given target device to the given composite device + async fn attach_target_device( + &mut self, + target_path: &str, + composite_path: &str, + ) -> Result<(), ManagerError> { + let Some(target) = self.target_devices.get(target_path) else { + let err = ManagerError::AttachTargetDeviceFailed("Failed to find target device".into()); + return Err(err); + }; + let Some(device) = self.composite_devices.get(composite_path) else { + let err = + ManagerError::AttachTargetDeviceFailed("Failed to find composite device".into()); + return Err(err); + }; + + // Check the target device type + let target_type = match target.get_type().await { + Ok(kind) => kind, + Err(e) => { + let err = ManagerError::AttachTargetDeviceFailed(format!( + "Failed to get target device type: {e:?}" + )); + return Err(err); + } + }; + let Some(target_type) = TargetDeviceTypeId::try_from(target_type.as_str()).ok() else { + let err = ManagerError::AttachTargetDeviceFailed( + "Target device returned an invalid device type!".into(), + ); + return Err(err); + }; + + // If the target device is a gamepad, maintain the order in which it + // was connected. + if target_type.is_gamepad() + && !self + .target_gamepad_order + .contains(&composite_path.to_owned()) + { + self.target_gamepad_order.push(composite_path.to_string()); + log::info!("Gamepad order: {:?}", self.target_gamepad_order); + } + + // Send the attach command to the composite device + let mut targets = HashMap::new(); + targets.insert(target_path.to_string(), target.clone()); + if let Err(e) = device.attach_target_devices(targets).await { + let err = ManagerError::AttachTargetDeviceFailed(format!( + "Failed to send attach command: {e:?}" + )); + return Err(err); + } + + // Track the composite device and target device + self.composite_device_targets + .entry(composite_path.to_string()) + .and_modify(|paths| { + paths.insert(target_path.to_string()); + }) + .or_insert({ + let mut paths = HashSet::new(); + paths.insert(target_path.to_string()); + paths + }); + log::trace!("Used target devices: {:?}", self.composite_device_targets); + + log::debug!("Finished handling attach request for: {target_path}"); + + Ok(()) + } + /// Create and start the given type of target device and return a mapping /// of the dbus path to the target device and sender to send messages to the /// device. @@ -625,36 +772,23 @@ impl Manager { } device.set_dbus_devices(dbus_devices); - // Create target devices based on the configuration - let mut target_devices = Vec::new(); - if let Some(target_devices_config) = target_types { - for kind in target_devices_config { - let device = self.create_target_device(kind.as_str()).await?; - target_devices.push(device); - } - } - - // Start the target input devices - let targets = self.start_target_devices(target_devices).await?; - let target_paths = targets.keys(); - for target_path in target_paths { - target_device_paths.push(target_path.clone()); - } - // Add the device to our maps self.composite_devices .insert(composite_path.clone(), client); log::trace!("Managed source devices: {:?}", self.source_devices_used); self.used_configs.insert(composite_path.clone(), config); log::trace!("Used configs: {:?}", self.used_configs); - self.composite_device_targets - .insert(composite_path.clone(), target_device_paths); - log::trace!("Used target devices: {:?}", self.composite_device_targets); + self.composite_device_targets.insert( + composite_path.to_string(), + HashSet::with_capacity(target_device_paths.len()), + ); // Run the device let composite_path = String::from(device.dbus_path()); + let composite_path_clone = composite_path.clone(); let tx = self.tx.clone(); - Ok(tokio::spawn(async move { + let task = tokio::spawn(async move { + let targets = HashMap::new(); if let Err(e) = device.run(targets).await { log::error!("Error running {composite_path}: {}", e.to_string()); } @@ -670,7 +804,49 @@ impl Manager { e.to_string() ); } - })) + }); + + // Create target devices based on the configuration + let mut target_devices = Vec::new(); + if let Some(target_devices_config) = target_types { + for kind in target_devices_config { + let device = self.create_target_device(kind.as_str()).await?; + target_devices.push(device); + } + } + + // Start the target input devices + let targets = self.start_target_devices(target_devices).await?; + let target_paths = targets.keys(); + for target_path in target_paths { + target_device_paths.push(target_path.clone()); + } + + // Attach the target devices + let tx = self.tx.clone(); + tokio::task::spawn(async move { + // Queue the target device attachment to the composite device + for target_path in targets.into_keys() { + let (sender, mut receiver) = mpsc::channel(1); + let result = tx + .send(ManagerCommand::AttachTargetDevice { + target_path, + composite_path: composite_path_clone.clone(), + sender, + }) + .await; + + if let Err(e) = result { + log::error!("Failed to send target device attach command to manager: {e:?}"); + continue; + } + tokio::task::spawn(async move { + receiver.recv().await; + }); + } + }); + + Ok(task) } /// Called when a composite device stops running @@ -712,6 +888,14 @@ impl Manager { } } + // Remove the device from gamepad order + self.target_gamepad_order = self + .target_gamepad_order + .drain(..) + .filter(|paf| paf.as_str() != path.as_str()) + .collect(); + log::info!("Gamepad order: {:?}", self.target_gamepad_order); + // Remove the composite device from our list self.composite_devices.remove::(&path); log::debug!("Composite device removed: {}", path); @@ -1590,4 +1774,61 @@ impl Manager { client.add_source_device(device).await?; Ok(()) } + + /// Set the player order of the given composite device paths. Each device + /// will be suspended and resumed in player order. + async fn set_gamepad_order(&mut self, order: Vec) { + log::info!("Setting player order to: {order:?}"); + + // Ensure the given paths are valid composite device paths + self.target_gamepad_order = order + .into_iter() + .filter(|path| { + let is_valid = self.composite_devices.contains_key(path); + if !is_valid { + log::error!("Invalid composite device path to set gamepad order: {path}"); + } + is_valid + }) + .collect(); + + let manager_tx = self.tx.clone(); + tokio::task::spawn(async move { + // Send suspend command to manager + let (tx, mut rx) = mpsc::channel(1); + if let Err(e) = manager_tx + .send(ManagerCommand::SystemSleep { sender: tx }) + .await + { + log::error!("Failed to send system sleep command to manager: {e:?}"); + return; + } + + // Wait for all devices to suspend + if rx.recv().await.is_none() { + log::error!("Failed to get response from manager for system sleep command"); + return; + } + + // Sleep a little bit before resuming target devices + tokio::time::sleep(Duration::from_millis(100)).await; + + // Send wake command to manager + let (tx, mut rx) = mpsc::channel(1); + if let Err(e) = manager_tx + .send(ManagerCommand::SystemWake { sender: tx }) + .await + { + log::error!("Failed to send system wake command to manager: {e:?}"); + return; + } + + // Wait for all devices to resume + if rx.recv().await.is_none() { + log::error!("Failed to get response from manager for system wake command"); + } + }); + + // + } } diff --git a/src/input/target/mod.rs b/src/input/target/mod.rs index 4f17d4c4..4ca4bfee 100644 --- a/src/input/target/mod.rs +++ b/src/input/target/mod.rs @@ -216,6 +216,14 @@ impl TargetDeviceTypeId { pub fn name(&self) -> &str { self.name } + + /// Returns true if the target type is a gamepad + pub fn is_gamepad(&self) -> bool { + !matches!( + self.id, + "dbus" | "null" | "touchscreen" | "touchpad" | "mouse" | "keyboard" + ) + } } impl Display for TargetDeviceTypeId { From ac777007944fb19573dc2319c2e0ab25505e13e8 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Mon, 10 Feb 2025 21:42:10 -0800 Subject: [PATCH 2/2] fix(Manager): Don't retain references to dead targets. --- src/input/manager.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/input/manager.rs b/src/input/manager.rs index 7b842398..b646751e 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -346,6 +346,13 @@ impl Manager { continue; }; + self.composite_device_targets + .entry(device_path.clone()) + .and_modify(|paths| { + paths.remove(&path); + }); + log::debug!("Used target devices: {:?}", self.composite_device_targets); + let is_suspended = match device.is_suspended().await { Ok(suspended) => suspended, Err(e) => { @@ -366,12 +373,6 @@ impl Manager { .filter(|paf| paf.as_str() != device_path.as_str()) .collect(); log::info!("Gamepad order: {:?}", self.target_gamepad_order); - - self.composite_device_targets - .entry(device_path) - .and_modify(|paths| { - paths.remove(&path); - }); } ManagerCommand::DeviceAdded { device } => { let dev_name = device.name(); @@ -686,7 +687,7 @@ impl Manager { paths.insert(target_path.to_string()); paths }); - log::trace!("Used target devices: {:?}", self.composite_device_targets); + log::debug!("Used target devices: {:?}", self.composite_device_targets); log::debug!("Finished handling attach request for: {target_path}");