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; }