diff --git a/Cargo.lock b/Cargo.lock index ebfd3f8..9504eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,19 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -280,27 +293,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] -name = "env_filter" -version = "0.1.2" +name = "encode_unicode" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "equivalent" @@ -399,12 +395,6 @@ dependencies = [ "windows", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "indenter" version = "0.3.3" @@ -442,6 +432,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.167" @@ -640,8 +636,8 @@ name = "pitchfork-cli" version = "0.1.0" dependencies = [ "clap", + "console", "dirs", - "env_logger", "eyre", "interprocess", "log", diff --git a/Cargo.toml b/Cargo.toml index 64d87d0..25c6141 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ dirs = "5.0.1" once_cell = "1.20.2" xx = {version = "2", features = ["fslock"]} log = "0.4.22" -env_logger = "0.11.5" psutil = "3.3.0" eyre = "0.6.12" interprocess = { version = "2.2.2", features = ["tokio"] } @@ -23,3 +22,4 @@ tokio = { version = "1.42.0", features = ["full"] } sysinfo = "0.33.0" serde = { version = "1.0.215", features = ["derive"] } toml = "0.8.19" +console = "0.15.8" diff --git a/src/cli/daemon/mod.rs b/src/cli/daemon/mod.rs new file mode 100644 index 0000000..e73083f --- /dev/null +++ b/src/cli/daemon/mod.rs @@ -0,0 +1,22 @@ +use crate::Result; + +mod run; + +#[derive(Debug, clap::Args)] +pub struct Daemon { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, clap::Subcommand)] +enum Commands { + Run(run::Run), +} + +impl Daemon { + pub async fn run(self) -> Result<()> { + match self.command { + Commands::Run(run) => run.run().await, + } + } +} diff --git a/src/cli/daemon.rs b/src/cli/daemon/run.rs similarity index 87% rename from src/cli/daemon.rs rename to src/cli/daemon/run.rs index 4e89b0a..2c35dad 100644 --- a/src/cli/daemon.rs +++ b/src/cli/daemon/run.rs @@ -1,28 +1,26 @@ use std::time::Duration; use log::{info, warn}; -use sysinfo::Pid; use crate::{env, procs}; use crate::Result; use tokio::time; use crate::pid_file::PidFile; #[derive(Debug, clap::Args)] -#[clap(hide = false)] -pub struct Daemon { +pub struct Run { #[clap(short, long)] force: bool, } -impl Daemon { +impl Run { pub async fn run(&self) -> Result<()> { let mut pid_file = PidFile::read(&*env::PITCHFORK_PID_FILE)?; - if let Some(existing_pid) = pid_file.pids.get("pitchfork") { + if let Some(existing_pid) = pid_file.get("pitchfork") { if self.kill_or_stop(*existing_pid)? == false { return Ok(()); } } let pid = std::process::id(); - pid_file.pids.insert("pitchfork".to_string(), pid); + pid_file.set("pitchfork".to_string(), pid); pid_file.write(&*env::PITCHFORK_PID_FILE)?; let mut interval = time::interval(Duration::from_millis(1000)); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 4b19d3b..36dcdbf 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,17 +1,17 @@ use clap::Parser; use crate::Result; -mod daemon; mod start; +mod daemon; #[derive(Debug, clap::Parser)] struct Cli { #[clap(subcommand)] - command: Command, + command: Commands, } #[derive(Debug, clap::Subcommand)] -enum Command { +enum Commands { Daemon(daemon::Daemon), Start(start::Start), } @@ -20,7 +20,7 @@ enum Command { pub async fn run() -> Result<()> { let args = Cli::parse(); match args.command { - Command::Daemon(daemon) => daemon.run().await, - Command::Start(start) => start.run().await, + Commands::Daemon(daemon) => daemon.run().await, + Commands::Start(start) => start.run().await, } } diff --git a/src/env.rs b/src/env.rs index 00afa53..733b7af 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,12 +1,37 @@ -pub use std::env; use once_cell::sync::Lazy; +pub use std::env; use std::path::PathBuf; -use log::trace; pub static HOME_DIR: Lazy = Lazy::new(|| dirs::home_dir().unwrap_or(PathBuf::new())); pub static PITCHFORK_STATE_DIR: Lazy = Lazy::new(|| { - dirs::state_dir() - .unwrap_or(HOME_DIR.join(".local").join("state")) - .join("pitchfork") + var_path("PITCHFORK_STATE_DIR").unwrap_or( + dirs::state_dir() + .unwrap_or(HOME_DIR.join(".local").join("state")) + .join("pitchfork"), + ) }); pub static PITCHFORK_PID_FILE: Lazy = Lazy::new(|| PITCHFORK_STATE_DIR.join("pids.toml")); +pub static PITCHFORK_LOG: Lazy = Lazy::new(|| { + env::var("PITCHFORK_LOG") + .ok() + .and_then(|level| level.parse().ok()) + .unwrap_or(log::LevelFilter::Info) +}); +pub static PITCHFORK_LOG_FILE_LEVEL: Lazy = Lazy::new(|| { + env::var("PITCHFORK_LOG_FILE_LEVEL") + .ok() + .and_then(|level| level.parse().ok()) + .unwrap_or(*PITCHFORK_LOG) +}); +pub static PITCHFORK_LOG_FILE: Lazy = Lazy::new(|| { + var_path("PITCHFORK_LOG_FILE").unwrap_or( + PITCHFORK_STATE_DIR + .join("logs") + .join("pitchfork") + .join("pitchfork.log"), + ) +}); + +fn var_path(name: &str) -> Option { + env::var(name).map(|path| PathBuf::from(path)).ok() +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..be7ab54 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,140 @@ +use eyre::Result; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::path::Path; +use std::sync::Mutex; +use std::thread; + +use crate::{env, ui}; +use log::{warn, Level, LevelFilter, Metadata, Record}; +use once_cell::sync::Lazy; + +#[derive(Debug)] +struct Logger { + level: LevelFilter, + term_level: LevelFilter, + file_level: LevelFilter, + log_file: Option>, +} + +impl log::Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &Record) { + if record.level() <= self.file_level { + if let Some(log_file) = &self.log_file { + let mut log_file = log_file.lock().unwrap(); + let out = self.render(record, self.file_level); + let _ = writeln!(log_file, "{}", console::strip_ansi_codes(&out)); + } + } + if record.level() <= self.term_level { + let out = self.render(record, self.term_level); + eprintln!("{}", out); + } + } + + fn flush(&self) {} +} + +static LOGGER: Lazy = Lazy::new(Logger::init); + +impl Logger { + fn init() -> Self { + let term_level = *env::PITCHFORK_LOG; + let file_level = *env::PITCHFORK_LOG_FILE_LEVEL; + + let mut logger = Logger { + level: std::cmp::max(term_level, file_level), + file_level, + term_level, + log_file: None, + }; + + let log_file = &*env::PITCHFORK_LOG_FILE; + if let Ok(log_file) = init_log_file(log_file) { + logger.log_file = Some(Mutex::new(log_file)); + } else { + warn!("could not open log file: {log_file:?}"); + } + + logger + } + + fn render(&self, record: &Record, level: LevelFilter) -> String { + match level { + LevelFilter::Off => "".to_string(), + LevelFilter::Trace => { + let meta = ui::style::edim(format!( + "{thread_id:>2} [{file}:{line}]", + thread_id = thread_id(), + file = record.file().unwrap_or(""), + line = record.line().unwrap_or(0), + )); + format!( + "{level} {meta} {args}", + level = self.styled_level(record.level()), + args = record.args() + ) + } + LevelFilter::Debug => format!( + "{level} {args}", + level = self.styled_level(record.level()), + args = record.args() + ), + _ => { + let mise = match record.level() { + Level::Error => ui::style::ered("mise"), + Level::Warn => ui::style::eyellow("mise"), + _ => ui::style::edim("mise"), + }; + match record.level() { + Level::Info => format!("{mise} {args}", args = record.args()), + _ => format!( + "{mise} {level} {args}", + level = self.styled_level(record.level()), + args = record.args() + ), + } + } + } + } + + fn styled_level(&self, level: Level) -> String { + let level = match level { + Level::Error => ui::style::ered("ERROR").to_string(), + Level::Warn => ui::style::eyellow("WARN").to_string(), + Level::Info => ui::style::ecyan("INFO").to_string(), + Level::Debug => ui::style::emagenta("DEBUG").to_string(), + Level::Trace => ui::style::edim("TRACE").to_string(), + }; + console::pad_str(&level, 5, console::Alignment::Left, None).to_string() + } +} + +pub fn thread_id() -> String { + let id = format!("{:?}", thread::current().id()); + let id = id.replace("ThreadId(", ""); + id.replace(")", "") +} + +pub fn init() { + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(|| { + if let Err(err) = log::set_logger(&*LOGGER).map(|()| log::set_max_level(LOGGER.level)) { + eprintln!("mise: could not initialize logger: {err}"); + } + }); +} + +fn init_log_file(log_file: &Path) -> Result { + if let Some(log_dir) = log_file.parent() { + xx::file::mkdirp(log_dir)?; + } + Ok(OpenOptions::new() + .create(true) + .append(true) + .open(log_file)?) +} diff --git a/src/main.rs b/src/main.rs index f82d618..e2eff26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,12 @@ mod cli; mod env; mod pid_file; mod procs; +mod logger; +mod ui; pub use eyre::Result; fn main() -> Result<()> { - let env = env_logger::Env::new().filter("PITCHFORK_LOG").write_style("PITCHFORK_LOG_STYLE"); - env_logger::init_from_env(env); + logger::init(); cli::run() } diff --git a/src/pid_file.rs b/src/pid_file.rs index 89d6e5f..d339adb 100644 --- a/src/pid_file.rs +++ b/src/pid_file.rs @@ -2,13 +2,16 @@ use std::collections::BTreeMap; use std::path::Path; use crate::Result; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct PidFile { - pub pids: BTreeMap, + pids: BTreeMap, } impl PidFile { pub fn read>(path: P) -> Result { + if !path.as_ref().exists() { + return Ok(Self::default()); + } let raw = xx::file::read_to_string(path)?; let pids = toml::from_str(&raw)?; Ok(pids) @@ -19,4 +22,12 @@ impl PidFile { xx::file::write(path, raw)?; Ok(()) } + + pub fn set(&mut self, key: String, value: u32) { + self.pids.insert(key, value); + } + + pub fn get(&self, key: &str) -> Option<&u32> { + self.pids.get(key) + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..1c7770a --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1 @@ +pub(crate) mod style; diff --git a/src/ui/style.rs b/src/ui/style.rs new file mode 100644 index 0000000..36f7577 --- /dev/null +++ b/src/ui/style.rs @@ -0,0 +1,87 @@ +#![allow(unused)] + +use console::{style, StyledObject}; + +pub fn ereset() -> String { + if console::colors_enabled_stderr() { + "\x1b[0m".to_string() + } else { + "".to_string() + } +} + +pub fn estyle(val: D) -> StyledObject { + style(val).for_stderr() +} + +pub fn ecyan(val: D) -> StyledObject { + estyle(val).cyan() +} + +pub fn eblue(val: D) -> StyledObject { + estyle(val).blue() +} + +pub fn emagenta(val: D) -> StyledObject { + estyle(val).magenta() +} + +pub fn egreen(val: D) -> StyledObject { + estyle(val).green() +} + +pub fn eyellow(val: D) -> StyledObject { + estyle(val).yellow() +} + +pub fn ered(val: D) -> StyledObject { + estyle(val).red() +} + +pub fn eblack(val: D) -> StyledObject { + estyle(val).black() +} + +pub fn eunderline(val: D) -> StyledObject { + estyle(val).underlined() +} + +pub fn edim(val: D) -> StyledObject { + estyle(val).dim() +} + +pub fn ebold(val: D) -> StyledObject { + estyle(val).bold() +} + +pub fn nstyle(val: D) -> StyledObject { + style(val).for_stdout() +} + +pub fn nblue(val: D) -> StyledObject { + nstyle(val).blue() +} + +pub fn ncyan(val: D) -> StyledObject { + nstyle(val).cyan() +} + +pub fn nbold(val: D) -> StyledObject { + nstyle(val).bold() +} + +pub fn nunderline(val: D) -> StyledObject { + nstyle(val).underlined() +} + +pub fn nyellow(val: D) -> StyledObject { + nstyle(val).yellow() +} + +pub fn nred(val: D) -> StyledObject { + nstyle(val).red() +} + +pub fn ndim(val: D) -> StyledObject { + nstyle(val).dim() +}