Compare commits

..

5 Commits

Author SHA1 Message Date
ea16f34c73 Initial rewrite start 2025-12-13 01:21:52 +01:00
6dea63f818 Config save and load start 2025-12-09 00:56:09 +01:00
7782e71990 Full startup await with Done ()! parser 2025-12-08 10:29:37 +01:00
7e7bfbc576 server bindings and eula util 2025-12-08 00:26:07 +01:00
e2cbd16dc8 Banner to SVG 2025-12-07 23:24:33 +01:00
21 changed files with 320 additions and 2618 deletions

1426
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,26 @@
[package] [package]
name = "mineguard" name = "mgrewrite"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "Opinionated Minecraft server supervisor engine built primarily for RustyMine"
readme = "README.md"
homepage = "https://mineguard.h3cx.dev"
repository = "https://github.com/H3ct0r55/MineGuard"
license = "MIT"
# publish set to false during development
publish = false
[features] [features]
default = ["core", "events", "mc-vanilla"] default = ["core", "mc-vanilla"]
# Core runtime requirements for the currently implemented functionality.
core = ["dep:thiserror", "dep:tokio", "dep:tokio-stream", "dep:tokio-util"]
# Placeholder for upcoming event-driven functionality.
events = ["dep:uuid", "dep:chrono", "dep:regex"]
mc-vanilla = ["dep:serde", "dep:serde_json", "dep:reqwest"] core = ["dep:serde", "dep:serde_json", "dep:uuid", "dep:tokio", "dep:tokio-util"]
# Add new feature groups here; attach their optional dependencies to the relevant feature list.
version-custom = []
events = []
mc-vanilla = []
mc-paper = []
tokio = ["dep:tokio"]
[dependencies] [dependencies]
async-trait = "0.1.89" serde = { version = "1.0.228", features = ["derive"], optional = true }
chrono = {version = "0.4.42", optional = true}
regex = {version = "1.12.2", optional = true}
reqwest = { version = "0.12.24", optional = true, features = ["json"] }
serde = { version = "1.0.228", optional = true, features = ["derive"] }
serde_json = {version = "1.0.145", optional = true} serde_json = {version = "1.0.145", optional = true}
thiserror = { version = "2.0.17", optional = true } thiserror = "2.0.17"
# Core async runtime and utilities tokio = { version = "1.48.0", features = ["fs", "io-std", "io-util", "process", "rt-multi-thread"], optional = true }
# Add new feature-specific optional dependencies alongside the relevant feature entry above. tokio-util ={version = "0.7.17", optional = true}
tokio = { version = "1.48.0", features = ["process", "rt-multi-thread", "macros", "io-std", "io-util"], optional = true } uuid = { version = "1.19.0", features = ["serde", "v4"], optional = true }
tokio-stream = { version = "0.1.17", features = ["full", "io-util", "signal", "tokio-util"], optional = true }
tokio-util = { version = "0.7.17", features = ["full"], optional = true }
uuid = { version = "1.19.0", features = ["v4"], optional = true }

View File

@@ -1,8 +1,5 @@
<picture> <img src="assets/banner.svg" alt="Project Banner">
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png">
<source media="(prefers-color-scheme: light)" srcset="assets/banner-light.png">
<img src="assets/banner-light.png" alt="Project Banner">
</picture>
> [!IMPORTANT] > [!IMPORTANT]
> MineGuard is currently in active development and breaking updates can be merged to the main branch any time > MineGuard is currently in active development and breaking updates can be merged to the main branch any time

7
assets/banner.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 960 288" version="1.1" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><style>
:root { color-scheme: light dark; }
#Candi2 { fill: #000000; }
@media (prefers-color-scheme: dark) {
#Candi2 { fill: #ffffff; }
}
</style><rect id="Artboard1" x="0" y="0" width="960" height="288" style="fill:none;" /><path id="Candi2" d="M572.408,105.247l-0,50.499c-0,27.084 -58.055,74.003 -92.408,84.254c-34.353,-10.25 -92.408,-57.17 -92.408,-84.254l-0,-107.746l38.689,0l45.705,45.705l45.705,-45.705l54.715,0l-0,43.383l-27.358,0l0,-16.026l-16.026,-0l-57.037,57.037l-57.037,-57.037l0,79.563c0.096,2.885 9.718,14.902 14.778,20.045c14.344,14.58 34.069,28.878 50.272,35.919c16.203,-7.041 35.928,-21.339 50.272,-35.919c5.06,-5.143 14.683,-17.16 14.778,-20.045l0,-22.526l-45.705,0l27.147,-27.147l45.915,0Z" /></svg>

After

Width:  |  Height:  |  Size: 941 B

26
src/config/config.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::path::PathBuf;
use uuid::Uuid;
use crate::config::version::{MinecraftType, MinecraftVersion};
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub uuid: Uuid,
pub core_path: PathBuf,
pub jar_path: PathBuf,
pub mc_version: MinecraftVersion,
pub mc_type: MinecraftType,
}
impl ServerConfig {
pub fn new() -> Self {
Self {
uuid: Uuid::new_v4(),
core_path: PathBuf::new(),
jar_path: PathBuf::new(),
mc_version: MinecraftVersion::Unknown,
mc_type: MinecraftType::Unknown,
}
}
}

View File

@@ -1,5 +1,2 @@
pub mod stream; pub mod config;
pub mod version; pub mod version;
pub use stream::{LogMeta, StreamLine, StreamSource};
pub use version::{MinecraftType, MinecraftVersion, Snapshot, Version};

