wsb 0.291.8591

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The lifecycle held to what a workshop does: a stand-in systemd and the
//! daemons on one host, sharing one state, so a daemon the stand-in
//! starts beside another refuses the way a real one does.
//!
//! A real supervisor is not used: the service is one per user, and a test that
//! installed one would replace the daemon of whichever workshop ran the suite.

use super::*;
use crate::daemon::service::{systemd_state, launchd_state, Phase};
use std::cell::RefCell;
use std::rc::Rc;

const VERSION: &str = "1.2.3";

/// One workshop: the unit systemd keeps, and the daemon serving.
struct World {
    calls: Vec<String>,
    /// systemd's view of the unit: (ActiveState, SubState), its pid, restarts.
    unit: (&'static str, &'static str),
    unit_pid: Option<u32>,
    restarts: u32,
    last_exit: Option<i32>,
    /// The daemon serving the workshop, whoever started it.
    serving: Option<Serving>,
    /// The version the unit's definition starts.
    unit_version: String,
    /// Whether the unit's daemon fails as soon as it starts.
    crashes: bool,
    next_pid: u32,
}

impl World {
    fn new() -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Self {
            calls: Vec::new(),
            unit: ("inactive", "dead"),
            unit_pid: None,
            restarts: 0,
            last_exit: None,
            serving: None,
            unit_version: VERSION.to_string(),
            crashes: false,
            next_pid: 500,
        }))
    }

    fn start_unit(&mut self) {
        if self.unit.0 == "active" {
            return;
        }
        if self.crashes {
            self.unit = ("activating", "auto-restart");
            self.unit_pid = None;
            self.restarts += 3;
            self.last_exit = Some(1);
            return;
        }
        if self.serving.is_some() {
            // The daemon finds another serving, and exits with the status the
            // unit does not restart on.
            self.unit = ("failed", "failed");
            self.unit_pid = None;
            self.last_exit = Some(crate::daemon::ANOTHER_SERVES_EXIT);
            return;
        }
        self.next_pid += 1;
        self.unit = ("active", "running");
        self.unit_pid = Some(self.next_pid);
        self.last_exit = None;
        self.serving = Some(Serving { pid: self.next_pid, version: self.unit_version.clone(), port: Some(3000) });
    }

    fn stop_unit(&mut self) {
        if let Some(pid) = self.unit_pid.take() {
            if self.serving.as_ref().is_some_and(|daemon| daemon.pid == pid) {
                self.serving = None;
            }
        }
        self.unit = ("inactive", "dead");
    }
}

struct Systemd(Rc<RefCell<World>>);

impl Control for Systemd {
    fn systemctl(&mut self, arguments: &[&str]) -> Result<String, String> {
        let mut world = self.0.borrow_mut();
        let verb = arguments[0];
        if verb != "show" {
            world.calls.push(format!("systemctl {verb}"));
        }
        match verb {
            "show" => Ok(format!(
                "ActiveState={}\nSubState={}\nMainPID={}\nNRestarts={}\nExecMainStatus={}\nResult={}\n",
                world.unit.0,
                world.unit.1,
                world.unit_pid.unwrap_or(0),
                world.restarts,
                world.last_exit.unwrap_or(0),
                if world.last_exit.is_some() { "exit-code" } else { "success" }
            )),
            "start" => {
                world.start_unit();
                Ok(String::new())
            }
            "stop" => {
                world.stop_unit();
                Ok(String::new())
            }
            _ => Ok(String::new()),
        }
    }

    fn launchctl(&mut self, _arguments: &[String]) -> Result<String, String> {
        Err("this host has systemd".to_string())
    }

    fn launchd_loaded(&mut self, _target: &str) -> Result<bool, String> {
        Err("this host has systemd".to_string())
    }
}

struct OnTheWorkshop(Rc<RefCell<World>>);

impl Daemons for OnTheWorkshop {
    fn serving(&mut self) -> Result<Option<Serving>, String> {
        Ok(self.0.borrow().serving.clone())
    }

    fn stop(&mut self, daemon: &Serving) -> Result<(), String> {
        let mut world = self.0.borrow_mut();
        world.calls.push(format!("stop {}", daemon.pid));
        if world.unit_pid == Some(daemon.pid) {
            // A deliberate stop exits zero, which the unit does not restart.
            world.unit_pid = None;
            world.unit = ("inactive", "dead");
        }
        world.serving = None;
        Ok(())
    }
}

