pub mod handlers;
pub mod inference_probe;
pub mod webhook;
pub use handlers::AppState;
use std::net::SocketAddr;
use std::path::PathBuf;
use anyhow::Result;
use axum::{
Router,
routing::{get, post},
};
use tower_http::trace::TraceLayer;
use tracing::{info, warn};
use crate::service::handlers::{handle_health, handle_review, handle_status};
use crate::service::webhook::handle_github_webhook;
pub const DEFAULT_PORT: u16 = 7880;
pub(crate) fn dotfile_http_addr_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".trusty-review").join("http_addr"))
}
pub(crate) fn write_addr_to_path(path: &std::path::Path, addr: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, addr)
}
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/health", get(handle_health))
.route("/status", get(handle_status))
.route("/review", post(handle_review))
.route("/pr/github/webhook", post(handle_github_webhook))
.layer(TraceLayer::new_for_http())
.with_state(state)
}
pub async fn serve(state: AppState, addr: SocketAddr) -> Result<()> {
let listener = tokio::net::TcpListener::bind(addr).await?;
let actual = listener.local_addr()?;
let addr_str = actual.to_string();
info!("trusty-review listening on http://{actual}");
eprintln!("trusty-review: listening on http://{actual}");
let primary_path = match trusty_common::write_daemon_addr("trusty-review", &addr_str) {
Ok(()) => {
trusty_common::resolve_data_dir("trusty-review")
.ok()
.map(|d| d.join("http_addr"))
}
Err(e) => {
warn!("trusty-review: could not write OS-standard http_addr discovery file: {e:#}");
None
}
};
let dotfile_path = match dotfile_http_addr_path() {
Some(p) => match write_addr_to_path(&p, &addr_str) {
Ok(()) => {
info!(
"trusty-review: wrote dotfile discovery address to {}",
p.display()
);
Some(p)
}
Err(e) => {
warn!(
"trusty-review: could not write dotfile http_addr {}: {e}",
p.display()
);
None
}
},
None => {
warn!("trusty-review: no $HOME — skipping dotfile http_addr discovery file");
None
}
};
let app = build_router(state);
axum::serve(listener, app)
.with_graceful_shutdown(trusty_common::shutdown_signal())
.await?;
if let Some(p) = primary_path.as_ref() {
let _ = std::fs::remove_file(p);
}
if let Some(p) = dotfile_path.as_ref() {
let _ = std::fs::remove_file(p);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dotfile_http_addr_path_is_under_home() {
let Some(home) = dirs::home_dir() else {
eprintln!("skip: no $HOME");
return;
};
let path = dotfile_http_addr_path()
.expect("dotfile_http_addr_path returned None when $HOME is set");
assert_eq!(
path,
home.join(".trusty-review").join("http_addr"),
"dotfile path must be $HOME/.trusty-review/http_addr"
);
}
#[test]
fn addr_string_format_is_host_colon_port() {
let addr: SocketAddr = "127.0.0.1:7880".parse().unwrap();
let s = addr.to_string();
assert_eq!(
s, "127.0.0.1:7880",
"addr format must be host:port bare string"
);
assert!(!s.contains("http"), "addr must not include http:// scheme");
assert!(!s.ends_with('\n'), "addr must not have trailing newline");
}
#[test]
fn addr_file_write_creates_parent_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let target = tmp.path().join("subdir").join("http_addr");
write_addr_to_path(&target, "127.0.0.1:7880").expect("write_addr_to_path");
let content = std::fs::read_to_string(&target).expect("read back");
assert_eq!(content, "127.0.0.1:7880");
}
}