View File

@@ -1,78 +0,0 @@
use std::fmt::{self, Display};
use uuid::Uuid;
use crate::instance::InstanceStatus;
use super::line::StreamLine;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventPayload {
#[cfg(feature = "events")]
StateChange {
old: InstanceStatus,
new: InstanceStatus,
},
StdLine {
line: StreamLine,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstanceEvent {
pub id: Uuid,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub payload: EventPayload,
}
impl InstanceEvent {
pub fn stdout<S: Into<String>>(line: S) -> Self {
let line = line.into();
let s_line = StreamLine::stdout(line);
let timestamp = s_line.extract_timestamp().unwrap_or(chrono::Utc::now());
let payload = EventPayload::StdLine { line: s_line };
Self {
id: Uuid::new_v4(),
timestamp,
payload,
}
}
pub fn stderr<S: Into<String>>(line: S) -> Self {
let line = line.into();
let s_line = StreamLine::stderr(line);
let timestamp = s_line.extract_timestamp().unwrap_or(chrono::Utc::now());
let payload = EventPayload::StdLine { line: s_line };
Self {
id: Uuid::new_v4(),
timestamp,
payload,
}
}
}
impl Display for InstanceEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = format!(
"UUID: {}\nTimestamp:{}\nPayload:\n",
self.id, self.timestamp
);
match self.payload.clone() {
EventPayload::StdLine { line } => {
let full = format!("{}{}", head, line);
writeln!(f, "{}", full)
}
#[cfg(feature = "events")]
EventPayload::StateChange { old, new } => {
let full = format!("{}State changed: {:?} -> {:?}", head, old, new);
writeln!(f, "{}", full)
}
}
}
}

View File

@@ -1,76 +0,0 @@
use std::fmt::{self, Display};
use regex::Regex;
#[cfg(feature = "events")]
use chrono::{DateTime, Local, NaiveTime, TimeZone, Utc};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StreamSource {
Stdout,
Stderr,
#[cfg(feature = "events")]
Event,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamLine {
line: String,
source: StreamSource,
}
impl StreamLine {
pub fn new<S: Into<String>>(line: S, source: StreamSource) -> Self {
let line = line.into();
let re = Regex::new(r#"^\[[^\]]*\]\s*\[[^\]]*\]:\s*"#).unwrap();
let line = re.replace(&line, "").to_string();
Self { line, source }
}
pub fn stdout<S: Into<String>>(line: S) -> Self {
let line = line.into();
let re = Regex::new(r#"^\[[^\]]*\]\s*\[[^\]]*\]:\s*"#).unwrap();
let line = re.replace(&line, "").to_string();
Self {
line,
source: StreamSource::Stdout,
}
}
pub fn stderr<S: Into<String>>(line: S) -> Self {
let line = line.into();
let re = Regex::new(r#"^\[[^\]]*\]\s*\[[^\]]*\]:\s*"#).unwrap();
let line = re.replace(&line, "").to_string();
Self {
line,
source: StreamSource::Stderr,
}
}
pub fn msg(&self) -> String {
self.line.clone()
}
pub fn extract_timestamp(&self) -> Option<DateTime<Utc>> {
let input = self.line.as_str();
let re = Regex::new(r"\[(.*?)\]").unwrap();
let time_s = re.captures(input).map(|v| v[1].to_string());
time_s.as_ref()?;
let time = NaiveTime::parse_from_str(&time_s.unwrap(), "%H:%M:%S").ok()?;
let today = Local::now().date_naive();
let naive_dt = today.and_time(time);
let local_dt = Local.from_local_datetime(&naive_dt).unwrap();
let utc_dt = local_dt.with_timezone(&Utc);
Some(utc_dt)
}
}
impl Display for StreamLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.line)
}
}

View File

@@ -1,98 +0,0 @@
use std::fmt::{self, Display};
use crate::error::ParserError;
#[cfg(feature = "mc-vanilla")]
pub struct LogMeta {
pub time: String,
pub thread: String,
pub level: LogLevel,
pub msg: String,
}
#[cfg(feature = "mc-vanilla")]
pub enum LogLevel {
Info,
Warn,
Error,
Other,
}
#[cfg(feature = "mc-vanilla")]
impl LogMeta {
pub fn new<S: Into<String>>(line: S) -> Result<Option<Self>, ParserError> {
let line: String = line.into();
let line = line.trim();
if !line.starts_with('[') {
return Ok(None);
}
let time_end = match line.find(']') {
Some(i) => i,
None => return Ok(None),
};
let time = line[1..time_end].to_string();
let meta_start = match line[time_end + 1..].find('[') {
Some(j) => time_end + 1 + j,
None => return Ok(None),
};
let msg_sep = match line[meta_start..].find("]: ") {
Some(k) => meta_start + k,
None => return Ok(None),
};
let meta = &line[(meta_start + 1)..msg_sep]; // inside the brackets
let msg = line[(msg_sep + 3)..].to_string(); // after "]: "
let mut thread_level = meta.splitn(2, '/');
let thread = thread_level
.next()
.ok_or(ParserError::ParserError)?
.to_string();
let level_str = thread_level
.next()
.ok_or(ParserError::ParserError)?
.trim_end_matches(']'); // just in case
let level = match level_str {
"INFO" => LogLevel::Info,
"WARN" => LogLevel::Warn,
"ERROR" => LogLevel::Error,
_ => LogLevel::Other,
};
Ok(Some(LogMeta {
time,
thread,
level,
msg,
}))
}
}
#[cfg(feature = "mc-vanilla")]
impl Display for LogMeta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let line = format!(
"Time: {}\nThread: {}\nLevel: {}\nMessage: {}",
self.time, self.thread, self.level, self.msg
);
write!(f, "{}", line)
}
}
#[cfg(feature = "mc-vanilla")]
impl Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
LogLevel::Info => write!(f, "INFO"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
LogLevel::Other => write!(f, "OTHER"),
}
}
}

