sandbox-agent 0.4.0-rc.1

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

use chrono::{Datelike, Duration, TimeDelta, TimeZone, Utc};
use windows::{
    core::PCSTR,
    Win32::{
        Foundation::{HANDLE, INVALID_HANDLE_VALUE},
        Storage::FileSystem::{
            CreateFileA, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_WRITE, FILE_SHARE_READ, OPEN_ALWAYS,
        },
        System::Console::{SetStdHandle, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE},
    },
};

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 path_str = path
            .to_str()
            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "invalid path"))?;
        let path_cstr = std::ffi::CString::new(path_str)
            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;

        unsafe {
            let file_handle = CreateFileA(
                PCSTR(path_cstr.as_ptr() as *const u8),
                FILE_GENERIC_WRITE.0,
                FILE_SHARE_READ,
                None,
                OPEN_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                HANDLE::default(),
            )
            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;

            if file_handle == INVALID_HANDLE_VALUE {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::Other,
                    "failed to create log file",
                ));
            }

            SetStdHandle(STD_OUTPUT_HANDLE, file_handle)
                .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;
            SetStdHandle(STD_ERROR_HANDLE, file_handle)
                .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;
        }

        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(())
    }
}