From 9823db5eb70b0d835a0d800515b48892aff10188 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Tue, 19 Nov 2024 20:44:30 -0800 Subject: [PATCH] feat(Manager): add suspend/wake hook methods to handle reconnecting Deck target device --- Cargo.lock | 2 +- Cargo.toml | 3 +- Makefile | 1 + pkg/rpm/inputplumber.spec | 1 + .../system/inputplumber-suspend.service | 14 +++ src/dbus/interface/manager.rs | 38 +++++++ src/input/composite_device/client.rs | 22 +++++ src/input/composite_device/command.rs | 2 + src/input/composite_device/mod.rs | 98 +++++++++++++++++++ src/input/manager.rs | 50 ++++++++++ src/input/target/steam_deck.rs | 16 +++ 11 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 rootfs/usr/lib/systemd/system/inputplumber-suspend.service diff --git a/Cargo.lock b/Cargo.lock index 11be316..d1a5041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,7 +1836,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "virtual-usb" version = "0.1.0" -source = "git+https://github.com/ShadowBlip/virtual-usb-rs.git?rev=5a7a96a6aedc54f339d9ebff78bf484e5b17728d#5a7a96a6aedc54f339d9ebff78bf484e5b17728d" +source = "git+https://github.com/ShadowBlip/virtual-usb-rs.git?rev=4bca5c6fb9f2b63944a286854405e3e7e0b5d259#4bca5c6fb9f2b63944a286854405e3e7e0b5d259" dependencies = [ "libudev", "packed_struct", diff --git a/Cargo.toml b/Cargo.toml index 52d55fd..b51b8e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ assets = [ { source = "target/release/inputplumber", dest = "/usr/bin/inputplumber", mode = "755" }, { source = "rootfs/usr/share/dbus-1/system.d/org.shadowblip.InputPlumber.conf", dest = "/usr/share/dbus-1/system.d/org.shadowblip.InputPlumber.conf", mode = "644" }, { source = "rootfs/usr/lib/systemd/system/inputplumber.service", dest = "/usr/lib/systemd/system/inputplumber.service", mode = "644" }, + { source = "rootfs/usr/lib/systemd/system/inputplumber-suspend.service", dest = "/usr/lib/systemd/system/inputplumber-suspend.service", mode = "644" }, { source = "rootfs/usr/share/inputplumber/devices/*.yaml", dest = "/usr/share/inputplumber/devices/", mode = "644" }, { source = "rootfs/usr/share/inputplumber/schema/*.json", dest = "/usr/share/inputplumber/schema/", mode = "644" }, { source = "rootfs/usr/share/inputplumber/capability_maps/*.yaml", dest = "/usr/share/inputplumber/capability_maps/", mode = "644" }, @@ -48,7 +49,7 @@ thiserror = "1.0.61" tokio = { version = "*", features = ["full"] } udev = { version = "^0.8", features = ["mio"] } uhid-virt = "0.0.7" -virtual-usb = { git = "https://github.com/ShadowBlip/virtual-usb-rs.git", rev = "5a7a96a6aedc54f339d9ebff78bf484e5b17728d" } +virtual-usb = { git = "https://github.com/ShadowBlip/virtual-usb-rs.git", rev = "4bca5c6fb9f2b63944a286854405e3e7e0b5d259" } xdg = "2.5.2" zbus = { version = "4.3.1", default-features = false, features = ["tokio"] } zbus_macros = "4.3.1" diff --git a/Makefile b/Makefile index 4dd201c..5bc0554 100644 --- a/Makefile +++ b/Makefile @@ -67,6 +67,7 @@ uninstall: ## Uninstall inputplumber rm $(PREFIX)/bin/$(NAME) rm $(PREFIX)/share/dbus-1/system.d/$(DBUS_NAME).conf rm $(PREFIX)/lib/systemd/system/$(NAME).service + rm $(PREFIX)/lib/systemd/system/$(NAME)-suspend.service rm $(PREFIX)/lib/udev/hwdb.d/59-inputplumber.hwdb rm -rf $(PREFIX)/share/$(NAME)/devices/ rm -rf $(PREFIX)/share/$(NAME)/schema/ diff --git a/pkg/rpm/inputplumber.spec b/pkg/rpm/inputplumber.spec index 8195a74..a7231aa 100644 --- a/pkg/rpm/inputplumber.spec +++ b/pkg/rpm/inputplumber.spec @@ -61,6 +61,7 @@ systemctl disable inputplumber.servce /usr/bin/inputplumber /usr/share/dbus-1/system.d/org.shadowblip.InputPlumber.conf /usr/lib/systemd/system/inputplumber.service +/usr/lib/systemd/system/inputplumber-suspend.service /usr/lib/udev/hwdb.d/59-inputplumber.hwdb /usr/share/inputplumber/capability_maps/ally_type1.yaml /usr/share/inputplumber/capability_maps/anbernic_type1.yaml diff --git a/rootfs/usr/lib/systemd/system/inputplumber-suspend.service b/rootfs/usr/lib/systemd/system/inputplumber-suspend.service new file mode 100644 index 0000000..2c4f18d --- /dev/null +++ b/rootfs/usr/lib/systemd/system/inputplumber-suspend.service @@ -0,0 +1,14 @@ +[Unit] +Description=InputPlumber Suspend Inhibit Service +Before=sleep.target +StopWhenUnneeded=yes + +[Service] +Type=oneshot +StandardOutput=syslog +RemainAfterExit=yes +ExecStart=busctl call org.shadowblip.InputPlumber /org/shadowblip/InputPlumber/Manager org.shadowblip.InputManager HookSleep +ExecStop=busctl call org.shadowblip.InputPlumber /org/shadowblip/InputPlumber/Manager org.shadowblip.InputManager HookWake + +[Install] +WantedBy=sleep.target diff --git a/src/dbus/interface/manager.rs b/src/dbus/interface/manager.rs index 030c718..6923de5 100644 --- a/src/dbus/interface/manager.rs +++ b/src/dbus/interface/manager.rs @@ -159,4 +159,42 @@ impl ManagerInterface { Ok(()) } + + /// Used to prepare InputPlumber for system suspend + async fn hook_sleep(&self) -> fdo::Result<()> { + let (sender, mut receiver) = mpsc::channel(1); + self.tx + .send_timeout( + ManagerCommand::SystemSleep { sender }, + Duration::from_secs(5), + ) + .await + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + + // Read the response from the manager + if receiver.recv().await.is_none() { + return Err(fdo::Error::Failed("No response from manager".to_string())); + } + + Ok(()) + } + + /// Used to prepare InputPlumber for resume from system suspend + async fn hook_wake(&self) -> fdo::Result<()> { + let (sender, mut receiver) = mpsc::channel(1); + self.tx + .send_timeout( + ManagerCommand::SystemWake { sender }, + Duration::from_secs(5), + ) + .await + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + + // Read the response from the manager + if receiver.recv().await.is_none() { + return Err(fdo::Error::Failed("No response from manager".to_string())); + } + + Ok(()) + } } diff --git a/src/input/composite_device/client.rs b/src/input/composite_device/client.rs index ec25fcc..daca66d 100644 --- a/src/input/composite_device/client.rs +++ b/src/input/composite_device/client.rs @@ -306,4 +306,26 @@ impl CompositeDeviceClient { self.tx.send(CompositeCommand::Stop).await?; Ok(()) } + + /// Calls the suspend handler to perform system suspend-related tasks. + pub async fn suspend(&self) -> Result<(), ClientError> { + let (tx, mut rx) = channel(1); + self.tx.send(CompositeCommand::Suspend(tx)).await?; + + if let Some(result) = rx.recv().await { + return Ok(result); + } + Err(ClientError::ChannelClosed) + } + + /// Calls the resume handler to perform system wake from suspend-related tasks. + pub async fn resume(&self) -> Result<(), ClientError> { + let (tx, mut rx) = channel(1); + self.tx.send(CompositeCommand::Resume(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 751a92d..f073031 100644 --- a/src/input/composite_device/command.rs +++ b/src/input/composite_device/command.rs @@ -44,4 +44,6 @@ pub enum CompositeCommand { WriteEvent(NativeEvent), WriteSendEvent(NativeEvent), Stop, + Suspend(mpsc::Sender<()>), + Resume(mpsc::Sender<()>), } diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 0717c3f..70f5bd4 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -124,6 +124,9 @@ pub struct CompositeDevice { /// This is used to block/requeue multiple calls to set_target_devices(). /// E.g. ["/org/shadowblip/InputPlumber/devices/target/gamepad0"] target_devices_queued: HashSet, + /// List of active target device types (e.g. "deck", "ds5", "xb360") that + /// were active before system suspend. + target_devices_suspended: Vec, /// Map of DBusDevice DBus paths to their respective transmitter channel. /// E.g. {"/org/shadowblip/InputPlumber/devices/target/dbus0": } target_dbus_devices: HashMap, @@ -182,6 +185,7 @@ impl CompositeDevice { target_devices: HashMap::new(), target_devices_by_capability: HashMap::new(), target_devices_queued: HashSet::new(), + target_devices_suspended: Vec::new(), target_dbus_devices: HashMap::new(), ff_effect_ids: (0..64).collect(), ff_effect_id_source_map: HashMap::new(), @@ -470,6 +474,26 @@ impl CompositeDevice { ); break 'main; } + CompositeCommand::Suspend(sender) => { + log::info!( + "Preparing for system suspend for: {}", + self.dbus_path.as_ref().unwrap_or(&"".to_string()) + ); + self.handle_suspend().await; + if let Err(e) = sender.send(()).await { + log::error!("Failed to send suspend response: {e:?}"); + } + } + CompositeCommand::Resume(sender) => { + log::info!( + "Preparing for system resume for: {}", + self.dbus_path.as_ref().unwrap_or(&"".to_string()) + ); + self.handle_resume().await; + if let Err(e) = sender.send(()).await { + log::error!("Failed to send resume response: {e:?}"); + } + } } } @@ -1966,4 +1990,78 @@ impl CompositeDevice { } }); } + + /// Called when notified by the input manager that system suspend is about + /// to happen. + async fn handle_suspend(&mut self) { + // 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() { + let target_type = match target.get_type().await { + Ok(kind) => kind, + Err(err) => { + log::error!("Failed to get target device type: {err:?}"); + continue; + } + }; + + // 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); + } + if let Err(e) = target.stop().await { + log::error!("Failed to stop old target device: {e:?}"); + } + + // Wait a few beats to ensure that the target device is really gone + tokio::time::sleep(Duration::from_millis(200)).await; + } + } + + /// Called when notified by the input manager that system resume is about + /// to happen. + async fn handle_resume(&mut self) { + log::info!( + "Restoring target devices: {:?}", + 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()) + .await + { + log::error!("Failed to set restore target devices: {err:?}"); + } + + // Clear the list of suspended target devices + self.target_devices_suspended.clear(); + } } diff --git a/src/input/manager.rs b/src/input/manager.rs index 6c9b3a6..649558f 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -93,6 +93,12 @@ pub enum ManagerCommand { sender: mpsc::Sender, }, SetManageAllDevices(bool), + SystemSleep { + sender: mpsc::Sender<()>, + }, + SystemWake { + sender: mpsc::Sender<()>, + }, } /// Manages input devices @@ -341,6 +347,50 @@ impl Manager { log::error!("Failed to send response: {e:?}"); } } + ManagerCommand::SystemSleep { sender } => { + log::info!("Preparing for system suspend"); + + // Call the suspend handler on each composite device and wait + // for a response. + let composite_devices = self.composite_devices.clone(); + tokio::task::spawn(async move { + for device in composite_devices.values() { + if let Err(e) = device.suspend().await { + log::error!("Failed to call suspend handler on device: {e:?}"); + } + } + + // Respond to the sender to inform them that suspend tasks + // have completed. + if let Err(e) = sender.send(()).await { + log::error!("Failed to send response: {e:?}"); + } + + log::info!("Finished preparing for system suspend"); + }); + } + ManagerCommand::SystemWake { sender } => { + log::info!("Preparing for system resume"); + + // Call the resume handler on each composite device and wait + // for a response. + let composite_devices = self.composite_devices.clone(); + tokio::task::spawn(async move { + for device in composite_devices.values() { + if let Err(e) = device.resume().await { + log::error!("Failed to call resume handler on device: {e:?}"); + } + } + + // Respond to the sender to inform them that resume tasks + // have completed. + if let Err(e) = sender.send(()).await { + log::error!("Failed to send response: {e:?}"); + } + + log::info!("Finished preparing for system resume"); + }); + } } } diff --git a/src/input/target/steam_deck.rs b/src/input/target/steam_deck.rs index ad228e0..5cabaa5 100644 --- a/src/input/target/steam_deck.rs +++ b/src/input/target/steam_deck.rs @@ -767,7 +767,23 @@ impl TargetInputDevice for SteamDeckDevice { /// Stop the virtual USB read/write threads fn stop(&mut self) -> Result<(), InputError> { + log::debug!("Stopping virtual Deck controller"); self.device.stop(); + + // Read from the device + let xfer = self.device.blocking_read()?; + + // Handle any non-standard transfers + if let Some(xfer) = xfer { + let reply = self.handle_xfer(xfer); + + // Write to the device if a reply is necessary + if let Some(reply) = reply { + self.device.write(reply)?; + } + } + + log::debug!("Finished stopping"); Ok(()) } }