#![allow(
clippy::disallowed_types,
reason = "the probe runs this binary; it is the second named spawn site"
)]
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
type Tree = BTreeMap<String, Vec<u8>>;
const OVERLAY: &str = "a-file-this-provider-does-not-own.txt";
const OVERLAY_BYTES: &[u8] = b"kept verbatim\n";
struct Ran {
ok: bool,
out: String,
}
fn run(exe: &Path, arguments: &[&OsStr]) -> Ran {
match Command::new(exe).args(arguments).output() {
Ok(output) => {
let mut out = String::from_utf8_lossy(&output.stdout).into_owned();
out.push_str(&String::from_utf8_lossy(&output.stderr));
Ran {
ok: output.status.success(),
out: out.replace("\r\n", "\n"),
}
}
Err(error) => Ran {
ok: false,
out: format!("{} could not be run: {error}", exe.display()),
},
}
}
fn at(exe: &Path, target: &Path, arguments: &[&str]) -> Ran {
let mut all: Vec<&OsStr> = arguments.iter().map(OsStr::new).collect();
all.push(OsStr::new("--target"));
all.push(target.as_os_str());
run(exe, &all)
}
fn files_under(root: &Path) -> Vec<(String, PathBuf)> {
let mut found = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(directory) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&directory) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
let Ok(relative) = path.strip_prefix(root) else {
continue;
};
let parts: Vec<String> = relative
.components()
.map(|part| part.as_os_str().to_string_lossy().into_owned())
.collect();
found.push((parts.join("/"), path));
}
}
found
}
fn tree(target: &Path, own: &[String]) -> Tree {
let mut found = Tree::new();
for (key, path) in files_under(target) {
let mine = own
.iter()
.any(|name| key == *name || key.starts_with(&format!("{name}/")));
if !mine {
found.insert(key, std::fs::read(&path).unwrap_or_default());
}
}
found
}
fn names(target: &Path, own: &[String]) -> Vec<String> {
tree(target, own).into_keys().collect()
}
fn scratch(label: &str) -> Result<PathBuf, String> {
let path =
std::env::temp_dir().join(format!("setup-system-probe-{}-{label}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path)
.map(|()| path.clone())
.map_err(|error| format!("no scratch directory at {}: {error}", path.display()))
}
fn catalog(exe: &Path) -> Result<Vec<String>, String> {
let listed = run(exe, &[OsStr::new("list")]);
if !listed.ok {
return Err(format!("list failed:\n{}", listed.out));
}
Ok(listed
.out
.lines()
.filter_map(|line| {
let body = line.trim_end().strip_prefix(" ")?;
if body.starts_with(' ') || body.is_empty() {
return None;
}
Some(body.to_owned())
})
.collect())
}
fn control_state(exe: &Path, setup: &str) -> Result<Vec<String>, String> {
let target = scratch("control-state")?;
let install = at(exe, &target, &["install", setup]);
if !install.ok {
return Err(format!(
"install into an empty target failed:\n{}",
install.out
));
}
let removed = at(exe, &target, &["remove"]);
if !removed.ok {
return Err(format!("remove failed:\n{}", removed.out));
}
let mut own: Vec<String> = Vec::new();
for (key, _) in files_under(&target) {
let Some(first) = key.split('/').next() else {
continue;
};
if !own.iter().any(|held| held == first) {
own.push(first.to_owned());
}
}
own.sort();
let _ = std::fs::remove_dir_all(&target);
Ok(own)
}
fn overlay_survives(target: &Path, after: &str, found: &mut Vec<String>) {
match std::fs::read(target.join(OVERLAY)) {
Ok(bytes) if bytes == OVERLAY_BYTES => {}
Ok(_) => found.push(format!("the sibling overlay was rewritten by {after}")),
Err(_) => found.push(format!("the sibling overlay was removed by {after}")),
}
}
fn through_install(
exe: &Path,
target: &Path,
own: &[String],
setups: (&str, &str),
found: &mut Vec<String>,
) -> Option<Tree> {
let (first, second) = setups;
let empty = at(exe, target, &["status"]);
if !empty.ok {
found.push(format!(
"status on an untouched target failed:\n{}",
empty.out
));
}
if !empty.out.contains("none applied") {
found.push(format!(
"status on an untouched target does not say nothing is applied:\n{}",
empty.out
));
}
let installed = at(exe, target, &["install", first]);
if !installed.ok {
found.push(format!("install {first} failed:\n{}", installed.out));
return None;
}
let after_install = tree(target, own);
if !after_install.contains_key(OVERLAY) {
found.push("install removed the sibling overlay".to_owned());
}
if after_install.len() < 2 {
found.push(format!(
"install {first} left {} file(s) beside the overlay",
after_install.len().saturating_sub(1)
));
}
overlay_survives(target, "install", found);
let applied = at(exe, target, &["status"]);
if !applied.out.contains(first) {
found.push(format!(
"status does not name the applied setup:\n{}",
applied.out
));
}
if !applied.out.contains("Drift none") {
found.push(format!(
"status reports drift on a fresh install:\n{}",
applied.out
));
}
let selected = at(exe, target, &["select", second]);
if !selected.ok {
found.push(format!("select {second} failed:\n{}", selected.out));
return None;
}
if tree(target, own) == after_install {
found.push(format!(
"select {second} left the target byte-identical to {first}"
));
}
overlay_survives(target, "select", found);
Some(after_install)
}
fn through_restore(
exe: &Path,
target: &Path,
own: &[String],
first: &str,
after_install: &Tree,
found: &mut Vec<String>,
) {
let listed = at(exe, target, &["backups"]);
if !listed.out.contains("slot-000000000001") || !listed.out.contains("slot-000000000002") {
found.push(format!(
"backups does not list both captures:\n{}",
listed.out
));
}
let held = at(
exe,
target,
&[
"hold",
"--backup",
"slot-000000000001",
"--reason",
"the probe",
],
);
if !held.ok {
found.push(format!("hold failed:\n{}", held.out));
}
let restored = at(exe, target, &["restore"]);
if !restored.ok {
found.push(format!("restore failed:\n{}", restored.out));
return;
}
let after_restore = tree(target, own);
if after_restore != *after_install {
found.push(format!(
"restore did not return the target to what {first} left: {} file(s) then, {} now",
after_install.len(),
after_restore.len()
));
}
overlay_survives(target, "restore", found);
let to_first = at(exe, target, &["restore", "--backup", "slot-000000000001"]);
if !to_first.ok {
found.push(format!("restore --backup failed:\n{}", to_first.out));
}
let at_origin = names(target, own);
if at_origin != vec![OVERLAY.to_owned()] {
found.push(format!(
"a restore to the first slot left {at_origin:?} instead of the overlay alone"
));
}
overlay_survives(target, "restore --backup", found);
let released = at(exe, target, &["release", "--backup", "slot-000000000001"]);
if !released.ok {
found.push(format!("release failed:\n{}", released.out));
}
}
fn through_remove(exe: &Path, target: &Path, own: &[String], first: &str, found: &mut Vec<String>) {
let reinstalled = at(exe, target, &["install", first]);
if !reinstalled.ok {
found.push(format!(
"a second install of {first} failed:\n{}",
reinstalled.out
));
}
let removed = at(exe, target, &["remove"]);
if !removed.ok {
found.push(format!("remove failed:\n{}", removed.out));
}
for wanted in [
"go whole, not file by file",
"the backup slot holds it",
"restore it with",
] {
if !removed.out.contains(wanted) {
found.push(format!(
"remove no longer tells a person {wanted:?}; it said:\n{}",
removed.out
));
}
}
let left = names(target, own);
if left != vec![OVERLAY.to_owned()] {
found.push(format!("remove left {left:?} instead of the overlay alone"));
}
overlay_survives(target, "remove", found);
let recovered = at(exe, target, &["recover-operation", "--json"]);
if !recovered.ok {
found.push(format!(
"recover-operation on a settled target failed:\n{}",
recovered.out
));
}
}
#[must_use]
pub fn round_trip(exe: &Path) -> Vec<String> {
let mut found = Vec::new();
let setups = match catalog(exe) {
Ok(setups) => setups,
Err(problem) => {
found.push(problem);
return found;
}
};
if setups.len() < 2 {
found.push(format!(
"this binary carries {} setup(s); a round trip needs two to select between",
setups.len()
));
return found;
}
let (first, second) = (setups[0].clone(), setups[1].clone());
let own = match control_state(exe, &first) {
Ok(own) if own.is_empty() => {
found.push(
"an install followed by a remove left nothing at all, so this provider \
records no state of its own"
.to_owned(),
);
return found;
}
Ok(own) => own,
Err(problem) => {
found.push(problem);
return found;
}
};
let target = match scratch("target") {
Ok(target) => target,
Err(problem) => {
found.push(problem);
return found;
}
};
if let Err(error) = std::fs::write(target.join(OVERLAY), OVERLAY_BYTES) {
found.push(format!("the sibling overlay could not be written: {error}"));
return found;
}
if let Some(after_install) = through_install(exe, &target, &own, (&first, &second), &mut found)
{
through_restore(exe, &target, &own, &first, &after_install, &mut found);
through_remove(exe, &target, &own, &first, &mut found);
}
let _ = std::fs::remove_dir_all(&target);
found
}
#[must_use]
pub fn one_writer_at_a_time(exe: &Path) -> Vec<String> {
let mut found = Vec::new();
let Ok(target) = scratch("one-writer") else {
found.push("no scratch directory for the concurrency check".to_owned());
return found;
};
let Ok(setups) = catalog(exe) else {
found.push("the catalog could not be read for the concurrency check".to_owned());
return found;
};
let Some(setup) = setups.first() else {
found.push("no setup to install concurrently".to_owned());
return found;
};
let mut children = Vec::new();
for _ in 0..4 {
match Command::new(exe)
.args([
OsStr::new("install"),
OsStr::new(setup),
OsStr::new("--target"),
target.as_os_str(),
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(child) => children.push(child),
Err(error) => found.push(format!("a concurrent install could not start: {error}")),
}
}
let mut applied = 0_usize;
let mut refused_for_another_reason = Vec::new();
for child in children {
match child.wait_with_output() {
Ok(output) => {
if output.status.success() {
applied += 1;
} else {
let said = String::from_utf8_lossy(&output.stderr).into_owned()
+ &String::from_utf8_lossy(&output.stdout);
let at_the_lock = said.contains("target.lock");
let after_the_lock =
said.contains("the target changed after the lock was taken");
if !at_the_lock && !after_the_lock {
refused_for_another_reason.push(said.replace("\r\n", "\n"));
}
}
}
Err(error) => found.push(format!(
"a concurrent install could not be waited on: {error}"
)),
}
}
if applied == 0 {
found.push("no concurrent install applied, so the target was never written".to_owned());
}
for said in refused_for_another_reason {
found.push(format!(
"a concurrent install was refused for neither of the two reasons a race \
produces -- not at the lock, and not by the target moving after it:\n{said}"
));
}
let after = run(
exe,
&[
OsStr::new("diff"),
OsStr::new("--target"),
target.as_os_str(),
],
);
if !after.ok || !after.out.contains("matches the setup recorded in it") {
found.push(format!(
"after concurrent installs the target does not match its own record:\n{}",
after.out
));
}
let _ = std::fs::remove_dir_all(&target);
found
}
#[must_use]
pub fn refuses_a_target_it_should(exe: &Path) -> Vec<String> {
let mut found = Vec::new();
let relative = run(
exe,
&[
OsStr::new("status"),
OsStr::new("--target"),
OsStr::new("a-relative-name"),
],
);
if relative.ok {
found.push("a relative --target was accepted".to_owned());
}
let Ok(holder) = scratch("not-a-target") else {
found.push("no scratch directory for the refusal checks".to_owned());
return found;
};
let absent = holder.join("never-created");
let missing = run(
exe,
&[
OsStr::new("status"),
OsStr::new("--target"),
absent.as_os_str(),
],
);
if missing.ok {
found.push("a --target that does not exist was accepted".to_owned());
}
let file = holder.join("a-file");
if std::fs::write(&file, b"not a directory\n").is_err() {
found.push("the not-a-directory check could not write its file".to_owned());
} else {
let not_a_directory = run(
exe,
&[
OsStr::new("status"),
OsStr::new("--target"),
file.as_os_str(),
],
);
if not_a_directory.ok {
found.push("a --target that is a file was accepted".to_owned());
}
}
let _ = std::fs::remove_dir_all(&holder);
found
}