View File

@@ -1,9 +0,0 @@
mod event;
mod line;
#[cfg(feature = "mc-vanilla")]
mod log;
pub use event::{EventPayload, InstanceEvent};
pub use line::{StreamLine, StreamSource};
#[cfg(feature = "mc-vanilla")]
pub use log::{LogLevel, LogMeta};

View File

@@ -1,141 +1,23 @@
use std::{ #[derive(Debug, Clone)]
fmt::{self, Display},
str::FromStr,
};
use tokio::sync::watch;
use crate::error::VersionError;
/// Identifies the type of Minecraft distribution supported by the configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MinecraftType {
Vanilla,
}
/// Semantic release version parsed from strings like `1.20.4`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version { pub struct Version {
pub major: u32, major: u32,
pub minor: u32, minor: u32,
pub patch: u32, patch: u32,
} }
/// Snapshot version parsed from strings like `23w13b`. #[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
pub year: u32,
pub week: u32,
pub build: char,
}
/// Enum covering both release and snapshot Minecraft version formats.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MinecraftVersion { pub enum MinecraftVersion {
Unknown,
Release(Version), Release(Version),
Snapshot(Snapshot), #[cfg(feature = "version-custom")]
Custom(String),
} }
impl Display for Version { #[derive(Debug, Clone)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { pub enum MinecraftType {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch) Unknown,
} #[cfg(feature = "mc-vanilla")]
} Vanilla,
#[cfg(feature = "mc-paper")]
impl Display for Snapshot { Paper,
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}w{:02}{}", self.year, self.week, self.build)
}
}
impl Display for MinecraftVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MinecraftVersion::Release(version) => write!(f, "{}", version.to_string()),
MinecraftVersion::Snapshot(snapshot) => write!(f, "{}", snapshot.to_string()),
}
}
}
impl FromStr for Version {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.split('.');
let major_str = split.next().ok_or(VersionError::MissingMajor)?;
let minor_str = split.next().ok_or(VersionError::MissingMinor)?;
let patch_str = split.next().ok_or(VersionError::MissingPatch)?;
if split.next().is_some() {
return Err(VersionError::ExtraComponents);
}
let major = major_str
.parse::<u32>()
.map_err(|_| VersionError::IncorrectMajor(major_str.to_string()))?;
let minor = minor_str
.parse::<u32>()
.map_err(|_| VersionError::IncorrectMinor(minor_str.to_string()))?;
let patch = patch_str
.parse::<u32>()
.map_err(|_| VersionError::IncorrectPatch(patch_str.to_string()))?;
Ok(Self {
major,
minor,
patch,
})
}
}
impl FromStr for Snapshot {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (year_str, rest) = s
.split_once('w')
.ok_or(VersionError::InvalidSnapshotFormat)?;
if rest.len() < 3 {
return Err(VersionError::InvalidSnapshotFormat);
}
let week_str = &rest[..2];
let build_str = &rest[2..];
let year = year_str
.parse::<u32>()
.map_err(|_| VersionError::IncorrectYear(year_str.to_string()))?;
let week = week_str
.parse::<u32>()
.map_err(|_| VersionError::IncorrectWeek(week_str.to_string()))?;
let build = if build_str.len() == 1 {
build_str.chars().next().unwrap()
} else {
return Err(VersionError::IncorrectBuild(build_str.to_string()));
};
Ok(Self { year, week, build })
}
}
impl FromStr for MinecraftVersion {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(ver) = Version::from_str(s) {
return Ok(MinecraftVersion::Release(ver));
}
if let Ok(snap) = Snapshot::from_str(s) {
return Ok(MinecraftVersion::Snapshot(snap));
}
Err(VersionError::UnknownVersionFormat(s.to_string()))
}
} }

View File

