Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
REVIEW: using a more business logic approach
✗ CI failure Fintan Halpenny committed 10 months ago
commit f3e08aca570472cc03104b705b9e542164ddac30
parent dbd924f479e19b61212bb7956df11a62cac515f2
1 failed 1 pending (2 total) View logs
2 files changed +185 -58
modified crates/radicle-cli/src/commands/node/control.rs
@@ -1,9 +1,11 @@
+
mod logs;
+
use logs::{LogRotatorFileSystem, Rotated};
+

use std::collections::HashMap;
use std::ffi::OsString;
-
use std::fs::{File, OpenOptions};
+
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
-
use std::path::PathBuf;
-
use std::{fs, io, path::Path, process, thread, time};
+
use std::{path::Path, process, thread, time};

use anyhow::{anyhow, Context};
use localtime::LocalTime;
@@ -18,10 +20,6 @@ use crate::terminal::Element as _;

/// How long to wait for the node to start before returning an error.
pub const NODE_START_TIMEOUT: time::Duration = time::Duration::from_secs(6);
-
/// Node log file name.
-
pub const NODE_LOG: &str = "node.log";
-
/// Node log old file name, after rotation.
-
pub const NODE_LOG_OLD_PREFIX: &str = "node.log.";

pub fn start(
    node: Node,
@@ -57,7 +55,10 @@ pub fn start(
        options.push(OsString::from("--force"));
    }

-
    let (log_path, log_file) = log_rotate(profile)?;
+
    let Rotated {
+
        path: log_path,
+
        log: log_file,
+
    } = LogRotatorFileSystem::from_profile(profile).rotate()?;

    if daemon {
        let child = process::Command::new(cmd)
@@ -125,7 +126,8 @@ pub fn stop(node: Node, profile: &Profile) {
        spinner.message("Node stopped");
        spinner.finish();
    }
-
    let _ = log_remove(profile);
+
    let rotator = LogRotatorFileSystem::from_profile(profile);
+
    rotator.remove().ok();
}

pub fn debug(node: &mut Node) -> anyhow::Result<()> {
@@ -401,55 +403,6 @@ pub fn config(node: &Node) -> anyhow::Result<()> {
    Ok(())
}

-
fn log_path(profile: &Profile) -> PathBuf {
-
    profile.home.node().join(NODE_LOG)
-
}
-

-
fn log_rotate(profile: &Profile) -> io::Result<(PathBuf, File)> {
-
    let _ = log_remove(profile);
-

-
    let base = profile.home.node();
-

-
    let next = base
-
        .read_dir()
-
        .ok()
-
        .map(|read_dir| {
-
            read_dir
-
                .filter_map(Result::ok)
-
                .filter_map(|dir_entry| dir_entry.file_name().into_string().ok())
-
                .filter_map(|filename| {
-
                    filename
-
                        .strip_prefix(NODE_LOG_OLD_PREFIX)
-
                        .and_then(|suffix| suffix.parse::<u16>().ok())
-
                })
-
                .max()
-
                .unwrap_or_default()
-
                + 1
-
        })
-
        .unwrap_or(1u16);
-

-
    let path = base.join(NODE_LOG_OLD_PREFIX.to_owned() + &next.to_string());
-

-
    let log = OpenOptions::new()
-
        .write(true)
-
        .create_new(true)
-
        .open(&path)?;
-

-
    fs::hard_link(&path, log_path(profile))?;
-

-
    Ok((path, log))
-
}
-

-
fn log_remove(profile: &Profile) -> io::Result<()> {
-
    let path = log_path(profile);
-

-
    if path.exists() {
-
        fs::remove_file(path)
-
    } else {
-
        Ok(())
-
    }
-
}
-

fn state_label() -> term::Paint<String> {
    term::Paint::from("?".to_string())
}
added crates/radicle-cli/src/commands/node/logs.rs
@@ -0,0 +1,174 @@
+
use std::collections::BinaryHeap;
+
use std::fs::{File, OpenOptions};
+
use std::path::PathBuf;
+
use std::{fs, io};
+

+
use radicle::Profile;
+

+
/// [`LogRotator`] manages the rotation of the log files when the Radicle node
+
/// is run in the background, without being managed by a service manager.
+
pub struct LogRotator {
+
    /// The base path where the logs should live.
+
    base: PathBuf,
+
    /// All existing logs, identified by a suffix.
+
    existing_logs: BinaryHeap<u16>,
+
}
+

+
impl LogRotator {
+
    /// Node log file name.
+
    pub const NODE_LOG: &str = "node.log";
+
    /// Node log old file name, after rotation.
+
    pub const NODE_LOG_OLD_PREFIX: &str = "node.log.";
+

+
    /// Construct a new [`LogRotator`] with the give `base` path.
+
    pub fn new(base: PathBuf) -> Self {
+
        Self {
+
            base,
+
            existing_logs: BinaryHeap::new(),
+
        }
+
    }
+

+
    /// Add a set of existing suffixes to known logs.
+
    pub fn found_logs(&mut self, suffixes: impl Iterator<Item = u16>) {
+
        self.existing_logs.extend(suffixes);
+
    }
+

+
    /// Specify that the [`LogRotator::current`] log should be removed.
+
    ///
+
    /// Returns `None` if the file does not exist.
+
    pub fn remove_current(&self) -> Option<Remove> {
+
        let current = self.current();
+
        current.exists().then_some(Remove { current })
+
    }
+

+
    /// Specify that the logs should be rotated.
+
    pub fn rotate(&self) -> Rotate {
+
        let next = self.next_log();
+
        let remove = self.remove_current();
+
        Rotate {
+
            next,
+
            link: self.current(),
+
            remove,
+
        }
+
    }
+

+
    /// The current log file that should be logged to.
+
    pub fn current(&self) -> PathBuf {
+
        self.base.join(Self::NODE_LOG)
+
    }
+

+
    fn next_log(&self) -> PathBuf {
+
        let suffix = self
+
            .existing_logs
+
            .peek()
+
            .copied()
+
            .unwrap_or(0)
+
            .saturating_add(1);
+
        self.base
+
            .join(Self::NODE_LOG_OLD_PREFIX.to_owned() + suffix.to_string().as_str())
+
    }
+
}
+

+
/// A [`LogRotator`] that implements the removal and rotation by accessing the
+
/// filesystem.
+
pub struct LogRotatorFileSystem {
+
    rotator: LogRotator,
+
}
+

+
impl LogRotatorFileSystem {
+
    /// Create a new [`LogRotatorFileSystem`] from a [`Profile`].
+
    ///
+
    /// The [`LogRotator`]'s base path will be the node path.
+
    pub fn from_profile(profile: &Profile) -> Self {
+
        Self {
+
            rotator: LogRotator::new(profile.home.node()),
+
        }
+
    }
+

+
    /// Rotate the log files, returning [`Rotated`].
+
    pub fn rotate(mut self) -> io::Result<Rotated> {
+
        self.rotator.found_logs(self.existing_logs().into_iter());
+
        self.rotator.rotate().execute()
+
    }
+

+
    /// Remove the current log file, returning `true` if the file existed and
+
    /// was removed.
+
    pub fn remove(self) -> io::Result<bool> {
+
        self.rotator
+
            .remove_current()
+
            .map(|remove| remove.execute())
+
            .transpose()
+
            .map(|res| res.is_some())
+
    }
+

+
    fn parse_suffix(filename: String) -> Option<u16> {
+
        filename
+
            .strip_prefix(LogRotator::NODE_LOG_OLD_PREFIX)
+
            .and_then(|suffix| suffix.parse::<u16>().ok())
+
    }
+

+
    fn existing_logs(&self) -> BinaryHeap<u16> {
+
        self.rotator
+
            .base
+
            .read_dir()
+
            .ok()
+
            .map(|dir| {
+
                dir.filter_map(Result::ok)
+
                    .filter_map(|entry| entry.file_name().into_string().ok())
+
                    .filter_map(Self::parse_suffix)
+
                    .collect()
+
            })
+
            .unwrap_or_default()
+
    }
+
}
+

+
/// Remove the path identified by [`Remove::current`].
+
pub struct Remove {
+
    current: PathBuf,
+
}
+

+
impl Remove {
+
    /// Use [`fs::remove_file`] to remove the file.
+
    pub fn execute(self) -> io::Result<()> {
+
        fs::remove_file(self.current)
+
    }
+
}
+

+
/// Rotate the logs to the next log.
+
pub struct Rotate {
+
    /// The next log that needs to be created
+
    next: PathBuf,
+
    /// The path to create a hard link to.
+
    link: PathBuf,
+
    /// If the current log exists, then we need to remove it
+
    remove: Option<Remove>,
+
}
+

+
impl Rotate {
+
    /// Remove the existing file, if it exists. Then create the next log, and
+
    /// create a hard link to it.
+
    pub fn execute(self) -> io::Result<Rotated> {
+
        if let Some(to_remove) = self.remove {
+
            if let Err(err) = to_remove.execute() {
+
                log::warn!(target: "cli", "Failed to remove current log file: {err}");
+
            }
+
        }
+
        let log = OpenOptions::new()
+
            .write(true)
+
            .create_new(true)
+
            .open(&self.next)?;
+
        fs::hard_link(&self.next, &self.link)?;
+
        Ok(Rotated {
+
            path: self.next,
+
            log,
+
        })
+
    }
+
}
+

+
/// The result of rotating the logs.
+
pub struct Rotated {
+
    /// The [`PathBuf`] to the new log file.
+
    pub path: PathBuf,
+
    /// The [`File`] handle for the log file.
+
    pub log: File,
+
}