initial commit

This commit is contained in:
Mark
2023-11-23 20:48:52 +01:00
commit b2fefc92ee
19 changed files with 1471 additions and 0 deletions

8
minecraft_manager/Cargo.toml Executable file
View File

@@ -0,0 +1,8 @@
[package]
name = "minecraft_manager"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

5
minecraft_manager/src/chat.rs Executable file
View File

@@ -0,0 +1,5 @@
#[derive(Debug)]
pub struct ChatMessage {
pub author: String,
pub message: String,
}

27
minecraft_manager/src/events.rs Executable file
View File

@@ -0,0 +1,27 @@
use crate::chat::ChatMessage;
#[derive(Debug)]
pub struct MinecraftServerEvent {
pub time: (),
pub event: MinecraftServerEventType,
}
#[derive(Debug)]
pub enum MinecraftServerEventType {
Warning(MinecraftServerWarning),
JoinLeave(JoinLeaveEvent),
ChatMessage(ChatMessage),
}
#[derive(Debug)]
pub enum MinecraftServerWarning {
/// The server process was spawned, but std{in,out,err} was not captured.
CouldNotGetServerProcessStdio,
CantWriteToStdin(std::io::Error),
}
#[derive(Debug)]
pub struct JoinLeaveEvent {
pub username: String,
pub joined: bool,
}

295
minecraft_manager/src/lib.rs Executable file
View File

