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}