Round 6, one blocking P2 — in code I wrote in the round-5 fix, not in the token design, which the review passed again. The verdict cache was keyed on pid alone. A pid can legitimately be claimed by a tokened module *and* an untagged one at the same time — a host crashes, an older build restarts and reuses the number — and those are not the same question: one is answerable directly from the token, the other only if the degradation signals allow it. Collapsing them let whichever module sorted last decide both, so under `--repair-legacy-untagged` with a degraded probe an untagged winner made safely attributable debris `Unknown`, and a tokened winner planned the untagged debris as dead (the execution recheck happened to stop the destruction, which is luck, not design). Verdicts are now cached per `(pid, Attribution)` and each fingerprint is filtered by its own, never by looking its pid up in `dead_pids`. Those three pid sets are documented as reporting-only, since a pid can now honestly appear in two of them. Mutation-verified: restoring the `dead_pids.contains(pid)` filter fails the new test, which runs both module-id orders because the bug was order-dependent, and both polarities — the second asserts that a *live* tokened owner does not lose its module because an untagged claim on the same pid looked dead. Also, the degraded-probe warning had become false (P3): it announced "refusing to unload anything" while the tokened path can now legitimately unload, which in the exact container-recovery case the token was added for would print a categorical refusal and then destroy state. It is now scoped to what it actually means — modules *without* a token will be left alone. 256 tests, clippy clean, fmt clean, field gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
554 lines
23 KiB
Rust
554 lines
23 KiB
Rust
//! `--repair`: clean up the Pulse modules left behind by a crashed pixelpass
|
|
//! host.
|
|
//!
|
|
//! All of the judgement lives in [`plan`], which is pure. What remains here is
|
|
//! I/O plus the two rules that cannot be expressed in a plan:
|
|
//!
|
|
//! - **Re-verify immediately before destroying anything.** Pulse module indices
|
|
//! are reused verbatim, and a host can die (or come back) between the snapshot
|
|
//! and the unload, so the plan is treated as evidence that expires — never as a
|
|
//! licence.
|
|
//! - **Gate the sink on what is still attached to it**, not on the plan's ordering
|
|
//! having succeeded. An unload can fail or be skipped, and a loopback can appear
|
|
//! after the plan was made.
|
|
//!
|
|
//! Repair never touches native PipeWire nodes. Since phase 0c the capture sink is
|
|
//! connection-owned and removes itself when its host dies, so there is nothing
|
|
//! there for repair to do and no safe way for it to help.
|
|
//!
|
|
//! # Where the observations come from
|
|
//!
|
|
//! Structured Pulse introspection over one verified-local connection — see
|
|
//! [`introspect`], which also documents the three defects that parsing `pactl`'s
|
|
//! text output turned out to have. Listing *and* unloading both go through that
|
|
//! same connection.
|
|
|
|
pub mod introspect;
|
|
pub mod plan;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use std::path::Path;
|
|
|
|
use introspect::PulseSession;
|
|
use plan::{Fingerprint, Liveness, Shape};
|
|
|
|
pub async fn run(clean_untagged: bool) -> Result<()> {
|
|
let liveness = LivenessProbe::new();
|
|
if let Some(reason) = liveness.degraded_reason() {
|
|
// Scoped deliberately: modules carrying a token that matches this machine,
|
|
// boot and pid namespace are still cleaned, because the token establishes
|
|
// what these signals can only guess at. Saying "refusing to unload
|
|
// anything" here would be false in exactly the container-recovery case the
|
|
// token was added for.
|
|
eprintln!(
|
|
"[pixelpass] --repair: cannot independently determine process liveness \
|
|
({reason}); modules WITHOUT an ownership token will be left alone."
|
|
);
|
|
}
|
|
|
|
let local = local_identity().context("could not establish this process's own identity")?;
|
|
let policy = plan::Policy {
|
|
local,
|
|
untagged: if clean_untagged {
|
|
plan::UntaggedPolicy::CleanByPidAlone
|
|
} else {
|
|
plan::UntaggedPolicy::Refuse
|
|
},
|
|
};
|
|
if clean_untagged {
|
|
eprintln!(
|
|
"[pixelpass] --repair: --repair-legacy-untagged given; untagged modules will be \
|
|
judged by process id ALONE. That is only safe on the machine and in the pid \
|
|
namespace that ran the crashed host."
|
|
);
|
|
}
|
|
|
|
let mut pulse = PulseSession::connect().context("could not observe the Pulse module table")?;
|
|
let modules = pulse
|
|
.list_modules()
|
|
.context("could not list Pulse modules")?;
|
|
|
|
// Say so loudly when something names our sinks but matches no shape we know:
|
|
// that is either a third party using our names, or a newer pixelpass whose
|
|
// modules this build cannot recognise. The second is how repair would go
|
|
// silently blind, so it never gets inferred from a clean exit.
|
|
let unrecognised = plan::unrecognised_pixelpass_modules(&modules);
|
|
if !unrecognised.is_empty() {
|
|
eprintln!(
|
|
"[pixelpass] --repair: {} module(s) name a pixelpass capture sink but do not match \
|
|
any shape this build knows; they are being LEFT ALONE:",
|
|
unrecognised.len()
|
|
);
|
|
for obs in &unrecognised {
|
|
eprintln!(
|
|
"[pixelpass] --repair: #{} {} {}",
|
|
obs.id, obs.name, obs.args
|
|
);
|
|
}
|
|
}
|
|
|
|
let planned = plan::plan(&modules, &policy, |pid, attribution| {
|
|
liveness_for(&liveness, attribution, pid)
|
|
});
|
|
|
|
// Ours by shape, but carrying no proof of whose pid they name. Never unloaded by
|
|
// default — listed, so an explicit legacy run has something to look at first.
|
|
if !planned.untagged.is_empty() {
|
|
eprintln!(
|
|
"[pixelpass] --repair: {} module(s) are pixelpass's but carry no ownership token, so \
|
|
the process id in their name cannot be attributed to this machine or pid namespace. \
|
|
LEFT ALONE. Re-run with --repair-legacy-untagged to clean them by pid alone:",
|
|
planned.untagged.len()
|
|
);
|
|
for fp in &planned.untagged {
|
|
eprintln!(
|
|
"[pixelpass] --repair: #{} {} (claims pid {})",
|
|
fp.id,
|
|
fp.shape.label(),
|
|
fp.pid
|
|
);
|
|
}
|
|
}
|
|
// Tokened, but the token belongs to another machine, boot or namespace.
|
|
if !planned.foreign.is_empty() {
|
|
eprintln!(
|
|
"[pixelpass] --repair: {} module(s) belong to another machine, boot or pid namespace; \
|
|
their process ids mean nothing here. LEFT ALONE:",
|
|
planned.foreign.len()
|
|
);
|
|
for fp in &planned.foreign {
|
|
eprintln!(
|
|
"[pixelpass] --repair: #{} {} (claims pid {})",
|
|
fp.id,
|
|
fp.shape.label(),
|
|
fp.pid
|
|
);
|
|
}
|
|
}
|
|
|
|
if planned.is_empty() {
|
|
let mut held = Vec::new();
|
|
if !planned.live_pids.is_empty() {
|
|
held.push(format!(
|
|
"{} live pixelpass host(s)",
|
|
planned.live_pids.len()
|
|
));
|
|
}
|
|
if !planned.unknown_pids.is_empty() {
|
|
held.push(format!(
|
|
"{} pid(s) of undeterminable liveness",
|
|
planned.unknown_pids.len()
|
|
));
|
|
}
|
|
if held.is_empty() {
|
|
println!("[pixelpass] --repair: nothing to clean up.");
|
|
} else {
|
|
println!(
|
|
"[pixelpass] --repair: nothing to clean up ({} left alone).",
|
|
held.join(", ")
|
|
);
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let mut unloaded = 0u32;
|
|
let mut skipped = 0u32;
|
|
let mut failed = 0u32;
|
|
|
|
for fp in &planned.unload {
|
|
// ORDER MATTERS, and it is the opposite of what reads naturally.
|
|
//
|
|
// Liveness is asked FIRST, and the fresh snapshot is taken AFTER it. The
|
|
// tempting order — verify the module, then check liveness, then unload —
|
|
// leaves the dangerous window wide open: between `kill` returning ESRCH and
|
|
// the unload, this process can be descheduled long enough for the planned
|
|
// module to vanish, a new host to inherit both the pid and the module index,
|
|
// and its differently-nonced arguments to occupy that index. Nothing would
|
|
// re-read those arguments, so the reused index gets unloaded.
|
|
//
|
|
// Asking liveness first and re-verifying the fingerprint after it means a
|
|
// replacement arriving in that window is caught by the argument comparison,
|
|
// and only the irreducible snapshot-to-unload interval remains.
|
|
match attributed_liveness(&liveness, fp) {
|
|
Liveness::Dead => {}
|
|
Liveness::Alive => {
|
|
println!(
|
|
"[pixelpass] --repair: pid {} is alive again; leaving module #{} alone",
|
|
fp.pid, fp.id
|
|
);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
Liveness::Unknown => {
|
|
eprintln!(
|
|
"[pixelpass] --repair: pid {}'s liveness became undeterminable; \
|
|
leaving module #{} alone",
|
|
fp.pid, fp.id
|
|
);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Fresh snapshot per action, taken after the liveness answer. Deliberately
|
|
// not hoisted out of the loop: each unload changes the module table, and the
|
|
// point is to decide against the table as it is *now*.
|
|
let current = pulse
|
|
.list_modules()
|
|
.context("could not re-list Pulse modules")?;
|
|
let Some(obs) = current.iter().find(|m| m.id == fp.id) else {
|
|
println!(
|
|
"[pixelpass] --repair: module #{} is already gone; skipping",
|
|
fp.id
|
|
);
|
|
skipped += 1;
|
|
continue;
|
|
};
|
|
if !fp.still_matches(obs) {
|
|
// The index now names something else, or the same module's
|
|
// arguments changed. Either way we no longer know what we would be
|
|
// destroying, so we do not destroy it.
|
|
eprintln!(
|
|
"[pixelpass] --repair: module #{} no longer matches what was planned \
|
|
(index reused?); refusing to unload it",
|
|
fp.id
|
|
);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
// The sink goes last in the plan, but "last" is not the same as "nothing
|
|
// is attached any more": a loopback unload may have failed or been
|
|
// skipped, or a new one may have arrived since. Ask the fresh snapshot.
|
|
if fp.shape == Shape::LegacyCaptureSink
|
|
&& let Some(holder) = plan::sink_still_referenced(¤t, fp.pid, fp.id)
|
|
{
|
|
eprintln!(
|
|
"[pixelpass] --repair: module #{} (capture sink for pid {}) is still referenced \
|
|
by module #{}; leaving the sink loaded",
|
|
fp.id, fp.pid, holder
|
|
);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
|
|
match pulse.unload_module(fp.id) {
|
|
Ok(()) => {
|
|
println!("[pixelpass] --repair: {}", describe(fp));
|
|
unloaded += 1;
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[pixelpass] --repair: failed to unload #{}: {e:#}", fp.id);
|
|
failed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !planned.live_pids.is_empty() {
|
|
println!(
|
|
"[pixelpass] --repair: left {} live pixelpass host(s) alone.",
|
|
planned.live_pids.len()
|
|
);
|
|
}
|
|
if !planned.unknown_pids.is_empty() {
|
|
println!(
|
|
"[pixelpass] --repair: left {} pid(s) alone whose liveness could not be determined.",
|
|
planned.unknown_pids.len()
|
|
);
|
|
}
|
|
if skipped > 0 {
|
|
// Deliberately not "changed under us": a skip can also mean the module is
|
|
// still referenced, or its owner's liveness stopped being decidable. The
|
|
// per-module reason was printed above.
|
|
println!("[pixelpass] --repair: skipped {skipped} module(s) (reasons above).");
|
|
}
|
|
if failed > 0 {
|
|
bail!("--repair: {failed} module(s) failed to unload (see errors above)");
|
|
}
|
|
println!("[pixelpass] --repair: cleaned up {unloaded} module(s).");
|
|
Ok(())
|
|
}
|
|
|
|
fn describe(fp: &Fingerprint) -> String {
|
|
format!(
|
|
"unloaded {} #{} (orphaned from pid {})",
|
|
fp.shape.label(),
|
|
fp.id,
|
|
fp.pid
|
|
)
|
|
}
|
|
|
|
/// Ask liveness with the module's *attribution* in hand.
|
|
///
|
|
/// A module whose token matches this machine, boot and pid namespace has already
|
|
/// proven that its pid is a number meaningful here — that is the token's entire
|
|
/// job. Running such a module through the probe's degradation checks would defeat
|
|
/// it in exactly the situation it exists for: a host crashing inside a container
|
|
/// leaves a token that matches perfectly, while a container marker or a multi-entry
|
|
/// `NSpid` makes the probe answer `Unknown` for everything, so token-qualified
|
|
/// repair would do nothing precisely where it is now safe.
|
|
///
|
|
/// The degradation signals therefore guard only the *untagged* path, where a bare
|
|
/// pid is all there is and those signals are the only protection left.
|
|
fn liveness_for(probe: &LivenessProbe, attribution: plan::Attribution, pid: u32) -> Liveness {
|
|
match attribution {
|
|
plan::Attribution::Tokened => probe.of_attributed(pid),
|
|
plan::Attribution::Untagged => probe.of(pid),
|
|
}
|
|
}
|
|
|
|
/// The same rule, for a fingerprint at execution time.
|
|
fn attributed_liveness(probe: &LivenessProbe, fp: &Fingerprint) -> Liveness {
|
|
let attribution = match fp.owner {
|
|
Some(_) => plan::Attribution::Tokened,
|
|
None => plan::Attribution::Untagged,
|
|
};
|
|
liveness_for(probe, attribution, fp.pid)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Identity
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// This process's machine, boot and pid-namespace identity.
|
|
///
|
|
/// Read from the kernel and the system, never guessed: without all three, a token
|
|
/// cannot be compared and no module can be attributed. Dashes are stripped so every
|
|
/// component is safe inside a single unquoted Pulse property value.
|
|
pub fn local_identity() -> Result<plan::LocalIdentity> {
|
|
let machine = read_identity_file("/etc/machine-id")
|
|
.or_else(|_| read_identity_file("/var/lib/dbus/machine-id"))
|
|
.context("could not read a machine id")?;
|
|
let boot = read_identity_file("/proc/sys/kernel/random/boot_id")
|
|
.context("could not read the boot id")?;
|
|
let pid_ns = pid_namespace_id().context("could not read this process's pid namespace")?;
|
|
Ok(plan::LocalIdentity {
|
|
machine,
|
|
boot,
|
|
pid_ns,
|
|
})
|
|
}
|
|
|
|
fn read_identity_file(path: &str) -> Result<String> {
|
|
let raw = std::fs::read_to_string(path).with_context(|| format!("could not read {path}"))?;
|
|
let cleaned: String = raw
|
|
.trim()
|
|
.chars()
|
|
.filter(|c| c.is_ascii_hexdigit())
|
|
.collect();
|
|
if cleaned.is_empty() {
|
|
bail!("{path} held no usable identity");
|
|
}
|
|
Ok(cleaned)
|
|
}
|
|
|
|
/// The inode of `/proc/self/ns/pid` — the kernel's identity for a pid namespace.
|
|
///
|
|
/// This is the value that makes a pid meaningful: two processes in different pid
|
|
/// namespaces can hold the same number, and only this distinguishes them.
|
|
fn pid_namespace_id() -> Result<u64> {
|
|
use std::os::unix::fs::MetadataExt;
|
|
let meta =
|
|
std::fs::metadata("/proc/self/ns/pid").context("could not stat /proc/self/ns/pid")?;
|
|
Ok(meta.ino())
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Liveness
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// Answers "is this pid still around", and knows when it must refuse to answer.
|
|
///
|
|
/// **An error is not an absence.** `Path::exists()` folds permission errors and a
|
|
/// missing `/proc` into `false`, which here would read as "dead, go ahead and
|
|
/// unload". Liveness is asked with `kill(pid, 0)` instead, where `EPERM` *proves*
|
|
/// existence.
|
|
///
|
|
/// # The limit of what this can prove, stated rather than papered over
|
|
///
|
|
/// A pid can be alive and invisible. Inside a pid namespace — a container, a
|
|
/// distrobox — `/proc/self` is perfectly visible while every process in the
|
|
/// *parent* namespace is not, and `hidepid` has the same self-visible,
|
|
/// others-invisible shape. Repair in such a place can reach the host's Pulse
|
|
/// socket, see a live host's modules, get `ESRCH` for its pid and unload a running
|
|
/// host's audio.
|
|
///
|
|
/// The signals below are **negative** ones: they detect *some* cases where pid
|
|
/// numbers cannot be trusted, and every one of them fails closed. What they cannot
|
|
/// do is prove the converse. `NSpid` reports this process's pid in each namespace
|
|
/// that its procfs can see, and its leftmost value is relative to the pid namespace
|
|
/// that mounted that procfs — so a nested namespace with its own `/proc` reports a
|
|
/// single entry quite legitimately. `NSpid > 1` therefore means "definitely
|
|
/// nested", while `NSpid == 1` means only "not detectably nested".
|
|
///
|
|
/// Closing that properly needs the module itself to carry an owner token (machine
|
|
/// and boot identity plus pid-namespace identity) written at load time, with
|
|
/// token-less modules treated as `Unknown`. That changes what pixelpass writes into
|
|
/// the graph and how far back `--repair` can clean up, so it is a design decision
|
|
/// recorded in the impl plan rather than guessed at here.
|
|
struct LivenessProbe {
|
|
/// `None` when no signal says pid numbers are untrustworthy; `Some(reason)`
|
|
/// when every answer must be [`Liveness::Unknown`].
|
|
degraded: Option<String>,
|
|
}
|
|
|
|
impl LivenessProbe {
|
|
fn new() -> Self {
|
|
Self {
|
|
degraded: Self::detect_degradation(),
|
|
}
|
|
}
|
|
|
|
fn detect_degradation() -> Option<String> {
|
|
// Locality is deliberately NOT checked here. `PULSE_SERVER` is a fallback
|
|
// *list*, so `unix:/missing tcp:remote:4713` starts with "unix:" and still
|
|
// connects to another machine, and a remote server can be selected by client
|
|
// configuration with the variable unset entirely. The authoritative answer
|
|
// comes from `pa_context_is_local()` on the connection that actually got
|
|
// established — see `introspect::PulseSession::connect`.
|
|
match std::fs::read_to_string("/proc/self/status") {
|
|
Ok(status) => {
|
|
let nspid = status
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("NSpid:"))
|
|
.map(|rest| rest.split_whitespace().count());
|
|
match nspid {
|
|
Some(n) if n > 1 => {
|
|
return Some(format!(
|
|
"this process is in a nested pid namespace (NSpid has {n} entries), \
|
|
so pids in module names may belong to processes it cannot see"
|
|
));
|
|
}
|
|
// NB: a single entry is not proof of the initial namespace — see
|
|
// the type's doc comment. It only means nothing detected it.
|
|
// A kernel too old to report NSpid cannot rule nesting out.
|
|
None => {
|
|
return Some(
|
|
"/proc/self/status does not report NSpid, so pid-namespace identity \
|
|
cannot be established"
|
|
.to_string(),
|
|
);
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
Err(e) => return Some(format!("/proc/self/status could not be read: {e}")),
|
|
}
|
|
// Belt and braces: container runtimes that leave a marker.
|
|
for marker in ["/run/.containerenv", "/.dockerenv"] {
|
|
if Path::new(marker).try_exists().unwrap_or(false) {
|
|
return Some(format!("{marker} exists, so this is a container"));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn degraded_reason(&self) -> Option<&str> {
|
|
self.degraded.as_deref()
|
|
}
|
|
|
|
/// Liveness for a pid this process has **no** independent reason to trust —
|
|
/// an untagged module. Here the degradation signals are the only protection.
|
|
fn of(&self, pid: u32) -> Liveness {
|
|
if self.degraded.is_some() {
|
|
return Liveness::Unknown;
|
|
}
|
|
self.of_attributed(pid)
|
|
}
|
|
|
|
/// Liveness for a pid already proven to belong to this machine, boot and pid
|
|
/// namespace by an [`plan::OwnerToken`].
|
|
///
|
|
/// The degradation checks are deliberately skipped: they exist to guess at
|
|
/// whether a bare pid is meaningful, and here that is not a guess any more.
|
|
fn of_attributed(&self, pid: u32) -> Liveness {
|
|
// `kill(0, …)` signals our whole process group and a negative pid signals
|
|
// another group, so neither may ever reach `kill`. Neither is a pid we
|
|
// could have written into a sink name anyway.
|
|
if pid == 0 || pid > i32::MAX as u32 {
|
|
return Liveness::Unknown;
|
|
}
|
|
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), None) {
|
|
Ok(()) => Liveness::Alive,
|
|
// The process exists; we merely may not signal it.
|
|
Err(nix::errno::Errno::EPERM) => Liveness::Alive,
|
|
Err(nix::errno::Errno::ESRCH) => Liveness::Dead,
|
|
Err(_) => Liveness::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// The `pactl`-text parser that used to live here is gone, and so are its
|
|
// tests: `introspect` gets index, name and argument as structured fields, so
|
|
// there is no format left to mis-parse. What replaced those tests is the live
|
|
// field gate, since the remaining risk is in talking to the server, which no
|
|
// unit test can exercise. The decisions all live in `plan`, which is pure and
|
|
// tested there.
|
|
|
|
/// The probe must never say `Dead` when it cannot see the whole pid space, and
|
|
/// must never ask `kill` about a pid that would signal something other than one
|
|
/// process.
|
|
#[test]
|
|
fn a_degraded_probe_never_reports_dead() {
|
|
let degraded = LivenessProbe {
|
|
degraded: Some("test".to_string()),
|
|
};
|
|
assert_eq!(degraded.of(1), Liveness::Unknown);
|
|
assert_eq!(degraded.of(u32::MAX), Liveness::Unknown);
|
|
|
|
let probe = LivenessProbe::new();
|
|
// Our own pid is alive by construction — unless this test itself runs
|
|
// somewhere the probe must abstain, which is exactly the other branch.
|
|
let me = std::process::id();
|
|
match probe.degraded_reason() {
|
|
None => assert_eq!(probe.of(me), Liveness::Alive),
|
|
Some(_) => assert_eq!(probe.of(me), Liveness::Unknown),
|
|
}
|
|
// `kill(0, …)` would signal our whole process group, and a pid past
|
|
// `i32::MAX` cannot be expressed to `kill` at all.
|
|
assert_eq!(probe.of(0), Liveness::Unknown);
|
|
assert_eq!(probe.of(u32::MAX), Liveness::Unknown);
|
|
}
|
|
|
|
/// A token proves the pid is meaningful here, so the probe's namespace
|
|
/// guesswork must not veto it. Without this, a host crashing inside a
|
|
/// container leaves a perfectly matching token while a container marker makes
|
|
/// every answer `Unknown` — and token-qualified repair does nothing in exactly
|
|
/// the situation the token was built for.
|
|
///
|
|
/// This runs against a *degraded* probe deliberately: on an ordinary desktop
|
|
/// the two paths agree, so a test using the real probe's state would pass
|
|
/// whether or not the distinction exists.
|
|
#[test]
|
|
fn a_token_beats_the_degradation_signals_but_a_bare_pid_does_not() {
|
|
let degraded = LivenessProbe {
|
|
degraded: Some("pretending to be in a container".to_string()),
|
|
};
|
|
let me = std::process::id();
|
|
|
|
assert_eq!(
|
|
degraded.of_attributed(me),
|
|
Liveness::Alive,
|
|
"an attributed pid must still be answered when the probe is degraded"
|
|
);
|
|
assert_eq!(
|
|
degraded.of(me),
|
|
Liveness::Unknown,
|
|
"a bare pid must not be, since the signals are all it has"
|
|
);
|
|
|
|
// And the routing between them, which is what the caller actually uses.
|
|
assert_eq!(
|
|
liveness_for(°raded, plan::Attribution::Tokened, me),
|
|
Liveness::Alive
|
|
);
|
|
assert_eq!(
|
|
liveness_for(°raded, plan::Attribution::Untagged, me),
|
|
Liveness::Unknown
|
|
);
|
|
}
|
|
}
|