@@ -0,0 +1,295 @@
pub mod chat;
pub mod events;
pub mod parse_line;
pub mod tasks;
pub mod thread;
pub mod threaded;
use std::{
fmt::Display,
io::BufReader,
process::{Child, ChildStdin, ChildStdout, Command},
sync::{Arc, Mutex},
};
use thread::MinecraftServerThread;
#[derive(Clone)]
pub struct MinecraftServerSettings {
pub server_type: MinecraftServerType,
pub directory: String,
pub executable: String,
/// the amount of dedicated wam for the JVM in [TODO!] (-Xm{s,x}...M)
pub dedicated_wam: u32,
pub java_cmd: Option<String>,
}
impl Display for MinecraftServerSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} @ {} :: {} @ {}MB",
self.server_type, self.directory, self.executable, self.dedicated_wam
)
}
}
impl MinecraftServerSettings {
/// takes lines from the provided iterator until an empty line is reached (line.trim().is_empty()) or the iterator ends.
/// Note: The iterator items should NOT contain newline characters!
pub fn from_lines<'a, L: Iterator<Item = &'a str>>(
lines: &mut L,
) -> Result<Self, MinecraftServerSettingsFromLinesError> {
let mut server_type = Err(MinecraftServerSettingsFromLinesError::MissingServerType);
let mut directory = Err(MinecraftServerSettingsFromLinesError::MissingDirectory);
let mut executable = Err(MinecraftServerSettingsFromLinesError::MissingExecutable);
let mut ram = None;
let mut java_cmd = None;
let mut extra_line = None;
loop {
if let Some(line) = if let Some(l) = extra_line.take() {
Some(l)
} else {
lines.next()
} {
if let Some((key, value)) = line.split_once('=') {
match key {
"type" => {
server_type = Ok(match value.trim() {
"vanilla-mojang" => MinecraftServerType::VanillaMojang,
"vanilla-papermc" => MinecraftServerType::VanillaPaperMC,
"custom" => {
let mut name = Err(MinecraftServerSettingsFromLinesError::CustomServerTypeMissingName);
let mut line_parser = Err(MinecraftServerSettingsFromLinesError::CustomServerTypeMissingLineParser);
let mut command_override = None;
loop {
if let Some(line) = lines.next() {
if let Some(c) = line.chars().next() {
if c.is_whitespace() {
if let Some((key, val)) =
line.trim_start().split_once('=')
{
match key {
"name" => name = Ok(val.to_owned()),
"parser" => line_parser = Ok(val.to_owned()),
"command-override" => command_override = Some(val.to_owned()),
_ =>
return Err(MinecraftServerSettingsFromLinesError::CustomTypeUnknownKey(
key.to_owned()
)),
}
} else {
return Err(MinecraftServerSettingsFromLinesError::CustomTypeUnknownKey(
line.trim_start().to_owned()
));
}
} else {
extra_line = Some(line);
break;
}
}
} else {
break;
}
}
MinecraftServerType::Custom {
name: name?,
line_parser: line_parser?,
line_parser_proc: Arc::new(Mutex::new(None)),
command_override,
}
}
other => {
return Err(
MinecraftServerSettingsFromLinesError::UnknownServerType(
other.to_owned(),
),
)
}
});
}
"dir" => directory = Ok(value.to_owned()),
"exec" => executable = Ok(value.to_owned()),
"ram" => {
if let Ok(v) = value.trim().parse() {
ram = Some(v);
} else {
return Err(MinecraftServerSettingsFromLinesError::RamNotAnInt(
value.to_owned(),
));
}
}
"java_cmd" => java_cmd = Some(value.to_owned()),
k => {
return Err(MinecraftServerSettingsFromLinesError::UnknownKey(
k.to_owned(),
))
}
}
} else if line.trim().is_empty() {
break;
} else {
return Err(MinecraftServerSettingsFromLinesError::UnknownKey(
line.to_owned(),
));
}
} else {
break;
}
}
let mut o = Self::new(server_type?, directory?, executable?);
if let Some(ram) = ram {
o = o.with_ram(ram);
}
if let Some(java_cmd) = java_cmd {
o = o.with_java_cmd(Some(java_cmd));
}
Ok(o)
}
}
#[derive(Debug)]
pub enum MinecraftServerSettingsFromLinesError {
UnknownKey(String),
MissingServerType,
UnknownServerType(String),
MissingDirectory,
MissingExecutable,
RamNotAnInt(String),
CustomTypeUnknownKey(String),
CustomServerTypeMissingName,
CustomServerTypeMissingLineParser,
}
impl MinecraftServerSettings {
pub fn spawn(self) -> MinecraftServerThread {
MinecraftServerThread::start(self)
}
pub fn new(server_type: MinecraftServerType, directory: String, executable: String) -> Self {
Self {
server_type,
directory,
executable,
dedicated_wam: 1024,
java_cmd: None,
}
}
pub fn with_ram(mut self, ram_mb: u32) -> Self {
self.dedicated_wam = ram_mb;
self
}
pub fn with_java_cmd(mut self, java_cmd: Option<String>) -> Self {
self.java_cmd = java_cmd;
self
}
pub fn get_command(&self) -> Command {
let mut cmd = Command::new(if let Some(c) = &self.java_cmd {
c.as_str()
} else {
match &self.server_type {
MinecraftServerType::VanillaMojang => "java", // "/usr/lib/jvm/openjdk17/bin/java",
MinecraftServerType::VanillaPaperMC => "java", // "/usr/lib/jvm/openjdk17/bin/java",
MinecraftServerType::Custom {
command_override, ..
} => {
if let Some(cmd) = command_override {
cmd
} else {
"java"
}
}
}
});
cmd.current_dir(&self.directory);
// match &self.server_type {
// MinecraftServerType::VanillaMojang | MinecraftServerType::VanillaPaperMC =>
cmd.args([
format!("-Xms{}M", self.dedicated_wam),
format!("-Xmx{}M", self.dedicated_wam),
"-Dsun.stdout.encoding=UTF-8".to_owned(),
"-Dsun.stderr.encoding=UTF-8".to_owned(),
"-DFile.Encoding=UTF-8".to_owned(),
"-jar".to_string(),
self.executable.to_string(),
"nogui".to_string(),
]);
cmd
}
}
#[derive(Clone)]
pub enum MinecraftServerType {
VanillaMojang,
VanillaPaperMC,
Custom {
/// your custom server type's name
name: String,
/// each time a line is received from the mc server's stdout, it is sent to this programs stdin.
/// if the program has terminated, it is started again.
/// for best performance, the program should read stdin lines in a loop and never exit
line_parser: String,
line_parser_proc: Arc<Mutex<Option<(Child, ChildStdin, BufReader<ChildStdout>)>>>,
/// instead of running java -jar [...], use this to run a shell script which then starts the server.
/// things like ram etc will be ignored if this is used.
command_override: Option<String>,
},
}
impl Display for MinecraftServerType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::VanillaMojang => write!(f, "vanilla-mojang"),
Self::VanillaPaperMC => write!(f, "vanilla-papermc"),
Self::Custom {
name: identifier, ..
} => write!(f, "custom ({identifier})"),
}
}
}
pub fn test() {
// create minecraft server config
let minecraft_server_settings = MinecraftServerSettings {
server_type: MinecraftServerType::VanillaPaperMC,
directory: "/home/mark/Dokumente/minecraft_server/1".to_string(),
executable: "paper-1.19-81.jar".to_string(),
dedicated_wam: 1024,
java_cmd: None,
};
// start server
let mut thread = minecraft_server_settings.spawn();
// handle stdin
if false {
let sender = thread.clone_task_sender();
std::thread::spawn(move || {
let stdin = std::io::stdin();
loop {
let mut line = String::new();
if let Ok(_) = stdin.read_line(&mut line) {
if line.trim().is_empty() {
std::thread::sleep(std::time::Duration::from_secs(300));
continue;
}
if let Err(_) = sender.send_task(tasks::MinecraftServerTask::RunCommand(line)) {
break;
}
} else {
break;
}
}
});
}
// handle stdout
loop {
if !thread.is_finished() {
thread.update();
for event in thread.handle_new_events() {
eprintln!("Event: {event:?}");
}
std::thread::sleep(std::time::Duration::from_millis(100));
} else {
if let Ok(stop_reason) = thread.get_stop_reason() {
eprintln!("Thread stopped: {stop_reason}");
}
break;
}
}
}

