From b11dd081f9a4d72f1f86428b4d314c0d8670d565 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Sat, 16 Dec 2023 16:24:53 +0200 Subject: [PATCH] Write logs to files (and also copy to stdout), keep stdout on windows in debug build --- Cargo.lock | 44 ++++++++++++ Cargo.toml | 2 + src/backend/config.rs | 2 +- src/main.rs | 151 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 172 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f156e6d..e8afd25e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2616,6 +2616,18 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +[[package]] +name = "duct" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ae3fc31835f74c2a7ceda3aeede378b0ae2e74c8f1c36559fcc9ae2a4e7d3e" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "dyn-clonable" version = "0.9.0" @@ -3000,6 +3012,16 @@ dependencies = [ "log", ] +[[package]] +name = "file-rotate" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf221ceec4517f3cb764dae3541b2bd87666fc8832e51322fbb97250b468c71" +dependencies = [ + "chrono", + "flate2", +] + [[package]] name = "filetime" version = "0.2.22" @@ -6643,6 +6665,16 @@ dependencies = [ "sp-std", ] +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -9597,6 +9629,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -10495,7 +10537,9 @@ dependencies = [ "bytesize", "dark-light", "dirs 5.0.1", + "duct", "event-listener-primitives", + "file-rotate", "futures", "gtk4", "hex", diff --git a/Cargo.toml b/Cargo.toml index cd42ac9e..6f43033d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,9 @@ atomic = "0.5.3" bytesize = "1.3.0" dark-light = "1.0.0" dirs = "5.0.1" +duct = "0.13.6" event-listener-primitives = "2.0.1" +file-rotate = "0.7.5" futures = "0.3.29" gtk = { version = "0.7.3", package = "gtk4" } hex = "0.4.3" diff --git a/src/backend/config.rs b/src/backend/config.rs index 9fe8bf17..d07074c3 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -63,7 +63,7 @@ impl RawConfig { return Err(RawConfigError::FailedToDetermineConfigDirectory); }; - let app_config_dir = config_local_dir.join("space-acres"); + let app_config_dir = config_local_dir.join(env!("CARGO_PKG_NAME")); let config_file_path = match fs::create_dir(&app_config_dir).await { Ok(()) => app_config_dir.join("config.json"), Err(error) => { diff --git a/src/main.rs b/src/main.rs index b6ab50e7..ba631e83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![windows_subsystem = "windows"] +#![cfg_attr(debug_assertions, windows_subsystem = "windows")] #![feature(const_option, trait_alias, try_blocks)] mod backend; @@ -10,17 +10,22 @@ use crate::frontend::configuration::{ConfigurationInput, ConfigurationOutput, Co use crate::frontend::loading::{LoadingInput, LoadingView}; use crate::frontend::running::{RunningInput, RunningView}; use atomic::Atomic; +use duct::cmd; +use file_rotate::compression::Compression; +use file_rotate::suffix::AppendCount; +use file_rotate::{ContentLimit, FileRotate}; use futures::channel::mpsc; use futures::{select, FutureExt, SinkExt, StreamExt}; use gtk::prelude::*; use relm4::prelude::*; use relm4::RELM_THREADS; use std::future::Future; -use std::process::{Command, ExitCode, Stdio, Termination}; +use std::io::{Read, Write}; +use std::process::{ExitCode, Termination}; use std::sync::atomic::Ordering; use std::sync::Arc; use std::thread::available_parallelism; -use std::{env, io, process}; +use std::{env, fs, io, process}; use subspace_farmer::utils::{run_future_in_dedicated_thread, AsyncJoinOnDrop}; use subspace_proof_of_space::chia::ChiaTable; use tracing::{info, warn}; @@ -28,6 +33,12 @@ use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; +/// Number of log files to keep +const LOG_FILE_LIMIT_COUNT: usize = 5; +/// Size of one log file +const LOG_FILE_LIMIT_SIZE: usize = 1024 * 1024 * 10; +const LOG_READ_BUFFER: usize = 1024 * 1024; + #[derive(Debug, Copy, Clone)] enum AppStatusCode { Exit, @@ -509,6 +520,12 @@ fn app() -> AppStatusCode { ) .init(); + info!( + "Starting {} {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); + // The default in `relm4` is `1`, set this back to Tokio's default RELM_THREADS .set( @@ -536,39 +553,121 @@ fn app() -> AppStatusCode { exit_status_code: Arc::clone(&exit_status_code), }); - exit_status_code.load(Ordering::Acquire) + let exit_status_code = exit_status_code.load(Ordering::Acquire); + info!( + ?exit_status_code, + "Exiting {} {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); + exit_status_code } fn supervisor() -> io::Result<()> { + let maybe_app_data_dir = dirs::data_local_dir() + .map(|data_local_dir| data_local_dir.join(env!("CARGO_PKG_NAME"))) + .and_then(|app_data_dir| { + if !app_data_dir.exists() { + if let Err(error) = fs::create_dir_all(&app_data_dir) { + eprintln!( + "App data directory \"{}\" doesn't exist and can't be created: {}", + app_data_dir.display(), + error + ); + return None; + } + } + + Some(app_data_dir) + }); + let mut maybe_logger = maybe_app_data_dir.as_ref().map(|app_data_dir| { + FileRotate::new( + app_data_dir.join("space-acres.log"), + AppendCount::new(LOG_FILE_LIMIT_COUNT), + ContentLimit::Bytes(LOG_FILE_LIMIT_SIZE), + Compression::OnRotate(0), + #[cfg(unix)] + None, + ) + }); + + let mut log_read_buffer = vec![0u8; LOG_READ_BUFFER]; + loop { - let mut child_process = Command::new(env::current_exe()?) + let expression = cmd!(env::current_exe()?) .env("CHILD_PROCESS", "1") - .stdin(Stdio::null()) - // TODO: Use pipes and capture stdout/stderr to write logs to files - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn()?; - - match child_process.wait()?.code() { - Some(status_code) => { - match AppStatusCode::from_status_code(status_code) { - AppStatusCode::Exit => { - // Exited gracefully - info!("Application exited gracefully"); - break; - } - AppStatusCode::Restart => { - info!("Restarting application"); - continue; + .stderr_to_stdout() + // We use non-zero status codes and they don't mean error necessarily + .unchecked(); + + let exit_status = if let Some(logger) = maybe_logger.as_mut() { + let mut expression = expression.reader()?; + + let mut stdout = io::stdout(); + loop { + match expression.read(&mut log_read_buffer) { + Ok(bytes_count) => { + if bytes_count == 0 { + break; + } + + let write_result: io::Result<()> = try { + stdout.write_all(&log_read_buffer[..bytes_count])?; + logger.write_all(&log_read_buffer[..bytes_count])?; + }; + + if let Err(error) = write_result { + eprintln!("Error while writing output of child process: {error}"); + break; + } } - AppStatusCode::Unknown(status_code) => { - warn!(%status_code, "Application exited with unexpected status code"); - process::exit(status_code); + Err(error) => { + if error.kind() == io::ErrorKind::Interrupted { + // Try again + continue; + } + eprintln!("Error while reading output of child process: {error}"); + break; } } } + + stdout.flush()?; + if let Err(error) = logger.flush() { + eprintln!("Error while flushing logs: {error}"); + } + + match expression.try_wait()? { + Some(output) => output.status, + None => { + return Err(io::Error::new( + io::ErrorKind::Other, + "Logs writing ended before child process did, exiting", + )); + } + } + } else { + eprintln!("App data directory doesn't exist, not creating log file"); + expression.run()?.status + }; + + match exit_status.code() { + Some(status_code) => match AppStatusCode::from_status_code(status_code) { + AppStatusCode::Exit => { + eprintln!("Application exited gracefully"); + break; + } + AppStatusCode::Restart => { + eprintln!("Restarting application"); + continue; + } + AppStatusCode::Unknown(status_code) => { + eprintln!("Application exited with unexpected status code {status_code}"); + process::exit(status_code); + } + }, None => { - warn!("Application terminated by signal"); + eprintln!("Application terminated by signal"); break; } }