Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mc"
version = "0.10.2"
version = "0.11.0"
edition = "2024"
license = "MIT"

Expand Down
20 changes: 20 additions & 0 deletions docs/src/commands/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ is hidden unless `--server-logs` is passed; it is always written to
`instance/logs/` by the server itself. The console is not interactive; use
the remote console (RCON) for live administration.

mc reads the console output whether or not it is shown, and prints a status
line for the events it recognizes: a player joining or leaving the instance,
and a world save flushed to disk with the time it took. These are shown
with `--verbose`. A player dropped for any reason other than quitting or
the instance stopping, such as a timeout or a kick, is reported as a warning
with the reason in place of the leave line. A login turned away is reported
as a warning naming the cause: the allow list, a ban, a full instance, or a
taken name. Warnings are shown by default and hidden with `--quiet`.

```text
Joined Notch
Saved all dimensions flushed to disk in 1.2s
warning: connection refused for Notch, the user is not in the allow list
```

With `--server-logs`, the console lines shown follow mc's verbosity:
warnings and errors by default, more with `--verbose`. When the
`jvm-arguments` in the manifest select a logging configuration file of their
own, no events are recognized.

With a `tunnel` section, the tunnel agent starts beside the instance and
is restarted if it stops on its own; the public address is printed at
startup. The agent's output is hidden unless `--tunnel-logs` is passed, and
Expand Down
2 changes: 1 addition & 1 deletion docs/src/guides/systemd.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ journalctl -u myserver -f
```

mc's own log verbosity is controlled by the global `--verbose` flag; the
instance's console log level follows it.
instance's console lines shown in the journal follow it.
4 changes: 0 additions & 4 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/cli/styles.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anstyle::Ansi256Color;
use anstyle::AnsiColor;
use anstyle::Effects;
use anstyle::Style;
Expand All @@ -11,6 +12,11 @@ pub const ERROR: Style = annotate_snippets::renderer::DEFAULT_ERROR_STYLE;
pub const WARN: Style = annotate_snippets::renderer::DEFAULT_WARNING_STYLE;
pub const VALID: Style = AnsiColor::BrightCyan.on_default().effects(Effects::BOLD);
pub const INVALID: Style = annotate_snippets::renderer::DEFAULT_WARNING_STYLE;
pub const MINECRAFT: Style = Ansi256Color(28).on_default().effects(Effects::BOLD);
pub const TUNNEL: Style = Ansi256Color(208).on_default().effects(Effects::BOLD);
pub const LOG_INFO: Style = AnsiColor::Green.on_default();
pub const LOG_DEBUG: Style = AnsiColor::Cyan.on_default();
pub const LOG_TRACE: Style = Style::new().effects(Effects::DIMMED);

pub fn styles() -> Styles {
Styles::styled()
Expand Down
189 changes: 189 additions & 0 deletions src/minecraft/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use std::fmt;

/// Ordered from most to least severe, so `level <= threshold` means the
/// line is at least as important as the threshold.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ServerLogLevel {
Fatal,
Error,
Warn,
Info,
Debug,
Trace
}

impl ServerLogLevel {
fn parse(level: &str) -> Option<Self> {
match level {
"FATAL" => Some(ServerLogLevel::Fatal),
"ERROR" => Some(ServerLogLevel::Error),
"WARN" => Some(ServerLogLevel::Warn),
"INFO" => Some(ServerLogLevel::Info),
"DEBUG" => Some(ServerLogLevel::Debug),
"TRACE" => Some(ServerLogLevel::Trace),
_ => None
}
}
}

/// A console line, either printed with the pattern mc writes into
/// `log4j2.xml` (`[LEVEL] [thread]: message`) or by the JVM itself outside
/// the logger (`WARNING: message`, `ERROR: message`), which has no thread.
pub struct ServerLogLine<'a> {
pub level: ServerLogLevel,
pub thread: Option<&'a str>,
pub message: &'a str
}

impl<'a> ServerLogLine<'a> {
pub fn parse(line: &'a str) -> Option<Self> {
Self::parse_logger(line).or_else(|| Self::parse_jvm(line))
}

fn parse_logger(line: &'a str) -> Option<Self> {
let rest = line.strip_prefix('[')?;
let (level, rest) = rest.split_once("] [")?;
let (thread, message) = rest.split_once("]: ")?;

Some(ServerLogLine {
level: ServerLogLevel::parse(level)?,
thread: Some(thread),
message
})
}

fn parse_jvm(line: &'a str) -> Option<Self> {
let (level, message) = if let Some(message) = line.strip_prefix("WARNING: ") {
(ServerLogLevel::Warn, message)
} else if let Some(message) = line.strip_prefix("ERROR: ") {
(ServerLogLevel::Error, message)
} else {
return None;
};

Some(ServerLogLine {
level,
thread: None,
message
})
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum RefusalReason {
NotInAllowList,
Banned,
AddressBanned,
Full,
NameTaken,
Other(String)
}

impl RefusalReason {
fn parse(reason: &str) -> Self {
match reason.trim_end_matches(['!', '.']) {
"You are not white-listed on this server" => RefusalReason::NotInAllowList,
"You are banned from this server" => RefusalReason::Banned,
"Your IP address is banned from this server"
| "You have been IP banned from this server" => RefusalReason::AddressBanned,
"The server is full" => RefusalReason::Full,
"That name is already taken" => RefusalReason::NameTaken,
_ => RefusalReason::Other(reason.to_string())
}
}

pub fn raw(&self) -> Option<&str> {
match self {
RefusalReason::Other(reason) => Some(reason),
_ => None
}
}
}

impl fmt::Display for RefusalReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RefusalReason::NotInAllowList => write!(f, "the user is not in the allow list"),
RefusalReason::Banned => write!(f, "the user is banned"),
RefusalReason::AddressBanned => write!(f, "the address is banned"),
RefusalReason::Full => write!(f, "the instance is full"),
RefusalReason::NameTaken => write!(f, "the name is already taken"),
RefusalReason::Other(reason) => write!(f, "{}", reason)
}
}
}

#[derive(Debug, PartialEq, Eq)]
pub enum ServerLogEvent {
Joined(String),
Left(String),
Disconnected {
name: String,
reason: String
},
/// A connection dropped while the player was still logging in, which is
/// how a login turned away by the server is reported.
Refused {
name: String,
address: String,
reason: RefusalReason
},
SaveStarted,
SaveCompleted
}

impl ServerLogEvent {
pub fn recognize(line: &ServerLogLine<'_>) -> Option<Self> {
if line.level != ServerLogLevel::Info || line.thread != Some("Server thread") {
return None;
}

if let Some(name) = line.message.strip_suffix(" joined the game") {
return player_name(name).map(ServerLogEvent::Joined);
}

if let Some(name) = line.message.strip_suffix(" left the game") {
return player_name(name).map(ServerLogEvent::Left);
}

if let Some((subject, reason)) = line.message.split_once(" lost connection: ") {
let login = subject
.strip_suffix(')')
.and_then(|subject| subject.split_once(" (/"));

return match login {
Some((name, address)) => player_name(name).map(|name| ServerLogEvent::Refused {
name,
address: address.to_string(),
reason: RefusalReason::parse(reason)
}),
None => player_name(subject).map(|name| ServerLogEvent::Disconnected {
name,
reason: reason.to_string()
})
};
}

if line.message.starts_with("Saving chunks for level '") {
return Some(ServerLogEvent::SaveStarted);
}

if line.message == "ThreadedAnvilChunkStorage: All dimensions are saved" {
return Some(ServerLogEvent::SaveCompleted);
}

None
}
}

// Chat, `/say`, and `/me` all put a space or angle brackets before the
// player-controlled text, so a bare name is the only shape accepted.
fn player_name(name: &str) -> Option<String> {
let valid = !name.is_empty()
&& name.len() <= 16
&& name
.chars()
.all(|c| c.is_ascii_graphic() && !matches!(c, '<' | '>'));

valid.then(|| name.to_string())
}
18 changes: 9 additions & 9 deletions src/minecraft/log4j.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const TEMPLATE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
<PatternLayout pattern="[%level] [%t]: %msg{nolookups}%n" />
</Console>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
Expand All @@ -13,24 +13,24 @@ const TEMPLATE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="{root_level}">
<Root level="{level}">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
</filters>
<AppenderRef ref="SysOut" level="{console_level}" />
<AppenderRef ref="SysOut" level="{level}" />
<AppenderRef ref="File" level="info" />
</Root>
</Loggers>
</Configuration>
"#;

pub fn configuration(console_level: &str) -> String {
let root_level = match console_level {
"debug" | "trace" => console_level,
// The console never drops below info: mc reads it to recognize events, and
// filters what it echoes against its own verbosity.
pub fn configuration(level: &str) -> String {
let level = match level {
"debug" | "trace" => level,
_ => "info"
};

TEMPLATE
.replace("{root_level}", root_level)
.replace("{console_level}", console_level)
TEMPLATE.replace("{level}", level)
}
1 change: 1 addition & 0 deletions src/minecraft/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::Serialize;
use serde::Serializer;

pub mod eula;
pub mod log;
pub mod log4j;
pub mod players;
pub mod seed;
Expand Down
Loading
Loading