serbo/lib.rs
1//! Allows for simple control, and input / output of minecraft servers.
2//!
3//! # Examples
4//! ```
5//!use serbo;
6//!use std::error::Error;
7//!use std::io;
8//!
9//!fn main() -> Result<(), Box<dyn Error>> {
10//! let mut manager = serbo::Manager::new("servers", "versions","fabric-server-launch.jar");
11//! let port = 25565;
12//! let id = "1";
13//! loop {
14//! let reader = io::stdin();
15//! let mut buf = String::new();
16//! println!("Enter your command.");
17//! reader.read_line(&mut buf)?;
18//! match buf.trim() {
19//! "delete" => {
20//! match manager.delete(id){
21//! Ok(_) => println!("Server deleted."),
22//! Err(e) => println!("{}",e)
23//! }
24//! }
25//! "change_version" => {
26//! let mut send_buf = String::new();
27//! println!("Enter the version to change to.");
28//! reader.read_line(&mut send_buf)?;
29//! //Remove the newline from read_line
30//! send_buf = send_buf[..send_buf.chars().count() - 1].to_string();
31//! manager.change_version(id, &send_buf)?;
32//! }
33//! "create" => match manager.create(id, "1.16.1-fabric") {
34//! Ok(_) => println!("Server Created"),
35//! Err(e) => println!("{}", e),
36//! },
37//! "stop" => {
38//! //Stops the server
39//! println!("Server stopping.");
40//! manager.stop(id)?;
41//! break Ok(());
42//! }
43//! "start" => {
44//! //Starts the server
45//! println!("Server starting.");
46//! match manager.start(id, port) {
47//! Err(e) => println!("{}", e),
48//! Ok(_) => println!("Server started!"),
49//! };
50//! }
51//! "send" => {
52//! //Prompts for a command to send to the server
53//! let instance = manager.get(id);
54//! match instance {
55//! Some(i) => {
56//! let mut send_buf = String::new();
57//! println!("Enter the command to send to the server.");
58//! reader.read_line(&mut send_buf)?;
59//! //Remove the newline from read_line
60//! send_buf = send_buf[..send_buf.chars().count() - 1].to_string();
61//! i.send(send_buf)?;
62//! }
63//! None => println!("Server offline."),
64//! }
65//! }
66//! "get" => {
67//! //Gets the last 5 stdout lines
68//! let instance: &serbo::Instance = manager.get(id).unwrap();
69//! let vec = instance.get(0);
70//! let length = vec.len();
71//! //Create a vec from the last 5 lines
72//! let trimmed_vec;
73//! if length >= 5 {
74//! trimmed_vec = Vec::from(&vec[length - 5..]);
75//! } else {
76//! trimmed_vec = Vec::from(vec);
77//! }
78//! for line in trimmed_vec {
79//! println!("{}", line);
80//! }
81//! }
82//! _ => {
83//! println!("Unrecognized command");
84//! }
85//! }
86//! }
87//!}
88//! ```
89
90use copy_dir::copy_dir;
91use std::collections::HashMap;
92use std::fmt;
93use std::fs;
94use std::io::{BufRead, BufReader, BufWriter, Write};
95use std::path::Path;
96use std::process::{Child, Command, Stdio};
97use std::sync::{Arc, Mutex};
98use std::thread;
99use std::thread::sleep;
100use std::time;
101
102type Result<T> = std::result::Result<T, Error>;
103
104
105#[derive(Debug)]
106pub enum Error {
107 /// Arises when there is an error regarding IO
108 IoError(std::io::Error),
109 /// Arises when an offline server is attempted to be used
110 ServerOffline(String),
111 /// Arises when a server's files are missing
112 ServerFilesMissing(String),
113 /// Arises when attempting to create a server with the same id as an existing server
114 ServerAlreadyExists(String),
115 /// Arises when there is an error involving a server's stdin/stdout threads
116 ThreadError(String)
117}
118
119impl std::error::Error for Error{
120 fn description(&self) -> &str{
121 match *self{
122 Error::IoError(_) => "IOError",
123 Error::ServerFilesMissing(_) => "MissingServer",
124 Error::ServerOffline(_) => "ServerOffline",
125 Error::ServerAlreadyExists(_) => "ServerAlreadyExists",
126 Error::ThreadError(_) => "ThreadError"
127 }
128 }
129}
130
131impl fmt::Display for Error {
132 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133 match *self {
134 Error::IoError(ref a) => write!(f,"Io error: {}",a),
135 Error::ServerFilesMissing(ref a) => write!(f,"Server missing with id:{}",a),
136 Error::ServerOffline(ref a) => write!(f,"Server offline with id:{}",a),
137 Error::ServerAlreadyExists(ref a) => write!(f,"Server already exists with id:{}",a),
138 Error::ThreadError(ref a) => write!(f,"Error while creating stdin/stdout threads for id:{}",a)
139 }
140 }
141}
142
143impl From<std::io::Error> for Error{
144 fn from(e:std::io::Error) -> Self{
145 Error::IoError(e)
146 }
147}
148
149/// Controls the creation and deleting of servers, and whether they are currently active.
150pub struct Manager {
151 servers: HashMap<String, Instance>,
152 server_files_folder: String,
153 version_folder: String,
154 jar_name: String
155}
156
157impl Manager {
158 /// Creates a new server manager
159 /// # Arguments
160 /// * `server_files_folder` - the folder that will hold each server's folder, which contains its server files.
161 /// * `version_folder` - the folder containing the base files of servers for the MC versions that you wish to host. Used as a base to create new servers.
162 /// # Examples
163 /// ```
164 /// let manager = serbo::Manager::new("folder1","folder2");
165 /// ```
166 /// # Remarks
167 /// The version_folder should be a folder that contains folders that are named the same as the MC server files they contain.
168 pub fn new(server_files_folder: &str, version_folder: &str, jar_name:&str) -> Manager {
169 Manager {
170 servers: HashMap::new(),
171 server_files_folder: server_files_folder.to_string(),
172 version_folder: version_folder.to_string(),
173 jar_name: jar_name.to_string()
174 }
175 }
176 /// Creates a new MC server folder under the `server_files_folder`
177 /// # Arguments
178 /// * `id` - The id for the server
179 /// * `version` - The target version for the server.
180 /// * `jar_name` - The name of the jar file that should be executed to start the server.
181 /// # Examples
182 /// ```
183 /// let manager = serbo::Manager::new("folder1","folder2");
184 /// manager.create("1","1.16.1");
185 /// ```
186 /// # Remarks
187 /// Returns a result that contains the id that was assigned to the server
188 pub fn create(&mut self, id: &str, version: &str) -> Result<()> {
189 if self.exists(id){
190 return Err(Error::ServerAlreadyExists(id.to_string()));
191 }
192 let target_folder = format!("{}/{}", self.server_files_folder, id);
193 let base_folder = format!("{}/{}", self.version_folder, version);
194 copy_dir(base_folder, target_folder)?;
195 Ok(())
196 }
197 /// Returns an Option<t> containing a [Instance](struct.Instance.html) that represents the currently online server represented by the provided id
198 /// # Arguments
199 /// * `id` - The id that represents the requested server
200 /// # Examples
201 /// ```
202 /// let manager = serbo::Manager::new("folder1","folder2");
203 /// //Returns an Option
204 /// let instance:serbo::Instance = manager.get("1").unwrap();
205 /// ```
206 /// # Remarks
207 /// Queries the currently online servers, for get to return, must have been launched by calling [start](struct.Manager.html#method.start)
208 pub fn get(&mut self, id: &str) -> Option<&mut Instance> {
209 self.servers.get_mut(id)
210 }
211 /// Checks if server files exist for a given id
212 /// # Arguments
213 /// * `id` - The id that represents the requested server
214 pub fn exists(&mut self, id: &str) -> bool {
215 Path::new(&format!("{}/{}", self.server_files_folder, id)).exists()
216 }
217 /// Checks if the server is online
218 /// # Arguments
219 /// * `id` - The id that represents the requested server
220 /// # Remarks
221 /// Queries the currently online servers, must have been launched by calling [start](struct.Manager.html#method.start)
222 pub fn is_online(&mut self, id: &str) -> bool {
223 match self.get(id) {
224 Some(_) => true,
225 None => false,
226 }
227 }
228 /// Launches a server
229 /// # Arguments
230 /// * `id` - The id that represents the requested server
231 /// * `port` - The port that the server should be started on
232 pub fn start(&mut self, id: &str, port: u32) -> Result<u32> {
233
234 if !self.exists(id){
235 return Err(Error::ServerFilesMissing(id.to_string()));
236 }
237
238 let mut command = Command::new("java");
239 command
240 .stdin(Stdio::piped())
241 .stdout(Stdio::piped())
242 .args(&[
243 "-Xmx1024M",
244 "-Xms1024M",
245 "-jar",
246 &self.jar_name,
247 "nogui",
248 "--port",
249 &port.to_string(),
250 ])
251 .current_dir(format!("{}/{}", self.server_files_folder, id.to_string()));
252 let child = command.spawn()?;
253 let mut serv_inst = Instance {
254 server_process: child,
255 stdout_join: None,
256 stdin_join: None,
257 console_log: Arc::new(Mutex::new(Vec::new())),
258 stdin_queue: Arc::new(Mutex::new(Vec::new())),
259 port: port,
260 id: id.to_string()
261 };
262 let stdout = match serv_inst.server_process.stdout.take(){
263 Some(e) => e,
264 None => return Err(Error::ThreadError(id.to_string()))
265 };
266 let stdin = match serv_inst.server_process.stdin.take(){
267 Some(e) => e,
268 None => return Err(Error::ThreadError(id.to_string()))
269 };
270 let stdout_arc = serv_inst.console_log.clone();
271 let stdin_arc = serv_inst.stdin_queue.clone();
272 let stdout_thread_handle = thread::spawn(move || {
273 let reader = BufReader::new(stdout).lines();
274 reader.filter_map(|line| line.ok()).for_each(|line| {
275 let mut lock = stdout_arc.lock().unwrap();
276 lock.push(line);
277 });
278 });
279 let stdin_thread_handle = thread::spawn(move || {
280 let mut writer = BufWriter::new(stdin);
281 loop {
282 let mut vec = stdin_arc.lock().unwrap();
283 vec.drain(..).for_each(|x| {
284 writeln!(writer, "{}", x);
285 writer.flush();
286 });
287 drop(vec);
288 sleep(time::Duration::from_secs(2));
289 }
290 });
291 serv_inst.stdin_join = Some(stdin_thread_handle);
292 serv_inst.stdout_join = Some(stdout_thread_handle);
293 &self.servers.insert(id.to_string(), serv_inst);
294 Ok(port)
295 }
296 /// Stops a server
297 /// # Arguments
298 /// * `id` - The id that represents the requested server
299 pub fn stop(&mut self, id: &str) -> Result<()> {
300 let serv = self.servers.get_mut(id);
301 if let Some(inst) = serv {
302 inst.stop()?;
303 inst.stdout_join.take().unwrap().join();
304 inst.stdin_join.take().unwrap().join();
305 return Ok(());
306 }
307 Err(Error::ServerOffline(id.to_string()))
308 }
309 /// Deletes a server's files
310 /// # Arguments
311 /// * `id` - The id that represents the requested server
312 /// # Remarks
313 /// Stops the server if it's currently running
314 pub fn delete(&mut self, id: &str) -> Result<()> {
315 let _ = self.stop(id);
316 if !self.exists(id){
317 return Err(Error::ServerFilesMissing(id.to_string()));
318 }
319 fs::remove_dir_all(format!("{}/{}", &self.server_files_folder, id))?;
320 Ok(())
321 }
322 /// Changes a server's version
323 /// # Arguments
324 /// * `id` - The id that represents the requested server
325 /// * `target` - The target version to be switched to
326 /// # Remarks
327 /// Stops the server if it's currently running
328 pub fn change_version(&mut self, id: &str, target: &str) -> Result<()> {
329 let _ = self.stop(id);
330 if !self.exists(id){
331 return Err(Error::ServerFilesMissing(id.to_string()));
332 }
333 fs::remove_file(format!("{}/{}/server.jar", self.server_files_folder, id))?;
334 fs::copy(
335 format!("{}/{}/server.jar", self.version_folder, target),
336 format!("{}/{}/server.jar", self.server_files_folder, id),
337 )?;
338 Ok(())
339 }
340}
341
342/// Represents a currently online server.
343/// Created by calling [start](struct.Manager.html#method.start) from a [Manager](struct.Manager.html)
344pub struct Instance {
345 pub server_process: Child,
346 stdout_join: Option<thread::JoinHandle<()>>,
347 stdin_join: Option<thread::JoinHandle<()>>,
348 console_log: Arc<Mutex<Vec<String>>>,
349 stdin_queue: Arc<Mutex<Vec<String>>>,
350 pub port: u32,
351 pub id: String
352}
353
354impl Instance {
355 /// Stops the server, killing the server process and the stdin
356 /// and stdout threads
357 pub fn stop(&mut self) -> Result<()> {
358 self.server_process.kill()?;
359 Ok(())
360 }
361 /// Sends a string to the server stdin
362 /// # Arguments
363 /// * `msg` - A String that contains the message to be sent to the server.
364 ///
365 /// # Remarks
366 /// The message should not contain a trailing newline, as the send method handles it.
367 pub fn send(&mut self, msg: String) -> Result<()> {
368 let vec_lock = self.stdin_queue.clone();
369 let mut vec = vec_lock.lock().unwrap();
370 vec.push(msg);
371 Ok(())
372 }
373 //// Gets the output from server stdout
374 /// # Arguments
375 /// * `start` The line number of the first line that should be returned
376 ///
377 /// # Remarks
378 /// The returned Vec will contain the lines in the range of start to the end of output
379 pub fn get(&self, start: u32) -> Vec<String> {
380 let vec_lock = self.console_log.clone();
381 let vec = vec_lock.lock().unwrap();
382 let mut start_line = start as usize;
383 if start_line > vec.len() {
384 start_line = vec.len()
385 }
386 Vec::from(&vec[start_line..])
387 }
388}