use crate::api::MyceliumClient; use crate::elevation; use crate::error::{AppError, AppResult}; use parking_lot::Mutex; use std::collections::VecDeque; use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; use std::time::{Duration, Instant}; use tauri::{AppHandle, Emitter, Manager}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Child; use tracing::{info, warn}; const HEALTH_CHECK_TIMEOUT_SECS: u64 = 20; const HEALTH_CHECK_INTERVAL_MS: u64 = 400; const LOG_RING_CAPACITY: usize = 500; #[derive(Debug, Clone, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct SidecarConfig { pub peers: Vec, pub tun_name: Option, pub no_tun: bool, } impl Default for SidecarConfig { fn default() -> Self { Self { // A small set of well-known public peers from the mycelium README, // used as bootstrap when the user hasn't configured their own. peers: vec![ "tcp://188.40.132.242:9651".into(), "quic://[2a01:4f8:212:fa6::2]:9651".into(), ], tun_name: None, no_tun: false, } } } /// Holds the running mycelium child process plus a small in-memory log /// buffer so the Settings page can show recent stderr/stdout without /// reading from disk. pub struct SidecarHandle { child: Mutex>, api_url: Mutex>, logs: Mutex>, config_path: Mutex>, key_path: Mutex>, } impl SidecarHandle { pub fn new() -> Arc { Arc::new(Self { child: Mutex::new(None), api_url: Mutex::new(None), logs: Mutex::new(VecDeque::with_capacity(LOG_RING_CAPACITY)), config_path: Mutex::new(None), key_path: Mutex::new(None), }) } pub fn is_running(&self) -> bool { self.api_url.lock().is_some() } pub fn client(&self) -> Option { self.api_url .lock() .as_ref() .map(|u| MyceliumClient::new(u.clone())) } pub fn logs_snapshot(&self) -> Vec { self.logs.lock().iter().cloned().collect() } pub fn key_path(&self) -> Option { self.key_path.lock().clone() } pub fn config_path(&self) -> Option { self.config_path.lock().clone() } fn push_log(&self, line: String) { let mut buf = self.logs.lock(); if buf.len() >= LOG_RING_CAPACITY { buf.pop_front(); } buf.push_back(line); } pub async fn start( self: &Arc, app: &AppHandle, config: &SidecarConfig, ) -> AppResult { if self.is_running() { return Err(AppError::DaemonAlreadyRunning); } let bin = locate_sidecar(app)?; let port = portpicker::pick_unused_port() .ok_or_else(|| AppError::Other("no free port available".into()))?; let data_dir = app .path() .app_data_dir() .map_err(|e| AppError::TauriPath(e.to_string()))?; std::fs::create_dir_all(&data_dir)?; let key_path = data_dir.join("priv_key.bin"); let config_path = data_dir.join("mycelium.toml"); let mut args = vec![ "--api-addr".to_string(), format!("127.0.0.1:{port}"), "--key-file".to_string(), key_path.display().to_string(), ]; if config_path.exists() { args.push("--config-file".to_string()); args.push(config_path.display().to_string()); } if config.no_tun { args.push("--no-tun".to_string()); } if let Some(name) = &config.tun_name { args.push("--tun-name".to_string()); args.push(name.clone()); } if !config.peers.is_empty() { args.push("--peers".to_string()); for p in &config.peers { args.push(p.clone()); } } info!(?bin, port, "spawning mycelium sidecar via pkexec"); let mut cmd = elevation::elevated(&bin, &args); cmd.stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); let mut child = cmd.spawn()?; let stdout = child.stdout.take(); let stderr = child.stderr.take(); // Stash before we await the health check, so a slow daemon // doesn't leave us with a zombie process if anything panics. let api_url = format!("http://127.0.0.1:{port}"); *self.child.lock() = Some(child); *self.api_url.lock() = Some(api_url.clone()); *self.config_path.lock() = Some(config_path); *self.key_path.lock() = Some(key_path); if let Some(out) = stdout { let me = Arc::clone(self); tokio::spawn(async move { let mut lines = BufReader::new(out).lines(); while let Ok(Some(line)) = lines.next_line().await { me.push_log(format!("[stdout] {line}")); } }); } if let Some(err) = stderr { let me = Arc::clone(self); tokio::spawn(async move { let mut lines = BufReader::new(err).lines(); while let Ok(Some(line)) = lines.next_line().await { me.push_log(format!("[stderr] {line}")); } }); } // Background watcher: polls every 2s and emits `sidecar://exited` // when the child dies after the start sequence has succeeded. { let me = Arc::clone(self); let app = app.clone(); tokio::spawn(async move { loop { tokio::time::sleep(Duration::from_secs(2)).await; if !me.is_running() { break; } if let Some(code) = me.child_exit_status() { me.handle_exit(&app, code); break; } } }); } // Health-check loop. The pkexec dialog can take several seconds, so // give the daemon a generous window to come up before failing. let client = MyceliumClient::new(&api_url); let started = Instant::now(); loop { // Bail early if the child died (auth cancel, missing TUN cap, etc.). if let Some(code) = self.child_exit_status() { self.cleanup(); if elevation::is_auth_failure(code) { return Err(AppError::ElevationCancelled); } return Err(AppError::SidecarExited(format!("exit code {code}"))); } if client.is_alive().await { info!(api_url = %api_url, "mycelium sidecar healthy"); let _ = app.emit("sidecar://ready", &api_url); return Ok(api_url); } if started.elapsed() > Duration::from_secs(HEALTH_CHECK_TIMEOUT_SECS) { self.stop().await; return Err(AppError::HealthCheckTimeout(HEALTH_CHECK_TIMEOUT_SECS)); } tokio::time::sleep(Duration::from_millis(HEALTH_CHECK_INTERVAL_MS)).await; } } fn child_exit_status(&self) -> Option { let mut lock = self.child.lock(); let child = lock.as_mut()?; match child.try_wait() { Ok(Some(s)) => Some(s.code().unwrap_or(-1)), _ => None, } } fn handle_exit(&self, app: &AppHandle, code: i32) { warn!(code, "mycelium sidecar exited"); self.cleanup(); let _ = app.emit("sidecar://exited", code); } fn cleanup(&self) { *self.api_url.lock() = None; let _ = self.child.lock().take(); } pub async fn stop(&self) { // Take the child by value so the parking_lot guard isn't held across // the await on `wait()`. `kill_on_drop(true)` is set, but pkexec runs // mycelium as root, so we still ask politely first; the polkit agent // reaps the elevated child. let child_opt = self.child.lock().take(); if let Some(mut child) = child_opt { let _ = child.start_kill(); let _ = child.wait().await; } self.cleanup(); } } /// Resolve the bundled `mycelium-` binary in both `tauri dev` /// (cargo manifest) and bundled (resource_dir) modes. fn locate_sidecar(app: &AppHandle) -> AppResult { let triple = std::env::var("TAURI_ENV_TARGET_TRIPLE") .ok() .or_else(|| option_env!("TARGET").map(|s| s.to_string())) .unwrap_or_else(|| "x86_64-unknown-linux-gnu".to_string()); let name = format!("mycelium-{triple}"); let mut tried: Vec = Vec::new(); if let Ok(resource) = app.path().resource_dir() { let p = resource.join(&name); if p.exists() { return Ok(p); } tried.push(p); } let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let dev_path = manifest_dir.join("binaries").join(&name); if dev_path.exists() { return Ok(dev_path); } tried.push(dev_path); Err(AppError::SidecarNotFound(tried)) }