ownpg 0.1.1

OwnPG serves PostgreSQL DBA tools to AI clients over the Model Context Protocol, one database and one schema per run
mod app;
mod audit_cmd;
mod build_info;
mod cli;
mod config_cmd;
mod context;
mod doctor;
mod health;
mod logging;
mod man;
mod output;
mod serve;

use std::io::{self, Write};
use std::process::ExitCode;

use crate::cli::Cli;

fn main() -> ExitCode {
    install_panic_hook();
    let args = Cli::parse_args(build_info::version_line());
    ExitCode::from(app::run(args).code())
}

fn install_panic_hook() {
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let with_backtrace = backtrace_requested();
        {
            let mut stderr = io::stderr().lock();
            write_panic_report(&mut stderr, info, with_backtrace);
        }
        if with_backtrace {
            previous(info);
        }
    }));
}

fn write_panic_report(
    out: &mut impl Write,
    info: &std::panic::PanicHookInfo<'_>,
    with_backtrace: bool,
) {
    let _ = writeln!(out, "OwnPG stopped unexpectedly.");
    let _ = writeln!(
        out,
        "This is a bug. Please report it with the lines below at"
    );
    let _ = writeln!(out, "{}", build_info::ISSUES_URL);
    let _ = writeln!(out, "  version: {}", build_info::version_line());
    if !with_backtrace {
        let _ = writeln!(out, "{info}");
    }
}

fn backtrace_requested() -> bool {
    std::env::var("RUST_BACKTRACE").is_ok_and(|value| value != "0")
}

#[cfg(test)]
mod tests {
    use std::sync::{Arc, Mutex};

    use super::*;

    #[test]
    fn the_report_names_the_panic_location_and_message_without_a_backtrace_flag() {
        let captured: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
        let captured_for_hook = Arc::clone(&captured);
        let previous = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |info| {
            if let Ok(mut buffer) = captured_for_hook.lock() {
                write_panic_report(&mut *buffer, info, false);
            }
        }));
        let result = std::panic::catch_unwind(|| panic!("a deliberate test panic"));
        std::panic::set_hook(previous);
        assert!(result.is_err());
        let text = String::from_utf8(captured.lock().unwrap_or_else(|e| e.into_inner()).clone())
            .unwrap_or_default();
        assert!(text.contains("OwnPG stopped unexpectedly."), "{text}");
        assert!(text.contains("a deliberate test panic"), "{text}");
        assert!(text.contains(file!()), "{text}");
    }

    #[test]
    fn the_report_omits_the_raw_panic_line_when_a_backtrace_will_follow() {
        let captured: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
        let captured_for_hook = Arc::clone(&captured);
        let previous = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |info| {
            if let Ok(mut buffer) = captured_for_hook.lock() {
                write_panic_report(&mut *buffer, info, true);
            }
        }));
        let result = std::panic::catch_unwind(|| panic!("a second deliberate test panic"));
        std::panic::set_hook(previous);
        assert!(result.is_err());
        let text = String::from_utf8(captured.lock().unwrap_or_else(|e| e.into_inner()).clone())
            .unwrap_or_default();
        assert!(text.contains("OwnPG stopped unexpectedly."), "{text}");
        assert!(!text.contains("panicked at"), "{text}");
    }
}