diff --git a/CHANGELOG.md b/CHANGELOG.md index 8faea3a..c78c568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ the contract is "it works on RS50 and G Pro as listed here". ## Unreleased +**The DirectInput proxy is aimed at the wheel, says why it failed, and can +open `/dev/uhid` on a fresh package install.** Three faults from one report +([#105](../../issues/105)). The launcher's lookup of the wheel's USB parent, +used to aim `logi-ffb` at the session's wheel, resolved `..` through a +sysfs symlink logically and landed in `/sys/bus/hid`, so the aim never +fired on any machine and the proxy fell back to its own scan. Fixed with a +real path. The proxy's messages, including the reason it refused to start, +went to Steam's console; they now also land in the launcher log next to the +plan that started it. And the Debian and RPM packages never reloaded udev +after installing their rules, so `/dev/uhid` kept its root-only mode until +the next reboot and the proxy could not create its virtual wheel; both now +reload and re-trigger (Arch's own package hook already does this, and the +from-source installer always did). + **Steam no longer thinks a game is still running after it exits.** Steam runs the launch command under a subreaper, so a process the launcher started and left behind was handed to Steam when the game exited, and Steam kept the diff --git a/packaging/akmods/logitech-trueforce-kmod.spec b/packaging/akmods/logitech-trueforce-kmod.spec index e7f86aa..f03a457 100644 --- a/packaging/akmods/logitech-trueforce-kmod.spec +++ b/packaging/akmods/logitech-trueforce-kmod.spec @@ -96,6 +96,13 @@ force-feedback device). %post -n %{kmod_name}-kmod-common %{_bindir}/logi-wheel-initramfs >/dev/null 2>&1 || : +# udev rules only reach devices that appear after they load: reload and +# re-trigger so /dev/uhid and an already-plugged wheel pick them up now +# rather than at the next reboot (#105). Never fatal. +if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload >/dev/null 2>&1 || : + udevadm trigger --subsystem-match=misc --subsystem-match=hidraw --subsystem-match=input >/dev/null 2>&1 || : +fi %files -n %{kmod_name}-kmod-common %{_bindir}/logi-wheel-initramfs diff --git a/packaging/aur/logitech-trueforce-dkms/logitech-trueforce-dkms.install b/packaging/aur/logitech-trueforce-dkms/logitech-trueforce-dkms.install index 68c0eed..62269b4 100644 --- a/packaging/aur/logitech-trueforce-dkms/logitech-trueforce-dkms.install +++ b/packaging/aur/logitech-trueforce-dkms/logitech-trueforce-dkms.install @@ -26,8 +26,11 @@ post_install() { (or point --sdk-dir / $LOGITECH_TRUEFORCE_SDK_DIR elsewhere) 2. Run as your normal user (NOT root): logi-shim --all-steam - 3. Per SDK sim in Steam: set launch option PROTON_ENABLE_HIDRAW=1 - and disable Steam Input for the wheel. + 3. Per game in Steam: launch option `logi-launch %command%` and + Steam Input off. The launcher works out raw HID, the proxies + and the daemon per game and per wheel; never set + PROTON_ENABLE_HIDRAW by hand, on a G923 it costs the force + feedback you have. Full guide: https://github.com/mescon/logitech-trueforce-linux-driver/wiki/Installation MSG diff --git a/packaging/debian/logitech-trueforce-dkms.postinst b/packaging/debian/logitech-trueforce-dkms.postinst index 1501f25..72d2e03 100755 --- a/packaging/debian/logitech-trueforce-dkms.postinst +++ b/packaging/debian/logitech-trueforce-dkms.postinst @@ -10,4 +10,15 @@ if [ "$1" = "configure" ] && [ -x /usr/bin/logi-wheel-initramfs ]; then /usr/bin/logi-wheel-initramfs || echo "warning: could not update the initramfs; run logi-wheel-initramfs by hand" >&2 fi +# The udev rules this package installs only take effect for devices that +# appear after they are loaded. /dev/uhid exists from boot, so without a +# reload and a trigger it keeps its root-only mode until the next reboot +# and the logi-ffb proxy cannot create its virtual wheel (#105). The same +# applies to a wheel that was already plugged in. Never fatal: a system +# without a running udev (a container, a chroot) installs fine. +if [ "$1" = "configure" ] && command -v udevadm >/dev/null 2>&1; then + udevadm control --reload 2>/dev/null || true + udevadm trigger --subsystem-match=misc --subsystem-match=hidraw --subsystem-match=input 2>/dev/null || true +fi + exit 0 diff --git a/packaging/obs/logitech-trueforce-dkms.spec b/packaging/obs/logitech-trueforce-dkms.spec index 362cc38..d07ca3e 100644 --- a/packaging/obs/logitech-trueforce-dkms.spec +++ b/packaging/obs/logitech-trueforce-dkms.spec @@ -333,6 +333,13 @@ fi # Into the initramfs as well, so it registers before the in-tree driver can # claim a G923 at boot (#90); a no-op note when the module is not built yet. %{_bindir}/logi-wheel-initramfs >/dev/null 2>&1 || : +# udev rules only reach devices that appear after they load: reload and +# re-trigger so /dev/uhid and an already-plugged wheel pick them up now +# rather than at the next reboot (#105). Never fatal. +if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload >/dev/null 2>&1 || : + udevadm trigger --subsystem-match=misc --subsystem-match=hidraw --subsystem-match=input >/dev/null 2>&1 || : +fi %preun dkms remove -m %{module} -v %{modver} --all --rpm_safe_upgrade >/dev/null 2>&1 || true diff --git a/tools/logi-launch.sh b/tools/logi-launch.sh index ba634c8..66a5ef8 100755 --- a/tools/logi-launch.sh +++ b/tools/logi-launch.sh @@ -73,6 +73,10 @@ EXTRA_HELPERS="${LOGI_LAUNCH_HELPERS:-}" WAIT_SECONDS="${LOGI_LAUNCH_WAIT:-120}" SETTLE_SECONDS="${LOGI_LAUNCH_SETTLE:-15}" LOG="${LOGI_LAUNCH_LOG:-/tmp/logi-launch.log}" +# Shared with the tools this wrapper starts (logi-ffb reads it), so what +# they have to say lands in the same file as the plan that started them +# rather than on Steam's console, where nobody looks (#105). +export LOGI_LAUNCH_LOG="$LOG" # Where the Windows-side pieces we stage into games live: the dinput8 # escape proxy, the telemetry relay, the recorded init burst. @@ -443,7 +447,11 @@ resolve_wheel() { if [ -n "$wheel_dir" ]; then # ../.. from the HID device directory is the USB device: the wheel's # interfaces (hidraw here, input there) are siblings under it. - wheel_usb=$(cd "$wheel_dir/../.." 2>/dev/null && pwd -P) || wheel_usb="" + # readlink -f, not cd: /sys/bus/hid/devices/ is a symlink, and + # the shell's cd resolves ".." logically, so "cd /../.." landed + # in /sys/bus/hid and nothing below matched. The proxy was never + # aimed on any machine and fell back to its own scan (#105). + wheel_usb=$(readlink -f "$wheel_dir/../.." 2>/dev/null) || wheel_usb="" for e in /sys/class/input/event*; do [ -d "$e/device" ] || continue [ -n "$wheel_usb" ] || break diff --git a/userspace/logi-wheel/crates/logi-ffb/src/cli.rs b/userspace/logi-wheel/crates/logi-ffb/src/cli.rs index 98a0707..5654ce1 100644 --- a/userspace/logi-wheel/crates/logi-ffb/src/cli.rs +++ b/userspace/logi-wheel/crates/logi-ffb/src/cli.rs @@ -140,11 +140,11 @@ fn run_game(cmd: Vec) -> crate::Result { std::env::var("STEAM_COMPAT_DATA_PATH").ok().as_deref(), ); match &prefix { - Some(p) => eprintln!("logi-ffb: hiding {name:?} from DirectInput in {p}"), - None => eprintln!( + Some(p) => crate::note(&format!("logi-ffb: hiding {name:?} from DirectInput in {p}")), + None => crate::note(&format!( "logi-ffb: no Wine prefix known (WINEPREFIX and STEAM_COMPAT_DATA_PATH unset); a DirectInput game may list the real wheel too, bind it to {:?}", crate::descriptor::VIRTUAL_NAME - ), + )), } steering::apply(&plan, prefix.as_deref())?; @@ -181,8 +181,8 @@ fn run_game(cmd: Vec) -> crate::Result { match join_result { Ok(Ok(())) => {} - Ok(Err(e)) => eprintln!("logi-ffb: proxy loop error: {e}"), - Err(_) => eprintln!("logi-ffb: proxy thread panicked"), + Ok(Err(e)) => crate::note(&format!("logi-ffb: proxy loop error: {e}")), + Err(_) => crate::note("logi-ffb: proxy thread panicked"), } Ok(ExitCode::from(status.code().unwrap_or(0) as u8)) diff --git a/userspace/logi-wheel/crates/logi-ffb/src/lib.rs b/userspace/logi-wheel/crates/logi-ffb/src/lib.rs index d745e11..66126b7 100644 --- a/userspace/logi-wheel/crates/logi-ffb/src/lib.rs +++ b/userspace/logi-wheel/crates/logi-ffb/src/lib.rs @@ -12,6 +12,49 @@ pub mod cli; use std::fmt; +/// Say something to the person running this: on stderr, and also appended +/// to the file `LOGI_LAUNCH_LOG` names when that is set. The launcher sets +/// it to its own log so that a proxy that refuses to start explains itself +/// next to the plan that started it. Under Steam, stderr goes to a console +/// nobody reads, and a game that "did not start" came with no reason +/// attached (#105). Never fails: a log that cannot be written is not a +/// reason to lose the message on stderr. +pub fn note(msg: &str) { + eprintln!("{msg}"); + if let Some(path) = std::env::var_os("LOGI_LAUNCH_LOG") { + note_to(std::path::Path::new(&path), msg); + } +} + +/// The file half of [`note`], separated so it can be tested without +/// touching the process environment. +pub fn note_to(path: &std::path::Path, msg: &str) { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().append(true).create(true).open(path) { + let _ = writeln!(f, "{msg}"); + } +} + +#[cfg(test)] +mod note_tests { + #[test] + fn note_to_appends_one_line_per_call() { + let dir = std::env::temp_dir().join(format!("logi-ffb-note-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("launch.log"); + super::note_to(&log, "logi-ffb: first"); + super::note_to(&log, "logi-ffb: second"); + let text = std::fs::read_to_string(&log).unwrap(); + assert_eq!(text, "logi-ffb: first\nlogi-ffb: second\n"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn note_to_an_unwritable_path_is_silent() { + super::note_to(std::path::Path::new("/proc/no-such-dir/launch.log"), "dropped"); + } +} + #[derive(Debug)] pub enum Error { /// The real wheel's evdev FF node could not be found. diff --git a/userspace/logi-wheel/crates/logi-ffb/src/main.rs b/userspace/logi-wheel/crates/logi-ffb/src/main.rs index 065c612..c14f319 100644 --- a/userspace/logi-wheel/crates/logi-ffb/src/main.rs +++ b/userspace/logi-wheel/crates/logi-ffb/src/main.rs @@ -11,7 +11,7 @@ fn main() -> ExitCode { match logi_ffb::cli::dispatch(&args) { Ok(code) => code, Err(e) => { - eprintln!("logi-ffb: {e}"); + logi_ffb::note(&format!("logi-ffb: {e}")); ExitCode::FAILURE } } diff --git a/userspace/logi-wheel/crates/logi-ffb/src/proxy.rs b/userspace/logi-wheel/crates/logi-ffb/src/proxy.rs index eacb969..18a4329 100644 --- a/userspace/logi-wheel/crates/logi-ffb/src/proxy.rs +++ b/userspace/logi-wheel/crates/logi-ffb/src/proxy.rs @@ -202,11 +202,11 @@ fn warn_if_virtual_wheel_hidraw_inaccessible() { if let Some(node) = find_virtual_wheel_hidraw_node() { let dev_path = format!("/dev/{node}"); if access(dev_path.as_str(), AccessFlags::R_OK | AccessFlags::W_OK).is_err() { - eprintln!( + crate::note(&format!( "logi-ffb: warning: the virtual wheel's hidraw node ({dev_path}) is not \ user-accessible; DirectInput force feedback will not work. Install the \ 71-logi-ffb-uhid.rules udev rule (issue #50)." - ); + )); } return; } @@ -267,7 +267,7 @@ pub struct Proxy { fn run_sink_worker(mut sink: sink::Sink, rx: std::sync::mpsc::Receiver) { for op in &rx { if let Err(e) = sink.apply(op) { - eprintln!("logi-ffb: failed to apply FF operation: {e}"); + crate::note(&format!("logi-ffb: failed to apply FF operation: {e}")); } } sink.shutdown();