systemg 0.33.0

A simple process manager.
Documentation
#[path = "common/mod.rs"]
mod common;

#[cfg(target_os = "linux")]
use std::env;
#[cfg(target_os = "linux")]
use std::fs;
#[cfg(target_os = "linux")]
use std::path::Path;
#[cfg(target_os = "linux")]
use std::process::Command as StdCommand;

#[cfg(target_os = "linux")]
use assert_cmd::Command;
#[cfg(target_os = "linux")]
use common::HomeEnvGuard;
#[cfg(target_os = "linux")]
use predicates::prelude::PredicateBooleanExt;
#[cfg(target_os = "linux")]
use systemg::{
    config::load_config,
    daemon::{PidFile, ServiceLifecycleStatus, ServiceStateFile},
};
#[cfg(target_os = "linux")]
use tempfile::tempdir;

#[cfg(target_os = "linux")]
#[test]
fn logs_streams_when_pid_has_no_fds() {
    let temp = tempdir().expect("failed to create tempdir");
    let dir = temp.path();
    let home = dir.join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home = HomeEnvGuard::set(&home);

    let config_path = dir.join("systemg.yaml");
    fs::write(
        &config_path,
        r#"
version: "1"
services:
  arb_rs:
    command: "/bin/sleep 30"
"#,
    )
    .expect("write config");

    let log_dir = home.join(".local/share/systemg/logs");
    fs::create_dir_all(&log_dir).expect("make log dir");
    let stdout_path = log_dir.join("arb_rs_stdout.log");
    let stderr_path = log_dir.join("arb_rs_stderr.log");
    fs::write(&stdout_path, "streamed stdout line\n").expect("write stdout log");
    fs::write(&stderr_path, "").expect("write stderr log");

    let mut pid_file = PidFile::load().expect("load pid file");
    pid_file.insert("arb_rs", 999_999).expect("insert pid");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("sysg"));
    cmd.env("SYSTEMG_TAIL_MODE", "oneshot")
        .arg("logs")
        .arg("--config")
        .arg(&config_path)
        .arg("--service")
        .arg("arb_rs")
        .arg("--kind")
        .arg("stdout")
        .arg("--lines")
        .arg("1")
        .assert()
        .success()
        .stdout(predicates::str::contains("arb_rs"))
        .stdout(predicates::str::contains("streamed stdout line"));

    unsafe { env::remove_var("SYSTEMG_TAIL_MODE") };
}

#[cfg(target_os = "linux")]
#[test]
fn logs_uses_snapshot_runtime_when_pid_file_is_missing_service() {
    let temp = tempdir().expect("failed to create tempdir");
    let dir = temp.path();
    let home = dir.join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home = HomeEnvGuard::set(&home);

    let config_path = dir.join("systemg.yaml");
    fs::write(
        &config_path,
        r#"
version: "1"
services:
  demo:
    command: "/bin/sleep 30"
"#,
    )
    .expect("write config");

    let config =
        load_config(Some(config_path.to_string_lossy().as_ref())).expect("load config");
    let hash = config
        .services
        .get("demo")
        .expect("demo service")
        .compute_hash();

    let mut child = StdCommand::new("/bin/sleep")
        .arg("30")
        .spawn()
        .expect("spawn sleep");

    let mut state = ServiceStateFile::load().expect("load state");
    state
        .set(
            &hash,
            ServiceLifecycleStatus::Running,
            Some(child.id()),
            None,
            None,
        )
        .expect("persist state");

    let mut pid_file = PidFile::load().expect("load pid file");
    let _ = pid_file.remove("demo");

    let log_dir = home.join(".local/share/systemg/logs");
    fs::create_dir_all(&log_dir).expect("make log dir");
    fs::write(log_dir.join("demo_stdout.log"), "snapshot log line\n")
        .expect("write stdout log");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("sysg"));
    cmd.env("SYSTEMG_TAIL_MODE", "oneshot")
        .arg("logs")
        .arg("--config")
        .arg(&config_path)
        .arg("--service")
        .arg("demo")
        .arg("--kind")
        .arg("stdout")
        .arg("--lines")
        .arg("1")
        .assert()
        .success()
        .stdout(predicates::str::contains(format!(
            "demo [pid {}]",
            child.id()
        )))
        .stdout(predicates::str::contains("snapshot log line"))
        .stdout(predicates::str::contains("offline").not());

    let _ = child.kill();
    let _ = child.wait();
    unsafe { env::remove_var("SYSTEMG_TAIL_MODE") };
}

