use std::process::Stdio; use std::sync::Arc; use std::time::Duration; use tokio::process::Command; use tokio::sync::Notify; use tracing::{error, info, warn}; /// Supervises the go2rtc subprocess used for camera restreaming /// (RTSP -> WebRTC). Restarts it with a fixed backoff whenever it exits, and /// immediately when `restart` fires. /// /// `restart` is what `camera::run` rings after rewriting the config: go2rtc /// reads its configuration once at startup, so a config that changed while it /// was running would otherwise be ignored until the process happened to die. /// Before that existed this watchdog supervised a process whose config file /// nothing wrote - it restarted go2rtc forever over a file that did not exist. pub async fn watchdog(bin: String, config_path: String, restart: Arc) { loop { info!(%bin, %config_path, "starting go2rtc"); let spawned = Command::new(&bin) .arg("-config") .arg(&config_path) .stdout(Stdio::null()) .stderr(Stdio::inherit()) .kill_on_drop(true) .spawn(); match spawned { Ok(mut child) => { tokio::select! { // Whichever happens first. `kill_on_drop` means the // child dies when `child` goes out of scope at the end // of this arm, so the restart path needs no explicit // kill - and cannot leave an orphan serving the old // config. waited = child.wait() => match waited { Ok(status) => warn!(%status, "go2rtc exited, restarting after backoff"), Err(err) => error!(?err, "failed to wait on go2rtc process"), }, _ = restart.notified() => { info!("go2rtc config changed, restarting it"); // Explicitly, rather than relying on the drop: this // awaits the kill so the next iteration cannot race // the old process for the API port. if let Err(err) = child.kill().await { warn!(?err, "could not stop go2rtc for a config change"); } // No backoff on this path - a config change is a // deliberate act, not a crash loop. continue; } } } Err(err) => { error!(?err, "failed to spawn go2rtc, is it installed and on PATH?"); } } tokio::time::sleep(Duration::from_secs(5)).await; } }