#[macro_use] extern crate enum_display_derive; use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; use anyhow::{Context, Result}; use bigdecimal::ToPrimitive; use cached::proc_macro::cached; use postgres_types::{FromSql, ToSql}; use serde::Serialize; use std::fmt::Display; use yaml_rust::YamlLoader; const STYLE_HTML: &'static str = r#""#; #[derive(Debug, ToSql, FromSql, Display, PartialEq)] #[postgres(name = "batchevent")] enum BatchEvent { #[postgres(name = "start")] Start, #[postgres(name = "stop")] Stop, } #[derive(Debug, ToSql, FromSql, Display)] #[postgres(name = "buildresult")] enum BuildResult { #[postgres(name = "successful")] Successful, #[postgres(name = "failed")] Failed, #[postgres(name = "skipped")] Skipped, #[postgres(name = "staged")] Staged, } #[derive(Debug, ToSql, FromSql, Display)] #[postgres(name = "buildstatus")] enum BuildStatus { #[postgres(name = "pending")] Pending, #[postgres(name = "building")] Building, #[postgres(name = "done")] Done, } #[derive(Serialize)] struct StatusResponse { ts: i64, event: String, } #[derive(Serialize)] struct CurrentResponse { updated_at: i64, pkgbase: String, status: String, reasons: String, elapsed: i32, } #[derive(Serialize)] struct LogsResponse { ts: i64, pkgbase: String, pkg_version: String, maintainer: String, elapsed: i32, result: String, cpu: i32, memory: f64, } #[cached(time = 86400, result = true)] fn get_maintainer(pkg: String) -> Result { let contents = std::fs::read_to_string(format!( "/data/archgitrepo-webhook/archlinuxcn/{}/lilac.yaml", pkg ))?; let docs = YamlLoader::load_from_str(&contents)?; let doc = &docs[0]; let maintainers_yaml = doc["maintainers"] .as_vec() .context("maintainers is empty")?; let mut maintainers: Vec<&str> = vec![]; for m in maintainers_yaml { maintainers.push(m["github"].as_str().unwrap_or("None")); } Ok(maintainers.join(", ")) } #[get("/imlonghao-api/status")] async fn status(db: web::Data) -> impl Responder { let conn = db.get().await.unwrap(); let rows = conn .query("select * from lilac.batch order by id desc limit 1", &[]) .await .unwrap(); let ts: chrono::DateTime = rows[0].get("ts"); let event: BatchEvent = rows[0].get("event"); let result: StatusResponse = StatusResponse { ts: ts.timestamp(), event: event.to_string(), }; HttpResponse::Ok().json(result) } #[get("/imlonghao-api/current")] async fn current(db: web::Data) -> impl Responder { let conn = db.get().await.unwrap(); let rows = conn .query( "select coalesce(log.elapsed, -1) as elapsed, c.updated_at, c.pkgbase, c.status, c.build_reasons::TEXT from lilac.pkgcurrent AS c left join lateral ( select elapsed from lilac.pkglog where pkgbase = c.pkgbase order by ts desc limit 1) as log on true", &[], ) .await .unwrap(); let mut result: Vec = vec![]; for row in rows { let updated_at: chrono::DateTime = row.get("updated_at"); let pkgbase: String = row.get("pkgbase"); let build_status: BuildStatus = row.get("status"); let build_reasons: String = row.get("build_reasons"); let elapsed: i32 = row.get("elapsed"); result.push(CurrentResponse { updated_at: updated_at.timestamp(), pkgbase: pkgbase, status: build_status.to_string(), reasons: build_reasons, elapsed: elapsed, }) } HttpResponse::Ok().json(result) } #[get("/imlonghao-api/logs")] async fn logs(db: web::Data) -> impl Responder { let conn = db.get().await.unwrap(); let rows = conn .query( "select ts, pkgbase, COALESCE(pkg_version, '') AS pkg_version, elapsed, result, COALESCE(case when elapsed = 0 then 0 else cputime * 100 / elapsed end, -1) AS cpu, COALESCE(round(memory / 1073741824.0, 3), -1) AS memory from ( select *, row_number() over (partition by pkgbase order by ts desc) as k from lilac.pkglog ) as w where k = 1 order by ts desc", &[], ) .await .unwrap(); let mut results: Vec = vec![]; for row in rows { let ts: chrono::DateTime = row.get("ts"); let pkgbase: String = row.get("pkgbase"); let pkg_version: String = row.get("pkg_version"); let maintainer = get_maintainer(pkgbase.clone()).unwrap_or("Unknown".to_string()); let elapsed: i32 = row.get("elapsed"); let result: BuildResult = row.get("result"); let cpu: i32 = row.get("cpu"); let memory: pg_bigdecimal::PgNumeric = row.get("memory"); let memory_bd: bigdecimal::BigDecimal = memory.n.unwrap(); results.push(LogsResponse { ts: ts.timestamp(), pkgbase: pkgbase, pkg_version: pkg_version, maintainer: maintainer, elapsed: elapsed, result: result.to_string(), cpu: cpu, memory: memory_bd.to_f64().unwrap(), }) } HttpResponse::Ok().json(results) } #[get("/imlonghao-api/pkg/{name}")] async fn get_pkg( name: web::Path, db: web::Data, ) -> impl Responder { let conn = db.get().await.unwrap(); let rows = conn .query( "select ts, pkgbase, COALESCE(pkg_version, '') AS pkg_version, elapsed, result, COALESCE(case when elapsed = 0 then 0 else cputime * 100 / elapsed end, -1) AS cpu, COALESCE(round(memory / 1073741824.0, 3), -1) AS memory from lilac.pkglog WHERE pkgbase=$1 order by id desc", &[&name.to_string()], ) .await .unwrap(); let mut results: Vec = vec![]; for row in rows { let ts: chrono::DateTime = row.get("ts"); let pkgbase: String = row.get("pkgbase"); let pkg_version: String = row.get("pkg_version"); let maintainer = get_maintainer(pkgbase.clone()).unwrap_or("Unknown".to_string()); let elapsed: i32 = row.get("elapsed"); let result: BuildResult = row.get("result"); let cpu: i32 = row.get("cpu"); let memory: pg_bigdecimal::PgNumeric = row.get("memory"); let memory_bd: bigdecimal::BigDecimal = memory.n.unwrap(); results.push(LogsResponse { ts: ts.timestamp(), pkgbase: pkgbase, pkg_version: pkg_version, maintainer: maintainer, elapsed: elapsed, result: result.to_string(), cpu: cpu, memory: memory_bd.to_f64().unwrap(), }) } HttpResponse::Ok().json(results) } #[get("/imlonghao-api/pkg/{name}/log/{ts}")] async fn get_pkg_log( path: web::Path<(String, i64)>, db: web::Data, ) -> impl Responder { let (name, ts) = path.into_inner(); let dt = chrono::DateTime::::from_utc( chrono::naive::NaiveDateTime::from_timestamp(ts, 0), chrono::Utc, ); let conn = db.get().await.unwrap(); let rows = conn .query( "select * from lilac.batch where ts < $1 order by id desc limit 1", &[&dt], ) .await .unwrap(); let event: BatchEvent = rows[0].get("event"); if event != BatchEvent::Start { return HttpResponse::BadRequest().body("wrong time"); } let logdir: String = rows[0].get("logdir"); let contents = std::fs::read_to_string(format!("/home/lilydjwg/.lilac/log/{}/{}.log", logdir, name)) .unwrap(); let converted = ansi_to_html::convert(&contents, true, false).unwrap(); let html = format!("{}{}", STYLE_HTML, converted); HttpResponse::Ok() .content_type("text/html; charset=UTF-8") .body(html) } #[actix_web::main] async fn main() -> std::io::Result<()> { let mut cfg = deadpool_postgres::Config::new(); cfg.user = Some("imlonghao".to_string()); cfg.dbname = Some("lilydjwg".to_string()); cfg.host = Some("/run/postgresql".to_string()); cfg.manager = Some(deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Fast, }); let pool = cfg .create_pool( Some(deadpool_postgres::Runtime::Tokio1), tokio_postgres::NoTls, ) .unwrap(); HttpServer::new(move || { App::new() .app_data(web::Data::new(pool.clone())) .service(status) .service(current) .service(logs) .service(get_pkg) .service(get_pkg_log) }) .bind("127.0.0.1:9077")? .run() .await }