@@ -1,129 +1,33 @@
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Clone, Error)] #[derive(Error, Debug)]
pub enum Error {
#[error("Undefined error")]
Generic,
}
#[derive(Debug, Clone, Error)]
pub enum VersionError {
#[error("Incorrect major version: {0}")]
IncorrectMajor(String),
#[error("Incorrect minor version: {0}")]
IncorrectMinor(String),
#[error("Incorrect patch version: {0}")]
IncorrectPatch(String),
#[error("Incorrect major version: {0}")]
IncorrectYear(String),
#[error("Incorrect minor version: {0}")]
IncorrectWeek(String),
#[error("Incorrect patch version: {0}")]
IncorrectBuild(String),
#[error("Missing major version")]
MissingMajor,
#[error("Missing minor version")]
MissingMinor,
#[error("Missing patch version")]
MissingPatch,
#[error("Invalid snapshot format")]
InvalidSnapshotFormat,
#[error("Too many components")]
ExtraComponents,
#[error("Unrecognized version format: {0}")]
UnknownVersionFormat(String),
}
#[derive(Debug, Clone, Error)]
pub enum HandleError { pub enum HandleError {
#[error("Invalid Minecraft Version: {0}")] #[error("internal library error, this shouldn't happen")]
InvalidVersion(String), InternalError,
#[error("Invalid server root directory: {0}")] #[error("failed to start handle, not stopped")]
InvalidDirectory(String), StartFailedNotStopped,
#[error("Invalid relative JAR path: {0}")] #[error("failed to stop handle, not running")]
InvalidPathJAR(String), StopFailedNotRunning,
}
#[error("failed to start handle, child exists")]
#[derive(Debug, Clone, Error)] StartFailedChildExists,
pub enum SubscribeError { #[error("failed to stop handle, child doesn't exists")]
#[error("No stdout found")] StopFailedChildNotExists,
NoStdout,
#[error("failed to kill handle, child doesn't exists")]
#[error("No stderr found")] KillFailedChildNotExists,
NoStderr,
} #[error("failed to kill handle, internal error")]
KillFailledInternal,
#[derive(Debug, Clone, Error)]
pub enum ServerError { #[error("failed to start pumps, child inexistant")]
#[error("Server is already running")] PumpsFailedNoChild,
AlreadyRunning,
#[error("failed to start pumps, no stdout in child")]
#[error("Server is not running")] PumpsFailedNoStdout,
NotRunning,
#[error("failed to start pumps, no stderr in child")]
#[error("Server crashed early")] PumpsFailedNoStderr,
EarlyCrash,
#[error("Failed to run java command")]
CommandFailed,
#[error("Failed to access child stdout pipe")]
NoStdoutPipe,
#[error("Failed to access child stdin pipe")]
NoStdinPipe,
#[error("Failed to access child stderr pipe")]
NoStderrPipe,
#[error("Failed to write to stdin")]
StdinWriteFailed,
}
#[cfg(feature = "events")]
#[derive(Debug, Clone, Error)]
pub enum ParserError {
#[error("ParserError")]
ParserError,
}
#[derive(Debug, Clone, Error)]
pub enum CreationError {
#[error("CreationError")]
CreationError,
#[error("Invalid directory")]
DirectoryError,
#[error("Failed to parse manifest")]
ManifestError,
#[error("Version does not exist")]
VersionError,
#[error("Network Error")]
NetworkError,
}
#[derive(Debug, Clone, Error)]
pub enum ManifestError {
#[error("ManifestError")]
ManifestError,
#[error("Failed to load mainfest")]
LoadUrlError,
#[error("Failed to parse manifest json")]
JsonParseError,
} }

View File

