mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
743 lines
28 KiB
Rust
743 lines
28 KiB
Rust
// Not warn event sending macros
|
|
#![allow(unused_labels)]
|
|
|
|
#[macro_use]
|
|
extern crate pumpkin_macros;
|
|
|
|
use crate::crash::CrashReport;
|
|
use crate::data::VanillaData;
|
|
use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper};
|
|
use crate::net::bedrock::BedrockClient;
|
|
use crate::net::java::JavaClient;
|
|
use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult};
|
|
use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer};
|
|
use crate::plugin::server::server_command::ServerCommandEvent;
|
|
use crate::server::{Server, ticker::Ticker};
|
|
use plugin::server::server_load::{LoadType, ServerLoadEvent};
|
|
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration};
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_util::text::color::{Color, NamedColor};
|
|
use rustyline::Editor;
|
|
use rustyline::history::FileHistory;
|
|
use rustyline::{Config, error::ReadlineError};
|
|
use std::collections::HashMap;
|
|
use std::io::{Cursor, ErrorKind, IsTerminal, stdin};
|
|
use std::process::exit;
|
|
use std::str::FromStr;
|
|
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
|
use std::sync::{Arc, OnceLock};
|
|
use std::time::Duration;
|
|
use std::{net::SocketAddr, sync::LazyLock};
|
|
use tokio::net::{TcpListener, UdpSocket};
|
|
use tokio::select;
|
|
use tokio::sync::Mutex;
|
|
use tokio::time::sleep;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tokio_util::task::TaskTracker;
|
|
use tracing::{debug, error, info, warn};
|
|
use tracing_subscriber::filter::LevelFilter;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
pub mod block;
|
|
pub mod command;
|
|
pub mod crash;
|
|
pub mod data;
|
|
pub mod entity;
|
|
pub mod error;
|
|
pub mod item;
|
|
pub mod logging;
|
|
pub mod net;
|
|
pub mod plugin;
|
|
pub mod server;
|
|
pub mod world;
|
|
|
|
pub struct LoggingConfig {
|
|
pub color: bool,
|
|
pub threads: bool,
|
|
pub timestamp: bool,
|
|
}
|
|
|
|
pub type LoggerOption = Option<(ReadlineLogWrapper, LevelFilter, LoggingConfig)>;
|
|
pub static LOGGER_IMPL: LazyLock<Arc<OnceLock<LoggerOption>>> =
|
|
LazyLock::new(|| Arc::new(OnceLock::new()));
|
|
|
|
#[expect(clippy::print_stderr)]
|
|
pub fn init_logger(advanced_config: &AdvancedConfiguration) {
|
|
use tracing_subscriber::EnvFilter;
|
|
use tracing_subscriber::fmt;
|
|
|
|
let logger = advanced_config.logging.enabled.then(|| {
|
|
let level = std::env::var("RUST_LOG")
|
|
.ok()
|
|
.as_deref()
|
|
.map(LevelFilter::from_str)
|
|
.and_then(Result::ok)
|
|
.unwrap_or(LevelFilter::INFO);
|
|
|
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
|
let level_str = match level {
|
|
LevelFilter::OFF => "off",
|
|
LevelFilter::ERROR => "error",
|
|
LevelFilter::WARN => "warn",
|
|
LevelFilter::INFO => "info",
|
|
LevelFilter::DEBUG => "debug",
|
|
LevelFilter::TRACE => "trace",
|
|
};
|
|
EnvFilter::new(level_str)
|
|
});
|
|
|
|
let file_logger: Option<GzipRollingLogger> = if advanced_config.logging.file.is_empty() {
|
|
None
|
|
} else {
|
|
Some(
|
|
GzipRollingLogger::new(level, advanced_config.logging.file.clone())
|
|
.expect("Failed to initialize file logger."),
|
|
)
|
|
};
|
|
|
|
let (logger, rl): (
|
|
Box<dyn std::io::Write + Send + 'static>,
|
|
Option<Editor<PumpkinCommandCompleter, FileHistory>>,
|
|
) = if advanced_config.commands.use_tty && stdin().is_terminal() {
|
|
let rl_config = Config::builder()
|
|
.auto_add_history(true)
|
|
.completion_type(rustyline::CompletionType::List)
|
|
.edit_mode(rustyline::EditMode::Emacs)
|
|
.build();
|
|
let helper = PumpkinCommandCompleter::new();
|
|
|
|
match Editor::with_config(rl_config) {
|
|
Ok(mut rl) => {
|
|
rl.set_helper(Some(helper));
|
|
(Box::new(std::io::stdout()), Some(rl))
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Failed to initialize console input ({e}); falling back to simple logger"
|
|
);
|
|
(Box::new(std::io::stdout()), None)
|
|
}
|
|
}
|
|
} else {
|
|
(Box::new(std::io::stdout()), None)
|
|
};
|
|
|
|
let fmt_layer = fmt::layer()
|
|
.with_writer(std::sync::Mutex::new(logger))
|
|
.with_ansi(advanced_config.logging.color)
|
|
.with_ansi_sanitization(false)
|
|
.with_target(true)
|
|
.with_thread_names(advanced_config.logging.threads)
|
|
.with_thread_ids(advanced_config.logging.threads);
|
|
|
|
if advanced_config.logging.timestamp {
|
|
let local_offset =
|
|
time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
|
let fmt_layer = fmt_layer.with_timer(fmt::time::OffsetTime::new(
|
|
local_offset,
|
|
time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
|
));
|
|
let registry = tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer);
|
|
if let Some(file_logger) = file_logger {
|
|
registry.with(file_logger).init();
|
|
} else {
|
|
registry.init();
|
|
}
|
|
} else {
|
|
let fmt_layer = fmt_layer.without_time();
|
|
let registry = tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer);
|
|
if let Some(file_logger) = file_logger {
|
|
registry.with(file_logger).init();
|
|
} else {
|
|
registry.init();
|
|
}
|
|
}
|
|
|
|
let logging_config = LoggingConfig {
|
|
color: advanced_config.logging.color,
|
|
threads: advanced_config.logging.threads,
|
|
timestamp: advanced_config.logging.timestamp,
|
|
};
|
|
|
|
(ReadlineLogWrapper::new(rl), level, logging_config)
|
|
});
|
|
|
|
assert!(
|
|
LOGGER_IMPL.set(logger).is_ok(),
|
|
"Failed to set logger. already initialized"
|
|
);
|
|
}
|
|
|
|
pub static SHOULD_STOP: AtomicBool = AtomicBool::new(false);
|
|
pub static STOP_INTERRUPT: LazyLock<CancellationToken> = LazyLock::new(CancellationToken::new);
|
|
pub static SERVER_IS_STOPPING: AtomicBool = AtomicBool::new(false);
|
|
pub static CRASH_REPORT: OnceLock<CrashReport> = OnceLock::new();
|
|
pub static SERVER_EXIT_CODE: AtomicI32 = AtomicI32::new(0);
|
|
|
|
pub fn stop_server() {
|
|
SHOULD_STOP.store(true, Ordering::Relaxed);
|
|
STOP_INTERRUPT.cancel();
|
|
}
|
|
|
|
pub fn stop_or_exit_server() {
|
|
if SERVER_IS_STOPPING.load(Ordering::Acquire) {
|
|
// Server is already stopping, so we forcefully exit.
|
|
exit(SERVER_EXIT_CODE.load(Ordering::Acquire));
|
|
}
|
|
stop_server();
|
|
}
|
|
|
|
fn resolve_some<T: Future, D, F: FnOnce(D) -> T>(
|
|
opt: Option<D>,
|
|
func: F,
|
|
) -> futures::future::Either<T, std::future::Pending<T::Output>> {
|
|
use futures::future::Either;
|
|
opt.map_or_else(
|
|
|| Either::Right(std::future::pending()),
|
|
|val| Either::Left(func(val)),
|
|
)
|
|
}
|
|
|
|
pub struct PumpkinServer {
|
|
pub server: Arc<Server>,
|
|
pub tcp_listener: Option<TcpListener>,
|
|
pub udp_socket: Option<Arc<UdpSocket>>,
|
|
}
|
|
|
|
impl PumpkinServer {
|
|
pub fn log_info(&self, message: &str) {
|
|
tracing::info!(target: "plugin", "{}", message);
|
|
}
|
|
pub async fn new(
|
|
basic_config: BasicConfiguration,
|
|
advanced_config: AdvancedConfiguration,
|
|
vanilla_data: VanillaData,
|
|
) -> Self {
|
|
let server = Server::new(basic_config, advanced_config, vanilla_data).await;
|
|
|
|
let rcon = server.advanced_config.networking.rcon.clone();
|
|
|
|
if rcon.enabled {
|
|
warn!(
|
|
"RCON is enabled, but it's highly insecure as it transmits passwords and commands in plain text. This makes it vulnerable to interception and exploitation by anyone on the network"
|
|
);
|
|
let rcon_server = server.clone();
|
|
server.spawn_task(async move {
|
|
RCONServer::run(&rcon, rcon_server).await;
|
|
});
|
|
}
|
|
|
|
let tcp_listener = if server.advanced_config.networking.java.enabled {
|
|
let address = server.advanced_config.networking.java.address;
|
|
// Setup the TCP server socket.
|
|
let listener = match TcpListener::bind(address).await {
|
|
Ok(l) => l,
|
|
Err(e) => match e.kind() {
|
|
ErrorKind::AddrInUse => {
|
|
error!("Error: Address {address} is already in use.");
|
|
error!("Make sure another instance of the server isn't already running");
|
|
std::process::exit(1);
|
|
}
|
|
ErrorKind::PermissionDenied => {
|
|
error!("Error: Permission denied when binding to {address}.");
|
|
error!("You might need sudo/admin privileges to use ports below 1024");
|
|
std::process::exit(1);
|
|
}
|
|
ErrorKind::AddrNotAvailable => {
|
|
error!("Error: The address {address} is not available on this machine");
|
|
std::process::exit(1);
|
|
}
|
|
_ => {
|
|
error!("Failed to start TcpListener on {address}: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
},
|
|
};
|
|
// In the event the user puts 0 for their port, this will allow us to know what port it is running on
|
|
let addr = listener
|
|
.local_addr()
|
|
.expect("Unable to get the address of the server!");
|
|
|
|
if server.advanced_config.networking.query.enabled {
|
|
info!("Query protocol is enabled. Starting...");
|
|
server.spawn_task(query::start_query_handler(
|
|
server.clone(),
|
|
server.advanced_config.networking.query.address,
|
|
));
|
|
}
|
|
|
|
if server.advanced_config.networking.lan_broadcast.enabled {
|
|
info!("LAN broadcast is enabled. Starting...");
|
|
|
|
let lan_broadcast = LANBroadcast::new(
|
|
&server.advanced_config.networking.lan_broadcast,
|
|
&server.advanced_config.networking.java.motd,
|
|
);
|
|
server.spawn_task(lan_broadcast.start(addr));
|
|
}
|
|
|
|
Some(listener)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Ticker
|
|
{
|
|
let ticker_server = server.clone();
|
|
server.spawn_task(async move {
|
|
Ticker::run(&ticker_server).await;
|
|
});
|
|
};
|
|
|
|
let udp_socket = if server.advanced_config.networking.bedrock.enabled {
|
|
Some(Arc::new(
|
|
UdpSocket::bind(server.advanced_config.networking.bedrock.address)
|
|
.await
|
|
.expect("Failed to bind UDP Socket"),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Self {
|
|
server,
|
|
tcp_listener,
|
|
udp_socket,
|
|
}
|
|
}
|
|
|
|
pub async fn init_plugins(&self) -> std::time::Duration {
|
|
match self.server.plugin_manager.load_plugins(&self.server).await {
|
|
Ok(duration) => duration,
|
|
Err(err) => {
|
|
error!("{err}");
|
|
std::time::Duration::ZERO
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn unload_plugins(&self) {
|
|
if let Err(err) = self.server.plugin_manager.unload_all_plugins().await {
|
|
error!("Error unloading plugins: {err}");
|
|
} else {
|
|
info!("All plugins unloaded successfully");
|
|
}
|
|
}
|
|
|
|
pub async fn start(&self) {
|
|
if self.server.advanced_config.commands.use_console
|
|
&& let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
|
|
{
|
|
if let Some(rl) = wrapper.take_readline() {
|
|
setup_console(rl, self.server.clone());
|
|
} else {
|
|
if self.server.advanced_config.commands.use_tty {
|
|
warn!(
|
|
"The input is not a TTY; falling back to simple logger and ignoring `use_tty` setting"
|
|
);
|
|
}
|
|
setup_stdin_console(self.server.clone());
|
|
}
|
|
}
|
|
|
|
let tasks = Arc::new(TaskTracker::new());
|
|
let mut master_client_id: u64 = 0;
|
|
let bedrock_clients = Arc::new(Mutex::new(HashMap::new()));
|
|
|
|
self.server
|
|
.plugin_manager
|
|
.fire(&self.server, &mut ServerLoadEvent::new(LoadType::Startup))
|
|
.await;
|
|
|
|
while !SHOULD_STOP.load(Ordering::Relaxed) {
|
|
if !self
|
|
.unified_listener_task(&mut master_client_id, &tasks, &bedrock_clients)
|
|
.await
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
SERVER_IS_STOPPING.store(true, Ordering::Release);
|
|
|
|
if let Some(crash_report) = CRASH_REPORT.get() {
|
|
crash_report.print_to_console();
|
|
crash_report.save_and_log();
|
|
|
|
info!(
|
|
"{}",
|
|
TextComponent::text("Gracefully shutting down...")
|
|
.color(Color::Named(NamedColor::Green))
|
|
.to_pretty_console()
|
|
);
|
|
|
|
SERVER_EXIT_CODE.store(1, Ordering::Release);
|
|
}
|
|
|
|
info!("Stopped accepting incoming connections");
|
|
|
|
if let Err(e) = self
|
|
.server
|
|
.player_data_storage
|
|
.save_all_players(&self.server)
|
|
.await
|
|
{
|
|
error!("Error saving all players during shutdown: {e}");
|
|
}
|
|
|
|
if let Err(e) = self
|
|
.server
|
|
.advancement_manager
|
|
.save_all_players(&self.server.get_all_players())
|
|
.await
|
|
{
|
|
error!("Error saving all players advancements during shutdown: {e}");
|
|
}
|
|
|
|
let kick_message = TextComponent::text("Server stopped");
|
|
for player in self.server.get_all_players() {
|
|
player
|
|
.kick(DisconnectReason::Shutdown, kick_message.clone())
|
|
.await;
|
|
}
|
|
|
|
info!("Ending player tasks");
|
|
|
|
tasks.close();
|
|
tasks.wait().await;
|
|
|
|
self.unload_plugins().await;
|
|
|
|
info!("Starting save.");
|
|
|
|
self.server.shutdown().await;
|
|
|
|
info!("Completed save!");
|
|
|
|
if let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
|
|
&& let Some(rl) = wrapper.take_readline()
|
|
{
|
|
let _ = rl;
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
pub async fn unified_listener_task(
|
|
&self,
|
|
master_client_id_counter: &mut u64,
|
|
tasks: &Arc<TaskTracker>,
|
|
bedrock_clients: &Arc<Mutex<HashMap<SocketAddr, Arc<BedrockClient>>>>,
|
|
) -> bool {
|
|
let mut udp_buf = [0; 1496]; // Buffer for UDP receive
|
|
|
|
select! {
|
|
// Branch for TCP connections (Java Edition)
|
|
tcp_result = resolve_some(self.tcp_listener.as_ref(), tokio::net::TcpListener::accept) => {
|
|
match tcp_result {
|
|
Ok((connection, client_addr)) => {
|
|
if let Err(e) = connection.set_nodelay(true) {
|
|
warn!("Failed to set TCP_NODELAY: {e}");
|
|
}
|
|
|
|
let client_id = *master_client_id_counter;
|
|
*master_client_id_counter += 1;
|
|
|
|
let formatted_address = if self.server.basic_config.scrub_ips {
|
|
scrub_address(&format!("{client_addr}"))
|
|
} else {
|
|
format!("{client_addr}")
|
|
};
|
|
debug!("Accepted connection from Java Edition: {formatted_address} (id {client_id})");
|
|
let server_clone = self.server.clone();
|
|
|
|
tasks.spawn(async move {
|
|
let mut java_client = JavaClient::new(connection, client_addr, client_id);
|
|
java_client.start_outgoing_packet_task();
|
|
let login_result = java_client.handle_login_sequence(&server_clone).await;
|
|
|
|
match login_result {
|
|
PacketHandlerResult::Stop => {
|
|
java_client.close();
|
|
java_client.await_tasks().await;
|
|
},
|
|
PacketHandlerResult::ReadyToPlay(profile,config) => {
|
|
if let Some((player, world)) = server_clone
|
|
.add_player(Arc::new(ClientPlatform::Java(java_client)), profile, Some(config))
|
|
.await
|
|
{
|
|
// Set the player on the client BEFORE spawning so that chunk
|
|
// sends during spawn_java_player don't get silently dropped.
|
|
if let ClientPlatform::Java(client) = player.client.as_ref() {
|
|
*client.player.lock().await = Some(player.clone());
|
|
}
|
|
world
|
|
.spawn_java_player(&server_clone.basic_config, &player, &server_clone)
|
|
.await;
|
|
if let ClientPlatform::Java(client) = player.client.as_ref() {
|
|
client.progress_player_packets(&player, &server_clone).await;
|
|
|
|
// Close when done
|
|
client.close();
|
|
client.await_tasks().await;
|
|
}
|
|
player.remove().await;
|
|
server_clone.remove_player(&player).await;
|
|
if let Err(e) = server_clone.player_data_storage
|
|
.handle_player_leave(&player)
|
|
.await {
|
|
error!("Failed to save player data on disconnect: {e}");
|
|
}
|
|
if let Err(e) = server_clone.advancement_manager
|
|
.save_player(&player)
|
|
.await {
|
|
error!("Failed to save player advancement on disconnect: {e}");
|
|
}
|
|
}
|
|
},
|
|
}
|
|
});
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to accept Java client connection: {e}");
|
|
sleep(Duration::from_millis(50)).await;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Branch for UDP packets (Bedrock Edition)
|
|
udp_result = resolve_some(self.udp_socket.as_ref(), |sock: &Arc<UdpSocket>| sock.recv_from(&mut udp_buf)) => {
|
|
match udp_result {
|
|
Ok((len, client_addr)) => {
|
|
if len > 0 {
|
|
let Some(socket) = self.udp_socket.clone() else {
|
|
error!("UDP socket disappeared during receive");
|
|
return true;
|
|
};
|
|
|
|
let id = udp_buf[0];
|
|
let is_online = id & pumpkin_protocol::bedrock::RAKNET_VALID != 0;
|
|
|
|
if is_online {
|
|
let be_clients = bedrock_clients.clone();
|
|
let mut clients_guard = bedrock_clients.lock().await;
|
|
|
|
if clients_guard
|
|
.get(&client_addr)
|
|
.is_some_and(|client| client.is_closed())
|
|
{
|
|
clients_guard.remove(&client_addr);
|
|
}
|
|
|
|
let mut is_new = false;
|
|
let client = clients_guard.entry(client_addr).or_insert_with(|| {
|
|
is_new = true;
|
|
*master_client_id_counter += 1;
|
|
|
|
let new_client = Arc::new(BedrockClient::new(
|
|
socket,
|
|
client_addr,
|
|
be_clients
|
|
));
|
|
|
|
new_client.start_outgoing_packet_task();
|
|
new_client
|
|
}).clone();
|
|
|
|
if is_new {
|
|
let server_clone = self.server.clone();
|
|
let client_clone = client.clone();
|
|
tasks.spawn(async move {
|
|
let login_result = client_clone.handle_login_sequence(&server_clone).await;
|
|
|
|
match login_result {
|
|
PacketHandlerResult::Stop => {
|
|
client_clone.close().await;
|
|
client_clone.await_tasks().await;
|
|
}
|
|
PacketHandlerResult::ReadyToPlay(profile, config) => {
|
|
if let Some((player, _world)) = server_clone
|
|
.add_player(Arc::new(ClientPlatform::Bedrock(client_clone.clone())), profile, Some(config))
|
|
.await
|
|
{
|
|
*client_clone.player.lock().await = Some(player.clone());
|
|
|
|
client_clone.progress_player_packets(&player, &server_clone).await;
|
|
|
|
client_clone.close().await;
|
|
client_clone.await_tasks().await;
|
|
player.remove().await;
|
|
server_clone.remove_player(&player).await;
|
|
if let Err(e) = server_clone.player_data_storage
|
|
.handle_player_leave(&player)
|
|
.await {
|
|
error!("Failed to save player data on disconnect: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let packet_bytes = udp_buf[..len].to_vec();
|
|
let server = self.server.clone();
|
|
|
|
tasks.spawn(async move {
|
|
client.process_packet(&server, packet_bytes.into()).await;
|
|
});
|
|
} else if let Some(sock) = self.udp_socket.as_ref() {
|
|
let _ = BedrockClient::handle_offline_packet(
|
|
&self.server,
|
|
id,
|
|
&mut Cursor::new(&udp_buf[1..len]),
|
|
client_addr,
|
|
sock,
|
|
bedrock_clients,
|
|
).await;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => error!("UDP socket error: {e}"),
|
|
}
|
|
},
|
|
|
|
// Branch for the global stop signal
|
|
() = STOP_INTERRUPT.cancelled() => {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
fn setup_stdin_console(server: Arc<Server>) {
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
let rt = tokio::runtime::Handle::current();
|
|
std::thread::spawn(move || {
|
|
while !SHOULD_STOP.load(Ordering::Relaxed) {
|
|
let mut line = String::new();
|
|
if let Ok(size) = stdin().read_line(&mut line) {
|
|
// if no bytes were read, we may have hit EOF
|
|
if size == 0 {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
if line.is_empty() || line.as_bytes()[line.len() - 1] != b'\n' {
|
|
warn!("Console command was not terminated with a newline");
|
|
}
|
|
rt.block_on(tx.send(line.trim().to_string()))
|
|
.expect("Failed to send command to server");
|
|
}
|
|
});
|
|
tokio::spawn(async move {
|
|
while !SHOULD_STOP.load(Ordering::Relaxed) {
|
|
if let Some(command) = rx.recv().await {
|
|
let mut event = ServerCommandEvent::new(command.clone());
|
|
server.plugin_manager.fire(&server, &mut event).await;
|
|
if !event.cancelled {
|
|
server
|
|
.command_dispatcher
|
|
.read()
|
|
.await
|
|
.handle_command(
|
|
&command::CommandSender::Console.into_source(&server).await,
|
|
command.as_str(),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn setup_console(mut rl: Editor<PumpkinCommandCompleter, FileHistory>, server: Arc<Server>) {
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
let (tx_reply, mut rx_reply) = tokio::sync::mpsc::channel(1);
|
|
|
|
if let Some(helper) = rl.helper_mut() {
|
|
if let Ok(mut server_lock) = helper.server.write() {
|
|
*server_lock = Some(server.clone());
|
|
}
|
|
let _ = helper.rt.set(tokio::runtime::Handle::current());
|
|
}
|
|
|
|
std::thread::spawn(move || {
|
|
while !SHOULD_STOP.load(Ordering::Relaxed) {
|
|
let readline = rl.readline("$ ");
|
|
match readline {
|
|
Ok(line) => {
|
|
let _ = rl.add_history_entry(line.clone());
|
|
if tx.blocking_send(line).is_err() {
|
|
break;
|
|
}
|
|
|
|
// Wait for the command to be fully processed before continuing
|
|
let _ = rx_reply.blocking_recv();
|
|
}
|
|
Err(ReadlineError::Interrupted) => {
|
|
info!("CTRL-C");
|
|
stop_or_exit_server();
|
|
break;
|
|
}
|
|
Err(ReadlineError::Eof) => {
|
|
info!("CTRL-D");
|
|
stop_server();
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
error!("Error reading console input: {err}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if let Some((wrapper, _, _)) = LOGGER_IMPL.wait() {
|
|
wrapper.return_readline(rl);
|
|
}
|
|
});
|
|
|
|
server.clone().spawn_task(async move {
|
|
while !SHOULD_STOP.load(Ordering::Relaxed) {
|
|
let t1 = rx.recv();
|
|
let t2 = STOP_INTERRUPT.cancelled();
|
|
|
|
let result = select! {
|
|
line = t1 => line,
|
|
() = t2 => None,
|
|
};
|
|
|
|
if let Some(line) = result {
|
|
let mut event = ServerCommandEvent::new(line.clone());
|
|
server.plugin_manager.fire(&server, &mut event).await;
|
|
if !event.cancelled {
|
|
server
|
|
.command_dispatcher
|
|
.read()
|
|
.await
|
|
.handle_command(
|
|
&command::CommandSender::Console.into_source(&server).await,
|
|
&line,
|
|
)
|
|
.await;
|
|
let _ = tx_reply.send(1).await;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
drop(rx);
|
|
debug!("Stopped console commands task");
|
|
});
|
|
}
|
|
|
|
fn scrub_address(ip: &str) -> String {
|
|
ip.chars()
|
|
.map(|ch| if ch == '.' || ch == ':' { ch } else { 'x' })
|
|
.collect()
|
|
}
|