Some checks are pending
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Waiting to run
joule-code conformance gate / joule-moq-relay, every target (push) Waiting to run
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Waiting to run
joule-code conformance gate / joule-moq-ingest, every target (push) Waiting to run
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Blocked by required conditions
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Blocked by required conditions
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Blocked by required conditions
joule-moq-room room service / joule-moq-room, every target (push) Blocked by required conditions
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Waiting to run
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Blocked by required conditions
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Blocked by required conditions
WAI examples / WAI examples · does this change touch them (push) Waiting to run
WAI examples / WAI examples · portable · linux (push) Blocked by required conditions
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Blocked by required conditions
WAI examples / WAI examples · android-app · linux-arm64 (push) Blocked by required conditions
wai release binaries build on Windows / the seven release binaries · windows (push) Waiting to run
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Has started running
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has started running
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has started running
wai-app and Create / wai-app · does this change touch it (push) Has started running
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 37s
joule-moq-agent AI participants / joule-moq-agent, every target (push) Waiting to run
wai-fleet service / does this change touch it (push) Successful in 40s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 41s
WAI Studio / does this change touch it (push) Successful in 41s
wai-fleet service / service · linux (push) Waiting to run
wai-fleet service / service · macos (push) Waiting to run
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Waiting to run
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Waiting to run
WAI Studio / Studio in Chromium and Firefox · macos (push) Waiting to run
WAI Studio / Studio in WebKit · linux (push) Waiting to run
The injector signs its cues as WEV1 events under a key and room the configuration fixes, and held its chain in memory: a restarted gateway signed seq 0 again in the same room (live-events §6.2: seq_replay) and forgot the break it had open. injector.state_path (default: key_path + .chain.json; required for a key given in code) keeps the chain: head, open break and its out, and the events of the last signing. Each flush signs, writes the chain whole and synced, and only then publishes and answers completions; a failed write holds both. A start reads it, goes on from its head, refuses another room's or principal's chain, and republishes the tail. The sweep stops a first run at each write and after each write before its publication, and with none; a second run reads what was kept and signs on; what the two published passes §6.2 and makes breaks a room accepts. With the start made to begin the chain again, it fails: seq_replay at stop 3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
137 lines
6.5 KiB
Rust
137 lines
6.5 KiB
Rust
//! A start refused part way returns its error with every port it bound free, and no task of
|
|
//! its left running, so a start that follows binds them at once: a gateway restarting on fixed
|
|
//! ports whose RTMP, RTMPS, SRT or SCTE 104 port is not yet to be had (another process's copy
|
|
//! of a socket, a stop elsewhere still under way), or whose RTMPS identity does not read. Each
|
|
//! check holds one of the gateway's ports, or spoils the identity, starts the gateway on all of
|
|
//! them, and binds every other at once when the start returns; then a start with nothing held
|
|
//! comes up on every one. The test runs in a task on a runtime of one worker, as a restart from
|
|
//! a spawned task does: there a task a refused start only told to go (`JoinHandle::abort`)
|
|
//! cannot have let its socket go until the caller yields, and one it did not end at all, an
|
|
//! accept loop that waits for a shutdown that never comes, holds its port for the life of the
|
|
//! process.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use ed25519_dalek::SigningKey;
|
|
use joule_moq_ingest::room::{build, Descriptor};
|
|
use joule_moq_ingest::{GatewayConfig, GatewayServer};
|
|
use std::net::SocketAddr;
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
const ROUNDS: usize = 5;
|
|
const INJECTOR_SEED: [u8; 32] = [0x71; 32];
|
|
|
|
fn at(port: u16) -> SocketAddr {
|
|
SocketAddr::from(([127, 0, 0, 1], port))
|
|
}
|
|
|
|
/// Every listener a gateway can have, each on a fixed port of its own below the range handed
|
|
/// out for port 0 (`low_tcp`, `low_udp`): HTTP, RTMP, RTMPS, SRT, and the SCTE 104 injector (its
|
|
/// key, its grant from the room's authority, seed 0x51 as `World`'s).
|
|
fn every_listener(c: &mut GatewayConfig) {
|
|
c.http.listen = at(low_tcp());
|
|
c.rtmp.listen = Some(at(low_tcp()));
|
|
c.rtmp.tls_listen = Some(at(low_tcp()));
|
|
let id = joule_moq_relay::cert::RelayIdentity::dev_self_signed(&["localhost".to_string()]).unwrap();
|
|
c.rtmp.tls_der = Some((id.chain.iter().map(|c| c.to_vec()).collect(), id.key.secret_der().to_vec()));
|
|
c.contrib.srt.listen = Some(at(low_udp()));
|
|
let authority = SigningKey::from_bytes(&[0x51; 32]);
|
|
let d = Descriptor::read(c.rooms[0].descriptor.as_ref().unwrap()).unwrap();
|
|
let key = SigningKey::from_bytes(&INJECTOR_SEED);
|
|
let s = now_us() / 1_000_000;
|
|
let g = build::grant(&d, &authority, &key.verifying_key().to_bytes(), 0, s - 60, s + 3600);
|
|
c.injector.listen = Some(at(low_tcp()));
|
|
c.injector.room = hex::encode(d.room_id());
|
|
c.injector.key_seed = Some(INJECTOR_SEED);
|
|
// The principal's chain, kept across a restart of this gateway (a key given in code needs a
|
|
// place for it).
|
|
c.injector.state_path = Some(std::env::temp_dir().join(format!("wai-ingest-injector-ports-{}-{}.chain.json", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos())));
|
|
c.injector.grant = Some(g.to_bytes());
|
|
}
|
|
|
|
/// The ports a gateway listens on, TCP (HTTP, RTMP, RTMPS, the injector's) and UDP (SRT).
|
|
struct Ports {
|
|
http: SocketAddr,
|
|
rtmp: SocketAddr,
|
|
rtmps: SocketAddr,
|
|
injector: SocketAddr,
|
|
srt: SocketAddr,
|
|
}
|
|
|
|
/// The port a check holds.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Held {
|
|
Nothing,
|
|
Tcp(SocketAddr),
|
|
Srt,
|
|
}
|
|
|
|
/// Bind each of `p`'s ports but the one `held` at once, as a start following the refused one
|
|
/// does first.
|
|
fn free(p: &Ports, held: Held, why: &str) {
|
|
for a in [p.http, p.rtmp, p.rtmps, p.injector] {
|
|
if held != Held::Tcp(a)
|
|
&& let Err(e) = std::net::TcpListener::bind(a)
|
|
{
|
|
panic!("{why}: TCP {a} is still bound when the refused start returns: {e}");
|
|
}
|
|
}
|
|
if held != Held::Srt
|
|
&& let Err(e) = std::net::UdpSocket::bind(p.srt)
|
|
{
|
|
panic!("{why}: UDP {} is still bound when the refused start returns: {e}", p.srt);
|
|
}
|
|
}
|
|
|
|
async fn refused(cfg: GatewayConfig, why: &str) -> String {
|
|
match GatewayServer::start(cfg).await {
|
|
Ok(_) => panic!("{why}: the gateway starts"),
|
|
Err(e) => e,
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn a_start_refused_part_way_frees_every_port_it_bound() {
|
|
let t = tokio::spawn(async {
|
|
let World { relay, gateway, .. } = World::start(every_listener).await;
|
|
assert!(gateway.injector.is_some(), "the injector runs");
|
|
// The ports every start names.
|
|
let p = Ports { http: gateway.addr, rtmp: gateway.rtmp_addr.unwrap(), rtmps: gateway.rtmps_addr.unwrap(), injector: gateway.injector_addr.unwrap(), srt: gateway.srt_addr.unwrap() };
|
|
let cfg = gateway.gateway.cfg.clone();
|
|
gateway.shutdown().await;
|
|
for round in 0..ROUNDS {
|
|
for (name, at) in [("RTMP", p.rtmp), ("RTMPS", p.rtmps), ("SCTE 104", p.injector)] {
|
|
let why = format!("round {round}, the {name} port held");
|
|
let held = std::net::TcpListener::bind(at).unwrap();
|
|
let e = refused(cfg.clone(), &why).await;
|
|
assert!(e.contains(&at.to_string()), "{why}: the refusal names the port: {e}");
|
|
free(&p, Held::Tcp(at), &why);
|
|
drop(held);
|
|
}
|
|
let why = format!("round {round}, the SRT port held");
|
|
let held = std::net::UdpSocket::bind(p.srt).unwrap();
|
|
let e = refused(cfg.clone(), &why).await;
|
|
assert!(e.contains(&p.srt.to_string()), "{why}: the refusal names the port: {e}");
|
|
free(&p, Held::Srt, &why);
|
|
drop(held);
|
|
// An RTMPS identity whose key does not read.
|
|
let why = format!("round {round}, the RTMPS identity spoiled");
|
|
let mut c = cfg.clone();
|
|
if let Some((_, key)) = c.rtmp.tls_der.as_mut() {
|
|
*key = vec![0x30, 0x03, 0x02, 0x01, 0x00];
|
|
}
|
|
let e = refused(c, &why).await;
|
|
assert!(e.contains("RTMPS identity"), "{why}: the refusal names the identity: {e}");
|
|
free(&p, Held::Nothing, &why);
|
|
// Nothing held: a start on every one of the ports comes up at once.
|
|
let g = GatewayServer::start(cfg.clone()).await.unwrap_or_else(|e| panic!("round {round}: the gateway starts on the ports a refused start bound: {e}"));
|
|
assert_eq!((g.addr, g.rtmp_addr, g.rtmps_addr, g.injector_addr, g.srt_addr), (p.http, Some(p.rtmp), Some(p.rtmps), Some(p.injector), Some(p.srt)));
|
|
g.shutdown().await;
|
|
free(&p, Held::Nothing, &format!("round {round}, the shutdown"));
|
|
}
|
|
relay.shutdown().await;
|
|
});
|
|
timeout(Duration::from_secs(300), t).await.expect("in time").unwrap();
|
|
}
|