@@ -1,396 +1,231 @@
use std::{path::PathBuf, process::Stdio, sync::Arc, time::Duration}; use std::{process::Stdio, sync::Arc};
use chrono::Utc;
use tokio::{ use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}, io::{AsyncBufReadExt, BufReader},
process::{self, Child}, process::{Child, Command},
sync::{RwLock, broadcast, mpsc}, sync::{broadcast, RwLock},
time::sleep,
}; };
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use uuid::Uuid;
#[cfg(feature = "events")] use crate::{config::config::ServerConfig, error::HandleError, instance::types::InstanceStatus};
use crate::config::stream::InstanceEvent;
use crate::{
config::{MinecraftType, MinecraftVersion, StreamSource, stream::EventPayload},
error::{HandleError, ServerError, SubscribeError},
};
use super::{InstanceData, InstanceStatus};
#[derive(Debug)] #[derive(Debug)]
pub struct InstanceHandle { pub struct InstanceHandle {
pub data: InstanceData, config: Arc<RwLock<ServerConfig>>,
pub status: Arc<RwLock<InstanceStatus>>, status: Arc<RwLock<InstanceStatus>>,
stdout_tx: broadcast::Sender<InstanceEvent>,
stderr_tx: Option<broadcast::Sender<InstanceEvent>>,
#[cfg(feature = "events")]
events_tx: broadcast::Sender<InstanceEvent>,
#[cfg(feature = "events")]
internal_tx: mpsc::Sender<InstanceEvent>,
#[cfg(feature = "events")]
internal_rx: Option<mpsc::Receiver<InstanceEvent>>,
stdin_tx: mpsc::Sender<String>,
stdin_rx: Option<mpsc::Receiver<String>>,
child: Option<Arc<RwLock<Child>>>, child: Option<Arc<RwLock<Child>>>,
shutdown: CancellationToken, shutdown: CancellationToken,
stdout_tx: broadcast::Sender<String>,
stderr_tx: broadcast::Sender<String>,
} }
impl InstanceHandle { impl InstanceHandle {
pub fn new_with_params( /// Create a new `InstanceHandle` with a blank `ServerConfig`
root_dir: PathBuf, pub fn new() -> Self {
jar_path: PathBuf, Self {
mc_version: MinecraftVersion, config: Arc::new(RwLock::new(ServerConfig::new())),
mc_type: MinecraftType, status: Arc::new(RwLock::new(InstanceStatus::Stopped)),
) -> Result<Self, HandleError> {
let parsed_version: MinecraftVersion = mc_version;
let root: PathBuf = root_dir.clone().into();
if !root.exists() || !root.is_dir() {
return Err(HandleError::InvalidDirectory(
root_dir.to_str().unwrap().to_string(),
));
}
let path: PathBuf = jar_path.clone().into();
let conc = root.join(path.clone());
if !path.is_relative() || !conc.is_file() {
return Err(HandleError::InvalidPathJAR(
jar_path.to_str().unwrap().to_string(),
));
}
let data = InstanceData {
root_dir: root,
jar_path: path,
mc_version: parsed_version,
mc_type,
};
let status = InstanceStatus::Stopped;
let (stdin_tx, stdin_rx) = mpsc::channel(1024);
let (internal_tx, internal_rx) = mpsc::channel(1024);
Ok(Self {
data,
status: Arc::new(RwLock::new(status)),
stdout_tx: broadcast::Sender::new(2048),
stderr_tx: None,
#[cfg(feature = "events")]
events_tx: broadcast::Sender::new(2048),
#[cfg(feature = "events")]
internal_tx,
#[cfg(feature = "events")]
internal_rx: Some(internal_rx),
stdin_tx,
stdin_rx: Some(stdin_rx),
child: None, child: None,
shutdown: CancellationToken::new(), shutdown: CancellationToken::new(),
stdout_tx: broadcast::Sender::new(1024),
stderr_tx: broadcast::Sender::new(1024),
}
}
/// Create a new `InstanceHandle` with a `ServerConfig`, config is consumed
pub fn with_cfg(config: ServerConfig) -> Result<Self, HandleError> {
Ok(Self {
config: Arc::new(RwLock::new(config)),
status: Arc::new(RwLock::new(InstanceStatus::Stopped)),
child: None,
shutdown: CancellationToken::new(),
stdout_tx: broadcast::Sender::new(1024),
stderr_tx: broadcast::Sender::new(1024),
}) })
} }
pub async fn send_command<S: Into<String>>(&self, cmd: S) -> Result<(), ServerError> { pub async fn start(&mut self) -> Result<(), HandleError> {
let mut command = cmd.into(); if !self.stopped_killed_or_crashed().await {
if !command.ends_with('\n') { return Err(HandleError::StartFailedNotStopped);
command.push('\n');
} }
self.stdin_tx
.send(command)
.await
.map_err(|_| ServerError::StdinWriteFailed)?;
Ok(())
}
pub async fn start(&mut self) -> Result<(), ServerError> {
self.validate_start_parameters().await?;
self.transition_status(InstanceStatus::Starting).await;
let command = self.build_start_command();
let child = self.spawn_child_process(command)?;
self.setup_stream_pumps(child)?;
self.transition_status(InstanceStatus::Running).await;
self.setup_parser()?;
Ok(())
}
async fn validate_start_parameters(&self) -> Result<(), ServerError> {
if self.child.is_some() { if self.child.is_some() {
return Err(ServerError::AlreadyRunning); return Err(HandleError::StartFailedChildExists);
} }
Ok(()) _ = self.change_status(InstanceStatus::Starting);
let mut command = self.build_command().await;
let child = command.spawn();
self.setup_std_pumps().await?;
if cfg!(feature = "event") {
// TODO: await server Done (...)! before status::Running for event module
todo!()
} else {
_ = self.change_status(InstanceStatus::Running);
}
todo!()
} }
async fn transition_status(&self, status: InstanceStatus) { pub async fn stop(&mut self) -> Result<(), HandleError> {
let r_guard = self.status.read().await; if self.get_status().await != InstanceStatus::Running {
let old = r_guard.clone(); return Err(HandleError::StopFailedNotRunning);
drop(r_guard);
let new = status.clone();
let mut guard = self.status.write().await;
*guard = status;
drop(guard);
let event = InstanceEvent {
id: Uuid::new_v4(),
timestamp: Utc::now(),
payload: EventPayload::StateChange { old, new },
};
_ = self.internal_tx.send(event).await;
} }
fn build_start_command(&self) -> process::Command { let child = self.child.clone().ok_or(HandleError::StopFailedChildNotExists)?;
let mut command = process::Command::new("java");
command _ = self.change_status(InstanceStatus::Stopping);
.arg("-jar")
.arg(&self.data.jar_path) let child_w = child.write().await;
.arg("nogui")
.current_dir(&self.data.root_dir) // TODO:: Create send command for graceful stop, finish logic with shutdown token
.stdout(Stdio::piped()) todo!()
.stderr(Stdio::piped()) }
.stdin(Stdio::piped());
pub async fn kill(&mut self) -> Result<(), HandleError> {
let child = self.child.clone().ok_or(HandleError::KillFailedChildNotExists)?;
_ = self.change_status(InstanceStatus::Killing);
let mut child_w = child.write().await;
child_w.kill().await.map_err(|_| HandleError::KillFailledInternal)?;
// TODO:: Finish kill logic including updating shutdown token status
todo!()
}
}
// region: --- Utils
impl InstanceHandle {
async fn stopped_killed_or_crashed(&self) -> bool {
let status = self.get_status().await;
if status == InstanceStatus::Stopped
|| status == InstanceStatus::Killed
|| status == InstanceStatus::Crashed
{
return true;
}
false
}
async fn get_status(&self) -> InstanceStatus {
let status_r = self.status.read().await;
let res = status_r.clone();
drop(status_r);
res
}
async fn change_status(
&mut self,
new_status: InstanceStatus,
) -> (InstanceStatus, InstanceStatus) {
let mut status_w = self.status.write().await;
let old = status_w.clone();
*status_w = new_status.clone();
drop(status_w);
(old, new_status)
}
async fn get_config(&self) -> ServerConfig {
let config_r = self.config.read().await;
let res = config_r.clone();
drop(config_r);
res
}
async fn build_command(&self) -> Command {
let cfg = self.get_config().await;
let mut command = Command::new("java");
command.arg("-jar").arg(&cfg.jar_path).arg("nogui").current_dir(&cfg.core_path);
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command.process_group(0);
command command
} }
fn spawn_child_process(&self, mut command: process::Command) -> Result<Child, ServerError> {
command.spawn().map_err(|_| ServerError::CommandFailed)
} }
fn setup_stream_pumps(&mut self, mut child: Child) -> Result<(), ServerError> { async fn get_status_arc(arc: Arc<RwLock<InstanceStatus>>) -> InstanceStatus {
let stdout = child.stdout.take().ok_or(ServerError::NoStdoutPipe)?; let status_r = arc.read().await;
let stderr = child.stderr.take().ok_or(ServerError::NoStderrPipe)?; let res = status_r.clone();
let stdin = child.stdin.take().ok_or(ServerError::NoStdinPipe)?; drop(status_r);
let child = Arc::new(RwLock::new(child)); res
self.child = Some(child); }
async fn change_status_arc(
arc: Arc<RwLock<InstanceStatus>>,
new_status: InstanceStatus,
) -> (InstanceStatus, InstanceStatus) {
let mut status_w = arc.write().await;
let old = status_w.clone();
*status_w = new_status.clone();
drop(status_w);
(old, new_status)
}
// endregion: --- Utils
// region: --- StdStream
impl InstanceHandle {
async fn setup_std_pumps(&mut self) -> Result<(), HandleError> {
let child = self.child.clone().ok_or(HandleError::PumpsFailedNoChild)?;
let mut child_w = child.write().await;
let stdout_rx = child_w.stdout.take().ok_or(HandleError::PumpsFailedNoStdout)?;
let stderr_rx = child_w.stderr.take().ok_or(HandleError::PumpsFailedNoStderr)?;
let stdout_tx = self.stdout_tx.clone(); let stdout_tx = self.stdout_tx.clone();
let stderr_tx = broadcast::Sender::new(2048); let stderr_tx = self.stderr_tx.clone();
self.stderr_tx = Some(stderr_tx.clone());
let shutdown = self.shutdown.clone();
let stdout_status = self.status.clone(); let status_stdout = self.status.clone();
let stderr_status = self.status.clone(); let status_stderr = self.status.clone();
let internal_tx1 = self.internal_tx.clone();
let internal_tx2 = self.internal_tx.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut stdout_reader = BufReader::new(stdout).lines(); let mut stdout_br = BufReader::new(stdout_rx).lines();
loop { loop {
match stdout_reader.next_line().await { match stdout_br.next_line().await {
Ok(Some(line)) => { Ok(Some(line)) => {
let _ = stdout_tx.send(InstanceEvent::stdout(line)); _ = stdout_tx.send(line);
} }
_ => { _ => {
let status_guard = stdout_status.read().await; let status = get_status_arc(status_stdout.clone()).await;
let state = status_guard.clone(); if status == InstanceStatus::Running {
if state == InstanceStatus::Running && state == InstanceStatus::Starting { change_status_arc(status_stdout, InstanceStatus::Crashed).await;
let old = status_guard.clone();
drop(status_guard);
let mut status = stdout_status.write().await;
*status = InstanceStatus::Crashed;
let event = InstanceEvent {
id: Uuid::new_v4(),
timestamp: Utc::now(),
payload: EventPayload::StateChange {
old,
new: status.clone(),
},
};
_ = internal_tx1.send(event).await;
drop(status);
break;
} }
drop(status_guard); break;
} }
} }
} }
}); });
tokio::spawn(async move { tokio::spawn(async move {
let mut stderr_reader = BufReader::new(stderr).lines(); let mut stdout_br = BufReader::new(stderr_rx).lines();
loop { loop {
match stderr_reader.next_line().await { match stdout_br.next_line().await {
Ok(Some(line)) => { Ok(Some(line)) => {
let _ = stderr_tx.send(InstanceEvent::stderr(line)); _ = stderr_tx.send(line);
} }
_ => { _ => {
let status_guard = stderr_status.read().await; let status = get_status_arc(status_stderr.clone()).await;
let state = status_guard.clone(); if status == InstanceStatus::Running {
if state == InstanceStatus::Running && state == InstanceStatus::Starting { change_status_arc(status_stderr, InstanceStatus::Crashed).await;
let old = status_guard.clone();
drop(status_guard);
let mut status = stderr_status.write().await;
*status = InstanceStatus::Crashed;
let event = InstanceEvent {
id: Uuid::new_v4(),
timestamp: Utc::now(),
payload: EventPayload::StateChange {
old,
new: status.clone(),
},
};
_ = internal_tx2.send(event).await;
drop(status);
break;
} }
drop(status_guard); break;
} }
} }
} }
}); });
let mut stdin_rx = self.stdin_rx.take().ok_or(ServerError::NoStdinPipe)?; todo!()
tokio::spawn(async move {
let mut writer = BufWriter::new(stdin);
loop {
tokio::select! {
_ = shutdown.cancelled() => {
break;
}
maybe_cmd = stdin_rx.recv() => {
if let Some(cmd) = maybe_cmd {
_ = writer.write_all(cmd.as_bytes()).await;
_ = writer.flush().await;
}
}
}
}
});
Ok(())
}
#[cfg(all(feature = "events", any(feature = "mc-vanilla")))]
fn setup_parser(&mut self) -> Result<(), ServerError> {
let stdout_stream = self
.subscribe(StreamSource::Stdout)
.map_err(|_| ServerError::NoStdoutPipe)?;
let shutdown1 = self.shutdown.clone();
let shutdown2 = self.shutdown.clone();
let event_tx = self.events_tx.clone();
if let Some(mut internal_rx) = self.internal_rx.take() {
tokio::spawn(async move {
let tx = event_tx;
loop {
tokio::select! {
_ = shutdown1.cancelled() => {
break;
}
maybe_event = internal_rx.recv() => {
if let Some(event) = maybe_event {
_ = tx.send(event);
}
}
}
}
});
}
#[cfg(feature = "mc-vanilla")]
if self.data.mc_type == MinecraftType::Vanilla {
tokio::spawn(async move {
let mut rx = stdout_stream;
loop {
tokio::select! {
_ = shutdown2.cancelled() => {
break;
}
line = rx.next() => {
if let Some(Ok(val)) = line {
// TODO: Call parser
}
}
}
}
});
}
Ok(())
}
pub async fn kill(&mut self) -> Result<(), ServerError> {
if let Some(child_arc) = self.child.clone() {
self.transition_status(InstanceStatus::Killing).await;
let mut child = child_arc.write().await;
child.kill().await.map_err(|_| ServerError::CommandFailed)?;
self.transition_status(InstanceStatus::Killed).await;
sleep(Duration::from_secs(1)).await;
self.shutdown.cancel();
self.child = None;
Ok(())
} else {
Err(ServerError::NotRunning)
}
}
pub async fn stop(&mut self) -> Result<(), ServerError> {
if let Some(child_arc) = self.child.clone() {
self.transition_status(InstanceStatus::Stopping).await;
_ = self.send_command("stop").await;
let mut child = child_arc.write().await;
child.wait().await.map_err(|_| ServerError::CommandFailed)?;
self.transition_status(InstanceStatus::Stopped).await;
sleep(Duration::from_secs(1)).await;
self.shutdown.cancel();
self.child = None;
Ok(())
} else {
Err(ServerError::NotRunning)
}
}
pub fn subscribe(
&self,
stream: StreamSource,
) -> Result<BroadcastStream<InstanceEvent>, SubscribeError> {
match stream {
StreamSource::Stdout => {
let rx = self.stdout_tx.subscribe();
Ok(BroadcastStream::new(rx))
}
StreamSource::Stderr => {
let rx = match &self.stderr_tx {
Some(value) => value.subscribe(),
None => return Err(SubscribeError::NoStderr),
};
Ok(BroadcastStream::new(rx))
}
#[cfg(feature = "events")]
StreamSource::Event => {
let rx = self.events_tx.subscribe();
Ok(BroadcastStream::new(rx))
}
}
} }
} }
// endregion: --- StdStream

