Skip to main content

ghidra_mcp/
logging.rs

1//! Server logging: a per-instance, size-bounded, owner-only file log — never stdout (spec §5). stdout
2//! is the MCP JSON-RPC channel; any byte there corrupts the protocol. The 128-bit connection token is
3//! NEVER passed to a log macro anywhere in the server (enforced by review, not code).
4
5use ghidra_ipc::owner_only::restrict_to_owner;
6use std::io;
7use std::path::Path;
8use tracing_appender::non_blocking::WorkerGuard;
9
10/// Initialize file-only tracing. Returns a guard that MUST be held for the process lifetime (dropping
11/// it flushes+stops the writer). `log_path` is the per-instance path from `paths::instance_log_path()`.
12pub fn init_file_logging(log_path: &Path) -> io::Result<WorkerGuard> {
13    let dir = log_path
14        .parent()
15        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "log path has no parent"))?;
16    std::fs::create_dir_all(dir)?;
17    restrict_to_owner(dir)?;
18
19    let file_name = log_path
20        .file_name()
21        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "log path has no file name"))?;
22    let appender = tracing_appender::rolling::Builder::new()
23        .rotation(tracing_appender::rolling::Rotation::DAILY)
24        .filename_prefix(file_name.to_string_lossy().as_ref())
25        .max_log_files(5)
26        .build(dir)
27        .map_err(|e| io::Error::other(format!("log appender: {e}")))?;
28    let (nb, guard) = tracing_appender::non_blocking(appender);
29
30    // File-only writer: stdout is the MCP JSON-RPC channel and must never see a stray byte.
31    tracing_subscriber::fmt()
32        .with_writer(nb)
33        .with_ansi(false)
34        .init();
35    // Re-assert ownership after the appender created today's log file (belt-and-suspenders: the
36    // directory ACL already restricts new files, but this covers any window before that ACL applied).
37    let _ = restrict_to_owner(dir);
38    Ok(guard)
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn init_creates_owner_only_log_dir_and_never_panics() {
47        let base = std::env::temp_dir().join(format!("ghidra-mcp-log-{}", std::process::id()));
48        let log = base.join("logs").join("worker-test.log");
49        let guard = init_file_logging(&log).expect("logging inits");
50        tracing::info!("hello file log");
51        drop(guard);
52        assert!(log.parent().unwrap().exists());
53        #[cfg(unix)]
54        {
55            use std::os::unix::fs::PermissionsExt;
56            let mode = std::fs::metadata(base.join("logs"))
57                .unwrap()
58                .permissions()
59                .mode();
60            assert_eq!(mode & 0o777, 0o700, "log dir must be owner-only");
61        }
62        std::fs::remove_dir_all(&base).ok();
63    }
64}