Compare commits
10 Commits
e2dd5b6c0b
...
668870b92e
| Author | SHA1 | Date | |
|---|---|---|---|
| 668870b92e | |||
| 6b2757a52f | |||
| f3c7172178 | |||
|
|
a1f72ba842 | ||
| ae52f1113f | |||
| 9f0c253d30 | |||
|
|
454776fcf2 | ||
| 84e79bd179 | |||
| 9c4c23f881 | |||
| 2169e95423 |
1451
Cargo.lock
generated
1451
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -15,15 +15,22 @@ default = ["core", "events", "mc-vanilla"]
|
|||||||
# Core runtime requirements for the currently implemented functionality.
|
# Core runtime requirements for the currently implemented functionality.
|
||||||
core = ["dep:thiserror", "dep:tokio", "dep:tokio-stream", "dep:tokio-util"]
|
core = ["dep:thiserror", "dep:tokio", "dep:tokio-stream", "dep:tokio-util"]
|
||||||
# Placeholder for upcoming event-driven functionality.
|
# Placeholder for upcoming event-driven functionality.
|
||||||
events = []
|
events = ["dep:uuid", "dep:chrono", "dep:regex"]
|
||||||
|
|
||||||
mc-vanilla = []
|
mc-vanilla = ["dep:serde", "dep:serde_json", "dep:reqwest"]
|
||||||
# Add new feature groups here; attach their optional dependencies to the relevant feature list.
|
# Add new feature groups here; attach their optional dependencies to the relevant feature list.
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
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}
|
||||||
thiserror = { version = "2.0.17", optional = true }
|
thiserror = { version = "2.0.17", optional = true }
|
||||||
# Core async runtime and utilities
|
# Core async runtime and utilities
|
||||||
# Add new feature-specific optional dependencies alongside the relevant feature entry above.
|
# Add new feature-specific optional dependencies alongside the relevant feature entry above.
|
||||||
tokio = { version = "1.48.0", features = ["process", "rt-multi-thread", "macros", "io-std", "io-util"], optional = true }
|
tokio = { version = "1.48.0", features = ["process", "rt-multi-thread", "macros", "io-std", "io-util"], optional = true }
|
||||||
tokio-stream = { version = "0.1.17", features = ["full", "io-util", "signal", "tokio-util"], 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 }
|
tokio-util = { version = "0.7.17", features = ["full"], optional = true }
|
||||||
|
uuid = { version = "1.19.0", features = ["v4"], optional = true }
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -4,10 +4,21 @@
|
|||||||
<img src="assets/banner-light.png" alt="Project Banner">
|
<img src="assets/banner-light.png" alt="Project Banner">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> MineGuard is currently in active development and breaking updates can be merged to the main branch any time
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
# MineGuard
|
||||||
|
Rust based Minecraft server lifecycle controller and interface
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="./LICENSE">MIT License</a> • H3cx
|
<a href="./LICENSE">MIT License</a> • h3cx
|
||||||
|
|
||||||
Built on coffee, late nights, and a fully up-to-date Arch install (btw).
|
Built on coffee, late nights, and a fully up-to-date Arch install (btw).
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
78
src/config/stream/event.rs
Normal file
78
src/config/stream/event.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/config/stream/line.rs
Normal file
76
src/config/stream/line.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,13 @@
|
|||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
#[cfg(feature = "mc-vanilla")]
|
|
||||||
use crate::error::ParserError;
|
use crate::error::ParserError;
|
||||||
|
|
||||||
/// Identifies which process stream produced a line of output.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum StreamSource {
|
|
||||||
Stdout,
|
|
||||||
Stderr,
|
|
||||||
#[cfg(feature = "events")]
|
|
||||||
Event,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Captures a single line of process output along with its origin stream.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct StreamLine {
|
|
||||||
line: String,
|
|
||||||
source: StreamSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "events")]
|
|
||||||
pub struct InstanceEvent {}
|
|
||||||
|
|
||||||
#[cfg(feature = "events")]
|
|
||||||
pub enum Events {}
|
|
||||||
|
|
||||||
#[cfg(feature = "mc-vanilla")]
|
#[cfg(feature = "mc-vanilla")]
|
||||||
pub struct LogMeta {
|
pub struct LogMeta {
|
||||||
time: String,
|
pub time: String,
|
||||||
thread: String,
|
pub thread: String,
|
||||||
level: LogLevel,
|
pub level: LogLevel,
|
||||||
msg: String,
|
pub msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mc-vanilla")]
|
#[cfg(feature = "mc-vanilla")]
|
||||||
@@ -119,36 +96,3 @@ impl Display for LogLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamLine {
|
|
||||||
pub fn new<S: Into<String>>(line: S, source: StreamSource) -> Self {
|
|
||||||
Self {
|
|
||||||
line: line.into(),
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stdout<S: Into<String>>(line: S) -> Self {
|
|
||||||
Self {
|
|
||||||
line: line.into(),
|
|
||||||
source: StreamSource::Stdout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stderr<S: Into<String>>(line: S) -> Self {
|
|
||||||
Self {
|
|
||||||
line: line.into(),
|
|
||||||
source: StreamSource::Stderr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn msg(&self) -> String {
|
|
||||||
self.line.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for StreamLine {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
src/config/stream/mod.rs
Normal file
9
src/config/stream/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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};
|
||||||
@@ -3,6 +3,8 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use crate::error::VersionError;
|
use crate::error::VersionError;
|
||||||
|
|
||||||
/// Identifies the type of Minecraft distribution supported by the configuration.
|
/// Identifies the type of Minecraft distribution supported by the configuration.
|
||||||
@@ -46,6 +48,15 @@ impl Display for Snapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl FromStr for Version {
|
||||||
type Err = VersionError;
|
type Err = VersionError;
|
||||||
|
|
||||||
|
|||||||
28
src/error.rs
28
src/error.rs
@@ -99,3 +99,31 @@ pub enum ParserError {
|
|||||||
#[error("ParserError")]
|
#[error("ParserError")]
|
||||||
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,47 +1,38 @@
|
|||||||
use std::{path::PathBuf, process::Stdio, sync::Arc};
|
use std::{path::PathBuf, process::Stdio, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
|
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||||
process::{self, Child},
|
process::{self, Child},
|
||||||
sync::{RwLock, broadcast, mpsc},
|
sync::{RwLock, broadcast, mpsc},
|
||||||
|
time::sleep,
|
||||||
};
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
use crate::config::stream::InstanceEvent;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{MinecraftType, MinecraftVersion, StreamLine, StreamSource},
|
config::{MinecraftType, MinecraftVersion, StreamSource, stream::EventPayload},
|
||||||
error::{HandleError, ServerError, SubscribeError},
|
error::{HandleError, ServerError, SubscribeError},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio_stream::StreamExt;
|
use super::{InstanceData, InstanceStatus};
|
||||||
|
|
||||||
#[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)]
|
|
||||||
pub enum InstanceStatus {
|
|
||||||
Starting,
|
|
||||||
Running,
|
|
||||||
Stopping,
|
|
||||||
Stopped,
|
|
||||||
Crashed,
|
|
||||||
Killing,
|
|
||||||
Killed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InstanceHandle {
|
pub struct InstanceHandle {
|
||||||
pub data: InstanceData,
|
pub data: InstanceData,
|
||||||
pub status: Arc<RwLock<InstanceStatus>>,
|
pub status: Arc<RwLock<InstanceStatus>>,
|
||||||
stdout_tx: broadcast::Sender<StreamLine>,
|
stdout_tx: broadcast::Sender<InstanceEvent>,
|
||||||
stderr_tx: Option<broadcast::Sender<StreamLine>>,
|
stderr_tx: Option<broadcast::Sender<InstanceEvent>>,
|
||||||
#[cfg(feature = "events")]
|
#[cfg(feature = "events")]
|
||||||
events_tx: broadcast::Sender<StreamLine>,
|
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_tx: mpsc::Sender<String>,
|
||||||
stdin_rx: Option<mpsc::Receiver<String>>,
|
stdin_rx: Option<mpsc::Receiver<String>>,
|
||||||
child: Option<Arc<RwLock<Child>>>,
|
child: Option<Arc<RwLock<Child>>>,
|
||||||
@@ -50,24 +41,26 @@ pub struct InstanceHandle {
|
|||||||
|
|
||||||
impl InstanceHandle {
|
impl InstanceHandle {
|
||||||
pub fn new_with_params(
|
pub fn new_with_params(
|
||||||
root_dir: &str,
|
root_dir: PathBuf,
|
||||||
jar_path: &str,
|
jar_path: PathBuf,
|
||||||
mc_version: &str,
|
mc_version: MinecraftVersion,
|
||||||
mc_type: MinecraftType,
|
mc_type: MinecraftType,
|
||||||
) -> Result<Self, HandleError> {
|
) -> Result<Self, HandleError> {
|
||||||
let parsed_version: MinecraftVersion = mc_version
|
let parsed_version: MinecraftVersion = mc_version;
|
||||||
.parse()
|
|
||||||
.map_err(|_| HandleError::InvalidVersion(mc_version.to_string()))?;
|
|
||||||
|
|
||||||
let root: PathBuf = root_dir.into();
|
let root: PathBuf = root_dir.clone().into();
|
||||||
if !root.exists() || !root.is_dir() {
|
if !root.exists() || !root.is_dir() {
|
||||||
return Err(HandleError::InvalidDirectory(root_dir.to_string()));
|
return Err(HandleError::InvalidDirectory(
|
||||||
|
root_dir.to_str().unwrap().to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path: PathBuf = jar_path.into();
|
let path: PathBuf = jar_path.clone().into();
|
||||||
let conc = root.join(path.clone());
|
let conc = root.join(path.clone());
|
||||||
if !path.is_relative() || !conc.is_file() {
|
if !path.is_relative() || !conc.is_file() {
|
||||||
return Err(HandleError::InvalidPathJAR(jar_path.to_string()));
|
return Err(HandleError::InvalidPathJAR(
|
||||||
|
jar_path.to_str().unwrap().to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = InstanceData {
|
let data = InstanceData {
|
||||||
@@ -80,6 +73,7 @@ impl InstanceHandle {
|
|||||||
let status = InstanceStatus::Stopped;
|
let status = InstanceStatus::Stopped;
|
||||||
|
|
||||||
let (stdin_tx, stdin_rx) = mpsc::channel(1024);
|
let (stdin_tx, stdin_rx) = mpsc::channel(1024);
|
||||||
|
let (internal_tx, internal_rx) = mpsc::channel(1024);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
data,
|
data,
|
||||||
status: Arc::new(RwLock::new(status)),
|
status: Arc::new(RwLock::new(status)),
|
||||||
@@ -87,6 +81,10 @@ impl InstanceHandle {
|
|||||||
stderr_tx: None,
|
stderr_tx: None,
|
||||||
#[cfg(feature = "events")]
|
#[cfg(feature = "events")]
|
||||||
events_tx: broadcast::Sender::new(2048),
|
events_tx: broadcast::Sender::new(2048),
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
internal_tx,
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
internal_rx: Some(internal_rx),
|
||||||
stdin_tx,
|
stdin_tx,
|
||||||
stdin_rx: Some(stdin_rx),
|
stdin_rx: Some(stdin_rx),
|
||||||
child: None,
|
child: None,
|
||||||
@@ -134,9 +132,25 @@ impl InstanceHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn transition_status(&self, status: InstanceStatus) {
|
async fn transition_status(&self, status: InstanceStatus) {
|
||||||
|
let r_guard = self.status.read().await;
|
||||||
|
let old = r_guard.clone();
|
||||||
|
drop(r_guard);
|
||||||
|
|
||||||
|
let new = status.clone();
|
||||||
|
|
||||||
let mut guard = self.status.write().await;
|
let mut guard = self.status.write().await;
|
||||||
*guard = status;
|
*guard = status;
|
||||||
drop(guard);
|
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 {
|
fn build_start_command(&self) -> process::Command {
|
||||||
@@ -173,22 +187,36 @@ impl InstanceHandle {
|
|||||||
|
|
||||||
let stdout_status = self.status.clone();
|
let stdout_status = self.status.clone();
|
||||||
let stderr_status = self.status.clone();
|
let stderr_status = 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_reader = BufReader::new(stdout).lines();
|
||||||
loop {
|
loop {
|
||||||
match stdout_reader.next_line().await {
|
match stdout_reader.next_line().await {
|
||||||
Ok(Some(line)) => {
|
Ok(Some(line)) => {
|
||||||
let _ = stdout_tx.send(StreamLine::stdout(line));
|
let _ = stdout_tx.send(InstanceEvent::stdout(line));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let status_guard = stdout_status.read().await;
|
let status_guard = stdout_status.read().await;
|
||||||
if *status_guard != InstanceStatus::Killing
|
let state = status_guard.clone();
|
||||||
|| *status_guard != InstanceStatus::Stopping
|
if state == InstanceStatus::Running && state == InstanceStatus::Starting {
|
||||||
{
|
let old = status_guard.clone();
|
||||||
drop(status_guard);
|
drop(status_guard);
|
||||||
let mut status = stdout_status.write().await;
|
let mut status = stdout_status.write().await;
|
||||||
*status = InstanceStatus::Crashed;
|
*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);
|
drop(status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -203,16 +231,28 @@ impl InstanceHandle {
|
|||||||
loop {
|
loop {
|
||||||
match stderr_reader.next_line().await {
|
match stderr_reader.next_line().await {
|
||||||
Ok(Some(line)) => {
|
Ok(Some(line)) => {
|
||||||
let _ = stderr_tx.send(StreamLine::stderr(line));
|
let _ = stderr_tx.send(InstanceEvent::stderr(line));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let status_guard = stderr_status.read().await;
|
let status_guard = stderr_status.read().await;
|
||||||
if *status_guard != InstanceStatus::Killing
|
let state = status_guard.clone();
|
||||||
|| *status_guard != InstanceStatus::Stopping
|
if state == InstanceStatus::Running && state == InstanceStatus::Starting {
|
||||||
{
|
let old = status_guard.clone();
|
||||||
drop(status_guard);
|
drop(status_guard);
|
||||||
let mut status = stderr_status.write().await;
|
let mut status = stderr_status.write().await;
|
||||||
*status = InstanceStatus::Crashed;
|
*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);
|
drop(status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -250,29 +290,42 @@ impl InstanceHandle {
|
|||||||
let stdout_stream = self
|
let stdout_stream = self
|
||||||
.subscribe(StreamSource::Stdout)
|
.subscribe(StreamSource::Stdout)
|
||||||
.map_err(|_| ServerError::NoStdoutPipe)?;
|
.map_err(|_| ServerError::NoStdoutPipe)?;
|
||||||
let shutdown = self.shutdown.clone();
|
let shutdown1 = self.shutdown.clone();
|
||||||
// TODO: Stream events!!!!
|
let shutdown2 = self.shutdown.clone();
|
||||||
let _event_tx = self.events_tx.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")]
|
#[cfg(feature = "mc-vanilla")]
|
||||||
if self.data.mc_type == MinecraftType::Vanilla {
|
if self.data.mc_type == MinecraftType::Vanilla {
|
||||||
use crate::config::LogMeta;
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut rx = stdout_stream;
|
let mut rx = stdout_stream;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown.cancelled() => {
|
_ = shutdown2.cancelled() => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
line = rx.next() => {
|
line = rx.next() => {
|
||||||
if let Some(Ok(val)) = line {
|
if let Some(Ok(val)) = line {
|
||||||
let msg = val.msg();
|
// TODO: Call parser
|
||||||
let meta = LogMeta::new(msg);
|
|
||||||
if let Ok(val) = meta
|
|
||||||
&& val.is_some() {
|
|
||||||
println!("{}", val.unwrap());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,10 +342,10 @@ impl InstanceHandle {
|
|||||||
|
|
||||||
child.kill().await.map_err(|_| ServerError::CommandFailed)?;
|
child.kill().await.map_err(|_| ServerError::CommandFailed)?;
|
||||||
|
|
||||||
|
self.transition_status(InstanceStatus::Killed).await;
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
self.shutdown.cancel();
|
self.shutdown.cancel();
|
||||||
self.child = None;
|
self.child = None;
|
||||||
|
|
||||||
self.transition_status(InstanceStatus::Killed).await;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ServerError::NotRunning)
|
Err(ServerError::NotRunning)
|
||||||
@@ -306,10 +359,11 @@ impl InstanceHandle {
|
|||||||
_ = self.send_command("stop").await;
|
_ = self.send_command("stop").await;
|
||||||
let mut child = child_arc.write().await;
|
let mut child = child_arc.write().await;
|
||||||
child.wait().await.map_err(|_| ServerError::CommandFailed)?;
|
child.wait().await.map_err(|_| ServerError::CommandFailed)?;
|
||||||
self.shutdown.cancel();
|
|
||||||
self.child = None;
|
|
||||||
|
|
||||||
self.transition_status(InstanceStatus::Stopped).await;
|
self.transition_status(InstanceStatus::Stopped).await;
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
self.shutdown.cancel();
|
||||||
|
self.child = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ServerError::NotRunning)
|
Err(ServerError::NotRunning)
|
||||||
@@ -319,7 +373,7 @@ impl InstanceHandle {
|
|||||||
pub fn subscribe(
|
pub fn subscribe(
|
||||||
&self,
|
&self,
|
||||||
stream: StreamSource,
|
stream: StreamSource,
|
||||||
) -> Result<BroadcastStream<StreamLine>, SubscribeError> {
|
) -> Result<BroadcastStream<InstanceEvent>, SubscribeError> {
|
||||||
match stream {
|
match stream {
|
||||||
StreamSource::Stdout => {
|
StreamSource::Stdout => {
|
||||||
let rx = self.stdout_tx.subscribe();
|
let rx = self.stdout_tx.subscribe();
|
||||||
5
src/instance/mod.rs
Normal file
5
src/instance/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod handle;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use handle::InstanceHandle;
|
||||||
|
pub use types::{InstanceData, InstanceStatus};
|
||||||
22
src/instance/types.rs
Normal file
22
src/instance/types.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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)]
|
||||||
|
pub enum InstanceStatus {
|
||||||
|
Starting,
|
||||||
|
Running,
|
||||||
|
Stopping,
|
||||||
|
Stopped,
|
||||||
|
Crashed,
|
||||||
|
Killing,
|
||||||
|
Killed,
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
pub mod manifests;
|
||||||
|
pub mod server;
|
||||||
|
pub mod utils;
|
||||||
|
|||||||
1
src/manifests/mod.rs
Normal file
1
src/manifests/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod vanilla;
|
||||||
114
src/manifests/vanilla.rs
Normal file
114
src/manifests/vanilla.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#![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)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/server/domain.rs
Normal file
123
src/server/domain.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/server/mod.rs
Normal file
1
src/server/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod domain;
|
||||||
24
src/utils.rs
Normal file
24
src/utils.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user