#[cfg(target_os = "linux")]
#[test]
fn logs_without_service_groups_running_before_offline() {
    let temp = tempdir().expect("failed to create tempdir");
    let dir = temp.path();
    let home = dir.join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home = HomeEnvGuard::set(&home);

    let config_path = dir.join("systemg.yaml");
    fs::write(
        &config_path,
        r#"
version: "1"
services:
  beta:
    command: "/bin/sleep 30"
  alpha:
    command: "/bin/echo alpha"
"#,
    )
    .expect("write config");

    let config =
        load_config(Some(config_path.to_string_lossy().as_ref())).expect("load config");
    let beta_hash = config
        .services
        .get("beta")
        .expect("beta service")
        .compute_hash();

    let mut child = StdCommand::new("/bin/sleep")
        .arg("30")
        .spawn()
        .expect("spawn sleep");

    let mut state = ServiceStateFile::load().expect("load state");
    state
        .set(
            &beta_hash,
            ServiceLifecycleStatus::Running,
            Some(child.id()),
            None,
            None,
        )
        .expect("persist state");

    let log_dir = home.join(".local/share/systemg/logs");
    fs::create_dir_all(&log_dir).expect("make log dir");
    fs::write(log_dir.join("beta_stdout.log"), "beta line\n").expect("write beta log");
    fs::write(log_dir.join("alpha_stdout.log"), "alpha line\n").expect("write alpha log");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("sysg"));
    let output = cmd
        .env("SYSTEMG_TAIL_MODE", "oneshot")
        .arg("logs")
        .arg("--config")
        .arg(&config_path)
        .arg("--kind")
        .arg("stdout")
        .arg("--lines")
        .arg("1")
        .output()
        .expect("run sysg logs");

    assert!(
        output.status.success(),
        "sysg logs should succeed, stderr was: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    let running_idx = stdout.find("Running Services").expect("running section");
    let offline_idx = stdout.find("Offline Services").expect("offline section");
    let beta_idx = stdout.find("beta [pid ").expect("running service heading");
    let alpha_idx = stdout
        .find("alpha [offline]")
        .expect("offline service heading");

    assert!(
        running_idx < offline_idx,
        "running section should come first"
    );
    assert!(
        running_idx < beta_idx,
        "running service should appear in running section"
    );
    assert!(
        offline_idx < alpha_idx,
        "offline service should appear in offline section"
    );
    assert!(
        beta_idx < alpha_idx,
        "running service output should appear before offline service output"
    );
    assert!(stdout.contains("beta line"));
    assert!(stdout.contains("alpha line"));

    let _ = child.kill();
    let _ = child.wait();
    unsafe { env::remove_var("SYSTEMG_TAIL_MODE") };
}

#[cfg(target_os = "linux")]
fn read_log(path: &Path) -> String {
    fs::read_to_string(path).expect("read log file")
}

#[cfg(target_os = "linux")]
#[test]
fn logs_purge_truncates_only_selected_service() {
    let temp = tempdir().expect("failed to create tempdir");
    let dir = temp.path();
    let home = dir.join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home = HomeEnvGuard::set(&home);

    let log_dir = home.join(".local/share/systemg/logs");
    fs::create_dir_all(&log_dir).expect("make log dir");

    let api_stdout = log_dir.join("api_stdout.log");
    let api_stderr = log_dir.join("api_stderr.log");
    let worker_stdout = log_dir.join("worker_stdout.log");
    let worker_stderr = log_dir.join("worker_stderr.log");

    fs::write(&api_stdout, "api stdout\n").expect("write api stdout");
    fs::write(&api_stderr, "api stderr\n").expect("write api stderr");
    fs::write(&worker_stdout, "worker stdout\n").expect("write worker stdout");
    fs::write(&worker_stderr, "worker stderr\n").expect("write worker stderr");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("sysg"));
    cmd.arg("logs")
        .arg("--service")
        .arg("api")
        .arg("--purge")
        .assert()
        .success()
        .stdout(predicates::str::is_empty());

    assert_eq!(read_log(&api_stdout), "");
    assert_eq!(read_log(&api_stderr), "");
    assert_eq!(read_log(&worker_stdout), "worker stdout\n");
    assert_eq!(read_log(&worker_stderr), "worker stderr\n");
}

#[cfg(target_os = "linux")]
#[test]
fn logs_purge_without_service_truncates_all_logs() {
    let temp = tempdir().expect("failed to create tempdir");
    let dir = temp.path();
    let home = dir.join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home = HomeEnvGuard::set(&home);

    let log_dir = home.join(".local/share/systemg/logs");
    fs::create_dir_all(&log_dir).expect("make log dir");

    let api_stdout = log_dir.join("api_stdout.log");
    let api_stderr = log_dir.join("api_stderr.log");
    let supervisor = log_dir.join("supervisor.log");
    let spawn_log = log_dir.join("spawn/child_stdout.log");

    fs::write(&api_stdout, "api stdout\n").expect("write api stdout");
    fs::write(&api_stderr, "api stderr\n").expect("write api stderr");
    fs::write(&supervisor, "supervisor event\n").expect("write supervisor");
    fs::create_dir_all(spawn_log.parent().expect("spawn parent"))
        .expect("create spawn dir");
    fs::write(&spawn_log, "spawn output\n").expect("write spawn log");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("sysg"));
    cmd.arg("logs")
        .arg("--purge")
        .assert()
        .success()
        .stdout(predicates::str::is_empty());

    assert_eq!(read_log(&api_stdout), "");
    assert_eq!(read_log(&api_stderr), "");
    assert_eq!(read_log(&supervisor), "");
    assert_eq!(read_log(&spawn_log), "spawn output\n");
}