View File

@@ -0,0 +1,214 @@
use std::{
io::{BufRead, BufReader, Write},
process::{self, Stdio},
};
use crate::{
chat::ChatMessage,
events::{self, MinecraftServerEventType},
MinecraftServerSettings, MinecraftServerType,
};
pub enum ParseOutput {
Nothing,
Error(ParseError),
Event(MinecraftServerEventType),
}
pub enum ParseError {
/// any other errors (for custom line parser implementations)
Custom(String),
}
pub fn parse_line(line: &str, settings: &MinecraftServerSettings) -> ParseOutput {
if line.trim().is_empty() {
return ParseOutput::Nothing;
}
match &settings.server_type {
MinecraftServerType::Custom {
line_parser,
line_parser_proc,
..
} => {
let mut proc = line_parser_proc.lock().unwrap();
let proc = &mut *proc;
let make_new_proc = if let Some((proc, _, _)) = proc {
if let Ok(Some(_)) = proc.try_wait() {
// has exited
true
} else {
false
}
} else {
true
};
if make_new_proc {
if let Ok(mut new_proc) = process::Command::new(line_parser)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
if let (Some(stdin), Some(stdout)) =
(new_proc.stdin.take(), new_proc.stdout.take())
{
*proc = Some((new_proc, stdin, BufReader::new(stdout)));
} else {
eprintln!("[WARN/CUSTOM-LINE-PARSER] No stdin/stdout handles!");
_ = new_proc.kill();
}
} else {
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't spawn command '{line_parser}'!");
}
}
if let Some((_proc, stdin, stdout)) = proc {
if let Err(e) = writeln!(stdin, "{line}") {
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't write to stdin: {e:?}");
return ParseOutput::Nothing;
};
let mut buf = String::new();
if let Err(e) = stdout.read_line(&mut buf) {
eprintln!("[WARN/CUSTOM-LINE-PARSER] Can't read_line: {e:?}");
return ParseOutput::Nothing;
};
if buf.ends_with('\n') || buf.ends_with('\r') {
buf.pop();
}
if buf.len() > 0 {
match buf.as_bytes()[0] {
b'c' => {
ParseOutput::Event(MinecraftServerEventType::ChatMessage(ChatMessage {
author: buf[1..].to_owned(),
message: {
let mut o = String::new();
if let Err(e) = stdout.read_line(&mut o) {
eprintln!(
"[WARN/CUSTOM-LINE-PARSER] Can't read_line: {e:?}"
);
return ParseOutput::Nothing;
}
o
},
}))
}
b'j' => ParseOutput::Event(MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: buf[1..].to_owned(),
joined: true,
},
)),
b'l' => ParseOutput::Event(MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: buf[1..].to_owned(),
joined: false,
},
)),
b'e' => ParseOutput::Error({
if buf.len() > 1 {
match buf.as_bytes()[1] {
b'c' => ParseError::Custom(buf[2..].to_string()),
_ => ParseError::Custom(String::new()),
}
} else {
ParseError::Custom(String::new())
}
}),
_ => ParseOutput::Nothing,
}
} else {
ParseOutput::Nothing
}
} else {
eprintln!("[WARN/CUSTOM-LINE-PARSER] No process!");
ParseOutput::Nothing
}
}
MinecraftServerType::VanillaMojang => {
if let Some((_time, rest)) = line[1..].split_once("] [Server thread/INFO]: ") {
let rest = rest.trim();
if rest.starts_with("<") {
if let Some((user, msg)) = rest[1..].split_once("> ") {
return ParseOutput::Event(MinecraftServerEventType::ChatMessage(
ChatMessage {
author: user.to_owned(),
message: msg.to_owned(),
},
));
}
} else if rest.ends_with(" joined the game") {
return ParseOutput::Event(MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: rest[0..rest.len() - " joined the game".len()].to_owned(),
joined: true,
},
));
} else if rest.ends_with(" left the game") {
return ParseOutput::Event(MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: rest[0..rest.len() - " left the game".len()].to_owned(),
joined: false,
},
));
}
}
ParseOutput::Nothing
// Vanilla servers not yet supported...
}
MinecraftServerType::VanillaPaperMC => {
match line.chars().next() {
Some('[') => {
if let Some((_time, rest)) = line[1..].split_once(' ') {
if let Some((severity, rest)) = rest.split_once(']') {
if rest.starts_with(": ") {
let rest = &rest[2..];
// eprintln!("Time: '{time}', Severity: '{severity}', Rest: '{rest}'.");
match severity {
"INFO" => {
if let Some('<') = rest.chars().next() {
if let Some((username, message)) =
rest[1..].split_once('>')
{
return ParseOutput::Event(
MinecraftServerEventType::ChatMessage(
ChatMessage {
author: username.to_string(),
message: message[1..].to_string(),
},
),
);
}
} // join/leave
if rest.trim_end().ends_with(" joined the game") {
let username = &rest[..rest.len() - 16];
return ParseOutput::Event(
MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: username.to_string(),
joined: true,
},
),
);
}
if rest.trim_end().ends_with(" left the game") {
let username = &rest[..rest.len() - 14];
return ParseOutput::Event(
MinecraftServerEventType::JoinLeave(
events::JoinLeaveEvent {
username: username.to_string(),
joined: false,
},
),
);
}
}
_ => (),
}
}
}
}
}
_ => (),
}
ParseOutput::Nothing
}
}
}

