go2rtc has been supervised since before this commit, with nothing to serve: `CONTINUUM_GO2RTC_CONFIG` pointed at `./go2rtc.yaml`, and that file did not exist in the repository or anywhere else. A watchdog was restarting a process that had no cameras configured. `camera.rs` writes it, from the `rtsp_url` a printer publishes in its own `print.push_status` `ipcam` block rather than from `printers.toml`. The URL is already something the machine tells us, so asking an operator to keep a second copy of it - and to update it by hand when a printer's address changes - would be asking for drift. Two tasks, not one: the builder decides what should be served, the watchdog keeps the thing serving it alive. They meet at one `Notify`, because go2rtc reads its configuration only at startup, so a changed config needs a restart. The watchdog now selects the child's exit against that notification and skips its backoff on a deliberate change. What this does *not* cover is most printers, and that is the honest limit. Only the X1/H2 class publishes an RTSP URL - confirmed against two real machines: the H2C reports `rtsps://192.168.0.21:322/streaming/live/1`, the P1P reports an `ipcam` block with `ipcam_dev: "1"` and no URL at all. "A camera exists" and "a camera you can connect to" are different facts. A P1/A1 needs Bambu's proprietary port-6000 JPEG stream, which this crate does not speak, so those printers report no `camera_stream` and a UI should offer a player only when one is present. Details worth keeping: - The config is rendered sorted so an unchanged fleet is byte-identical, and on the first pass it is compared against the file on disk - without that, every daemon restart rewrites an identical file and drops every live viewer. - Written through a `.tmp` and renamed. The watchdog can restart go2rtc at any moment, and a half-written config fails to parse, which takes every working stream down rather than one. - Never logged, and gitignored: the credentials go in the URL because that is the only place go2rtc takes them for an RTSP source, so the file holds printer access codes. - A reported URL with no scheme is refused rather than repaired. One bad entry makes go2rtc fail to load the whole file. - No `rtsp:` or `webrtc:` block. The API listener is what serves WebRTC to a browser, which is the only consumer here; opening go2rtc's own RTSP server would publish every printer's camera on the LAN unauthenticated. - `stream_base_url` is reported in `hello`, not derived from the listen address: `:1984` is a bind address and not something a browser can open. Unset means "advertise nothing", which is the right default, because a wrong URL makes the UI offer a player that cannot connect. It is a LAN address either way - there is no relay, so this works for an operator in the workshop and not for anyone outside it. 63 tests, clippy unchanged at the two deliberate variant-size warnings, `cargo deny check licenses` ok. go2rtc itself stays operator-installed and is not distributed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
2.7 KiB
Rust
64 lines
2.7 KiB
Rust
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<Notify>) {
|
|
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;
|
|
}
|
|
}
|