fn bring_up(world: &Rc<RefCell<World>>, restart: bool) -> Result<Brought, String> {
    bring_up_with(
        &mut Systemd(world.clone()),
        &mut OnTheWorkshop(world.clone()),
        Supervisor::Systemd,
        Path::new("/nowhere/wsb-daemon.service"),
        VERSION,
        restart,
        |control| control.systemctl(&["daemon-reload"]).map(|_| ()),
        Duration::from_secs(5),
        Duration::from_millis(1),
    )
}

/// TEST20215: a daemon started on demand is replaced by the service's, before
/// the service is started, and the install ends with the service's daemon
/// answering and says what it replaced.
///
/// Started the other way round, the service's daemon found the port taken and
/// refused, every five seconds, while the install had reported success.
#[test]
fn test20215_a_stray_daemon_gives_way_to_the_service() {
    let world = World::new();
    world.borrow_mut().serving = Some(Serving { pid: 281935, version: VERSION.to_string(), port: Some(3000) });

    let brought = bring_up(&world, false).expect("the service's daemon comes up");

    let world = world.borrow();
    assert_eq!(Some(brought.serving.pid), world.unit_pid, "what serves is not the service's daemon");
    assert_eq!(world.calls, ["stop 281935", "systemctl daemon-reload", "systemctl reset-failed", "systemctl start"]);
    assert!(brought.replaced[0].contains("281935") && brought.replaced[0].contains("not keeping"), "{:?}", brought.replaced);
}

/// TEST20216: the service's own daemon is kept when it is current, replaced
/// when it is another version, and started again on a restart.
#[test]
fn test20216_the_services_daemon_is_kept_replaced_or_restarted() {
    let world = World::new();
    world.borrow_mut().start_unit();
    let first = world.borrow().unit_pid;

    let kept = bring_up(&world, false).expect("a current daemon is kept");
    assert_eq!(Some(kept.serving.pid), first);
    assert!(kept.replaced.is_empty(), "{:?}", kept.replaced);

    let restarted = bring_up(&world, true).expect("a restart starts it again");
    assert_ne!(Some(restarted.serving.pid), first, "a restart left the same daemon serving");
    assert!(restarted.replaced.is_empty(), "a restart is not a replacement: {:?}", restarted.replaced);

    // The pair was replaced under the running service.
    world.borrow_mut().serving.as_mut().unwrap().version = "1.2.2".to_string();
    let replaced = bring_up(&world, false).expect("an older daemon is replaced");
    assert_eq!(replaced.serving.version, VERSION);
    assert!(replaced.replaced[0].contains("1.2.2"), "{:?}", replaced.replaced);

    // And a definition that starts another build is said, not waited on.
    world.borrow_mut().unit_version = "0.9.0".to_string();
    let refused = bring_up(&world, true).expect_err("the service starts another build");
    assert!(refused.contains("0.9.0") && refused.contains("wsb daemon install"), "{refused}");
}

/// TEST20217: a service whose daemon keeps failing is reported as failing as
/// soon as the supervisor says so, with how it last exited.
#[test]
fn test20217_a_failing_service_is_never_reported_as_up() {
    let world = World::new();
    world.borrow_mut().crashes = true;
    let refused = bring_up(&world, false).expect_err("a crash loop is not a daemon");
    assert!(refused.starts_with("the service is failing") && refused.contains("status 1"), "{refused}");
    assert!(!refused.contains("has not answered"), "the failure was waited out, not seen: {refused}");
}