37
minecraft_manager/src/tasks.rs Executable file
View File

@@ -0,0 +1,37 @@
use std::sync::mpsc;
#[derive(Clone, Debug)]
pub enum MinecraftServerTask {
Stop,
Kill,
RunCommand(String),
}
impl MinecraftServerTask {
pub fn generate_callback(
self,
) -> (
(Self, mpsc::Sender<Result<u8, String>>),
MinecraftServerTaskCallback,
) {
let (sender, update_receiver) = mpsc::channel();
(
(self, sender),
MinecraftServerTaskCallback::new(update_receiver),
)
}
}
pub struct MinecraftServerTaskCallback {
/// Ok(n) if n < 100 = progress in %
/// Ok(100) = finished
/// Ok(n) if n > 100 = task ended with non-standard exit status (advise checking log)
/// Err(_) = custom message (for log)
pub recv: mpsc::Receiver<Result<u8, String>>, // TODO: NOT PUBLIC
}
impl MinecraftServerTaskCallback {
pub fn new(recv: mpsc::Receiver<Result<u8, String>>) -> Self {
Self { recv }
}
}

109
minecraft_manager/src/thread.rs Executable file
View File

@@ -0,0 +1,109 @@
use std::thread::JoinHandle;
use crate::tasks::MinecraftServerTaskCallback;
use {
crate::{
events::MinecraftServerEvent,
tasks::MinecraftServerTask,
threaded::{self, MinecraftServerStopReason},
MinecraftServerSettings,
},
std::{collections::VecDeque, sync::mpsc},
};
pub struct MinecraftServerThread {
events: ThreadData<MinecraftServerEvent>,
task_sender: MinecraftServerTaskSender,
join_handle: JoinHandle<MinecraftServerStopReason>,
}
/// A clonable type allowing multiple threads to send tasks to the server.
#[derive(Clone)]
pub struct MinecraftServerTaskSender(
mpsc::Sender<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>,
);
impl MinecraftServerTaskSender {
pub fn send_task(&self, task: MinecraftServerTask) -> Result<MinecraftServerTaskCallback, ()> {
let (sendable, callback) = task.generate_callback();
if let Ok(_) = self.0.send(sendable) {
Ok(callback)
} else {
Err(())
}
}
}
impl MinecraftServerThread {
pub fn start(settings: MinecraftServerSettings) -> Self {
let (task_sender, event_receiver, join_handle) = threaded::run(settings);
Self {
events: ThreadData::new(event_receiver, 100),
task_sender: MinecraftServerTaskSender(task_sender),
join_handle,
}
}
pub fn is_finished(&self) -> bool {
self.join_handle.is_finished()
}
pub fn get_stop_reason(self) -> Result<MinecraftServerStopReason, ()> {
if self.is_finished() {
if let Ok(v) = self.join_handle.join() {
Ok(v)
} else {
Err(())
}
} else {
Err(())
}
}
pub fn update(&mut self) {
self.events.update();
}
pub fn handle_new_events(
&mut self,
) -> std::iter::Skip<std::collections::vec_deque::Iter<MinecraftServerEvent>> {
self.events.handle_all()
}
pub fn clone_task_sender(&self) -> MinecraftServerTaskSender {
self.task_sender.clone()
}
}
struct ThreadData<T> {
mpsc: mpsc::Receiver<T>,
buffer: VecDeque<T>,
unhandeled: usize,
capacity: usize,
}
impl<T> ThreadData<T> {
pub fn new(mpsc_receiver: mpsc::Receiver<T>, capacity: usize) -> Self {
Self {
mpsc: mpsc_receiver,
buffer: VecDeque::with_capacity(capacity),
unhandeled: 0,
capacity,
}
}
pub fn update(&mut self) -> usize {
let mut unhandeled = 0;
while let Ok(new_content) = self.mpsc.try_recv() {
if self.buffer.len() == self.capacity {
self.buffer.pop_front();
}
self.buffer.push_back(new_content);
unhandeled += 1;
}
self.unhandeled += unhandeled;
unhandeled
}
pub fn handle_all(&mut self) -> std::iter::Skip<std::collections::vec_deque::Iter<T>> {
let unhandeled = self.unhandeled;
self.unhandeled = 0;
self.buffer
.iter()
.skip(self.buffer.len().saturating_sub(unhandeled))
}
}

