sandbox-agent 0.4.0-rc.1

Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;

use chrono::{Datelike, Duration, TimeDelta, TimeZone, Utc};

pub struct ServerLogs {
    path: PathBuf,
    retention: Duration,

    last_rotation: chrono::DateTime<Utc>,
    next_rotation: chrono::DateTime<Utc>,
}

impl ServerLogs {
    pub fn new(path: PathBuf, retention: std::time::Duration) -> Self {
        Self {
            path,
            retention: chrono::Duration::from_std(retention).expect("invalid retention duration"),
            last_rotation: Utc.timestamp_opt(0, 0).unwrap(),
            next_rotation: Utc.timestamp_opt(0, 0).unwrap(),
        }
    }

    pub fn start_sync(mut self) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
        std::fs::create_dir_all(&self.path)?;
        self.rotate_sync()?;

        Ok(std::thread::spawn(|| self.run_sync()))
    }

    fn run_sync(mut self) {
        loop {
            let now = Utc::now();

            if self.next_rotation - now > Duration::seconds(5) {
                std::thread::sleep(
                    (self.next_rotation - now - Duration::seconds(5))
                        .max(TimeDelta::default())
                        .to_std()
                        .expect("bad duration"),
                );
            } else if now.ordinal() != self.last_rotation.ordinal() {
                if let Err(err) = self.rotate_sync() {
                    tracing::error!(?err, "failed logs rotation");
                }
            } else {
                std::thread::sleep(std::time::Duration::from_millis(250));
            }
        }
    }

    fn rotate_sync(&mut self) -> Result<(), std::io::Error> {
        self.last_rotation = Utc::now();
        self.next_rotation = Utc.from_utc_datetime(
            &(self
                .last_rotation
                .date_naive()
                .and_hms_opt(0, 0, 0)
                .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "invalid date"))?
                + Duration::days(1)),
        );

        let file_name = format!("log-{}", self.last_rotation.format("%m-%d-%y"));
        let path = self.path.join(file_name);

        let log_file = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .append(true)
            .open(&path)?;
        let log_fd = log_file.as_raw_fd();

        unsafe {
            libc::dup2(log_fd, libc::STDOUT_FILENO);
            libc::dup2(log_fd, libc::STDERR_FILENO);
        }

        self.prune_sync()
    }

    fn prune_sync(&self) -> Result<(), std::io::Error> {
        let mut entries = std::fs::read_dir(&self.path)?;
        let mut pruned = 0;

        while let Some(entry) = entries.next() {
            let entry = entry?;
            let metadata = entry.metadata()?;
            let modified = chrono::DateTime::<Utc>::from(metadata.modified()?);
            if modified < Utc::now() - self.retention {
                pruned += 1;
                let _ = std::fs::remove_file(entry.path());
            }
        }

        if pruned != 0 {
            tracing::debug!("pruned {pruned} log files");
        }

        Ok(())
    }
}