diff --git a/Cargo.lock b/Cargo.lock index 818e711135..e35d60dd5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8052e2d8aabbb8d556d6abbcce2a22b9590996c5f849b9c7ce4544a2e3b984e" +[[package]] +name = "ansi-parser" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761ac675f1638a6a49e26f6ac3a4067ca3fefa8029816ae4ef8d3fa3d06a5194" +dependencies = [ + "nom 4.2.3", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -93,6 +102,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arc-swap" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" + [[package]] name = "arr_macro" version = "0.1.3" @@ -170,7 +185,7 @@ dependencies = [ "kv-log-macro", "log", "memchr", - "mio", + "mio 0.6.22", "mio-uds", "num_cpus", "once_cell", @@ -440,11 +455,17 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aa2097be53a00de9e8fc349fea6d76221f398f5c4fa550d420669906962d160" dependencies = [ - "mio", + "mio 0.6.22", "mio-extras", "nix 0.14.1", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.2.3" @@ -950,6 +971,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio 0.7.0", + "parking_lot 0.10.2", + "signal-hook", + "winapi 0.3.8", +] + +[[package]] +name = "crossterm_winapi" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "csv" version = "1.1.3" @@ -2527,12 +2573,26 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.1", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9971bc8349a361217a8f2a41f5d011274686bd4436465ba51730921039d7fb" +dependencies = [ + "lazy_static", + "libc", + "log", + "miow 0.3.5", + "ntapi", + "winapi 0.3.8", +] + [[package]] name = "mio-extras" version = "2.0.6" @@ -2541,7 +2601,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" dependencies = [ "lazycell", "log", - "mio", + "mio 0.6.22", "slab", ] @@ -2553,7 +2613,7 @@ checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ "iovec", "libc", - "mio", + "mio 0.6.22", ] [[package]] @@ -2568,6 +2628,16 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.8", +] + [[package]] name = "mopa" version = "0.2.2" @@ -2702,12 +2772,21 @@ dependencies = [ "fsevent-sys", "inotify", "libc", - "mio", + "mio 0.6.22", "mio-extras", "walkdir", "winapi 0.3.8", ] +[[package]] +name = "ntapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "num" version = "0.1.42" @@ -3909,6 +3988,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5752e017e03af9d735b4b069f53b7a7fd90fefafa04d8bd0c25581b0bff437f" +[[package]] +name = "signal-hook" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" +dependencies = [ + "libc", + "mio 0.7.0", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +dependencies = [ + "arc-swap", + "libc", +] + [[package]] name = "slab" version = "0.4.2" @@ -3956,6 +4056,18 @@ dependencies = [ "smithay-client-toolkit", ] +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + [[package]] name = "specs" version = "0.16.1" @@ -4266,7 +4378,7 @@ checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" dependencies = [ "bytes", "futures 0.1.29", - "mio", + "mio 0.6.22", "num_cpus", "tokio-current-thread", "tokio-executor", @@ -4328,7 +4440,7 @@ dependencies = [ "futures 0.1.29", "lazy_static", "log", - "mio", + "mio 0.6.22", "num_cpus", "parking_lot 0.9.0", "slab", @@ -4356,7 +4468,7 @@ dependencies = [ "bytes", "futures 0.1.29", "iovec", - "mio", + "mio 0.6.22", "tokio-io", "tokio-reactor", ] @@ -4517,6 +4629,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +[[package]] +name = "tui" +version = "0.10.0" +source = "git+https://github.com/fdehau/tui-rs.git?branch=paragraph-scroll#54b841fab6cfdb38e8dc1382176e965787964b4c" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "tuple_utils" version = "0.3.0" @@ -4785,8 +4909,13 @@ dependencies = [ name = "veloren-server-cli" version = "0.7.0" dependencies = [ + "ansi-parser", + "clap", + "crossterm", + "lazy_static", "tracing", "tracing-subscriber", + "tui", "veloren-common", "veloren-server", ] @@ -5025,7 +5154,7 @@ dependencies = [ "calloop", "downcast-rs", "libc", - "mio", + "mio 0.6.22", "nix 0.14.1", "wayland-commons", "wayland-scanner", @@ -5162,7 +5291,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio", + "mio 0.6.22", "mio-extras", "ndk", "ndk-glue", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 93ab4b095b..cdb05ded2a 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -14,3 +14,13 @@ common = { package = "veloren-common", path = "../common" } tracing = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.2.3", default-features = false, features = ["env-filter", "fmt", "chrono", "ansi", "smallvec"] } +crossterm = "0.17" +lazy_static = "1" +ansi-parser = "0.6" +clap = "2.33" + +[dependencies.tui] +git = "https://github.com/fdehau/tui-rs.git" +branch="paragraph-scroll" +default-features = false +features = ['crossterm'] \ No newline at end of file diff --git a/server-cli/Dockerfile b/server-cli/Dockerfile index 3cb85181f9..3ed1434712 100644 --- a/server-cli/Dockerfile +++ b/server-cli/Dockerfile @@ -13,4 +13,4 @@ COPY ./assets/common /opt/assets/common COPY ./assets/world /opt/assets/world WORKDIR /opt -CMD [ "sh", "-c", "RUST_BACKTRACE=1 /opt/veloren-server-cli" ] +CMD [ "sh", "-c", "RUST_BACKTRACE=1 /opt/veloren-server-cli -b" ] diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 3668f8c97a..526c1be55d 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -1,15 +1,53 @@ #![deny(unsafe_code)] +mod tui_runner; +mod tuilog; + +#[macro_use] extern crate lazy_static; + +use crate::{ + tui_runner::{Message, Tui}, + tuilog::TuiLog, +}; use common::clock::Clock; use server::{Event, Input, Server, ServerSettings}; -use std::time::Duration; use tracing::{info, Level}; use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; +use clap::{App, Arg}; +use std::{io, sync::mpsc, time::Duration}; + const TPS: u64 = 30; const RUST_LOG_ENV: &str = "RUST_LOG"; -fn main() { +lazy_static! { + static ref LOG: TuiLog<'static> = TuiLog::default(); +} + +fn main() -> io::Result<()> { + let matches = App::new("Veloren server cli") + .version( + format!( + "{}-{}", + env!("CARGO_PKG_VERSION"), + common::util::GIT_HASH.to_string() + ) + .as_str(), + ) + .author("The veloren devs ") + .about("The veloren server cli provides an easy to use interface to start a veloren server") + .arg( + Arg::with_name("basic") + .short("b") + .long("basic") + .help("Disables the tui") + .takes_value(false), + ) + .get_matches(); + + let basic = matches.is_present("basic"); + let (mut tui, msg_r) = Tui::new(); + // Init logging let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { Some(Ok(env)) => { @@ -30,10 +68,17 @@ fn main() { .add_directive(LevelFilter::INFO.into()), }; - FmtSubscriber::builder() + let subscriber = FmtSubscriber::builder() .with_max_level(Level::ERROR) - .with_env_filter(filter) - .init(); + .with_env_filter(filter); + + if basic { + subscriber.init(); + } else { + subscriber.with_writer(|| LOG.clone()).init(); + } + + tui.run(basic); info!("Starting server..."); @@ -44,7 +89,6 @@ fn main() { let settings = ServerSettings::load(); let server_port = &settings.gameserver_address.port(); let metrics_port = &settings.metrics_address.port(); - // Create server let mut server = Server::new(settings).expect("Failed to create server instance!"); @@ -68,7 +112,22 @@ fn main() { // Clean up the server after a tick. server.cleanup(); + match msg_r.try_recv() { + Ok(msg) => match msg { + Message::Quit => { + info!("Closing the server"); + break; + }, + }, + Err(e) => match e { + mpsc::TryRecvError::Empty => {}, + mpsc::TryRecvError::Disconnected => panic!(), + }, + }; + // Wait for the next tick. clock.tick(Duration::from_millis(1000 / TPS)); } + + Ok(()) } diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs new file mode 100644 index 0000000000..8b2432dac6 --- /dev/null +++ b/server-cli/src/tui_runner.rs @@ -0,0 +1,245 @@ +use crate::LOG; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + io::{self, Write}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, + }, + time::Duration, +}; +use tracing::{error, info, warn}; +use tui::{ + backend::CrosstermBackend, + layout::Rect, + text::Text, + widgets::{Block, Borders, Paragraph, Wrap}, + Terminal, +}; + +#[derive(Debug, Clone)] +pub enum Message { + Quit, +} + +pub struct Command<'a> { + pub name: &'a str, + pub description: &'a str, + // Whether or not the command splits the arguments on whitespace + pub split_spaces: bool, + pub args: usize, + pub cmd: fn(Vec, &mut mpsc::Sender), +} + +pub const COMMANDS: [Command; 2] = [ + Command { + name: "quit", + description: "Closes the server", + split_spaces: true, + args: 0, + cmd: |_, sender| sender.send(Message::Quit).unwrap(), + }, + Command { + name: "help", + description: "List all command available", + split_spaces: true, + args: 0, + cmd: |_, _| { + info!("===== Help ====="); + for command in COMMANDS.iter() { + info!("{} - {}", command.name, command.description) + } + info!("================"); + }, + }, +]; + +pub struct Tui { + msg_s: Option>, + background: Option>, + running: Arc, +} + +impl Tui { + pub fn new() -> (Self, mpsc::Receiver) { + let (msg_s, msg_r) = mpsc::channel(); + ( + Self { + msg_s: Some(msg_s), + background: None, + running: Arc::new(AtomicBool::new(true)), + }, + msg_r, + ) + } + + fn handle_events(input: &mut String, msg_s: &mut mpsc::Sender) { + use crossterm::event::*; + if let Event::Key(event) = read().unwrap() { + match event.code { + KeyCode::Char('c') => { + if event.modifiers.contains(KeyModifiers::CONTROL) { + msg_s.send(Message::Quit).unwrap() + } else { + input.push('c'); + } + }, + KeyCode::Char(c) => input.push(c), + KeyCode::Backspace => { + input.pop(); + }, + KeyCode::Enter => { + parse_command(input, msg_s); + + *input = String::new(); + }, + _ => {}, + } + } + } + + pub fn run(&mut self, basic: bool) { + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + Self::shutdown(); + hook(info); + })); + + let mut msg_s = self.msg_s.take().unwrap(); + let running = self.running.clone(); + + if basic { + std::thread::spawn(move || { + while running.load(Ordering::Relaxed) { + let mut buf = String::new(); + + io::stdin().read_line(&mut buf).unwrap(); + + parse_command(&buf, &mut msg_s); + } + }); + } else { + self.background = Some(std::thread::spawn(move || { + // Start the tui + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + + enable_raw_mode().unwrap(); + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + + let mut input = String::new(); + + if let Err(e) = terminal.clear() { + error!(?e, "clouldn't clean terminal"); + }; + + while running.load(Ordering::Relaxed) { + if let Err(e) = terminal.draw(|f| { + let (log_rect, input_rect) = if f.size().height > 6 { + let mut log_rect = f.size(); + log_rect.height -= 3; + + let mut input_rect = f.size(); + input_rect.y = input_rect.height - 3; + input_rect.height = 3; + + (log_rect, input_rect) + } else { + (f.size(), Rect::default()) + }; + + let block = Block::default().borders(Borders::ALL); + + let mut wrap = Wrap::default(); + wrap.scroll_callback = Some(Box::new(|text_area, lines| { + LOG.resize(text_area.height as usize); + let len = lines.len() as u16; + (len.saturating_sub(text_area.height), 0) + })); + + let logger = Paragraph::new(LOG.inner.lock().unwrap().clone()) + .block(block) + .wrap(wrap); + f.render_widget(logger, log_rect); + + let text: Text = input.as_str().into(); + + let block = Block::default().borders(Borders::ALL); + let size = block.inner(input_rect); + + let x = (size.x + text.width() as u16).min(size.width); + + let input_field = Paragraph::new(text).block(block); + f.render_widget(input_field, input_rect); + + f.set_cursor(x, size.y); + }) { + warn!(?e, "couldn't draw frame"); + }; + if crossterm::event::poll(Duration::from_millis(10)).unwrap() { + Self::handle_events(&mut input, &mut msg_s); + }; + } + })); + } + } + + fn shutdown() { + let mut stdout = io::stdout(); + + execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); + disable_raw_mode().unwrap(); + } +} + +impl Drop for Tui { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + self.background.take().map(|m| m.join()); + Self::shutdown(); + } +} + +fn parse_command(input: &str, msg_s: &mut mpsc::Sender) { + let mut args = input.split_whitespace(); + + if let Some(cmd_name) = args.next() { + if let Some(cmd) = COMMANDS.iter().find(|cmd| cmd.name == cmd_name) { + let args = args.collect::>(); + + let (arg_len, args) = if cmd.split_spaces { + ( + args.len(), + args.into_iter() + .map(|s| s.to_string()) + .collect::>(), + ) + } else { + (1, vec![args.into_iter().collect::()]) + }; + + match arg_len.cmp(&cmd.args) { + std::cmp::Ordering::Less => error!("{} takes {} arguments", cmd_name, cmd.args), + std::cmp::Ordering::Greater => { + warn!("{} only takes {} arguments", cmd_name, cmd.args); + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + std::cmp::Ordering::Equal => { + let cmd = cmd.cmd; + + cmd(args, msg_s) + }, + } + } else { + error!("{} not found", cmd_name); + } + } +} diff --git a/server-cli/src/tuilog.rs b/server-cli/src/tuilog.rs new file mode 100644 index 0000000000..273626477b --- /dev/null +++ b/server-cli/src/tuilog.rs @@ -0,0 +1,88 @@ +use std::{ + io::{self, Write}, + sync::{Arc, Mutex}, +}; +use tracing::warn; +use tui::text::Text; + +#[derive(Debug, Default, Clone)] +pub struct TuiLog<'a> { + pub inner: Arc>>, +} + +impl<'a> TuiLog<'a> { + pub fn resize(&self, h: usize) { + let mut inner = self.inner.lock().unwrap(); + + inner.lines.truncate(h); + } +} + +impl<'a> Write for TuiLog<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + use ansi_parser::{AnsiParser, AnsiSequence, Output}; + use tui::{ + style::{Color, Modifier}, + text::{Span, Spans}, + }; + + let line = String::from_utf8(buf.into()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let mut spans = Vec::new(); + let mut span = Span::raw(""); + + for out in line.ansi_parse() { + match out { + Output::TextBlock(text) => { + span.content = format!("{}{}", span.content.to_owned(), text).into() + }, + Output::Escape(seq) => { + if span.content.len() != 0 { + spans.push(span); + + span = Span::raw(""); + } + + match seq { + AnsiSequence::SetGraphicsMode(values) => { + const COLOR_TABLE: [Color; 8] = [ + Color::Black, + Color::Red, + Color::Green, + Color::Yellow, + Color::Blue, + Color::Magenta, + Color::Cyan, + Color::White, + ]; + + let mut iter = values.iter(); + + match iter.next().unwrap() { + 0 => {}, + 1 => span.style.add_modifier = Modifier::BOLD, + 2 => span.style.add_modifier = Modifier::DIM, + idx @ 30..=37 => { + span.style.fg = Some(COLOR_TABLE[(idx - 30) as usize]) + }, + _ => warn!("Unknown color {:#?}", values), + } + }, + _ => warn!("Unknown sequence {:#?}", seq), + } + }, + } + } + + if span.content.len() != 0 { + spans.push(span); + } + + self.inner.lock().unwrap().lines.push(Spans(spans)); + + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 8047372312..70afd430fe 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -17,7 +17,7 @@ extern crate diesel; use diesel::{connection::SimpleConnection, prelude::*}; use diesel_migrations::embed_migrations; use std::{env, fs, path::PathBuf}; -use tracing::warn; +use tracing::{info, warn}; // See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html // This macro is called at build-time, and produces the necessary migration info @@ -27,16 +27,28 @@ use tracing::warn; // when needed. embed_migrations!(); +struct TracingOut; + +impl std::io::Write for TracingOut { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + info!("{}", String::from_utf8_lossy(buf)); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } +} + /// Runs any pending database migrations. This is executed during server startup pub fn run_migrations(db_dir: &str) -> Result<(), diesel_migrations::RunMigrationsError> { let db_dir = &apply_saves_dir_override(db_dir); let _ = fs::create_dir(format!("{}/", db_dir)); + embedded_migrations::run_with_output( &establish_connection(db_dir).expect( "If we cannot execute migrations, we should not be allowed to launch the server, so \ we don't populate it with bad data.", ), - &mut std::io::stdout(), + &mut std::io::LineWriter::new(TracingOut), ) }