235
minecraft_manager/src/threaded.rs Executable file
View File

@@ -0,0 +1,235 @@
use std::{
fmt::Display,
io::{BufRead, BufReader, Write},
process::{ExitStatus, Stdio},
};
use crate::{
parse_line::{parse_line, ParseOutput},
MinecraftServerType,
};
use {
crate::tasks::MinecraftServerTask,
crate::{
events::{self as MinecraftServerEvents, MinecraftServerEvent, MinecraftServerEventType},
MinecraftServerSettings,
},
std::sync::mpsc,
};
pub fn run(
settings: MinecraftServerSettings,
) -> (
mpsc::Sender<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>,
mpsc::Receiver<MinecraftServerEvent>,
std::thread::JoinHandle<MinecraftServerStopReason>,
) {
let (return_task_sender, tasks) =
mpsc::channel::<(MinecraftServerTask, mpsc::Sender<Result<u8, String>>)>();
let (events, return_events_receiver) = mpsc::channel();
// thread
let join_handle = std::thread::spawn(move || {
let mut command = settings.get_command();
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
eprintln!("Spawning {command:?}");
match command.spawn() {
Ok(mut process) => {
if let (Some(mut stdin), Some(stdout), Some(mut _stderr)) = (
process.stdin.take(),
process.stdout.take(),
process.stderr.take(),
) {
let stdout_lines = {
// the stdout reading thread
let (lines, stdout_lines) = mpsc::channel();
std::thread::spawn(move || {
let mut stdout = BufReader::new(stdout);
let mut line = String::new();
loop {
line.clear();
match stdout.read_line(&mut line) {
Ok(_) if !line.trim().is_empty() => {
eprintln!("> {}", line.trim());
match lines.send(line.trim().to_owned()) {
Ok(_) => (),
Err(_) => return,
}
}
Ok(0) => {
eprintln!(
" [ Stdout read thread ] Reached EOF, stopping."
);
return;
}
Ok(_) => {} // empty line, but read newline char - ignore
Err(e) => {
eprintln!(
" [ Stdout read thread ] Read error, stopping. ({e:?})"
);
return;
}
}
}
});
stdout_lines
};
loop {
while let Ok(task) = tasks.try_recv() {
eprintln!("[GOT TASK] {task:?}");
// iterate over all new tasks
match task.0 {
MinecraftServerTask::Stop => match writeln!(stdin, "stop") {
Ok(_) => {
task.1.send(Ok(0));
while let Ok(None) = process.try_wait() {
std::thread::sleep(std::time::Duration::from_millis(
250,
));
}
task.1.send(Ok(100));
}
Err(e) => {
events.send(MinecraftServerEvent {
time: (),
event: MinecraftServerEventType::Warning(
MinecraftServerEvents::MinecraftServerWarning::CantWriteToStdin(e),
),
});
}
},
MinecraftServerTask::Kill => {
process.kill();
task.1.send(Ok(100));
return MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::KilledDueToTask,
};
}
MinecraftServerTask::RunCommand(command) => {
match writeln!(
stdin,
"{}",
command.replace("\n", "\\n").replace("\r", "\\r")
) {
Ok(_) => task.1.send(Ok(100)),
Err(_) => task.1.send(Ok(101)),
};
}
}
}
while let Ok(line) = stdout_lines.try_recv() {
// iterate over all new lines from stdout
// eprintln!(" [ server manager thread ] Found line '{}'", line);
match parse_line(&line, &settings) {
ParseOutput::Event(event) => {
events.send(MinecraftServerEvent { time: (), event });
}
ParseOutput::Error(_) => (),
ParseOutput::Nothing => (),
}
}
// stop the loop once the process exits
match process.try_wait() {
Ok(None) => (),
Ok(Some(exit_status)) => {
if let MinecraftServerType::Custom {
line_parser_proc, ..
} = &settings.server_type
{
if let Some(proc) = &mut *line_parser_proc.lock().unwrap() {
_ = proc.0.kill();
}
}
return MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::ProcessEnded(exit_status),
};
}
Err(e) => {
return MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::ProcessCouldNotBeAwaited(e),
}
}
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
} else {
eprintln!("No stdin/out!");
events.send(MinecraftServerEvent {
time: (),
event: MinecraftServerEventType::Warning(
MinecraftServerEvents::MinecraftServerWarning::CouldNotGetServerProcessStdio,
),
});
match process.wait() {
Ok(status) => MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::ProcessEnded(status),
},
Err(e) => MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::ProcessCouldNotBeAwaited(e),
},
}
}
}
Err(e) => {
eprintln!("Couldn't spawn server process: {e:?}");
MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons::ProcessCouldNotBeSpawned(e),
}
}
}
});
// return the mpsc channel parts
(return_task_sender, return_events_receiver, join_handle)
}
pub struct MinecraftServerStopReason {
time: (),
reason: MinecraftServerStopReasons,
}
impl Display for MinecraftServerStopReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.reason)
}
}
pub enum MinecraftServerStopReasons {
KilledDueToTask,
ProcessEnded(ExitStatus),
ProcessCouldNotBeSpawned(std::io::Error),
ProcessCouldNotBeAwaited(std::io::Error),
}
impl Display for MinecraftServerStopReasons {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::KilledDueToTask => write!(f, "killed (due to task)"),
Self::ProcessEnded(exit_status) => {
if let Some(s) = exit_status.code() {
if s == 0 {
write!(f, "Stopped")
} else {
write!(f, "Stopped (Exited with status {s})!")
}
} else {
write!(f, "Stopped!")
}
}
Self::ProcessCouldNotBeSpawned(_e) => {
write!(f, "Couldn't spawn process (check your paths!)")
}
Self::ProcessCouldNotBeAwaited(_e) => write!(
f,
"Couldn't wait for process to end (check console/log for errors)"
),
}
}
}