From b65bdfb259aefc19e02401d81fc5a0e279151fbd Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 28 Apr 2026 04:30:48 -0300 Subject: [PATCH] fix(sync): skip soft connect on dwc2 --- Cargo.lock | 6 ++-- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- scripts/daemon/lesavka-core.sh | 23 ++++++++++-- server/Cargo.toml | 2 +- server/src/gadget/cycle_control.rs | 10 +++--- server/src/gadget/sysfs_state.rs | 23 ++++++++++++ testing/tests/server_core_script_contract.rs | 15 ++++++++ .../tests/server_gadget_include_contract.rs | 35 ++++++++++++++++++- 9 files changed, 105 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3fe9631..15a4fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.38" +version = "0.14.39" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.38" +version = "0.14.39" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.38" +version = "0.14.39" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 1452c38..5522970 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.38" +version = "0.14.39" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 4f12dc3..3d1dce1 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.38" +version = "0.14.39" edition = "2024" build = "build.rs" diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index 6377eec..8647278 100755 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -27,6 +27,25 @@ udc_state() { cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" } +udc_driver() { + local udc="$1" + local driver="" + driver="$(readlink -f "/sys/bus/platform/devices/$udc/driver" 2>/dev/null || true)" + [[ -n $driver ]] || return 0 + basename "$driver" +} + +udc_supports_soft_connect() { + local udc="$1" + [[ -n $udc ]] || return 1 + [[ -w /sys/class/udc/$udc/soft_connect ]] || return 1 + [[ ${LESAVKA_FORCE_SOFT_CONNECT:-0} == 1 ]] && return 0 + local driver="" + driver="$(udc_driver "$udc")" + [[ $driver == "dwc2" ]] && return 1 + return 0 +} + is_attached_state() { case "$1" in configured|addressed|default|suspended) @@ -47,7 +66,7 @@ detach_gadget() { return 0 ;; esac - if [[ -n $udc && -w /sys/class/udc/$udc/soft_connect ]]; then + if udc_supports_soft_connect "$udc"; then echo 0 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true fi if [[ -n ${LESAVKA_DETACH_CLEAR_UDC:-} && -e $G/UDC ]]; then @@ -71,7 +90,7 @@ attach_gadget() { log "UDC not found; need full setup" return 1 fi - if [[ -n $udc && -w /sys/class/udc/$udc/soft_connect ]]; then + if udc_supports_soft_connect "$udc"; then echo 1 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true fi if [[ -n ${LESAVKA_ATTACH_WRITE_UDC:-} && -e $G/UDC ]]; then diff --git a/server/Cargo.toml b/server/Cargo.toml index f88250f..30dccc8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.38" +version = "0.14.39" edition = "2024" autobins = false diff --git a/server/src/gadget/cycle_control.rs b/server/src/gadget/cycle_control.rs index 04ded6b..ec66fe6 100644 --- a/server/src/gadget/cycle_control.rs +++ b/server/src/gadget/cycle_control.rs @@ -91,8 +91,10 @@ impl UsbGadget { /* 1 - detach gadget */ info!("🔌 detaching gadget from {ctrl}"); // a) drop pull-ups (if the controller offers the switch) - let sc = format!("{}/class/udc/{ctrl}/soft_connect", Self::sysfs_root()); - let _ = Self::write_attr(&sc, "0"); // ignore errors - not all HW has it + let sc = Self::soft_connect_path(&ctrl); + if let Some(sc) = sc.as_deref() { + let _ = Self::write_attr(sc, "0"); // ignore errors - not all HW has it + } // b) clear the UDC attribute; the kernel may transiently answer EBUSY for attempt in 1..=10 { @@ -124,9 +126,9 @@ impl UsbGadget { /* 4 - re-attach + pull-up */ info!("🔌 re-attaching gadget to {ctrl}"); Self::write_attr(self.udc_file, &ctrl)?; - if Path::new(&sc).exists() { + if let Some(sc) = sc.as_deref() { // try to set the pull-up; ignore if the kernel rejects it - match Self::write_attr(&sc, "1") { + match Self::write_attr(sc, "1") { Err(err) => { // only swallow specific errno values if let Some(io) = err.downcast_ref::() { diff --git a/server/src/gadget/sysfs_state.rs b/server/src/gadget/sysfs_state.rs index 7c0d99c..7f2ee8d 100644 --- a/server/src/gadget/sysfs_state.rs +++ b/server/src/gadget/sysfs_state.rs @@ -124,4 +124,27 @@ impl UsbGadget { Ok(None) } + fn platform_driver(ctrl: &str) -> Option { + fs::read_link(format!( + "{}/bus/platform/devices/{ctrl}/driver", + Self::sysfs_root() + )) + .ok() + .and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned())) + } + + fn soft_connect_path(ctrl: &str) -> Option { + let path = format!("{}/class/udc/{ctrl}/soft_connect", Self::sysfs_root()); + if !Path::new(&path).exists() { + return None; + } + if env::var("LESAVKA_FORCE_SOFT_CONNECT").ok().as_deref() == Some("1") { + return Some(path); + } + if Self::platform_driver(ctrl).as_deref() == Some("dwc2") { + return None; + } + Some(path) + } + } diff --git a/testing/tests/server_core_script_contract.rs b/testing/tests/server_core_script_contract.rs index f49d6b4..2ba10c2 100644 --- a/testing/tests/server_core_script_contract.rs +++ b/testing/tests/server_core_script_contract.rs @@ -23,3 +23,18 @@ fn core_script_rebuilds_incomplete_bound_gadgets() { ); } } + +#[test] +fn core_script_skips_soft_connect_for_dwc2() { + for expected in [ + "udc_driver()", + "udc_supports_soft_connect()", + "[[ $driver == \"dwc2\" ]] && return 1", + "if udc_supports_soft_connect \"$udc\"; then", + ] { + assert!( + CORE_SCRIPT.contains(expected), + "lesavka-core soft_connect guard missing: {expected}" + ); + } +} diff --git a/testing/tests/server_gadget_include_contract.rs b/testing/tests/server_gadget_include_contract.rs index c4d9e01..a5b80e2 100644 --- a/testing/tests/server_gadget_include_contract.rs +++ b/testing/tests/server_gadget_include_contract.rs @@ -11,7 +11,7 @@ mod gadget_include_contract { include!(env!("LESAVKA_SERVER_GADGET_SRC")); use serial_test::serial; - use std::os::unix::fs::PermissionsExt; + use std::os::unix::fs::{PermissionsExt, symlink}; use temp_env::with_var; use tempfile::{NamedTempFile, tempdir}; @@ -47,6 +47,12 @@ mod gadget_include_contract { &base.join("sys/bus/platform/drivers/dwc2/bind"), "placeholder\n", ); + let driver_target = base.join("sys/bus/platform/drivers/dwc2"); + let driver_link = base.join(format!("sys/bus/platform/devices/{ctrl}/driver")); + if let Some(parent) = driver_link.parent() { + std::fs::create_dir_all(parent).expect("create driver link parent"); + } + symlink(&driver_target, &driver_link).expect("link controller driver"); write_file( &base.join(format!("cfg/{gadget_name}/UDC")), &format!("{ctrl}\n"), @@ -181,6 +187,33 @@ mod gadget_include_contract { let _ = UsbGadget::probe_platform_udc(); } + #[test] + #[serial] + fn soft_connect_path_skips_dwc2_platform_driver() { + let dir = tempdir().expect("tempdir"); + let ctrl = "fake-ctrl.usb"; + build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached"); + + with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { + assert!(UsbGadget::soft_connect_path(ctrl).is_none()); + }); + } + + #[test] + #[serial] + fn soft_connect_path_can_be_forced_for_dwc2() { + let dir = tempdir().expect("tempdir"); + let ctrl = "fake-ctrl.usb"; + build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached"); + + with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || { + with_var("LESAVKA_FORCE_SOFT_CONNECT", Some("1"), || { + let path = UsbGadget::soft_connect_path(ctrl).expect("forced soft_connect path"); + assert!(path.ends_with("/soft_connect")); + }); + }); + } + #[test] fn find_controller_returns_name_or_error_without_panicking() { let result = UsbGadget::find_controller();