128 lines
4.4 KiB
Rust
128 lines
4.4 KiB
Rust
use std::{
|
|
path::PathBuf,
|
|
sync::{Arc, Weak},
|
|
};
|
|
|
|
use async_channel::Sender;
|
|
use async_fs::OpenOptions;
|
|
use futures::AsyncWriteExt as _;
|
|
use galaxyui::r#async::executor::{Background, BackgroundTask};
|
|
|
|
pub mod manager;
|
|
|
|
/// Shared state for a [`SimpleLogger`].
|
|
///
|
|
/// When all [`SimpleLogger`] clones are dropped, this is dropped too, which closes
|
|
/// the logging channel and lets the background writing task finish.
|
|
///
|
|
/// We also support explicit shutdown via [`SimpleLogger::close`]. That allows a
|
|
/// caller to mark a log stream as finished immediately, even if some incidental
|
|
/// clones are still alive briefly in background tasks or callback state.
|
|
pub(crate) struct LogFileWriter {
|
|
log_tx: Sender<String>,
|
|
_logging_task: BackgroundTask,
|
|
}
|
|
|
|
impl LogFileWriter {
|
|
/// Returns true if the underlying channel has been closed.
|
|
///
|
|
/// A closed writer is logically dead even if some [`Arc`] handles still
|
|
/// exist, because it can no longer accept new log lines.
|
|
pub(crate) fn is_closed(&self) -> bool {
|
|
self.log_tx.is_closed()
|
|
}
|
|
}
|
|
|
|
/// A simple file-based logger for server stderr output.
|
|
/// Writes timestamped log entries to a file asynchronously.
|
|
#[derive(Clone)]
|
|
pub struct SimpleLogger {
|
|
// Cheaply cloneable reference to the log file writer.
|
|
writer: Arc<LogFileWriter>,
|
|
}
|
|
|
|
impl SimpleLogger {
|
|
/// Creates a new logger that writes to the specified file path.
|
|
/// The file is truncated on creation.
|
|
pub(crate) fn new(log_path: PathBuf, executor: Arc<Background>) -> Self {
|
|
let (log_tx, log_rx) = async_channel::unbounded::<String>();
|
|
|
|
if let Some(directory) = log_path.parent() {
|
|
let _ = std::fs::create_dir_all(directory);
|
|
}
|
|
|
|
let logging_task = executor.spawn(async move {
|
|
let mut log_file = match OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(&log_path)
|
|
.await
|
|
{
|
|
Ok(log_file) => log_file,
|
|
Err(e) => {
|
|
log::warn!("Could not open file for logging: {:?}. {:?}", &log_path, e);
|
|
return;
|
|
}
|
|
};
|
|
loop {
|
|
match log_rx.recv().await {
|
|
Ok(log_line) => {
|
|
let _ = log_file
|
|
.write_all(
|
|
format!(
|
|
"{} | {}\n",
|
|
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
|
log_line
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.await;
|
|
// Flush after each line to ensure logs are visible immediately
|
|
let _ = log_file.flush().await;
|
|
}
|
|
Err(e) => {
|
|
log::warn!("SimpleLogger: channel closed: {e}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final flush
|
|
let _ = log_file.flush().await;
|
|
});
|
|
|
|
Self {
|
|
writer: Arc::new(LogFileWriter {
|
|
log_tx,
|
|
_logging_task: logging_task,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Log a message to the file.
|
|
pub fn log(&self, message: String) {
|
|
let _ = self.writer.log_tx.try_send(message);
|
|
}
|
|
|
|
/// Explicitly close the logger channel before all clones are dropped.
|
|
///
|
|
/// This is useful when the caller wants "this log stream is finished" to be
|
|
/// a first-class state, rather than waiting for every clone to be dropped.
|
|
/// For example, a failed connection attempt may want to write a final error
|
|
/// line, close the stream immediately, and let a later retry reclaim the
|
|
/// same log path even if some transient clones have not been dropped yet.
|
|
///
|
|
/// This is a no-op if the channel is already closed. Shutdown also happens
|
|
/// automatically when the last [`SimpleLogger`] clone is dropped.
|
|
pub fn close(&self) {
|
|
self.writer.log_tx.close();
|
|
}
|
|
|
|
/// Returns a weak reference to the shared writer, used by [`manager::LogManager`]
|
|
/// to track liveness without preventing shutdown.
|
|
pub(crate) fn downgrade(&self) -> Weak<LogFileWriter> {
|
|
Arc::downgrade(&self.writer)
|
|
}
|
|
}
|