From da242a701be92d5355f6d225392ee742680ff2b5 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:17:57 +0200 Subject: [PATCH] feat(mobile): serve phones whichever daemon is running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A daemon started by `tty7 server start` is the lean tty7-server, which carries no gateway. With phone access on, the Settings status stayed on "Starting…" and no phone could connect. The GUI now checks, 5 s after it starts and whenever phone access is switched on. If nothing is serving, it starts `tty7-app --mobile-gateway`, detached the same way as the daemon (`spawn::detach_helper`). The helper serves until the switch goes off, and exits at once if another process already holds the gateway lock. When the daemon's own gateway is up, no helper is started. The helper logs under its own role, "mobile". --- crates/tty7-core/src/daemon/spawn.rs | 7 +++ mobile/README.md | 4 +- src/core/mobile.rs | 79 ++++++++++++++++++++++++++++ src/main.rs | 15 +++++- src/ui/settings/mobile.rs | 3 ++ 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/crates/tty7-core/src/daemon/spawn.rs b/crates/tty7-core/src/daemon/spawn.rs index 6b0a05e0..0e3f4714 100644 --- a/crates/tty7-core/src/daemon/spawn.rs +++ b/crates/tty7-core/src/daemon/spawn.rs @@ -1333,6 +1333,13 @@ fn strip_deleted_marker(path: String) -> String { } } +/// Detaches a helper the way the daemon itself is detached, so it outlives +/// the process that started it and has no console or window of its own. The +/// GUI starts the mobile gateway helper through this. +pub fn detach_helper(cmd: &mut Command) { + detach(cmd); +} + #[cfg(unix)] fn detach(cmd: &mut Command) { use std::os::unix::process::CommandExt; diff --git a/mobile/README.md b/mobile/README.md index 59b525f8..4af629c0 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -24,7 +24,9 @@ phone (Tauri: WebView + Rust) ──iroh──▶ tty7-gateway ──local so On the desktop: **Settings → Mobile → Allow phone access**. The local daemon runs the gateway from then on, with every window closed too. **Show code** there gives a QR code and a `tty7pair:` code, and the phones it paired are listed -below it, each with an Unpair button. +below it, each with an Unpair button. A daemon that cannot run the gateway, such as a lean +`tty7-server` started by `tty7 server start`, gets a helper instead: the GUI starts +`tty7-app --mobile-gateway`, detached, and it serves until phone access goes off. Without the GUI, the same gateway runs from the command line. It shares the state in `/mobile/`, so the two never run at once: diff --git a/src/core/mobile.rs b/src/core/mobile.rs index ea743a28..a8989609 100644 --- a/src/core/mobile.rs +++ b/src/core/mobile.rs @@ -6,6 +6,13 @@ //! from Settings. It is a client of this same daemon over the local sockets, //! exactly as the standalone `tty7-gateway serve` is, and shares its state //! directory — the two cannot run at once (see `State::lock_serve`). +//! +//! Not every daemon can: one started by `tty7 server start` is the lean +//! `tty7-server`, which carries no gateway. So the GUI checks, and when +//! phone access is on and nothing is serving it starts a helper of its own +//! (`tty7-app --mobile-gateway`), detached like the daemon, which serves until +//! the switch goes off. Whichever of the two takes the lock serves; the other +//! stands by. use std::path::PathBuf; use std::time::{Duration, Instant, SystemTime}; @@ -64,6 +71,78 @@ pub fn supervise() { } } +/// How long the GUI gives the daemon's own gateway to come up before it +/// starts the helper instead. +const DAEMON_GRACE: Duration = Duration::from_secs(5); + +/// The flag that runs this binary as the gateway helper. +pub const HELPER_FLAG: &str = "--mobile-gateway"; + +/// From the GUI: if phone access is on and, after the daemon has had its +/// chance, no gateway is serving, starts the helper. +pub fn ensure_served_soon() { + let _ = std::thread::Builder::new() + .name("mobile-check".into()) + .spawn(|| { + std::thread::sleep(DAEMON_GRACE); + if !Config::load().mobile_access { + return; + } + let Ok(state) = State::open_default() else { + return; + }; + if state.serving() { + return; + } + if let Err(e) = spawn_helper() { + log::warn!("could not start the mobile gateway helper: {e}"); + } + }); +} + +fn spawn_helper() -> std::io::Result<()> { + let mut cmd = std::process::Command::new(std::env::current_exe()?); + cmd.arg(HELPER_FLAG); + if let Some(dir) = tty7_core::core::config::config_dir_path() { + cmd.arg("--config-dir").arg(dir); + } + cmd.stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + tty7_core::daemon::spawn::detach_helper(&mut cmd); + cmd.spawn().map(drop) +} + +/// The helper's whole life: serve while phone access is on, and leave as +/// soon as it is off — or at once, if some other process is serving. +pub fn run_helper() { + let mut switch = Switch::default(); + loop { + if !switch.on() { + return; + } + let Ok(state) = State::open_default() else { + return; + }; + match service::start(state.clone()) { + Ok(gateway) => { + while switch.on() { + std::thread::sleep(POLL); + } + gateway.stop(); + return; + } + // The daemon's own gateway, or a `tty7-gateway serve`: it is + // served, and this helper has nothing to do. + Err(_) if state.serving() => return, + Err(e) => { + log::warn!("the mobile gateway helper could not start the gateway: {e:#}"); + std::thread::sleep(RETRY); + } + } + } +} + /// `mobile_access` as the config file says, re-read only when the file /// changes — the daemon polls it every couple of seconds for as long as it /// lives. diff --git a/src/main.rs b/src/main.rs index c7c7d728..3c545e7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -541,7 +541,14 @@ fn main() { let daemon = args .iter() .any(|arg| arg == std::ffi::OsStr::new("--daemon")); - let role = if daemon { "daemon" } else { "gui" }; + let mobile_helper = args + .iter() + .any(|arg| arg == std::ffi::OsStr::new(crate::core::mobile::HELPER_FLAG)); + let role = match (daemon, mobile_helper) { + (true, _) => "daemon", + (false, true) => "mobile", + (false, false) => "gui", + }; crate::core::crash::install(role); crate::core::logfile::install(role); @@ -570,6 +577,11 @@ fn main() { return; } + if mobile_helper { + crate::core::mobile::run_helper(); + return; + } + if daemon { crate::core::mobile::supervise(); if let Err(e) = crate::daemon::server::run_daemon() { @@ -652,6 +664,7 @@ fn main() { } else { crate::daemon::spawn::restart() }; + crate::core::mobile::ensure_served_soon(); // A pathless launch that found a GUI already registered hands the request // to it — the GUI may be sitting in the tray with no window — and exits. // The forward above cannot do this: it runs before the daemon exists, and diff --git a/src/ui/settings/mobile.rs b/src/ui/settings/mobile.rs index c01db96d..85e770bc 100644 --- a/src/ui/settings/mobile.rs +++ b/src/ui/settings/mobile.rs @@ -95,6 +95,9 @@ fn qr_image(code: &str) -> Option> { impl Tty7App { pub(crate) fn set_mobile_access(&mut self, on: bool, cx: &mut Context) { self.update_config(cx, |cfg| cfg.mobile_access = on); + if on { + crate::core::mobile::ensure_served_soon(); + } if !on && let Some(s) = self.active_settings_mut() { s.mobile_pairing = None; }