urma-node 0.7.0

Optional URMA archive node: Litecoin follower, record archive, libp2p overlay and loopback service
Documentation
use crate::{
    install::{
        Error, Init, Registration, Runner, Status, System,
        account::Account,
        ownership::{LogWriter, hand_over, installer, log_directory},
        system::text,
    },
    platform::{Paths, ensure_defaults},
};
use std::{
    fs::{self, Permissions},
    io::Write,
    os::unix::fs::{MetadataExt, PermissionsExt, symlink},
    path::{Path, PathBuf},
    thread,
    time::Duration,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdapterFile {
    pub path: PathBuf,
    pub content: String,
    pub mode: u32,
}

pub struct Adapter;

impl Adapter {
    pub const BINARY: &'static str = "/usr/bin/urma-node";
    pub const LOG_DIR: &'static str = "/var/log/urma-node";
    const UNIT: &'static str = "urma-node.service";
    const SYSTEMD_UNIT: &'static str = "/etc/systemd/system/urma-node.service";
    const OPENRC_SCRIPT: &'static str = "/etc/init.d/urma-node";
    const OPENRC_ENABLED: &'static str = "/etc/runlevels/default/urma-node";
    const RUNIT_DIR: &'static str = "/etc/sv/urma-node";
    const RUNIT_RUN: &'static str = "/etc/sv/urma-node/run";
    const RUNIT_LOG_RUN: &'static str = "/etc/sv/urma-node/log/run";
    const RUNIT_LOG_SERVICE: &'static str = "/etc/sv/urma-node/log";
    const RUNIT_SUPERVISED: &'static str = "/etc/sv/urma-node/supervise/ok";
    const SUPERVISED_STATES: [&'static str; 3] = ["run:", "down:", "finish:"];
    const RUNSV_POLLS: u32 = 40;
    const RUNSV_POLL: Duration = Duration::from_millis(250);
    const RUNIT_SERVICE_DIRS: [&'static str; 3] = [
        "/var/service",
        "/etc/runit/runsvdir/default",
        "/run/runit/service",
    ];
    const SYSTEMD: &'static str = include_str!("templates/urma-node.service");
    const OPENRC: &'static str = include_str!("templates/urma-node.openrc");
    const RUNIT: &'static str = include_str!("templates/runit-run");
    const RUNIT_LOG: &'static str = include_str!("templates/runit-log-run");
    const MARKERS: [(Init, &'static str); 3] = [
        (Init::Systemd, Self::SYSTEMD_UNIT),
        (Init::Openrc, Self::OPENRC_SCRIPT),
        (Init::Runit, Self::RUNIT_RUN),
    ];
    const WRITABLE_BY_OTHERS: u32 = 0o022;
}

fn foreign() -> Error {
    Error::Failed("the Windows service manager exists only on Windows".into())
}

fn remove_present(path: &Path) -> Result<(), Error> {
    if path.is_symlink() || path.exists() {
        fs::remove_file(path)?;
    }
    Ok(())
}

fn file(path: &str, content: String, mode: u32) -> AdapterFile {
    AdapterFile {
        path: PathBuf::from(path),
        content,
        mode,
    }
}

impl<R: Runner> System<R> {
    pub fn render(&self, init: Init) -> Result<Vec<AdapterFile>, Error> {
        let state = Paths::linux_under(Path::new("/")).state_dir;
        let binary = text(&self.host.binary)?;
        let state = text(&state)?;
        let fill = |template: &str| {
            template
                .replace("@BIN@", binary)
                .replace("@STATE@", state)
                .replace("@LOG@", Adapter::LOG_DIR)
        };
        Ok(match init {
            Init::Systemd => vec![file(Adapter::SYSTEMD_UNIT, fill(Adapter::SYSTEMD), 0o644)],
            Init::Openrc => vec![file(Adapter::OPENRC_SCRIPT, fill(Adapter::OPENRC), 0o755)],
            Init::Runit => vec![
                file(Adapter::RUNIT_RUN, fill(Adapter::RUNIT), 0o755),
                file(Adapter::RUNIT_LOG_RUN, fill(Adapter::RUNIT_LOG), 0o755),
            ],
            Init::Windows => return Err(foreign()),
        })
    }

    pub fn trusted_binary(&self) -> Result<(), Error> {
        let expected = Path::new(Adapter::BINARY);
        let refuse = || {
            Error::Failed(format!(
                "refusing to register {}: the service binary must be {}, a regular file owned by \
                 root and writable by nobody else; install it there first",
                self.host.binary.display(),
                Adapter::BINARY
            ))
        };
        if self.host.binary != expected {
            return Err(refuse());
        }
        let meta = fs::symlink_metadata(self.at(expected))?;
        let sound = meta.is_file()
            && meta.uid() == installer()?
            && meta.mode() & Adapter::WRITABLE_BY_OTHERS == 0;
        match sound {
            true => Ok(()),
            false => Err(refuse()),
        }
    }

    fn write_adapter(&self, init: Init) -> Result<(), Error> {
        for adapter in self.render(init)? {
            let path = self.at(&adapter.path);
            let Some(parent) = path.parent() else {
                return Err(Error::Failed(format!("{} has no parent", path.display())));
            };
            fs::create_dir_all(parent)?;
            fs::write(&path, adapter.content)?;
            fs::set_permissions(&path, Permissions::from_mode(adapter.mode))?;
        }
        Ok(())
    }

    pub fn registration(&self) -> Result<Registration, Error> {
        for (init, marker) in Adapter::MARKERS {
            if self.at(Path::new(marker)).is_file() {
                return Ok(Registration::Registered(init));
            }
        }
        Ok(Registration::Absent)
    }

    pub fn install(&mut self, init: Init, out: &mut impl Write) -> Result<(), Error> {
        self.render(init)?;
        self.trusted_binary()?;
        let ids = self.ensure_account()?;
        let paths = self.paths();
        let created = ensure_defaults(&paths)?;
        hand_over(&created, &paths, ids)?;
        let log = self.at(Path::new(Adapter::LOG_DIR));
        match init {
            Init::Openrc => log_directory(&log, LogWriter::Root, ids)?,
            Init::Runit => log_directory(&log, LogWriter::Service, ids)?,
            Init::Systemd | Init::Windows => {}
        }
        let registration = self.registration()?;
        self.write_adapter(init)?;
        match registration {
            Registration::Registered(..) => self.restart(init)?,
            Registration::Absent => self.start(init)?,
        }
        self.join_group(out)
    }

    pub fn update(&mut self, init: Init) -> Result<(), Error> {
        self.trusted_binary()?;
        self.write_adapter(init)?;
        self.restart(init)
    }

    pub fn uninstall(&mut self, purge: bool) -> Result<(), Error> {
        match self.registration()? {
            Registration::Registered(init) => self.remove(init)?,
            Registration::Absent => {}
        }
        if purge {
            let paths = self.paths();
            let log = self.at(Path::new(Adapter::LOG_DIR));
            for dir in [&paths.settings_dir, &paths.state_dir, &log] {
                if dir.is_dir() && !dir.is_symlink() {
                    fs::remove_dir_all(dir)?;
                }
            }
            self.remove_account()?;
        }
        Ok(())
    }

    fn start(&mut self, init: Init) -> Result<(), Error> {
        match init {
            Init::Systemd => {
                self.exec("systemctl", &["daemon-reload"])?;
                self.exec("systemctl", &["enable", "--now", Adapter::UNIT])?;
            }
            Init::Openrc => {
                self.exec("rc-update", &["add", Account::NAME, "default"])?;
                self.exec("rc-service", &[Account::NAME, "start"])?;
            }
            Init::Runit => self.link_runit()?,
            Init::Windows => return Err(foreign()),
        }
        Ok(())
    }

    fn restart(&mut self, init: Init) -> Result<(), Error> {
        match init {
            Init::Systemd => {
                self.exec("systemctl", &["daemon-reload"])?;
                self.exec("systemctl", &["restart", Adapter::UNIT])?;
            }
            Init::Openrc => {
                self.exec("rc-service", &[Account::NAME, "restart"])?;
            }
            Init::Runit => match self.runit_links()?.is_empty() {
                true => self.link_runit()?,
                false => {
                    self.exec("sv", &["restart", Adapter::RUNIT_DIR])?;
                }
            },
            Init::Windows => return Err(foreign()),
        }
        Ok(())
    }

    fn remove(&mut self, init: Init) -> Result<(), Error> {
        match init {
            Init::Systemd => {
                self.exec("systemctl", &["disable", "--now", Adapter::UNIT])?;
                remove_present(&self.at(Path::new(Adapter::SYSTEMD_UNIT)))?;
                self.exec("systemctl", &["daemon-reload"])?;
            }
            Init::Openrc => {
                self.query("rc-service", &[Account::NAME, "stop"])?;
                if self.at(Path::new(Adapter::OPENRC_ENABLED)).is_symlink() {
                    self.exec("rc-update", &["del", Account::NAME, "default"])?;
                }
                remove_present(&self.at(Path::new(Adapter::OPENRC_SCRIPT)))?;
            }
            Init::Runit => {
                let supervised = self.at(Path::new(Adapter::RUNIT_SUPERVISED)).exists();
                if supervised {
                    self.exec("sv", &["-w", "30", "down", Adapter::RUNIT_DIR])?;
                }
                for link in self.runit_links()? {
                    remove_present(&link)?;
                }
                if supervised {
                    self.query("sv", &["exit", Adapter::RUNIT_LOG_SERVICE])?;
                    self.query("sv", &["exit", Adapter::RUNIT_DIR])?;
                    self.await_runsv_exit()?;
                }
                let dir = self.at(Path::new(Adapter::RUNIT_DIR));
                if dir.is_dir() && !dir.is_symlink() {
                    fs::remove_dir_all(dir)?;
                }
            }
            Init::Windows => return Err(foreign()),
        }
        Ok(())
    }

    fn await_runsv_exit(&mut self) -> Result<(), Error> {
        for _ in 0..Adapter::RUNSV_POLLS {
            let status = self.query("sv", &["status", Adapter::RUNIT_DIR])?;
            let supervised = Adapter::SUPERVISED_STATES
                .iter()
                .any(|state| status.stdout.starts_with(state));
            if !supervised {
                return Ok(());
            }
            thread::sleep(Adapter::RUNSV_POLL);
        }
        Err(Error::Failed(format!(
            "runsv for {} did not exit; remove it once runsvdir has dropped the service",
            Adapter::RUNIT_DIR
        )))
    }

    fn runit_links(&self) -> Result<Vec<PathBuf>, Error> {
        let mut parents: Vec<PathBuf> = Vec::new();
        for dir in Adapter::RUNIT_SERVICE_DIRS {
            let active = self.at(Path::new(dir));
            if !active.is_dir() {
                continue;
            }
            let parent = fs::canonicalize(&active)?;
            if !parents.contains(&parent) {
                parents.push(parent);
            }
        }
        Ok(parents
            .into_iter()
            .map(|parent| parent.join(Account::NAME))
            .filter(|link| link.is_symlink())
            .collect())
    }

    fn link_runit(&self) -> Result<(), Error> {
        for dir in Adapter::RUNIT_SERVICE_DIRS {
            let active = self.at(Path::new(dir));
            if active.is_dir() {
                symlink(Adapter::RUNIT_DIR, active.join(Account::NAME))?;
                return Ok(());
            }
        }
        Err(Error::Failed(format!(
            "no active runit service directory ({})",
            Adapter::RUNIT_SERVICE_DIRS.join(", ")
        )))
    }

    pub fn state(&mut self, init: Init) -> Result<Status, Error> {
        let (enabled, running) = match init {
            Init::Systemd => {
                let enabled = self.query("systemctl", &["is-enabled", Adapter::UNIT])?;
                let active = self.query("systemctl", &["is-active", Adapter::UNIT])?;
                (
                    enabled.stdout.trim() == "enabled",
                    active.stdout.trim() == "active",
                )
            }
            Init::Openrc => {
                let enabled = self.at(Path::new(Adapter::OPENRC_ENABLED)).is_symlink();
                let status = self.query("rc-service", &[Account::NAME, "status"])?;
                (enabled, status.success)
            }
            Init::Runit => {
                let status = self.query("sv", &["status", Adapter::RUNIT_DIR])?;
                (
                    !self.runit_links()?.is_empty(),
                    status.stdout.starts_with("run:"),
                )
            }
            Init::Windows => return Err(foreign()),
        };
        Ok(Status {
            init,
            registered: true,
            enabled,
            running,
            listening: false,
        })
    }
}