mirror of
https://github.com/Dummi26/mcdcbot.git
synced 2025-12-13 17:06:16 +01:00
initial commit
This commit is contained in:
8
minecraft_manager/Cargo.toml
Executable file
8
minecraft_manager/Cargo.toml
Executable 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
5
minecraft_manager/src/chat.rs
Executable file
@@ -0,0 +1,5 @@
|
||||
#[derive(Debug)]
|
||||
pub struct ChatMessage {
|
||||
pub author: String,
|
||||
pub message: String,
|
||||
}
|
||||
27
minecraft_manager/src/events.rs
Executable file
27
minecraft_manager/src/events.rs
Executable 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
295
minecraft_manager/src/lib.rs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
214
minecraft_manager/src/parse_line.rs
Executable file
214
minecraft_manager/src/parse_line.rs
Executable 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
37
minecraft_manager/src/tasks.rs
Executable 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
109
minecraft_manager/src/thread.rs
Executable 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
235
minecraft_manager/src/threaded.rs
Executable 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)"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user