Survive a missing touch panel at boot; retry the open briefly

Two changes to the unattended path, both prompted by deploying to the rack.

The autostart can win the race against USB enumeration at boot, so the panel
may simply not exist yet when socktop-swipe starts. Opening it now retries for
ten seconds -- long enough for enumeration, short enough that a genuinely wrong
device path still reports promptly.

If the panel cannot be opened at all, `run` no longer treats that as fatal. By
that point the dashboard is already on the wall, and exiting would replace a
display you cannot swipe with no display at all. It now says loudly that
gestures are inactive, points at the movement subcommands, and carries on
serving the control socket. `daemon` and `doctor` still fail fast, since there
is no display at stake in either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jasonwitty
2026-09-09 13:30:11 -07:00
parent 9226224507
commit 7d2491ea10
+30 -2
View File
@@ -253,7 +253,16 @@ fn run(cfg: &Config, no_touch: bool) -> Result<()> {
});
if !no_touch {
spawn_panel(cfg, tx)?;
// Deliberately not fatal. The dashboard is already on the wall by this
// point; exiting because the panel is missing would replace a display
// you cannot swipe with no display at all. Say so loudly and carry on
// serving the control socket, which is still a way to drive it.
if let Err(e) = spawn_panel(cfg, tx) {
eprintln!("socktop-swipe: touch gestures are NOT active: {e:#}");
eprintln!(
"socktop-swipe: the dashboard is up; drive it with `socktop-swipe forward` etc."
);
}
}
drive(&tmux, &mut grid, rx)
}
@@ -281,11 +290,30 @@ fn daemon(cfg: &Config, no_touch: bool) -> Result<()> {
drive(&tmux, &mut grid, rx)
}
/// Open the touch panel, retrying briefly.
///
/// At boot the autostart can win the race against USB enumeration, so the
/// device is simply not there yet. Ten seconds covers that without making a
/// genuinely wrong device path take ten seconds to report.
fn open_panel(cfg: &Config) -> Result<Touchpanel> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
match Touchpanel::open(&cfg.touch) {
Ok(p) => return Ok(p),
Err(e) if std::time::Instant::now() < deadline => {
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = e;
}
Err(e) => return Err(e),
}
}
}
/// Read the panel on its own thread. Opening it here rather than in the thread
/// keeps a permission or grab failure on the main path, where it can be
/// reported properly instead of vanishing into a detached thread.
fn spawn_panel(cfg: &Config, tx: mpsc::Sender<Ctl>) -> Result<()> {
let mut panel = Touchpanel::open(&cfg.touch)?;
let mut panel = open_panel(cfg)?;
let gestures = cfg.gestures.clone();
std::thread::spawn(move || {
let result = panel.run(|ev| {