├── .gitignore ├── .travis.yml ├── COPYING.lesser ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── build.rs ├── header.txt ├── paperd-jni ├── Cargo.lock ├── Cargo.toml ├── header.h └── src │ ├── lib.rs │ ├── macros.rs │ └── util.rs ├── paperd-lib ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── protocol.md ├── readme.md ├── release ├── .gitignore ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── paperd_install.sh ├── readme.md ├── settings.gradle.kts └── targets │ ├── .dockerignore │ ├── build_release.sh │ ├── centos │ ├── centos.Dockerfile │ └── versions.txt │ ├── debian │ ├── debian.Dockerfile │ └── versions.txt │ ├── fedora │ ├── fedora.Dockerfile │ └── versions.txt │ └── ubuntu │ ├── ubuntu.Dockerfile │ └── versions.txt ├── src ├── cmd.rs ├── console │ ├── ansi.rs │ └── mod.rs ├── daemon.rs ├── log.rs ├── main.rs ├── messages.rs ├── messaging.rs ├── protocol.rs ├── restart.rs ├── runner.rs ├── send.rs ├── status.rs ├── stop.rs ├── timings.rs └── util.rs └── usage.md /.gitignore: -------------------------------------------------------------------------------- 1 | /paperd.tar.xz 2 | /paperd 3 | 4 | .idea/ 5 | 6 | /target 7 | /paperd-lib/target 8 | /paperd-jni/target 9 | **/*.rs.bk 10 | 11 | ### Linux ### 12 | *~ 13 | 14 | # temporary files which can be created if a process still has a handle open of a deleted file 15 | .fuse_hidden* 16 | 17 | # KDE directory preferences 18 | .directory 19 | 20 | # Linux trash folder which might appear on any partition or disk 21 | .Trash-* 22 | 23 | # .nfs files are created when an open file is removed but is still being accessed 24 | .nfs* 25 | 26 | ### macOS ### 27 | # General 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | ### Windows ### 55 | # Windows thumbnail cache files 56 | Thumbs.db 57 | ehthumbs.db 58 | ehthumbs_vista.db 59 | 60 | # Dump file 61 | *.stackdump 62 | 63 | # Folder config file 64 | [Dd]esktop.ini 65 | 66 | # Recycle Bin used on file shares 67 | $RECYCLE.BIN/ 68 | 69 | # Windows Installer files 70 | *.cab 71 | *.msi 72 | *.msix 73 | *.msm 74 | *.msp 75 | 76 | # Windows shortcuts 77 | *.lnk 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | env: 7 | - CMD="cargo build" 8 | - CMD="cargo build --features console" 9 | matrix: 10 | allow_failures: 11 | - rust: beta 12 | - rust: nightly 13 | fast_finish: true 14 | cache: cargo 15 | script: 16 | - $CMD 17 | addons: 18 | apt: 19 | packages: 20 | - libncurses5 21 | - libncursesw5 22 | - libncurses5-dev 23 | - libncursesw5-dev 24 | update: true 25 | -------------------------------------------------------------------------------- /COPYING.lesser: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paperd" 3 | version = "1.1.0-snapshot" 4 | authors = ["Kyle Wood "] 5 | edition = "2018" 6 | license = "LGPL-3.0-only" 7 | 8 | [features] 9 | console = ["ncurses"] 10 | 11 | [profile.release] 12 | opt-level = 'z' # Optimize for size. 13 | lto = true 14 | codegen-units = 1 15 | 16 | [dependencies] 17 | clap = "2.33.1" 18 | crossbeam-channel = "0.4.2" 19 | ncurses = { version = "5.99.0", optional = true, features = ['wide'] } 20 | nix = "0.17.0" 21 | serde = { version = "1.0.110", features = ["derive"] } 22 | serde_json = "1.0.53" 23 | shellexpand = "2.0.0" 24 | signal-hook = "0.1.15" 25 | sys-info = "0.6.1" 26 | zip = "0.5.5" 27 | paperd-lib = { path = "./paperd-lib/" } 28 | 29 | [build-dependencies] 30 | flate2 = "1.0.14" 31 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use flate2::write::GzEncoder; 17 | use flate2::Compression; 18 | use std::env; 19 | use std::fs::OpenOptions; 20 | use std::io::copy; 21 | use std::io::{BufReader, Read}; 22 | use std::process::{Command, Stdio}; 23 | 24 | fn main() { 25 | let out_dir = env::var("OUT_DIR").unwrap(); 26 | let cargo_loc = env::var("CARGO").unwrap(); 27 | let profile = env::var("PROFILE").unwrap(); 28 | let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 29 | let num_jobs = env::var("NUM_JOBS").unwrap(); 30 | 31 | let is_release = profile == "release"; 32 | let is_mac = target_os == "macos"; 33 | let extension = if is_mac { "dylib" } else { "so" }; 34 | 35 | build_jni(&cargo_loc, is_release, &out_dir, &num_jobs); 36 | 37 | let lib_file_name = if is_release { 38 | "release/libpaperd_jni" 39 | } else { 40 | "debug/libpaperd_jni" 41 | }; 42 | let lib_file = format!("{}/{}.{}", out_dir, lib_file_name, extension); 43 | 44 | if is_release { 45 | strip(lib_file.as_str(), is_mac); 46 | } 47 | compress(lib_file.as_str()); 48 | 49 | println!("cargo:rustc-env=PAPERD_JNI_LIB={}.gz", lib_file); 50 | } 51 | 52 | fn build_jni(cargo_loc: &str, is_release: bool, out_dir: &str, num_jobs: &str) { 53 | let mut command = Command::new(cargo_loc); 54 | let mut command = 55 | command 56 | .current_dir("paperd-jni") 57 | .args(&["build", "-j", num_jobs, "--target-dir", out_dir]); 58 | 59 | if is_release { 60 | command.arg("--release"); 61 | } 62 | 63 | execute(&mut command); 64 | } 65 | 66 | fn strip(lib_file: &str, is_mac: bool) { 67 | if is_mac { 68 | let mut command = Command::new("strip"); 69 | let mut command = command.args(&["-x", lib_file]); 70 | execute(&mut command); 71 | } else { 72 | let nm_process = Command::new("nm") 73 | .args(&["--extern-only", lib_file]) 74 | .stdout(Stdio::piped()) 75 | .spawn() 76 | .unwrap(); 77 | 78 | let mut output = String::new(); 79 | nm_process 80 | .stdout 81 | .unwrap() 82 | .read_to_string(&mut output) 83 | .unwrap(); 84 | 85 | let symbols: Vec<&str> = output 86 | .lines() 87 | .filter_map(|line| { 88 | let parts: Vec<&str> = line.split_whitespace().collect(); 89 | if parts.len() < 3 { 90 | return None; 91 | } 92 | let part = parts[2]; 93 | return if part.starts_with("Java_com_destroystokyo_paper") { 94 | Some(part) 95 | } else { 96 | None 97 | }; 98 | }) 99 | .collect(); 100 | 101 | let mut command = Command::new("strip"); 102 | for symbol in symbols { 103 | command.args(&["-K", symbol]); 104 | } 105 | command.arg(lib_file); 106 | 107 | execute(&mut command); 108 | } 109 | } 110 | 111 | fn compress(lib_file: &str) { 112 | let output_file = format!("{}.gz", lib_file); 113 | 114 | let file = OpenOptions::new() 115 | .write(true) 116 | .create(true) 117 | .truncate(true) 118 | .open(output_file) 119 | .unwrap(); 120 | 121 | let mut encoder = GzEncoder::new(file, Compression::best()); 122 | 123 | let source_file = OpenOptions::new().read(true).open(lib_file).unwrap(); 124 | let mut input = BufReader::new(source_file); 125 | 126 | copy(&mut input, &mut encoder).unwrap(); 127 | } 128 | 129 | fn execute(cmd: &mut Command) { 130 | if cmd.spawn().unwrap().wait().unwrap().code().unwrap() != 0 { 131 | panic!("Failed to execute command"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /header.txt: -------------------------------------------------------------------------------- 1 | This file is part of paperd, the PaperMC server daemon 2 | Copyright (C) 2019 Kyle Wood (DemonWav) 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Lesser General Public License as published by 6 | the Free Software Foundation, version 3 only. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Lesser General Public License for more details. 12 | 13 | You should have received a copy of the GNU Lesser General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /paperd-jni/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ascii" 5 | version = "0.9.2" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "backtrace" 10 | version = "0.3.32" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "backtrace-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "backtrace-sys" 21 | version = "0.1.30" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | dependencies = [ 24 | "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", 26 | ] 27 | 28 | [[package]] 29 | name = "bitflags" 30 | version = "1.1.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | 33 | [[package]] 34 | name = "byteorder" 35 | version = "1.3.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.37" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | 43 | [[package]] 44 | name = "cesu8" 45 | version = "1.1.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | 48 | [[package]] 49 | name = "cfg-if" 50 | version = "0.1.9" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | 53 | [[package]] 54 | name = "combine" 55 | version = "3.8.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | dependencies = [ 58 | "ascii 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 59 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 60 | "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 62 | "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 63 | ] 64 | 65 | [[package]] 66 | name = "either" 67 | version = "1.5.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | 70 | [[package]] 71 | name = "error-chain" 72 | version = "0.12.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | dependencies = [ 75 | "backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)", 76 | "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 77 | ] 78 | 79 | [[package]] 80 | name = "jni" 81 | version = "0.16.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | dependencies = [ 84 | "cesu8 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 85 | "combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 86 | "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 87 | "jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 90 | ] 91 | 92 | [[package]] 93 | name = "jni-sys" 94 | version = "0.3.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | 97 | [[package]] 98 | name = "libc" 99 | version = "0.2.60" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | 102 | [[package]] 103 | name = "log" 104 | version = "0.4.7" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | dependencies = [ 107 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 108 | ] 109 | 110 | [[package]] 111 | name = "memchr" 112 | version = "2.2.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | 115 | [[package]] 116 | name = "nix" 117 | version = "0.17.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 121 | "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", 122 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 123 | "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 125 | ] 126 | 127 | [[package]] 128 | name = "paperd-jni" 129 | version = "1.1.0-snapshot" 130 | dependencies = [ 131 | "jni 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", 132 | "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", 133 | "paperd-lib 1.1.0-snapshot", 134 | ] 135 | 136 | [[package]] 137 | name = "paperd-lib" 138 | version = "1.1.0-snapshot" 139 | dependencies = [ 140 | "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", 141 | ] 142 | 143 | [[package]] 144 | name = "rustc-demangle" 145 | version = "0.1.15" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | 148 | [[package]] 149 | name = "same-file" 150 | version = "1.0.4" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | dependencies = [ 153 | "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 154 | ] 155 | 156 | [[package]] 157 | name = "unreachable" 158 | version = "1.0.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | dependencies = [ 161 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 162 | ] 163 | 164 | [[package]] 165 | name = "version_check" 166 | version = "0.1.5" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | 169 | [[package]] 170 | name = "void" 171 | version = "1.0.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | 174 | [[package]] 175 | name = "walkdir" 176 | version = "2.2.8" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | dependencies = [ 179 | "same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 180 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 182 | ] 183 | 184 | [[package]] 185 | name = "winapi" 186 | version = "0.3.7" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | dependencies = [ 189 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 190 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 191 | ] 192 | 193 | [[package]] 194 | name = "winapi-i686-pc-windows-gnu" 195 | version = "0.4.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | 198 | [[package]] 199 | name = "winapi-util" 200 | version = "0.1.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | dependencies = [ 203 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 204 | ] 205 | 206 | [[package]] 207 | name = "winapi-x86_64-pc-windows-gnu" 208 | version = "0.4.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | 211 | [metadata] 212 | "checksum ascii 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "91e320562a8fa3286a481b7189f89578ace6b20df99e123c87f2f509c957c5d6" 213 | "checksum backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "18b50f5258d1a9ad8396d2d345827875de4261b158124d4c819d9b351454fae5" 214 | "checksum backtrace-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)" = "5b3a000b9c543553af61bc01cbfc403b04b5caa9e421033866f2e98061eb3e61" 215 | "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" 216 | "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 217 | "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" 218 | "checksum cesu8 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 219 | "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" 220 | "checksum combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" 221 | "checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b" 222 | "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" 223 | "checksum jni 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "22bbdc25b49340bc4fc3d9c96dd84d878c4beeca35e3651efa53db51a68d7d4d" 224 | "checksum jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 225 | "checksum libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb" 226 | "checksum log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c275b6ad54070ac2d665eef9197db647b32239c9d244bfb6f041a766d00da5b3" 227 | "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" 228 | "checksum nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" 229 | "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" 230 | "checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" 231 | "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 232 | "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 233 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 234 | "checksum walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c7904a7e2bb3cdf0cf5e783f44204a85a37a93151738fa349f06680f59a98b45" 235 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 236 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 237 | "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" 238 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 239 | -------------------------------------------------------------------------------- /paperd-jni/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paperd-jni" 3 | version = "1.1.0-snapshot" 4 | authors = ["Kyle Wood "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate_type = ["cdylib"] 9 | 10 | [profile.release] 11 | opt-level = 'z' # Optimize for size. 12 | lto = true 13 | codegen-units = 1 14 | 15 | [dependencies] 16 | jni = "0.16.0" 17 | nix = "0.17.0" 18 | paperd-lib = { path = "../paperd-lib/" } 19 | -------------------------------------------------------------------------------- /paperd-jni/header.h: -------------------------------------------------------------------------------- 1 | /* DO NOT EDIT THIS FILE - it is machine generated */ 2 | #include 3 | /* Header for class com_destroystokyo_paper_daemon_PaperDaemonJni */ 4 | 5 | #ifndef _Included_com_destroystokyo_paper_daemon_PaperDaemonJni 6 | #define _Included_com_destroystokyo_paper_daemon_PaperDaemonJni 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | /* 11 | * Class: com_destroystokyo_paper_daemon_PaperDaemonJni 12 | * Method: createQueue 13 | * Signature: (Ljava/nio/file/Path;)I 14 | */ 15 | JNIEXPORT jint JNICALL Java_com_destroystokyo_paper_daemon_PaperDaemonJni_createSocket 16 | (JNIEnv *, jclass, jobject); 17 | 18 | /* 19 | * Class: com_destroystokyo_paper_daemon_PaperDaemonJni 20 | * Method: acceptConnection 21 | * Signature: (I)Ljava/util/OptionalInt; 22 | */ 23 | JNIEXPORT jint JNICALL Java_com_destroystokyo_paper_daemon_PaperDaemonJni_acceptConnection 24 | (JNIEnv *, jclass, jint); 25 | 26 | /* 27 | * Class: com_destroystokyo_paper_daemon_PaperDaemonJni 28 | * Method: receiveMessage 29 | * Signature: (I)Lcom/destroystokyo/paper/daemon/PaperDaemonMessageBuffer; 30 | */ 31 | JNIEXPORT jobject JNICALL Java_com_destroystokyo_paper_daemon_PaperDaemonJni_receiveMessage 32 | (JNIEnv *, jclass, jint); 33 | 34 | /* 35 | * Class: com_destroystokyo_paper_daemon_PaperDaemonJni 36 | * Method: sendMessage 37 | * Signature: (ILcom/destroystokyo/paper/daemon/PaperDaemonMessageBuffer;)V 38 | */ 39 | JNIEXPORT void JNICALL Java_com_destroystokyo_paper_daemon_PaperDaemonJni_sendMessage 40 | (JNIEnv *, jclass, jint, jobject); 41 | 42 | /* 43 | * Class: com_destroystokyo_paper_daemon_PaperDaemonJni 44 | * Method: closeSocket 45 | * Signature: (I)V 46 | */ 47 | JNIEXPORT void JNICALL Java_com_destroystokyo_paper_daemon_PaperDaemonJni_closeSocket 48 | (JNIEnv *, jclass, jint); 49 | 50 | #ifdef __cplusplus 51 | } 52 | #endif 53 | #endif 54 | -------------------------------------------------------------------------------- /paperd-jni/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | extern crate alloc; 17 | extern crate jni; 18 | extern crate nix; 19 | extern crate paperd_lib; 20 | 21 | use jni::objects::{JClass, JObject, JString, JValue}; 22 | use jni::sys::{jint, jobject}; 23 | use jni::JNIEnv; 24 | use nix::errno::Errno; 25 | use paperd_lib::{accept_connection, bind_socket, Error}; 26 | 27 | use paperd_lib::{ 28 | close_socket, create_socket, receive_message, send_message, Message, MessageHeader, 29 | }; 30 | 31 | use crate::util::{ 32 | get_path_string, throw, throw_socket_closed, throw_timeout, throw_with_cause, JAVA_STRING_TYPE, 33 | NPE_CLASS, 34 | }; 35 | 36 | #[macro_use] 37 | mod macros; 38 | mod util; 39 | 40 | const BUFFER_CLASS: &str = "com/destroystokyo/paper/daemon/PaperDaemonMessageBuffer"; 41 | const BUFFER_CONST: &str = "(JLjava/lang/String;)V"; 42 | 43 | #[no_mangle] 44 | #[allow(non_snake_case)] 45 | pub extern "system" fn Java_com_destroystokyo_paper_daemon_PaperDaemonJni_createSocket( 46 | env: JNIEnv, 47 | _: JClass, 48 | sock_file: JObject, 49 | ) -> jint { 50 | let sock_file_path = match get_path_string(&env, sock_file) { 51 | Ok(str) => str, 52 | _ => { 53 | const MESSAGE: &'static str = "Failed to get absolute path to PID file"; 54 | match env.exception_occurred() { 55 | Ok(thrown) => { 56 | if thrown.is_null() { 57 | throw(&env, MESSAGE); 58 | } 59 | let _ = env.exception_clear(); 60 | throw_with_cause(&env, MESSAGE, &thrown); 61 | } 62 | Err(_) => { 63 | throw(&env, MESSAGE); 64 | } 65 | } 66 | return -1; 67 | } 68 | }; 69 | 70 | let sock = handle_syscall!(env, create_socket(), -1); 71 | handle_syscall!(env, bind_socket(sock, sock_file_path.as_str()), -1); 72 | 73 | return sock; 74 | } 75 | 76 | #[no_mangle] 77 | #[allow(non_snake_case)] 78 | pub extern "system" fn Java_com_destroystokyo_paper_daemon_PaperDaemonJni_acceptConnection( 79 | env: JNIEnv, 80 | _: JClass, 81 | sock: jint, 82 | ) -> jint { 83 | let client_sock = handle_syscall!(env, accept_connection(sock), 0); 84 | 85 | return if let Some(value) = client_sock { 86 | value 87 | } else { 88 | throw_timeout(&env); 89 | 0 90 | }; 91 | } 92 | 93 | #[no_mangle] 94 | #[allow(non_snake_case)] 95 | pub extern "system" fn Java_com_destroystokyo_paper_daemon_PaperDaemonJni_receiveMessage( 96 | env: JNIEnv, 97 | _: JClass, 98 | client_sock: jint, 99 | ) -> jobject { 100 | let message = match receive_message(client_sock) { 101 | Ok(opt) => match opt { 102 | Some(m) => m, 103 | None => return jnull!(), 104 | }, 105 | Err(Error::Nix(nix::Error::Sys(Errno::EAGAIN), _)) => { 106 | // timeout 107 | throw_timeout(&env); 108 | return jnull!(); 109 | } 110 | Err(e) => { 111 | let error_msg = format!("Error attempting system call: {}", e); 112 | throw(&env, error_msg.as_str()); 113 | return jnull!(); 114 | } 115 | }; 116 | 117 | let result_string = match env.new_string(message.message_text) { 118 | Ok(s) => s, 119 | Err(_) => return jnull!(), 120 | }; 121 | 122 | let result_obj = env.new_object( 123 | BUFFER_CLASS, 124 | BUFFER_CONST, 125 | &[ 126 | JValue::Long(message.header.message_type), 127 | JValue::Object(JObject::from(result_string)), 128 | ], 129 | ); 130 | 131 | return match result_obj { 132 | Ok(o) => o.into_inner(), 133 | Err(_) => jnull!(), 134 | }; 135 | } 136 | 137 | #[no_mangle] 138 | #[allow(non_snake_case)] 139 | pub extern "system" fn Java_com_destroystokyo_paper_daemon_PaperDaemonJni_sendMessage( 140 | env: JNIEnv, 141 | _: JClass, 142 | client_sock: jint, 143 | message: jobject, 144 | ) { 145 | if message.is_null() { 146 | let _ = env.throw_new(NPE_CLASS, "message must not be null"); 147 | return; 148 | } 149 | 150 | let message_type = get_field!(env, message, "messageType", Long); 151 | let message_data = get_field!(env, message, "messageData", Object(JAVA_STRING_TYPE)); 152 | 153 | let java_string = env.get_string(JString::from(message_data)); 154 | let java_string = match java_string { 155 | Ok(s) => String::from(s), 156 | Err(e) => { 157 | let error_msg = format!("Failed to retrieve string from message: {}", e); 158 | throw(&env, error_msg.as_str()); 159 | return; 160 | } 161 | }; 162 | 163 | let message = Message { 164 | header: MessageHeader { 165 | message_type, 166 | message_length: java_string.len() as i64, 167 | }, 168 | message_text: java_string, 169 | }; 170 | 171 | match send_message(client_sock, &message) { 172 | Err(Error::Nix(nix::Error::Sys(Errno::EPIPE), _)) => { 173 | throw_socket_closed(&env); 174 | } 175 | Err(e) => { 176 | let error_msg = format!("Failed to send message to {}: {}", client_sock, e); 177 | throw(&env, error_msg.as_str()); 178 | } 179 | _ => {} 180 | } 181 | } 182 | 183 | #[no_mangle] 184 | #[allow(non_snake_case)] 185 | pub extern "system" fn Java_com_destroystokyo_paper_daemon_PaperDaemonJni_closeSocket( 186 | env: JNIEnv, 187 | _: JClass, 188 | sock: jint, 189 | ) { 190 | if let Err(e) = close_socket(sock) { 191 | let error_msg = format!("Error while closing socket {}: {}", sock, e); 192 | throw(&env, error_msg.as_str()); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /paperd-jni/src/macros.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | macro_rules! jnull { 17 | () => { 18 | jni::objects::JObject::null().into_inner() 19 | }; 20 | } 21 | 22 | macro_rules! get_field { 23 | ($env:ident, $obj:ident, $name:expr, Object($ty:expr)) => { 24 | get_field!($env, $obj, $name, (jni::objects::JValue::Object, $ty)) 25 | }; 26 | ($env:ident, $obj:ident, $name:expr, Byte) => { 27 | get_field!($env, $obj, $name, (jni::objects::JValue::Byte, "B")) 28 | }; 29 | ($env:ident, $obj:ident, $name:expr, Char) => { 30 | get_field!($env, $obj, $name, (jni::objects::JValue::Char, "C")) 31 | }; 32 | ($env:ident, $obj:ident, $name:expr, Short) => { 33 | get_field!($env, $obj, $name, (jni::objects::JValue::Short, "S")) 34 | }; 35 | ($env:ident, $obj:ident, $name:expr, Int) => { 36 | get_field!($env, $obj, $name, (jni::objects::JValue::Int, "I")) 37 | }; 38 | ($env:ident, $obj:ident, $name:expr, Long) => { 39 | get_field!($env, $obj, $name, (jni::objects::JValue::Long, "J")) 40 | }; 41 | ($env:ident, $obj:ident, $name:expr, Bool) => { 42 | get_field!($env, $obj, $name, (jni::objects::JValue::Bool, "Z")) 43 | }; 44 | ($env:ident, $obj:ident, $name:expr, Float) => { 45 | get_field!($env, $obj, $name, (jni::objects::JValue::Float, "F")) 46 | }; 47 | ($env:ident, $obj:ident, $name:expr, Double) => { 48 | get_field!($env, $obj, $name, (jni::objects::JValue::Double, "D")) 49 | }; 50 | ($env:ident, $obj:ident, $name:expr, Void) => { 51 | get_field!($env, $obj, $name, (jni::objects::JValue::Void, "V")) 52 | }; 53 | ($env:ident, $obj:ident, $name:expr, ($ret:path, $ty:expr)) => { 54 | match $env.get_field($obj, $name, $ty) { 55 | Ok($ret(t)) => t, 56 | _ => { 57 | let class_name = crate::util::get_class_name(&$env, $obj); 58 | let error_msg = format!(stringify!(Failed to get $name from {}), class_name.as_str()); 59 | throw(&$env, error_msg.as_str()); 60 | return; 61 | } 62 | }; 63 | }; 64 | } 65 | 66 | macro_rules! handle_syscall { 67 | ($env:ident, $call:expr, $return:expr) => { 68 | match $call { 69 | Ok(v) => v, 70 | Err(e) => { 71 | let error_msg = format!("Error attempting system call: {}", e); 72 | throw(&$env, error_msg.as_str()); 73 | return $return; 74 | } 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /paperd-jni/src/util.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use jni::objects::JValue::Object; 17 | use jni::objects::{JObject, JString, JThrowable, JValue}; 18 | use jni::sys::jobject; 19 | use jni::JNIEnv; 20 | use std::string::String; 21 | 22 | pub const JAVA_STRING_TYPE: &'static str = "Ljava/lang/String;"; 23 | pub const NPE_CLASS: &'static str = "java/lang/NullPointerException"; 24 | 25 | pub fn get_path_string(env: &JNIEnv, path: JObject) -> Result { 26 | let abs_path = match env.call_method(path, "toAbsolutePath", "()Ljava/nio/file/Path;", &[]) { 27 | Ok(Object(obj)) => obj, 28 | _ => return Err(()), 29 | }; 30 | if env.exception_check().unwrap_or(false) { 31 | return Err(()); 32 | } 33 | 34 | let java_string = match env.call_method(abs_path, "toString", "()Ljava/lang/String;", &[]) { 35 | Ok(Object(obj)) => obj, 36 | _ => return Err(()), 37 | }; 38 | if env.exception_check().unwrap_or(false) { 39 | return Err(()); 40 | } 41 | 42 | let text = match env.get_string(java_string.into()) { 43 | Ok(str) => str, 44 | _ => return Err(()), 45 | }; 46 | return Ok(text.into()); 47 | } 48 | 49 | const NATIVE_EXCEPTION_CLASS: &'static str = "com/destroystokyo/paper/daemon/NativeErrorException"; 50 | const NATIVE_TIMEOUT_EXCEPTION_CLASS: &'static str = 51 | "com/destroystokyo/paper/daemon/NativeTimeoutException"; 52 | const NATIVE_SOCKET_CLOSED_CLASS: &'static str = 53 | "com/destroystokyo/paper/daemon/NativeSocketClosedException"; 54 | 55 | pub fn throw(env: &JNIEnv, message: &str) { 56 | let _ = env.throw_new(NATIVE_EXCEPTION_CLASS, message); 57 | } 58 | 59 | pub fn throw_timeout(env: &JNIEnv) { 60 | throw_blank(env, NATIVE_TIMEOUT_EXCEPTION_CLASS); 61 | } 62 | 63 | pub fn throw_socket_closed(env: &JNIEnv) { 64 | throw_blank(env, NATIVE_SOCKET_CLOSED_CLASS); 65 | } 66 | 67 | fn throw_blank(env: &JNIEnv, class: &str) { 68 | let obj = env.new_object(class, "()V", &[]); 69 | if obj.is_ok() { 70 | let _ = env.throw(JThrowable::from(obj.unwrap())); 71 | } 72 | } 73 | 74 | pub fn throw_with_cause(env: &JNIEnv, message: &str, cause: &JThrowable) { 75 | let java_string = match env.new_string(message) { 76 | Ok(string) => string, 77 | Err(_) => JString::from(jnull!()), 78 | }; 79 | 80 | let ex_obj = match env.new_object( 81 | NATIVE_EXCEPTION_CLASS, 82 | "(Ljava/lang/String;Ljava/lang/Throwable;)V", 83 | &[ 84 | JValue::Object(JObject::from(java_string)), 85 | JValue::Object(JObject::from(*cause)), 86 | ], 87 | ) { 88 | Ok(obj) => obj, 89 | Err(_) => { 90 | // Just attempt to throw an exception with a cause 91 | throw(env, message); 92 | return; 93 | } 94 | }; 95 | 96 | let _ = env.throw(JThrowable::from(ex_obj)); 97 | } 98 | 99 | pub fn get_class_name(env: &JNIEnv, obj: jobject) -> String { 100 | return env 101 | .get_object_class(obj) 102 | .and_then(|class| env.call_method(class, "getName", "()Ljava/lang/String;", &[])) 103 | .and_then(|class_name| class_name.l()) 104 | .and_then(|class_name| env.get_string(class_name.into())) 105 | .map(|str| String::from(str)) 106 | .unwrap_or(String::from("")); 107 | } 108 | -------------------------------------------------------------------------------- /paperd-lib/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "cc" 10 | version = "1.0.52" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "cfg-if" 15 | version = "0.1.10" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | 18 | [[package]] 19 | name = "libc" 20 | version = "0.2.69" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [[package]] 24 | name = "nix" 25 | version = "0.17.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | dependencies = [ 28 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 29 | "cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)", 30 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 31 | "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 33 | ] 34 | 35 | [[package]] 36 | name = "paperd-lib" 37 | version = "1.1.0-snapshot" 38 | dependencies = [ 39 | "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [[package]] 43 | name = "void" 44 | version = "1.0.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [metadata] 48 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 49 | "checksum cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d" 50 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 51 | "checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 52 | "checksum nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" 53 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 54 | -------------------------------------------------------------------------------- /paperd-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paperd-lib" 3 | version = "1.1.0-snapshot" 4 | authors = ["Kyle Wood "] 5 | edition = "2018" 6 | 7 | [profile.release] 8 | opt-level = 'z' # Optimize for size. 9 | lto = true 10 | codegen-units = 1 11 | 12 | [dependencies] 13 | nix = "0.17.0" 14 | -------------------------------------------------------------------------------- /paperd-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | extern crate nix; 17 | 18 | use nix::errno::Errno; 19 | use nix::sys::socket::sockopt::{ReceiveTimeout, SendTimeout}; 20 | use nix::sys::socket::{ 21 | accept, bind, connect, listen, recv, send, setsockopt, socket, AddressFamily, MsgFlags, 22 | SockAddr, SockFlag, SockType, UnixAddr, 23 | }; 24 | use nix::sys::time::{TimeVal, TimeValLike}; 25 | use nix::unistd::{close, unlink}; 26 | use nix::NixPath; 27 | use std::cmp::min; 28 | use std::fmt; 29 | use std::fmt::Display; 30 | use std::os::unix::io::RawFd; 31 | use std::string::FromUtf8Error; 32 | use std::time::{Duration, Instant}; 33 | 34 | macro_rules! syscall { 35 | ($syscall:ident($( $args:expr ),*)) => { 36 | $syscall($($args,)*).map_err(|e| crate::Error::from(e).for_syscall(stringify!($syscall))) 37 | }; 38 | } 39 | 40 | pub struct MessageHeader { 41 | pub message_type: i64, 42 | pub message_length: i64, 43 | } 44 | 45 | pub struct Message { 46 | pub header: MessageHeader, 47 | pub message_text: String, 48 | } 49 | 50 | pub type Socket = RawFd; 51 | 52 | const META_SIZE: usize = 8; 53 | const MESSAGE_SIZE: usize = 1000; 54 | const TIMEOUT_MILLIS: u64 = 500; 55 | 56 | pub fn create_socket() -> Result { 57 | let sock = syscall!(socket( 58 | AddressFamily::Unix, 59 | SockType::Stream, 60 | SockFlag::empty(), 61 | None 62 | ))?; 63 | 64 | let time_val = TimeVal::milliseconds((TIMEOUT_MILLIS / 2) as i64); 65 | syscall!(setsockopt(sock, ReceiveTimeout, &time_val))?; 66 | syscall!(setsockopt(sock, SendTimeout, &time_val))?; 67 | 68 | return Ok(sock); 69 | } 70 | 71 | pub fn close_socket(sock: Socket) -> Result<(), Error> { 72 | return syscall!(close(sock)); 73 | } 74 | 75 | pub fn bind_socket(sock: Socket, file_path: &str) -> Result<(), Error> { 76 | match syscall!(unlink(file_path)) { 77 | Ok(()) => Ok(()), 78 | // ENOENT == no such file or directory, we don't care if it doesn't exist 79 | Err(Error::Nix(nix::Error::Sys(Errno::ENOENT), _)) => Ok(()), 80 | Err(e) => Err(e), 81 | }?; 82 | 83 | let addr = UnixAddr::new(file_path)?; 84 | let sock_addr = SockAddr::Unix(addr); 85 | 86 | syscall!(bind(sock, &sock_addr))?; 87 | 88 | syscall!(listen(sock, 128))?; 89 | 90 | return Ok(()); 91 | } 92 | 93 | pub fn connect_socket(sock_file: &P) -> Result { 94 | let sock = create_socket()?; 95 | 96 | let addr = UnixAddr::new(sock_file)?; 97 | let socket_addr = SockAddr::Unix(addr); 98 | 99 | loop { 100 | match syscall!(connect(sock, &socket_addr)) { 101 | Ok(_) => break, 102 | Err(Error::Nix(nix::Error::Sys(Errno::EINPROGRESS), _)) => continue, 103 | Err(e) => return Err(e), 104 | } 105 | } 106 | 107 | return Ok(sock); 108 | } 109 | 110 | macro_rules! handle_timeout { 111 | ($res:ident, $timeout:ident, $start:ident, $has_data:expr) => { 112 | match $res { 113 | Ok(amt) => { 114 | $start = std::time::Instant::now(); 115 | Ok(amt) 116 | } 117 | Err(Error::Nix(nix::Error::Sys(Errno::EAGAIN), s)) => { 118 | if $start.elapsed() > $timeout { 119 | if ($has_data) { 120 | // If we've received data and we have a timeout, we can't keep listening 121 | Err((Error::Nix(nix::Error::Sys(Errno::UnknownErrno), s))) 122 | } else { 123 | Err((Error::Nix(nix::Error::Sys(Errno::EAGAIN), s))) 124 | } 125 | } else { 126 | continue; 127 | } 128 | } 129 | Err(e) => Err(e), 130 | } 131 | }; 132 | } 133 | 134 | pub fn receive_message(sock: Socket) -> Result, Error> { 135 | let message_header = read_meta(sock)?; 136 | let message_length = message_header.message_length as usize; 137 | 138 | let timeout = Duration::from_millis(TIMEOUT_MILLIS); 139 | let mut start = Instant::now(); 140 | 141 | let mut message_buffer: [u8; MESSAGE_SIZE] = [0; MESSAGE_SIZE]; 142 | 143 | let mut output_buffer = Vec::::new(); 144 | 145 | let mut total_received: usize = 0; 146 | while total_received < message_length { 147 | let amount_left = message_length - total_received; 148 | let buffer_size = min(MESSAGE_SIZE, amount_left); 149 | 150 | let res = syscall!(recv( 151 | sock, 152 | &mut message_buffer[..buffer_size], 153 | MsgFlags::empty() 154 | )); 155 | let amount_received = handle_timeout!(res, timeout, start, true)?; 156 | if amount_received == 0 { 157 | return Ok(None); 158 | } 159 | total_received += amount_received; 160 | 161 | output_buffer.extend_from_slice(&message_buffer[..amount_received]); 162 | } 163 | 164 | let message_string = String::from_utf8(output_buffer)?; 165 | 166 | return Ok(Some(Message { 167 | header: message_header, 168 | message_text: message_string, 169 | })); 170 | } 171 | 172 | pub fn send_message(sock: Socket, message: &Message) -> Result<(), Error> { 173 | send_meta(sock, &message.header)?; 174 | 175 | let message_data = message.message_text.as_bytes(); 176 | 177 | let timeout = Duration::from_millis(TIMEOUT_MILLIS); 178 | let mut start = Instant::now(); 179 | 180 | let mut total_sent: usize = 0; 181 | let message_size = message_data.len(); 182 | 183 | while total_sent < message_size { 184 | let res = syscall!(send(sock, &message_data[total_sent..], MsgFlags::empty())); 185 | let amount_sent = handle_timeout!(res, timeout, start, true)?; 186 | total_sent += amount_sent; 187 | } 188 | 189 | return Ok(()); 190 | } 191 | 192 | pub fn accept_connection(sock: Socket) -> Result, Error> { 193 | let res = syscall!(accept(sock)); 194 | return match res { 195 | Ok(client_sock) => Ok(Some(client_sock)), 196 | Err(Error::Nix(nix::Error::Sys(Errno::EAGAIN), _)) => Ok(None), 197 | Err(e) => Err(e), 198 | }; 199 | } 200 | 201 | fn read_meta(sock: Socket) -> Result { 202 | // meta_buffer will contain: 203 | // * message_type (first 8 bytes) 204 | // * message_length (last 8 bytes) 205 | // 8 bytes for each is overkill by an enormous margin, but 16 bytes is cheap and easy to do so 206 | // there's not really much downside 207 | // 208 | // Both numbers are big endian 209 | let mut meta_buffer: [u8; META_SIZE] = [0; META_SIZE]; 210 | 211 | let message_type = read_i64(sock, &mut meta_buffer, true)?; 212 | let message_length = read_i64(sock, &mut meta_buffer, false)?; 213 | 214 | return Ok(MessageHeader { 215 | message_type, 216 | message_length, 217 | }); 218 | } 219 | 220 | fn send_meta(sock: Socket, message_header: &MessageHeader) -> Result<(), Error> { 221 | write_i64(sock, message_header.message_type, true)?; 222 | write_i64(sock, message_header.message_length, false)?; 223 | 224 | return Ok(()); 225 | } 226 | 227 | fn read_i64(sock: Socket, buffer: &mut [u8; META_SIZE], is_start: bool) -> Result { 228 | let timeout = Duration::from_millis(TIMEOUT_MILLIS); 229 | let mut start = Instant::now(); 230 | 231 | let mut total_received: usize = 0; 232 | while total_received < META_SIZE { 233 | let res = syscall!(recv(sock, &mut buffer[total_received..], MsgFlags::empty())); 234 | let amount_received = 235 | handle_timeout!(res, timeout, start, !is_start || total_received > 0)?; 236 | total_received += amount_received; 237 | } 238 | 239 | return Ok(i64::from_be_bytes(*buffer)); 240 | } 241 | 242 | fn write_i64(sock: Socket, value: i64, is_start: bool) -> Result<(), Error> { 243 | let buffer: [u8; META_SIZE] = value.to_be_bytes(); 244 | 245 | let timeout = Duration::from_millis(TIMEOUT_MILLIS); 246 | let mut start = Instant::now(); 247 | 248 | let mut total_sent: usize = 0; 249 | while total_sent < META_SIZE { 250 | let res = syscall!(send(sock, &buffer[total_sent..], MsgFlags::empty())); 251 | let amount_sent = handle_timeout!(res, timeout, start, !is_start || total_sent > 0)?; 252 | total_sent += amount_sent; 253 | } 254 | 255 | return Ok(()); 256 | } 257 | 258 | pub enum Error { 259 | Nix(nix::Error, Option), 260 | Internal(String), 261 | } 262 | 263 | impl Error { 264 | pub fn with_message(msg: &str) -> Error { 265 | return Error::Internal(msg.to_string()); 266 | } 267 | 268 | pub fn for_syscall(&self, syscall: &str) -> Self { 269 | return match &self { 270 | Error::Nix(e, _) => Error::Nix(e.clone(), Some(syscall.to_string())), 271 | Error::Internal(s) => Error::Internal(s.clone()), 272 | }; 273 | } 274 | } 275 | 276 | impl Display for Error { 277 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 278 | return match &self { 279 | Error::Nix(e, Some(syscall)) => { 280 | write!(f, "In syscall: {}, ", syscall)?; 281 | e.fmt(f) 282 | } 283 | Error::Nix(e, None) => e.fmt(f), 284 | Error::Internal(s) => write!(f, "{}", s), 285 | }; 286 | } 287 | } 288 | 289 | impl From for Error { 290 | fn from(e: nix::Error) -> Self { 291 | return Error::Nix(e, None); 292 | } 293 | } 294 | 295 | impl From for Error { 296 | fn from(e: FromUtf8Error) -> Self { 297 | return Error::Internal(e.to_string()); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /protocol.md: -------------------------------------------------------------------------------- 1 | paperd Protocol 2 | =============== 3 | 4 | In order to minimize dependencies and overhead, `paperd` uses Unix sockets. The Unix sockets are implemented using only 5 | a few functions, a socket file, and no other dependencies. 6 | 7 | Using such an old system does provide a small amount of complexity, though, which is what will be described here. There 8 | are three layers to how we use these message queues, described below. 9 | 10 | ### The Unix socket 11 | 12 | This is not really a layer. Instead, a brief introduction to how Unix sockets work. 13 | 14 | Sockets are managed by the kernel, and we retrieve a new Unix socket by calling: 15 | 16 | ```c 17 | int socket(int domain, int type, int protocol); 18 | ``` 19 | 20 | The `domain` parameter is `AF_UNIX`, and the `type` parameter is `SOCK_STREAM`. This just means to create a Unix socket 21 | in stream mode, you can read more about what that all means in the [man pages](https://man7.org/linux/man-pages/man7/unix.7.html). 22 | 23 | Now we have a socket address (that is what the `socket` function returns), we need to bind it to a file on disk so that 24 | other processes can access this socket. We do this with: 25 | 26 | ```c 27 | int bind(int socket, const struct sockaddr *address, socklen_t address_len); 28 | ``` 29 | 30 | The socket address we got from calling `socket()` above is passed to the `socket` parameter, and the `address` parameter 31 | is just a struct which contains the name (full path) of the socket file to create. 32 | 33 | Now we've created a socket and bound it to a file so clients can access it, we need to listen to that socket for new 34 | connections. We do this with the `listen` function: 35 | 36 | ```c 37 | int listen(int socket, int backlog); 38 | ``` 39 | 40 | Again, the `socket` parameter is the socket id that we got from `socket()`. The `backlog` parameter is simply the number 41 | of incoming connections that can be queued up before `ECONNREFUSED` is returned. We try to accept new connections as 42 | soon as they come in, but we use `128` to be safe. 43 | 44 | Now to wait for a new connection we need to call: 45 | 46 | ```c 47 | int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len); 48 | ``` 49 | 50 | This returns a new socket descriptor in the `address` struct. The old socket descriptor we created is still listening 51 | for connections, this new socket descriptor can be used to communicate with the client. 52 | 53 | We read data from this new socket descriptor with: 54 | 55 | ```c 56 | ssize_t recv(int socket, void *buffer, size_t length, int flags); 57 | ``` 58 | 59 | This will return up to the number of bytes into `buffer` requested with `length`, unless there is not more data from the 60 | client. The amount of data copied into `buffer` is the return value of this function. 61 | 62 | When we're done with the connection we call: 63 | 64 | ```c 65 | int close(int fildes); 66 | ``` 67 | 68 | With the socket id passed in. 69 | 70 | ---- 71 | 72 | In the client we do something similar: 73 | 74 | First we create a socket with `socket()`. Then we connect to the server using 75 | 76 | ```c 77 | int connect(int socket, const struct sockaddr *address, socklen_t address_len); 78 | ``` 79 | 80 | Where we pass our socket id into `socket`, and the socket address for the server's socket into `address`. 81 | 82 | Once we have a connection we can send data to the server with: 83 | 84 | ```c 85 | ssize_t send(int socket, const void *buffer, size_t length, int flags); 86 | ``` 87 | 88 | Which functions the same way as `recv` described above. Note 2 things: 89 | 90 | 1. Both the client and the server can call `send` and `recv`, the socket is bi-directional. This is used in `paperd` 91 | often when the server needs to respond to the client's request. 92 | 1. If the data for a message doesn't fit into a single message, `send` and `recv` will be called in succession until 93 | all of the data is transferred. 94 | 95 | ### A message 96 | 97 | A complete message is just a complete string of bytes representing a single message. In this context, 'single message' 98 | refers to a single discrete command, rather than a single socket message. 99 | 100 | For simplicity, Paper and `paperd` use JSON for passing commands and responses between each other. The JSON data is 101 | encoded using UTF-8 and sent to between the client and server through a series of `send` and `recv` calls with a 102 | buffer size of 1000 bytes. 103 | 104 | All messages contain the at least 16 bytes. These 16 bytes represent 2 64-bit integers representing the following 2 105 | fields, in order: 106 | 107 | * `message_type` 108 | * `message_length` 109 | 110 | The fields are sent big endian. The `message_type` determines how the message is parsed. The `message_length` determines 111 | how much data the receiver will expect to receive for a complete message. Note the `message_length` does not include the 112 | first 16 bytes, since that's implicit. 113 | 114 | > Note: Several of the messages have a request that is nothing more than `{}`, as the message type is all that needs to 115 | > be known. the reason an empty object is still sent is simply for consistency. 116 | 117 | ---- 118 | 119 | ### List of messages 120 | 121 | #### Protocol Version `0` 122 | 123 | Request: 124 | ```json 125 | {} 126 | ``` 127 | 128 | Response: 129 | ```json 130 | { 131 | "protocolVersion": 1 132 | } 133 | ``` 134 | 135 | Protocol version is a special case. The "protocol version" is a single integer which specifies the version of the 136 | following messages. This allows updating, adding, reordering, and removing messages below without breaking 137 | compatibility. As long as the protocol version number is bumped accordingly, `paperd` will verify the versions match 138 | before issuing commands to the server. 139 | 140 | That being said, the protocol version message `0` _must not change_ else compatibility will be broken. Even between 141 | protocol versions this message must stay the same. 142 | 143 | #### Stop `1` 144 | 145 | Request: 146 | ```json 147 | {} 148 | ``` 149 | No response. 150 | 151 | #### Restart `2` 152 | 153 | Request: 154 | ```json 155 | {} 156 | ``` 157 | 158 | #### Status `3` 159 | 160 | Request: 161 | ```json 162 | {} 163 | ``` 164 | 165 | Single Response: 166 | ```json 167 | { 168 | "motd": "", 169 | "serverName": "", 171 | "apiVersion": "", 172 | "players": ["player1", "player2"], 173 | "worlds": [ 174 | { 175 | "name": "world", 176 | "dimension": "Normal", 177 | "seed": -4235823458239452, 178 | "difficulty": "Easy", 179 | "players": ["player1"], 180 | "time": "309" 181 | }, 182 | { 183 | "name": "world_nether", 184 | "dimension": "Nether", 185 | "seed": -4235823458239452, 186 | "difficulty": "Easy", 187 | "players": ["player2"], 188 | "time": "309" 189 | } 190 | ], 191 | "tps": { 192 | "oneMin": 20.0, 193 | "fiveMin": 20.0, 194 | "fifteenMin": 20.0 195 | }, 196 | "memoryUsage": { 197 | "usedMemory": "5000 MB", 198 | "totalMemory": "10000 MB", 199 | "maxMemory": "10000 MB" 200 | } 201 | } 202 | ``` 203 | 204 | #### Send Command `4` 205 | 206 | Request: 207 | ```json 208 | { 209 | "message": "" 210 | } 211 | ``` 212 | 213 | No response. 214 | 215 | #### Timings `5` 216 | 217 | Request: 218 | ```json 219 | {} 220 | ``` 221 | 222 | Multiple Responses: 223 | ```json 224 | { 225 | "message": "some message", 226 | "done": false 227 | } 228 | ``` 229 | 230 | Responses for the timings command will be read until `done` is `true`. 231 | 232 | #### Logs Message `6` (for console) 233 | 234 | Request: 235 | ```json 236 | { 237 | "pid": 0 238 | } 239 | ``` 240 | 241 | Multiple Responses: 242 | ```json 243 | { 244 | "message": "some message" 245 | } 246 | ``` 247 | 248 | Response for new log messages will read until `End Logs Message` below is received. 249 | 250 | #### End Logs Message `7` (for console) 251 | 252 | Request: 253 | ```json 254 | { 255 | "pid": 0 256 | } 257 | ``` 258 | 259 | No Response. 260 | 261 | #### Console Status Message `8` (for console) 262 | 263 | Request: 264 | ```json 265 | {} 266 | ``` 267 | 268 | Single Response: 269 | ```json 270 | { 271 | "serverName": "Server Name", 272 | "players": 0, 273 | "maxPlayers": 0, 274 | "tps": 1.0 275 | } 276 | ``` 277 | 278 | #### Tab Complete Message `9` (for console) 279 | 280 | Request: 281 | ```json 282 | { 283 | "command": "command string" 284 | } 285 | ``` 286 | 287 | Single Response: 288 | ```json 289 | { 290 | "suggestions": [ 291 | "suggestion 1", 292 | "suggestion 2" 293 | ] 294 | } 295 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | paperd [![Current Release](https://img.shields.io/badge/release-1.0.0-orange.svg)](https://papermc.io/ci/job/paperd/) 2 | ====== 3 | 4 | | Build | Status | 5 | |----------------|--------| 6 | | Latest Commit | [![Build Status](https://travis-ci.org/PaperMC/paperd.svg?branch=master)](https://travis-ci.org/PaperMC/paperd/branches) | 7 | | Latest Release | [![Build Status](https://papermc.io/ci/job/paperd/badge/icon)](https://papermc.io/ci/job/paperd/) | 8 | 9 | paperd is a wrapper application which enables the PaperMC Minecraft server to be run more properly in the background as 10 | a daemon, rather than simply backgrounded using `screen` or `tmux`. This is accomplished both by the `paperd` 11 | application and custom changes in the Paper server. 12 | 13 | **Support and Project Discussion:** 14 | - [IRC](https://webchat.esper.net/?channels=paper) or [Discord](https://discord.gg/papermc) 15 | 16 | Building 17 | -------- 18 | 19 | `paperd` is strictly Unix / POSIX compatible. Windows is not supported. 20 | 21 | A 64 bit JDK is required to build `paperd` and a 64 bit JVM is required to use `paperd`. 22 | 23 | This project requires the [Rust](https://www.rust-lang.org/) toolchain. `paperd` is built on the latest release of Rust, 24 | currently version `1.44.1`. 25 | 26 | To build for your own system with console support: 27 | ```sh 28 | cargo build --release --features console 29 | ``` 30 | 31 | If you don't want the console feature, or don't have `ncurses` installed, simply omit the feature to build without it: 32 | ```sh 33 | cargo build --release 34 | ``` 35 | 36 | To build a release binary like the pre-built binaries Jenkins provides, you'll need Docker installed: 37 | ```sh 38 | ./build_release.sh 39 | ``` 40 | 41 | This will produce a `paperd.tar.xz` file in the current working directory, which is the file available for download 42 | from Jenkins. 43 | 44 | Documentation 45 | ------------- 46 | 47 | [For general usage instructions, please click here.](usage.md) 48 | 49 | [For technical info on how `paperd` works and communicates with the Paper server, please click here.](protocol.md) 50 | 51 | Contributing 52 | ------------ 53 | 54 | PRs are greatly appreciated, but when a change requires modifications to both this project and to the 55 | [Paper](https://github.com/PaperMC/Paper) server itself, please link both PRs together in the PR description. 56 | 57 | For this project in particular, please run `rustfmt` with all default settings across the whole project before 58 | committing. 59 | 60 | License 61 | ------- 62 | 63 | This project is licensed under LGPLv3 only, no future versions. 64 | -------------------------------------------------------------------------------- /release/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | *.jar 4 | build/ 5 | gen/ 6 | 7 | !gradle/wrapper/gradle-wrapper.jar 8 | -------------------------------------------------------------------------------- /release/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.ByteArrayOutputStream 2 | import java.io.IOException 3 | import org.gradle.api.tasks.Copy 4 | 5 | val rustVersion: String by project 6 | 7 | val releaseTasks = mutableMapOf>>>() 8 | val cleanTasks = mutableMapOf>>>() 9 | 10 | subprojects { 11 | // make sure we're actually a fully qualified project 12 | val parentProj = parent ?: return@subprojects 13 | val grandParentProj = parentProj.parent ?: return@subprojects 14 | parentProj.parent?.parent?.parent ?: return@subprojects 15 | 16 | val typeName = name 17 | val versionName = parentProj.name 18 | val systemName = grandParentProj.name 19 | 20 | val dockerFile = grandParentProj.file("${grandParentProj.name}.Dockerfile") 21 | if (!dockerFile.exists()) { 22 | throw Exception("Dockerfile doesn't exist for $grandParentProj: $dockerFile") 23 | } 24 | 25 | val imageName = "paperd/$systemName:$versionName" 26 | val dockerImage = DockerImage(systemName, versionName, imageName, dockerFile) 27 | 28 | // Make sure the docker build task is set up on the parent project 29 | val dockerBuildTask: TaskProvider = try { 30 | val copyDockerIgnore = parentProj.copyDockerIgnore() 31 | val dockerBuildTask = parentProj.createDockerBuildTask(dockerImage, rustVersion) 32 | dockerBuildTask { 33 | dependsOn(copyDockerIgnore) 34 | } 35 | dockerBuildTask 36 | } catch (e: Exception) { 37 | parentProj.tasks.named("buildDockerImage") 38 | } 39 | 40 | val copyDockerIgnore = copyDockerIgnore() 41 | val runBuildTask = createRunBuildTask(dockerImage, typeName == "full") 42 | runBuildTask { 43 | dependsOn(dockerBuildTask, copyDockerIgnore) 44 | } 45 | 46 | val clean by tasks.registering { 47 | group = "clean" 48 | val extra = if (typeName == "full") "" else " (no console)" 49 | description = "Clean outputs of for ${systemName.capitalize()} $versionName$extra" 50 | doLast { 51 | delete(file("build")) 52 | delete(file(".dockerignore")) 53 | delete(runBuildTask) 54 | } 55 | } 56 | 57 | // 58 | // Build task hierarchy 59 | val versionTargetBuild = parentProj.findOrCreateTask("build") { 60 | group = "paperd" 61 | description = "Build all targets for ${systemName.capitalize()} $versionName" 62 | }.apply { 63 | configure { 64 | dependsOn(runBuildTask) 65 | } 66 | } 67 | val systemTargetBuild = grandParentProj.findOrCreateTask("build") { 68 | group = "paperd" 69 | description = "Build all targets for all versions for ${systemName.capitalize()}" 70 | }.apply { 71 | configure { 72 | dependsOn(versionTargetBuild) 73 | } 74 | } 75 | grandParentProj.parent!!.findOrCreateTask("build") { 76 | group = "paperd" 77 | description = "Build all targets for all versions of all platforms" 78 | }.apply { 79 | configure { 80 | dependsOn(systemTargetBuild) 81 | } 82 | } 83 | 84 | // 85 | // Clean task hierarchy 86 | val versionTargetClean = parentProj.findOrCreateTask("clean") { 87 | group = "clean" 88 | description = "Clean all outputs for ${systemName.capitalize()} $versionName" 89 | doLast { 90 | delete(parentProj.file(".dockerignore")) 91 | } 92 | }.apply { 93 | configure { 94 | dependsOn(clean) 95 | } 96 | } 97 | val systemTargetClean = grandParentProj.findOrCreateTask("clean") { 98 | group = "clean" 99 | description = "Clean all outputs for all versions ${systemName.capitalize()}" 100 | }.apply { 101 | configure { 102 | dependsOn(versionTargetClean) 103 | } 104 | } 105 | grandParentProj.parent!!.findOrCreateTask("clean") { 106 | group = "clean" 107 | description = "Clean all outputs for all versions of all platforms" 108 | }.apply { 109 | configure { 110 | dependsOn(systemTargetClean) 111 | } 112 | } 113 | } 114 | 115 | tasks.register("build") { 116 | dependsOn(findProject(":targets")!!.tasks.named("build")) 117 | group = "paperd" 118 | description = "Alias of :targets:buildReleases" 119 | } 120 | tasks.register("clean") { 121 | dependsOn(findProject(":targets")!!.tasks.named("clean")) 122 | group = "clean" 123 | description = "Alias for :targets:clean" 124 | } 125 | 126 | fun Project.createDockerBuildTask( 127 | dockerImage: DockerImage, 128 | rustVersion: String 129 | ): TaskProvider { 130 | return tasks.register("buildDockerImage") { 131 | group = "paperd" 132 | description = "Build Docker image for ${dockerImage.systemName.capitalize()} ${dockerImage.versionName}" 133 | 134 | inputs.file(dockerImage.dockerFile) 135 | 136 | doLast { 137 | docker( 138 | "build", "-t", dockerImage.imageName, 139 | "-f", dockerImage.dockerFile.absolutePath, 140 | "--build-arg", "version=${dockerImage.versionName}", 141 | "--build-arg", "rustVersion=$rustVersion", 142 | "." 143 | ) 144 | } 145 | } 146 | } 147 | 148 | fun Project.createRunBuildTask( 149 | dockerImage: DockerImage, 150 | includeConsole: Boolean 151 | ): TaskProvider { 152 | return tasks.register("build") { 153 | group = "paperd" 154 | val extra = if (includeConsole) "" else " (no console)" 155 | description = "Build release for ${dockerImage.systemName.capitalize()} ${dockerImage.versionName}$extra" 156 | 157 | val baseDir = rootProject.file("..").absoluteFile 158 | val inputSource = fileTree(baseDir) { 159 | include( 160 | "src/**/*.rs", 161 | "build.rs", 162 | "paperd-jni/src/**/*.rs", 163 | "paperd-lib/src/**/*.rs", 164 | "Cargo.*", 165 | "paperd-jni/Cargo.*", 166 | "paperd-lib/Cargo.*" 167 | ) 168 | } 169 | inputs.files(inputSource) 170 | 171 | val targetFileName = if (includeConsole) { 172 | "paperd-${dockerImage.systemName}-${dockerImage.versionName}.tar.xz" 173 | } else { 174 | "paperd-${dockerImage.systemName}-${dockerImage.versionName}-no-console.tar.xz" 175 | } 176 | val targetFile = file("${rootProject.buildDir}/$targetFileName") 177 | outputs.file(targetFile) 178 | 179 | val outputDir = file("$buildDir/cargo-target") 180 | val outputFile = file("$outputDir/paperd.tar.xz") 181 | val registryDir = file("$buildDir/cargo-registry") 182 | 183 | doLast { 184 | if (!outputDir.exists() && !outputDir.mkdirs()) { 185 | throw IOException("Failed to create output directory $outputDir") 186 | } 187 | if (!registryDir.exists() && !registryDir.mkdirs()) { 188 | throw IOException("Failed to create registry directory $registryDir") 189 | } 190 | 191 | val uid = runCmd("id", "-u") 192 | val gid = runCmd("id", "-g") 193 | 194 | docker( 195 | "run", "--rm", 196 | "--user", "$uid:$gid", 197 | "-v", "$baseDir:/usr/src/paperd", 198 | "-v", "$outputDir:/usr/src/target", 199 | "-v", "$registryDir:/usr/local/cargo/registry", 200 | "-e", "INCLUDE_CONSOLE_BUILD=$includeConsole", 201 | dockerImage.imageName 202 | ) 203 | 204 | outputFile.renameTo(targetFile) 205 | } 206 | } 207 | } 208 | 209 | fun Project.copyDockerIgnore(): TaskProvider { 210 | return tasks.register("copyDockerIgnore", Copy::class) { 211 | from("${rootProject.projectDir}/targets/.dockerignore") 212 | into(projectDir) 213 | } 214 | } 215 | 216 | fun Project.docker(vararg args: String) { 217 | exec { 218 | commandLine("docker", *args) 219 | workingDir = projectDir 220 | 221 | } 222 | } 223 | 224 | inline fun Project.findOrCreateTask(name: String, crossinline config: (Task).() -> Unit): TaskProvider { 225 | return try { 226 | tasks.register(name) { 227 | config() 228 | } 229 | } catch (e: Exception) { 230 | tasks.named(name) 231 | } 232 | } 233 | 234 | fun runCmd(vararg args: String): String { 235 | val os = ByteArrayOutputStream() 236 | exec { 237 | workingDir = projectDir 238 | commandLine(*args) 239 | standardOutput = os 240 | } 241 | return String(os.toByteArray()).trim() 242 | } 243 | 244 | data class DockerImage( 245 | val systemName: String, 246 | val versionName: String, 247 | val imageName: String, 248 | val dockerFile: File 249 | ) 250 | -------------------------------------------------------------------------------- /release/gradle.properties: -------------------------------------------------------------------------------- 1 | rustVersion=1.44.1 2 | 3 | org.gradle.parallel=true 4 | -------------------------------------------------------------------------------- /release/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperMC/paperd/c5881f5c6a5819e882b794ad8e1b285bf61e3f44/release/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /release/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /release/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /release/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /release/paperd_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s nocasematch 4 | 5 | function help_message() { 6 | echo "paperd installer script" 7 | echo 8 | echo "This script will install the paperd binary to your system. The current OS and versions supported are:" 9 | echo " * Ubuntu 16.04, 18.04, 19.10, 20.04" 10 | echo " * Debian Jessie (8), Stretch (9), Buster (10)" 11 | echo " * Fedora 32, 31" 12 | echo " * CentOS 8, 7" 13 | echo 14 | echo "By default this script will install paperd into /usr/bin and install any packages needed first." 15 | echo "paperd will ask before attempting to run any commands as root. The paperd binary will be installed" 16 | echo "with the owner being the same as the directory it's installed into, and with permissions 755." 17 | echo 18 | echo "Arguments:" 19 | echo " --no-console | Install paperd with no console support. This removes" 20 | echo " | the dependency on ncurses." 21 | echo " --yes | Assume 'yes' answer to all questions, good for running" 22 | echo " | non-interactively in a script." 23 | echo " --bin-location= | Install paperd to a custom directory other than /usr/bin." 24 | echo " -h, --help | Print this help message" 25 | exit 0 26 | } 27 | 28 | function self_build() { 29 | echo "Please see for instructions on how to build paperd yourself." 30 | exit 1 31 | } 32 | 33 | function print_detect_info() { 34 | echo "Detected system info:" 35 | echo "System: $1" 36 | echo "Version: $2" 37 | echo 38 | } 39 | 40 | function ask_okay() { 41 | if [[ "$1" == "true" ]] ; then 42 | echo "Assuming okay because --yes was passed" 43 | return 44 | fi 45 | read -r -n 1 -p "Is this okay? [yN] " val 46 | echo 47 | if [[ "$val" != "y" ]] ; then 48 | echo "Exiting" 49 | exit 1 50 | fi 51 | } 52 | 53 | function debian_package_exists() { 54 | dpkg -s "$1" &> /dev/null 55 | return 56 | } 57 | 58 | function fedora_package_exists() { 59 | [[ "$(rpm -qa "$1")" != "" ]] 60 | return 61 | } 62 | 63 | function setup_debian() { 64 | PACKAGES_NEEDED="" 65 | 66 | if ! debian_package_exists curl ; then 67 | PACKAGES_NEEDED="curl" 68 | fi 69 | if ! debian_package_exists tar ; then 70 | PACKAGES_NEEDED="$PACKAGES_NEEDED tar" 71 | fi 72 | if ! debian_package_exists xz-utils ; then 73 | PACKAGES_NEEDED="$PACKAGES_NEEDED xz-utils" 74 | fi 75 | if [[ "$1" == "true" ]] ; then 76 | if ! debian_package_exists libncurses5 ; then 77 | PACKAGES_NEEDED="$PACKAGES_NEEDED libncurses5" 78 | fi 79 | fi 80 | 81 | if [[ "$PACKAGES_NEEDED" != "" ]] ; then 82 | echo "In order to extract and use paperd, the following packages need to be installed: $PACKAGES_NEEDED" 83 | echo "The following command will be run:" 84 | echo 85 | echo "sudo apt-get install $PACKAGES_NEEDED" 86 | echo 87 | ask_okay "$2" 88 | 89 | # shellcheck disable=SC2086 90 | sudo apt-get install $PACKAGES_NEEDED 91 | echo 92 | fi 93 | } 94 | 95 | function setup_fedora() { 96 | PACKAGES_NEEDED="" 97 | 98 | if ! debian_package_exists curl ; then 99 | PACKAGES_NEEDED="curl" 100 | fi 101 | if ! debian_package_exists tar ; then 102 | PACKAGES_NEEDED="$PACKAGES_NEEDED tar" 103 | fi 104 | if ! debian_package_exists xz-utils ; then 105 | PACKAGES_NEEDED="$PACKAGES_NEEDED xz" 106 | fi 107 | if [[ "$1" == 1 ]] ; then 108 | if ! debian_package_exists libncurses5 ; then 109 | PACKAGES_NEEDED="$PACKAGES_NEEDED ncurses" 110 | fi 111 | fi 112 | 113 | if [[ "$PACKAGES_NEEDED" != "" ]] ; then 114 | echo "In order to extract and use paperd, the following packages need to be installed: $PACKAGES_NEEDED" 115 | echo "The following command will be run:" 116 | echo 117 | echo "sudo yum install $PACKAGES_NEEDED" 118 | echo 119 | ask_okay "$2" 120 | 121 | # shellcheck disable=SC2086 122 | sudo yum install $PACKAGES_NEEDED 123 | echo 124 | fi 125 | } 126 | 127 | SUPPORT_CONSOLE="true" 128 | ALWAYS_YES="false" 129 | INSTALL_DIRECTORY=/usr/bin 130 | 131 | while [[ $# -gt 0 ]] ; do 132 | key="$1" 133 | case $key in 134 | --no-console) 135 | SUPPORT_CONSOLE="false" 136 | shift 137 | ;; 138 | --yes) 139 | ALWAYS_YES="true" 140 | shift 141 | ;; 142 | --bin-location) 143 | INSTALL_DIRECTORY="$2" 144 | shift 145 | shift 146 | ;; 147 | -h|--help) 148 | help_message 149 | ;; 150 | esac 151 | done 152 | 153 | if [[ ! -d "$INSTALL_DIRECTORY" ]] ; then 154 | echo "Error: $INSTALL_DIRECTORY does not exist, exiting." 155 | exit 1 156 | fi 157 | 158 | if [[ ! -f "/etc/os-release" ]] ; then 159 | echo "Error: Cannot determine your distro, which means you're probably using a distro which doesn't have a pre-built version available." 160 | self_build 161 | fi 162 | 163 | . /etc/os-release 164 | 165 | SYSTEM_NAME="" 166 | VERSION_NAME="" 167 | PRETTY_SYSTEM_NAME="" 168 | PRETTY_VERSION_NAME="" 169 | 170 | case "$ID" in 171 | "ubuntu") 172 | SYSTEM_NAME="ubuntu" 173 | PRETTY_SYSTEM_NAME="${SYSTEM_NAME^}" 174 | case "$VERSION_ID" in 175 | "20.04"*) 176 | VERSION_NAME="20.04" 177 | ;; 178 | "19.10"*) 179 | VERSION_NAME="19.10" 180 | ;; 181 | "18.04"*) 182 | VERSION_NAME="18.04" 183 | ;; 184 | "16.04"*) 185 | VERSION_NAME="16.04" 186 | ;; 187 | *) 188 | echo "Error: Unsupported version, only current Ubuntu versions are supported (20.04, 19.10, 18.04, 16.04)" 189 | self_build 190 | ;; 191 | esac 192 | PRETTY_VERSION_NAME="$VERSION_NAME" 193 | 194 | print_detect_info "$PRETTY_SYSTEM_NAME" "$PRETTY_VERSION_NAME" 195 | setup_debian "$SUPPORT_CONSOLE" "$ALWAYS_YES" 196 | ;; 197 | "debian") 198 | SYSTEM_NAME="debian" 199 | PRETTY_SYSTEM_NAME="${SYSTEM_NAME^}" 200 | case "$VERSION_CODENAME" in 201 | "buster") 202 | VERSION_NAME="buster" 203 | ;; 204 | "stretch") 205 | VERSION_NAME="stretch" 206 | ;; 207 | "jessie") 208 | VERSION_NAME="jessie" 209 | ;; 210 | *) 211 | echo "Error: Unsupported version, only current Debian versions are supported (buster, stretch, jessie)" 212 | self_build 213 | ;; 214 | esac 215 | PRETTY_VERSION_NAME="${VERSION_NAME^}" 216 | 217 | print_detect_info "$PRETTY_SYSTEM_NAME" "$PRETTY_VERSION_NAME" 218 | setup_debian "$SUPPORT_CONSOLE" "$ALWAYS_YES" 219 | ;; 220 | "fedora") 221 | SYSTEM_NAME="fedora" 222 | PRETTY_VERSION_NAME="${SYSTEM_NAME^}" 223 | case "$VERSION_ID" in 224 | "32"*) 225 | VERSION_NAME="32" 226 | ;; 227 | "31"*) 228 | VERSION_NAME="31" 229 | ;; 230 | *) 231 | echo "Error: Unsupported version, only current Fedora versions are supported (32, 31)" 232 | self_build 233 | ;; 234 | esac 235 | PRETTY_VERSION_NAME="$VERSION_NAME" 236 | 237 | print_detect_info "$PRETTY_SYSTEM_NAME" "$PRETTY_VERSION_NAME" 238 | setup_fedora "$SUPPORT_CONSOLE" "$ALWAYS_YES" 239 | ;; 240 | "centos") 241 | SYSTEM_NAME="centos" 242 | PRETTY_SYSTEM_NAME="CentOS" 243 | case "$VERSION_ID" in 244 | "8"*) 245 | VERSION_NAME="8" 246 | ;; 247 | "7"*) 248 | VERSION_NAME="7" 249 | ;; 250 | *) 251 | echo "Error: Unsupported version, only current CentOS versions are supported (8, 7)" 252 | self_build 253 | ;; 254 | esac 255 | PRETTY_VERSION_NAME="$VERSION_NAME" 256 | 257 | print_detect_info "$PRETTY_SYSTEM_NAME" "$PRETTY_VERSION_NAME" 258 | setup_fedora "$SUPPORT_CONSOLE" "$ALWAYS_YES" 259 | ;; 260 | *) 261 | echo "Error: Unsupported distro: $ID. Prebuild binaries are only available for Debian, Ubuntu, Fedora, or CentOS." 262 | self_build 263 | ;; 264 | esac 265 | 266 | URL_TARGET="paperd-$SYSTEM_NAME-$VERSION_NAME" 267 | if [[ "$SUPPORT_CONSOLE" == "false" ]] ; then 268 | URL_TARGET="$URL_TARGET-no-console" 269 | fi 270 | URL_TARGET="$URL_TARGET.tar.xz" 271 | 272 | OWNER_USER="$(stat -c "%U" "$INSTALL_DIRECTORY")" 273 | OWNER_GROUP="$(stat -c "%G" "$INSTALL_DIRECTORY")" 274 | 275 | echo "About to download https://dl.demonwav.com/paperd-$URL_TARGET" 276 | ask_okay "$ALWAYS_YES" 277 | echo 278 | 279 | TMP_FILE_NAME="paperd_output_$RANDOM" 280 | TMP_FILE_NAME_TAR="$TMP_FILE_NAME.tar.xz" 281 | curl --proto '=https' --tlsv1.2 -fL -o "/tmp/$TMP_FILE_NAME_TAR" "https://dl.demonwav.com/$URL_TARGET" 282 | 283 | mkdir "/tmp/$TMP_FILE_NAME" 284 | tar fx "/tmp/$TMP_FILE_NAME_TAR" -C "/tmp/$TMP_FILE_NAME" 285 | rm "/tmp/$TMP_FILE_NAME_TAR" 286 | 287 | echo 288 | echo "About to copy paperd to the destination directory and set permissions. The following commands will be run:" 289 | echo 290 | echo "sudo mv /tmp/$TMP_FILE_NAME/paperd $INSTALL_DIRECTORY/paperd" 291 | echo "sudo chown $OWNER_USER:$OWNER_GROUP $INSTALL_DIRECTORY/paperd" 292 | echo "sudo chmod 755 $INSTALL_DIRECTORY/paperd" 293 | echo 294 | ask_okay "$ALWAYS_YES" 295 | 296 | sudo mv "/tmp/$TMP_FILE_NAME/paperd" "$INSTALL_DIRECTORY/paperd" 297 | sudo chown "$OWNER_USER":"$OWNER_GROUP" "$INSTALL_DIRECTORY/paperd" 298 | sudo chmod 755 "$INSTALL_DIRECTORY/paperd" 299 | -------------------------------------------------------------------------------- /release/readme.md: -------------------------------------------------------------------------------- 1 | paperd release build config 2 | =========================== 3 | 4 | To ensure libraries properly link with `paperd` we build a binary for each platform. The base distros are listed in the 5 | `targets` directory, and the `versions.txt` file in each distro directory lists the versions of each distro that we 6 | provide builds for. 7 | 8 | The way this Gradle project is set up, 2 Gradle sub-projects are created for each line in each `versions.txt` file. This 9 | allows for parallel building of each platform. Each version has a `full` build project and a `no console` build project. 10 | 11 | We use Docker to specify the build environment for each platform, so it must be installed on your machine in order to 12 | run a release build. Your user on your machine must also have permission to submit Docker commands as well. 13 | 14 | Running a build 15 | --------------- 16 | 17 | ### Warning (1): 18 | **This build is set up to run parallel builds by default. Each of these builds include creating new Docker containers 19 | and running a full release Cargo build. This can be _extremely_ taxing on your computer depending on how powerful your 20 | machine is and how many threads it can run at the same time. In order to control how many threads Gradle uses for the 21 | build you should specify the `org.gradle.workers.max` property for Gradle: 22 | `./gradlew -Dorg.gradle.workers.max= `. You can also specify smaller builds rather than building 23 | everything, as described below.** 24 | 25 | ### Warning (2): 26 | **This build process creates a Docker image for each version listed in the `versions.txt` file for each distro listed in 27 | the `targets` directory. It then performs 2 separate Cargo builds per Docker image and stores the Cargo registry and 28 | build outputs in the `build/` directory separately for each build configuration. This will download and generate a 29 | significant amount of data, on the order of _30GB or more_. Limit the number of platforms you build if you want to reduce 30 | the amount of data created.** 31 | 32 | #### Build all targets for all platforms: 33 | ```sh 34 | ./gradlew build 35 | ``` 36 | 37 | #### Build all targets for a single platform: 38 | ```sh 39 | ./gradlew :targets::build 40 | Example: 41 | ./gradlew :targets:debian:build 42 | ``` 43 | 44 | #### Build the console and no console target for a single platform version 45 | ```sh 46 | ./gradlew :targets:::build 47 | Example: 48 | ./gradlew :targets:debian:buster:build 49 | ``` 50 | 51 | #### Build a single target 52 | ```sh 53 | ./gradlew :targets::::build 54 | Example: 55 | ./gradlew :targets:debian:buster:full:build 56 | ``` 57 | 58 | #### Clean build outputs 59 | Clean tasks follow the exact same pattern as shown above. 60 | ```sh 61 | ./gradlew clean # clean all outputs for all versions of all platforms 62 | ./gradlew :targets::clean # clean all builds for all versions of a platform 63 | ./gradlew :targets:::clean # clean all outputs for a version of a platform 64 | ./gradlew :targets:::full:clean # clean outputs for a single target 65 | ``` 66 | -------------------------------------------------------------------------------- /release/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.nio.file.Files 2 | import java.nio.file.Path 3 | import java.nio.file.Paths 4 | 5 | rootProject.name = "paperd" 6 | 7 | val rootProjectDir: Path = rootProject.projectDir.toPath() 8 | val targetsDir: Path = file("targets").toPath() 9 | 10 | Files.walk(targetsDir) 11 | .filter { Files.isRegularFile(it) && it.fileName.toString() == "versions.txt" } 12 | .forEach { path -> 13 | val systemName = targetsDir.relativize(path).getName(0).toString() 14 | Files.lines(path) 15 | .filter { it.isNotBlank() } 16 | .forEach { line -> 17 | val systemProjectName = "targets:$systemName:$line" 18 | val fullProjectName = "$systemProjectName:full" 19 | val fullProjectDir = rootProjectDir.resolve(Paths.get("build", systemName, line, "full")) 20 | Files.createDirectories(fullProjectDir) 21 | val noConsoleProjectName = "$systemProjectName:noConsole" 22 | val noConsoleProjectDir = rootProjectDir.resolve(Paths.get("build", systemName, line, "noConsole")) 23 | Files.createDirectories(noConsoleProjectDir) 24 | 25 | include(fullProjectName) 26 | project(":$fullProjectName").projectDir = fullProjectDir.toFile() 27 | include(noConsoleProjectName) 28 | project(":$noConsoleProjectName").projectDir = noConsoleProjectDir.toFile() 29 | project(":$systemProjectName").projectDir = fullProjectDir.parent.toFile() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /release/targets/.dockerignore: -------------------------------------------------------------------------------- 1 | build/ 2 | full/ 3 | noConsole/ 4 | -------------------------------------------------------------------------------- /release/targets/build_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "Building paperd" 6 | 7 | if [[ "$INCLUDE_CONSOLE_BUILD" == "true" ]] ; then 8 | EXTRA_ARGS="--features console" 9 | fi 10 | # shellcheck disable=SC2086 11 | cargo build -j 1 --target-dir=/usr/src/target --color always --release $EXTRA_ARGS 12 | 13 | paperd_path="/usr/src/target/release/" 14 | paperd_file="${paperd_path}paperd" 15 | 16 | echo "Stripping unneeded symbols from paperd" 17 | strip "$paperd_file" 18 | 19 | echo "Packaging paperd" 20 | XZ_OPT=-9 tar -Jcf /usr/src/target/paperd.tar.xz -C "$paperd_path" "paperd" 21 | 22 | echo "Build complete, output file: /usr/src/target/paperd.tar.xz" 23 | -------------------------------------------------------------------------------- /release/targets/centos/centos.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG version 2 | FROM centos:$version 3 | 4 | RUN yum install -y \ 5 | curl \ 6 | ncurses-devel \ 7 | && yum group install -y "Development Tools" 8 | 9 | # Install Rust 10 | ARG rustVersion 11 | ENV RUSTUP_HOME=/usr/local/rustup \ 12 | CARGO_HOME=/usr/local/cargo \ 13 | PATH=/usr/local/cargo/bin:$PATH \ 14 | RUST_VERSION=$rustVersion 15 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path \ 16 | && chmod -R a+w $RUSTUP_HOME $CARGO_HOME 17 | 18 | # Install AdoptOpenJDK 8 19 | RUN echo $'[AdoptOpenJDK]\n\ 20 | name=AdoptOpenJDK\n\ 21 | baseurl=http://adoptopenjdk.jfrog.io/adoptopenjdk/rpm/centos/$releasever/$basearch\n\ 22 | enabled=1\n\ 23 | gpgcheck=1\n\ 24 | gpgkey=https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public\n'\ 25 | > /etc/yum.repos.d/adoptopenjdk.repo \ 26 | && yum install adoptopenjdk-8-hotspot -y 27 | 28 | WORKDIR /usr/src/paperd 29 | CMD ["./release/targets/build_release.sh"] 30 | -------------------------------------------------------------------------------- /release/targets/centos/versions.txt: -------------------------------------------------------------------------------- 1 | 8 2 | 7 3 | -------------------------------------------------------------------------------- /release/targets/debian/debian.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG version 2 | FROM buildpack-deps:$version 3 | 4 | # Base utilities 5 | RUN mkdir -p /usr/share/man/man1 \ 6 | && apt-get update \ 7 | && apt-get install --no-install-recommends -y \ 8 | apt-transport-https \ 9 | apt-utils \ 10 | ca-certificates \ 11 | gnupg \ 12 | libncurses-dev \ 13 | libncursesw5-dev \ 14 | software-properties-common \ 15 | wget \ 16 | xz-utils 17 | 18 | # Install Rust 19 | ARG rustVersion 20 | ENV RUSTUP_HOME=/usr/local/rustup \ 21 | CARGO_HOME=/usr/local/cargo \ 22 | PATH=/usr/local/cargo/bin:$PATH \ 23 | RUST_VERSION=$rustVersion 24 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path \ 25 | && chmod -R a+w $RUSTUP_HOME $CARGO_HOME 26 | 27 | # Install AdoptOpenJDK 8 28 | RUN wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - \ 29 | && add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ \ 30 | && apt-get update \ 31 | && apt-get install --no-install-recommends -y adoptopenjdk-8-hotspot 32 | 33 | WORKDIR /usr/src/paperd 34 | 35 | CMD ["./release/targets/build_release.sh"] 36 | -------------------------------------------------------------------------------- /release/targets/debian/versions.txt: -------------------------------------------------------------------------------- 1 | buster 2 | stretch 3 | jessie 4 | -------------------------------------------------------------------------------- /release/targets/fedora/fedora.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG version 2 | FROM fedora:$version 3 | 4 | RUN yum install -y \ 5 | curl \ 6 | ncurses-devel \ 7 | xz \ 8 | && yum group install -y "Development Tools" 9 | 10 | # Install Rust 11 | ARG rustVersion 12 | ENV RUSTUP_HOME=/usr/local/rustup \ 13 | CARGO_HOME=/usr/local/cargo \ 14 | PATH=/usr/local/cargo/bin:$PATH \ 15 | RUST_VERSION=$rustVersion 16 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path \ 17 | && chmod -R a+w $RUSTUP_HOME $CARGO_HOME 18 | 19 | # Install AdoptOpenJDK 8 20 | RUN echo $'[AdoptOpenJDK]\n\ 21 | name=AdoptOpenJDK\n\ 22 | baseurl=http://adoptopenjdk.jfrog.io/adoptopenjdk/rpm/fedora/$releasever/$basearch\n\ 23 | enabled=1\n\ 24 | gpgcheck=1\n\ 25 | gpgkey=https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public\n'\ 26 | > /etc/yum.repos.d/adoptopenjdk.repo \ 27 | && yum install adoptopenjdk-8-hotspot -y 28 | 29 | WORKDIR /usr/src/paperd 30 | CMD ["./release/targets/build_release.sh"] 31 | -------------------------------------------------------------------------------- /release/targets/fedora/versions.txt: -------------------------------------------------------------------------------- 1 | 32 2 | 31 3 | -------------------------------------------------------------------------------- /release/targets/ubuntu/ubuntu.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG version 2 | FROM ubuntu:$version 3 | 4 | # Base utilities 5 | ENV DEBIAN_FRONTEND="noninteractive" 6 | RUN apt-get update \ 7 | && apt-get install --no-install-recommends -y \ 8 | apt-transport-https \ 9 | ca-certificates \ 10 | curl \ 11 | build-essential \ 12 | gnupg \ 13 | libncurses-dev \ 14 | libncursesw5-dev \ 15 | software-properties-common \ 16 | wget 17 | 18 | # Install Rust 19 | ARG rustVersion 20 | ENV RUSTUP_HOME=/usr/local/rustup \ 21 | CARGO_HOME=/usr/local/cargo \ 22 | PATH=/usr/local/cargo/bin:$PATH \ 23 | RUST_VERSION=$rustVersion 24 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path \ 25 | && chmod -R a+w $RUSTUP_HOME $CARGO_HOME 26 | 27 | # Install AdoptOpenJDK 8 28 | RUN wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - \ 29 | && add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ \ 30 | && apt-get update \ 31 | && apt-get install --no-install-recommends -y adoptopenjdk-8-hotspot 32 | 33 | WORKDIR /usr/src/paperd 34 | CMD ["./release/targets/build_release.sh"] 35 | -------------------------------------------------------------------------------- /release/targets/ubuntu/versions.txt: -------------------------------------------------------------------------------- 1 | 20.04 2 | 19.10 3 | 18.04 4 | 16.04 5 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, Shell, SubCommand}; 17 | use std::io; 18 | 19 | pub fn get_cmd_line_matches<'a>() -> ArgMatches<'a> { 20 | let start_text = run_after_text("start"); 21 | let run_text = run_after_text("run"); 22 | return handle_cmd_line(start_text.as_str(), run_text.as_str()).get_matches(); 23 | } 24 | 25 | pub fn gen_completions(shell: &str) { 26 | let start_text = run_after_text("start"); 27 | let run_text = run_after_text("run"); 28 | handle_cmd_line(start_text.as_str(), run_text.as_str()).gen_completions_to( 29 | "paperd", 30 | shell.parse::().unwrap(), 31 | &mut io::stdout(), 32 | ); 33 | } 34 | 35 | fn handle_cmd_line<'a, 'b>(start_after: &'b str, run_after: &'b str) -> App<'a, 'b> { 36 | let sock_arg = Arg::<'a, 'b>::with_name("SOCK") 37 | .help( 38 | "Custom socket file to send commands to a running server. If not set, the \ 39 | PAPERD_SOCK environment variable will be checked. If neither are set, the default \ 40 | value is ./paper.sock.", 41 | ) 42 | .short("s") 43 | .long("sock") 44 | .takes_value(true); 45 | 46 | let license_text = r"ISSUES: 47 | Please submit any bugs or issues with paperd to the paperd issue tracker: 48 | https://github.com/PaperMC/paperd/issues 49 | 50 | SOURCE: 51 | The source code for this program is available on GitHub: 52 | https://github.com/PaperMC/paperd 53 | 54 | LICENSE: 55 | GNU LGPLv3 only (no future versions) 56 | https://www.gnu.org/licenses/lgpl-3.0.en.html"; 57 | 58 | return App::new("paperd") 59 | .setting(AppSettings::SubcommandRequiredElseHelp) 60 | .setting(AppSettings::GlobalVersion) 61 | .version(crate_version!()) 62 | .author("PaperMC (papermc.io)") 63 | .about("PaperMC daemon for running and controlling daemonized PaperMC servers.") 64 | .subcommand( 65 | SubCommand::with_name("status") 66 | .about("Get the status of the currently running server.") 67 | .arg(&sock_arg) 68 | .display_order(1) 69 | .after_help(license_text), 70 | ) 71 | .subcommand( 72 | SubCommand::with_name("send") 73 | .about("Send a command to the running MC server.") 74 | .arg(&sock_arg) 75 | .arg(tail_arg( 76 | "Tail the server log after sending the command to the \ 77 | server, useful for viewing the response. Press C-c to quit.", 78 | )) 79 | .arg( 80 | Arg::with_name("COMMAND") 81 | .help( 82 | "The command to send to the MC server. Arguments will be appended \ 83 | together as one string, with a single space between arguments. If you \ 84 | prefer to have more control over whitespace then you should quote your \ 85 | arguments first.", 86 | ) 87 | .multiple(true) 88 | .allow_hyphen_values(true) 89 | .required(true), 90 | ) 91 | .display_order(1), 92 | ) 93 | .console(&sock_arg) 94 | .subcommand( 95 | SubCommand::with_name("log") 96 | .about("Print recent log messages from the running MC server.") 97 | .arg(&sock_arg) 98 | .arg( 99 | Arg::with_name("LINES") 100 | .help("The number of log messages to print.") 101 | .short("l") 102 | .long("lines") 103 | .default_value("10"), 104 | ) 105 | .arg(tail_arg( 106 | "Tail the server log rather than just printing recent \ 107 | messages. Press C-c.", 108 | )) 109 | .display_order(1), 110 | ) 111 | .subcommand( 112 | SubCommand::with_name("timings") 113 | .about("If timings is enabled, generate a report and return the URL.") 114 | .arg(&sock_arg) 115 | .display_order(1), 116 | ) 117 | .subcommand( 118 | SubCommand::with_name("start") 119 | .about("Start the MC server in the background.") 120 | .arg(tail_arg( 121 | "Tail the server log after starting the server. Press C-c to \ 122 | quit (will NOT stop the server).", 123 | )) 124 | .java_run(start_after) 125 | .arg( 126 | Arg::with_name("KEEP_ALIVE") 127 | .help( 128 | "Restart the server when it crashes. If the server stops gracefully \ 129 | from a shutdown command it will not restart, but if the server \ 130 | shutdowns down due to a crash, paperd will restart it automatically.", 131 | ) 132 | .short("k") 133 | .long("keep-alive"), 134 | ) 135 | .display_order(2), 136 | ) 137 | .subcommand( 138 | SubCommand::with_name("run") 139 | .about("Start the MC server in the foreground.") 140 | .java_run(run_after) 141 | .display_order(2), 142 | ) 143 | .subcommand( 144 | SubCommand::with_name("stop") 145 | .about( 146 | "Stop the MC server gracefully. This is functionally \ 147 | equivalent to sending the 'stop' command to the server.", 148 | ) 149 | .arg(&sock_arg) 150 | .arg( 151 | Arg::with_name("FORCE") 152 | .help( 153 | "Forcefully kill the server if it does not respond \ 154 | within a timeout. This will first attempt to stop the server \ 155 | gracefully as normal before forcefully killing the server. \ 156 | Forcefully killing the server can result in loss of data and \ 157 | is not recommended. Only do so if the server is not responding.", 158 | ) 159 | .short("-f") 160 | .long("--force"), 161 | ) 162 | .arg( 163 | Arg::with_name("KILL") 164 | .help( 165 | "Immediately forcefully kill the server. This will \ 166 | NOT attempt to gracefully shutdown the server. This can result \ 167 | in loss of data, it is not recommended unless the server is \ 168 | not responding.", 169 | ) 170 | .short("-k") 171 | .long("--kill"), 172 | ) 173 | .group(ArgGroup::with_name("FORCE_ARGS").args(&["FORCE", "KILL"])) 174 | .display_order(3), 175 | ) 176 | .subcommand( 177 | SubCommand::with_name("restart") 178 | .about( 179 | "Tell the server to shutdown with an exit code telling paperd to restart. \ 180 | This will reuse the same command-line the original command invocation \ 181 | used, but if the jar has been replaced it will be used instead of the \ 182 | original jar. The paperd instance will not be changed if it has been updated, \ 183 | however, as it does not restart.", 184 | ) 185 | .arg(&sock_arg) 186 | .arg(tail_arg( 187 | "Tail the server log after asking the server to restart. Press \ 188 | C-c to quit.", 189 | )) 190 | .display_order(3), 191 | ) 192 | .subcommand( 193 | SubCommand::with_name("completions") 194 | .about("Generate completion scripts for your shell") 195 | .setting(AppSettings::ArgRequiredElseHelp) 196 | .arg( 197 | Arg::with_name("SHELL") 198 | .help("The shell to generate the completion script for") 199 | .possible_values(&["bash", "zsh", "fish"]), 200 | ) 201 | .after_help(COMPLETIONS_HELP) 202 | .display_order(4), 203 | ) 204 | .after_help(license_text); 205 | } 206 | 207 | trait PaperArg<'a, 'b> { 208 | fn java_run(self, after_text: &'b str) -> Self; 209 | fn console(self, arg: &Arg<'a, 'b>) -> Self; 210 | } 211 | 212 | impl<'a, 'b> PaperArg<'a, 'b> for App<'a, 'b> { 213 | fn java_run(self, after_text: &'b str) -> Self { 214 | return self 215 | .arg( 216 | Arg::with_name("JVM") 217 | .help( 218 | "The java binary to use to execute the paperclip jar. By default \ 219 | paperd will search the PATH. If there is no java binary on the PATH, \ 220 | paperd will use the JAVA_HOME environment variable instead. If neither of \ 221 | these finds a JVM, this argument must be supplied.", 222 | ) 223 | .long("jvm") 224 | .takes_value(true), 225 | ) 226 | .arg( 227 | Arg::with_name("JAR") 228 | .help("The jar to run.") 229 | .long("jar") 230 | .takes_value(true) 231 | .default_value("paperclip.jar"), 232 | ) 233 | .arg( 234 | Arg::with_name("CWD") 235 | .help( 236 | "The working directory of the server. Default is the parent \ 237 | directory of the jar.", 238 | ) 239 | .short("w") 240 | .long("--working-dir") 241 | .takes_value(true), 242 | ) 243 | .arg( 244 | Arg::with_name("DEFAULT_ARGS") 245 | .help( 246 | "Use a default set of recommended JVM arguments (Aikar's flags) \ 247 | with the specified amount of memory. The format should be something \ 248 | like 500m or 10G. It's recommended to provide as much memory as possible \ 249 | up to 10G. You may not provide custom arguments if defaults are used.", 250 | ) 251 | .short("d") 252 | .long("default-args") 253 | .value_name("MEMORY") 254 | .takes_value(true), 255 | ) 256 | .arg( 257 | Arg::with_name("CUSTOM_ARGS") 258 | .help( 259 | "Provide a custom set of JVM arguments to be used when running \ 260 | the jar. This argument specifies all JVM arguments which will be \ 261 | passed, there are no defaults when using this argument. You may not pass \ 262 | custom arguments while also using -d or --default-args.", 263 | ) 264 | .takes_value(true) 265 | .allow_hyphen_values(true) 266 | .multiple(true), 267 | ) 268 | .arg( 269 | Arg::with_name("SERVER_ARGS") 270 | .help( 271 | "Provide a set of server arguments to be used when running the jar. One \ 272 | instance of this argument represents a single quoted argument, including \ 273 | any whitespace present. Repeat this argument with a value for each \ 274 | argument you want to include. For example, to pass '--port 2000' as server \ 275 | arguments, you would pass '-s --port -s 2000'.", 276 | ) 277 | .short("s") 278 | .long("server-arg") 279 | .takes_value(true) 280 | .multiple(true) 281 | .number_of_values(1), 282 | ) 283 | .arg( 284 | Arg::with_name("CONFIG_FILE") 285 | .help( 286 | "Define a JSON configuration file which specifies all other arguments. \ 287 | This allows defining complex or large startup commands permanently for \ 288 | using them again for each server startup. The JSON configuration file can \ 289 | define more configuration past what is possible with just command line \ 290 | arguments. See documentation for the JSON configuration file below in the \ 291 | CONFIG FILE section.", 292 | ) 293 | .long("config-file") 294 | .takes_value(true), 295 | ) 296 | .group( 297 | ArgGroup::with_name("JVM_ARGS") 298 | .arg("DEFAULT_ARGS") 299 | .arg("CUSTOM_ARGS"), 300 | ) 301 | .after_help(after_text); 302 | } 303 | 304 | #[cfg(feature = "console")] 305 | fn console(self, arg: &Arg<'a, 'b>) -> Self { 306 | return self.subcommand( 307 | SubCommand::with_name("console") 308 | .about("Attach to the console of the running MC server.") 309 | .arg(arg) 310 | .display_order(1), 311 | ); 312 | } 313 | 314 | #[cfg(not(feature = "console"))] 315 | fn console(self, _: &Arg<'a, 'b>) -> Self { 316 | return self; 317 | } 318 | } 319 | 320 | fn tail_arg(message: &str) -> Arg { 321 | return Arg::with_name("TAIL").help(message).short("t").long("tail"); 322 | } 323 | 324 | fn run_after_text(command_text: &str) -> String { 325 | return format!( 326 | r#"EXAMPLES: 327 | The --default-args argument or the 'CUSTOM_ARGS' arguments are mutually exclusive. That is, you 328 | can either use --default-args OR specify custom arguments, but not both. 329 | 330 | Examples: 331 | $ paperd {cmd} -d 10G 332 | OR 333 | $ paperd {cmd} --default-args 2G 334 | OR 335 | $ paperd {cmd} -- -Xmx5G -Xms5G 336 | 337 | CONFIG FILE: 338 | You may pass options to this command using a JSON configuration file instead of command line 339 | arguments using the --config-file argument. When using this argument the config file values 340 | have lower precedence than the other command line arguments, so any other arguments specified 341 | will effectively override any configuration values present in the file. The config file must be 342 | a valid JSON file with the following keys. All keys are optional. 343 | 344 | * jvm | This is equivalent to the --jvm argument. 345 | * jarFile | This is equivalent to the --jar argument. 346 | * workingDir | This is equivalent to the -w or --working-dir argument. 347 | * serverArgs | This is equivalent to the -s or --server-arg argument. 348 | * jvmArgs | This is equivalent to the CUSTOM_ARGS argument. 349 | 350 | The serverArgs and jvmArgs fields are lists of arguments, where each entry in the list is one 351 | argument to be passed to either the server or the JVM respectively. This includes any whitespace 352 | which may appear in the argument. All other fields are JSON strings. 353 | 354 | Example JSON file: 355 | {{ 356 | "jarFile": "../some/global/paperclip.jar", 357 | "workingDir": "/minecraft/servers/paper", 358 | "jvmArgs": ["-Xmx5G", "-Xms5G"], 359 | "serverArgs": ["--port", "22222"] 360 | }}"#, 361 | cmd = command_text 362 | ); 363 | } 364 | 365 | // This excellent description was taken from rustup 366 | // https://github.com/rust-lang/rustup.rs/blob/256488923d3fb2637b7d706002b3e6d2db917590/src/cli/help.rs#L156 367 | pub static COMPLETIONS_HELP: &str = r"DISCUSSION: 368 | One can generate a completion script for `paperd` that is 369 | compatible with a given shell. The script is output on `stdout` 370 | allowing one to re-direct the output to the file of their 371 | choosing. Where you place the file will depend on which shell, and 372 | which operating system you are using. Your particular 373 | configuration may also determine where these scripts need to be 374 | placed. 375 | 376 | Here are some common set ups for the three supported shells under 377 | Unix and similar operating systems (such as GNU/Linux). 378 | 379 | BASH: 380 | 381 | Completion files are commonly stored in `/etc/bash_completion.d/` for 382 | system-wide commands, but can be stored in 383 | `~/.local/share/bash-completion/completions` for user-specific commands. 384 | Run the command: 385 | 386 | $ mkdir -p ~/.local/share/bash-completion/completions 387 | $ paperd completions bash >> ~/.local/share/bash-completion/completions/paperd 388 | 389 | This installs the completion script. You may have to log out and 390 | log back in to your shell session for the changes to take affect. 391 | 392 | BASH (macOS/Homebrew): 393 | 394 | Homebrew stores bash completion files within the Homebrew directory. 395 | With the `bash-completion` brew formula installed, run the command: 396 | 397 | $ mkdir -p $(brew --prefix)/etc/bash_completion.d 398 | $ paperd completions bash > $(brew --prefix)/etc/bash_completion.d/paperd.bash-completion 399 | 400 | FISH: 401 | 402 | Fish completion files are commonly stored in 403 | `$HOME/.config/fish/completions`. Run the command: 404 | 405 | $ mkdir -p ~/.config/fish/completions 406 | $ paperd completions fish > ~/.config/fish/completions/paperd.fish 407 | 408 | This installs the completion script. You may have to log out and 409 | log back in to your shell session for the changes to take affect. 410 | 411 | ZSH: 412 | 413 | ZSH completions are commonly stored in any directory listed in 414 | your `$fpath` variable. To use these completions, you must either 415 | add the generated script to one of those directories, or add your 416 | own to this list. 417 | 418 | Adding a custom directory is often the safest bet if you are 419 | unsure of which directory to use. First create the directory; for 420 | this example we'll create a hidden directory inside our `$HOME` 421 | directory: 422 | 423 | $ mkdir ~/.zfunc 424 | 425 | Then add the following lines to your `.zshrc` just before 426 | `compinit`: 427 | 428 | fpath+=~/.zfunc 429 | 430 | Now you can install the completions script using the following 431 | command: 432 | 433 | $ paperd completions zsh > ~/.zfunc/_paperd 434 | 435 | You must then either log out and log back in, or simply run 436 | 437 | $ exec zsh 438 | 439 | for the new completions to take affect. 440 | 441 | CUSTOM LOCATIONS: 442 | 443 | Alternatively, you could save these files to the place of your 444 | choosing, such as a custom directory inside your $HOME. Doing so 445 | will require you to add the proper directives, such as `source`ing 446 | inside your login script. Consult your shells documentation for 447 | how to add such directives."; 448 | -------------------------------------------------------------------------------- /src/console/ansi.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::console::{ 17 | COLOR_BRIGHT_BLUE, COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN, COLOR_BRIGHT_MAGENTA, 18 | COLOR_BRIGHT_RED, COLOR_BRIGHT_WHITE, COLOR_BRIGHT_YELLOW, COLOR_DARK_GRAY, 19 | }; 20 | use ncurses::{ 21 | attr_t, attroff, attron, chtype, init_pair, mvaddstr, mvhline, A_BLINK, A_BOLD, A_ITALIC, 22 | A_UNDERLINE, COLOR_BLACK, COLOR_BLUE, COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_PAIR, 23 | COLOR_RED, COLOR_WHITE, COLOR_YELLOW, 24 | }; 25 | use std::cmp::min; 26 | use std::slice::Iter; 27 | 28 | const BLACK_PAIR: i16 = 1; 29 | const DARK_BLUE_PAIR: i16 = 2; 30 | const DARK_GREEN_PAIR: i16 = 3; 31 | const DARK_AQUA_PAIR: i16 = 4; 32 | const DARK_RED_PAIR: i16 = 5; 33 | const DARK_PURPLE_PAIR: i16 = 6; 34 | const GOLD_PAIR: i16 = 7; 35 | const GRAY_PAIR: i16 = 8; 36 | const DARK_GRAY_PAIR: i16 = 9; 37 | const BLUE_PAIR: i16 = 10; 38 | const GREEN_PAIR: i16 = 11; 39 | const AQUA_PAIR: i16 = 12; 40 | const RED_PAIR: i16 = 13; 41 | const LIGHT_PURPLE_PAIR: i16 = 14; 42 | const YELLOW_PAIR: i16 = 15; 43 | const WHITE_PAIR: i16 = 16; 44 | 45 | // These obviously don't cover all ANSI code cases, not even close. These simply represent the 46 | // possible output codes from TerminalConsoleAppender: 47 | // - https://github.com/Minecrell/TerminalConsoleAppender/blob/b8117c8f0301c832a06c4fcbbf372528a70bcaf4/src/main/java/net/minecrell/terminalconsole/MinecraftFormattingConverter.java#L89-L110 48 | // - https://github.com/Minecrell/TerminalConsoleAppender/blob/b8117c8f0301c832a06c4fcbbf372528a70bcaf4/src/main/java/net/minecrell/terminalconsole/HighlightErrorConverter.java#L62-L64 49 | #[derive(Copy, Clone, Debug, PartialEq)] 50 | pub enum AnsiCode { 51 | Black, // §0 52 | DarkBlue, // §1 53 | DarkGreen, // §2 54 | DarkAqua, // §3 55 | DarkRed, // §4 56 | DarkPurple, // §5 57 | Gold, // §6 58 | Gray, // §7 59 | DarkGray, // §8 60 | Blue, // §9 61 | Green, // §a 62 | Aqua, // §b 63 | Red, // §c 64 | LightPurple, // §d 65 | Yellow, // §e 66 | White, // §f 67 | Obfuscated, // §k 68 | Bold, // §l 69 | Strikethrough, // §m 70 | Underline, // §n 71 | Italic, // §o 72 | Reset, // §r 73 | Warn, 74 | Error, 75 | } 76 | 77 | impl AnsiCode { 78 | #[inline] 79 | fn prefix() -> &'static str { 80 | return "\u{001B}["; 81 | } 82 | 83 | #[inline] 84 | fn suffix() -> &'static str { 85 | return "m"; 86 | } 87 | 88 | #[inline] 89 | fn mc_prefix() -> char { 90 | return '§'; 91 | } 92 | 93 | pub fn init_colors() { 94 | for code in Self::iter() { 95 | // Warn and Error use yellow and red colors 96 | if *code == AnsiCode::Warn || *code == AnsiCode::Error { 97 | continue; 98 | } 99 | if let (Some(pair), _) = code.attr_pair() { 100 | init_pair(pair.id, pair.foreground, pair.background); 101 | } 102 | } 103 | } 104 | 105 | pub fn ansi_code(&self) -> &'static str { 106 | return match *self { 107 | AnsiCode::Black => "\u{001B}[0;30m", 108 | AnsiCode::DarkBlue => "\u{001B}[0;34m", 109 | AnsiCode::DarkGreen => "\u{001B}[0;32m", 110 | AnsiCode::DarkAqua => "\u{001B}[0;36m", 111 | AnsiCode::DarkRed => "\u{001B}[0;31m", 112 | AnsiCode::DarkPurple => "\u{001B}[0;35m", 113 | AnsiCode::Gold => "\u{001B}[0;33m", 114 | AnsiCode::Gray => "\u{001B}[0;37m", 115 | AnsiCode::DarkGray => "\u{001B}[0;30;1m", 116 | AnsiCode::Blue => "\u{001B}[0;34;1m", 117 | AnsiCode::Green => "\u{001B}[0;32;1m", 118 | AnsiCode::Aqua => "\u{001B}[0;36;1m", 119 | AnsiCode::Red => "\u{001B}[0;31;1m", 120 | AnsiCode::LightPurple => "\u{001B}[0;35;1m", 121 | AnsiCode::Yellow => "\u{001B}[0;33;1m", 122 | AnsiCode::White => "\u{001B}[0;37;1m", 123 | AnsiCode::Obfuscated => "\u{001B}[5m", 124 | AnsiCode::Bold => "\u{001B}[21m", 125 | AnsiCode::Strikethrough => "\u{001B}[9m", 126 | AnsiCode::Underline => "\u{001B}[4m", 127 | AnsiCode::Italic => "\u{001B}[3m", 128 | AnsiCode::Reset => "\u{001B}[m", 129 | AnsiCode::Warn => "\u{001B}[31;1m", 130 | AnsiCode::Error => "\u{001B}[33;1m", 131 | }; 132 | } 133 | 134 | fn attr_pair(&self) -> (Option, Option) { 135 | return match *self { 136 | AnsiCode::Black => (Some(pair(BLACK_PAIR, COLOR_BLACK, COLOR_WHITE)), None), 137 | AnsiCode::DarkBlue => (Some(pair_fg(DARK_BLUE_PAIR, COLOR_BLUE)), None), 138 | AnsiCode::DarkGreen => (Some(pair_fg(DARK_GREEN_PAIR, COLOR_GREEN)), None), 139 | AnsiCode::DarkAqua => (Some(pair_fg(DARK_AQUA_PAIR, COLOR_CYAN)), None), 140 | AnsiCode::DarkRed => (Some(pair_fg(DARK_RED_PAIR, COLOR_RED)), None), 141 | AnsiCode::DarkPurple => (Some(pair_fg(DARK_PURPLE_PAIR, COLOR_MAGENTA)), None), 142 | AnsiCode::Gold => (Some(pair_fg(GOLD_PAIR, COLOR_YELLOW)), None), 143 | AnsiCode::Gray => (Some(pair(GRAY_PAIR, COLOR_WHITE, COLOR_BLACK)), None), 144 | AnsiCode::DarkGray => ( 145 | Some(pair(DARK_GRAY_PAIR, COLOR_DARK_GRAY, COLOR_WHITE)), 146 | None, 147 | ), 148 | AnsiCode::Blue => (Some(pair_fg(BLUE_PAIR, COLOR_BRIGHT_BLUE)), None), 149 | AnsiCode::Green => (Some(pair_fg(GREEN_PAIR, COLOR_BRIGHT_GREEN)), None), 150 | AnsiCode::Aqua => (Some(pair_fg(AQUA_PAIR, COLOR_BRIGHT_CYAN)), None), 151 | AnsiCode::Red => (Some(pair_fg(RED_PAIR, COLOR_BRIGHT_RED)), None), 152 | AnsiCode::LightPurple => (Some(pair_fg(LIGHT_PURPLE_PAIR, COLOR_BRIGHT_MAGENTA)), None), 153 | AnsiCode::Yellow => (Some(pair_fg(YELLOW_PAIR, COLOR_BRIGHT_YELLOW)), None), 154 | AnsiCode::White => ( 155 | Some(pair(WHITE_PAIR, COLOR_BRIGHT_WHITE, COLOR_BLACK)), 156 | None, 157 | ), 158 | AnsiCode::Obfuscated => (None, Some(A_BLINK())), 159 | AnsiCode::Bold => (None, Some(A_BOLD())), 160 | AnsiCode::Strikethrough => (None, None), // ncurses doesn't support strikethrough, so this does nothing 161 | AnsiCode::Underline => (None, Some(A_UNDERLINE())), 162 | AnsiCode::Italic => (None, Some(A_ITALIC())), 163 | AnsiCode::Reset => (None, None), // this is a special case, will cause all other effects to undo 164 | AnsiCode::Warn => (Some(pair(YELLOW_PAIR, COLOR_YELLOW, -1)), Some(A_BOLD())), 165 | AnsiCode::Error => (Some(pair(RED_PAIR, COLOR_RED, -1)), Some(A_BOLD())), 166 | }; 167 | } 168 | 169 | pub fn mc_code(&self) -> &'static str { 170 | return match *self { 171 | AnsiCode::Black => "§0", 172 | AnsiCode::DarkBlue => "§1", 173 | AnsiCode::DarkGreen => "§2", 174 | AnsiCode::DarkAqua => "§3", 175 | AnsiCode::DarkRed => "§4", 176 | AnsiCode::DarkPurple => "§5", 177 | AnsiCode::Gold => "§6", 178 | AnsiCode::Gray => "§7", 179 | AnsiCode::DarkGray => "§8", 180 | AnsiCode::Blue => "§9", 181 | AnsiCode::Green => "§a", 182 | AnsiCode::Aqua => "§b", 183 | AnsiCode::Red => "§c", 184 | AnsiCode::LightPurple => "§d", 185 | AnsiCode::Yellow => "§e", 186 | AnsiCode::White => "§f", 187 | AnsiCode::Obfuscated => "§k", 188 | AnsiCode::Bold => "§l", 189 | AnsiCode::Strikethrough => "§m", 190 | AnsiCode::Underline => "§n", 191 | AnsiCode::Italic => "§o", 192 | AnsiCode::Reset => "§r", 193 | AnsiCode::Warn => "", 194 | AnsiCode::Error => "", 195 | }; 196 | } 197 | 198 | fn iter() -> Iter<'static, AnsiCode> { 199 | static CODES: [AnsiCode; 24] = [ 200 | AnsiCode::Black, 201 | AnsiCode::DarkBlue, 202 | AnsiCode::DarkGreen, 203 | AnsiCode::DarkAqua, 204 | AnsiCode::DarkRed, 205 | AnsiCode::DarkPurple, 206 | AnsiCode::Gold, 207 | AnsiCode::Gray, 208 | AnsiCode::DarkGray, 209 | AnsiCode::Blue, 210 | AnsiCode::Green, 211 | AnsiCode::Aqua, 212 | AnsiCode::Red, 213 | AnsiCode::LightPurple, 214 | AnsiCode::Yellow, 215 | AnsiCode::White, 216 | AnsiCode::Obfuscated, 217 | AnsiCode::Bold, 218 | AnsiCode::Strikethrough, 219 | AnsiCode::Underline, 220 | AnsiCode::Italic, 221 | AnsiCode::Reset, 222 | AnsiCode::Warn, 223 | AnsiCode::Error, 224 | ]; 225 | return CODES.iter(); 226 | } 227 | 228 | fn enable(self) { 229 | let (pair, attr) = self.attr_pair(); 230 | if let Some(PairValues { id, .. }) = pair { 231 | attron(COLOR_PAIR(id)); 232 | } 233 | if let Some(attr) = attr { 234 | attron(attr); 235 | } 236 | } 237 | 238 | fn disable(self) { 239 | let (pair, attr) = self.attr_pair(); 240 | if let Some(attr) = attr { 241 | attroff(attr); 242 | } 243 | if let Some(PairValues { id, .. }) = pair { 244 | attroff(COLOR_PAIR(id)); 245 | } 246 | } 247 | } 248 | 249 | struct PairValues { 250 | id: i16, 251 | foreground: i16, 252 | background: i16, 253 | } 254 | 255 | fn pair(id: i16, foreground: i16, background: i16) -> PairValues { 256 | return PairValues { 257 | id, 258 | foreground, 259 | background, 260 | }; 261 | } 262 | 263 | fn pair_fg(id: i16, foreground: i16) -> PairValues { 264 | return PairValues { 265 | id, 266 | foreground, 267 | background: -1, 268 | }; 269 | } 270 | 271 | pub enum MessageElement { 272 | Text(String), 273 | Code(AnsiCode), 274 | } 275 | 276 | impl MessageElement { 277 | pub fn is_code(&self) -> bool { 278 | return match self { 279 | MessageElement::Code(_) => true, 280 | _ => false, 281 | }; 282 | } 283 | } 284 | 285 | pub struct StyledMessage { 286 | pub messages: Vec, 287 | } 288 | 289 | impl StyledMessage { 290 | pub fn parse(message: &str) -> Self { 291 | let mut result = Vec::::new(); 292 | 293 | let mut slice = &message[..]; 294 | 295 | 'outer: while !slice.is_empty() { 296 | let found_index = match slice 297 | .find(AnsiCode::prefix()) 298 | .or_else(|| slice.find(AnsiCode::mc_prefix())) 299 | { 300 | Some(i) => i, 301 | None => { 302 | // There aren't any codes to find 303 | result.push(MessageElement::Text(slice.to_string())); 304 | break; 305 | } 306 | }; 307 | 308 | if found_index > 0 { 309 | // Grab text that happened before the found prefix 310 | result.push(MessageElement::Text(slice[..found_index].to_string())); 311 | } 312 | 313 | let is_mc_code = slice[found_index..] 314 | .chars() 315 | .nth(0) 316 | .filter(|c| *c == AnsiCode::mc_prefix()) 317 | .is_some(); 318 | 319 | // Once we have found the next code, we need to find the end 320 | let end_index = if is_mc_code { 321 | // MC codes are only 2 bytes long 322 | found_index + 2 323 | } else { 324 | // We have to actually find the last char for ANSI codes 325 | match &slice[found_index..].find(AnsiCode::suffix()) { 326 | Some(i) => i + found_index, 327 | None => { 328 | // This doesn't seem to be a valid code...just ignore it then 329 | slice = &slice[AnsiCode::prefix().len()..]; 330 | continue; 331 | } 332 | } 333 | }; 334 | 335 | let code = &slice[found_index..=end_index]; 336 | for ansi_code in AnsiCode::iter() { 337 | if code == ansi_code.ansi_code() || code == ansi_code.mc_code() { 338 | result.push(MessageElement::Code(*ansi_code)); 339 | slice = &slice[(end_index + 1)..]; 340 | continue 'outer; 341 | } 342 | } 343 | 344 | // If we're gotten here then we didn't find the code 345 | // in this case, ignore it 346 | slice = &slice[(end_index + 1)..]; 347 | } 348 | 349 | return StyledMessage { messages: result }; 350 | } 351 | 352 | pub fn output_text(&self, y: i32, x: i32, length: i32) { 353 | let mut applied_codes = Vec::::new(); 354 | 355 | let mut index = x; 356 | 357 | for m in &self.messages { 358 | match m { 359 | MessageElement::Text(s) => { 360 | let s = s.as_str(); 361 | let to_print = min(length - index, s.len() as i32); 362 | if to_print <= 0 { 363 | continue; 364 | } 365 | mvaddstr(y, index, &s[..to_print as usize]); 366 | index += to_print; 367 | } 368 | MessageElement::Code(c) => { 369 | if *c == AnsiCode::Reset { 370 | // Undo all still enabled codes 371 | applied_codes.into_iter().rev().for_each(AnsiCode::disable); 372 | applied_codes = Vec::::new(); 373 | } else { 374 | c.enable(); 375 | applied_codes.push(*c); 376 | } 377 | } 378 | } 379 | } 380 | 381 | // Disable any still enabled codes now that we've finished parsing this line 382 | applied_codes.into_iter().rev().for_each(AnsiCode::disable); 383 | 384 | mvhline(y, index, ' ' as chtype, length - index); // clear rest of row 385 | } 386 | 387 | pub fn get_string(&self) -> String { 388 | let mut last_code: Option = None; 389 | 390 | let mut result = String::new(); 391 | 392 | for m in &self.messages { 393 | match m { 394 | MessageElement::Text(s) => { 395 | result.push_str(s); 396 | } 397 | MessageElement::Code(c) => { 398 | result.push_str(c.ansi_code()); 399 | last_code = Some(*c); 400 | } 401 | } 402 | } 403 | 404 | if let Some(last_code) = last_code { 405 | if last_code != AnsiCode::Reset { 406 | result.push_str(AnsiCode::Reset.ansi_code()); 407 | } 408 | } 409 | 410 | return result; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::util::ExitValue; 17 | use nix::sys::stat::{umask, Mode}; 18 | use nix::unistd::{close, fork, setsid, ForkResult}; 19 | use std::io::{stderr, stdin, stdout}; 20 | use std::os::unix::io::AsRawFd; 21 | 22 | pub enum Status { 23 | CONTINUE, 24 | QUIT(i32), 25 | } 26 | 27 | /// Forks the currently running process and returns either an error int (to exit with) or a `Status` 28 | /// telling the caller whether to exit or to continue. The parent process should quit, with the 29 | /// child process continuing, now as a separate daemon process. 30 | pub fn run_daemon() -> Result { 31 | // Create a new pid and execute from there 32 | match fork() { 33 | Ok(ForkResult::Parent { child }) => { 34 | // Continue in the child, we're done in the parent 35 | return Ok(Status::QUIT(child.as_raw())); 36 | } 37 | Ok(ForkResult::Child) => {} 38 | Err(_) => { 39 | eprintln!("Fork failed"); 40 | return Err(ExitValue::Code(1)); 41 | } 42 | } 43 | 44 | // At this point we are the forked process 45 | 46 | // create a new session, making this the leader of a new process group, preventing this from 47 | // becoming an orphan 48 | match setsid() { 49 | Ok(_) => {} 50 | Err(_) => { 51 | eprintln!("Failed to start a new session"); 52 | return Err(ExitValue::Code(1)); 53 | } 54 | }; 55 | 56 | umask(Mode::from_bits(0o022).unwrap()); 57 | 58 | // Close stdin, stdout, stderr; we won't be using them from here on 59 | close_fd(stdin()); 60 | close_fd(stdout()); 61 | close_fd(stderr()); 62 | 63 | return Ok(Status::CONTINUE); 64 | } 65 | 66 | fn close_fd(fd: T) { 67 | let _ = close(fd.as_raw_fd()); 68 | } 69 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::util::{find_program, find_sock_file, ExitError, ExitValue}; 17 | use clap::ArgMatches; 18 | use std::path::{Path, PathBuf}; 19 | use std::process::Command; 20 | 21 | pub fn log(sub_m: &ArgMatches) -> Result<(), ExitValue> { 22 | let sock_file = find_sock_file(sub_m)?; 23 | let log_file = find_log_file(&sock_file)?; 24 | 25 | let follow = sub_m.is_present("TAIL"); 26 | let lines = match sub_m.value_of("LINES") { 27 | Some(l) => l 28 | .parse::() 29 | .conv("Failed to parse command line argument")?, 30 | None => { 31 | eprintln!("No value provided for --lines argument"); 32 | return Err(ExitValue::Code(1)); 33 | } 34 | }; 35 | 36 | return tail(log_file, lines, follow); 37 | } 38 | 39 | pub fn find_log_file>(sock_file: P) -> Result { 40 | let sock_file = sock_file.as_ref(); 41 | return match sock_file.parent().map(|p| p.join("logs/latest.log")) { 42 | Some(f) => Ok(f), 43 | None => { 44 | eprintln!("Failed to find log file in logs/latest.log"); 45 | Err(ExitValue::Code(1)) 46 | } 47 | }; 48 | } 49 | 50 | pub fn tail>(path: P, lines: i32, follow: bool) -> Result<(), ExitValue> { 51 | let path = path.as_ref(); 52 | if !path.is_file() { 53 | eprintln!("file could not be found: {}", path.to_string_lossy()); 54 | return Err(ExitValue::Code(1)); 55 | } 56 | 57 | let tail_prog = match find_program(&[("PATH", "tail")]) { 58 | Some(t) => t, 59 | None => { 60 | eprintln!("Failed to find 'tail' program on the PATH"); 61 | return Err(ExitValue::Code(1)); 62 | } 63 | }; 64 | 65 | let line_string = lines.to_string(); 66 | let mut args = Vec::<&str>::new(); 67 | args.push("-n"); 68 | args.push(line_string.as_str()); 69 | if follow { 70 | args.push("-F"); 71 | } 72 | 73 | let result = Command::new(&tail_prog).args(args).arg(&path).spawn(); 74 | 75 | let mut child = match result { 76 | Ok(c) => c, 77 | Err(err) => { 78 | eprintln!( 79 | "Failed to tail log file {}: {}", 80 | path.to_string_lossy(), 81 | err 82 | ); 83 | return Err(ExitValue::Code(1)); 84 | } 85 | }; 86 | 87 | return match child.wait().map(|status| status.code().unwrap_or(1)) { 88 | Ok(status) => { 89 | if status == 0 { 90 | Ok(()) 91 | } else { 92 | Err(ExitValue::Code(status)) 93 | } 94 | } 95 | Err(err) => { 96 | eprintln!( 97 | "Error while tailing log file {}: {}", 98 | path.to_string_lossy(), 99 | err 100 | ); 101 | Err(ExitValue::Code(1)) 102 | } 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | #[macro_use] 17 | extern crate clap; 18 | extern crate crossbeam_channel; 19 | #[cfg(feature = "console")] 20 | extern crate ncurses; 21 | extern crate nix; 22 | extern crate serde; 23 | extern crate serde_json; 24 | extern crate shellexpand; 25 | extern crate signal_hook; 26 | extern crate sys_info; 27 | extern crate zip; 28 | 29 | mod cmd; 30 | #[cfg(feature = "console")] 31 | mod console; 32 | mod daemon; 33 | mod log; 34 | mod messages; 35 | mod messaging; 36 | mod protocol; 37 | mod restart; 38 | mod runner; 39 | mod send; 40 | mod status; 41 | mod stop; 42 | mod timings; 43 | mod util; 44 | 45 | #[cfg(feature = "console")] 46 | use crate::console::console; 47 | use crate::log::log; 48 | use crate::restart::restart; 49 | use crate::runner::{run_cmd, start}; 50 | use crate::send::send; 51 | use crate::status::status; 52 | use crate::stop::stop; 53 | use crate::timings::timings; 54 | use crate::util::ExitValue; 55 | use std::process::exit; 56 | 57 | fn main() { 58 | exit(run()); 59 | } 60 | 61 | fn run() -> i32 { 62 | let matches = cmd::get_cmd_line_matches(); 63 | 64 | let ret: Result<(), ExitValue> = match matches.subcommand() { 65 | ("status", Some(sub_m)) => status(sub_m), 66 | ("send", Some(sub_m)) => send(sub_m), 67 | ("log", Some(sub_m)) => log(sub_m), 68 | ("start", Some(sub_m)) => start(sub_m), 69 | ("run", Some(sub_m)) => run_cmd(sub_m), 70 | ("stop", Some(sub_m)) => stop(sub_m), 71 | ("restart", Some(sub_m)) => restart(sub_m), 72 | ("timings", Some(sub_m)) => timings(sub_m), 73 | #[cfg(feature = "console")] 74 | ("console", Some(sub_m)) => console(sub_m), 75 | ("completions", Some(sub_m)) => { 76 | let shell = sub_m.value_of("SHELL").unwrap(); 77 | cmd::gen_completions(shell); 78 | Ok(()) 79 | } 80 | _ => { 81 | // This shouldn't happen, clap will error if no command is provided 82 | eprint!("Unknown command"); 83 | Err(ExitValue::Code(1)) 84 | } 85 | }; 86 | 87 | return match ret { 88 | Ok(()) => 0, 89 | Err(ExitValue::Code(c)) => exit(c), 90 | Err(ExitValue::Shutdown) => exit(0), 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::protocol::ProtocolVersionMessage; 17 | use crate::restart::RestartMessage; 18 | use crate::send::SendCommandMessage; 19 | use crate::status::StatusMessage; 20 | use crate::stop::StopMessage; 21 | use crate::timings::TimingsMessage; 22 | use serde::Deserialize; 23 | #[cfg(feature = "console")] 24 | use { 25 | crate::console::ConsoleStatusMessage, crate::console::EndLogsListenerMessage, 26 | crate::console::LogsMessage, crate::console::TabCompleteMessage, 27 | }; 28 | 29 | pub trait MessageHandler { 30 | fn type_id() -> i64; 31 | } 32 | 33 | // Special error message 34 | #[derive(Deserialize)] 35 | pub struct ServerErrorMessage { 36 | #[serde(rename = "error")] 37 | pub error: Option, 38 | #[serde(rename = "shutdown")] 39 | pub is_shutdown: bool, 40 | } 41 | 42 | macro_rules! message_version { 43 | ($ver:expr, $type:ty) => { 44 | impl MessageHandler for $type { 45 | fn type_id() -> i64 { 46 | return $ver; 47 | } 48 | } 49 | }; 50 | ($ver:expr, $type:ty, console) => { 51 | #[cfg(feature = "console")] 52 | message_version!($ver, $type); 53 | }; 54 | } 55 | 56 | message_version!(0, ProtocolVersionMessage); 57 | message_version!(1, StopMessage); 58 | message_version!(2, RestartMessage); 59 | message_version!(3, StatusMessage); 60 | message_version!(4, SendCommandMessage); 61 | message_version!(5, TimingsMessage); 62 | message_version!(6, LogsMessage, console); 63 | message_version!(7, EndLogsListenerMessage, console); 64 | message_version!(8, ConsoleStatusMessage, console); 65 | message_version!(9, TabCompleteMessage, console); 66 | -------------------------------------------------------------------------------- /src/messaging.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::messages::{MessageHandler, ServerErrorMessage}; 17 | use crate::util::{ExitError, ExitValue}; 18 | use paperd_lib::{close_socket, receive_message, send_message, Message, MessageHeader, Socket}; 19 | use serde::de::DeserializeOwned; 20 | use serde::Serialize; 21 | use {nix::errno::Errno, paperd_lib::Error}; 22 | 23 | pub struct MessageSocket { 24 | sock: Socket, 25 | pub print_err: bool, 26 | } 27 | 28 | macro_rules! message_resp { 29 | ($msg:ident, $self:ident) => { 30 | match $msg { 31 | Some(m) => m, 32 | None => { 33 | if $self.print_err { 34 | eprintln!("The Paper server closed the socket"); 35 | } 36 | return Err(ExitValue::Code(1)); 37 | } 38 | } 39 | }; 40 | } 41 | 42 | impl MessageSocket { 43 | pub fn new(sock: Socket) -> Self { 44 | return MessageSocket { 45 | sock, 46 | print_err: true, 47 | }; 48 | } 49 | 50 | pub fn send_message(&self, message: &T) -> Result<(), ExitValue> 51 | where 52 | T: MessageHandler + Serialize, 53 | { 54 | let msg = match serde_json::to_string(message) { 55 | Ok(s) => s, 56 | Err(e) => { 57 | if self.print_err { 58 | eprintln!("Failed to serialize JSON: {}", e); 59 | } 60 | return Err(ExitValue::Code(1)); 61 | } 62 | }; 63 | 64 | let message = Message { 65 | header: MessageHeader { 66 | message_type: T::type_id(), 67 | message_length: msg.len() as i64, 68 | }, 69 | message_text: msg, 70 | }; 71 | 72 | let res = send_message(self.sock, &message); 73 | match res { 74 | Err(Error::Nix(nix::Error::Sys(Errno::EPIPE), _)) => { 75 | if self.print_err { 76 | eprintln!("Socket closed"); 77 | } 78 | return Err(ExitValue::Shutdown); 79 | } 80 | Err(_) => { 81 | if self.print_err { 82 | res.conv("Error attempting to send message to Paper server")?; 83 | } else { 84 | res.map_err(|_| ExitValue::Code(1))?; 85 | }; 86 | } 87 | _ => {} 88 | } 89 | 90 | return Ok(()); 91 | } 92 | 93 | pub fn receive_message(&self) -> Result { 94 | return self.receive_loop(|| true); 95 | } 96 | 97 | pub fn receive_loop(&self, keep_waiting_filter: F) -> Result 98 | where 99 | R: DeserializeOwned, 100 | F: Fn() -> bool, 101 | { 102 | let msg = loop { 103 | match receive_message(self.sock) { 104 | Ok(m) => break m, 105 | Err(Error::Nix(nix::Error::Sys(Errno::EAGAIN), _)) => { 106 | if keep_waiting_filter() { 107 | continue; 108 | } else { 109 | return Err(ExitValue::Code(1)); 110 | } 111 | } 112 | Err(Error::Nix(nix::Error::Sys(Errno::UnknownErrno), s)) => { 113 | return Err(Error::Nix(nix::Error::Sys(Errno::EAGAIN), s)) 114 | .conv(format!("Timeout occurred during the transfer of a message")); 115 | } 116 | Err(e) => { 117 | return Err(e).conv("Error attempting to receive message from Paper server") 118 | } 119 | } 120 | }; 121 | 122 | let msg = message_resp!(msg, self); 123 | return self.handle_message(&msg); 124 | } 125 | 126 | fn handle_message(&self, msg: &Message) -> Result { 127 | let msg_text = msg.message_text.as_str(); 128 | 129 | return match serde_json::from_str::(msg_text) { 130 | Ok(r) => Ok(r), 131 | Err(e) => match serde_json::from_str::(msg_text) { 132 | Ok(message) => { 133 | if message.is_shutdown { 134 | Err(ExitValue::Shutdown) 135 | } else { 136 | if self.print_err && message.error.is_some() { 137 | eprintln!("{}", message.error.unwrap()); 138 | } 139 | Err(ExitValue::Code(1)) 140 | } 141 | } 142 | Err(_) => { 143 | if self.print_err { 144 | eprintln!("Failed to parse response from server: {}", e); 145 | } 146 | Err(ExitValue::Code(1)) 147 | } 148 | }, 149 | }; 150 | } 151 | } 152 | 153 | impl Drop for MessageSocket { 154 | fn drop(&mut self) { 155 | self.print_err = false; 156 | let _ = close_socket(self.sock); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::messaging::MessageSocket; 17 | use crate::util::{ExitError, ExitValue}; 18 | use serde::{Deserialize, Serialize}; 19 | use std::fs; 20 | use std::io::Read; 21 | use std::path::Path; 22 | use zip::ZipArchive; 23 | 24 | const PROTOCOL_VERSION: i64 = 1; 25 | 26 | pub fn check_jar_protocol>(path: P) -> Result<(), ExitValue> { 27 | let jar_path = path.as_ref(); 28 | 29 | let jar_file = fs::File::open(jar_path).conv("Failed to open jar file")?; 30 | let mut jar_archive = match ZipArchive::new(jar_file) { 31 | Ok(archive) => archive, 32 | Err(e) => { 33 | eprintln!( 34 | "Failed to open jar file ({}): {}", 35 | jar_path.to_string_lossy(), 36 | e 37 | ); 38 | return Err(ExitValue::Code(1)); 39 | } 40 | }; 41 | 42 | let file_path = "META-INF/io.papermc.paper.daemon.protocol"; 43 | let mut protocol_file = match jar_archive.by_name(file_path) { 44 | Ok(file) => file, 45 | Err(_) => { 46 | eprintln!( 47 | "The specified jar file ({}) does not have a protocol version file, \ 48 | it is not compatible with paperd.", 49 | jar_path.to_string_lossy() 50 | ); 51 | return Err(ExitValue::Code(1)); 52 | } 53 | }; 54 | 55 | let mut buffer = String::new(); 56 | if let Err(e) = protocol_file.read_to_string(&mut buffer) { 57 | eprintln!( 58 | "Failed to read contents of protocol version file in {}: {}", 59 | jar_path.to_string_lossy(), 60 | e 61 | ); 62 | return Err(ExitValue::Code(1)); 63 | } 64 | 65 | return match buffer.trim().parse::() { 66 | Ok(protocol) => { 67 | if protocol != PROTOCOL_VERSION { 68 | eprintln!( 69 | "Protocol versions of paperd and jar file({}) do not match. paperd \ 70 | protocol version: {}; jar protocol version: {}. Please use a version \ 71 | of paperd compatible with this build of Paper.", 72 | jar_path.to_string_lossy(), 73 | PROTOCOL_VERSION, 74 | protocol 75 | ); 76 | Err(ExitValue::Code(1)) 77 | } else { 78 | Ok(()) 79 | } 80 | } 81 | Err(e) => { 82 | eprintln!( 83 | "Failed to read protocol version file in jar {}: {}", 84 | jar_path.to_string_lossy(), 85 | e 86 | ); 87 | Err(ExitValue::Code(1)) 88 | } 89 | }; 90 | } 91 | 92 | pub fn check_protocol(sock: &MessageSocket) -> Result<(), ExitValue> { 93 | let message = ProtocolVersionMessage {}; 94 | sock.send_message(&message)?; 95 | 96 | let res = sock.receive_message::()?; 97 | 98 | if res.protocol_version != PROTOCOL_VERSION { 99 | eprintln!( 100 | "The protocol versions of paperd and the specified server do not match. \ 101 | paperd protocol version: {}; server protocol version: {}. Please use a version \ 102 | of paperd compatible with this build of Paper.", 103 | PROTOCOL_VERSION, res.protocol_version 104 | ); 105 | return Err(ExitValue::Code(1)); 106 | } 107 | 108 | return Ok(()); 109 | } 110 | 111 | // Request 112 | #[derive(Serialize)] 113 | pub struct ProtocolVersionMessage {} 114 | 115 | // Response 116 | #[derive(Serialize, Deserialize)] 117 | struct ProtocolVersionMessageResponse { 118 | #[serde(rename = "protocolVersion")] 119 | protocol_version: i64, 120 | } 121 | -------------------------------------------------------------------------------- /src/restart.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::log::{find_log_file, tail}; 17 | use crate::protocol::check_protocol; 18 | use crate::util::{get_sock, ExitValue}; 19 | use clap::ArgMatches; 20 | use serde::Serialize; 21 | 22 | pub fn restart(sub_m: &ArgMatches) -> Result<(), ExitValue> { 23 | let (sock, sock_file) = get_sock(sub_m)?; 24 | check_protocol(&sock)?; 25 | 26 | let message = RestartMessage {}; 27 | 28 | println!("Sending restart request..."); 29 | 30 | sock.send_message(&message)?; 31 | 32 | if sub_m.is_present("TAIL") { 33 | let log_file = find_log_file(&sock_file)?; 34 | return tail(log_file, 0, true); 35 | } 36 | 37 | return Ok(()); 38 | } 39 | 40 | // Request 41 | #[derive(Serialize)] 42 | pub struct RestartMessage {} 43 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::daemon::{run_daemon, Status}; 17 | use crate::log::{find_log_file, tail}; 18 | use crate::protocol::check_jar_protocol; 19 | use crate::util::{find_program, ExitError, ExitValue}; 20 | use clap::ArgMatches; 21 | use nix::errno::Errno::ESRCH; 22 | use nix::sys::signal; 23 | use nix::sys::signal::kill; 24 | use nix::unistd::Pid; 25 | use nix::Error; 26 | use serde::Deserialize; 27 | use signal_hook::iterator::Signals; 28 | use signal_hook::{SIGABRT, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTRAP}; 29 | use std::borrow::Cow; 30 | use std::cmp::{max, min}; 31 | use std::convert::TryFrom; 32 | use std::fs::{canonicalize, File}; 33 | use std::io::{BufRead, BufReader}; 34 | use std::path::PathBuf; 35 | use std::process::{Child, Command}; 36 | use std::time::{Duration, Instant}; 37 | use std::{env, fs, process, thread}; 38 | use sys_info::mem_info; 39 | 40 | static JNI_LIB: &'static [u8] = include_bytes!(env!("PAPERD_JNI_LIB")); 41 | 42 | pub const SOCK_FILE_NAME: &'static str = "paper.sock"; 43 | pub const PID_FILE_NAME: &'static str = "paper.pid"; 44 | 45 | const STOP_EXIT_CODE: i32 = 13; 46 | const RESTART_EXIT_CODE: i32 = 27; 47 | 48 | pub fn start(sub_m: &ArgMatches) -> Result<(), ExitValue> { 49 | let env = setup_java_env(sub_m)?; 50 | 51 | check_jar_protocol(&env.jar_file)?; 52 | 53 | if !check_eula(&env)? { 54 | return run_server_foreground(&env); 55 | } 56 | 57 | let mut lib_file = std::env::temp_dir(); 58 | { 59 | lib_file.push("libpaperd_jni.so.gz"); 60 | // Find a file name that is available 61 | let mut count = 1; 62 | while lib_file.exists() { 63 | lib_file.pop(); 64 | lib_file.push(format!("libpaperd_jni.so.gz.{}", count)); 65 | count += 1; 66 | } 67 | 68 | if let Err(e) = fs::write(&lib_file, JNI_LIB) { 69 | eprintln!("Failed to write JNI library to temp directory: {}", e); 70 | return Err(ExitValue::Code(1)); 71 | } 72 | } 73 | 74 | match run_daemon() { 75 | Ok(Status::QUIT(pid)) => { 76 | println!("Server starting in background, waiting for server to start..."); 77 | 78 | let pid_file = env.working_dir.join(PID_FILE_NAME); 79 | let dur = Duration::from_secs(5); 80 | let start = Instant::now(); 81 | 82 | // wait for pid file to be created, until timeout 83 | while Instant::now().duration_since(start) < dur && !pid_file.exists() { 84 | thread::yield_now(); 85 | } 86 | 87 | return if pid_file.exists() { 88 | println!("Server started in the background. PID: {}", pid); 89 | if sub_m.is_present("TAIL") { 90 | let log_file = find_log_file(&pid_file)?; 91 | tail(log_file, 0, true) 92 | } else { 93 | Ok(()) 94 | } 95 | } else { 96 | eprintln!("Timeout while waiting for server to start."); 97 | Err(ExitValue::Code(1)) 98 | }; 99 | } 100 | Ok(Status::CONTINUE) => {} 101 | Err(err) => return Err(err), 102 | } 103 | 104 | let mut env = env; 105 | env.args 106 | .push("-Dio.papermc.daemon.enabled=true".to_string()); 107 | env.args.push(format!( 108 | "-Dio.papermc.daemon.paperd.binary={}", 109 | lib_file.to_string_lossy() 110 | )); 111 | 112 | let mut result: i32; 113 | loop { 114 | let child = start_process(&env)?; 115 | 116 | let pid = process::id(); 117 | 118 | // Write pid file 119 | let pid_file = env.working_dir.join(PID_FILE_NAME); 120 | let pid_file = pid_file.as_path(); 121 | if let Err(_) = fs::write(pid_file, pid.to_string()) { 122 | result = 1; 123 | break; 124 | } 125 | 126 | let signals = forward_signals(pid)?; 127 | 128 | result = wait_for_child(child); 129 | 130 | signals.close(); 131 | 132 | let _ = fs::remove_file(pid_file); 133 | 134 | // Check to see if we should restart from error 135 | if sub_m.is_present("KEEP_ALIVE") { 136 | if result == STOP_EXIT_CODE { 137 | break; 138 | } else { 139 | // We need to restart, it looks like the server has crashed 140 | continue; 141 | } 142 | } 143 | 144 | if result != RESTART_EXIT_CODE { 145 | break; 146 | } 147 | } 148 | 149 | if result == STOP_EXIT_CODE { 150 | // This signifies a successful exit 151 | // But being non-zero that would look like an error to most other things 152 | // So paperd won't return that 153 | result = 0; 154 | } 155 | 156 | // Attempt to cleanup a little 157 | if lib_file.exists() { 158 | if let Ok(data) = fs::read_to_string(&lib_file) { 159 | let path = PathBuf::from(data); 160 | if path.exists() { 161 | let _ = fs::remove_file(&path); 162 | } 163 | } 164 | let _ = fs::remove_file(&lib_file); 165 | } 166 | 167 | return if result == 0 { 168 | Ok(()) 169 | } else { 170 | Err(ExitValue::Code(result)) 171 | }; 172 | } 173 | 174 | fn check_eula(env: &JavaEnv) -> Result { 175 | // If this property is set then the eula is agreed by default 176 | for arg in &env.args { 177 | let arg = arg.to_ascii_lowercase(); 178 | if arg.starts_with("-dcom.mojang.eula.agree=") && arg.ends_with("true") { 179 | return Ok(true); 180 | } 181 | } 182 | 183 | let eula_path = env.working_dir.join("eula.txt"); 184 | if !eula_path.exists() { 185 | println!("eula.txt file not found, running server in foreground instead."); 186 | return Ok(false); 187 | } 188 | 189 | let eula_file = fs::File::open(&eula_path).conv("Failed to open EULA file")?; 190 | let reader = BufReader::new(&eula_file); 191 | for line in reader.lines() { 192 | let line = line.conv("Failed to read EULA file")?; 193 | if line.trim() == "eula=true" { 194 | return Ok(true); 195 | } 196 | } 197 | 198 | println!("EULA not agreed to, running server in foreground instead."); 199 | return Ok(false); 200 | } 201 | 202 | pub fn run_cmd(sub_m: &ArgMatches) -> Result<(), ExitValue> { 203 | let env = setup_java_env(sub_m)?; 204 | return run_server_foreground(&env); 205 | } 206 | 207 | fn run_server_foreground(env: &JavaEnv) -> Result<(), ExitValue> { 208 | let child = start_process(env)?; 209 | 210 | let pid = child.id(); 211 | 212 | let signals = forward_signals(pid)?; 213 | 214 | let result = wait_for_child(child); 215 | 216 | signals.close(); 217 | 218 | return Err(ExitValue::Code(result)); 219 | } 220 | 221 | struct JavaEnv { 222 | java_file: PathBuf, 223 | jar_file: PathBuf, 224 | working_dir: PathBuf, 225 | args: Vec, 226 | cmd_args: Vec, 227 | } 228 | 229 | fn start_process(env: &JavaEnv) -> Result { 230 | let result = Command::new(&env.java_file) 231 | .args(&env.args) 232 | .arg("-jar") 233 | .arg(&env.jar_file) 234 | .args(&env.cmd_args) 235 | .current_dir(&env.working_dir) 236 | .spawn(); 237 | 238 | return match result { 239 | Ok(c) => Ok(c), 240 | Err(err) => { 241 | eprintln!("Failed to start server: {}", err); 242 | Err(ExitValue::Code(1)) 243 | } 244 | }; 245 | } 246 | 247 | fn shell_context(s: &str) -> Result>, env::VarError> { 248 | match env::var(s) { 249 | Ok(value) => Ok(Some(value.into())), 250 | Err(env::VarError::NotPresent) => Ok(Some("".into())), 251 | Err(e) => Err(e), 252 | } 253 | } 254 | 255 | fn setup_java_env(sub_m: &ArgMatches) -> Result { 256 | let mut config: Option = match sub_m.value_of("CONFIG_FILE") { 257 | Some(config_path_text) => { 258 | let config_path = PathBuf::from(config_path_text); 259 | if !config_path.exists() { 260 | eprintln!("No file found at {}", config_path_text); 261 | return Err(ExitValue::Code(1)); 262 | } 263 | 264 | let config_file = match File::open(config_path) { 265 | Ok(f) => f, 266 | Err(e) => { 267 | eprintln!("Failed to open config file {}: {}", config_path_text, e); 268 | return Err(ExitValue::Code(1)); 269 | } 270 | }; 271 | 272 | match serde_json::from_reader(config_file) { 273 | Ok(c) => c, 274 | Err(e) => { 275 | eprintln!("Failed to parse config file {}: {}", config_path_text, e); 276 | return Err(ExitValue::Code(1)); 277 | } 278 | } 279 | } 280 | None => None, 281 | }; 282 | 283 | // Replace shell variables in config file values if they are present 284 | if let Some(config) = config.as_mut() { 285 | let map_func = |text: String| { 286 | shellexpand::env_with_context(text.as_str(), shell_context) 287 | .unwrap() 288 | .to_string() 289 | }; 290 | 291 | config.jar_file = config.jar_file.clone().map(map_func); 292 | config.jvm = config.jvm.clone().map(map_func); 293 | config.working_dir = config.working_dir.clone().map(map_func); 294 | config.jvm_args = config 295 | .jvm_args 296 | .clone() 297 | .map(|args| args.into_iter().map(map_func).collect()); 298 | config.server_args = config 299 | .server_args 300 | .clone() 301 | .map(|args| args.into_iter().map(map_func).collect()); 302 | } 303 | 304 | let config = config.as_ref(); 305 | 306 | // Find Java executable 307 | let java_path = config 308 | .and_then(|c| c.jvm.as_ref().map(|s| s.as_str())) 309 | .or(sub_m.value_of("JVM")) 310 | .map(PathBuf::from) 311 | .or_else(find_java); 312 | 313 | let java_path = match java_path { 314 | Some(path) => path, 315 | None => { 316 | eprintln!( 317 | "Could not find a JVM executable. Either make sure it's present on the PATH, or \ 318 | there's a valid JAVA_HOME, or specify it with -j. See --help for more details." 319 | ); 320 | return Err(ExitValue::Code(1)); 321 | } 322 | }; 323 | 324 | // Find target jar file 325 | let jar_path = match config 326 | .and_then(|c| c.jar_file.as_ref().map(|s| s.as_str())) 327 | .or(sub_m.value_of("JAR")) 328 | { 329 | Some(path) => match canonicalize(PathBuf::from(path)) { 330 | Ok(canonical) => canonical, 331 | Err(e) => { 332 | eprintln!("Failed to get full path to jar {}: {}", path, e); 333 | return Err(ExitValue::Code(1)); 334 | } 335 | }, 336 | None => { 337 | eprintln!("Failed to resolve jar file path"); 338 | return Err(ExitValue::Code(1)); 339 | } 340 | }; 341 | if !jar_path.is_file() { 342 | eprintln!("Could not find jar {}", jar_path.to_string_lossy()); 343 | return Err(ExitValue::Code(1)); 344 | } 345 | 346 | // Get the jar's parent directory 347 | let parent_path = config 348 | .and_then(|c| c.working_dir.as_ref().map(|s| s.as_str())) 349 | .or(sub_m.value_of("CWD")) 350 | .map(|s| PathBuf::from(s)) 351 | .or_else(|| jar_path.parent().map(|p| p.to_path_buf())); 352 | 353 | let parent_path = match parent_path { 354 | Some(path) => path, 355 | None => { 356 | eprintln!( 357 | "Failed to find parent directory for jar {}", 358 | jar_path.to_string_lossy() 359 | ); 360 | return Err(ExitValue::Code(1)); 361 | } 362 | }; 363 | 364 | let pid_file = parent_path.join(PID_FILE_NAME); 365 | if pid_file.is_file() { 366 | let pid = fs::read_to_string(&pid_file).conv("Failed to read PID file")?; 367 | let pid = Pid::from_raw(pid.parse::().conv("Failed to parse PID file")?); 368 | 369 | match kill(pid, None) { 370 | Ok(()) => { 371 | eprintln!( 372 | "Found server already running in this directory with PID {}, will not continue", 373 | pid 374 | ); 375 | return Err(ExitValue::Code(1)); 376 | } 377 | Err(Error::Sys(e)) => { 378 | if e == ESRCH { 379 | println!("Found stale PID file, removing"); 380 | fs::remove_file(&pid_file).conv("Failed to delete PID file")?; 381 | } else { 382 | println!("Unknown error occurred (start): {}", e); 383 | return Err(ExitValue::Code(1)); 384 | } 385 | } 386 | _ => {} 387 | } 388 | } 389 | 390 | let jvm_args = get_jvm_args(&config, sub_m)?; 391 | let server_args = sub_m 392 | .values_of("SERVER_ARGS") 393 | .map(|values| values.map(|s| s.to_string()).collect()) 394 | .unwrap_or_else(|| { 395 | config 396 | .and_then(|c| c.server_args.as_ref().map(|a| a.clone())) 397 | .unwrap_or_else(|| Vec::new()) 398 | }); 399 | 400 | return Ok(JavaEnv { 401 | java_file: java_path, 402 | jar_file: jar_path, 403 | working_dir: parent_path, 404 | args: jvm_args, 405 | cmd_args: server_args, 406 | }); 407 | } 408 | 409 | fn forward_signals(pid: u32) -> Result { 410 | // While the server is running we'll redirect some signals to it 411 | let signals = Signals::new(&[SIGHUP, SIGINT, SIGQUIT, SIGTRAP, SIGABRT, SIGTERM]); 412 | let signals = match signals { 413 | Ok(s) => s, 414 | Err(err) => { 415 | eprintln!("Failed to register signal handlers: {}", err); 416 | return Err(ExitValue::Code(1)); 417 | } 418 | }; 419 | 420 | let signals_bg = signals.clone(); 421 | thread::spawn(move || { 422 | for sig_int in signals_bg.forever() { 423 | if let Ok(sig) = signal::Signal::try_from(sig_int) { 424 | let _ = signal::kill(Pid::from_raw(pid as i32), sig); 425 | } 426 | } 427 | }); 428 | 429 | return Ok(signals); 430 | } 431 | 432 | fn wait_for_child(mut child: Child) -> i32 { 433 | return match child.wait().map(|status| status.code().unwrap_or(1)) { 434 | Ok(status) => status, 435 | Err(err) => { 436 | eprintln!("Error while running server: {}", err); 437 | 1 438 | } 439 | }; 440 | } 441 | 442 | /// Searches the PATH for java. If that fails, JAVA_HOME is searched as well. 443 | fn find_java() -> Option { 444 | return find_program(&[("PATH", "java"), ("JAVA_HOME", "bin/java")]); 445 | } 446 | 447 | fn get_jvm_args( 448 | config: &Option<&RunnerConfig>, 449 | sub_m: &ArgMatches, 450 | ) -> Result, ExitValue> { 451 | if let Some(vals) = sub_m.values_of("CUSTOM_ARGS") { 452 | return Ok(vals.map(|s| s.to_string()).collect()); 453 | } 454 | if let Some(args) = config.and_then(|c| c.jvm_args.as_ref().map(|a| a.clone())) { 455 | return Ok(args); 456 | } 457 | 458 | // When all else fails, use 500m 459 | // This should hopefully be small enough to not cause problems for anyone 460 | let mut heap: String = "500m".to_string(); 461 | 462 | if let Some(value) = sub_m.value_of("DEFAULT_ARGS") { 463 | const ERROR_MSG: &str = 464 | "Invalid format for JVM heap size. Should be something like 500m or 2G."; 465 | if value.is_empty() { 466 | eprintln!("{}", ERROR_MSG); 467 | return Err(ExitValue::Code(1)); 468 | } 469 | 470 | if value[..value.len() - 1] 471 | .chars() 472 | .any(|c| !c.is_ascii_digit()) 473 | && value.chars().last().map_or(true, |c| c != 'm' && c != 'G') 474 | { 475 | eprintln!("{}", ERROR_MSG); 476 | return Err(ExitValue::Code(1)); 477 | } 478 | 479 | heap = value.to_string(); 480 | } else { 481 | // If no arguments are provided, use 1/2 of the current available memory with default flags 482 | if let Ok(info) = mem_info() { 483 | // info.avail should always be greater than free, but it seems there may be a bug 484 | // for macOS. Assuming most users are using linux this doesn't really affect much 485 | let mem = max(info.avail, info.free); 486 | // mem is in kb, so convert to mb by dividing by 1000 487 | // Then we take half of it 488 | // Cap the amount we automatically choose at 10G 489 | let mut mb = min((mem / 1000) / 2, 10000).to_string(); 490 | 491 | println!( 492 | "Warning: No memory argument provided, automatically determining to use {} MB \ 493 | instead. This is not recommended, please specify an amount of memory with -d or \ 494 | --default-args", 495 | mb 496 | ); 497 | 498 | mb.push_str("m"); 499 | heap = mb; 500 | } 501 | } 502 | 503 | let mut xms = "-Xms".to_string(); 504 | let mut xmx = "-Xmx".to_string(); 505 | xms.push_str(heap.as_str()); 506 | xmx.push_str(heap.as_str()); 507 | 508 | return Ok(vec![ 509 | xms, 510 | xmx, 511 | "-XX:+UseG1GC".to_string(), 512 | "-XX:+UnlockExperimentalVMOptions".to_string(), 513 | "-XX:MaxGCPauseMillis=100".to_string(), 514 | "-XX:+DisableExplicitGC".to_string(), 515 | "-XX:TargetSurvivorRatio=90".to_string(), 516 | "-XX:G1NewSizePercent=50".to_string(), 517 | "-XX:G1MaxNewSizePercent=80".to_string(), 518 | "-XX:G1MixedGCLiveThresholdPercent=35".to_string(), 519 | "-XX:+AlwaysPreTouch".to_string(), 520 | "-XX:+ParallelRefProcEnabled".to_string(), 521 | "-Dusing.aikars.flags=mcflags.emc.gs".to_string(), 522 | ]); 523 | } 524 | 525 | #[derive(Deserialize)] 526 | struct RunnerConfig { 527 | #[serde(rename = "jvm")] 528 | jvm: Option, 529 | #[serde(rename = "jarFile")] 530 | jar_file: Option, 531 | #[serde(rename = "workingDir")] 532 | working_dir: Option, 533 | #[serde(rename = "jvmArgs")] 534 | jvm_args: Option>, 535 | #[serde(rename = "serverArgs")] 536 | server_args: Option>, 537 | } 538 | -------------------------------------------------------------------------------- /src/send.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::log::{find_log_file, tail}; 17 | use crate::messaging::MessageSocket; 18 | use crate::protocol::check_protocol; 19 | use crate::util::{get_sock, ExitValue}; 20 | use clap::ArgMatches; 21 | use serde::Serialize; 22 | use std::sync::{Arc, Mutex}; 23 | use std::thread::{sleep, spawn}; 24 | use std::time::Duration; 25 | 26 | pub fn send(sub_m: &ArgMatches) -> Result<(), ExitValue> { 27 | let (sock, sock_file) = get_sock(sub_m)?; 28 | check_protocol(&sock)?; 29 | 30 | let command: String = match sub_m.values_of("COMMAND") { 31 | Some(s) => s.map(|a| a.to_string()).collect::>().join(" "), 32 | None => { 33 | eprintln!("No command given."); 34 | return Err(ExitValue::Code(1)); 35 | } 36 | }; 37 | 38 | return if sub_m.is_present("TAIL") { 39 | let lock: Arc>> = Arc::new(Mutex::new(None)); 40 | let thread_lock = lock.clone(); 41 | 42 | // Start tailing in a separate thread so it won't be too late and miss the response 43 | spawn(move || { 44 | let mut exit_value = thread_lock.lock().unwrap(); 45 | let log_file = match find_log_file(&sock_file) { 46 | Ok(f) => f, 47 | Err(e) => { 48 | *exit_value = Some(e.clone()); 49 | return; 50 | } 51 | }; 52 | if let Err(e) = tail(log_file, 0, true) { 53 | *exit_value = Some(e.clone()); 54 | } 55 | }); 56 | 57 | // Wait long enough to acquire the lock 58 | sleep(Duration::from_millis(1)); 59 | 60 | send_command(&sock, command.as_str())?; 61 | 62 | // Wait for tail to complete before returning 63 | let exit_value = lock.lock().unwrap(); 64 | if let Some(e) = &*exit_value { 65 | Err(e.clone()) 66 | } else { 67 | Ok(()) 68 | } 69 | } else { 70 | Ok(()) 71 | }; 72 | } 73 | 74 | pub fn send_command(sock: &MessageSocket, cmd: &str) -> Result<(), ExitValue> { 75 | let message = SendCommandMessage { 76 | message: cmd.to_string(), 77 | }; 78 | 79 | sock.send_message(&message)?; 80 | 81 | return Ok(()); 82 | } 83 | 84 | #[derive(Serialize)] 85 | pub struct SendCommandMessage { 86 | #[serde(rename = "message")] 87 | message: String, 88 | } 89 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::protocol::check_protocol; 17 | use crate::util; 18 | use crate::util::{get_sock, ExitValue}; 19 | use clap::ArgMatches; 20 | use serde::{Deserialize, Serialize}; 21 | 22 | pub fn status(sub_m: &ArgMatches) -> Result<(), ExitValue> { 23 | let (sock, _) = get_sock(sub_m)?; 24 | check_protocol(&sock)?; 25 | 26 | let message = StatusMessage {}; 27 | 28 | sock.send_message(&message)?; 29 | 30 | let res = sock.receive_message::()?; 31 | 32 | output_status(&res); 33 | 34 | return Ok(()); 35 | } 36 | 37 | #[cfg_attr(rustfmt, rustfmt_skip)] 38 | fn output_status(status: &StatusMessageResponse) { 39 | let line_length = 60; 40 | 41 | println!("======================= Server Info ======================="); 42 | println!(" Server | {}", status.server_name); 43 | println!(" MOTD | {}", status.motd); 44 | print_players(&status.players, " Players", line_length); 45 | println!(); 46 | println!("------------------------- Version --------------------------"); 47 | println!(" PaperMC Server Version | {}", status.server_version); 48 | println!(" Bukkit API Version | {}", status.api_version); 49 | println!(); 50 | println!("------------------------ Worlds --------------------------"); 51 | 52 | for world in &status.worlds { 53 | println!("************************************************************"); 54 | println!(" Name | {}", world.name); 55 | println!(" Dimension | {}", world.dimension); 56 | println!(" Seed | {}", world.seed); 57 | println!(" Difficulty | {}", world.difficulty); 58 | print_players(&world.players, " Players", line_length); 59 | println!(" Time | {}", format_time(world.time.as_str())); 60 | println!("************************************************************"); 61 | } 62 | 63 | println!(); 64 | println!("-------------------- Server Performance --------------------"); 65 | println!(" TPS"); 66 | println!(" Past 1 Minute | {:.2}", util::tps_cap(status.tps.one_min)); 67 | println!(" Past 5 Minutes | {:.2}", util::tps_cap(status.tps.five_min)); 68 | println!(" Past 15 Minutes | {:.2}", util::tps_cap(status.tps.fifteen_min)); 69 | println!(); 70 | println!(" Memory Usage"); 71 | println!(" Memory Currently Used | {}", status.memory_usage.used_memory); 72 | println!(" Total Memory Allocated | {}", status.memory_usage.total_memory); 73 | println!(" Maximum Possible Memory | {}", status.memory_usage.max_memory); 74 | println!(); 75 | } 76 | 77 | fn print_players(players: &Vec, prefix: &str, length: usize) { 78 | let mut current_line = String::with_capacity(length); 79 | current_line.push_str(prefix); 80 | current_line.push_str(" | ("); 81 | current_line.push_str(players.len().to_string().as_str()); 82 | current_line.push_str(") "); 83 | 84 | for (i, player) in players.iter().enumerate() { 85 | if current_line.len() + player.len() + 1 > length { 86 | println!("{}", current_line); 87 | current_line = String::with_capacity(length); 88 | let indent = " ".repeat(prefix.len()); 89 | current_line.push_str(indent.as_str()); 90 | current_line.push_str(" | ") 91 | } 92 | 93 | current_line.push_str(player.as_str()); 94 | if i != players.len() - 1 { 95 | current_line.push_str(", "); 96 | } 97 | } 98 | 99 | if !current_line.ends_with(" | ") { 100 | println!("{}", current_line); 101 | } 102 | } 103 | 104 | fn format_time(time: &str) -> String { 105 | let mut res = String::with_capacity(5); 106 | res.push_str(&time[..time.len() / 2]); 107 | res.push(':'); 108 | res.push_str(&time[time.len() / 2..]); 109 | return res; 110 | } 111 | 112 | // Request 113 | #[derive(Serialize)] 114 | pub struct StatusMessage {} 115 | 116 | // Response 117 | #[derive(Deserialize)] 118 | pub(crate) struct StatusMessageResponse { 119 | #[serde(rename = "motd")] 120 | motd: String, 121 | #[serde(rename = "serverName")] 122 | server_name: String, 123 | #[serde(rename = "serverVersion")] 124 | server_version: String, 125 | #[serde(rename = "apiVersion")] 126 | api_version: String, 127 | #[serde(rename = "players")] 128 | players: Vec, 129 | #[serde(rename = "worlds")] 130 | worlds: Vec, 131 | #[serde(rename = "tps")] 132 | tps: TpsStatus, 133 | #[serde(rename = "memoryUsage")] 134 | memory_usage: MemoryStatus, 135 | } 136 | 137 | #[derive(Deserialize)] 138 | struct WorldStatus { 139 | #[serde(rename = "name")] 140 | name: String, 141 | #[serde(rename = "dimension")] 142 | dimension: String, 143 | #[serde(rename = "seed")] 144 | seed: i64, 145 | #[serde(rename = "difficulty")] 146 | difficulty: String, 147 | #[serde(rename = "players")] 148 | players: Vec, 149 | #[serde(rename = "time")] 150 | time: String, 151 | } 152 | 153 | #[derive(Deserialize)] 154 | struct TpsStatus { 155 | #[serde(rename = "oneMin")] 156 | one_min: f64, 157 | #[serde(rename = "fiveMin")] 158 | five_min: f64, 159 | #[serde(rename = "fifteenMin")] 160 | fifteen_min: f64, 161 | } 162 | 163 | #[derive(Deserialize)] 164 | struct MemoryStatus { 165 | #[serde(rename = "usedMemory")] 166 | used_memory: String, 167 | #[serde(rename = "totalMemory")] 168 | total_memory: String, 169 | #[serde(rename = "maxMemory")] 170 | max_memory: String, 171 | } 172 | -------------------------------------------------------------------------------- /src/stop.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::protocol::check_protocol; 17 | use crate::util::{find_sock_file, get_pid, get_sock_from_file, ExitValue}; 18 | use clap::ArgMatches; 19 | use nix::errno::Errno::ESRCH; 20 | use nix::sys::signal::{kill, SIGKILL}; 21 | use nix::unistd::Pid; 22 | use nix::Error; 23 | use serde::Serialize; 24 | use std::io::Write; 25 | use std::path::Path; 26 | use std::thread::sleep; 27 | use std::time::Duration; 28 | use std::{fs, io}; 29 | 30 | pub fn stop(sub_m: &ArgMatches) -> Result<(), ExitValue> { 31 | let sock_file = find_sock_file(sub_m)?; 32 | let (pid_file, pid) = get_pid(&sock_file)?; 33 | 34 | if sub_m.is_present("KILL") { 35 | force_kill(&sock_file, &pid_file, pid); 36 | println!("Server killed"); 37 | return Ok(()); 38 | } 39 | 40 | let sock = get_sock_from_file(&sock_file)?; 41 | check_protocol(&sock)?; 42 | 43 | let message = StopMessage {}; 44 | 45 | println!("Sending stop command to the server.."); 46 | sock.send_message(&message)?; 47 | 48 | print!("Waiting for server to exit."); 49 | let _ = io::stdout().flush(); 50 | // If -f is set then we need to wait to see if it fails 51 | for _ in 0..30 { 52 | if let Err(_) = kill(pid, None) { 53 | break; 54 | } 55 | sleep(Duration::from_millis(500)); 56 | print!("."); 57 | let _ = io::stdout().flush(); 58 | } 59 | println!(); 60 | 61 | if let Err(Error::Sys(e)) = kill(pid, None) { 62 | return if e == ESRCH { 63 | println!("Server exited successfully"); 64 | Ok(()) 65 | } else { 66 | println!("Unknown error occurred (stop): {}", e); 67 | Err(ExitValue::Code(1)) 68 | }; 69 | } 70 | 71 | if !sub_m.is_present("FORCE") { 72 | println!("Server failed to exit cleanly"); 73 | return Err(ExitValue::Code(1)); 74 | } 75 | 76 | println!("Server failed to exit cleanly, killing now"); 77 | force_kill(&sock_file, &pid_file, pid); 78 | println!("Server killed"); 79 | 80 | return Ok(()); 81 | } 82 | 83 | fn force_kill>(sock_file: P, pid_file: P, pid: Pid) { 84 | let _ = kill(pid, SIGKILL); 85 | let _ = fs::remove_file(&sock_file); 86 | let _ = fs::remove_file(&pid_file); 87 | } 88 | 89 | #[derive(Serialize)] 90 | pub struct StopMessage {} 91 | -------------------------------------------------------------------------------- /src/timings.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | #[cfg(feature = "console")] 17 | use crate::console::ansi; 18 | use crate::protocol::check_protocol; 19 | use crate::util::{get_sock, ExitValue}; 20 | use clap::ArgMatches; 21 | use serde::{Deserialize, Serialize}; 22 | 23 | pub fn timings(sub_m: &ArgMatches) -> Result<(), ExitValue> { 24 | let (sock, _) = get_sock(sub_m)?; 25 | check_protocol(&sock)?; 26 | 27 | let message = TimingsMessage {}; 28 | 29 | sock.send_message(&message)?; 30 | 31 | loop { 32 | let res = sock.receive_message::()?; 33 | if res.done { 34 | break; 35 | } 36 | if let Some(msg) = res.message { 37 | #[cfg(feature = "console")] 38 | println!( 39 | "{}", 40 | ansi::StyledMessage::parse(msg.as_str()).get_string() 41 | ); 42 | 43 | #[cfg(not(feature = "console"))] 44 | println!("{}", mc_colors(msg.as_str())); 45 | } 46 | } 47 | 48 | return Ok(()); 49 | } 50 | 51 | #[cfg(not(feature = "console"))] 52 | fn mc_colors(s: &str) -> String { 53 | let mut out = String::with_capacity(s.len()); 54 | let mut skip = false; 55 | for ch in s.chars() { 56 | if skip { 57 | skip = false; 58 | continue; 59 | } 60 | if ch == '§' { 61 | skip = true; 62 | continue; 63 | } 64 | out.push(ch); 65 | } 66 | return out; 67 | } 68 | 69 | // Request 70 | #[derive(Serialize)] 71 | pub struct TimingsMessage {} 72 | 73 | // Response 74 | #[derive(Serialize, Deserialize)] 75 | struct TimingsMessageResponse { 76 | #[serde(rename = "message")] 77 | message: Option, 78 | #[serde(rename = "done")] 79 | done: bool, 80 | } 81 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // This file is part of paperd, the PaperMC server daemon 2 | // Copyright (C) 2019 Kyle Wood (DemonWav) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, version 3 only. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Lesser General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Lesser General Public License 14 | // along with this program. If not, see . 15 | 16 | use crate::messaging::MessageSocket; 17 | use crate::runner; 18 | use crate::runner::PID_FILE_NAME; 19 | use clap::ArgMatches; 20 | use nix::unistd::Pid; 21 | use paperd_lib::{connect_socket, Error}; 22 | use std::num::ParseIntError; 23 | use std::path::{Path, PathBuf}; 24 | use std::{env, fs, io}; 25 | 26 | pub fn get_sock(sub_m: &ArgMatches) -> Result<(MessageSocket, PathBuf), ExitValue> { 27 | let sock_file = find_sock_file(sub_m)?; 28 | let sock = get_sock_from_file(&sock_file)?; 29 | 30 | return Ok((sock, sock_file)); 31 | } 32 | 33 | pub fn find_sock_file(sub_m: &ArgMatches) -> Result { 34 | let sock_file = sub_m 35 | .value_of("SOCK") 36 | .map(PathBuf::from) 37 | .or_else(|| env::var_os("PAPERD_SOCK").map(PathBuf::from)) 38 | .unwrap_or_else(|| PathBuf::from(runner::SOCK_FILE_NAME)); 39 | 40 | if !sock_file.exists() { 41 | eprintln!("No socket file found to send commands to"); 42 | return Err(ExitValue::Code(1)); 43 | } 44 | 45 | return Ok(sock_file); 46 | } 47 | 48 | pub fn get_sock_from_file_direct>(sock_file: P) -> Result { 49 | let sock = connect_socket(sock_file.as_ref())?; 50 | 51 | return Ok(MessageSocket::new(sock)); 52 | } 53 | 54 | pub fn get_sock_from_file>(sock_file: P) -> Result { 55 | let msg = format!( 56 | "Failed to connect to socket {}", 57 | sock_file.as_ref().display() 58 | ); 59 | return get_sock_from_file_direct(sock_file.as_ref()).conv(msg); 60 | } 61 | 62 | pub fn find_program(searches: &[(&str, &str)]) -> Option { 63 | return searches 64 | .iter() 65 | .filter_map(|(var, file)| { 66 | env::var_os(var).and_then(|paths| { 67 | env::split_paths(&paths) 68 | .filter_map(|dir| { 69 | let full_path = dir.join(file); 70 | if full_path.is_file() { 71 | Some(full_path) 72 | } else { 73 | None 74 | } 75 | }) 76 | .next() 77 | }) 78 | }) 79 | .next(); 80 | } 81 | 82 | pub fn get_pid>(sock_file: P) -> Result<(PathBuf, Pid), ExitValue> { 83 | let pid_file = match sock_file.as_ref().parent().map(|p| p.join(PID_FILE_NAME)) { 84 | Some(path) => path, 85 | None => { 86 | eprintln!("Failed to find PID file {}", PID_FILE_NAME); 87 | return Err(ExitValue::Code(1)); 88 | } 89 | }; 90 | 91 | let pid_text = fs::read_to_string(&pid_file).conv("Failed to read PID file")?; 92 | let pid = Pid::from_raw(match pid_text.parse::() { 93 | Ok(p) => p, 94 | Err(e) => { 95 | eprintln!("Failed to parse PID file: {}", e); 96 | fs::remove_file(&pid_file).conv("Failed to delete PID file")?; 97 | eprintln!("No server found to send commands to"); 98 | return Err(ExitValue::Code(1)); 99 | } 100 | }); 101 | 102 | return Ok((pid_file, pid)); 103 | } 104 | 105 | pub fn tps_cap(tps: f64) -> f64 { 106 | return tps.min(20.0); 107 | } 108 | 109 | #[derive(Clone)] 110 | pub enum ExitValue { 111 | Code(i32), 112 | Shutdown, 113 | } 114 | 115 | pub trait ExitError { 116 | fn conv>(self, context: S) -> Result; 117 | } 118 | 119 | impl ExitError for io::Result { 120 | fn conv>(self, context: S) -> Result { 121 | return self.map_err(|e| { 122 | eprintln!("{}", context.as_ref()); 123 | eprintln!(" Caused by: IO Error: {}", e); 124 | return ExitValue::Code(1); 125 | }); 126 | } 127 | } 128 | 129 | impl ExitError for Result { 130 | fn conv>(self, context: S) -> Result { 131 | return self.map_err(|e| { 132 | eprintln!("{}", context.as_ref()); 133 | eprintln!(" Caused by: Failed to parse int: {}", e); 134 | return ExitValue::Code(1); 135 | }); 136 | } 137 | } 138 | 139 | impl ExitError for Result { 140 | fn conv>(self, context: S) -> Result { 141 | return self.map_err(|e| { 142 | eprintln!("{}", context.as_ref()); 143 | eprintln!(" Caused by: Error during system call: {}", e); 144 | return ExitValue::Code(1); 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | Using paperd 2 | ============ 3 | 4 | `paperd` require Paper build # and up to work correctly. Attempting to use `paperd` with an older version of Paper 5 | will not work. 6 | 7 | The first step in using `paperd` is to read the `--help` documentation on the command itself. The documentation is 8 | pretty complete and will not be repeated here. 9 | 10 | Installation 11 | ------------ 12 | 13 | To install `paperd` simply [download the latest release from Jenkins](https://papermc.io/ci/view/all/job/paperd/) on 14 | your server: 15 | 16 | ```sh 17 | curl https://papermc.io/ci/view/all/job/paperd/lastSuccessfulBuild/artifact/paperd.tar.xz -o paperd.tar.xz 18 | ``` 19 | 20 | Unpack the `.tar.xz` file to get the `paperd` binary: 21 | 22 | ```sh 23 | tar fxv paperd.tar.xz 24 | ``` 25 | 26 | This will result in the executable `paperd` binary being extracted. Place this anywhere you like, it can be next to your 27 | `paperclip.jar` file in your server directory, or you can place it in a directory somewhere on your `PATH` if you want 28 | to use it more like a typical command. 29 | 30 | Repeat this process any time you are updating `paperd`. 31 | 32 | Installing as a systemd service 33 | ------------------------------- 34 | 35 | ### TODO 36 | 37 | General Usage 38 | ------------- 39 | 40 | This document will not go into significant detail on how to use the `paperd` tool, as stated above reading the `--help` 41 | documentation is the easiest method of learning how to use it. But here is a quick breakdown of the commands `paperd` 42 | makes available: 43 | 44 | * Commands for general server administration: 45 | * `log`: View the latest log messages, or follow the log file. 46 | * `send`: Send a command to the server. 47 | * `status`: View the current status of the server. 48 | * `timings`: Generate a Timings report and get a URL to view it. 49 | * `console`: Attach to an emulated console for the server. 50 | * Commands for running the server: 51 | * `run`: Run the server in the foreground (not as a daemon, really only useful for testing) 52 | * `start`: Start the server in the background as a daemon 53 | * `restart`: Restart the server. While in daemon mode, this is the same as the `/restart` command in-game. This is 54 | a much cleaner system than the old "restart" script system. Instead, The server fully shuts down with 55 | an exit code telling `paperd` to restart it. 56 | * `stop`: Stop the server, optionally killing it if it does not respond. 57 | --------------------------------------------------------------------------------