#[macro_use] extern crate rocket; use std::{ fs, fs::File, io, path::{Path, PathBuf}, process::{Command, Output}, time::{Duration, Instant}, }; use glog::Flags; use log::{error, info}; use rocket::{fairing::AdHoc, fs::FileServer, response::Responder, State}; use rocket_dyn_templates::{context, Template}; use serde::Deserialize; use thiserror::Error; #[derive(Error, Debug, Responder)] pub enum SyncError { #[error("IO error")] IoError(#[from] io::Error), } // For testing #[get("/-/reload/")] fn get_reload(config: &State, repo: &str) -> Result { reload(config, repo) } #[post("/-/reload/")] fn post_reload(config: &State, repo: &str) -> Result { reload(config, repo) } fn indent_output(bytes: &[u8]) -> String { let s = String::from_utf8_lossy(bytes); let s = if s.is_empty() { "" } else { &s }; s.split('\n') .map(|l| format!("\t{l}")) .collect::>() .join("\n") } fn format_task_status(ts: &TaskStatus) -> String { format!( "Status {} ({}) {}s:\nStdout:\n{}\nStderr:\n{}", ts.command, ts.output.status, ts.duration.as_secs_f32(), indent_output(&ts.output.stdout), indent_output(&ts.output.stderr) ) } struct TaskStatus { command: String, output: Output, duration: Duration, } fn logging_run(cmd: &mut Command) -> Result { let start = Instant::now(); let o = cmd.output()?; let ts = TaskStatus { output: o, command: format!("{:?}", cmd), duration: start.elapsed(), }; info!("{}", format_task_status(&ts)); Ok(ts) } fn git_checkout(build_path: &Path, rev: &str) -> Result { logging_run( Command::new("git") .current_dir(&build_path) .arg("checkout") .arg("-q") .arg(rev), ) } fn bench_at_commit( commit: &str, build_path: &Path, target_path: &Path, ) -> Result, SyncError> { let mut output = Vec::new(); output.push(git_checkout(build_path, commit)?); // Run `cargo aoc bench` output.push(logging_run( Command::new("cargo") .env("CARGO_TARGET_DIR", target_path) .current_dir(&build_path) .arg("aoc") .arg("bench"), )?); output.push(git_checkout(build_path, "HEAD")?); Ok(output) } fn reload(config: &State, repo: &str) -> Result { info!("Need to reload '{}': {:?}", repo, config); let source_path = config.source_root.join(repo); let build_path = config.build_root.join("git").join(repo); let target_path = config.build_root.join("target").join(repo); let commits_root = config.build_root.join("commits"); let www_root = config.www_root.join(repo); dbg!( &source_path, &build_path, &target_path, &commits_root, &www_root ); if !commits_root.exists() { info!("Creating {}", commits_root.display()); fs::create_dir_all(&commits_root)?; } if !www_root.exists() { info!("Creating {}", www_root.display()); fs::create_dir_all(&www_root)?; } let mut output = Vec::new(); let needs_clone = !build_path.exists(); if needs_clone { output.push(logging_run( Command::new("git") .current_dir(&config.build_root) .arg("clone") .arg(&source_path) .arg(&build_path), )?); } output.push(logging_run( Command::new("git") .current_dir(&config.build_root) .arg("checkout") .arg("-f") .arg("origin"), )?); // Make sure buildable clone is up to date output.push(logging_run( Command::new("git").current_dir(&build_path).arg("pull"), )?); let commits = logging_run(Command::new("git").current_dir(&build_path).args([ "log", "--format=%H", "origin", ]))?; let binding = String::from_utf8_lossy(&commits.output.stdout).into_owned(); let mut unknown_commits: Vec<_> = binding .lines() .filter(|commit| !commits_root.join(commit).exists()) .collect(); unknown_commits.reverse(); output.push(commits); info!("Need to bench commits: {:?}", unknown_commits); for commit in unknown_commits { match bench_at_commit(commit, &build_path, &target_path) { Ok(outputs) => { output.extend(outputs); File::create(commits_root.join(commit))?; } Err(err) => error!("Failed to bench {}@{}: {}", repo, commit, err), } } // Copy files from `target/` to serving directory let bench_path = target_path.join("criterion"); info!("Copying {} -> {}", bench_path.display(), www_root.display()); copy_dir_all(bench_path, www_root)?; let response = output .iter() .map(|ts| format!("{}", format_task_status(ts))) .collect::>() .join("\n"); info!("{}", response); Ok(response) } // From https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { fs::create_dir_all(&dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let ty = entry.file_type()?; if ty.is_dir() { copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; } else { fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; } } Ok(()) } #[get("/")] fn index(config: &State) -> Result { let dirs: Vec<_> = fs::read_dir(&config.www_root)? .filter_map(|ent| ent.ok()) .map(|ent| ent.file_name().into_string()) .filter_map(|ent| ent.ok()) .collect(); Ok(Template::render( "index", context! { dirs }, )) } #[catch(500)] fn http500(req: &rocket::Request) -> String { // TODO(wathiede): figure out a way to retrieve the Error that got us here? format!("{:?}", req) } #[derive(Debug, Deserialize)] struct Config { source_root: PathBuf, build_root: PathBuf, www_root: PathBuf, } #[launch] fn rocket() -> _ { glog::new() .init(Flags { colorlogtostderr: true, //alsologtostderr: true, // use logtostderr to only write to stderr and not to files logtostderr: true, ..Default::default() }) .unwrap(); let config = rocket::Config::figment() .extract::() .expect("Couldn't parse config"); rocket::build() .mount("/", routes![index, get_reload, post_reload]) .mount("/results/", FileServer::from(config.www_root)) //.register("/", catchers![http500]) .attach(AdHoc::config::()) .attach(Template::fairing()) }