//! `[HH:MM:SS] ` to stdout (flushed per line) and appended to the log file. //! //! The launcher pipes this process's stdout into its own log buffer and *parses* //! some of these lines, so the per-line flush and the line shapes are a contract, //! not cosmetics. use std::fs::OpenOptions; use std::io::{self, Write}; use std::path::PathBuf; use crate::localtime; use crate::procmem; /// Environment override for the log file path. pub const LOG_PATH_ENV: &str = "OPENFUT_AUTOPATCH_LOG"; pub struct Logger { path: PathBuf, } impl Logger { /// `$OPENFUT_AUTOPATCH_LOG`, defaulting to `/tmp/openfut-autopatch-.log`. /// /// Resolved once at startup, exactly like the Python's module-level `LOG`, so /// a later environment change cannot move the file mid-run. pub fn from_env() -> io::Result { let path = match std::env::var_os(LOG_PATH_ENV) { Some(path) if !path.is_empty() => PathBuf::from(path), _ => PathBuf::from(format!( "/tmp/openfut-autopatch-{}.log", procmem::current_uid()? )), }; Ok(Self { path }) } pub fn path(&self) -> &std::path::Path { &self.path } /// Emit one line. Timestamped in local time. pub fn log(&self, msg: &str) { let (h, m, s) = localtime::now_hms(); let line = format!("[{h:02}:{m:02}:{s:02}] {msg}"); let mut stdout = io::stdout().lock(); // Ignore a broken pipe: the launcher may have stopped reading, and dying // here would leave FIFA's store patches unenforced. let _ = writeln!(stdout, "{line}"); let _ = stdout.flush(); drop(stdout); if let Err(e) = self.append(&line) { // The Python lets a failing log write kill the process. Patching the // running client matters more than the transcript, so report once to // stderr (the launcher captures it too) and carry on. let _ = writeln!(io::stderr(), "autopatch: cannot append to {}: {e}", self.path.display()); } } fn append(&self, line: &str) -> io::Result<()> { let mut file = OpenOptions::new().create(true).append(true).open(&self.path)?; file.write_all(line.as_bytes())?; file.write_all(b"\n") } } #[cfg(test)] mod tests { use super::*; #[test] fn appends_a_timestamped_line_to_the_configured_path() { let path = std::env::temp_dir().join(format!( "openfut-autopatch-test-{}.log", std::process::id() )); let _ = std::fs::remove_file(&path); let logger = Logger { path: path.clone(), }; logger.log("pid 4242: PATCHED cert gates"); logger.log("second line"); let body = std::fs::read_to_string(&path).unwrap(); let lines: Vec<&str> = body.lines().collect(); assert_eq!(lines.len(), 2); assert_eq!(&lines[0][..1], "["); assert_eq!(&lines[0][3..4], ":"); assert_eq!(&lines[0][6..7], ":"); assert_eq!(&lines[0][9..], "] pid 4242: PATCHED cert gates"); assert!(lines[1].ends_with("] second line")); let _ = std::fs::remove_file(&path); } #[test] fn a_bad_log_path_does_not_kill_the_patcher() { let logger = Logger { path: PathBuf::from("/proc/definitely/not/writable.log"), }; logger.log("still running"); } }