Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
feat: add an admin log to msg::helper
Merged liw opened 1 year ago

Also make runcmd and get_sources log what they do, to the admin log.

This originally comes from radicle-ci-ambient, the Radicle CI adapter for Ambient.

Signed-off-by: Lars Wirzenius liw@liw.fi

1 file changed +154 -10 43306e15 e57c1e1e
modified src/msg.rs
@@ -1223,11 +1223,19 @@ pub mod trigger_from_ci_event_tests {

/// Helper functions for writing adapters.
pub mod helper {
-
    use std::{path::Path, process::Command};
+

+
    use std::{
+
        fs::{File, OpenOptions},
+
        io::Write,
+
        path::{Path, PathBuf},
+
        process::Command,
+
    };

    use nonempty::{nonempty, NonEmpty};
    use radicle::prelude::{Profile, RepoId};

+
    use time::{macros::format_description, OffsetDateTime};
+

    use super::{MessageError, Oid, Request, Response, RunId, RunResult};

    /// Exit code to indicate we didn't get one from the process.
@@ -1278,38 +1286,79 @@ pub mod helper {
    }

    /// Get sources from the local node.
-
    pub fn get_sources(repoid: RepoId, commit: Oid, src: &Path) -> Result<(), MessageHelperError> {
+
    pub fn get_sources(
+
        adminlog: &mut AdminLog,
+
        dry_run: bool,
+
        repoid: RepoId,
+
        commit: Oid,
+
        src: &Path,
+
    ) -> Result<(), MessageHelperError> {
        let profile = Profile::load().map_err(MessageHelperError::Profile)?;
        let storage = profile.storage.path();
        let repo_path = storage.join(repoid.canonical());

-
        git_clone(&repo_path, src)?;
-
        git_checkout(commit, src)?;
+
        git_clone(adminlog, dry_run, &repo_path, src)?;
+
        git_checkout(adminlog, dry_run, commit, src)?;

        Ok(())
    }

    /// Run `git clone` for the repository.
-
    fn git_clone(repo_path: &Path, src: &Path) -> Result<(), MessageHelperError> {
+
    fn git_clone(
+
        adminlog: &mut AdminLog,
+
        dry_run: bool,
+
        repo_path: &Path,
+
        src: &Path,
+
    ) -> Result<(), MessageHelperError> {
        let repo_path = repo_path.to_string_lossy();
        let src = src.to_string_lossy();
-
        runcmd(&nonempty!["git", "clone", &repo_path, &src], Path::new("."))?;
+
        runcmd(
+
            adminlog,
+
            dry_run,
+
            &nonempty!["git", "clone", &repo_path, &src],
+
            Path::new("."),
+
        )?;
        Ok(())
    }

    // Check out the requested commit.
-
    fn git_checkout(commit: Oid, src: &Path) -> Result<(), MessageHelperError> {
+
    fn git_checkout(
+
        adminlog: &mut AdminLog,
+
        dry_run: bool,
+
        commit: Oid,
+
        src: &Path,
+
    ) -> Result<(), MessageHelperError> {
        runcmd(
+
            adminlog,
+
            dry_run,
            &nonempty!["git", "config", "advice.detachedHead", "false"],
            src,
        )?;
        let commit = commit.to_string();
-
        runcmd(&nonempty!["git", "checkout", &commit], src)?;
+
        runcmd(
+
            adminlog,
+
            dry_run,
+
            &nonempty!["git", "checkout", &commit],
+
            src,
+
        )?;
        Ok(())
    }

    /// Run a program.
-
    pub fn runcmd(argv: &NonEmpty<&str>, cwd: &Path) -> Result<i32, MessageHelperError> {
+
    pub fn runcmd(
+
        adminlog: &mut AdminLog,
+
        dry_run: bool,
+
        argv: &NonEmpty<&str>,
+
        cwd: &Path,
+
    ) -> Result<(i32, Vec<u8>), MessageHelperError> {
+
        if dry_run {
+
            adminlog
+
                .writeln(&format!("runcmd: pretend to run: argv={argv:?}"))
+
                .map_err(MessageHelperError::AdminLog)?;
+
            return Ok((0, vec![]));
+
        }
+

+
        adminlog.writeln(&format!("runcmd: argv={argv:?}"))?;
        let output = Command::new("bash")
            .arg("-c")
            .arg(r#""$@" 2>&1"#)
@@ -1320,7 +1369,98 @@ pub mod helper {
            .map_err(|err| MessageHelperError::Command("bash", err))?;

        let exit = output.status.code().unwrap_or(NO_EXIT);
-
        Ok(exit)
+

+
        if exit != 0 {
+
            adminlog.writeln(&format!("runcmd: exit={exit}"))?;
+
            indented(adminlog, "stdout", &output.stdout);
+
            indented(adminlog, "stderr", &output.stderr);
+
        }
+

+
        Ok((exit, output.stdout))
+
    }
+

+
    /// Log a string with every line indented.
+
    pub fn indented(adminlog: &mut AdminLog, msg: &str, bytes: &[u8]) {
+
        if !bytes.is_empty() {
+
            adminlog.writeln(&format!("{msg}:")).ok();
+
            let text = String::from_utf8_lossy(bytes);
+
            for line in text.lines() {
+
                adminlog.writeln(&format!("    {line}")).ok();
+
            }
+
        }
+
    }
+

+
    /// A log for the administrator, whose duty it is to keep the
+
    /// software running.
+
    #[derive(Debug, Default)]
+
    pub struct AdminLog {
+
        filename: Option<PathBuf>,
+
        file: Option<File>,
+
    }
+

+
    impl AdminLog {
+
        /// Create an admin log that doesn't write to a file, but to
+
        /// stderr.
+
        pub fn null() -> Self {
+
            Self::default()
+
        }
+

+
        /// Create an admin log that writes to a named file.
+
        pub fn open(filename: &Path) -> Result<Self, LogError> {
+
            let file = OpenOptions::new()
+
                .append(true)
+
                .create(true)
+
                .open(filename)
+
                .map_err(|e| LogError::OpenLogFile(filename.into(), e))?;
+
            Ok(Self {
+
                filename: Some(filename.into()),
+
                file: Some(file),
+
            })
+
        }
+

+
        /// Write a line to the admin log.
+
        pub fn writeln(&mut self, text: &str) -> Result<(), LogError> {
+
            self.write("[")?;
+
            self.write(&now()?)?;
+
            self.write("] ")?;
+
            self.write(text)?;
+
            self.write("\n")?;
+
            Ok(())
+
        }
+

+
        fn write(&mut self, msg: &str) -> Result<(), LogError> {
+
            if let Some(file) = &mut self.file {
+
                #[allow(clippy::unwrap_used)] // we know it's OK
+
                file.write_all(msg.as_bytes())
+
                    .map_err(|e| LogError::WriteLogFile(self.filename.clone().unwrap(), e))?;
+
            } else {
+
                eprintln!("ambient adapter: {msg}");
+
            }
+
            Ok(())
+
        }
+
    }
+

+
    fn now() -> Result<String, LogError> {
+
        let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
+
        OffsetDateTime::now_utc()
+
            .format(fmt)
+
            .map_err(LogError::TimeFormat)
+
    }
+

+
    /// Possible errors from using the admin log.
+
    #[derive(Debug, thiserror::Error)]
+
    pub enum LogError {
+
        /// Can't open named file.
+
        #[error("failed to open log file {0}")]
+
        OpenLogFile(PathBuf, #[source] std::io::Error),
+

+
        /// Can't write to file.
+
        #[error("failed to write to log file {0}")]
+
        WriteLogFile(PathBuf, #[source] std::io::Error),
+

+
        /// Can' format time stamp.
+
        #[error("failed to format time stamp")]
+
        TimeFormat(#[source] time::error::Format),
    }

    /// Possible errors from this module.
@@ -1341,5 +1481,9 @@ pub mod helper {
        /// Can't run command and capture its output.
        #[error("failed to run command {0}")]
        Command(&'static str, #[source] std::io::Error),
+

+
        /// Admin log error.
+
        #[error(transparent)]
+
        AdminLog(#[from] LogError),
    }
}