use std::io::Write as _;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::paths::routine_run_history_path;
use crate::routines::model::RunStatus;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct PersistedRun {
pub workbench: String,
pub started_at: u64,
pub finished_at: u64,
pub status: RunStatus,
pub exit_code: Option<i32>,
}
pub(crate) fn read_exit_code(workbench_path: &Path) -> Option<i32> {
std::fs::read_to_string(workbench_path.join("exit_code"))
.ok()
.and_then(|text| text.trim().parse::<i32>().ok())
}
const RUN_HISTORY_MAX_BYTES: u64 = 1024 * 1024;
fn rotate_run_history_if_oversized(path: &Path) {
let Ok(metadata) = std::fs::metadata(path) else {
return;
};
if metadata.len() <= RUN_HISTORY_MAX_BYTES {
return;
}
let rotated_path = path.with_extension("log.1");
let current = std::fs::read(path).unwrap_or_default();
let mut combined = std::fs::read(&rotated_path).unwrap_or_default();
combined.extend_from_slice(¤t);
if std::fs::write(&rotated_path, combined).is_ok() {
let _ = std::fs::remove_file(path);
}
}
pub(crate) fn append_persisted_run(id: &str, run: &PersistedRun) {
let path = routine_run_history_path(id);
rotate_run_history_if_oversized(&path);
let result = serde_json::to_string(run)
.map_err(std::io::Error::other)
.and_then(|line| {
crate::utils::fs_perms::parent_or_err(&path, "run history")
.and_then(crate::utils::fs_perms::create_private_dir_all)
.and_then(|()| {
open_history_append(&path).and_then(|mut file| writeln!(file, "{line}"))
})
});
if let Err(err) = result {
log::warn!("run history: failed to append for routine {id:?}: {err}");
}
}
fn open_history_append(path: &Path) -> std::io::Result<std::fs::File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)
}
#[cfg(not(unix))]
{
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
}
}
pub(crate) fn read_persisted_runs(id: &str) -> Vec<PersistedRun> {
let path = routine_run_history_path(id);
let rotated_path = path.with_extension("log.1");
[rotated_path, path]
.iter()
.filter_map(|file_path| std::fs::read_to_string(file_path).ok())
.flat_map(|text| {
text.lines()
.filter_map(|line| serde_json::from_str::<PersistedRun>(line).ok())
.collect::<Vec<_>>()
})
.collect()
}
pub(crate) fn has_persisted_run(id: &str, workbench: &str) -> bool {
read_persisted_runs(id)
.iter()
.any(|run| run.workbench == workbench)
}
#[cfg(test)]
#[path = "run_history_tests.rs"]
mod run_history_tests;