mirror of
https://github.com/Dummi26/mcdcbot.git
synced 2025-03-09 20:43:54 +01:00
initial commit
This commit is contained in:
commit
b2fefc92ee
2
.gitignore
vendored
Executable file
2
.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*/Cargo.lock
|
||||
*/target/
|
30
README.md
Executable file
30
README.md
Executable file
@ -0,0 +1,30 @@
|
||||
# mcdcbot
|
||||
|
||||
- shut down / start your minecraft server from discord
|
||||
- switch between multiple different worlds (/servers) from discord
|
||||
- forward messages from a certain discord channel to the minecraft chat, and minecraft chat messages to that discord channel
|
||||
|
||||
## usage
|
||||
|
||||
See the repository for examples, notable `mcdcbot/servers/*` and `mcdcbot/settings.txt`.
|
||||
|
||||
For advanced config options, check `minecraft_manager/src/lib.rs`, especially the `fn from_lines()`.
|
||||
|
||||
Documentation may be added in the future...
|
||||
|
||||
### In Discord:
|
||||
|
||||
`/list` lists Servers:
|
||||
|
||||
- (m) My Server
|
||||
- (t) Test World
|
||||
|
||||
`/starts` starts a server. You can choose which one to start.
|
||||
Only one server can be running at any given time.
|
||||
|
||||
`/start m` or `/start My Server`
|
||||
|
||||
`/start t` or `/start Test World`
|
||||
|
||||
`/stop` runs the `stop` command in the current server.
|
||||
Once the server shuts down, a message will be sent.
|
12
mcdcbot/Cargo.toml
Executable file
12
mcdcbot/Cargo.toml
Executable file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mcdcbot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
minecraft_manager = { path = "../minecraft_manager" }
|
||||
poise = "0.5.7"
|
||||
reqwest = "0.11.22"
|
||||
tokio = { version = "1.34.0", default-features = false }
|
1
mcdcbot/my_token.txt
Executable file
1
mcdcbot/my_token.txt
Executable file
@ -0,0 +1 @@
|
||||
MTE3NzE5NzAwNDgyOTUxMTcyMA.GShE0Z.EmJ9dyHeaH3-NUsjHwiu5icvIojDn6k6f3hU30
|
4
mcdcbot/servers/MyTest
Executable file
4
mcdcbot/servers/MyTest
Executable file
@ -0,0 +1,4 @@
|
||||
type=vanilla-mojang
|
||||
dir=/markone/temp/mc/MyTest
|
||||
exec=server.jar
|
||||
ram=2048
|
6
mcdcbot/settings.txt
Executable file
6
mcdcbot/settings.txt
Executable file
@ -0,0 +1,6 @@
|
||||
channel_id_info=1177200748648464464
|
||||
channel_id_chat=1177200760480612392
|
||||
send_join_and_leave_messages=true
|
||||
send_start_stop_messages_in_chat=true
|
||||
get_my_ip_url1=https://ipinfo.io/ip
|
||||
get_my_ip_url2=https://ipecho.net/plain
|
25
mcdcbot/src/data.rs
Executable file
25
mcdcbot/src/data.rs
Executable file
@ -0,0 +1,25 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use minecraft_manager::{thread::MinecraftServerThread, MinecraftServerSettings};
|
||||
use poise::futures_util::lock::Mutex;
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub struct Data {
|
||||
pub settings: Mutex<Settings>,
|
||||
pub servers: Mutex<Vec<Arc<Mutex<MinecraftServer>>>>,
|
||||
pub current: Arc<
|
||||
Mutex<
|
||||
Option<(
|
||||
Arc<Mutex<MinecraftServer>>,
|
||||
Arc<Mutex<Option<MinecraftServerThread>>>,
|
||||
)>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
pub struct MinecraftServer {
|
||||
pub name: String,
|
||||
pub short: Option<String>,
|
||||
pub settings: MinecraftServerSettings,
|
||||
}
|
55
mcdcbot/src/embed.rs
Executable file
55
mcdcbot/src/embed.rs
Executable file
@ -0,0 +1,55 @@
|
||||
use minecraft_manager::{
|
||||
chat::ChatMessage, events::JoinLeaveEvent, threaded::MinecraftServerStopReason,
|
||||
};
|
||||
use poise::serenity_prelude::{json::json, json::Value};
|
||||
|
||||
pub fn chat_message(e: &ChatMessage) -> Value {
|
||||
json!({
|
||||
"embeds": [{
|
||||
"title": e.author,
|
||||
"description": e.message
|
||||
}]
|
||||
})
|
||||
}
|
||||
pub fn join_leave(e: &JoinLeaveEvent) -> Value {
|
||||
json!({
|
||||
"embeds": [{
|
||||
"description": if e.joined {
|
||||
format!("{} joined", e.username)
|
||||
} else {
|
||||
format!("{} left", e.username)
|
||||
},
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
pub fn server_started(name: &str, ip: Option<String>) -> Value {
|
||||
json!({
|
||||
"embeds": [{
|
||||
"color": 26880,
|
||||
"title": name,
|
||||
"description": if let Some(ip) = ip {
|
||||
format!("Server was started, IP: {ip}")
|
||||
} else {
|
||||
format!("Server was started")
|
||||
},
|
||||
}]
|
||||
})
|
||||
}
|
||||
pub fn server_stopped(reason: Option<MinecraftServerStopReason>) -> Value {
|
||||
if let Some(reason) = reason {
|
||||
json!({
|
||||
"embeds": [{
|
||||
"color": 6881280,
|
||||
"title": reason.to_string(),
|
||||
}]
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"embeds": [{
|
||||
"color": 6881280,
|
||||
"title": "Stopped.",
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
31
mcdcbot/src/getmyip.rs
Executable file
31
mcdcbot/src/getmyip.rs
Executable file
@ -0,0 +1,31 @@
|
||||
pub async fn get_my_ip(url1: &str, url2: &str) -> String {
|
||||
let ip1 = get(url1).await;
|
||||
let ip2 = get(url2).await;
|
||||
match (ip1, ip2) {
|
||||
(Some(ip1), Some(ip2)) => {
|
||||
if ip1 == ip2 {
|
||||
ip1.to_string()
|
||||
} else {
|
||||
format!("{ip1} / {ip2}")
|
||||
}
|
||||
}
|
||||
(Some(ip), None) | (None, Some(ip)) => ip.to_string(),
|
||||
(None, None) => format!("unknown"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get(url: &str) -> Option<String> {
|
||||
if url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
reqwest::get(url)
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?
|
||||
.trim()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
324
mcdcbot/src/main.rs
Executable file
324
mcdcbot/src/main.rs
Executable file
@ -0,0 +1,324 @@
|
||||
mod data;
|
||||
mod embed;
|
||||
mod getmyip;
|
||||
mod settings;
|
||||
|
||||
use std::{collections::HashSet, env, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{data::Data, settings::Settings};
|
||||
use minecraft_manager::{
|
||||
events::{MinecraftServerEventType, MinecraftServerWarning},
|
||||
tasks::MinecraftServerTask,
|
||||
thread::MinecraftServerThread,
|
||||
};
|
||||
use poise::{futures_util::lock::Mutex, serenity_prelude as serenity};
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
async fn list(ctx: Context<'_>) -> Result<(), Error> {
|
||||
ctx.say({
|
||||
let mut acc = format!("Available servers:");
|
||||
for server in ctx.data().servers.lock().await.iter() {
|
||||
let server = server.lock().await;
|
||||
acc.push_str("\n- ");
|
||||
if let Some(short) = &server.short {
|
||||
acc.push('(');
|
||||
acc.push_str(short);
|
||||
acc.push_str(") ");
|
||||
}
|
||||
acc.push_str(server.name.as_str());
|
||||
}
|
||||
acc
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
async fn start(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Server's name (see /list)"] srv: String,
|
||||
) -> Result<(), Error> {
|
||||
// find server by name
|
||||
let servers_lock = ctx.data().servers.lock().await;
|
||||
let mut matching_server = None;
|
||||
for server in servers_lock.iter() {
|
||||
let server_lock = server.lock().await;
|
||||
if server_lock
|
||||
.short
|
||||
.as_ref()
|
||||
.is_some_and(|short| short == &srv)
|
||||
{
|
||||
matching_server = Some(Arc::clone(server));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if matching_server.is_none() {
|
||||
for server in servers_lock.iter() {
|
||||
let server_lock = server.lock().await;
|
||||
if server_lock.name == srv {
|
||||
matching_server = Some(Arc::clone(server));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(server) = matching_server {
|
||||
let mut current_lock = ctx.data().current.lock().await;
|
||||
if let Some((current, _)) = current_lock.as_ref() {
|
||||
let current = current.lock().await;
|
||||
ctx.say(format!(
|
||||
"Already running '{}'! (stop the server before starting it)",
|
||||
current.name,
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
let server_lock = server.lock().await;
|
||||
let settings = ctx.data().settings.lock().await;
|
||||
_ = ctx
|
||||
.http()
|
||||
.send_message(
|
||||
settings.channel_id_info,
|
||||
&embed::server_started(
|
||||
&server_lock.name,
|
||||
Some(
|
||||
getmyip::get_my_ip(&settings.get_my_ip_url1, &settings.get_my_ip_url2)
|
||||
.await,
|
||||
),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
if settings.send_start_stop_messages_in_chat {
|
||||
_ = ctx
|
||||
.http()
|
||||
.send_message(
|
||||
settings.channel_id_chat,
|
||||
&embed::server_started(&server_lock.name, None),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let thread = server_lock.settings.clone().spawn();
|
||||
drop(server_lock);
|
||||
*current_lock = Some((server, Arc::new(Mutex::new(Some(thread)))));
|
||||
ctx.say(format!("Starting...")).await?;
|
||||
}
|
||||
} else {
|
||||
ctx.say(format!("Can't find a server with that name!"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[poise::command(slash_command)]
|
||||
async fn stop(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let current_lock = ctx.data().current.lock().await;
|
||||
if let Some((_, thread)) = current_lock.as_ref() {
|
||||
_ = thread
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone_task_sender()
|
||||
.send_task(MinecraftServerTask::Stop);
|
||||
ctx.say(format!("Stopping...")).await?;
|
||||
} else {
|
||||
ctx.say(format!("Use /start to start a server first"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &poise::Event<'_>,
|
||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
match event {
|
||||
poise::Event::Message { new_message } => {
|
||||
if !new_message.author.bot
|
||||
&& new_message.channel_id.0 == data.settings.lock().await.channel_id_chat
|
||||
{
|
||||
let current_lock = data.current.lock().await;
|
||||
if let Some((_current, thread)) = current_lock.as_ref() {
|
||||
let msg = new_message.content_safe(&ctx.cache);
|
||||
let msg = msg
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
let author = new_message
|
||||
.author_nick(&ctx)
|
||||
.await
|
||||
.unwrap_or_else(|| new_message.author.name.clone());
|
||||
_ = thread
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone_task_sender()
|
||||
.send_task(MinecraftServerTask::RunCommand(format!(
|
||||
"tellraw @a \"<{author}> {msg}\"",
|
||||
)));
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
// read settings file
|
||||
let settings =
|
||||
Settings::from_file(env::var("McDcBotSettingsFile").unwrap_or(format!("settings.txt")))
|
||||
.unwrap();
|
||||
// read mc servers
|
||||
let mut servers = vec![];
|
||||
for file in std::fs::read_dir(env::var("McDcBotServersDir").unwrap_or(format!("servers")))
|
||||
.expect("Couldn't read servers dir, maybe specify the directory with the McDcBotServersDir env variable?") {
|
||||
let file = file.unwrap();
|
||||
let content = std::fs::read_to_string(file.path()).unwrap();
|
||||
let mut lines = content.lines().filter(|line| !line.trim().is_empty());
|
||||
let settings = minecraft_manager::MinecraftServerSettings::from_lines(&mut lines).unwrap();
|
||||
servers.push(data::MinecraftServer {
|
||||
name: file.file_name().to_string_lossy().into_owned(),
|
||||
short: None,
|
||||
settings
|
||||
});
|
||||
}
|
||||
let mut shorts = HashSet::new();
|
||||
for server in &mut servers {
|
||||
if let Some(ch) = server.name.trim().chars().next() {
|
||||
let ch = ch.to_lowercase();
|
||||
if shorts.insert(format!("{ch}")) {
|
||||
server.short = Some(format!("{ch}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let initials = server
|
||||
.name
|
||||
.trim()
|
||||
.split_whitespace()
|
||||
.filter_map(|v| v.chars().next().map(|c| c.to_uppercase()))
|
||||
.flatten()
|
||||
.collect::<String>();
|
||||
if !initials.is_empty() {
|
||||
if shorts.insert(initials.clone()) {
|
||||
server.short = Some(initials);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// current server
|
||||
let current = Arc::new(Mutex::new(None));
|
||||
let current_thread = Arc::clone(¤t);
|
||||
// start
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
commands: vec![list(), start(), stop()],
|
||||
event_handler: |ctx, event, framework, data| {
|
||||
Box::pin(event_handler(ctx, event, framework, data))
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.token(env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN env var"))
|
||||
.intents(
|
||||
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT,
|
||||
)
|
||||
.setup(|ctx, ready, framework: &poise::Framework<Data, Error>| {
|
||||
Box::pin(async move {
|
||||
ctx.idle().await;
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
eprintln!("Connected as '{}'.", ready.user.name);
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
let settings = settings.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let sleep_time = Duration::from_millis(100);
|
||||
let mut running = false;
|
||||
loop {
|
||||
tokio::time::sleep(sleep_time).await;
|
||||
let mut current_lock: poise::futures_util::lock::MutexGuard<
|
||||
'_,
|
||||
Option<(
|
||||
Arc<Mutex<data::MinecraftServer>>,
|
||||
Arc<Mutex<Option<MinecraftServerThread>>>,
|
||||
)>,
|
||||
> = current_thread.lock().await;
|
||||
if let Some((_current_server, current_thread_mutex)) =
|
||||
current_lock.as_ref()
|
||||
{
|
||||
let mut current_thread_opt = current_thread_mutex.lock().await;
|
||||
let current_thread = current_thread_opt.as_mut().unwrap();
|
||||
current_thread.update();
|
||||
for event in current_thread.handle_new_events() {
|
||||
match &event.event {
|
||||
MinecraftServerEventType::Warning(w) => match w {
|
||||
MinecraftServerWarning::CouldNotGetServerProcessStdio
|
||||
| MinecraftServerWarning::CantWriteToStdin(_) => {
|
||||
ctx.dnd().await;
|
||||
}
|
||||
},
|
||||
MinecraftServerEventType::JoinLeave(e) => {
|
||||
if settings.send_join_and_leave_messages {
|
||||
_ = ctx
|
||||
.http
|
||||
.send_message(
|
||||
settings.channel_id_chat,
|
||||
&embed::join_leave(
|
||||
e
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
MinecraftServerEventType::ChatMessage(e) => {
|
||||
_ = ctx
|
||||
.http
|
||||
.send_message(
|
||||
settings.channel_id_chat,
|
||||
&embed::chat_message(e),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if current_thread.is_finished() {
|
||||
let cto = current_thread_opt.take().unwrap();
|
||||
let msg = embed::server_stopped(cto.get_stop_reason().ok());
|
||||
_ = ctx.http.send_message(settings.channel_id_info, &msg).await;
|
||||
if settings.send_start_stop_messages_in_chat {
|
||||
_ = ctx
|
||||
.http
|
||||
.send_message(settings.channel_id_chat, &msg)
|
||||
.await;
|
||||
}
|
||||
running = false;
|
||||
drop(current_thread_opt);
|
||||
*current_lock = None;
|
||||
ctx.idle().await;
|
||||
} else if !running {
|
||||
running = true;
|
||||
ctx.online().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(Data {
|
||||
settings: Mutex::new(settings),
|
||||
current,
|
||||
servers: Mutex::new(
|
||||
servers
|
||||
.into_iter()
|
||||
.map(|s| Arc::new(Mutex::new(s)))
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
framework.run().await.unwrap();
|
||||
}
|
51
mcdcbot/src/settings.rs
Executable file
51
mcdcbot/src/settings.rs
Executable file
@ -0,0 +1,51 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
pub channel_id_info: u64,
|
||||
pub channel_id_chat: u64,
|
||||
pub send_join_and_leave_messages: bool,
|
||||
pub send_start_stop_messages_in_chat: bool,
|
||||
pub get_my_ip_url1: String,
|
||||
pub get_my_ip_url2: String,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_file(path: impl AsRef<Path>) -> std::io::Result<Self> {
|
||||
let file = std::fs::read_to_string(path)?;
|
||||
let mut cii = None;
|
||||
let mut cic = None;
|
||||
let mut send_join_and_leave_messages = false;
|
||||
let mut send_start_stop_messages_in_chat = false;
|
||||
let mut get_my_ip_url1 = String::new();
|
||||
let mut get_my_ip_url2 = String::new();
|
||||
for (name, value) in file
|
||||
.lines()
|
||||
.map(|line| line.split_once("=").unwrap_or((line, "")))
|
||||
{
|
||||
match name {
|
||||
"channel_id_info" => {
|
||||
cii = value.trim().parse().ok();
|
||||
}
|
||||
"channel_id_chat" => {
|
||||
cic = value.trim().parse().ok();
|
||||
}
|
||||
"get_my_ip_url1" => get_my_ip_url1 = value.trim().to_owned(),
|
||||
"get_my_ip_url2" => get_my_ip_url2 = value.trim().to_owned(),
|
||||
"send_join_and_leave_messages" => send_join_and_leave_messages = value != "false",
|
||||
"send_start_stop_messages_in_chat" => {
|
||||
send_start_stop_messages_in_chat = value != "false"
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
channel_id_info: cii.expect("[settings] Missing `channel_id_info`"),
|
||||
channel_id_chat: cic.expect("[settings] Missing `channel_id_chat`"),
|
||||
send_join_and_leave_messages,
|
||||
send_start_stop_messages_in_chat,
|
||||
get_my_ip_url1,
|
||||
get_my_ip_url2,
|
||||
})
|
||||
}
|
||||
}
|
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)"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user