/// TEST20218: what systemd and launchd say is read for what it means: a unit
/// "activating" between restarts after failures is failing, not running, and
/// a refusal because another daemon serves is named.
#[test]
fn test20218_a_crash_loop_reads_as_failing() {
    // `systemctl --user show` through the loop seen on 25 Oct.
    let looping = systemd_state(
        "ActiveState=activating\nSubState=auto-restart\nMainPID=0\nNRestarts=127\nExecMainStatus=1\nResult=exit-code\n",
    );
    assert_eq!(looping.phase, Phase::Restarting);
    assert!(looping.failing() && looping.keeping().is_none());
    assert_eq!(looping.failure(), "failing: started again 127 time(s); its daemon last exited with status 1");

    let running = systemd_state("ActiveState=active\nSubState=running\nMainPID=4242\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
    assert_eq!((running.phase, running.keeping(), running.failing()), (Phase::Running, Some(4242), false));
    let starting = systemd_state("ActiveState=activating\nSubState=start\nMainPID=4243\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
    assert_eq!((starting.phase, starting.keeping()), (Phase::Starting, None));
    let refused = systemd_state("ActiveState=failed\nSubState=failed\nMainPID=0\nNRestarts=0\nExecMainStatus=78\nResult=exit-code\n");
    assert!(refused.failing());
    assert!(refused.failure().contains("another daemon was serving"), "{}", refused.failure());
    let stopped = systemd_state("ActiveState=inactive\nSubState=dead\nMainPID=0\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
    assert_eq!((stopped.phase, stopped.failing()), (Phase::Stopped, false));

    let job = "gui/501/com.machinefabric.wsb-daemon = {\n\tactive count = 1\n\tstate = running\n\truns = 1\n\tpid = 812\n\tlast exit code = (never exited)\n}\n";
    let up = launchd_state(job);
    assert_eq!((up.phase, up.keeping()), (Phase::Running, Some(812)));
    let throttled = launchd_state("gui/501/x = {\n\tstate = not running\n\truns = 9\n\tlast exit code = 1\n}\n");
    assert_eq!((throttled.phase, throttled.restarts, throttled.last_exit), (Phase::Restarting, Some(8), Some(1)));
    let idle = launchd_state("gui/501/x = {\n\tstate = not running\n\truns = 1\n\tlast exit code = 0\n}\n");
    assert_eq!(idle.phase, Phase::Stopped);
}

/// TEST20219: a service's definition does not start again a daemon that
/// refused because another serves the workshop, and tells the daemon which
/// supervisor keeps it.
#[test]
fn test20219_a_refusal_is_never_restarted_into_a_loop() {
    let record = service::Record {
        executable: PathBuf::from("/opt/wsb/wsb-daemon"),
        arguments: vec!["--worktree".to_string(), "/w".to_string()],
        worktree_root: PathBuf::from("/w"),
        log: PathBuf::from("/m/daemon.log"),
        path: "/usr/bin".to_string(),
        host_dir: PathBuf::from("/m"),
        prj: None,
        autostart: true,
    };
    let unit = service::render(Supervisor::Systemd, &record);
    assert!(unit.contains(&format!("RestartPreventExitStatus={}\n", crate::daemon::ANOTHER_SERVES_EXIT)), "{unit}");
    assert!(unit.contains(&format!("Environment={}=systemd\n", crate::daemon::SUPERVISOR_ENV)), "{unit}");
    let plist = service::render(Supervisor::Launchd, &record);
    assert!(
        plist.contains(&format!("<key>{}</key>\n    <string>launchd</string>", crate::daemon::SUPERVISOR_ENV)),
        "{plist}"
    );
}

/// TEST20239: a service whose record an older wsb wrote is still stopped.
///
/// `wsb migrate` converts that record, and refuses while the daemon runs;
/// stopping read the record first and refused it, naming `wsb migrate`. Each
/// waited on the other, and the upgrade could not begin. Stopping needs only
/// the supervisor; what reads the record still refuses it.
#[test]
fn test20239_a_service_with_an_older_record_is_still_stopped() {
    let dir = tempfile::tempdir().unwrap();
    let older = serde_json::json!({ "worktree_root": "/w", crate::migrate::OLD_HOST_DIR_FIELD: "/h" });
    std::fs::write(service::record_path(dir.path()), older.to_string()).unwrap();
    let world = World::new();
    world.borrow_mut().start_unit();
    let kept = world.borrow().unit_pid.expect("the unit's daemon");

    let stopped = stop_in(dir.path(), &mut Systemd(world.clone()), &mut OnTheWorkshop(world.clone()), || Ok(Supervisor::Systemd))
        .expect("the service stops");

    assert!(world.borrow().calls.iter().any(|call| call == "systemctl stop"), "{:?}", world.borrow().calls);
    assert_eq!(world.borrow().unit, ("inactive", "dead"));
    assert!(stopped.iter().any(|line| line.contains(&kept.to_string())), "{stopped:?}");
    let read = installed(dir.path()).map(|_| ()).expect_err("an older record is not read as this wsb's");
    assert!(read.contains("wsb migrate"), "{read}");
}