mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
Three additions, all of them plumbing for a bitrate that something actually decides. `BootDescriptor` gains `VideoLimits`. It rides the boot document rather than a kernel command line because `nesinit` handles `Boot` by mounting and *then* bringing the stack up, so the value is in hand before `neshub` is spawned -- no parsing, no window where the service is running without its configuration. It is on the descriptor rather than a launch because its consumer is a service that comes up with the box; geometry went the other way for the same reason, its consumer being started per launch. `bitrate_kbps` is an `Option` and the distinction is load-bearing. "Nobody said" is not zero and is not unlimited, and a reader that conflates the first with the last reproduces the bug exactly: every session offered 10 Mbps because no number had ever been chosen and the encoder's own default stood in for one. `neshub` now says which it got, and falls back to something modest rather than to whatever it finds. Note what `deny_unknown_fields` means here, since it is deliberate: a host that sends `video` to a guest too old to know the field is refused rather than quietly served. That is the right direction to fail -- the alternative is a box that boots, streams, and ignores its ceiling -- and it means the guest image is rebuilt before a host starts sending one. `MSG_RECEIVER_REPORT` carries what a second looked like from the far end: goodput actually released to the decoder, frames released, incomplete and never-arrived, and the receiver's own RTT. The hub cannot work any of this out for itself. Its own view was measured saying the path was healthy while almost nothing was arriving, and one reason is structural -- `send_datagram` evicts the oldest queued datagrams and returns `Ok`, so the send side has no backpressure signal at all. `MSG_CONTROL_MODE` says who is choosing the bitrate. Manual exists because it is how this class of bug gets found: the original report said the bitrate had already been lowered, and the only way anyone established otherwise was by setting one by hand and watching the picture come back. Both decoders refuse what they cannot read rather than guessing. `loss()` returns `None` for a second that accounted for no frames at all, because a second with nothing sent and a second with nothing arriving are indistinguishable from there, and answering either 0% or 100% would tell a controller something nobody knows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
2.8 KiB
Rust
83 lines
2.8 KiB
Rust
// What the service stack does with its children when it goes away, against
|
|
// real processes.
|
|
//
|
|
// Its own test binary for the same reason as `reaping`: these wait on children,
|
|
// and a reaper in another test in the same binary would collect them.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use nesinit::reap::Waiters;
|
|
use nesinit::services::{Service, Services, Stack};
|
|
|
|
/// A service that stays up until something stops it, and one that binds a
|
|
/// socket -- which is all the table needs to be for either question here.
|
|
static SLEEPERS: &[Service] = &[
|
|
Service {
|
|
name: "sleeper",
|
|
argv: &["/bin/sleep", "60"],
|
|
env: &[],
|
|
user: None,
|
|
cost: "nothing: this is a test",
|
|
required: true,
|
|
umask: None,
|
|
ready: None,
|
|
},
|
|
Service {
|
|
name: "second-sleeper",
|
|
argv: &["/bin/sleep", "60"],
|
|
env: &[],
|
|
user: None,
|
|
cost: "nothing: this is a test",
|
|
required: true,
|
|
umask: None,
|
|
ready: None,
|
|
},
|
|
];
|
|
|
|
/// Whether a pid is still a live process, asked without reaping it.
|
|
fn alive(pid: i32) -> bool {
|
|
// Signal 0 checks for the process without sending anything.
|
|
unsafe { libc::kill(pid, 0) == 0 }
|
|
}
|
|
|
|
/// Dropping the stack stops what it started.
|
|
///
|
|
/// As PID 1 the ordered shutdown would reach these anyway. Run by hand -- which
|
|
/// is how a guest that will not boot is debugged -- nothing else does, and the
|
|
/// bus, the audio server and the hub were left running with sockets nobody was
|
|
/// serving.
|
|
#[tokio::test]
|
|
async fn a_stack_that_goes_away_takes_its_services_with_it() {
|
|
let waiters = Waiters::new();
|
|
let mut stack = Stack::from_table(waiters, SLEEPERS);
|
|
let up = stack.bring_up(Default::default()).expect("two sleeps did not start");
|
|
assert_eq!(up.len(), 2);
|
|
|
|
let pids = stack.pids();
|
|
assert_eq!(pids.len(), 2, "the stack did not keep what it started");
|
|
assert!(pids.iter().all(|&pid| alive(pid)));
|
|
|
|
drop(stack);
|
|
|
|
// Signalled, not waited for: the stack cannot reap on its way out, so what
|
|
// is asserted is that each one leaves, not how fast.
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
for pid in pids {
|
|
loop {
|
|
// Nothing here reaps, so a signalled child becomes a zombie rather
|
|
// than disappearing -- and a zombie still answers signal 0. It is
|
|
// waited for explicitly instead.
|
|
let mut status = 0;
|
|
let seen = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
|
|
if seen == pid || seen == -1 {
|
|
break;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"{pid} was still running five seconds after its stack was dropped"
|
|
);
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
}
|
|
}
|