Stop orphaning go2rtc, serve cameras sooner, keep its config private

Three defects found by running a real WebRTC viewer through the whole
stack for the first time (continuum-backend/scripts/e2e-camera.sh).

1. go2rtc was orphaned on every ordinary stop. `kill_on_drop(true)` was
   set, but main handled no signals, so SIGTERM - what `kill`, `docker
   stop` and a service manager send - ended the process on the spot with
   no destructor run and the child was never dropped. The orphan keeps
   :1984 and :8555 bound, so the restarted proxy cannot start its own,
   and it keeps a printer's access code in memory. One had been running
   for 4.5 days. main now selects on SIGTERM/SIGINT so it returns
   normally and the supervisor is dropped. A SIGKILL or OOM kill still
   orphans it; PR_SET_PDEATHSIG would cover that but needs unsafe and a
   libc dependency, so it is left for a decision.

2. A camera was advertised before go2rtc could serve it. The comment on
   REBUILD_INTERVAL said the first useful rebuild is "a few seconds
   after startup", but a tokio interval ticks immediately - with the
   fleet still empty - and the next tick was 30s away. For that window
   the cloud offered a camera and go2rtc answered "stream not found".
   Polling every 5s costs nothing: it reads in-memory reports and only
   restarts go2rtc when the rendered config changes.

3. The generated go2rtc.yaml was created 0644 by tokio::fs::write. It
   holds the printer's access code and go2rtc's API password, so any
   local user could read both. It is now created owner-only and fresh,
   because a temp file left by a crash would otherwise keep its old
   mode. The new test pre-creates a stale 0644 temp file; against the
   old implementation it fails with mode 420 (0o644) where 384 (0o600)
   is expected.

cargo test: 193 lib tests and all integration tests pass.
This commit is contained in:
2026-10-23 16:01:34 +00:00
parent c6cf9bcb5e
commit af8bac4bf3
2 changed files with 81 additions and 9 deletions
+28 -1
View File
@@ -107,7 +107,7 @@ async fn main() -> anyhow::Result<()> {
let restart_go2rtc = std::sync::Arc::new(tokio::sync::Notify::new());
// Built once, here, rather than inside the rebuild loop: the config is
// re-rendered every 30s and compared byte-for-byte to decide whether to
// re-rendered every few seconds and compared byte-for-byte to decide whether to
// restart go2rtc, so a credential that changed per render would restart
// it - and drop every viewer - on every tick.
let camera_auth = camera::ApiAuth::new()?;
@@ -124,6 +124,16 @@ async fn main() -> anyhow::Result<()> {
let (outbound, inbox) = uplink::channel();
tokio::select! {
// Ends the process through a normal return, so the supervisors below
// are dropped and go2rtc's `kill_on_drop` actually fires. Without this
// arm SIGTERM - what `kill`, `docker stop` and a service manager all
// send - ends the process on the spot with no destructor run, and
// go2rtc is orphaned: it keeps :1984 and :8555 bound, so the restarted
// proxy cannot start its own, and it keeps a printer's access code in
// memory. Found when a test run left one behind.
_ = shutdown_requested() => {
info!("stop requested; exiting so go2rtc is stopped with us");
}
_ = uplink::run(config.clone(), fleet.clone(), outbound.clone(), inbox) => {}
_ = continuum_proxy::discovery::report::run(
config.discovery.clone(),
@@ -157,6 +167,23 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
/// Resolves when the process is asked to stop: SIGTERM or SIGINT on Unix,
/// Ctrl-C elsewhere.
async fn shutdown_requested() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut terminate) = signal(SignalKind::terminate()) {
tokio::select! {
_ = terminate.recv() => {}
_ = tokio::signal::ctrl_c() => {}
}
return;
}
}
let _ = tokio::signal::ctrl_c().await;
}
/// Connects every configured printer concurrently (not one at a time -
/// Bambu's auto-detect fallback alone can take several seconds per
/// printer if it has to time out on MQTT v5 before trying v3.1.1;