View File

@@ -1,5 +1,2 @@
mod handle; pub mod handle;
mod types; pub mod types;
pub use handle::InstanceHandle;
pub use types::{InstanceData, InstanceStatus};

View File

@@ -1,15 +1,3 @@
use std::path::PathBuf;
use crate::config::{MinecraftType, MinecraftVersion};
#[derive(Debug, Clone)]
pub struct InstanceData {
pub root_dir: PathBuf,
pub jar_path: PathBuf,
pub mc_version: MinecraftVersion,
pub mc_type: MinecraftType,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstanceStatus { pub enum InstanceStatus {
Starting, Starting,

View File

@@ -1,6 +1,6 @@
pub mod config; mod config;
mod instance;
mod server;
pub use server::*;
pub mod error; pub mod error;
pub mod instance;
pub mod manifests;
pub mod server;
pub mod utils;

View File

@@ -1 +0,0 @@
pub mod vanilla;

View File

@@ -1,114 +0,0 @@
#![cfg(feature = "mc-vanilla")]
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use crate::{config::MinecraftVersion, error::ManifestError};
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaManifestV2 {
latest: VanillaManifestV2Latest,
versions: Vec<VanillaManifestV2Version>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaManifestV2Latest {
release: String,
snapshot: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaManifestV2Version {
id: String,
#[serde(rename = "type")]
mc_type: String,
url: String,
time: String,
#[serde(rename = "releaseTime")]
release_time: String,
sha1: String,
#[serde(rename = "complianceLevel")]
compliance_level: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaReleaseManifest {
downloads: VanillaReleaseManifestDownloads,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaReleaseManifestDownloads {
client: VanillaReleaseManifestDownloadsItem,
client_mappings: VanillaReleaseManifestDownloadsItem,
server: VanillaReleaseManifestDownloadsItem,
server_mappings: VanillaReleaseManifestDownloadsItem,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VanillaReleaseManifestDownloadsItem {
sha1: String,
size: u32,
url: String,
}
impl VanillaReleaseManifest {
pub async fn load(version: VanillaManifestV2Version) -> Result<Self, ManifestError> {
let client = Client::new();
let manifest: VanillaReleaseManifest = client
.get(&version.url)
.send()
.await
.map_err(|_| ManifestError::LoadUrlError)?
.error_for_status()
.map_err(|_| ManifestError::LoadUrlError)?
.json()
.await
.map_err(|e| {
eprintln!("{}", e);
ManifestError::JsonParseError
})?;
Ok(manifest)
}
pub fn server_url(&self) -> String {
self.downloads.server.url.clone()
}
}
impl VanillaManifestV2 {
pub async fn load() -> Result<Self, ManifestError> {
let client = Client::new();
let manifest: VanillaManifestV2 = client
.get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
.send()
.await
.map_err(|_| ManifestError::LoadUrlError)?
.error_for_status()
.map_err(|_| ManifestError::LoadUrlError)?
.json()
.await
.map_err(|e| {
eprintln!("{}", e);
ManifestError::JsonParseError
})?;
Ok(manifest)
}
pub fn find(
&self,
version: MinecraftVersion,
) -> Result<Option<VanillaManifestV2Version>, ManifestError> {
let id = version.to_string();
let found = match self.versions.iter().find(|p| p.id == id) {
Some(val) => Some(val.clone()),
None => None,
};
Ok(found)
}
}

View File

@@ -1,123 +0,0 @@
use std::{ops::RangeInclusive, path::PathBuf, str::FromStr};
use tokio::{
fs::{File, create_dir},
io::{self, AsyncWriteExt},
sync::RwLock,
};
use uuid::Uuid;
use crate::{
config::{MinecraftType, MinecraftVersion, Version},
error::CreationError,
instance::InstanceHandle,
manifests::vanilla::{VanillaManifestV2, VanillaManifestV2Version, VanillaReleaseManifest},
server,
};
pub struct MineGuardConfig {
uuid: Uuid,
server_dir: PathBuf,
jar_path: PathBuf,
mc_version: MinecraftVersion,
mc_type: MinecraftType,
}
pub struct MineGuardServer {
pub handle: RwLock<InstanceHandle>,
pub config: RwLock<MineGuardConfig>,
}
impl MineGuardConfig {
pub fn new() -> Self {
Self {
uuid: Uuid::new_v4(),
server_dir: PathBuf::new(),
jar_path: PathBuf::new(),
mc_version: MinecraftVersion::Release(Version::from_str("0.00.00").unwrap()),
mc_type: MinecraftType::Vanilla,
}
}
}
impl MineGuardServer {
pub async fn create(
mc_version: MinecraftVersion,
mc_type: MinecraftType,
directory: PathBuf,
) -> Result<Self, CreationError> {
if !directory.is_dir() {
return Err(CreationError::DirectoryError);
}
let uuid = Uuid::new_v4();
let server_root = directory.join(uuid.to_string());
let jar_path_rel =
PathBuf::from_str("server.jar").map_err(|_| CreationError::DirectoryError)?;
let jar_path_full = server_root.join(jar_path_rel.clone());
create_dir(server_root.clone())
.await
.map_err(|_| CreationError::DirectoryError)?;
let mut url = String::new();
if mc_type == MinecraftType::Vanilla {
let vanilla_manifest = VanillaManifestV2::load()
.await
.map_err(|_| CreationError::ManifestError)?;
let find_ver = match vanilla_manifest
.find(mc_version.clone())
.map_err(|_| CreationError::ManifestError)?
{
Some(val) => val,
None => return Err(CreationError::VersionError),
};
let release_manifest = VanillaReleaseManifest::load(find_ver)
.await
.map_err(|_| CreationError::ManifestError)?;
url = release_manifest.server_url();
}
let resp = reqwest::get(url)
.await
.map_err(|_| CreationError::NetworkError)?;
let mut body = resp
.bytes()
.await
.map_err(|_| CreationError::NetworkError)?;
let mut out = File::create(jar_path_full)
.await
.map_err(|_| CreationError::DirectoryError)?;
out.write_all_buf(&mut body)
.await
.map_err(|_| CreationError::DirectoryError)?;
let config = MineGuardConfig {
uuid: uuid,
server_dir: server_root,
jar_path: jar_path_rel,
mc_version: mc_version,
mc_type: mc_type,
};
let handle = InstanceHandle::new_with_params(
config.server_dir.clone(),
config.jar_path.clone(),
config.mc_version.clone(),
config.mc_type.clone(),
)
.map_err(|_| CreationError::CreationError)?;
let server = MineGuardServer {
config: RwLock::new(config),
handle: RwLock::new(handle),
};
Ok(server)
}
}

View File

@@ -1 +1,15 @@
pub mod domain; use tokio::sync::RwLock;
use crate::instance::handle::InstanceHandle;
#[derive(Debug)]
pub struct MineGuardServer {
handle: RwLock<InstanceHandle>,
}
impl MineGuardServer {
pub fn create() -> Self {
let new_instance = InstanceHandle::new();
Self { handle: RwLock::new(new_instance) }
}
}

View File

@@ -1,24 +0,0 @@
use chrono::{DateTime, Datelike, Local, NaiveTime, TimeZone, Timelike, Utc};
use regex::Regex;
pub fn extract_timestamp(input: &str) -> Option<DateTime<Utc>> {
let re = Regex::new(r"\[(.*?)\]").unwrap();
let time_s = re.captures(input).map(|v| v[1].to_string());
time_s.as_ref()?;
let time = NaiveTime::parse_from_str(&time_s.unwrap(), "%H:%M:%S").ok()?;
let today = Local::now().date_naive();
let local_dt = Local
.with_ymd_and_hms(
today.year(),
today.month(),
today.day(),
time.hour(),
time.minute(),
time.second(),
)
.single()?;
Some(local_dt.with_timezone(&Utc))
}