aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md99
-rw-r--r--example.conf33
-rwxr-xr-xmcwrapper434
-rwxr-xr-xtests/prepare.sh3
-rwxr-xr-xtests/t_codingstandard.sh2
5 files changed, 180 insertions, 391 deletions
diff --git a/README.md b/README.md
index a5eeac5..0c8ce72 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-MINECRAFT-WRAPPER
-=================
+MCSERVER-WRAPPER
+================
[![Build Status](https://travis-ci.org/Cynerd/minecraft-wrapper.svg?branch=master)](https://travis-ci.org/Cynerd/minecraft-wrapper)
-Python server wrapper for extracting informations about server status and list of
-online players.
+Minecraft server wrapper written in Python3 that extracts server status and list
+of online players.
Requires:
-----------------
@@ -10,72 +10,63 @@ Requires:
* Python3
* Dependencies for Minecraft server (Java)
-Usage
------------------
+MCWRAPPER
+---------
+### Usage
```
-mcwrapper [arguments...] IDENTIFIER
- This script is executing Minecraft server and reads its output. From output is
- extracted server status and list of online players.
+mcwrapper [-h] [--verbose] [--quiet] [--status-file] [--players-file]
+ ...
+
+This script is executing Minecraft server and reads its output. From output is
+extracted server status and list of online players. And standard input can be
+accessed by fifo file.
- arguments
- -h, --help
- Prints this help text.
- -v, --verbose
- Increase verbose level of output.
- -q, --quiet
- Decrease verbose level of output.
- --config CONFIG_FILE
- Specify configuration file to be used.
- --configfile
- prints used configuration file and exits.
+positional arguments:
+ command Command to be executed to start Minecraft server.
- IDENTIFIER
- Identifier for new server. This allows multiple servers running with this
- wrapper. Identifier is word without spaces and preferably without special
- characters.
+optional arguments:
+ -h, --help show this help message and exit
+ --verbose, -v Increase verbose level of output
+ --quiet, -q Decrease verbose level of output
+ --status-file, -s Outputs server status to file "status"
+ --players-file, -p Outputs list of online players to file "players"
```
-How it works
------------------
+### How it works
Script is reading Minercraft server standard and error output. It's looking for
-known lines that signals change of server output and players joining and leaving.
+known lines that signals change of server status and players joining and leaving.
Minecraft server output is well designed for information parsing. Informations are
-in exported to directory specified in configuration as `status`.
+exported to directory working directory or websocket server.
+
+#### Input pipe
+This is unix pipe. This file is located in working directory and named as
+`input_pipe`. This pipe is input to Minercraft server standard input. If you have
+write access rights (default 640), then you can send any command to Minecraft
+server by writing to this pipe.
+
+#### Server.pid file
+This file contains PID of Minecraft server process. This is used to detect if
+server is running when status files exists. It has probably no usage for user, but
+shouldn't be tempered with.
-###Status file
-This file is in status directory named as `status`. If it exists, it specifies in
-what status is server in the moment.
+#### Status file
+This file is in working directory and is named as `status`. If it exists, it
+specifies in what status is server in the moment.
Status can be:
+
* Starting
* Running
* Stopping
If file not exists, then server is not running at all.
-###Players file
+#### Players file
This file in in status directory and is named as `players`. If server is running,
it contains online players. Player name per line. If server isn't running, it
content don't have to be valid.
-###Input pipe
-This is unix pipe. This file is located in status directory and named as
-`input_pipe`. This pipe is input to Minercraft server standard input. If you have
-write access rights (default 640), then you can send any command to Minecraft
-server by writing to this pipe.
-
-###Server.pid file
-This file contains PID of Minecraft server process. This is used to detect if
-server is running when status files exists. It has probably no usage for user, but
-shouldn't be tempered with.
-
-Configuration
------------------
-You can use `example.conf` as base configuration. Configuration file is in fact
-Python3 script that is executed and its variables are used as configuration.
-Script is searching for configuration in these files (in order of precedence):
- * mcwrapper.conf
- * mcwrapper.conf
- * ~/.mcwrapper.conf
- * ~/.config/mcwrapper.conf
- * /etc/mcwrapper.conf
-Or you can use `--config` argument to specify any other file with valid content.
+MCWRAPPER-TERMINAL
+------------------
+This application is going to be used for interactive terminal access to minecraft
+server console. It should use latest minecraft server log as input and output will
+be pushed to input pipe of mcwrapper. This app is currently in development.
diff --git a/example.conf b/example.conf
deleted file mode 100644
index 42e4394..0000000
--- a/example.conf
+++ /dev/null
@@ -1,33 +0,0 @@
-# This is example configuration for mcwrapper
-# Use Python3 syntax to specify configuration.
-##############################################
-# vim: expandtab ft=python ts=4 sw=4 sts=4:
-
-# This is default identifier. It is used if no identifier is provided as
-# argument to mcwrapper. This is specially handy when you have only one
-# identifier to be run all the time.
-# Type: string
-identifier = 'exampleserver'
-
-# Definition of Minecraft server.
-class exampleserver:
- # Directory in which Minecraft server will be executed. It is where its
- # files will be placed
- # Type: string
- directory = 'minecraft-server'
- # Command to start Minecraft server. It is executed in directory specified
- # in option "directory".
- # Suggested is to always append "nogui" no disable graphical interface.
- # Type: string
- command = "java -jar minecraft_server.jar nogui"
- # Directory where wrapper writes files signaling status of server and
- # online players. In default it points to directory in /dev/shm. This
- # means that files are in such case stored only in ram.
- # Type: string
- statusdir = '/dev/shm/mcwrapper-exampleserver'
- # Automatic server shutdown when no player is online. This option defines
- # time in minutes before that happens. It is measured from time of last
- # player leaving server. Set this value to less or equal zero or comment
- # it to disable automatic shutdown.
- # Type: int
- #timeout = 15
diff --git a/mcwrapper b/mcwrapper
index 198299d..e65a0f5 100755
--- a/mcwrapper
+++ b/mcwrapper
@@ -5,18 +5,12 @@ import sys
import subprocess
import signal
import time
-import traceback
import atexit
+import argparse
from threading import Thread
-from threading import Timer
-import importlib.machinery as imp
###############################################################################
# Exit codes and prints helpers
-_EC_OK = 0
-_EC_ARG_UNKNOWN = 1
-_EC_ARG_MULTIPLE_CONFIG = 2
-_EC_MISSING_CONFIGURATION = 10
-_EC_SERVER_RUNNING = 11
+verbose_level = 0
def __print_message__(message, file=sys.stdout, notime=False):
@@ -28,115 +22,23 @@ def __print_message__(message, file=sys.stdout, notime=False):
def info(message, minverbose=0, notime=False):
"Prints message to stdout if minverbose >= verbose_level"
- try:
- if conf.verbose_level >= minverbose:
- __print_message__(message, notime=notime)
- except (NameError, TypeError):
+ if verbose_level >= minverbose:
__print_message__(message, notime=notime)
def warning(message, minverbose=-1, notime=False):
"Prints message to stderr if minverbose >= verbose_level"
- try:
- if conf.verbose_level >= minverbose:
- __print_message__(message, file=sys.stderr, notime=notime)
- except (NameError, TypeError):
+ if verbose_level >= minverbose:
__print_message__(message, file=sys.stderr, notime=notime)
-def error(message, minverbose=-2, ec=-1, notime=False):
+def error(message, minverbose=-2, errcode=-1, notime=False):
"Prints message to stderr if minverbose >= verbose_level"
- try:
- if conf.verbose_level >= minverbose:
- __print_message__(message, file=sys.stderr, notime=notime)
- except (NameError, TypeError):
+ if verbose_level >= minverbose:
__print_message__(message, file=sys.stderr, notime=notime)
- sys.exit(ec)
+ sys.exit(errcode)
###############################################################################
-# Load configuration
-
-__all_config_files__ = (
- 'mcwrapper.conf',
- '~/.mcwrapper.conf',
- '~/.config/mcwrapper.conf',
- '/etc/mcwrapper.conf',
- )
-
-
-def load_conf(config_file):
- """Load config_file to conf variable. Or if it has value None, search on
- default paths"""
- global conf
-
- def __set_empty_config__():
- global conf
- warning('User configuration not loaded. Using default.')
- conf = type('default config', (object,), {})
- if config_file is None:
- # Find configuration in predefined paths
- for cf in __all_config_files__:
- if os.path.isfile(os.path.expanduser(cf)):
- config_file = os.path.expanduser(cf)
- break
- if config_file is None: # If no configuration find. Set empty config
- __set_empty_config__()
- else: # else load configuration
- try:
- conf = imp.SourceFileLoader("conf", config_file).load_module()
- except Exception:
- traceback.print_exc()
- __set_empty_config__()
- # Set additional runtime configuration variables
- if 'verbose_level' not in vars(conf):
- conf.verbose_level = 0
-
-
-def __conf_check_bad_type__(config):
- error('Bad configuration type of configuration option: ' + config,
- ec=_EC_MISSING_CONFIGURATION)
-
-
-def __conf_check_missing__(config):
- error('Missing configuration option: ' + config,
- ec=_EC_MISSING_CONFIGURATION)
-
-
-def __conf_check_no_dir__(directory):
- error('No directory exists for configuration option: ' + directory,
- ec=_EC_MISSING_CONFIGURATION)
-
-
-def conf_checkserver(server):
- "Check and set configuration for server specified as agument."
- try:
- srv = vars(conf)[server]
- except KeyError:
- error("No configuration class found", ec=_EC_MISSING_CONFIGURATION)
- if 'timeout' not in vars(srv):
- srv.timeout = 0
- if isinstance(srv.timeout) != int:
- __conf_check_bad_type__('timeout')
- if 'directory' not in vars(srv):
- __conf_check_missing__('directory')
- if isinstance(srv.directory) != str:
- __conf_check_bad_type__('directory')
- srv.directory = os.path.expanduser(srv.directory)
- if not os.path.isdir(srv.directory):
- __conf_check_no_dir__('directory')
- if 'command' not in vars(srv):
- __conf_check_missing__('command')
- if isinstance(srv.command) != str:
- __conf_check_bad_type__('command')
- if 'statusdir' not in vars(srv):
- srv.statusdir = '/dev/shm/mcwrapper-' + server
- if isinstance(srv.statusdir) != str:
- __conf_check_bad_type__('statusdir')
- srv.statusdir = os.path.expanduser(srv.statusdir)
- return srv
-
-###############################################################################
-# Minecraft server
__STATUSSTRINGS__ = {
0: "Not running",
@@ -145,35 +47,25 @@ __STATUSSTRINGS__ = {
3: "Stopping",
}
+__INPUTPIPE__ = 'input_pipe'
+__STATUSFILE__ = 'status'
+__PLAYERSFILE__ = 'players'
+__PIDFILE__ = 'server.pid'
+
class MCServer:
- def __init__(self, identifier, conf):
- self.identifier = identifier
+ "Minecraft server wrapper class"
+ def __init__(self, command, statusfile=False, playersfile=False):
self.players = set()
self.status = 0
- self.conf = conf
- self.prc = None
- self.shutdownTimeout = None
- self.inputPipe = self.conf.statusdir + '/input_pipe'
- self.statusFile = self.conf.statusdir + '/status'
- self.playersFile = self.conf.statusdir + '/players'
- self.pidfile = self.conf.statusdir + '/server.pid'
- if isinstance(self.conf.command) != str:
- self.conf.command = ' '.join(self.conf.command)
+ self.process = None
+ self.command = command
+ self.statusfile = statusfile
+ self.plaersfile = playersfile
info("Server wrapper initializing")
- info("Folder: " + self.conf.directory, 1)
- info("Start command: " + self.conf.command, 1)
- try:
- os.mkdir(self.conf.statusdir)
- except FileExistsError:
- pass
- try:
- os.mkfifo(self.inputPipe, 0o640)
- except FileExistsError:
- pass
- if os.path.isfile(self.pidfile):
- with open(self.pidfile) as f:
- lpid = int(f.readline())
+ if os.path.isfile(__PIDFILE__):
+ with open(__PIDFILE__) as file:
+ lpid = int(file.readline())
try:
os.kill(lpid, 0)
except OSError:
@@ -181,122 +73,113 @@ class MCServer:
"wrapper instance.")
else:
error("Another wrapper is running with given identifier.",
- -1, _EC_SERVER_RUNNING)
- with open(self.statusFile, 'w') as f:
- f.write(__STATUSSTRINGS__[0] + '\n')
- with open(self.playersFile, 'w') as f:
+ -1, 1)
+ try:
+ os.mkfifo(__INPUTPIPE__, 0o640)
+ except FileExistsError:
pass
- self.inputThread = Thread(target=self.__input_thread__,
+ if statusfile:
+ with open(__STATUSFILE__, 'w') as file:
+ file.write(__STATUSSTRINGS__[0] + '\n')
+ if playersfile:
+ open(__PLAYERSFILE__, 'w')
+ self.inputthread = Thread(target=self.__input_thread__,
daemon=True)
- self.outpuThread = Thread(target=self.__output_thread__,
+ self.outputhread = Thread(target=self.__output_thread__,
daemon=True)
def clean(self):
+ "Cleans files generated by wrapper"
info("Server wrapper clean.")
try:
- os.remove(self.inputPipe)
+ os.remove(__INPUTPIPE__)
+ except FileNotFoundError:
+ pass
+ try:
+ os.path.isfile(__PIDFILE__)
except FileNotFoundError:
pass
try:
- os.remove(self.statusFile)
+ os.remove(__STATUSFILE__)
except FileNotFoundError:
pass
try:
- os.remove(self.playersFile)
+ os.remove(__STATUSFILE__)
except FileNotFoundError:
pass
- if os.path.isfile(self.pidfile):
- os.remove(self.pidfile)
def execstart(self):
- "Start execution of server"
+ "Start execution of Minecraft server and hold until its exits"
self.start()
- self.prc.wait()
+ self.process.wait()
def start(self):
"Start Minecraft server"
- self.prc = subprocess.Popen(
- self.conf.command, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
- start_new_session=False,
- cwd=os.path.expanduser(self.conf.directory))
- with open(self.pidfile, "w") as f:
- f.write(str(self.prc.pid))
+ self.process = subprocess.Popen(
+ self.command, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ start_new_session=False)
+ with open(__PIDFILE__, "w") as file:
+ file.write(str(self.process.pid))
self.status = 1
- with open(self.statusFile, 'w') as f:
- f.write(__STATUSSTRINGS__[1] + '\n')
- if not self.inputThread.is_alive():
- self.inputThread.start()
- if not self.outpuThread.is_alive():
- self.outpuThread.start()
+ if self.statusfile:
+ with open(__STATUSFILE__, 'w') as file:
+ file.write(__STATUSSTRINGS__[1] + '\n')
+ if not self.inputthread.is_alive():
+ self.inputthread.start()
+ if not self.outputhread.is_alive():
+ self.outputhread.start()
def stop(self):
+ "Sends /stop command to Minecraft server"
if self.running():
- self.prc.stdin.write(bytes("/stop\n", sys.getdefaultencoding()))
- self.prc.stdin.flush()
- self.__autoshutdown_disable__()
+ self.process.stdin.write(bytes(
+ "/stop\n", sys.getdefaultencoding()))
+ self.process.stdin.flush()
def running(self):
"Returns True if mc server is running. Othervise False."
- if self.status:
- return True
- else:
- return False
+ return bool(self.status)
def write_to_terminal(self, text):
"Write to server terminal. If server not running it does nothing"
if self.status == 2:
info("Input: " + text, 1)
- self.prc.stdin.write(bytes(text, sys.getdefaultencoding()))
- self.prc.stdin.flush()
+ self.process.stdin.write(bytes(text, sys.getdefaultencoding()))
+ self.process.stdin.flush()
return True
else:
return False
- def __autoshutdown_enable__(self):
- if self.conf.timeout > 0:
- info("Automatic shutdown after " + str(self.conf.timeout) +
- " min.")
- self.shutdownTimeout = Timer(self.conf.timeout * 60.0, self.stop)
- self.shutdownTimeout.start()
-
- def __autoshutdown_disable__(self):
- try:
- self.shutdownTimeout.cancel()
- del self.shutdownTimeout
- info("Automatic shutdown disabled.")
- except AttributeError:
- pass
-
def __user_join__(self, username):
info("User '" + username + "' joined server.")
self.players.add(username)
- with open(self.playersFile, 'a') as f:
- f.write(username + '\n')
- self.__autoshutdown_disable__()
+ if self.plaersfile:
+ with open(__PLAYERSFILE__, 'a') as file:
+ file.write(username + '\n')
def __user_leave__(self, username):
info("User '" + username + "' left server.")
self.players.remove(username)
- with open(self.playersFile, 'w') as f:
- f.writelines(self.players)
- if self.players:
- f.write('\n')
- if not self.players:
- self.__autoshutdown_enable__()
+ if self.plaersfile:
+ with open(__PLAYERSFILE__, 'w') as file:
+ file.writelines(self.players)
+ if self.players:
+ file.write('\n')
def __parse_line__(self, line):
if ': Done' in line:
info("Server start.")
self.status = 2
- with open(self.statusFile, 'w') as f:
- f.write(__STATUSSTRINGS__[2] + '\n')
- self.__autoshutdown_enable__()
+ if self.statusfile:
+ with open(__STATUSFILE__, 'w') as file:
+ file.write(__STATUSSTRINGS__[2] + '\n')
elif ': Stopping the server' in line:
info("Server stop.")
self.status = 3
- with open(self.statusFile, 'w') as f:
- f.write(__STATUSSTRINGS__[3] + '\n')
+ if self.statusfile:
+ with open(__STATUSFILE__, 'w') as file:
+ file.write(__STATUSSTRINGS__[3] + '\n')
elif 'logged in with entity id' in line:
name = line[len('[00:00:00] [Server thread/INFO]: '):]
name = name[:name.index('[')]
@@ -307,132 +190,83 @@ class MCServer:
self.__user_leave__(name)
def __output_thread__(self):
- for linen in self.prc.stdout:
+ for linen in self.process.stdout:
line = linen.decode(sys.getdefaultencoding())
info(line.rstrip(), 2, notime=True)
self.__parse_line__(line.rstrip())
- with open(self.statusFile, 'w') as f:
- f.write(__STATUSSTRINGS__[0] + '\n')
+ if self.statusfile:
+ with open(__STATUSFILE__, 'w') as file:
+ file.write(__STATUSSTRINGS__[0] + '\n')
def __input_thread__(self):
- with open(self.inputPipe, 'r') as p:
+ with open(__INPUTPIPE__, 'r') as pipe:
while True:
- ln = p.readline().rstrip()
- if ln:
- self.write_to_terminal(ln + "\n")
+ line = pipe.readline().rstrip()
+ if line:
+ self.write_to_terminal(line + "\n")
else:
time.sleep(3)
###############################################################################
+mcserver = None
-def wrapper_atexit():
+def __wrapper_atexit__():
"This is called when wrapper is exiting"
- _mcserver.clean()
+ mcserver.clean()
-def wrapper_toexit():
+def __wrapper_toexit__():
"This function is called when system signalizes that mcwrapper should exit"
- _mcserver.stop()
+ mcserver.stop()
def __signal_term__(_signo, _stack_frame):
- wrapper_toexit()
-
-
-def print_help():
- print('mcwrapper [arguments...] IDENTIFIER')
- print(' This script is executing Minecraft server and reads its output.')
- print(' From output isextracted server status and list of online')
- print(' players.')
- print('')
- print(' arguments')
- print(' -h, --help')
- print(' Prints this help text.')
- print(' -v, --verbose')
- print(' Increase verbose level of output.')
- print(' -q, --quiet')
- print(' Decrease verbose level of output.')
- print(' --config CONFIG_FILE')
- print(' Specify configuration file to be used.')
- print(' --configfile')
- print(' prints used configuration file and exits.')
- print(' IDENTIFIER')
- print(' Identifier for new server. This allows multiple servers')
- print(' running with this wrapper. Identifier is word without')
- print(' spaces and preferably without special characters.')
- sys.exit(_EC_OK)
-
-
-def print_conffile():
- if '__file__' in vars(conf):
- print(conf.__file__)
- else:
- print("No configuration file used.")
- sys.exit(_EC_OK)
-
-
-if __name__ == '__main__':
- identifier = None
- use_config = None
- verbose_level = 0
- print_config_file = False
- i = 1
- while i < len(sys.argv):
- arg = sys.argv[i]
- i += 1
- if arg[0] == '-':
- if len(arg) > 2 and arg[1] == '-':
- if arg == '--help':
- print_help()
- elif arg == '--verbose':
- verbose_level += 1
- elif arg == '--quiet':
- verbose_level += 1
- elif arg == '--config':
- if use_config is not None:
- error('Config option is used multiple times',
- ec=_EC_ARG_MULTIPLE_CONFIG)
- else:
- use_config = sys.argv[i]
- i += 1
- elif arg == '--configfile':
- print_config_file = True
- continue
- else:
- for l in arg[1:]:
- if l == 'h':
- print_help()
- elif l == 'v':
- verbose_level += 1
- elif l == 'q':
- verbose_level -= 1
- else:
- error("Unknown short argument " + l,
- ec=_EC_ARG_UNKNOWN)
- continue
- if identifier is None:
- identifier = arg
- continue
- error("Unknown argument: " + arg, ec=_EC_ARG_UNKNOWN)
- # Parsing args ends
-
- load_conf(use_config)
-
- if print_config_file:
- print_conffile()
-
- conf.verbose_level += verbose_level
- # Set identifier if provided
- if identifier:
- conf.identifier = identifier
- elif "identifier" not in vars(conf):
- print_help()
-
- server_conf = conf_checkserver(conf.identifier)
- _mcserver = MCServer(conf.identifier, server_conf)
+ __wrapper_toexit__()
+
+
+__HELP_DESC__ = """
+ This script is executing Minecraft server and reads its output. From output
+ is extracted server status and list of online players. And standard input
+ can be accessed by fifo file.
+ """
+
+
+def main():
+ "Main function"
+ global verbose_level
+ parser = argparse.ArgumentParser(description=__HELP_DESC__)
+ parser.add_argument('--verbose', '-v', action='count', default=0,
+ help="Increase verbose level of output")
+ parser.add_argument('--quiet', '-q', action='count', default=0,
+ help="Decrease verbose level of output")
+ parser.add_argument('--status-file', '-s', action='store_true',
+ help="Outputs server status to file \"status\"")
+ parser.add_argument('--players-file', '-p', action='store_true',
+ help="""Outputs list of online players to file
+ \"players\" """)
+ parser.add_argument('command', nargs=argparse.REMAINDER,
+ help="""Command to be executed to start Minecraft
+ server.""")
+ args = parser.parse_args()
+
+ verbose_level = args.verbose - args.quiet
+ command = args.command
+ sfile = args.status_file
+ pfile = args.players_file
+
+ if not command:
+ parser.print_help()
+ if 'nogui' not in command:
+ command.append('nogui')
+
+ global mcserver
+ mcserver = MCServer(command, pfile, sfile)
signal.signal(signal.SIGTERM, __signal_term__)
signal.signal(signal.SIGINT, __signal_term__)
- atexit.register(wrapper_atexit)
+ atexit.register(__wrapper_atexit__)
+
+ mcserver.execstart()
- _mcserver.execstart()
+if __name__ == '__main__':
+ main()
diff --git a/tests/prepare.sh b/tests/prepare.sh
index 89982c3..17e466d 100755
--- a/tests/prepare.sh
+++ b/tests/prepare.sh
@@ -4,9 +4,6 @@ if [[ "$(basename -- "$0")" = "prepare.sh" ]]; then
exit 1
fi
-# Write basic configuration
-cp ../example.conf mcwrapper.conf
-
if [[ $PREPARED != "y" ]]; then
# Move to known directory
cd "$( readlink -f "${BASH_SOURCE[0]}" )"
diff --git a/tests/t_codingstandard.sh b/tests/t_codingstandard.sh
index 4daba17..1b472b4 100755
--- a/tests/t_codingstandard.sh
+++ b/tests/t_codingstandard.sh
@@ -21,4 +21,4 @@ fi
# This test is not part of standard check because of errors caused by dynamic variable
# loading to configuration. But it should be run from time to time to found other mistakes
#echo "Checking bugs and poor quality"
-#pylint --msg-template="{line}: [{msg_id}({symbol}), {obj}] {msg}" --reports=n ../mcwrapper
+#pylint --reports=n ../mcwrapper