diff --git a/server/src/logger.rs b/server/src/logger.rs index 81b7201..6c32b36 100644 --- a/server/src/logger.rs +++ b/server/src/logger.rs @@ -1,53 +1,47 @@ use std::sync::mpsc; -use std::io::Write; +mod ansi; +use fern::Dispatch; -use crossterm::event::MouseEventKind; -use crossterm::{ - cursor::{MoveTo, MoveToNextLine}, - event::{Event, KeyCode, KeyModifiers}, - queue, - style::Print, - terminal::{Clear, ClearType}, -}; +use crate::{allocator::{self, Allocator}, command::command_handler, server_properties::ServerProperties}; -use fern::colors::{Color, ColoredLevelConfig}; +pub type Stdout = mpsc::Sender; +pub type Stdin = mpsc::Receiver; + +pub struct Close { + close: mpsc::Sender<()>, + handle: tokio::task::JoinHandle<()>, + close_handler: Box +} + +impl Close { + pub async fn close(self) { + self.close.send(()).unwrap(); + self.handle.await.unwrap(); + (self.close_handler)(); + } +} -mod color_message; -use color_message::print_line; +trait LoggingFrontend { + fn setup( + properties: &crate::server_properties::ServerProperties, + logger_filter: Dispatch + ) -> anyhow::Result<(Close, Stdin)> { + Self::setup_with_handler(properties, command_handler, logger_filter) + } -use crate::allocator::{self, Allocator}; -use crate::command::command_handler; + fn setup_with_handler String + Send + 'static>( + properties: &crate::server_properties::ServerProperties, + handler: F, + logger_filter: Dispatch + ) -> anyhow::Result<(Close, Stdin)>; -// TODO: Cleanup + fn cleanup(&self); -pub fn setup( - properties: &crate::server_properties::ServerProperties, -) -> Result<(Close, Stdin), fern::InitError> { - setup_with_handler(properties, command_handler) + fn name() -> &'static str; } -pub fn setup_with_handler String + Send + 'static>( - properties: &crate::server_properties::ServerProperties, - handler: F, -) -> Result<(Close, Stdin), fern::InitError> { - let (stdout, stdin, close, join_handle) = TerminalHandler::new(handler, properties.use_colors); - - // let simple_formatter = |out: fern::FormatCallback, message: _, record: &log::Record| { - // out.finish(format_args!( - // "{}[{}][{}] {}", - // chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - // record.level(), - // record.target(), - // message, - // )) - // }; - let stdout_logger = if properties.use_colors { - colored_formatter(fern::Dispatch::new()) - } else { - simple_formatter(fern::Dispatch::new()) - } - .chain(stdout); +pub fn setup(properties: &ServerProperties) -> anyhow::Result<(Close, Stdin)> { let file = simple_formatter(fern::Dispatch::new()).chain( std::fs::OpenOptions::new() .write(true) @@ -56,23 +50,14 @@ pub fn setup_with_handler String + Send + 'static>( .open("output.log")?, ); - fern::Dispatch::new() - .level(log::LevelFilter::Debug) - .level_for("h2", log::LevelFilter::Info) - .level_for("tokio::codec", log::LevelFilter::Info) - .level_for("sqlx::query", log::LevelFilter::Info) - .chain(stdout_logger) - .chain(file) - .apply()?; - log::info!("Saving output to output.log"); - // std::process::exit(1); - Ok(( - Close { - close, - handle: join_handle, - }, - stdin, - )) + let d = fern::Dispatch::new() + .level(log::LevelFilter::Debug) + .level_for("h2", log::LevelFilter::Info) + .level_for("tokio::codec", log::LevelFilter::Info) + .level_for("sqlx::query", log::LevelFilter::Info) + .chain(file); + // TODO select logging frontend + ansi::ANSIFrontend::setup(properties, d) } fn simple_formatter(d: fern::Dispatch) -> fern::Dispatch { @@ -89,321 +74,3 @@ fn simple_formatter(d: fern::Dispatch) -> fern::Dispatch { }, ) } - -fn colored_formatter(d: fern::Dispatch) -> fern::Dispatch { - let colors_line = ColoredLevelConfig::new() - .error(Color::Red) - .warn(Color::Yellow) - // we actually don't need to specify the color for debug and info, they are white by default - .info(Color::White) - .debug(Color::BrightBlack) - // depending on the terminals color scheme, this is the same as the background color - .trace(Color::BrightBlack); - let colors_level = colors_line.info(Color::Green).debug(Color::BrightBlack); - d.format( - move |out: fern::FormatCallback, message: _, record: &log::Record| { - out.finish(format_args!( - "{colors_line}{}[{}{colors_line}][{:>9}][{}] {}\x1B[0m", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - colors_level.clone().color(record.level()), - allocator::as_string(Allocator::allocated()), - record.target(), - message, - colors_line = format!( - "\x1B[{}m", - colors_line.clone().get_color(&record.level()).to_fg_str() - ), - // colors_level = colors_level.clone().get_color(&record.level()).to_fg_str(), - )) - }, - ) -} - -pub type Stdout = mpsc::Sender; -pub type Stdin = mpsc::Receiver; - -struct TerminalHandler -where - T: Send + 'static, - F: Fn(String) -> T + Send + 'static, -{ - stdout: mpsc::Receiver, - stdin: mpsc::Sender, - close: mpsc::Receiver<()>, - handler: F, - lines: LimitedVec, - command: String, - use_colors: bool, -} - -impl TerminalHandler -where - T: Send + 'static, - F: Fn(String) -> T + Send + 'static, -{ - #[allow(clippy::new_ret_no_self)] - fn new( - handler: F, - use_colors: bool, - ) -> ( - Stdout, - Stdin, - mpsc::Sender<()>, - tokio::task::JoinHandle<()>, - ) { - crossterm::terminal::enable_raw_mode().unwrap(); - let mut stdout = std::io::stdout(); - queue!(stdout, crossterm::event::EnableMouseCapture).unwrap(); - let (stdout_send, stdout_recv) = mpsc::channel(); - let (stdin_send, stdin_recv) = mpsc::channel(); - let (close_send, close_recv) = mpsc::channel(); - let join_handle = tokio::task::spawn_blocking(move || { - let mut s = Self { - stdout: stdout_recv, - stdin: stdin_send, - close: close_recv, - handler, - lines: LimitedVec::new(crossterm::terminal::size().unwrap().1 as usize - 1), - command: String::new(), - use_colors, - }; - loop { - if s.iterate() { - break; - } - } - cleanup(); - }); - std::panic::set_hook(Box::new(|info| { - let thread = std::thread::current(); - let name = thread.name().unwrap_or(""); - log::error!("thread '{}' {}", name, info); - log::info!("Press any key to exit"); - loop { - match crossterm::event::read() { - // Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1, - Ok(Event::Key(_)) => break, - Ok(_) => (), - Err(e) => { - log::error!( - "Errored while panicking, this is unrecoverable, exiting: {}", - e - ); - break; - } - } - } - cleanup(); - std::process::exit(0) - })); - (stdout_send, stdin_recv, close_send, join_handle) - } - - fn iterate(&mut self) -> bool { - let mut updated = false; - match self.close.try_recv() { - Err(mpsc::TryRecvError::Empty) => (), - _ => return true, - } - while is_event_available() { - updated = true; - match crossterm::event::read().unwrap() { - Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1, - Event::Key(k) => match (k.code, k.modifiers) { - (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - cleanup(); - std::process::exit(0) - } - (k, _m) => match k { - KeyCode::Char(c) => self.command.push(c), - KeyCode::Backspace => { - self.command.pop(); - } - KeyCode::Enter => { - log::info!(target: "command", " >> {}", self.command); - match self.stdin.send((self.handler)(self.command.clone())) { - Ok(_) => (), - Err(e) => log::error!("{}", e), - }; - self.command = String::new(); - } - KeyCode::Esc => { - self.lines.scroll_to_bottom(); - } - _ => (), - }, - }, - Event::Mouse(m) => match m.kind { - MouseEventKind::ScrollUp => self.lines.scroll_up(), - MouseEventKind::ScrollDown => self.lines.scroll_down(), - _ => { - updated = false; - } - }, - } - } - while let Some(x) = match self.stdout.try_recv() { - Err(mpsc::TryRecvError::Empty) => None, - Err(mpsc::TryRecvError::Disconnected) => return true, - Ok(v) => Some(v), - } { - for line in x.split('\n') { - if !line.is_empty() { - self.lines.push(line.trim_end().to_string()); - } - } - - updated = true; - } - let mut stdout = std::io::stdout(); - // println!("Got stdout"); - if updated { - queue!(stdout, Clear(ClearType::All), MoveTo(0, 0)).unwrap(); - let mut i = 0; - let size = crossterm::terminal::size().unwrap(); - for v in self.lines.iter(self.use_colors) { - print_line(&v, self.use_colors, &mut stdout); - queue!(stdout, MoveToNextLine(1)).unwrap(); - i += 1; - if i >= size.1 as usize - 1 { - break; - } - // println!("{}", v); - } - queue!( - stdout, - MoveTo(0, size.1), - // Clear(ClearType::CurrentLine), - Print(&format!(" >> {}", self.command)) - ) - .unwrap(); - stdout.flush().unwrap(); - } - false - } -} - -fn cleanup() { - log::info!(target: "cleanup", "Exiting server"); - let mut stdout = std::io::stdout(); - queue!( - stdout, - Clear(ClearType::All), - MoveTo(0, 0), - crossterm::event::DisableMouseCapture - ) - .unwrap(); - crossterm::terminal::disable_raw_mode().unwrap(); - stdout.flush().unwrap(); - println!("Exiting server, output has been saved to output.log"); -} - -impl Drop for TerminalHandler -where - T: Send + 'static, - F: Fn(String) -> T + Send + 'static, -{ - fn drop(&mut self) { - queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap(); - } -} - -fn is_event_available() -> bool { - crossterm::event::poll(std::time::Duration::from_millis(10)) - .expect("Error polling for terminal events") -} - -pub struct Close { - close: mpsc::Sender<()>, - handle: tokio::task::JoinHandle<()>, -} - -impl Close { - pub async fn close(self) { - self.close.send(()).unwrap(); - self.handle.await.unwrap(); - queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap(); - } -} - -struct LimitedVec { - inner: Vec, - size: usize, - pos: usize, -} - -impl LimitedVec { - fn new(size: usize) -> Self { - Self { - inner: Vec::with_capacity(size), - size, - pos: 0, - } - } - - fn push(&mut self, v: T) { - // while self.inner.len() >= self.size { - // self.inner.remove(0); - // } - if self.pos != 0 { - self.pos += 1; - } - self.inner.push(v); - // assert!(self.inner.len() <= self.size) - } - - fn iter<'a>(&'a self, use_colors: bool) -> Box + 'a> - where - T: std::fmt::Display, - { - let start = if self.inner.len() < self.size { - 0 - } else { - self.inner - .len() - .checked_sub(self.size) - .and_then(|x| x.checked_sub(self.pos)) - .unwrap_or(0) - }; - let mut end = self.inner.len().saturating_sub(self.pos); - if end < self.inner.len() { - end = self.inner.len() - } - let iter = self.inner[start..end].iter(); - - let map = iter.enumerate().map(move |(i, x)| { - format!( - "{}{}", - if use_colors { - format!( - "\x1B[{}m{:>5}\x1B[0m ", - fern::colors::Color::BrightBlack.to_fg_str(), - start + i - ) - } else { - String::new() - }, - x - ) - }); - Box::new(map) - } - - fn scroll_up(&mut self) { - if self.pos + self.size < self.inner.len() { - // log::info!("SCROLLING UP"); - self.pos += 1; - } - } - - fn scroll_down(&mut self) { - if self.pos > 0 { - // log::info!("SCROLLING DOWN"); - self.pos -= 1; - } - } - - fn scroll_to_bottom(&mut self) { - self.pos = 0; - } -} diff --git a/server/src/logger/ansi.rs b/server/src/logger/ansi.rs new file mode 100644 index 0000000..9a5374a --- /dev/null +++ b/server/src/logger/ansi.rs @@ -0,0 +1,376 @@ +use super::{Close, LoggingFrontend, Stdin, Stdout}; +use std::sync::mpsc; + +use std::io::Write; + +use crossterm::event::MouseEventKind; +use crossterm::{ + cursor::{MoveTo, MoveToNextLine}, + event::{Event, KeyCode, KeyModifiers}, + queue, + style::Print, + terminal::{Clear, ClearType}, +}; + +use fern::Dispatch; +use fern::colors::{Color, ColoredLevelConfig}; + +mod color_message; +use color_message::print_line; + +use crate::allocator::{self, Allocator}; +use crate::logger::simple_formatter; + +pub(super) struct ANSIFrontend; + +impl LoggingFrontend for ANSIFrontend { + fn setup_with_handler String + Send + 'static>( + properties: &crate::server_properties::ServerProperties, + handler: F, + logger_filter: Dispatch + ) -> anyhow::Result<(Close, Stdin)> { + let (stdout, stdin, close, join_handle) = + TerminalHandler::new(handler, properties.use_colors); + + // let simple_formatter = |out: fern::FormatCallback, message: _, record: &log::Record| { + // out.finish(format_args!( + // "{}[{}][{}] {}", + // chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + // record.level(), + // record.target(), + // message, + // )) + // }; + let stdout_logger = if properties.use_colors { + colored_formatter(fern::Dispatch::new()) + } else { + simple_formatter(fern::Dispatch::new()) + } + .chain(stdout); + + + logger_filter + .chain(stdout_logger) + .apply()?; + log::info!("Saving output to output.log"); + // std::process::exit(1); + Ok(( + Close { + close, + handle: join_handle, + close_handler: Box::new(|| queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap()) + }, + stdin, + )) + } + + fn cleanup(&self) { + cleanup() + } + + fn name() -> &'static str { + "ANSI" + } +} + +fn cleanup() { + log::info!(target: "cleanup", "Exiting server"); + let mut stdout = std::io::stdout(); + queue!( + stdout, + Clear(ClearType::All), + MoveTo(0, 0), + crossterm::event::DisableMouseCapture + ) + .unwrap(); + crossterm::terminal::disable_raw_mode().unwrap(); + stdout.flush().unwrap(); + println!("Exiting server, output has been saved to output.log"); +} + +fn colored_formatter(d: fern::Dispatch) -> fern::Dispatch { + let colors_line = ColoredLevelConfig::new() + .error(Color::Red) + .warn(Color::Yellow) + // we actually don't need to specify the color for debug and info, they are white by default + .info(Color::White) + .debug(Color::BrightBlack) + // depending on the terminals color scheme, this is the same as the background color + .trace(Color::BrightBlack); + let colors_level = colors_line.info(Color::Green).debug(Color::BrightBlack); + d.format( + move |out: fern::FormatCallback, message: _, record: &log::Record| { + out.finish(format_args!( + "{colors_line}{}[{}{colors_line}][{:>9}][{}] {}\x1B[0m", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + colors_level.clone().color(record.level()), + allocator::as_string(Allocator::allocated()), + record.target(), + message, + colors_line = format!( + "\x1B[{}m", + colors_line.clone().get_color(&record.level()).to_fg_str() + ), + // colors_level = colors_level.clone().get_color(&record.level()).to_fg_str(), + )) + }, + ) +} + +struct TerminalHandler +where + T: Send + 'static, + F: Fn(String) -> T + Send + 'static, +{ + stdout: mpsc::Receiver, + stdin: mpsc::Sender, + close: mpsc::Receiver<()>, + handler: F, + lines: LimitedVec, + command: String, + use_colors: bool, +} + +impl TerminalHandler +where + T: Send + 'static, + F: Fn(String) -> T + Send + 'static, +{ + #[allow(clippy::new_ret_no_self)] + fn new( + handler: F, + use_colors: bool, + ) -> ( + Stdout, + Stdin, + mpsc::Sender<()>, + tokio::task::JoinHandle<()>, + ) { + crossterm::terminal::enable_raw_mode().unwrap(); + let mut stdout = std::io::stdout(); + queue!(stdout, crossterm::event::EnableMouseCapture).unwrap(); + let (stdout_send, stdout_recv) = mpsc::channel(); + let (stdin_send, stdin_recv) = mpsc::channel(); + let (close_send, close_recv) = mpsc::channel(); + let join_handle = tokio::task::spawn_blocking(move || { + let mut s = Self { + stdout: stdout_recv, + stdin: stdin_send, + close: close_recv, + handler, + lines: LimitedVec::new(crossterm::terminal::size().unwrap().1 as usize - 1), + command: String::new(), + use_colors, + }; + loop { + if s.iterate() { + break; + } + } + cleanup(); + }); + std::panic::set_hook(Box::new(|info| { + let thread = std::thread::current(); + let name = thread.name().unwrap_or(""); + log::error!("thread '{}' {}", name, info); + log::info!("Press any key to exit"); + loop { + match crossterm::event::read() { + // Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1, + Ok(Event::Key(_)) => break, + Ok(_) => (), + Err(e) => { + log::error!( + "Errored while panicking, this is unrecoverable, exiting: {}", + e + ); + break; + } + } + } + cleanup(); + std::process::exit(0) + })); + (stdout_send, stdin_recv, close_send, join_handle) + } + + fn iterate(&mut self) -> bool { + let mut updated = false; + match self.close.try_recv() { + Err(mpsc::TryRecvError::Empty) => (), + _ => return true, + } + while is_event_available() { + updated = true; + match crossterm::event::read().unwrap() { + Event::Resize(_cols, rows) => self.lines.size = rows as usize - 1, + Event::Key(k) => match (k.code, k.modifiers) { + (KeyCode::Char('c'), KeyModifiers::CONTROL) => { + cleanup(); + std::process::exit(0) + } + (k, _m) => match k { + KeyCode::Char(c) => self.command.push(c), + KeyCode::Backspace => { + self.command.pop(); + } + KeyCode::Enter => { + log::info!(target: "command", " >> {}", self.command); + match self.stdin.send((self.handler)(self.command.clone())) { + Ok(_) => (), + Err(e) => log::error!("{}", e), + }; + self.command = String::new(); + } + KeyCode::Esc => { + self.lines.scroll_to_bottom(); + } + _ => (), + }, + }, + Event::Mouse(m) => match m.kind { + MouseEventKind::ScrollUp => self.lines.scroll_up(), + MouseEventKind::ScrollDown => self.lines.scroll_down(), + _ => { + updated = false; + } + }, + } + } + while let Some(x) = match self.stdout.try_recv() { + Err(mpsc::TryRecvError::Empty) => None, + Err(mpsc::TryRecvError::Disconnected) => return true, + Ok(v) => Some(v), + } { + for line in x.split('\n') { + if !line.is_empty() { + self.lines.push(line.trim_end().to_string()); + } + } + + updated = true; + } + let mut stdout = std::io::stdout(); + // println!("Got stdout"); + if updated { + queue!(stdout, Clear(ClearType::All), MoveTo(0, 0)).unwrap(); + let mut i = 0; + let size = crossterm::terminal::size().unwrap(); + for v in self.lines.iter(self.use_colors) { + print_line(&v, self.use_colors, &mut stdout); + queue!(stdout, MoveToNextLine(1)).unwrap(); + i += 1; + if i >= size.1 as usize - 1 { + break; + } + // println!("{}", v); + } + queue!( + stdout, + MoveTo(0, size.1), + // Clear(ClearType::CurrentLine), + Print(&format!(" >> {}", self.command)) + ) + .unwrap(); + stdout.flush().unwrap(); + } + false + } +} + +impl Drop for TerminalHandler +where + T: Send + 'static, + F: Fn(String) -> T + Send + 'static, +{ + fn drop(&mut self) { + queue!(std::io::stdout(), crossterm::event::DisableMouseCapture).unwrap(); + } +} + +fn is_event_available() -> bool { + crossterm::event::poll(std::time::Duration::from_millis(10)) + .expect("Error polling for terminal events") +} + +struct LimitedVec { + inner: Vec, + size: usize, + pos: usize, +} + +impl LimitedVec { + fn new(size: usize) -> Self { + Self { + inner: Vec::with_capacity(size), + size, + pos: 0, + } + } + + fn push(&mut self, v: T) { + // while self.inner.len() >= self.size { + // self.inner.remove(0); + // } + if self.pos != 0 { + self.pos += 1; + } + self.inner.push(v); + // assert!(self.inner.len() <= self.size) + } + + fn iter<'a>(&'a self, use_colors: bool) -> Box + 'a> + where + T: std::fmt::Display, + { + let start = if self.inner.len() < self.size { + 0 + } else { + self.inner + .len() + .checked_sub(self.size) + .and_then(|x| x.checked_sub(self.pos)) + .unwrap_or(0) + }; + let mut end = self.inner.len().saturating_sub(self.pos); + if end < self.inner.len() { + end = self.inner.len() + } + let iter = self.inner[start..end].iter(); + + let map = iter.enumerate().map(move |(i, x)| { + format!( + "{}{}", + if use_colors { + format!( + "\x1B[{}m{:>5}\x1B[0m ", + fern::colors::Color::BrightBlack.to_fg_str(), + start + i + ) + } else { + String::new() + }, + x + ) + }); + Box::new(map) + } + + fn scroll_up(&mut self) { + if self.pos + self.size < self.inner.len() { + // log::info!("SCROLLING UP"); + self.pos += 1; + } + } + + fn scroll_down(&mut self) { + if self.pos > 0 { + // log::info!("SCROLLING DOWN"); + self.pos -= 1; + } + } + + fn scroll_to_bottom(&mut self) { + self.pos = 0; + } +} diff --git a/server/src/logger/color_message.rs b/server/src/logger/ansi/color_message.rs similarity index 100% rename from server/src/logger/color_message.rs rename to server/src/logger/ansi/color_message.rs