feat(mobile): serve phones whichever daemon is running

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".
This commit is contained in:
l0ng-ai
2026-09-27 10:17:57 +02:00
parent 6dd75c3e65
commit da242a701b
5 changed files with 106 additions and 2 deletions
+7
View File
@@ -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;
+3 -1
View File
@@ -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 `<config dir>/mobile/`, so the two never run at once:
+79
View File
@@ -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.
+14 -1
View File
@@ -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
+3
View File
@@ -95,6 +95,9 @@ fn qr_image(code: &str) -> Option<Arc<gpui::Image>> {
impl Tty7App {
pub(crate) fn set_mobile_access(&mut self, on: bool, cx: &mut Context<Self>) {
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;
}