def __init__(self, wrapper): self.wrapper = wrapper self.api = API(wrapper, "Web", internal=True) self.log = logging.getLogger('Web') self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.socket = False self.data = Storage("web") if "keys" not in self.data.Data: self.data.Data["keys"] = [] # if not self.config["Web"]["web-password"] == None: # self.log.info("Changing web-mode password because web-password was changed in wrapper.properties") # ***** change code to hashlib if this gets uncommented # self.data.Data["password"] = md5.md5(self.config["Web"]["web-password"]).hexdigest() # self.config["Web"]["web-password"] = None # self.wrapper.configManager.save() self.api.registerEvent("server.consoleMessage", self.onServerConsole) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.login", self.onPlayerJoin) self.api.registerEvent("player.logout", self.onPlayerLeave) self.api.registerEvent("irc.message", self.onChannelMessage) self.consoleScrollback = [] self.chatScrollback = [] self.memoryGraph = [] self.loginAttempts = 0 self.lastAttempt = 0 self.disableLogins = 0
def __init__(self, wrapper): self.api = API(wrapper, "Scripts", internal=True) self.wrapper = wrapper self.config = wrapper.config # Register the events self.api.registerEvent("server.start", self._startserver) self.api.registerEvent("server.stopped", self._stopserver) self.api.registerEvent("wrapper.backupBegin", self._backupbegin) self.api.registerEvent("wrapper.backupEnd", self._backupend) self.createdefaultscripts()
def __init__(self, wrapper): self.wrapper = wrapper self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.log = wrapper.log self.api = API(wrapper, "Backups", internal=True) self.interval = 0 self.backup_interval = self.config["Backups"]["backup-interval"] self.time = time.time() self.backups = [] self.enabled = self.config["Backups"]["enabled"] # allow plugins to shutdown backups via api self.timerstarted = False if self.enabled and self.dotarchecks(): # only register event if used and tar installed! self.api.registerEvent("timer.second", self.eachsecond) self.timerstarted = True self.log.debug("Backups Enabled..")
def __init__(self, mcserver, log, wrapper): self.socket = False self.state = False self.javaserver = mcserver self.config = wrapper.config self.configmgr = wrapper.configManager self.wrapper = wrapper self.pass_handler = self.wrapper.cipher self.address = self.config["IRC"]["server"] self.port = self.config["IRC"]["port"] self.nickname = self.config["IRC"]["nick"] self.originalNickname = self.nickname[0:] self.nickAttempts = 0 self.channels = self.config["IRC"]["channels"] self.encoding = self.config["General"]["encoding"] self.log = log self.timeout = False self.ready = False self.msgQueue = [] self.authorized = {} self.api = API(self.wrapper, "IRC", internal=True) self.api.registerEvent("irc.message", self.onchannelmessage) self.api.registerEvent("irc.action", self.onchannelaction) self.api.registerEvent("irc.join", self.onchanneljoin) self.api.registerEvent("irc.part", self.onchannelpart) self.api.registerEvent("irc.quit", self.onchannelquit) self.api.registerEvent("server.starting", self.onServerStarting) self.api.registerEvent("server.started", self.onServerStarted) self.api.registerEvent("server.stopping", self.onServerStopping) self.api.registerEvent("server.stopped", self.onServerStopped) self.api.registerEvent("player.login", self.onPlayerLogin) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.action", self.onPlayerAction) self.api.registerEvent("player.logout", self.onPlayerLogout) self.api.registerEvent("player.achievement", self.onPlayerAchievement) self.api.registerEvent("player.death", self.onPlayerDeath) self.api.registerEvent("wrapper.backupBegin", self.onBackupBegin) self.api.registerEvent("wrapper.backupEnd", self.onBackupEnd) self.api.registerEvent("wrapper.backupFailure", self.onBackupFailure) self.api.registerEvent("server.say", self.onPlayerSay)
def __init__(self, wrapper): self.wrapper = wrapper self.api = API(wrapper, "Web", internal=True) self.log = logging.getLogger('Web') self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.socket = False self.data = Storage("web") self.pass_handler = self.wrapper.cipher if "keys" not in self.data.Data: self.data.Data["keys"] = [] self.api.registerEvent("server.consoleMessage", self.onServerConsole) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.login", self.onPlayerJoin) self.api.registerEvent("player.logout", self.onPlayerLeave) self.api.registerEvent("irc.message", self.onChannelMessage) self.consoleScrollback = [] self.chatScrollback = [] self.memoryGraph = [] self.loginAttempts = 0 self.lastAttempt = 0 self.disableLogins = 0
def __init__(self, wrapper): self.wrapper = wrapper self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.log = wrapper.log self.api = API(wrapper, "Backups", internal=True) # self.wrapper.backups.idle self.idle = True self.inprogress = False self.backup_interval = self.config["Backups"]["backup-interval"] self.timer = time.time() self.backups = [] # allow plugins to shutdown backups via api self.enabled = self.config["Backups"]["enabled"] self._start()
class Scripts(object): def __init__(self, wrapper): self.api = API(wrapper, "Scripts", internal=True) self.wrapper = wrapper self.config = wrapper.config # Register the events self.api.registerEvent("server.start", self._startserver) self.api.registerEvent("server.stopped", self._stopserver) self.api.registerEvent("wrapper.backupBegin", self._backupbegin) self.api.registerEvent("wrapper.backupEnd", self._backupend) self.createdefaultscripts() def createdefaultscripts(self): if not os.path.exists("wrapper-data"): mkdir_p("wrapper-data") if not os.path.exists("wrapper-data/scripts"): mkdir_p("wrapper-data/scripts") for script in scripts: path = "wrapper-data/scripts/%s" % script if not os.path.exists(path): with open(path, "w") as f: f.write(scripts[script]) os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) # Events def _startserver(self, payload): os.system("wrapper-data/scripts/server-start.sh") def _stopserver(self, payload): os.system("wrapper-data/scripts/server-stop.sh") def _backupbegin(self, payload): os.system("wrapper-data/scripts/backup-begin.sh %s" % payload["file"]) def _backupend(self, payload): os.system("wrapper-data/scripts/backup-finish.sh %s" % payload["file"])
class Wrapper(object): def __init__(self, secret_passphrase): # setup log and config # needs a false setting on first in case config does not # load (like after changes). self.storage = False self.log = logging.getLogger('Wrapper.py') self.configManager = Config() self.configManager.loadconfig() self.config = self.configManager.config # Read Config items # hard coded cursor for non-readline mode self.cursor = ">" # This was to allow alternate encodings self.encoding = self.config["General"]["encoding"] self.serverpath = self.config["General"]["server-directory"] self.proxymode = self.config["Proxy"]["proxy-enabled"] self.wrapper_onlinemode = self.config["Proxy"]["online-mode"] self.halt_message = self.config["Misc"]["halt-message"] # encryption items (for passwords and sensitive user data) # salt is generated and stored in wrapper.properties.json config_changes = False salt = self.config["General"]["salt"] if not salt: salt = gensalt(self.encoding) self.config["General"]["salt"] = salt config_changes = True # passphrase is provided at startup by the wrapper operator or script (not stored) passphrase = phrase_to_url_safebytes(secret_passphrase, self.encoding, salt) self.cipher = Crypt(passphrase, self.encoding) # Update passwords (hash any plaintext passwords) for groups in self.config: for cfg_items in self.config[groups]: if cfg_items[-10:] == "-plaintext": # i.e., cfg_items ===> like ["web-password-plaintext"] hash_item = cfg_items[:-10] # hash_item ===> i.e., ["web-password"] if hash_item in self.config[groups] and self.config[ groups][cfg_items]: # encrypt contents of (i.e.) ["web-password-plaintext"] hashed_item = self.cipher.encrypt( self.config[groups][cfg_items]) # store in "" ["Web"]["web-password"] self.config[groups][hash_item] = hashed_item # set plaintext item to false (successful digest) self.config[groups][cfg_items] = False config_changes = True # Patch any old update paths "..wrapper/development/build/version.json" # new paths are: "..wrapper/development" for entries in self.config["Updates"]: if "/build/version.json" in str(self.config["Updates"][entries]): oldentry = copy.copy(self.config["Updates"][entries]) self.config["Updates"][entries] = oldentry.split( "/build/version.json")[0] config_changes = True # save changes made to config file if config_changes: self.configManager.save() # reload branch update info. self.auto_update_wrapper = self.config["Updates"][ "auto-update-wrapper"] self.auto_update_branch = self.config["Updates"]["auto-update-branch"] if not self.auto_update_branch: self.update_url = "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/development" else: self.update_url = self.config["Updates"][self.auto_update_branch] self.use_timer_tick_event = self.config["Gameplay"][ "use-timer-tick-event"] self.use_readline = not (self.config["Misc"]["use-betterconsole"]) # Storages self.wrapper_storage = Storage("wrapper", encoding=self.encoding) self.wrapper_permissions = Storage("permissions", encoding=self.encoding, pickle=False) self.wrapper_usercache = Storage("usercache", encoding=self.encoding, pickle=False) # storage Data objects self.storage = self.wrapper_storage.Data self.usercache = self.wrapper_usercache.Data # self.wrapper_permissions accessed only by permissions module # core functions and datasets self.perms = Permissions(self) self.uuids = UUIDS(self.log, self.usercache) self.plugins = Plugins(self) self.commands = Commands(self) self.events = Events(self) self.players = {} self.registered_permissions = {} self.help = {} self.input_buff = "" self.sig_int = False self.command_hist = ['/help', 'help'] self.command_index = 1 # init items that are set up later (or opted out of/ not set up.) self.javaserver = None self.api = None self.irc = None self.scripts = None self.web = None self.proxy = None self.backups = None # HaltSig - Why? ... because if self.halt was just `False`, passing # a self.halt would simply be passing `False` (immutable). Changing # the value of self.halt would not necessarily change the value of the # passed parameter (unless it was specifically referenced back as # `wrapper.halt`). Since the halt signal needs to be passed, possibly # several layers deep, and into modules that it may be desireable to # not have direct access to wrapper, using a HaltSig object is # more desireable and reliable in behavior. self.halt = HaltSig() self.updated = False # future plan to expose this to api self.xplayer = ConsolePlayer(self) # Error messages for non-standard import failures. if not readline and self.use_readline: self.log.warning( "'readline' not imported. This is needed for proper" " console functioning. Press <Enter> to acknowledge...") sys.stdin.readline() # requests is just being used in too many places to try # and track its usages piece-meal. if not requests: self.log.error( "You must have the requests module installed to use wrapper!" " console functioning. Press <Enter> to Exit...") sys.stdin.readline() self._halt() # create server/proxy vitals and config objects self.servervitals = ServerVitals(self.players) # LETS TAKE A SECOND TO DISCUSS PLAYER OBJECTS: # The ServerVitals class gets passed the player object list now, but # player objects are now housed in wrapper. This is how we are # passing information between proxy and wrapper. self.servervitals.serverpath = self.config["General"][ "server-directory"] self.servervitals.state = OFF self.servervitals.command_prefix = self.config["Misc"][ "command-prefix"] self.proxyconfig = ProxyConfig() self.proxyconfig.proxy = self.config["Proxy"] self.proxyconfig.entity = self.config["Entities"] def __del__(self): """prevent error message on very first wrapper starts when wrapper exits after creating new wrapper.properties file. """ if self.storage: self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() def start(self): """wrapper execution starts here""" self.signals() self.backups = Backups(self) self.api = API(self, "Wrapper.py") self._registerwrappershelp() # This is not the actual server... the MCServer # class is a console wherein the server is started self.javaserver = MCServer(self, self.servervitals) self.javaserver.init() # load plugins self.plugins.loadplugins() if self.config["IRC"]["irc-enabled"]: # this should be a plugin self.irc = IRC(self.javaserver, self.log, self) t = threading.Thread(target=self.irc.init, args=()) t.daemon = True t.start() if self.config["Web"]["web-enabled"]: # this should be a plugin if manageweb.pkg_resources and manageweb.requests: self.log.warning( "Our apologies! Web mode is currently broken. Wrapper" " will start web mode anyway, but it will not likely " "function well (or at all). For now, you should turn " "off web mode in wrapper.properties.json.") self.web = manageweb.Web(self) t = threading.Thread(target=self.web.wrap, args=()) t.daemon = True t.start() else: self.log.error( "Web remote could not be started because you do not have" " the required modules installed: pkg_resources\n" "Hint: http://stackoverflow.com/questions/7446187") # Console Daemon runs while not wrapper.halt.halt consoledaemon = threading.Thread(target=self.parseconsoleinput, args=()) consoledaemon.daemon = True consoledaemon.start() # Timer runs while not wrapper.halt.halt ts = threading.Thread(target=self.event_timer_second, args=()) ts.daemon = True ts.start() if self.use_timer_tick_event: # Timer runs while not wrapper.halt.halt tt = threading.Thread(target=self.event_timer_tick, args=()) tt.daemon = True tt.start() if self.config["General"]["shell-scripts"]: if os.name in ("posix", "mac"): self.scripts = Scripts(self) else: self.log.error( "Sorry, but shell scripts only work on *NIX-based systems!" " If you are using a *NIX-based system, please file a " "bug report.") if self.proxymode: t = threading.Thread(target=self._startproxy, args=()) t.daemon = True t.start() if self.auto_update_wrapper: t = threading.Thread(target=self._auto_update_process, args=()) t.daemon = True t.start() self.javaserver.handle_server() # handle_server always runs, even if the actual server is not started self.plugins.disableplugins() self.log.info("Plugins disabled") self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() self.log.info("Wrapper Storages closed and saved.") # wrapper execution ends here. handle_server ends when # wrapper.halt.halt is True. if self.sig_int: self.log.info("Ending threads, please wait...") time.sleep(5) os.system("reset") def signals(self): signal.signal(signal.SIGINT, self.sigint) signal.signal(signal.SIGTERM, self.sigterm) # noinspection PyBroadException try: # lacking in Windows signal.signal(signal.SIGTSTP, self.sigtstp) except: pass def sigint(*args): self = args[0] # We are only interested in the self component self.log.info("Wrapper.py received SIGINT; halting...\n") self.sig_int = True self._halt() def sigterm(*args): self = args[0] # We are only interested in the self component self.log.info("Wrapper.py received SIGTERM; halting...\n") self._halt() def sigtstp(*args): # We are only interested in the 'self' component self = args[0] self.log.info("Wrapper.py received SIGTSTP; NO sleep support!" " Wrapper halting...\n") os.system("kill -CONT %d" % self.javaserver.proc.pid) self._halt() def _halt(self): self.javaserver.stop(self.halt_message, restart_the_server=False) self.halt.halt = True def shutdown(self): self._halt() def write_stdout(self, message="", source="print"): """ :param message: desired output line. Default is wrapper. :param source: "server", "wrapper", "print" or "log". Default is print. """ cursor = self.cursor if self.use_readline: print(message) return def _wrapper(msg): """_wrapper is normally displaying a live typing buffer. Therefore, there is no cr/lf at end because it is constantly being re-printed in the same spot as the user types.""" if msg != "": # re-print what the console user was typing right below that. # /wrapper commands receive special magenta coloring if msg[0:1] == '/': print("{0}{1}{2}{3}{4}{5}".format(UP_LINE, cursor, FG_YELLOW, msg, RESET, CLEAR_EOL)) else: print("{0}{1}{2}{3}".format(BACKSPACE, cursor, msg, CLEAR_EOL)) def _server(msg): # print server lines print("{0}{1}{2}\r\n".format(UP_LINE, CLEAR_LINE, msg, CLEAR_EOL)) def _print(msg): _server(msg) parse = { "server": _server, "wrapper": _wrapper, "print": _print, } # if this fails due to key error, we WANT that raised, as it is # a program code error, not a run-time error. parse[source](message) def getconsoleinput(self): """If wrapper is NOT using readline (self.use_readline == False), then getconsoleinput manually implements our own character reading, parsing, arrow keys, command history, etc. This is desireable because it allows the user to continue to see their input and modify it, even if the server is producing console line messages that would normally "carry away" the user's typing. Implemented in response to issue 326: 'Command being typed gets carried off by console every time server generates output #326' by @Darkness3840: https://github.com/benbaptist/minecraft-wrapper/issues/326 """ if self.use_readline: # Obtain a line of console input try: consoleinput = sys.stdin.readline().strip() except Exception as e: self.log.error( "[continue] variable 'consoleinput' in 'console()' did" " not evaluate \n%s" % e) consoleinput = "" else: arrow_index = 0 # working buffer allows arrow use to restore what they # were typing but did not enter as a command yet working_buff = '' while not self.halt.halt: keypress = readkey.getcharacter() keycode = readkey.convertchar(keypress) length = len(self.input_buff) if keycode == "right": arrow_index += 1 if arrow_index > length: arrow_index = length if keycode == "left": arrow_index -= 1 if arrow_index < 1: arrow_index = 0 if keycode == "up": # goes 'back' in command history time self.command_index -= 1 if self.command_index < 1: self.command_index = 0 self.input_buff = self.command_hist[self.command_index] arrow_index = len(self.input_buff) if keycode == "down": # goes forward in command history time self.command_index += 1 if self.command_index + 1 > len(self.command_hist): # These actions happen when at most recent typing self.command_index = len(self.command_hist) self.input_buff = '%s' % working_buff self.write_stdout("%s " % self.input_buff, source="wrapper") arrow_index = len(self.input_buff) continue self.input_buff = self.command_hist[self.command_index] arrow_index = len(self.input_buff) buff_left = "%s" % self.input_buff[:arrow_index] buff_right = "%s" % self.input_buff[arrow_index:] if keycode == "backspace": if len(buff_left) > 0: buff_left = buff_left[:-1] self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff arrow_index -= 1 if keycode == "delete": if len(buff_right) > 0: buff_right = buff_right[1:] self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff if keycode in ("enter", "cr", "lf"): # scroll up (because cr is not added to buffer) # print("") break if keycode in ("ctrl-c", "ctrl-x"): self.sigterm() break # hide special key codes like PAGE_UP, etc if not used if not keycode: buff_left = "%s%s" % (buff_left, keypress) self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff arrow_index += 1 # with open('readout.txt', "w") as f: # f.write("left: '%s'\nright: '%s'\nbuff: '%s'" % ( # buff_left, buff_right, self.input_buff)) if len(buff_right) > 0: self.write_stdout( "{0}{1}{2}{3}".format(REVERSED, buff_left, RESET, buff_right), "wrapper") else: self.write_stdout("%s " % self.input_buff, source="wrapper") consoleinput = "%s" % self.input_buff self.input_buff = "" if consoleinput in self.command_hist: # if the command is already in the history somewhere, # remove it and re-append to the end (most recent) self.command_hist.remove(consoleinput) self.command_hist.append(consoleinput) else: # or just add it. self.command_hist.append(consoleinput) self.command_index = len(self.command_hist) # print the finished command to console self.write_stdout("%s\r\n" % self.input_buff, source="wrapper") return consoleinput def parseconsoleinput(self): while not self.halt.halt: consoleinput = self.getconsoleinput() # No command (perhaps just a line feed or spaces?) if len(consoleinput) < 1: continue # for use with runwrapperconsolecommand() command wholecommandline = consoleinput[0:].split(" ") command = str(getargs(wholecommandline, 0)).lower() # this can be passed to runwrapperconsolecommand() command for args allargs = wholecommandline[1:] # Console only commands (not accessible in-game) if command in ("/halt", "halt"): self._halt() elif command in ("/stop", "stop"): self.javaserver.stop_server_command() # "kill" (with no slash) is a server command. elif command == "/kill": self.javaserver.kill("Server killed at Console...") elif command in ("/start", "start"): self.javaserver.start() elif command in ("/restart", "restart"): self.javaserver.restart() elif command in ("/update-wrapper", "update-wrapper"): self._checkforupdate(True) # "plugins" command (with no slash) reserved for server commands elif command == "/plugins": self.listplugins() elif command in ("/mem", "/memory", "mem", "memory"): self._memory() elif command in ("/raw", "raw"): self._raw(consoleinput) elif command in ("/freeze", "freeze"): self._freeze() elif command in ("/unfreeze", "unfreeze"): self._unfreeze() elif command == "/version": readout("/version", self.getbuildstring(), usereadline=self.use_readline) elif command in ("/mute", "/pause", "/cm", "/m", "/p"): self._mute_console(allargs) # Commands that share the commands.py in-game interface # "reload" (with no slash) may be used by bukkit servers elif command == "/reload": self.runwrapperconsolecommand("reload", []) # proxy mode ban system elif self.proxymode and command == "/ban": self.runwrapperconsolecommand("ban", allargs) elif self.proxymode and command == "/ban-ip": self.runwrapperconsolecommand("ban-ip", allargs) elif self.proxymode and command == "/pardon-ip": self.runwrapperconsolecommand("pardon-ip", allargs) elif self.proxymode and command == "/pardon": self.runwrapperconsolecommand("pardon", allargs) elif command in ("/perm", "/perms", "/super", "/permissions", "perm", "perms", "super", "permissions"): self.runwrapperconsolecommand("perms", allargs) elif command in ("/playerstats", "/stats", "playerstats", "stats"): self.runwrapperconsolecommand("playerstats", allargs) elif command in ("/ent", "/entity", "/entities", "ent", "entity", "entities"): self.runwrapperconsolecommand("ent", allargs) elif command in ("/config", "/con", "/prop", "/property", "/properties"): self.runwrapperconsolecommand("config", allargs) elif command in ("op", "/op"): self.runwrapperconsolecommand("op", allargs) elif command in ("deop", "/deop"): self.runwrapperconsolecommand("deop", allargs) elif command in ("pass", "/pass", "pw", "/pw", "password", "/password"): self.runwrapperconsolecommand("password", allargs) # TODO Add more commands below here, below the original items: # TODO __________________ # more commands here... # TODO __________________ # TODO add more commands above here, above the help-related items: # minecraft help command elif command == "help": readout("/help", "Get wrapper.py help.", separator=" (with a slash) - ", usereadline=self.use_readline) self.javaserver.console(consoleinput) # wrapper's help (console version) elif command == "/help": self._show_help_console() # wrapper ban help elif command == "/bans": self._show_help_bans() # Commmand not recognized by wrapper else: try: self.javaserver.console(consoleinput) except Exception as e: self.log.error("[BREAK] Console input exception" " (nothing passed to server) \n%s" % e) break continue def _registerwrappershelp(self): # All commands listed herein are accessible in-game # Also require player.isOp() new_usage = "<player> [-s SUPER-OP] [-o OFFLINE] [-l <level>]" self.api.registerHelp( "Wrapper", "Internal Wrapper.py commands ", [ ("/wrapper [update/memory/halt]", "If no subcommand is provided, it will" " show the Wrapper version.", None), ("/playerstats [all]", "Show the most active players. If no subcommand" " is provided, it'll show the top 10 players.", None), ("/plugins", "Show a list of the installed plugins", None), ("/reload", "Reload all plugins.", None), ("/op %s" % new_usage, "This and deop are Wrapper commands.", None), ("/permissions <groups/users/RESET>", "Command used to manage permission groups and" " users, add permission nodes, etc.", None), ("/entity <count/kill> [eid] [count]", "/entity help/? for more help.. ", None), ("/config", "Change wrapper.properties (type" " /config help for more..)", None), ("/password", "Sample usage: /pw IRC control-irc-pass <new" "password>", None), # Minimum server version for commands to appear is # 1.7.6 (registers perm later in serverconnection.py) # These won't appear if proxy mode is not on (since # serverconnection is part of proxy). ("/ban <name> [reason..] [d:<days>/h:<hours>]", "Ban a player. Specifying h:<hours> or d:<days>" " creates a temp ban.", "mc1.7.6"), ("/ban-ip <ip> [<reason..> <d:<number of days>]", "- Ban an IP address. Reason and days" " (d:) are optional.", "mc1.7.6"), ("/pardon <player> [False]", " - pardon a player. Default is byuuidonly." " To unban a specific " "name (without checking uuid), use `pardon" " <player> False`", "mc1.7.6"), ("/pardon-ip <address>", "Pardon an IP address.", "mc1.7.6"), ("/banlist [players|ips] [searchtext]", "search and display the banlist (warning -" " displays on single page!)", "mc1.7.6") ]) def runwrapperconsolecommand(self, wrappercommand, argslist): xpayload = { 'player': self.xplayer, 'command': wrappercommand, 'args': argslist } self.commands.playercommand(xpayload) def isonlinemode(self): """ :returns: Whether the server OR (for proxy mode) wrapper is in online mode. This should normally 'always' render True. Under rare circumstances it could be false, such as when this wrapper and its server are the target for a wrapper lobby with player.connect(). """ if self.proxymode: # if wrapper is using proxy mode (which should be set to online) return self.config["Proxy"]["online-mode"] if self.javaserver is not None: if self.servervitals.onlineMode: # if local server is online-mode return True return False def listplugins(self): readout("", "List of Wrapper.py plugins installed:", separator="", pad=4, usereadline=self.use_readline) for plid in self.plugins: plugin = self.plugins[plid] if plugin["good"]: name = plugin["name"] summary = plugin["summary"] if summary is None: summary = "No description available for this plugin" version = plugin["version"] readout(name, summary, separator=(" - v%s - " % ".".join([str(_) for _ in version])), usereadline=self.use_readline) else: readout("failed to load plugin", plugin, pad=25, usereadline=self.use_readline) def _start_emailer(self): alerts = self.config["Alerts"]["enabled"] if alerts: self.config["Alerts"] = "alerts true" def _startproxy(self): # error will raise if requests or cryptography is missing. self.proxy = Proxy(self.halt, self.proxyconfig, self.servervitals, self.log, self.usercache, self.events) # wait for server to start timer = 0 while self.servervitals.state < STARTED: timer += 1 time.sleep(.1) if timer > 1200: self.log.warning( "Proxy mode did not detect a started server within 2" " minutes. Disabling proxy mode because something is" " wrong.") self.disable_proxymode() return if self.proxy.proxy_port == self.servervitals.server_port: self.log.warning("Proxy mode cannot start because the wrapper" " port is identical to the server port.") self.disable_proxymode() return proxythread = threading.Thread(target=self.proxy.host, args=()) proxythread.daemon = True proxythread.start() def disable_proxymode(self): self.proxymode = False self.config["Proxy"]["proxy-enabled"] = False self.configManager.save() self.log.warning( "\nProxy mode is now turned off in wrapper.properties.json.\n") @staticmethod def getbuildstring(): if core_buildinfo_version.__branch__ == "dev": return "%s (development build #%d)" % ( core_buildinfo_version.__version__, core_buildinfo_version.__build__) elif core_buildinfo_version.__branch__ == "stable": return "%s (stable)" % core_buildinfo_version.__version__ else: return "Version: %s (%s build #%d)" % ( core_buildinfo_version.__version__, core_buildinfo_version.__branch__, core_buildinfo_version.__build__) def _auto_update_process(self): while not self.halt.halt: time.sleep(3600) if self.updated: self.log.info("An update for wrapper has been loaded," " Please restart wrapper.") else: self._checkforupdate() def _checkforupdate(self, update_now=False): """ checks for update """ self.log.info("Checking for new builds...") update = self.get_wrapper_update_info() if update: version, build, repotype, reponame = update self.log.info( "New Wrapper.py %s build #%d is available!" " (current build is #%d)", repotype, build, core_buildinfo_version.__build__) if self.auto_update_wrapper or update_now: self.log.info("Updating...") self.performupdate(version, build) else: self.log.info( "Because you have 'auto-update-wrapper' set to False," " you must manually update Wrapper.py. To update" " Wrapper.py manually, please type /update-wrapper.") else: self.log.info("No new versions available.") def get_wrapper_update_info(self, repotype=None): """get the applicable branch wrapper update""" # read the installed branch info if repotype is None: repotype = core_buildinfo_version.__branch__ if self.auto_update_branch: branch_key = self.auto_update_branch else: branch_key = "%s-branch" % repotype r = requests.get("%s/build/version.json" % self.config["Updates"][branch_key]) if r.status_code == 200: data = r.json() if data["__build__"] > core_buildinfo_version.__build__: if repotype == "dev": reponame = "development" elif repotype == "stable": reponame = "master" else: reponame = data["__branch__"] if "__version__" not in data: data["__version__"] = data["version"] return data["__version__"], data["__build__"], data[ "__branch__"], reponame else: self.log.warning( "Failed to check for updates - are you connected to the" " internet? (Status Code %d)", r.status_code) return False def performupdate(self, version, build): """ Perform update; returns True if update succeeds. User must still restart wrapper manually. :param version: first argument from get_wrapper_update_info() :param build: second argument from get_wrapper_update_info() :return: True if update succeeds """ wraphash = requests.get("%s/build/Wrapper.py.md5" % self.update_url) wrapperfile = requests.get("%s/Wrapper.py" % self.update_url) if wraphash.status_code == 200 and wrapperfile.status_code == 200: self.log.info("Verifying Wrapper.py...") if hashlib.md5(wrapperfile.content).hexdigest() == wraphash.text: self.log.info( "Update file successfully verified. Installing...") with open(sys.argv[0], "wb") as f: f.write(wrapperfile.content) self.log.info( "Wrapper.py %s (#%d) installed. Please reboot Wrapper.py.", ".".join([str(_) for _ in version]), build) self.updated = True return True else: return False else: self.log.error( "Failed to update due to an internal error (%d, %d)", wraphash.status_code, wrapperfile.status_code, exc_info=True) return False def event_timer_second(self): while not self.halt.halt: time.sleep(1) self.events.callevent("timer.second", None) """ eventdoc <group> wrapper <group> <description> a timer that is called each second. <description> <abortable> No <abortable> """ def event_timer_tick(self): while not self.halt.halt: self.events.callevent("timer.tick", None) time.sleep(0.05) """ eventdoc <group> wrapper <group> <description> a timer that is called each 1/20th <sp> of a second, like a minecraft tick. <description> <abortable> No <abortable> <comments> Use of this timer is not suggested and is turned off <sp> by default in the wrapper.config.json file <comments> """ def _pause_console(self, pause_time): if not self.javaserver: readout("ERROR - ", "There is no running server instance to mute.", separator="", pad=10, usereadline=self.use_readline) return self.javaserver.server_muted = True readout("Server is now muted for %d seconds." % pause_time, "", separator="", command_text_fg="yellow", usereadline=self.use_readline) time.sleep(pause_time) readout("Server now unmuted.", "", separator="", usereadline=self.use_readline) self.javaserver.server_muted = False for lines in self.javaserver.queued_lines: readout("Q\\", "", lines, pad=3, usereadline=self.use_readline) time.sleep(.1) self.javaserver.queued_lines = [] def _mute_console(self, all_args): pausetime = 30 if len(all_args) > 0: pausetime = get_int(all_args[0]) # spur off a pause thread cm = threading.Thread(target=self._pause_console, args=(pausetime, )) cm.daemon = True cm.start() def _freeze(self): try: self.javaserver.freeze() except OSError as ex: self.log.error(ex) except EnvironmentError as e: self.log.warning(e) except Exception as exc: self.log.exception( "Something went wrong when trying to freeze the" " server! (%s)", exc) def _memory(self): try: get_bytes = self.javaserver.getmemoryusage() except OSError as e: self.log.error(e) except Exception as ex: self.log.exception( "Something went wrong when trying to fetch" " memory usage! (%s)", ex) else: amount, units = format_bytes(get_bytes) self.log.info("Server Memory Usage: %s %s (%s bytes)" % (amount, units, get_bytes)) def _raw(self, console_input): try: if len(getargsafter(console_input[1:].split(" "), 1)) > 0: self.javaserver.console( getargsafter(console_input[1:].split(" "), 1)) else: self.log.info("Usage: /raw [command]") except EnvironmentError as e: self.log.warning(e) def _unfreeze(self): try: self.javaserver.unfreeze() except OSError as ex: self.log.error(ex) except EnvironmentError as e: self.log.warning(e) except Exception as exc: self.log.exception( "Something went wrong when trying to unfreeze" " the server! (%s)", exc) def _show_help_console(self): # This is the console help command display. readout("", "Get Minecraft help.", separator="help (no slash) - ", pad=0, usereadline=self.use_readline) readout("/reload", "Reload Wrapper.py plugins.", usereadline=self.use_readline) readout("/plugins", "Lists Wrapper.py plugins.", usereadline=self.use_readline) readout("/update-wrapper", "Checks for new Wrapper.py updates, and will install\n" "them automatically if one is available.", usereadline=self.use_readline) readout("/stop", "Stop the minecraft server without" " auto-restarting and without\n" " shuttingdown Wrapper.py.", usereadline=self.use_readline) readout("/start", "Start the minecraft server.", usereadline=self.use_readline) readout("/restart", "Restarts the minecraft server.", usereadline=self.use_readline) readout("/halt", "Shutdown Wrapper.py completely.", usereadline=self.use_readline) readout("/cm [seconds]", "Mute server output (Wrapper console" " logging still happens)", usereadline=self.use_readline) readout("/kill", "Force kill the server without saving.", usereadline=self.use_readline) readout("/freeze", "Temporarily locks the server up" " until /unfreeze is executed\n" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/unfreeze", "Unlocks a frozen state server" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/mem", "Get memory usage of the server" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/raw [command]", "Send command to the Minecraft" " Server. Useful for Forge\n" " commands like '/fml confirm'.", usereadline=self.use_readline) readout("/password", "run `/password help` for more...)", usereadline=self.use_readline) readout("/perms", "/perms for more...)", usereadline=self.use_readline) readout("/config", "Change wrapper.properties (type" " /config help for more..)", usereadline=self.use_readline) readout("/version", self.getbuildstring(), usereadline=self.use_readline) readout("/entity", "Work with entities (run /entity for more...)", usereadline=self.use_readline) readout("/bans", "Display the ban help page.", usereadline=self.use_readline) def _show_help_bans(self): # ban commands help. if not self.proxymode: readout("ERROR - ", "Wrapper proxy-mode bans are not enabled " "(proxy mode is not on).", separator="", pad=10, usereadline=self.use_readline) return readout("", "Bans - To use the server's versions, do not type a slash.", separator="", pad=5, usereadline=self.use_readline) readout("", "", separator="-----1.7.6 and later ban commands-----", pad=10, usereadline=self.use_readline) readout("/ban", " - Ban a player. Specifying h:<hours> or d:<days>" " creates a temp ban.", separator="<name> [reason..] [d:<days>/h:<hours>] ", pad=12, usereadline=self.use_readline) readout("/ban-ip", " - Ban an IP address. Reason and days (d:) are optional.", separator="<ip> [<reason..> <d:<number of days>] ", pad=12, usereadline=self.use_readline) readout("/pardon", " - pardon a player. Default is byuuidonly. To unban a" "specific name (without checking uuid), use" " `pardon <player> False`", separator="<player> [byuuidonly(true/false)]", pad=12, usereadline=self.use_readline) readout("/pardon-ip", " - Pardon an IP address.", separator="<address> ", pad=12, usereadline=self.use_readline) readout("/banlist", " - search and display the banlist (warning -" " displays on single page!)", separator="[players|ips] [searchtext] ", pad=12, usereadline=self.use_readline)
def start(self): """wrapper execution starts here""" self.signals() self.backups = Backups(self) self.api = API(self, "Wrapper.py") self._registerwrappershelp() # This is not the actual server... the MCServer # class is a console wherein the server is started self.javaserver = MCServer(self, self.servervitals) self.javaserver.init() # load plugins self.plugins.loadplugins() if self.config["IRC"]["irc-enabled"]: # this should be a plugin self.irc = IRC(self.javaserver, self.log, self) t = threading.Thread(target=self.irc.init, args=()) t.daemon = True t.start() if self.config["Web"]["web-enabled"]: # this should be a plugin if manageweb.pkg_resources and manageweb.requests: self.log.warning( "Our apologies! Web mode is currently broken. Wrapper" " will start web mode anyway, but it will not likely " "function well (or at all). For now, you should turn " "off web mode in wrapper.properties.json.") self.web = manageweb.Web(self) t = threading.Thread(target=self.web.wrap, args=()) t.daemon = True t.start() else: self.log.error( "Web remote could not be started because you do not have" " the required modules installed: pkg_resources\n" "Hint: http://stackoverflow.com/questions/7446187") # Console Daemon runs while not wrapper.halt.halt consoledaemon = threading.Thread(target=self.parseconsoleinput, args=()) consoledaemon.daemon = True consoledaemon.start() # Timer runs while not wrapper.halt.halt ts = threading.Thread(target=self.event_timer_second, args=()) ts.daemon = True ts.start() if self.use_timer_tick_event: # Timer runs while not wrapper.halt.halt tt = threading.Thread(target=self.event_timer_tick, args=()) tt.daemon = True tt.start() if self.config["General"]["shell-scripts"]: if os.name in ("posix", "mac"): self.scripts = Scripts(self) else: self.log.error( "Sorry, but shell scripts only work on *NIX-based systems!" " If you are using a *NIX-based system, please file a " "bug report.") if self.proxymode: t = threading.Thread(target=self._startproxy, args=()) t.daemon = True t.start() if self.auto_update_wrapper: t = threading.Thread(target=self._auto_update_process, args=()) t.daemon = True t.start() self.javaserver.handle_server() # handle_server always runs, even if the actual server is not started self.plugins.disableplugins() self.log.info("Plugins disabled") self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() self.log.info("Wrapper Storages closed and saved.") # wrapper execution ends here. handle_server ends when # wrapper.halt.halt is True. if self.sig_int: self.log.info("Ending threads, please wait...") time.sleep(5) os.system("reset")
class IRC(object): def __init__(self, mcserver, log, wrapper): self.socket = False self.state = False self.javaserver = mcserver self.config = wrapper.config self.configmgr = wrapper.configManager self.wrapper = wrapper self.pass_handler = self.wrapper.cipher self.address = self.config["IRC"]["server"] self.port = self.config["IRC"]["port"] self.nickname = self.config["IRC"]["nick"] self.originalNickname = self.nickname[0:] self.nickAttempts = 0 self.channels = self.config["IRC"]["channels"] self.encoding = self.config["General"]["encoding"] self.log = log self.timeout = False self.ready = False self.msgQueue = [] self.authorized = {} self.api = API(self.wrapper, "IRC", internal=True) self.api.registerEvent("irc.message", self.onchannelmessage) self.api.registerEvent("irc.action", self.onchannelaction) self.api.registerEvent("irc.join", self.onchanneljoin) self.api.registerEvent("irc.part", self.onchannelpart) self.api.registerEvent("irc.quit", self.onchannelquit) self.api.registerEvent("server.starting", self.onServerStarting) self.api.registerEvent("server.started", self.onServerStarted) self.api.registerEvent("server.stopping", self.onServerStopping) self.api.registerEvent("server.stopped", self.onServerStopped) self.api.registerEvent("player.login", self.onPlayerLogin) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.action", self.onPlayerAction) self.api.registerEvent("player.logout", self.onPlayerLogout) self.api.registerEvent("player.achievement", self.onPlayerAchievement) self.api.registerEvent("player.death", self.onPlayerDeath) self.api.registerEvent("wrapper.backupBegin", self.onBackupBegin) self.api.registerEvent("wrapper.backupEnd", self.onBackupEnd) self.api.registerEvent("wrapper.backupFailure", self.onBackupFailure) self.api.registerEvent("server.say", self.onPlayerSay) def init(self): while not self.wrapper.haltsig.halt: try: self.log.info("Connecting to IRC...") self.connect() t = threading.Thread(target=self.queue, args=()) t.daemon = True t.start() self.handle() except Exception as e: self.log.exception(e) self.disconnect("Error in Wrapper.py - restarting") self.log.info("Disconnected from IRC") time.sleep(5) def connect(self): self.nickname = self.originalNickname[0:] self.socket = socket.socket() self.socket.connect((self.address, self.port)) self.socket.setblocking(120) self.auth() def auth(self): if self.config["IRC"]["password"]: plain_password = self.pass_handler.decrypt(self.config["IRC"]["password"]) if plain_password: self.send("PASS %s" % plain_password) else: # fall back if password did not decrypt successfully self.send("PASS %s" % self.config["IRC"]["password"]) self.send("NICK %s" % self.nickname) self.send("USER %s 0 * :%s" % (self.nickname, self.nickname)) def disconnect(self, message): try: self.send("QUIT :%s" % message) self.socket.close() self.socket = False except Exception as e: self.log.debug("Exception in IRC disconnect: \n%s", e) def send(self, payload): pay = py_bytes("%s\n" % payload, self.encoding) if self.socket: self.socket.send(pay) else: return False # Event Handlers def messagefromchannel(self, channel, message): if self.config["IRC"]["show-channel-server"]: self.javaserver.broadcast("&6[%s] %s" % (channel, message)) else: self.javaserver.broadcast(message) def onchanneljoin(self, payload): channel, nick = payload["channel"], payload["nick"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rjoined the channel" % nick) def onchannelpart(self, payload): channel, nick = payload["channel"], payload["nick"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rparted the channel" % nick) def onchannelmessage(self, payload): channel, nick, message = payload["channel"], payload["nick"], payload["message"] final = "" for i, chunk in enumerate(message.split(" ")): if not i == 0: final += " " try: if chunk[0:7] in ("http://", "https://"): final += "&b&n&@%s&@&r" % chunk else: final += chunk except Exception as e: self.log.debug("Exception in IRC onchannelmessage: \n%s", e) final += chunk self.messagefromchannel(channel, "&a<%s> &r%s" % (nick, final)) def onchannelaction(self, payload): channel, nick, action = payload["channel"], payload["nick"], payload["action"] self.messagefromchannel(channel, "&a* %s &r%s" % (nick, action)) def onchannelquit(self, payload): channel, nick, message = payload["channel"], payload["nick"], payload["message"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rquit: %s" % (nick, message)) def onPlayerLogin(self, payload): player = self.filterName(payload["player"]) self.msgQueue.append("[%s connected]" % player) def onPlayerLogout(self, payload): player = payload["player"] self.msgQueue.append("[%s disconnected]" % player) def onPlayerMessage(self, payload): player = self.filterName(payload["player"]) message = payload["message"] self.msgQueue.append("<%s> %s" % (player, message)) def onPlayerAction(self, payload): player = self.filterName(payload["player"]) action = payload["action"] self.msgQueue.append("* %s %s" % (player, action)) def onPlayerSay(self, payload): player = self.filterName(payload["player"]) message = payload["message"] self.msgQueue.append("[%s] %s" % (player, message)) def onPlayerAchievement(self, payload): player = self.filterName(payload["player"]) achievement = payload["achievement"] self.msgQueue.append("%s has just earned the achievement %s" % (player, achievement)) def onPlayerDeath(self, payload): player = self.filterName(payload["player"]) death = payload["death"] self.msgQueue.append("%s %s" % (player, death)) def onBackupBegin(self, payload): self.msgQueue.append("Backing up... lag may occur!") def onBackupEnd(self, payload): time.sleep(1) self.msgQueue.append("Backup complete!") def onBackupFailure(self, payload): if "reasonText" in payload: self.msgQueue.append("ERROR: %s" % payload["reasonText"]) else: self.msgQueue.append("An unknown error occurred while trying to backup.") def onServerStarting(self, payload): self.msgQueue.append("Server starting...") def onServerStarted(self, payload): self.msgQueue.append("Server started!") def onServerStopping(self, payload): self.msgQueue.append("Server stopping...") def onServerStopped(self, payload): self.msgQueue.append("Server stopped!") def handle(self): while self.socket: try: irc_buffer = self.socket.recv(1024) if irc_buffer == b"": self.log.error("Disconnected from IRC") self.socket = False self.ready = False break except socket.timeout: if self.timeout: self.socket = False break else: self.send("PING :%s" % str(random.randint())) self.timeout = True irc_buffer = "" except Exception as e: self.log.debug("Exception in IRC handle: \n%s", e) irc_buffer = "" for line in irc_buffer.split(b"\n"): self.parse(line) def queue(self): while self.socket: if not self.ready: time.sleep(0.1) continue for i, message in enumerate(self.msgQueue): for channel in self.channels: if len(message) > 400: for l in xrange(int(math.ceil(len(message) / 400.0))): chunk = message[l * 400:(l + 1) * 400] self.send("PRIVMSG %s :%s" % (channel, chunk)) else: self.send("PRIVMSG %s :%s" % (channel, message)) del self.msgQueue[i] self.msgQueue = [] time.sleep(0.1) def filterName(self, name): if self.config["IRC"]["obstruct-nicknames"]: return "_" + str(name)[1:] else: return name def rawConsole(self, payload): self.javaserver.console(payload) def console(self, channel, payload): if self.config["IRC"]["show-channel-server"]: self.rawConsole({"text": "[%s] " % channel, "color": "gold", "extra": payload}) else: self.rawConsole({"extra": payload}) def parse(self, dataline): _line = py_str(dataline, self.encoding) if getargs(_line.split(" "), 1) == "001": for command in self.config["IRC"]["autorun-irc-commands"]: self.send(command) for channel in self.channels: self.send("JOIN %s" % channel) self.ready = True self.log.info("Connected to IRC!") self.state = True self.nickAttempts = 0 if getargs(_line.split(" "), 1) == "433": self.log.info("Nickname '%s' already in use.", self.nickname) self.nickAttempts += 1 if self.nickAttempts > 2: name = bytearray(self.nickname) for i in xrange(3): name[len(self.nickname) / 3 * i] = chr(random.randrange(97, 122)) self.nickname = str(name) else: self.nickname += "_" self.auth() self.log.info("Attemping to use nickname '%s'.", self.nickname) if getargs(_line.split(" "), 1) == "JOIN": nick = getargs(_line.split(" "), 0)[1:getargs(_line.split(" "), 0).find("!")] channel = getargs(_line.split(" "), 2)[1:][:-1] self.log.info("%s joined %s", nick, channel) self.wrapper.events.callevent("irc.join", {"nick": nick, "channel": channel}, abortable=False) if getargs(_line.split(" "), 1) == "PART": nick = getargs(_line.split(" "), 0)[1:getargs(_line.split(" "), 0).find("!")] channel = getargs(_line.split(" "), 2) self.log.info("%s parted %s", nick, channel) self.wrapper.events.callevent("irc.part", {"nick": nick, "channel": channel}, abortable=False) if getargs(_line.split(" "), 1) == "MODE": try: nick = getargs(_line.split(" "), 0)[1:getargs(_line.split(" "), 0).find('!')] channel = getargs(_line.split(" "), 2) modes = getargs(_line.split(" "), 3) user = getargs(_line.split(" "), 4)[:-1] self.console(channel, [{ "text": user, "color": "green" }, { "text": " received modes %s from %s" % (modes, nick), "color": "white" }]) except Exception as e: self.log.debug("Exception in IRC in parse (MODE): \n%s", e) pass if getargs(_line.split(" "), 0) == "PING": self.send("PONG %s" % getargs(_line.split(" "), 1)) if getargs(_line.split(" "), 1) == "QUIT": nick = getargs(_line.split(" "), 0)[1:getargs(_line.split(" "), 0).find("!")] message = getargsafter(_line.split(" "), 2)[1:].strip("\n").strip("\r") self.wrapper.events.callevent("irc.quit", {"nick": nick, "message": message, "channel": None}, abortable=False) if getargs(_line.split(" "), 1) == "PRIVMSG": channel = getargs(_line.split(" "), 2) nick = getargs(_line.split(" "), 0)[1:getargs(_line.split(" "), 0).find("!")] message = getargsafter(_line.split(" "), 3)[1:].strip("\n").strip("\r") if channel[0] == "#": if message.strip() == ".players": users = "" for user in self.javaserver.players: users += "%s " % user self.send("PRIVMSG %s :There are currently %s users on the server: %s" % (channel, len(self.javaserver.players), users)) elif message.strip() == ".about": self.send("PRIVMSG %s :Wrapper.py Version %s" % (channel, self.wrapper.getbuildstring())) else: if not PY3: message = message.decode(self.encoding, "ignore") # TODO - not sure if this part is going to work in PY3 # now that message is a properly encoded string, not a b"" sequence if getargs(message.split(" "), 0) == "\x01ACTION": self.wrapper.events.callevent("irc.action", {"nick": nick, "channel": channel, "action": getargsafter(message.split(" "), 1)[:-1]}, abortable = False ) self.log.info("[%s] * %s %s", channel, nick, getargsafter(message.split(" "), 1)[:-1]) else: self.wrapper.events.callevent("irc.message", {"nick": nick, "channel": channel, "message": message}, abortable=False ) self.log.info("[%s] <%s> %s", channel, nick, message) elif self.config["IRC"]["control-from-irc"]: self.log.info("[PRIVATE] (%s) %s", nick, message) def msg(string): self.log.info("[PRIVATE] (%s) %s", self.nickname, string) self.send("PRIVMSG %s :%s" % (nick, string)) if self.config["IRC"]["control-irc-pass"] == "password": msg("A new password is required in wrapper.properties. Please change it.") if "password" in self.config["IRC"]["control-irc-pass"]: msg("The password is not secure. You must use the console to enter a password.") return if nick in self.authorized: if int(time.time()) - self.authorized[nick] < 900: if getargs(message.split(" "), 0) == 'hi': msg('Hey there!') elif getargs(message.split(" "), 0) == 'help': # eventually I need to make help only one or two # lines, to prevent getting kicked/banned for spam msg("run [command] - run command on server") msg("togglebackups - temporarily turn backups on or off. this setting is not permanent " "and will be lost on restart") msg("halt - shutdown server and Wrapper.py, will not auto-restart") msg("kill - force server restart without clean shutdown - only use when server " "is unresponsive") msg("start/restart/stop - start the server/automatically stop and start server/stop " "the server without shutting down Wrapper") msg("status - show status of the server") msg("check-update - check for new Wrapper.py updates, but don't install them") msg("update-wrapper - check and install new Wrapper.py updates") msg("Wrapper.py Version %s by benbaptist" % self.wrapper.getbuildstring()) # msg('console - toggle console output to this private message') elif getargs(message.split(" "), 0) == 'togglebackups': self.config["Backups"]["enabled"] = not self.config["Backups"]["enabled"] if self.config["Backups"]["enabled"]: msg('Backups are now on.') else: msg('Backups are now off.') self.configmgr.save() # 'config' is just the json dictionary of items, not the Config class elif getargs(message.split(" "), 0) == 'run': if getargs(message.split(" "), 1) == '': msg('Usage: run [command]') else: command = " ".join(message.split(' ')[1:]) self.javaserver.console(command) elif getargs(message.split(" "), 0) == 'halt': msg("Halting wrapper... Bye.") self.wrapper.shutdown() elif getargs(message.split(" "), 0) == 'restart': msg("restarting from IRC remote") self.log.info("Restarting server from IRC remote") self.javaserver.restart() elif getargs(message.split(" "), 0) == 'stop': msg("Stopping from IRC remote") self.log.info("Stopped from IRC remote") self.javaserver.stop_server_command() elif getargs(message.split(" "), 0) == 'start': self.javaserver.start() msg("Server starting") elif getargs(message.split(" "), 0) == 'kill': self.javaserver.kill("Killing server from IRC remote") msg("Server terminated.") elif getargs(message.split(" "), 0) == 'status': if self.javaserver.state == 2: msg("Server is running.") elif self.javaserver.state == 1: msg("Server is currently starting/frozen.") elif self.javaserver.state == 0: msg("Server is stopped. Type 'start' to fire it back up.") elif self.javaserver.state == 3: msg("Server is in the process of shutting down/restarting.") else: msg("Server is in unknown state. This is probably a Wrapper.py bug - report it! " "(state #%d)" % self.javaserver.state) if self.wrapper.javaserver.getmemoryusage(): msg("Server Memory Usage: %d bytes" % self.wrapper.javaserver.getmemoryusage()) elif getargs(message.split(" "), 0) in ('check-update', 'update-wrapper'): msg("Checking for new updates...") update = self.wrapper.get_wrapper_update_info() repotype = None version = None if update: version, repotype = update build = version[4] newversion = version_handler.get_version(version) yourversion = version_handler.get_version(version_info.__version__) msg( "New Wrapper.py Version %s (%s) is available! (you have %s)" % (newversion, repotype, yourversion) ) msg("To perform the update, type update-wrapper.") else: msg("No new %s Wrapper.py versions available." % version_info.__branch__) if getargs(message.split(" "), 0) == 'update-wrapper' and update: msg("Performing update..") if self.wrapper.performupdate(version, repotype): msg( "Update completed! Version %s (%s) is now installed. Please reboot " "Wrapper.py to apply changes." % (version, repotype) ) else: msg("An error occured while performing update.") msg("Please check the Wrapper.py console as soon as possible for an explanation " "and traceback.") msg("If you are unsure of the cause, please file a bug report.") elif getargs(message.split(" "), 0) == "about": msg("Wrapper.py by benbaptist - Version %s (%d)" % ( version_info.__version__, version_info.__branch__)) else: msg('Unknown command. Type help for more commands') else: msg("Session expired, re-authorize.") del self.authorized[nick] else: if getargs(message.split(" "), 0) == 'auth': if self.pass_handler.check_pw(getargs(message.split(" "), 1), self.config["IRC"]["control-irc-pass"]): msg("Authorization success! You'll remain logged in for 15 minutes.") self.authorized[nick] = int(time.time()) else: msg("Invalid password.") else: msg('Not authorized. Type "auth [password]" to login.')
class Web(object): def __init__(self, wrapper): self.wrapper = wrapper self.api = API(wrapper, "Web", internal=True) self.log = logging.getLogger('Web') self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.socket = False self.data = Storage("web") self.pass_handler = self.wrapper.cipher if "keys" not in self.data.Data: self.data.Data["keys"] = [] self.api.registerEvent("server.consoleMessage", self.onServerConsole) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.login", self.onPlayerJoin) self.api.registerEvent("player.logout", self.onPlayerLeave) self.api.registerEvent("irc.message", self.onChannelMessage) self.consoleScrollback = [] self.chatScrollback = [] self.memoryGraph = [] self.loginAttempts = 0 self.lastAttempt = 0 self.disableLogins = 0 # t = threading.Thread(target=self.updateGraph, args=()) # t.daemon = True # t.start() def __del__(self): self.data.close() def onServerConsole(self, payload): while len(self.consoleScrollback) > 1000: try: del self.consoleScrollback[0] except Exception as e: break self.consoleScrollback.append((time.time(), payload["message"])) def onPlayerMessage(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "player", "payload": { "player": payload["player"].username, "message": payload["message"] } })) def onPlayerJoin(self, payload): # print(payload) while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "playerJoin", "payload": { "player": payload["player"].username } })) def onPlayerLeave(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "playerLeave", "payload": { "player": payload["player"] } })) def onChannelMessage(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), {"type": "irc", "payload": payload})) def updateGraph(self): while not self.wrapper.halt.halt: while len(self.memoryGraph) > 200: del self.memoryGraph[0] if self.wrapper.javaserver.getmemoryusage(): self.memoryGraph.append([time.time(), self.wrapper.javaserver.getmemoryusage()]) time.sleep(1) def checkLogin(self, password): if time.time() - self.disableLogins < 60: return False # Threshold for logins if self.pass_handler.check_pw(password, self.config["Web"]["web-password"]): return True self.loginAttempts += 1 if self.loginAttempts > 10 and time.time() - self.lastAttempt < 60: self.disableLogins = time.time() self.log.warning("Disabled login attempts for one minute") self.lastAttempt = time.time() def makeKey(self, rememberme): a = "" z = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@-_" for i in range(64): # not enough performance issue to justify xrange a += z[random.randrange(0, len(z))] # a += chr(random.randrange(97, 122)) if rememberme: print("Will remember!") self.data.Data["keys"].append([a, time.time(), rememberme]) return a def validateKey(self, key): for i in self.data.Data["keys"]: expiretime = 2592000 if len(i) > 2: if not i[2]: expiretime = 21600 # Validate key and ensure it's under a week old if i[0] == key and time.time() - i[1] < expiretime: self.loginAttempts = 0 return True return False def removeKey(self, key): # we dont want to do things like this. Never delete or insert while iterating over a dictionary # because dictionaries change order as the hashtables are changed during insert and delete operations... for i, v in enumerate(self.data.Data["keys"]): if v[0] == key: del self.data.Data["keys"][i] def wrap(self): while not self.wrapper.halt.halt: try: if self.bind(): self.listen() else: self.log.error("Could not bind web to %s:%d - retrying in 5 seconds", self.config["Web"]["web-bind"], self.config["Web"]["web-port"]) except Exception as e: self.log.exception(e) time.sleep(5) def bind(self): if self.socket is not False: self.socket.close() try: self.socket = socket.socket() self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind((self.config["Web"]["web-bind"], self.config["Web"]["web-port"])) self.socket.listen(5) return True except Exception as e: return False def listen(self): self.log.info("Web Interface bound to %s:%d", self.config["Web"]["web-bind"], self.config["Web"]["web-port"]) while not self.wrapper.halt.halt: # noinspection PyUnresolvedReferences sock, addr = self.socket.accept() # self.log.debug("(WEB) Connection %s started", str(addr)) client = WebClient(sock, addr, self) t = threading.Thread(target=client.wrap, args=()) t.daemon = True t.start()
class MCServer(object): def __init__(self, wrapper): self.log = wrapper.log self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.serverpath = self.config["General"]["server-directory"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config[ "General"]["timed-reboot-minutes"] self.reboot_warning_minutes = self.config[ "General"]["timed-reboot-warning-minutes"] # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) self.state = OFF self.bootTime = time.time() # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.rebootWarnings = 0 self.lastsizepoll = 0 self.console_output_data = [] self.spammy_stuff = ["found nothing", "vehicle of", "Wrong location!", "Tried to add entity"] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.players = {} self.player_eids = {} self.worldname = None self.worldSize = 0 self.maxPlayers = 20 # -1 until proxy mode checks the server's MOTD on boot self.protocolVersion = -1 # this is string name of the version, collected by console output self.version = None # a comparable number = x0y0z, where x, y, z = release, # major, minor, of version. self.version_compute = 0 # this port should be hidden from outside traffic. self.server_port = "25564" self.world = None self.entity_control = None self.motd = None # -1 until a player logs on and server sends a time update self.timeofday = -1 self.onlineMode = True self.serverIcon = None # get OPs self.ownernames = {} self.operator_list = [] self.refresh_ops() self.properties = {} # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"] or self.config[ "Web"]["web-enabled"]: self.api.registerEvent("timer.second", self.eachsecond) def init(self): """ Start up the listen threads for reading server console output. """ capturethread = threading.Thread(target=self.__stdout__, args=()) capturethread.daemon = True capturethread.start() capturethread = threading.Thread(target=self.__stderr__, args=()) capturethread.daemon = True capturethread.start() def __del__(self): self.state = 0 def accepteula(self): if os.path.isfile("%s/eula.txt" % self.serverpath): self.log.debug("Checking EULA agreement...") with open("%s/eula.txt" % self.serverpath) as f: eula = f.read() # if forced, should be at info level since acceptance # is a legal matter. if "eula=false" in eula: self.log.warning( "EULA agreement was not accepted, accepting on" " your behalf...") set_item("eula", "true", "eula.txt", self.serverpath) self.log.debug("EULA agreement has been accepted.") return True else: return False def handle_server(self): """ Function that handles booting the server, parsing console output, and such. """ trystart = 0 while not self.wrapper.halt: trystart += 1 self.proc = None # endless loop for not booting the server (while still # allowing handle to run). if not self.boot_server: time.sleep(0.2) trystart = 0 continue self.changestate(STARTING) self.log.info("Starting server...") self.reloadproperties() # stuff I was trying to get colorized output to come through # for non-vanilla servers. command = '2>&1' self.args.append(command) command2 = self.args # print("args:\n%s\n" % command2) self.proc = subprocess.Popen( command2, cwd=self.serverpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) self.players = {} self.accepteula() # Auto accept eula if self.proc.poll() is None and trystart > 3: self.log.error( "Could not start server. check your server.properties," " wrapper.properties and this your startup 'command'" " from wrapper.properties:\n'%s'", " ".join(self.args)) self.changestate(OFF) # halt wrapper self.wrapper.halt = True # exit server_handle break # The server loop while True: # Loop runs continously as long as server console is running time.sleep(0.1) if self.proc.poll() is not None: self.changestate(OFF) trystart = 0 self.boot_server = self.server_autorestart # break back out to `while not self.wrapper.halt:` loop # to (possibly) connect to server again. break # is is only reading server console output for line in self.console_output_data: try: self.readconsole(line.replace("\r", "")) except Exception as e: self.log.exception(e) self.console_output_data = [] # code ends here on wrapper.halt and execution returns to # the end of wrapper.start() def _toggle_server_started(self, server_started=True): self.wrapper.storage["ServerStarted"] = server_started self.wrapper.wrapper_storage.save() def start(self): """ Start the Minecraft server """ self.server_autorestart = self.config["General"]["auto-restart"] if self.state in (STARTED, STARTING): self.log.warning("The server is already running!") return if not self.boot_server: self.boot_server = True else: self.handle_server() self._toggle_server_started() def restart(self, reason=""): """Restart the Minecraft server, and kick people with the specified reason """ if reason == "": reason = self.restart_message if self.state in (STOPPING, OFF): self.log.warning( "The server is not already running... Just use '/start'.") return self.stop(reason) def stop(self, reason="", restart_the_server=True): """Stop the Minecraft server from an automatic process. Allow it to restart by default. """ self.log.info("Stopping Minecraft server with reason: %s", reason) self.changestate(STOPPING, reason) for player in self.players: self.console("kick %s %s" % (player, reason)) self.console("stop") # False will allow this loop to run with no server (and # reboot if permitted). self.boot_server = restart_the_server def stop_server_command(self, reason="", restart_the_server=False): """ Stop the Minecraft server (as a command). By default, do not restart. """ if reason == "": reason = self.stop_message if self.state == OFF: self.log.warning("The server is not running... :?") return if self.state == FROZEN: self.log.warning("The server is currently frozen.\n" "To stop it, you must /unfreeze it first") return self.server_autorestart = False self.stop(reason, restart_the_server) self._toggle_server_started(restart_the_server) def kill(self, reason="Killing Server"): """Forcefully kill the server. It will auto-restart if set in the configuration file. """ if self.state in (STOPPING, OFF): self.log.warning("The server is already dead, my friend...") return self.log.info("Killing Minecraft server with reason: %s", reason) self.changestate(OFF, reason) self.proc.kill() def freeze(self, reason="Server is now frozen. You may disconnect."): """Freeze the server with `kill -STOP`. Can be used to stop the server in an emergency without shutting it down, so it doesn't write corrupted data - e.g. if the disk is full, you can freeze the server, free up some disk space, and then unfreeze 'reason' argument is printed in the chat for all currently-connected players, unless you specify None. This command currently only works for *NIX based systems. """ if self.state != OFF: if os.name == "posix": self.log.info("Freezing server with reason: %s", reason) self.broadcast("&c%s" % reason) time.sleep(0.5) self.changestate(FROZEN) os.system("kill -STOP %d" % self.proc.pid) else: raise UnsupportedOSException( "Your current OS (%s) does not support this" " command at this time." % os.name) else: raise InvalidServerStartedError( "Server is not started. You may run '/start' to boot it up.") def unfreeze(self): """Unfreeze the server with `kill -CONT`. Counterpart to .freeze(reason) This command currently only works for *NIX based systems. """ if self.state != OFF: if os.name == "posix": self.log.info("Unfreezing server (ignore any" " messages to type /start)...") self.broadcast("&aServer unfrozen.") self.changestate(STARTED) os.system("kill -CONT %d" % self.proc.pid) else: raise UnsupportedOSException( "Your current OS (%s) does not support this command" " at this time." % os.name) else: raise InvalidServerStartedError( "Server is not started. Please run '/start' to boot it up.") def broadcast(self, message, who="@a"): """Broadcasts the specified message to all clients connected. message can be a JSON chat object, or a string with formatting codes using the § as a prefix. """ if isinstance(message, dict): if self.version_compute < 10700: self.console("say %s %s" % (who, chattocolorcodes(message))) else: encoding = self.wrapper.encoding self.console("tellraw %s %s" % ( who, json.dumps(message, ensure_ascii=False))) else: if self.version_compute < 10700: temp = processcolorcodes(message) self.console("say %s %s" % ( who, chattocolorcodes(json.loads(temp)))) else: self.console("tellraw %s %s" % ( who, processcolorcodes(message))) def login(self, username, eid, location): """Called when a player logs in.""" # place to store EID if proxy is not fully connected yet. self.player_eids[username] = [eid, location] if username not in self.players: self.players[username] = Player(username, self.wrapper) if self.wrapper.proxy: playerclient = self.getplayer(username).getClient() if playerclient: playerclient.server_connection.eid = eid playerclient.position = location self.players[username].loginposition = self.player_eids[username][1] self.wrapper.events.callevent( "player.login", {"player": self.getplayer(username)}) def logout(self, players_name): """Called when a player logs out.""" # self.wrapper.callEvent( # "player.logout", {"player": self.getPlayer(username)}) self.wrapper.events.callevent( "player.logout", self.getplayer(players_name)) if self.wrapper.proxy: self.wrapper.proxy.removestaleclients() # remove a hub player or not?? if players_name in self.players: self.players[players_name].abort = True del self.players[players_name] def getplayer(self, username): """Returns a player object with the specified name, or False if the user is not logged in/doesn't exist. """ if username in self.players: return self.players[username] return False def reloadproperties(self): # Load server icon if os.path.exists("%s/server-icon.png" % self.serverpath): with open("%s/server-icon.png" % self.serverpath, "rb") as f: theicon = f.read() iconencoded = base64.standard_b64encode(theicon) self.serverIcon = b"data:image/png;base64," + iconencoded # Read server.properties and extract some information out of it # the PY3.5 ConfigParser seems broken. This way was much more # straightforward and works in both PY2 and PY3 self.properties = config_to_dict_read( "server.properties", self.serverpath) if self.properties == {}: self.log.warning("File 'server.properties' not found.") return False if "level-name" in self.properties: self.worldname = self.properties["level-name"] else: self.log.warning("No 'level-name=(worldname)' was" " found in the server.properties.") return False self.motd = self.properties["motd"] if "max-players" in self.properties: self.maxPlayers = self.properties["max-players"] else: self.log.warning( "No 'max-players=(count)' was found in the" " server.properties. The default of '20' will be used.") self.maxPlayers = 20 self.onlineMode = self.properties["online-mode"] def console(self, command): """Execute a console command on the server.""" if self.state in (STARTING, STARTED, STOPPING) and self.proc: self.proc.stdin.write("%s\n" % command) self.proc.stdin.flush() else: self.log.debug("Attempted to run console command" " '%s' but the Server is not started.", command) def changestate(self, state, reason=None): """Change the boot state indicator of the server, with a reason message. """ self.state = state if self.state == OFF: self.wrapper.events.callevent( "server.stopped", {"reason": reason}) elif self.state == STARTING: self.wrapper.events.callevent( "server.starting", {"reason": reason}) elif self.state == STARTED: self.wrapper.events.callevent( "server.started", {"reason": reason}) elif self.state == STOPPING: self.wrapper.events.callevent( "server.stopping", {"reason": reason}) self.wrapper.events.callevent( "server.state", {"state": state, "reason": reason}) def getservertype(self): if "spigot" in self.config["General"]["command"].lower(): return "spigot" elif "bukkit" in self.config["General"]["command"].lower(): return "bukkit" else: return "vanilla" def server_reload(self): """This is not used yet.. intended to restart a server without kicking players restarts the server quickly. Wrapper "auto-restart" must be set to True. If wrapper is in proxy mode, it will reconnect all clients to the serverconnection. """ if self.state in (STOPPING, OFF): self.log.warning( "The server is not already running... Just use '/start'.") return if self.wrapper.proxymode: # discover who all is playing and store that knowledge # tell the serverconnection to stop processing play packets self.server_stalled = True # stop the server. # Call events to "do stuff" while server is down (write # whilelists, OP files, server properties, etc) # restart the server. if self.wrapper.proxymode: pass # once server is back up, Reconnect stalled/idle # clients back to the serverconnection process. # Do I need to create a new serverconnection, # or can the old one be tricked into continuing?? self.stop_server_command() def __stdout__(self): """handles server output, not lines typed in console.""" while not self.wrapper.halt: # noinspection PyBroadException,PyUnusedLocal # this reads the line and puts the line in the # 'self.data' buffer for processing by # readconsole() (inside handle_server) try: data = self.proc.stdout.readline() for line in data.split("\n"): if len(line) < 1: continue self.console_output_data.append(line) except Exception as e: time.sleep(0.1) continue def __stderr__(self): """like __stdout__, handles server output (not lines typed in console).""" while not self.wrapper.halt: try: data = self.proc.stderr.readline() if len(data) > 0: for line in data.split("\n"): self.console_output_data.append(line.replace("\r", "")) except Exception as e: time.sleep(0.1) continue def read_ops_file(self, read_super_ops=True): """Keep a list of ops in the server instance to stop reading the disk for it. :rtype: Dictionary """ ops = False # (4 = PROTOCOL_1_7 ) - 1.7.6 or greater use ops.json if self.protocolVersion > 4: ops = getjsonfile("ops", self.serverpath, encodedas=self.encoding) if not ops: # try for an old "ops.txt" file instead. ops = [] opstext = getfileaslines("ops.txt", self.serverpath) if not opstext: return False for op in opstext: # create a 'fake' ops list from the old pre-1.8 # text line name list notice that the level (an # option not the old list) is set to 1 This will # pass as true, but if the plugin is also # checking op-levels, it may not pass truth. indivop = {"uuid": op, "name": op, "level": 1} ops.append(indivop) # Grant "owner" an op level above 4. required for some wrapper commands if read_super_ops: for eachop in ops: if eachop["name"] in self.ownernames: eachop["level"] = self.ownernames[eachop["name"]] return ops def refresh_ops(self, read_super_ops=True): self.ownernames = config_to_dict_read("superops.txt", ".") if self.ownernames == {}: sample = "<op_player_1>=10\n<op_player_2>=9" with open("superops.txt", "w") as f: f.write(sample) self.operator_list = self.read_ops_file(read_super_ops) def getmemoryusage(self): """Returns allocated memory in bytes. This command currently only works for *NIX based systems. """ if not resource or not os.name == "posix" or self.proc is None: raise UnsupportedOSException( "Your current OS (%s) does not support" " this command at this time." % os.name) try: with open("/proc/%d/statm" % self.proc.pid) as f: getbytes = int(f.read().split(" ")[1]) * resource.getpagesize() return getbytes except Exception as e: raise e @staticmethod def getstorageavailable(folder): """Returns the disk space for the working directory in bytes. """ if platform.system() == "Windows": free_bytes = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes)) return free_bytes.value else: st = os.statvfs(folder) return st.f_bavail * st.f_frsize @staticmethod def stripspecial(text): a = "" it = iter(range(len(text))) for i in it: char = text[i] if char == "\xc2": try: next(it) next(it) except Exception as e: pass else: a += char return a def readconsole(self, buff): """Internally-used function that parses a particular console line. """ if not self.wrapper.events.callevent( "server.consoleMessage", {"message": buff}): return False if len(buff) < 1: return # Standardize the line to only include the text (removing # time and log pre-pends) line_words = buff.split(' ')[self.prepends_offset:] # find the actual offset is where server output line # starts (minus date/time and info stamps). # .. and load the proper ops file if "Starting minecraft server version" in buff and \ self.prepends_offset == 0: for place in range(len(line_words)-1): self.prepends_offset = place if line_words[place] == "Starting": break line_words = buff.split(' ')[self.prepends_offset:] self.version = getargs(line_words, 4) semantics = self.version.split(".") release = get_int(getargs(semantics, 0)) major = get_int(getargs(semantics, 1)) minor = get_int(getargs(semantics, 2)) self.version_compute = minor + (major * 100) + (release * 10000) # 1.7.6 (protocol 5) is the cutoff where ops.txt became ops.json if self.version_compute > 10705 and self.protocolVersion < 0: self.protocolVersion = 5 self.refresh_ops() if len(line_words) < 1: return # the server attempted to print a blank line if len(line_words[0]) < 1: print('') return # parse or modify the server output section # # # Over-ride OP help console display if "/op <player>" in buff: new_usage = "player> [-s SUPER-OP] [-o OFFLINE] [-l <level>]" message = buff.replace("player>", new_usage) buff = message if "While this makes the game possible to play" in buff: prefix = " ".join(buff.split(' ')[:self.prepends_offset]) if not self.wrapper.wrapper_onlinemode: message = ( "%s Since you are running Wrapper in OFFLINE mode, THIS " "COULD BE SERIOUS!\n%s Wrapper is not handling any" " authenication.\n%s This is only ok if this wrapper " "is not accessible from either port %s or port %s" " (I.e., this wrapper is a multiworld for a hub server, or" " you are doing your own authorization via a plugin)." % ( prefix, prefix, prefix, self.server_port, self.wrapper.proxy.proxy_port)) else: message = ( "%s Since you are running Wrapper in proxy mode, this" " should be ok because Wrapper is handling the" " authenication, PROVIDED no one can access port" " %s from outside your network." % ( prefix, self.server_port)) if self.wrapper.proxymode: buff = message # check for server console spam before printing to wrapper console server_spaming = False for things in self.spammy_stuff: if things in buff: server_spaming = True # server_spaming setting does not stop it from being parsed below. if not server_spaming: if not self.server_muted: self.wrapper.write_stdout(buff, "server") else: self.queued_lines.append(buff) # region server console parsing section # read port of server if "Starting Minecraft server" in buff: self.server_port = get_int(buff.split('on *:')[1]) # confirm server start elif "Done (" in buff: self._toggle_server_started() self.changestate(STARTED) self.log.info("Server started") self.bootTime = time.time() # Getting world name elif "Preparing level" in buff: self.worldname = getargs(line_words, 2).replace('"', "") self.world = World(self.worldname, self) if self.wrapper.proxymode: self.entity_control = EntityControl(self) # Player Message if getargs(line_words, 0)[0] == "<": name = self.stripspecial(getargs(line_words, 0)[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) self.wrapper.events.callevent("player.message", { "player": self.getplayer(name), "message": message, "original": original }) # Player Login elif getargs(line_words, 1) == "logged": name = self.stripspecial( getargs(line_words, 0)[0:getargs(line_words, 0).find("[")]) eid = get_int(getargs(line_words, 6)) locationtext = getargs(buff.split(" ("), 1)[:-1].split(", ") locationtext[0] = locationtext[0].replace("[world]", "") location = get_int( float(locationtext[0])), get_int( float(locationtext[1])), get_int( float(locationtext[2])) self.login(name, eid, location) # Player Logout elif "lost connection" in buff: name = getargs(line_words, 0) self.logout(name) # player action elif getargs(line_words, 0) == "*": name = self.stripspecial(getargs(line_words, 1)) message = self.stripspecial(getargsafter(line_words, 2)) self.wrapper.events.callevent("player.action", { "player": self.getplayer(name), "action": message }) # Player Achievement elif "has just earned the achievement" in buff: name = self.stripspecial(getargs(line_words, 0)) achievement = getargsafter(line_words, 6) self.wrapper.events.callevent("player.achievement", { "player": name, "achievement": achievement }) # /say command elif getargs( line_words, 0)[0] == "[" and getargs(line_words, 0)[-1] == "]": if self.getservertype != "vanilla": # Unfortunately, Spigot and Bukkit output things # that conflict with this. return name = self.stripspecial(getargs(line_words, 0)[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) self.wrapper.events.callevent("server.say", { "player": name, "message": message, "original": original }) # Player Death elif getargs(line_words, 1) in self.deathprefixes: name = self.stripspecial(getargs(line_words, 0)) self.wrapper.events.callevent("player.death", { "player": self.getplayer(name), "death": getargsafter(line_words, 1) }) # server lagged elif "Can't keep up!" in buff: skipping_ticks = getargs(line_words, 17) self.wrapper.events.callevent("server.lagged", { "ticks": get_int(skipping_ticks) }) # mcserver.py onsecond Event Handler def eachsecond(self, payload): if self.config["General"]["timed-reboot"]: if time.time() - self.bootTime > (self.reboot_minutes * 60): if self.config["General"]["timed-reboot-warning-minutes"] > 0: if self.rebootWarnings <= self.reboot_warning_minutes: l = (time.time() - self.bootTime - self.reboot_minutes * 60) if l > self.rebootWarnings: self.rebootWarnings += 1 if int(self.reboot_warning_minutes - l + 1) > 0: self.broadcast( "&cServer will reboot in %d minute(s)!" % int(self.reboot_warning_minutes - l + 1)) return self.restart(self.reboot_message) self.bootTime = time.time() self.rebootWarnings = 0 # only used by web management module if self.config["Web"]["web-enabled"]: if time.time() - self.lastsizepoll > 120: if self.worldname is None: return True self.lastsizepoll = time.time() size = 0 # os.scandir not in standard library on early py2.7.x systems for i in os.walk("%s/%s" % (self.serverpath, self.worldname)): for f in os.listdir(i[0]): size += os.path.getsize(os.path.join(i[0], f)) self.worldSize = size
def __init__(self, wrapper): self.log = wrapper.log self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.state = OFF self.properties = {} self.worldname = None self.worldsize = 0 # owner/op info self.ownernames = {} self.operator_list = [] self.spammy_stuff = ["found nothing", "vehicle of", "Wrong location!", "Tried to add entity", ] # this is string name of the version, collected by console output self.version = "" self.version_compute = 0 self.servericon = None self.motd = None self.timeofday = -1 self.protocolVersion = -1 self.server_port = "25564" self.encoding = self.config["General"]["encoding"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config["General"]["timed-reboot-minutes"] self.reboot_warn_minutes = self.config["General"]["timed-reboot-warning-minutes"] # noqa # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.lastsizepoll = 0 self.console_output_data = [] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot", "slain"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.world = None # get OPs self.refresh_ops() # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"]: rb = threading.Thread(target=self.reboot_timer, args=()) rb.daemon = True rb.start() if self.config["Web"]["web-enabled"]: wb = threading.Thread(target=self.eachsecond_web, args=()) wb.daemon = True wb.start() # This event is used to allow proxy to make console commands via # callevent() without referencing mcserver.py code (the eventhandler # is passed as an argument to the proxy). self.api.registerEvent("proxy.console", self._console_event)
class IRC(object): def __init__(self, mcserver, log, wrapper): self.socket = False self.state = False self.javaserver = mcserver self.config = wrapper.config self.configmgr = wrapper.configManager self.wrapper = wrapper self.address = self.config["IRC"]["server"] self.port = self.config["IRC"]["port"] self.nickname = self.config["IRC"]["nick"] self.originalNickname = self.nickname[0:] self.nickAttempts = 0 self.channels = self.config["IRC"]["channels"] self.log = log self.timeout = False self.ready = False self.msgQueue = [] self.authorized = {} self.line = "" self.api = API(self.wrapper, "IRC", internal=True) self.api.registerEvent("irc.message", self.onchannelmessage) self.api.registerEvent("irc.action", self.onchannelaction) self.api.registerEvent("irc.join", self.onchanneljoin) self.api.registerEvent("irc.part", self.onchannelpart) self.api.registerEvent("irc.quit", self.onchannelquit) self.api.registerEvent("server.starting", self.onServerStarting) self.api.registerEvent("server.started", self.onServerStarted) self.api.registerEvent("server.stopping", self.onServerStopping) self.api.registerEvent("server.stopped", self.onServerStopped) self.api.registerEvent("player.login", self.onPlayerLogin) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.action", self.onPlayerAction) self.api.registerEvent("player.logout", self.onPlayerLogout) self.api.registerEvent("player.achievement", self.onPlayerAchievement) self.api.registerEvent("player.death", self.onPlayerDeath) self.api.registerEvent("wrapper.backupBegin", self.onBackupBegin) self.api.registerEvent("wrapper.backupEnd", self.onBackupEnd) self.api.registerEvent("wrapper.backupFailure", self.onBackupFailure) self.api.registerEvent("server.say", self.onPlayerSay) def init(self): while not self.wrapper.halt: try: self.log.info("Connecting to IRC...") self.connect() t = threading.Thread(target=self.queue, args=()) t.daemon = True t.start() self.handle() except Exception as e: self.log.exception(e) self.disconnect("Error in Wrapper.py - restarting") self.log.info("Disconnected from IRC") time.sleep(5) def connect(self): self.nickname = self.originalNickname[0:] self.socket = socket.socket() self.socket.connect((self.address, self.port)) self.socket.setblocking(120) self.auth() def auth(self): if self.config["IRC"]["password"]: self.send("PASS %s" % self.config["IRC"]["password"]) self.send("NICK %s" % self.nickname) self.send("USER %s 0 * :%s" % (self.nickname, self.nickname)) def disconnect(self, message): try: self.send("QUIT :%s" % message) self.socket.close() self.socket = False except Exception as e: self.log.debug("Exception in IRC disconnect: \n%s", e) def send(self, payload): if self.socket: self.socket.send("%s\n" % payload) else: return False # Event Handlers def messagefromchannel(self, channel, message): if self.config["IRC"]["show-channel-server"]: self.javaserver.broadcast("&6[%s] %s" % (channel, message)) else: self.javaserver.broadcast(message) def onchanneljoin(self, payload): channel, nick = payload["channel"], payload["nick"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rjoined the channel" % nick) def onchannelpart(self, payload): channel, nick = payload["channel"], payload["nick"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rparted the channel" % nick) def onchannelmessage(self, payload): channel, nick, message = payload["channel"], payload["nick"], payload["message"] final = "" for i, chunk in enumerate(message.split(" ")): if not i == 0: final += " " try: if chunk[0:7] in ("http://", "https://"): final += "&b&n&@%s&@&r" % chunk else: final += chunk except Exception as e: self.log.debug("Exception in IRC onchannelmessage: \n%s", e) final += chunk self.messagefromchannel(channel, "&a<%s> &r%s" % (nick, final)) def onchannelaction(self, payload): channel, nick, action = payload["channel"], payload["nick"], payload["action"] self.messagefromchannel(channel, "&a* %s &r%s" % (nick, action)) def onchannelquit(self, payload): channel, nick, message = payload["channel"], payload["nick"], payload["message"] if not self.config["IRC"]["show-irc-join-part"]: return self.messagefromchannel(channel, "&a%s &rquit: %s" % (nick, message)) def onPlayerLogin(self, payload): player = self.filterName(payload["player"]) self.msgQueue.append("[%s connected]" % player) def onPlayerLogout(self, payload): player = payload["player"] self.msgQueue.append("[%s disconnected]" % player) def onPlayerMessage(self, payload): player = self.filterName(payload["player"]) message = payload["message"] self.msgQueue.append("<%s> %s" % (player, message)) def onPlayerAction(self, payload): player = self.filterName(payload["player"]) action = payload["action"] self.msgQueue.append("* %s %s" % (player, action)) def onPlayerSay(self, payload): player = self.filterName(payload["player"]) message = payload["message"] self.msgQueue.append("[%s] %s" % (player, message)) def onPlayerAchievement(self, payload): player = self.filterName(payload["player"]) achievement = payload["achievement"] self.msgQueue.append("%s has just earned the achievement %s" % (player, achievement)) def onPlayerDeath(self, payload): player = self.filterName(payload["player"]) death = payload["death"] self.msgQueue.append("%s %s" % (player, death)) def onBackupBegin(self, payload): self.msgQueue.append("Backing up... lag may occur!") def onBackupEnd(self, payload): time.sleep(1) self.msgQueue.append("Backup complete!") def onBackupFailure(self, payload): if "reasonText" in payload: self.msgQueue.append("ERROR: %s" % payload["reasonText"]) else: self.msgQueue.append("An unknown error occurred while trying to backup.") def onServerStarting(self, payload): self.msgQueue.append("Server starting...") def onServerStarted(self, payload): self.msgQueue.append("Server started!") def onServerStopping(self, payload): self.msgQueue.append("Server stopping...") def onServerStopped(self, payload): self.msgQueue.append("Server stopped!") def handle(self): while self.socket: try: irc_buffer = self.socket.recv(1024) # more duck typing if irc_buffer == "": self.log.error("Disconnected from IRC") self.socket = False self.ready = False break except socket.timeout: if self.timeout: self.socket = False break else: self.send("PING :%s" % str(random.randint())) self.timeout = True irc_buffer = "" except Exception as e: self.log.debug("Exception in IRC handle: \n%s", e) irc_buffer = "" for line in irc_buffer.split("\n"): self.line = line self.parse() def queue(self): while self.socket: if not self.ready: time.sleep(0.1) continue for i, message in enumerate(self.msgQueue): for channel in self.channels: if len(message) > 400: for l in xrange(int(math.ceil(len(message) / 400.0))): chunk = message[l * 400:(l + 1) * 400] self.send("PRIVMSG %s :%s" % (channel, chunk)) else: self.send("PRIVMSG %s :%s" % (channel, message)) del self.msgQueue[i] self.msgQueue = [] time.sleep(0.1) def filterName(self, name): if self.config["IRC"]["obstruct-nicknames"]: return "_" + str(name)[1:] else: return name def rawConsole(self, payload): self.javaserver.console(payload) def console(self, channel, payload): if self.config["IRC"]["show-channel-server"]: self.rawConsole({"text": "[%s] " % channel, "color": "gold", "extra": payload}) else: self.rawConsole({"extra": payload}) def parse(self): if getargs(self.line.split(" "), 1) == "001": for command in self.config["IRC"]["autorun-irc-commands"]: self.send(command) for channel in self.channels: self.send("JOIN %s" % channel) self.ready = True self.log.info("Connected to IRC!") self.state = True self.nickAttempts = 0 if getargs(self.line.split(" "), 1) == "433": self.log.info("Nickname '%s' already in use.", self.nickname) self.nickAttempts += 1 if self.nickAttempts > 2: name = bytearray(self.nickname) for i in xrange(3): name[len(self.nickname) / 3 * i] = chr(random.randrange(97, 122)) self.nickname = str(name) else: self.nickname += "_" self.auth() self.log.info("Attemping to use nickname '%s'.", self.nickname) if getargs(self.line.split(" "), 1) == "JOIN": nick = getargs(self.line.split(" "), 0)[1:getargs(self.line.split(" "), 0).find("!")] channel = getargs(self.line.split(" "), 2)[1:][:-1] self.log.info("%s joined %s", nick, channel) self.wrapper.events.callevent("irc.join", {"nick": nick, "channel": channel}) if getargs(self.line.split(" "), 1) == "PART": nick = getargs(self.line.split(" "), 0)[1:getargs(self.line.split(" "), 0).find("!")] channel = getargs(self.line.split(" "), 2) self.log.info("%s parted %s", nick, channel) self.wrapper.events.callevent("irc.part", {"nick": nick, "channel": channel}) if getargs(self.line.split(" "), 1) == "MODE": try: nick = getargs(self.line.split(" "), 0)[1:getargs(self.line.split(" "), 0).find('!')] channel = getargs(self.line.split(" "), 2) modes = getargs(self.line.split(" "), 3) user = getargs(self.line.split(" "), 4)[:-1] self.console(channel, [{ "text": user, "color": "green" }, { "text": " received modes %s from %s" % (modes, nick), "color": "white" }]) except Exception as e: self.log.debug("Exception in IRC in parse (MODE): \n%s", e) pass if getargs(self.line.split(" "), 0) == "PING": self.send("PONG %s" % getargs(self.line.split(" "), 1)) if getargs(self.line.split(" "), 1) == "QUIT": nick = getargs(self.line.split(" "), 0)[1:getargs(self.line.split(" "), 0).find("!")] message = getargsafter(self.line.split(" "), 2)[1:].strip("\n").strip("\r") self.wrapper.events.callevent("irc.quit", {"nick": nick, "message": message, "channel": None}) if getargs(self.line.split(" "), 1) == "PRIVMSG": channel = getargs(self.line.split(" "), 2) nick = getargs(self.line.split(" "), 0)[1:getargs(self.line.split(" "), 0).find("!")] message = getargsafter(self.line.split(" "), 3)[1:].strip("\n").strip("\r") if channel[0] == "#": if message.strip() == ".players": users = "" for user in self.javaserver.players: users += "%s " % user self.send("PRIVMSG %s :There are currently %s users on the server: %s" % (channel, len(self.javaserver.players), users)) elif message.strip() == ".about": self.send("PRIVMSG %s :Wrapper.py Version %s" % (channel, self.wrapper.getbuildstring())) else: message = message.decode("utf-8", "ignore") if getargs(message.split(" "), 0) == "\x01ACTION": self.wrapper.events.callevent("irc.action", {"nick": nick, "channel": channel, "action": getargsafter(message.split(" "), 1)[:-1]}) self.log.info("[%s] * %s %s", channel, nick, getargsafter(message.split(" "), 1)[:-1]) else: self.wrapper.events.callevent("irc.message", {"nick": nick, "channel": channel, "message": message}) self.log.info("[%s] <%s> %s", channel, nick, message) elif self.config["IRC"]["control-from-irc"]: self.log.info("[PRIVATE] (%s) %s", nick, message) def msg(string): self.log.info("[PRIVATE] (%s) %s", self.nickname, string) self.send("PRIVMSG %s :%s" % (nick, string)) if self.config["IRC"]["control-irc-pass"] == "password": msg("A new password is required in wrapper.properties. Please change it.") return if "password" in self.config["IRC"]["control-irc-pass"]: msg("Please choose a password that doesn't contain the term 'password'.") return if nick in self.authorized: if int(time.time()) - self.authorized[nick] < 900: if getargs(message.split(" "), 0) == 'hi': msg('Hey there!') elif getargs(message.split(" "), 0) == 'help': # eventually I need to make help only one or two # lines, to prevent getting kicked/banned for spam msg("run [command] - run command on server") msg("togglebackups - temporarily turn backups on or off. this setting is not permanent " "and will be lost on restart") msg("halt - shutdown server and Wrapper.py, will not auto-restart") msg("kill - force server restart without clean shutdown - only use when server " "is unresponsive") msg("start/restart/stop - start the server/automatically stop and start server/stop " "the server without shutting down Wrapper") msg("status - show status of the server") msg("check-update - check for new Wrapper.py updates, but don't install them") msg("update-wrapper - check and install new Wrapper.py updates") msg("Wrapper.py Version %s by benbaptist" % self.wrapper.getbuildstring()) # msg('console - toggle console output to this private message') elif getargs(message.split(" "), 0) == 'togglebackups': self.config["Backups"]["enabled"] = not self.config["Backups"]["enabled"] if self.config["Backups"]["enabled"]: msg('Backups are now on.') else: msg('Backups are now off.') self.configmgr.save() # 'config' is just the json dictionary of items, not the Config class elif getargs(message.split(" "), 0) == 'run': if getargs(message.split(" "), 1) == '': msg('Usage: run [command]') else: command = " ".join(message.split(' ')[1:]) self.javaserver.console(command) elif getargs(message.split(" "), 0) == 'halt': self.wrapper.halt = True self.javaserver.console("stop") self.javaserver.changestate(3) elif getargs(message.split(" "), 0) == 'restart': self.javaserver.restart("Restarting server from IRC remote") self.javaserver.changestate(3) elif getargs(message.split(" "), 0) == 'stop': self.javaserver.console('stop') self.javaserver.stop_server_command("Stopped from IRC remote") msg("Server stopping") elif getargs(message.split(" "), 0) == 'start': self.javaserver.start() msg("Server starting") elif getargs(message.split(" "), 0) == 'kill': self.javaserver.kill("Killing server from IRC remote") msg("Server terminated.") elif getargs(message.split(" "), 0) == 'status': if self.javaserver.state == 2: msg("Server is running.") elif self.javaserver.state == 1: msg("Server is currently starting/frozen.") elif self.javaserver.state == 0: msg("Server is stopped. Type 'start' to fire it back up.") elif self.javaserver.state == 3: msg("Server is in the process of shutting down/restarting.") else: msg("Server is in unknown state. This is probably a Wrapper.py bug - report it! " "(state #%d)" % self.javaserver.state) if self.wrapper.javaserver.getmemoryusage(): msg("Server Memory Usage: %d bytes" % self.wrapper.javaserver.getmemoryusage()) elif getargs(message.split(" "), 0) == 'check-update': msg("Checking for new updates...") update = self.wrapper.get_wrapper_update_info() if update: version, build, repotype = update if repotype == "stable": msg("New Wrapper.py Version %s available! (you have %s)" % (".".join([str(_) for _ in version]), self.wrapper.getbuildstring())) elif repotype == "dev": msg("New Wrapper.py development build %s #%d available! (you have %s #%d)" % (".".join([str(_) for _ in version]), build, version_info.__version__, version_info.__build__)) else: msg("Unknown new version: %s | %d | %s" % (version, build, repotype)) msg("To perform the update, type update-wrapper.") else: if version_info.__branch__ == "stable": msg("No new stable Wrapper.py versions available.") elif version_info.__branch__ == "dev": msg("No new development Wrapper.py versions available.") elif getargs(message.split(" "), 0) == 'update-wrapper': msg("Checking for new updates...") update = self.wrapper.get_wrapper_update_info() if update: version, build, repotype = update if repotype == "stable": msg("New Wrapper.py Version %s available! (you have %s)" % (".".join([str(_) for _ in version]), self.wrapper.getbuildstring())) elif repotype == "dev": msg("New Wrapper.py development build %s #%d available! (you have %s #%d)" % (".".join(version), build, version_info.__version__, version_info.__build__)) else: msg("Unknown new version: %s | %d | %s" % (version, build, repotype)) msg("Performing update..") if self.wrapper.performupdate(version, build, repotype): msg("Update completed! Version %s #%d (%s) is now installed. Please reboot " "Wrapper.py to apply changes." % (version, build, repotype)) else: msg("An error occured while performing update.") msg("Please check the Wrapper.py console as soon as possible for an explanation " "and traceback.") msg("If you are unsure of the cause, please file a bug report on http://github.com" "/benbaptist/minecraft-wrapper.") else: if version_info.__branch__ == "stable": msg("No new stable Wrapper.py versions available.") elif version_info.__branch__ == "dev": msg("No new development Wrapper.py versions available.") elif getargs(message.split(" "), 0) == "about": msg("Wrapper.py by benbaptist - Version %s (build #%d)" % (version_info.__version__, version_info.__branch__)) else: msg('Unknown command. Type help for more commands') else: msg("Session expired, re-authorize.") del self.authorized[nick] else: if getargs(message.split(" "), 0) == 'auth': if getargs(message.split(" "), 1) == self.config["IRC"]["control-irc-pass"]: msg("Authorization success! You'll remain logged in for 15 minutes.") self.authorized[nick] = int(time.time()) else: msg("Invalid password.") else: msg('Not authorized. Type "auth [password]" to login.')
class Backups(object): def __init__(self, wrapper): self.wrapper = wrapper self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.log = wrapper.log self.api = API(wrapper, "Backups", internal=True) self.interval = 0 self.backup_interval = self.config["Backups"]["backup-interval"] self.time = time.time() self.backups = [] self.enabled = self.config["Backups"]["enabled"] # allow plugins to shutdown backups via api self.timerstarted = False if self.enabled and self.dotarchecks(): # only register event if used and tar installed! self.api.registerEvent("timer.second", self.eachsecond) self.timerstarted = True self.log.debug("Backups Enabled..") # noinspection PyUnusedLocal def eachsecond(self, payload): self.interval += 1 if time.time() - self.time > self.backup_interval and self.enabled: self.dobackup() def pruneoldbackups(self, filename="IndependentPurge"): if len(self.backups) > self.config["Backups"]["backups-keep"]: self.log.info("Deleting old backups...") while len(self.backups) > self.config["Backups"]["backups-keep"]: backup = self.backups[0] if not self.wrapper.events.callevent("wrapper.backupDelete", {"file": filename}): """ eventdoc <group> Backups <group> <description> Called upon deletion of a backup file. <description> <abortable> Yes, return False to abort. <abortable> <comments> <comments> <payload> "file": filename <payload> """ break try: os.remove('%s/%s' % (self.config["Backups"]["backup-location"], backup[1])) except Exception as e: self.log.error("Failed to delete backup (%s)", e) self.log.info("Deleting old backup: %s", datetime.datetime.fromtimestamp(int(backup[0])).strftime('%Y-%m-%d_%H:%M:%S')) # hink = self.backups[0][1][:] # not used... del self.backups[0] putjsonfile(self.backups, "backups", self.config["Backups"]["backup-location"]) def dotarchecks(self): # Check if tar is installed which = "where" if platform.system() == "Windows" else "which" if not subprocess.call([which, "tar"]) == 0: self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 1, "reasonText": "Tar is not installed. Please install " "tar before trying to make backups."}) """ eventdoc <group> Backups <group> <description> Indicates failure of backup. <description> <abortable> No - informatinal only <abortable> <comments> Reasoncode and text provide more detail about specific problem. 1 - Tar not installed. 2 - Backup file does not exist after the tar operation. 3 - Specified file does not exist. 4 - backups.json is corrupted <comments> <payload> "reasonCode": an integer 1-4 "reasonText": a string description of the failure. <payload> """ self.log.error("Backups will not work, because tar does not appear to be installed!") self.log.error("If you are on a Linux-based system, please install it through your preferred package " "manager.") self.log.error("If you are on Windows, you can find GNU/Tar from this link: http://goo.gl/SpJSVM") return False else: return True def dobackup(self): self.log.debug("Backup starting.") self._settime() self._checkforbackupfolder() self._getbackups() # populate self.backups self._performbackup() self.log.debug("Backup cycle complete.") def _checkforbackupfolder(self): if not os.path.exists(self.config["Backups"]["backup-location"]): self.log.warning("Backup location %s does not exist -- creating target location...", self.config["Backups"]["backup-location"]) mkdir_p(self.config["Backups"]["backup-location"]) def _doserversaving(self, desiredstate=True): """ :param desiredstate: True = turn serversaving on False = turn serversaving off :return: Future expansion to allow config of server saving state glabally in config. Plan to include a glabal config option for periodic or continuous server disk saving of the minecraft server. """ if desiredstate: self.api.minecraft.console("save-all flush") # flush argument is required self.api.minecraft.console("save-on") else: self.api.minecraft.console("save-all flush") # flush argument is required self.api.minecraft.console("save-off") time.sleep(0.5) def _performbackup(self): timestamp = int(time.time()) # Turn off server saves... self._doserversaving(False) # Create tar arguments filename = "backup-%s.tar" % datetime.datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d_%H.%M.%S") if self.config["Backups"]["backup-compression"]: filename += ".gz" arguments = ["tar", "czf", "%s/%s" % (self.config["Backups"]["backup-location"].replace(" ", "\\ "), filename)] else: arguments = ["tar", "cfpv", "%s/%s" % (self.config["Backups"]["backup-location"], filename)] # Process begin Events if not self.wrapper.events.callevent("wrapper.backupBegin", {"file": filename}): self.log.warning("A backup was scheduled, but was cancelled by a plugin!") """ eventdoc <group> Backups <group> <description> Indicates a backup is being initiated. <description> <abortable> Yes, return False to abort. <abortable> <comments> A console warning will be issued if a plugin cancels the backup. <comments> <payload> "file": Name of backup file. <payload> """ return if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&cBacking up... lag may occur!", irc=False) # Do backups serverpath = self.config["General"]["server-directory"] for backupfile in self.config["Backups"]["backup-folders"]: backup_file_and_path = "%s/%s" % (serverpath, backupfile) if os.path.exists(backup_file_and_path): arguments.append(backup_file_and_path) else: self.log.warning("Backup file '%s' does not exist - canceling backup", backup_file_and_path) self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 3, "reasonText": "Backup file '%s' does not exist." % backup_file_and_path}) """ eventdoc <description> internalfunction <description> """ return statuscode = os.system(" ".join(arguments)) # TODO add a wrapper properties config item to set save mode of server # restart saves, call finish Events self._doserversaving() if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&aBackup complete!", irc=False) self.wrapper.events.callevent("wrapper.backupEnd", {"file": filename, "status": statuscode}) """ eventdoc <group> Backups <group> <description> Indicates a backup is complete. <description> <abortable> No - informational only <abortable> <comments> <comments> <payload> "file": Name of backup file. <payload> """ self.backups.append((timestamp, filename)) # Prune backups self.pruneoldbackups(filename) # Check for success if not os.path.exists(self.config["Backups"]["backup-location"] + "/" + filename): self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 2, "reasonText": "Backup file didn't exist after the tar " "command executed - assuming failure."}) """ eventdoc <description> internalfunction <description> """ def _getbackups(self): if len(self.backups) == 0 and os.path.exists(self.config["Backups"]["backup-location"] + "/backups.json"): loadcode = getjsonfile("backups", self.config["Backups"]["backup-location"], encodedas=self.encoding) if not loadcode: self.log.error("NOTE - backups.json was unreadable. It might be corrupted. Backups will no " "longer be automatically pruned.") self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 4, "reasonText": "backups.json is corrupted. Please contact an administer instantly, as this " "may be critical." }) """ eventdoc <description> internalfunction <description> """ self.backups = [] else: self.backups = loadcode else: if len(os.listdir(self.config["Backups"]["backup-location"])) > 0: # import old backups from previous versions of Wrapper.py backuptimestamps = [] for backupNames in os.listdir(self.config["Backups"]["backup-location"]): # noinspection PyBroadException,PyUnusedLocal try: backuptimestamps.append(int(backupNames[backupNames.find('-') + 1:backupNames.find('.')])) except Exception as e: pass backuptimestamps.sort() for backupI in backuptimestamps: self.backups.append((int(backupI), "backup-%s.tar" % str(backupI))) def _settime(self): self.time = time.time()
class Wrapper(object): def __init__(self, secret_passphrase): # setup log and config # needs a false setting on first in case config does not # load (like after changes). self.storage = False self.log = logging.getLogger('Wrapper.py') self.configManager = Config() self.configManager.loadconfig() self.config = self.configManager.config # Read Config items # hard coded cursor for non-readline mode self.cursor = ">" # This was to allow alternate encodings self.encoding = self.config["General"]["encoding"] self.serverpath = self.config["General"]["server-directory"] self.proxymode = self.config["Proxy"]["proxy-enabled"] self.wrapper_onlinemode = self.config["Proxy"]["online-mode"] self.halt_message = self.config["Misc"]["halt-message"] # encryption items (for passwords and sensitive user data) # salt is generated and stored in wrapper.properties.json config_changes = False salt = self.config["General"]["salt"] if not salt: salt = gensalt(self.encoding) self.config["General"]["salt"] = salt config_changes = True # passphrase is provided at startup by the wrapper operator or script (not stored) passphrase = phrase_to_url_safebytes(secret_passphrase, self.encoding, salt) self.cipher = Crypt(passphrase, self.encoding) # Update passwords (hash any plaintext passwords) for groups in self.config: for cfg_items in self.config[groups]: if cfg_items[-10:] == "-plaintext": # i.e., cfg_items ===> like ["web-password-plaintext"] hash_item = cfg_items[:-10] # hash_item ===> i.e., ["web-password"] if hash_item in self.config[groups] and self.config[groups][cfg_items]: # encrypt contents of (i.e.) ["web-password-plaintext"] hashed_item = self.cipher.encrypt( self.config[groups][cfg_items]) # store in "" ["Web"]["web-password"] self.config[groups][hash_item] = hashed_item # set plaintext item to false (successful digest) self.config[groups][cfg_items] = False config_changes = True # Patch any old update paths "..wrapper/development/build/version.json" # new paths are: "..wrapper/development" for entries in self.config["Updates"]: if "/build/version.json" in str(self.config["Updates"][entries]): oldentry = copy.copy(self.config["Updates"][entries]) self.config["Updates"][entries] = oldentry.split("/build/version.json")[0] config_changes = True # save changes made to config file if config_changes: self.configManager.save() # reload branch update info. self.auto_update_wrapper = self.config["Updates"]["auto-update-wrapper"] self.auto_update_branch = self.config["Updates"]["auto-update-branch"] if not self.auto_update_branch: self.update_url = "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/development" else: self.update_url = self.config["Updates"][self.auto_update_branch] self.use_timer_tick_event = self.config[ "Gameplay"]["use-timer-tick-event"] self.use_readline = not(self.config["Misc"]["use-betterconsole"]) # Storages self.wrapper_storage = Storage( "wrapper", encoding=self.encoding) self.wrapper_permissions = Storage( "permissions", encoding=self.encoding, pickle=False) self.wrapper_usercache = Storage( "usercache", encoding=self.encoding, pickle=False) # storage Data objects self.storage = self.wrapper_storage.Data self.usercache = self.wrapper_usercache.Data # self.wrapper_permissions accessed only by permissions module # core functions and datasets self.perms = Permissions(self) self.uuids = UUIDS(self.log, self.usercache) self.plugins = Plugins(self) self.commands = Commands(self) self.events = Events(self) self.players = {} self.registered_permissions = {} self.help = {} self.input_buff = "" self.sig_int = False self.command_hist = ['/help', 'help'] self.command_index = 1 # init items that are set up later (or opted out of/ not set up.) self.javaserver = None self.api = None self.irc = None self.scripts = None self.web = None self.proxy = None self.backups = None # HaltSig - Why? ... because if self.halt was just `False`, passing # a self.halt would simply be passing `False` (immutable). Changing # the value of self.halt would not necessarily change the value of the # passed parameter (unless it was specifically referenced back as # `wrapper.halt`). Since the halt signal needs to be passed, possibly # several layers deep, and into modules that it may be desireable to # not have direct access to wrapper, using a HaltSig object is # more desireable and reliable in behavior. self.halt = HaltSig() self.updated = False # future plan to expose this to api self.xplayer = ConsolePlayer(self) # Error messages for non-standard import failures. if not readline and self.use_readline: self.log.warning( "'readline' not imported. This is needed for proper" " console functioning. Press <Enter> to acknowledge...") sys.stdin.readline() # requests is just being used in too many places to try # and track its usages piece-meal. if not requests: self.log.error( "You must have the requests module installed to use wrapper!" " console functioning. Press <Enter> to Exit...") sys.stdin.readline() self._halt() # create server/proxy vitals and config objects self.servervitals = ServerVitals(self.players) # LETS TAKE A SECOND TO DISCUSS PLAYER OBJECTS: # The ServerVitals class gets passed the player object list now, but # player objects are now housed in wrapper. This is how we are # passing information between proxy and wrapper. self.servervitals.serverpath = self.config[ "General"]["server-directory"] self.servervitals.state = OFF self.servervitals.command_prefix = self.config[ "Misc"]["command-prefix"] self.proxyconfig = ProxyConfig() self.proxyconfig.proxy = self.config["Proxy"] self.proxyconfig.entity = self.config["Entities"] def __del__(self): """prevent error message on very first wrapper starts when wrapper exits after creating new wrapper.properties file. """ if self.storage: self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() def start(self): """wrapper execution starts here""" self.signals() self.backups = Backups(self) self.api = API(self, "Wrapper.py") self._registerwrappershelp() # This is not the actual server... the MCServer # class is a console wherein the server is started self.javaserver = MCServer(self, self.servervitals) self.javaserver.init() # load plugins self.plugins.loadplugins() if self.config["IRC"]["irc-enabled"]: # this should be a plugin self.irc = IRC(self.javaserver, self.log, self) t = threading.Thread(target=self.irc.init, args=()) t.daemon = True t.start() if self.config["Web"]["web-enabled"]: # this should be a plugin if manageweb.pkg_resources and manageweb.requests: self.log.warning( "Our apologies! Web mode is currently broken. Wrapper" " will start web mode anyway, but it will not likely " "function well (or at all). For now, you should turn " "off web mode in wrapper.properties.json.") self.web = manageweb.Web(self) t = threading.Thread(target=self.web.wrap, args=()) t.daemon = True t.start() else: self.log.error( "Web remote could not be started because you do not have" " the required modules installed: pkg_resources\n" "Hint: http://stackoverflow.com/questions/7446187") # Console Daemon runs while not wrapper.halt.halt consoledaemon = threading.Thread( target=self.parseconsoleinput, args=()) consoledaemon.daemon = True consoledaemon.start() # Timer runs while not wrapper.halt.halt ts = threading.Thread(target=self.event_timer_second, args=()) ts.daemon = True ts.start() if self.use_timer_tick_event: # Timer runs while not wrapper.halt.halt tt = threading.Thread(target=self.event_timer_tick, args=()) tt.daemon = True tt.start() if self.config["General"]["shell-scripts"]: if os.name in ("posix", "mac"): self.scripts = Scripts(self) else: self.log.error( "Sorry, but shell scripts only work on *NIX-based systems!" " If you are using a *NIX-based system, please file a " "bug report.") if self.proxymode: t = threading.Thread(target=self._startproxy, args=()) t.daemon = True t.start() if self.auto_update_wrapper: t = threading.Thread(target=self._auto_update_process, args=()) t.daemon = True t.start() self.javaserver.handle_server() # handle_server always runs, even if the actual server is not started self.plugins.disableplugins() self.log.info("Plugins disabled") self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() self.log.info("Wrapper Storages closed and saved.") # wrapper execution ends here. handle_server ends when # wrapper.halt.halt is True. if self.sig_int: self.log.info("Ending threads, please wait...") time.sleep(5) os.system("reset") def signals(self): signal.signal(signal.SIGINT, self.sigint) signal.signal(signal.SIGTERM, self.sigterm) # noinspection PyBroadException try: # lacking in Windows signal.signal(signal.SIGTSTP, self.sigtstp) except: pass def sigint(*args): self = args[0] # We are only interested in the self component self.log.info("Wrapper.py received SIGINT; halting...\n") self.sig_int = True self._halt() def sigterm(*args): self = args[0] # We are only interested in the self component self.log.info("Wrapper.py received SIGTERM; halting...\n") self._halt() def sigtstp(*args): # We are only interested in the 'self' component self = args[0] self.log.info("Wrapper.py received SIGTSTP; NO sleep support!" " Wrapper halting...\n") os.system("kill -CONT %d" % self.javaserver.proc.pid) self._halt() def _halt(self): self.javaserver.stop(self.halt_message, restart_the_server=False) self.halt.halt = True def shutdown(self): self._halt() def write_stdout(self, message="", source="print"): """ :param message: desired output line. Default is wrapper. :param source: "server", "wrapper", "print" or "log". Default is print. """ cursor = self.cursor if self.use_readline: print(message) return def _wrapper(msg): """_wrapper is normally displaying a live typing buffer. Therefore, there is no cr/lf at end because it is constantly being re-printed in the same spot as the user types.""" if msg != "": # re-print what the console user was typing right below that. # /wrapper commands receive special magenta coloring if msg[0:1] == '/': print("{0}{1}{2}{3}{4}{5}".format( UP_LINE, cursor, FG_YELLOW, msg, RESET, CLEAR_EOL)) else: print("{0}{1}{2}{3}".format( BACKSPACE, cursor, msg, CLEAR_EOL)) def _server(msg): # print server lines print("{0}{1}{2}\r\n".format(UP_LINE, CLEAR_LINE, msg, CLEAR_EOL)) def _print(msg): _server(msg) parse = { "server": _server, "wrapper": _wrapper, "print": _print, } # if this fails due to key error, we WANT that raised, as it is # a program code error, not a run-time error. parse[source](message) def getconsoleinput(self): """If wrapper is NOT using readline (self.use_readline == False), then getconsoleinput manually implements our own character reading, parsing, arrow keys, command history, etc. This is desireable because it allows the user to continue to see their input and modify it, even if the server is producing console line messages that would normally "carry away" the user's typing. Implemented in response to issue 326: 'Command being typed gets carried off by console every time server generates output #326' by @Darkness3840: https://github.com/benbaptist/minecraft-wrapper/issues/326 """ if self.use_readline: # Obtain a line of console input try: consoleinput = sys.stdin.readline().strip() except Exception as e: self.log.error( "[continue] variable 'consoleinput' in 'console()' did" " not evaluate \n%s" % e) consoleinput = "" else: arrow_index = 0 # working buffer allows arrow use to restore what they # were typing but did not enter as a command yet working_buff = '' while not self.halt.halt: keypress = readkey.getcharacter() keycode = readkey.convertchar(keypress) length = len(self.input_buff) if keycode == "right": arrow_index += 1 if arrow_index > length: arrow_index = length if keycode == "left": arrow_index -= 1 if arrow_index < 1: arrow_index = 0 if keycode == "up": # goes 'back' in command history time self.command_index -= 1 if self.command_index < 1: self.command_index = 0 self.input_buff = self.command_hist[self.command_index] arrow_index = len(self.input_buff) if keycode == "down": # goes forward in command history time self.command_index += 1 if self.command_index + 1 > len(self.command_hist): # These actions happen when at most recent typing self.command_index = len(self.command_hist) self.input_buff = '%s' % working_buff self.write_stdout( "%s " % self.input_buff, source="wrapper") arrow_index = len(self.input_buff) continue self.input_buff = self.command_hist[self.command_index] arrow_index = len(self.input_buff) buff_left = "%s" % self.input_buff[:arrow_index] buff_right = "%s" % self.input_buff[arrow_index:] if keycode == "backspace": if len(buff_left) > 0: buff_left = buff_left[:-1] self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff arrow_index -= 1 if keycode == "delete": if len(buff_right) > 0: buff_right = buff_right[1:] self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff if keycode in ("enter", "cr", "lf"): # scroll up (because cr is not added to buffer) # print("") break if keycode in ("ctrl-c", "ctrl-x"): self.sigterm() break # hide special key codes like PAGE_UP, etc if not used if not keycode: buff_left = "%s%s" % (buff_left, keypress) self.input_buff = "%s%s" % (buff_left, buff_right) working_buff = "%s" % self.input_buff arrow_index += 1 # with open('readout.txt', "w") as f: # f.write("left: '%s'\nright: '%s'\nbuff: '%s'" % ( # buff_left, buff_right, self.input_buff)) if len(buff_right) > 0: self.write_stdout("{0}{1}{2}{3}".format( REVERSED, buff_left, RESET, buff_right), "wrapper") else: self.write_stdout( "%s " % self.input_buff, source="wrapper") consoleinput = "%s" % self.input_buff self.input_buff = "" if consoleinput in self.command_hist: # if the command is already in the history somewhere, # remove it and re-append to the end (most recent) self.command_hist.remove(consoleinput) self.command_hist.append(consoleinput) else: # or just add it. self.command_hist.append(consoleinput) self.command_index = len(self.command_hist) # print the finished command to console self.write_stdout( "%s\r\n" % self.input_buff, source="wrapper") return consoleinput def parseconsoleinput(self): while not self.halt.halt: consoleinput = self.getconsoleinput() # No command (perhaps just a line feed or spaces?) if len(consoleinput) < 1: continue # for use with runwrapperconsolecommand() command wholecommandline = consoleinput[0:].split(" ") command = str(getargs(wholecommandline, 0)).lower() # this can be passed to runwrapperconsolecommand() command for args allargs = wholecommandline[1:] # Console only commands (not accessible in-game) if command in ("/halt", "halt"): self._halt() elif command in ("/stop", "stop"): self.javaserver.stop_server_command() # "kill" (with no slash) is a server command. elif command == "/kill": self.javaserver.kill("Server killed at Console...") elif command in ("/start", "start"): self.javaserver.start() elif command in ("/restart", "restart"): self.javaserver.restart() elif command in ("/update-wrapper", "update-wrapper"): self._checkforupdate(True) # "plugins" command (with no slash) reserved for server commands elif command == "/plugins": self.listplugins() elif command in ("/mem", "/memory", "mem", "memory"): self._memory() elif command in ("/raw", "raw"): self._raw(consoleinput) elif command in ("/freeze", "freeze"): self._freeze() elif command in ("/unfreeze", "unfreeze"): self._unfreeze() elif command == "/version": readout("/version", self.getbuildstring(), usereadline=self.use_readline) elif command in ("/mute", "/pause", "/cm", "/m", "/p"): self._mute_console(allargs) # Commands that share the commands.py in-game interface # "reload" (with no slash) may be used by bukkit servers elif command == "/reload": self.runwrapperconsolecommand("reload", []) # proxy mode ban system elif self.proxymode and command == "/ban": self.runwrapperconsolecommand("ban", allargs) elif self.proxymode and command == "/ban-ip": self.runwrapperconsolecommand("ban-ip", allargs) elif self.proxymode and command == "/pardon-ip": self.runwrapperconsolecommand("pardon-ip", allargs) elif self.proxymode and command == "/pardon": self.runwrapperconsolecommand("pardon", allargs) elif command in ("/perm", "/perms", "/super", "/permissions", "perm", "perms", "super", "permissions"): self.runwrapperconsolecommand("perms", allargs) elif command in ("/playerstats", "/stats", "playerstats", "stats"): self.runwrapperconsolecommand("playerstats", allargs) elif command in ("/ent", "/entity", "/entities", "ent", "entity", "entities"): self.runwrapperconsolecommand("ent", allargs) elif command in ("/config", "/con", "/prop", "/property", "/properties"): self.runwrapperconsolecommand("config", allargs) elif command in ("op", "/op"): self.runwrapperconsolecommand("op", allargs) elif command in ("deop", "/deop"): self.runwrapperconsolecommand("deop", allargs) elif command in ("pass", "/pass", "pw", "/pw", "password", "/password"): self.runwrapperconsolecommand("password", allargs) # TODO Add more commands below here, below the original items: # TODO __________________ # more commands here... # TODO __________________ # TODO add more commands above here, above the help-related items: # minecraft help command elif command == "help": readout("/help", "Get wrapper.py help.", separator=" (with a slash) - ", usereadline=self.use_readline) self.javaserver.console(consoleinput) # wrapper's help (console version) elif command == "/help": self._show_help_console() # wrapper ban help elif command == "/bans": self._show_help_bans() # Commmand not recognized by wrapper else: try: self.javaserver.console(consoleinput) except Exception as e: self.log.error("[BREAK] Console input exception" " (nothing passed to server) \n%s" % e) break continue def _registerwrappershelp(self): # All commands listed herein are accessible in-game # Also require player.isOp() new_usage = "<player> [-s SUPER-OP] [-o OFFLINE] [-l <level>]" self.api.registerHelp( "Wrapper", "Internal Wrapper.py commands ", [ ("/wrapper [update/memory/halt]", "If no subcommand is provided, it will" " show the Wrapper version.", None), ("/playerstats [all]", "Show the most active players. If no subcommand" " is provided, it'll show the top 10 players.", None), ("/plugins", "Show a list of the installed plugins", None), ("/reload", "Reload all plugins.", None), ("/op %s" % new_usage, "This and deop are Wrapper commands.", None), ("/permissions <groups/users/RESET>", "Command used to manage permission groups and" " users, add permission nodes, etc.", None), ("/entity <count/kill> [eid] [count]", "/entity help/? for more help.. ", None), ("/config", "Change wrapper.properties (type" " /config help for more..)", None), ("/password", "Sample usage: /pw IRC control-irc-pass <new" "password>", None), # Minimum server version for commands to appear is # 1.7.6 (registers perm later in serverconnection.py) # These won't appear if proxy mode is not on (since # serverconnection is part of proxy). ("/ban <name> [reason..] [d:<days>/h:<hours>]", "Ban a player. Specifying h:<hours> or d:<days>" " creates a temp ban.", "mc1.7.6"), ("/ban-ip <ip> [<reason..> <d:<number of days>]", "- Ban an IP address. Reason and days" " (d:) are optional.", "mc1.7.6"), ("/pardon <player> [False]", " - pardon a player. Default is byuuidonly." " To unban a specific " "name (without checking uuid), use `pardon" " <player> False`", "mc1.7.6"), ("/pardon-ip <address>", "Pardon an IP address.", "mc1.7.6"), ("/banlist [players|ips] [searchtext]", "search and display the banlist (warning -" " displays on single page!)", "mc1.7.6") ]) def runwrapperconsolecommand(self, wrappercommand, argslist): xpayload = { 'player': self.xplayer, 'command': wrappercommand, 'args': argslist } self.commands.playercommand(xpayload) def isonlinemode(self): """ :returns: Whether the server OR (for proxy mode) wrapper is in online mode. This should normally 'always' render True. Under rare circumstances it could be false, such as when this wrapper and its server are the target for a wrapper lobby with player.connect(). """ if self.proxymode: # if wrapper is using proxy mode (which should be set to online) return self.config["Proxy"]["online-mode"] if self.javaserver is not None: if self.servervitals.onlineMode: # if local server is online-mode return True return False def listplugins(self): readout("", "List of Wrapper.py plugins installed:", separator="", pad=4, usereadline=self.use_readline) for plid in self.plugins: plugin = self.plugins[plid] if plugin["good"]: name = plugin["name"] summary = plugin["summary"] if summary is None: summary = "No description available for this plugin" version = plugin["version"] readout(name, summary, separator=( " - v%s - " % ".".join([str(_) for _ in version])), usereadline=self.use_readline) else: readout("failed to load plugin", plugin, pad=25, usereadline=self.use_readline) def _start_emailer(self): alerts = self.config["Alerts"]["enabled"] if alerts: self.config["Alerts"] = "alerts true" def _startproxy(self): # error will raise if requests or cryptography is missing. self.proxy = Proxy(self.halt, self.proxyconfig, self.servervitals, self.log, self.usercache, self.events) # wait for server to start timer = 0 while self.servervitals.state < STARTED: timer += 1 time.sleep(.1) if timer > 1200: self.log.warning( "Proxy mode did not detect a started server within 2" " minutes. Disabling proxy mode because something is" " wrong.") self.disable_proxymode() return if self.proxy.proxy_port == self.servervitals.server_port: self.log.warning("Proxy mode cannot start because the wrapper" " port is identical to the server port.") self.disable_proxymode() return proxythread = threading.Thread(target=self.proxy.host, args=()) proxythread.daemon = True proxythread.start() def disable_proxymode(self): self.proxymode = False self.config["Proxy"]["proxy-enabled"] = False self.configManager.save() self.log.warning( "\nProxy mode is now turned off in wrapper.properties.json.\n") @staticmethod def getbuildstring(): if core_buildinfo_version.__branch__ == "dev": return "%s (development build #%d)" % ( core_buildinfo_version.__version__, core_buildinfo_version.__build__) elif core_buildinfo_version.__branch__ == "stable": return "%s (stable)" % core_buildinfo_version.__version__ else: return "Version: %s (%s build #%d)" % ( core_buildinfo_version.__version__, core_buildinfo_version.__branch__, core_buildinfo_version.__build__) def _auto_update_process(self): while not self.halt.halt: time.sleep(3600) if self.updated: self.log.info( "An update for wrapper has been loaded," " Please restart wrapper.") else: self._checkforupdate() def _checkforupdate(self, update_now=False): """ checks for update """ self.log.info("Checking for new builds...") update = self.get_wrapper_update_info() if update: version, build, repotype, reponame = update self.log.info( "New Wrapper.py %s build #%d is available!" " (current build is #%d)", repotype, build, core_buildinfo_version.__build__) if self.auto_update_wrapper or update_now: self.log.info("Updating...") self.performupdate(version, build) else: self.log.info( "Because you have 'auto-update-wrapper' set to False," " you must manually update Wrapper.py. To update" " Wrapper.py manually, please type /update-wrapper.") else: self.log.info("No new versions available.") def get_wrapper_update_info(self, repotype=None): """get the applicable branch wrapper update""" # read the installed branch info if repotype is None: repotype = core_buildinfo_version.__branch__ if self.auto_update_branch: branch_key = self.auto_update_branch else: branch_key = "%s-branch" % repotype r = requests.get("%s/build/version.json" % self.config["Updates"][branch_key]) if r.status_code == 200: data = r.json() if data["__build__"] > core_buildinfo_version.__build__: if repotype == "dev": reponame = "development" elif repotype == "stable": reponame = "master" else: reponame = data["__branch__"] if "__version__" not in data: data["__version__"] = data["version"] return data["__version__"], data["__build__"], data["__branch__"], reponame else: self.log.warning( "Failed to check for updates - are you connected to the" " internet? (Status Code %d)", r.status_code) return False def performupdate(self, version, build): """ Perform update; returns True if update succeeds. User must still restart wrapper manually. :param version: first argument from get_wrapper_update_info() :param build: second argument from get_wrapper_update_info() :return: True if update succeeds """ wraphash = requests.get("%s/build/Wrapper.py.md5" % self.update_url) wrapperfile = requests.get("%s/Wrapper.py" % self.update_url) if wraphash.status_code == 200 and wrapperfile.status_code == 200: self.log.info("Verifying Wrapper.py...") if hashlib.md5(wrapperfile.content).hexdigest() == wraphash.text: self.log.info( "Update file successfully verified. Installing...") with open(sys.argv[0], "wb") as f: f.write(wrapperfile.content) self.log.info( "Wrapper.py %s (#%d) installed. Please reboot Wrapper.py.", ".".join([str(_) for _ in version]), build) self.updated = True return True else: return False else: self.log.error( "Failed to update due to an internal error (%d, %d)", wraphash.status_code, wrapperfile.status_code, exc_info=True) return False def event_timer_second(self): while not self.halt.halt: time.sleep(1) self.events.callevent("timer.second", None) """ eventdoc <group> wrapper <group> <description> a timer that is called each second. <description> <abortable> No <abortable> """ def event_timer_tick(self): while not self.halt.halt: self.events.callevent("timer.tick", None) time.sleep(0.05) """ eventdoc <group> wrapper <group> <description> a timer that is called each 1/20th <sp> of a second, like a minecraft tick. <description> <abortable> No <abortable> <comments> Use of this timer is not suggested and is turned off <sp> by default in the wrapper.config.json file <comments> """ def _pause_console(self, pause_time): if not self.javaserver: readout("ERROR - ", "There is no running server instance to mute.", separator="", pad=10, usereadline=self.use_readline) return self.javaserver.server_muted = True readout("Server is now muted for %d seconds." % pause_time, "", separator="", command_text_fg="yellow", usereadline=self.use_readline) time.sleep(pause_time) readout("Server now unmuted.", "", separator="", usereadline=self.use_readline) self.javaserver.server_muted = False for lines in self.javaserver.queued_lines: readout("Q\\", "", lines, pad=3, usereadline=self.use_readline) time.sleep(.1) self.javaserver.queued_lines = [] def _mute_console(self, all_args): pausetime = 30 if len(all_args) > 0: pausetime = get_int(all_args[0]) # spur off a pause thread cm = threading.Thread(target=self._pause_console, args=(pausetime,)) cm.daemon = True cm.start() def _freeze(self): try: self.javaserver.freeze() except OSError as ex: self.log.error(ex) except EnvironmentError as e: self.log.warning(e) except Exception as exc: self.log.exception( "Something went wrong when trying to freeze the" " server! (%s)", exc) def _memory(self): try: get_bytes = self.javaserver.getmemoryusage() except OSError as e: self.log.error(e) except Exception as ex: self.log.exception( "Something went wrong when trying to fetch" " memory usage! (%s)", ex) else: amount, units = format_bytes(get_bytes) self.log.info( "Server Memory Usage: %s %s (%s bytes)" % ( amount, units, get_bytes)) def _raw(self, console_input): try: if len(getargsafter(console_input[1:].split(" "), 1)) > 0: self.javaserver.console( getargsafter(console_input[1:].split(" "), 1)) else: self.log.info("Usage: /raw [command]") except EnvironmentError as e: self.log.warning(e) def _unfreeze(self): try: self.javaserver.unfreeze() except OSError as ex: self.log.error(ex) except EnvironmentError as e: self.log.warning(e) except Exception as exc: self.log.exception( "Something went wrong when trying to unfreeze" " the server! (%s)", exc) def _show_help_console(self): # This is the console help command display. readout("", "Get Minecraft help.", separator="help (no slash) - ", pad=0, usereadline=self.use_readline) readout("/reload", "Reload Wrapper.py plugins.", usereadline=self.use_readline) readout("/plugins", "Lists Wrapper.py plugins.", usereadline=self.use_readline) readout("/update-wrapper", "Checks for new Wrapper.py updates, and will install\n" "them automatically if one is available.", usereadline=self.use_readline) readout("/stop", "Stop the minecraft server without" " auto-restarting and without\n" " shuttingdown Wrapper.py.", usereadline=self.use_readline) readout("/start", "Start the minecraft server.", usereadline=self.use_readline) readout("/restart", "Restarts the minecraft server.", usereadline=self.use_readline) readout("/halt", "Shutdown Wrapper.py completely.", usereadline=self.use_readline) readout("/cm [seconds]", "Mute server output (Wrapper console" " logging still happens)", usereadline=self.use_readline) readout("/kill", "Force kill the server without saving.", usereadline=self.use_readline) readout("/freeze", "Temporarily locks the server up" " until /unfreeze is executed\n" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/unfreeze", "Unlocks a frozen state server" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/mem", "Get memory usage of the server" " (Only works on *NIX servers).", usereadline=self.use_readline) readout("/raw [command]", "Send command to the Minecraft" " Server. Useful for Forge\n" " commands like '/fml confirm'.", usereadline=self.use_readline) readout("/password", "run `/password help` for more...)", usereadline=self.use_readline) readout("/perms", "/perms for more...)", usereadline=self.use_readline) readout("/config", "Change wrapper.properties (type" " /config help for more..)", usereadline=self.use_readline) readout("/version", self.getbuildstring(), usereadline=self.use_readline) readout("/entity", "Work with entities (run /entity for more...)", usereadline=self.use_readline) readout("/bans", "Display the ban help page.", usereadline=self.use_readline) def _show_help_bans(self): # ban commands help. if not self.proxymode: readout( "ERROR - ", "Wrapper proxy-mode bans are not enabled " "(proxy mode is not on).", separator="", pad=10, usereadline=self.use_readline) return readout( "", "Bans - To use the server's versions, do not type a slash.", separator="", pad=5, usereadline=self.use_readline) readout( "", "", separator="-----1.7.6 and later ban commands-----", pad=10, usereadline=self.use_readline) readout( "/ban", " - Ban a player. Specifying h:<hours> or d:<days>" " creates a temp ban.", separator="<name> [reason..] [d:<days>/h:<hours>] ", pad=12, usereadline=self.use_readline) readout( "/ban-ip", " - Ban an IP address. Reason and days (d:) are optional.", separator="<ip> [<reason..> <d:<number of days>] ", pad=12, usereadline=self.use_readline) readout( "/pardon", " - pardon a player. Default is byuuidonly. To unban a" "specific name (without checking uuid), use" " `pardon <player> False`", separator="<player> [byuuidonly(true/false)]", pad=12, usereadline=self.use_readline) readout( "/pardon-ip", " - Pardon an IP address.", separator="<address> ", pad=12, usereadline=self.use_readline) readout( "/banlist", " - search and display the banlist (warning -" " displays on single page!)", separator="[players|ips] [searchtext] ", pad=12, usereadline=self.use_readline)
def start(self): """wrapper execution starts here""" self.signals() self.backups = Backups(self) self.api = API(self, "Wrapper.py") self._registerwrappershelp() # This is not the actual server... the MCServer # class is a console wherein the server is started self.javaserver = MCServer(self, self.servervitals) self.javaserver.init() # load plugins self.plugins.loadplugins() if self.config["IRC"]["irc-enabled"]: # this should be a plugin self.irc = IRC(self.javaserver, self.log, self) t = threading.Thread(target=self.irc.init, args=()) t.daemon = True t.start() if self.config["Web"]["web-enabled"]: # this should be a plugin if manageweb.pkg_resources and manageweb.requests: self.log.warning( "Our apologies! Web mode is currently broken. Wrapper" " will start web mode anyway, but it will not likely " "function well (or at all). For now, you should turn " "off web mode in wrapper.properties.json.") self.web = manageweb.Web(self) t = threading.Thread(target=self.web.wrap, args=()) t.daemon = True t.start() else: self.log.error( "Web remote could not be started because you do not have" " the required modules installed: pkg_resources\n" "Hint: http://stackoverflow.com/questions/7446187") # Console Daemon runs while not wrapper.halt.halt consoledaemon = threading.Thread( target=self.parseconsoleinput, args=()) consoledaemon.daemon = True consoledaemon.start() # Timer runs while not wrapper.halt.halt ts = threading.Thread(target=self.event_timer_second, args=()) ts.daemon = True ts.start() if self.use_timer_tick_event: # Timer runs while not wrapper.halt.halt tt = threading.Thread(target=self.event_timer_tick, args=()) tt.daemon = True tt.start() if self.config["General"]["shell-scripts"]: if os.name in ("posix", "mac"): self.scripts = Scripts(self) else: self.log.error( "Sorry, but shell scripts only work on *NIX-based systems!" " If you are using a *NIX-based system, please file a " "bug report.") if self.proxymode: t = threading.Thread(target=self._startproxy, args=()) t.daemon = True t.start() if self.auto_update_wrapper: t = threading.Thread(target=self._auto_update_process, args=()) t.daemon = True t.start() self.javaserver.handle_server() # handle_server always runs, even if the actual server is not started self.plugins.disableplugins() self.log.info("Plugins disabled") self.wrapper_storage.close() self.wrapper_permissions.close() self.wrapper_usercache.close() self.log.info("Wrapper Storages closed and saved.") # wrapper execution ends here. handle_server ends when # wrapper.halt.halt is True. if self.sig_int: self.log.info("Ending threads, please wait...") time.sleep(5) os.system("reset")
class Web(object): def __init__(self, wrapper): self.wrapper = wrapper self.api = API(wrapper, "Web", internal=True) self.log = logging.getLogger('Web') self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.socket = False self.data = Storage("web") self.pass_handler = self.wrapper.cipher if "keys" not in self.data.Data: self.data.Data["keys"] = [] self.api.registerEvent("server.consoleMessage", self.onServerConsole) self.api.registerEvent("player.message", self.onPlayerMessage) self.api.registerEvent("player.login", self.onPlayerJoin) self.api.registerEvent("player.logout", self.onPlayerLeave) self.api.registerEvent("irc.message", self.onChannelMessage) self.consoleScrollback = [] self.chatScrollback = [] self.memoryGraph = [] self.loginAttempts = 0 self.lastAttempt = 0 self.disableLogins = 0 # t = threading.Thread(target=self.updateGraph, args=()) # t.daemon = True # t.start() def __del__(self): self.data.close() def onServerConsole(self, payload): while len(self.consoleScrollback) > 1000: try: del self.consoleScrollback[0] except Exception as e: break self.consoleScrollback.append((time.time(), payload["message"])) def onPlayerMessage(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "player", "payload": { "player": payload["player"].username, "message": payload["message"] } })) def onPlayerJoin(self, payload): # print(payload) while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "playerJoin", "payload": { "player": payload["player"].username } })) def onPlayerLeave(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "playerLeave", "payload": { "player": payload["player"] } })) def onChannelMessage(self, payload): while len(self.chatScrollback) > 200: try: del self.chatScrollback[0] except Exception as e: break self.chatScrollback.append((time.time(), { "type": "irc", "payload": payload })) def updateGraph(self): while not self.wrapper.halt.halt: while len(self.memoryGraph) > 200: del self.memoryGraph[0] if self.wrapper.javaserver.getmemoryusage(): self.memoryGraph.append( [time.time(), self.wrapper.javaserver.getmemoryusage()]) time.sleep(1) def checkLogin(self, password): if time.time() - self.disableLogins < 60: return False # Threshold for logins if self.pass_handler.check_pw(password, self.config["Web"]["web-password"]): return True self.loginAttempts += 1 if self.loginAttempts > 10 and time.time() - self.lastAttempt < 60: self.disableLogins = time.time() self.log.warning("Disabled login attempts for one minute") self.lastAttempt = time.time() def makeKey(self, rememberme): a = "" z = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@-_" for i in range(64): # not enough performance issue to justify xrange a += z[random.randrange(0, len(z))] # a += chr(random.randrange(97, 122)) if rememberme: print("Will remember!") self.data.Data["keys"].append([a, time.time(), rememberme]) return a def validateKey(self, key): for i in self.data.Data["keys"]: expiretime = 2592000 if len(i) > 2: if not i[2]: expiretime = 21600 # Validate key and ensure it's under a week old if i[0] == key and time.time() - i[1] < expiretime: self.loginAttempts = 0 return True return False def removeKey(self, key): # we dont want to do things like this. Never delete or insert while iterating over a dictionary # because dictionaries change order as the hashtables are changed during insert and delete operations... for i, v in enumerate(self.data.Data["keys"]): if v[0] == key: del self.data.Data["keys"][i] def wrap(self): while not self.wrapper.halt.halt: try: if self.bind(): self.listen() else: self.log.error( "Could not bind web to %s:%d - retrying in 5 seconds", self.config["Web"]["web-bind"], self.config["Web"]["web-port"]) except Exception as e: self.log.exception(e) time.sleep(5) def bind(self): if self.socket is not False: self.socket.close() try: self.socket = socket.socket() self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind((self.config["Web"]["web-bind"], self.config["Web"]["web-port"])) self.socket.listen(5) return True except Exception as e: return False def listen(self): self.log.info("Web Interface bound to %s:%d", self.config["Web"]["web-bind"], self.config["Web"]["web-port"]) while not self.wrapper.halt.halt: # noinspection PyUnresolvedReferences sock, addr = self.socket.accept() # self.log.debug("(WEB) Connection %s started", str(addr)) client = WebClient(sock, addr, self) t = threading.Thread(target=client.wrap, args=()) t.daemon = True t.start()
def __init__(self, wrapper, servervitals): self.log = wrapper.log self.config = wrapper.config self.vitals = servervitals self.encoding = self.config["General"]["encoding"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config["General"]["timed-reboot-minutes"] self.reboot_warn_minutes = self.config["General"]["timed-reboot-warning-minutes"] # noqa # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.vitals.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.lastsizepoll = 0 self.console_output_data = [] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot", "slain"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.world = None # get OPs self.refresh_ops() # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"]: rb = threading.Thread(target=self.reboot_timer, args=()) rb.daemon = True rb.start() if self.config["Web"]["web-enabled"]: wb = threading.Thread(target=self.eachsecond_web, args=()) wb.daemon = True wb.start() # This event is used to allow proxy to make console commands via # callevent() without referencing mcserver.py code (the eventhandler # is passed as an argument to the proxy). self.api.registerEvent("proxy.console", self._console_event)
class Backups(object): def __init__(self, wrapper): self.wrapper = wrapper self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.log = wrapper.log self.api = API(wrapper, "Backups", internal=True) self.interval = 0 self.backup_interval = self.config["Backups"]["backup-interval"] self.time = time.time() self.backups = [] self.enabled = self.config["Backups"]["enabled"] # allow plugins to shutdown backups via api self.timerstarted = False if self.enabled and self.dotarchecks(): # only register event if used and tar installed! self.api.registerEvent("timer.second", self.eachsecond) self.timerstarted = True self.log.debug("Backups Enabled..") # noinspection PyUnusedLocal def eachsecond(self, payload): self.interval += 1 if time.time() - self.time > self.backup_interval and self.enabled: self.dobackup() def pruneoldbackups(self, filename="IndependentPurge"): if len(self.backups) > self.config["Backups"]["backups-keep"]: self.log.info("Deleting old backups...") while len(self.backups) > self.config["Backups"]["backups-keep"]: backup = self.backups[0] if not self.wrapper.events.callevent("wrapper.backupDelete", {"file": filename}): break try: os.remove('%s/%s' % (self.config["Backups"]["backup-location"], backup[1])) except Exception as e: self.log.error("Failed to delete backup (%s)", e) self.log.info("Deleting old backup: %s", datetime.datetime.fromtimestamp(int(backup[0])).strftime('%Y-%m-%d_%H:%M:%S')) # hink = self.backups[0][1][:] # not used... del self.backups[0] putjsonfile(self.backups, "backups", self.config["Backups"]["backup-location"]) def dotarchecks(self): # Check if tar is installed which = "where" if platform.system() == "Windows" else "which" if not subprocess.call([which, "tar"]) == 0: self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 1, "reasonText": "Tar is not installed. Please install " "tar before trying to make backups."}) self.log.error("Backups will not work, because tar does not appear to be installed!") self.log.error("If you are on a Linux-based system, please install it through your preferred package " "manager.") self.log.error("If you are on Windows, you can find GNU/Tar from this link: http://goo.gl/SpJSVM") return False else: return True def dobackup(self): self.log.debug("Backup starting.") self._settime() self._checkforbackupfolder() self._getbackups() # populate self.backups self._performbackup() self.log.debug("Backup cycle complete.") def _checkforbackupfolder(self): if not os.path.exists(self.config["Backups"]["backup-location"]): self.log.warning("Backup location %s does not exist -- creating target location...", self.config["Backups"]["backup-location"]) mkdir_p(self.config["Backups"]["backup-location"]) def _doserversaving(self, desiredstate=True): """ :param desiredstate: True = turn serversaving on False = turn serversaving off :return: Future expansion to allow config of server saving state glabally in config. Plan to include a glabal config option for periodic or continuous server disk saving of the minecraft server. """ if desiredstate: self.api.minecraft.console("save-all flush") # flush argument is required self.api.minecraft.console("save-on") else: self.api.minecraft.console("save-all flush") # flush argument is required self.api.minecraft.console("save-off") time.sleep(0.5) def _performbackup(self): timestamp = int(time.time()) # Turn off server saves... self._doserversaving(False) # Create tar arguments filename = "backup-%s.tar" % datetime.datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d_%H.%M.%S") if self.config["Backups"]["backup-compression"]: filename += ".gz" arguments = ["tar", "czf", "%s/%s" % (self.config["Backups"]["backup-location"].replace(" ", "\\ "), filename)] else: arguments = ["tar", "cfpv", "%s/%s" % (self.config["Backups"]["backup-location"], filename)] # Process begin Events if not self.wrapper.events.callevent("wrapper.backupBegin", {"file": filename}): self.log.warning("A backup was scheduled, but was cancelled by a plugin!") return if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&cBacking up... lag may occur!", irc=False) # Do backups serverpath = self.config["General"]["server-directory"] for backupfile in self.config["Backups"]["backup-folders"]: backup_file_and_path = "%s/%s" % (serverpath, backupfile) if os.path.exists(backup_file_and_path): arguments.append(backup_file_and_path) else: self.log.warning("Backup file '%s' does not exist - canceling backup", backup_file_and_path) self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 3, "reasonText": "Backup file '%s' does not exist." % backup_file_and_path}) return statuscode = os.system(" ".join(arguments)) # TODO add a wrapper properties config item to set save mode of server # restart saves, call finish Events self._doserversaving() if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&aBackup complete!", irc=False) self.wrapper.events.callevent("wrapper.backupEnd", {"file": filename, "status": statuscode}) self.backups.append((timestamp, filename)) # Prune backups self.pruneoldbackups(filename) # Check for success if not os.path.exists(self.config["Backups"]["backup-location"] + "/" + filename): self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 2, "reasonText": "Backup file didn't exist after the tar " "command executed - assuming failure."}) def _getbackups(self): if len(self.backups) == 0 and os.path.exists(self.config["Backups"]["backup-location"] + "/backups.json"): loadcode = getjsonfile("backups", self.config["Backups"]["backup-location"], encodedas=self.encoding) if not loadcode: self.log.error("NOTE - backups.json was unreadable. It might be corrupted. Backups will no " "longer be automatically pruned.") self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 4, "reasonText": "backups.json is corrupted. Please contact an administer instantly, as this " "may be critical." }) self.backups = [] else: self.backups = loadcode else: if len(os.listdir(self.config["Backups"]["backup-location"])) > 0: # import old backups from previous versions of Wrapper.py backuptimestamps = [] for backupNames in os.listdir(self.config["Backups"]["backup-location"]): # noinspection PyBroadException,PyUnusedLocal try: backuptimestamps.append(int(backupNames[backupNames.find('-') + 1:backupNames.find('.')])) except Exception as e: pass backuptimestamps.sort() for backupI in backuptimestamps: self.backups.append((int(backupI), "backup-%s.tar" % str(backupI))) def _settime(self): self.time = time.time()
class MCServer(object): def __init__(self, wrapper): self.log = wrapper.log self.config = wrapper.config self.serverpath = self.config["General"]["server-directory"] self.state = OFF self.properties = {} self.worldname = None self.worldsize = 0 # owner/op info self.ownernames = {} self.operator_list = [] self.spammy_stuff = ["found nothing", "vehicle of", "Wrong location!", "Tried to add entity", ] # this is string name of the version, collected by console output self.version = "" self.version_compute = 0 self.servericon = None self.motd = None self.timeofday = -1 self.protocolVersion = -1 self.server_port = "25564" self.encoding = self.config["General"]["encoding"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config["General"]["timed-reboot-minutes"] self.reboot_warn_minutes = self.config["General"]["timed-reboot-warning-minutes"] # noqa # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.lastsizepoll = 0 self.console_output_data = [] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot", "slain"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.world = None # get OPs self.refresh_ops() # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"]: rb = threading.Thread(target=self.reboot_timer, args=()) rb.daemon = True rb.start() if self.config["Web"]["web-enabled"]: wb = threading.Thread(target=self.eachsecond_web, args=()) wb.daemon = True wb.start() # This event is used to allow proxy to make console commands via # callevent() without referencing mcserver.py code (the eventhandler # is passed as an argument to the proxy). self.api.registerEvent("proxy.console", self._console_event) def init(self): """ Start up the listen threads for reading server console output. """ capturethread = threading.Thread(target=self.__stdout__, args=()) capturethread.daemon = True capturethread.start() capturethread = threading.Thread(target=self.__stderr__, args=()) capturethread.daemon = True capturethread.start() def __del__(self): self.state = 0 def accepteula(self): if os.path.isfile("%s/eula.txt" % self.serverpath): self.log.debug("Checking EULA agreement...") with open("%s/eula.txt" % self.serverpath) as f: eula = f.read() # if forced, should be at info level since acceptance # is a legal matter. if "eula=false" in eula: self.log.warning( "EULA agreement was not accepted, accepting on" " your behalf...") set_item("eula", "true", "eula.txt", self.serverpath) self.log.debug("EULA agreement has been accepted.") return True else: return False def handle_server(self): """ Function that handles booting the server, parsing console output, and such. """ trystart = 0 while not self.wrapper.haltsig.halt: trystart += 1 self.proc = None # endless loop for not booting the server (while still # allowing handle to run). if not self.boot_server: time.sleep(0.2) trystart = 0 continue self.changestate(STARTING) self.log.info("Starting server...") self.reloadproperties() command = self.args self.proc = subprocess.Popen( command, cwd=self.serverpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) self.wrapper.players = {} self.accepteula() # Auto accept eula if self.proc.poll() is None and trystart > 3: self.log.error( "Could not start server. check your server.properties," " wrapper.properties and this your startup 'command'" " from wrapper.properties:\n'%s'", " ".join(self.args)) self.changestate(OFF) # halt wrapper self.wrapper.haltsig.halt = True # exit server_handle break # The server loop while True: # Loop runs continously as long as server console is running time.sleep(0.1) if self.proc.poll() is not None: self.changestate(OFF) trystart = 0 self.boot_server = self.server_autorestart # break out to `while not self.wrapper.haltsig.halt:` loop # to (possibly) connect to server again. break # is is only reading server console output for line in self.console_output_data: try: self.readconsole(line.replace("\r", "")) except Exception as e: self.log.exception(e) self.console_output_data = [] # code ends here on wrapper.haltsig.halt and execution returns to # the end of wrapper.start() def _toggle_server_started(self, server_started=True): self.wrapper.storage["ServerStarted"] = server_started self.wrapper.wrapper_storage.save() def start(self): """ Start the Minecraft server """ self.server_autorestart = self.config["General"]["auto-restart"] if self.state in (STARTED, STARTING): self.log.warning("The server is already running!") return if not self.boot_server: self.boot_server = True else: self.handle_server() self._toggle_server_started() def restart(self, reason=""): """Restart the Minecraft server, and kick people with the specified reason. If server was already stopped, restart it. """ if reason == "": reason = self.restart_message if self.state in (STOPPING, OFF): self.start() return self.doserversaving() self.stop(reason) def kick_players(self, reasontext): playerlist = copy.copy(self.wrapper.players) for player in playerlist: self.kick_player(player, reasontext) def kick_player(self, player, reasontext): if self.wrapper.proxymode: try: playerclient = self.wrapper.players[player].client playerclient.notify_disconnect(reasontext) except AttributeError: self.log.warning( "Proxy kick failed - Gould not get client %s.\n" "I'll try using the console..", player) self.console("kick %s %s" % (player, reasontext)) except KeyError: self.log.warning( "Kick failed - No player called %s", player) except Exception as e: self.log.warning( "Kick failed - something else went wrong:" " %s\n%s", player, e,) else: self.console("kick %s %s" % (player, reasontext)) # this sleep is here for Spigot McBans reasons/compatibility. time.sleep(2) def stop(self, reason="", restart_the_server=True): """Stop the Minecraft server from an automatic process. Allow it to restart by default. """ self.doserversaving() self.log.info("Stopping Minecraft server with reason: %s", reason) self.kick_players(reason) self.changestate(STOPPING, reason) self.console("stop") # False will allow this loop to run with no server (and # reboot if permitted). self.boot_server = restart_the_server def stop_server_command(self, reason="", restart_the_server=False): """ Stop the Minecraft server (as a command). By default, do not restart. """ if reason == "": reason = self.stop_message if self.state == OFF: self.log.warning("The server is not running... :?") return if self.state == FROZEN: self.log.warning("The server is currently frozen.\n" "To stop it, you must /unfreeze it first") return self.server_autorestart = False self.stop(reason, restart_the_server) self._toggle_server_started(restart_the_server) def kill(self, reason="Killing Server"): """Forcefully kill the server. It will auto-restart if set in the configuration file. """ if self.state in (STOPPING, OFF): self.log.warning("The server is already dead, my friend...") return self.log.info("Killing Minecraft server with reason: %s", reason) self.changestate(OFF, reason) self.proc.kill() def freeze(self, reason="Server is now frozen. You may disconnect."): """Freeze the server with `kill -STOP`. Can be used to stop the server in an emergency without shutting it down, so it doesn't write corrupted data - e.g. if the disk is full, you can freeze the server, free up some disk space, and then unfreeze 'reason' argument is printed in the chat for all currently-connected players, unless you specify None. This command currently only works for *NIX based systems. """ if self.state != OFF: if os.name == "posix": self.log.info("Freezing server with reason: %s", reason) self.broadcast("&c%s" % reason) time.sleep(0.5) self.changestate(FROZEN) os.system("kill -STOP %d" % self.proc.pid) else: raise OSError( "Your current OS (%s) does not support this" " command at this time." % os.name) else: raise EnvironmentError( "Server is not started. You may run '/start' to boot it up.") def unfreeze(self): """Unfreeze the server with `kill -CONT`. Counterpart to .freeze(reason) This command currently only works for *NIX based systems. """ if self.state != OFF: if os.name == "posix": self.log.info("Unfreezing server (ignore any" " messages to type /start)...") self.broadcast("&aServer unfrozen.") self.changestate(STARTED) os.system("kill -CONT %d" % self.proc.pid) else: raise OSError( "Your current OS (%s) does not support this command" " at this time." % os.name) else: raise EnvironmentError( "Server is not started. Please run '/start' to boot it up.") def broadcast(self, message, who="@a"): """Broadcasts the specified message to all clients connected. message can be a JSON chat object, or a string with formatting codes using the § as a prefix. """ if isinstance(message, dict): if self.version_compute < 10700: self.console("say %s %s" % (who, chattocolorcodes(message))) else: encoding = self.wrapper.encoding self.console("tellraw %s %s" % ( who, json.dumps(message, ensure_ascii=False))) else: temp = processcolorcodes(message) if self.version_compute < 10700: temp = processcolorcodes(message) self.console("say %s %s" % ( who, chattocolorcodes(temp))) else: self.console("tellraw %s %s" % ( who, json.dumps(processcolorcodes(message)))) def login(self, username, servereid, position, ipaddr): """Called when a player logs in.""" if username not in self.wrapper.players: self.wrapper.players[username] = Player(username, self.wrapper) # store EID if proxy is not fully connected yet (or is not enabled). self.wrapper.players[username].playereid = servereid self.wrapper.players[username].loginposition = position if self.wrapper.players[username].ipaddress == "127.0.0.0": self.wrapper.players[username].ipaddress = ipaddr if self.wrapper.proxy and self.wrapper.players[username].client: self.wrapper.players[username].client.server_eid = servereid self.wrapper.players[username].client.position = position # activate backup status self.wrapper.backups.idle = False player = self.getplayer(username) # proxy will handle the login event if enabled if player and player.client: return self.wrapper.events.callevent( "player.login", {"player": player, "playername": username}, abortable=False ) """ eventdoc <group> core/mcserver.py <group> <description> When player logs into the java MC server. <description> <abortable> No <abortable> <comments> All events in the core/mcserver.py group are collected from the console output, do not require proxy mode, and therefore, also, cannot be aborted. <comments> <payload> "player": player object (if object available -could be False if not) "playername": user name of player (string) <payload> """ def logout(self, players_name): """Called when a player logs out.""" if players_name in self.wrapper.players: player = self.wrapper.players[players_name] self.wrapper.events.callevent( "player.logout", {"player": player, "playername": players_name}, abortable=True ) """ eventdoc <group> core/mcserver.py <group> <description> When player logs out of the java MC server. <description> <abortable> No - but This will pause long enough for you to deal with the playerobject. <abortable> <comments> All events in the core/mcserver.py group are collected from the console output, do not require proxy mode, and therefore, also, cannot be aborted. <comments> <payload> "player": player object (if object available -could be False if not) "playername": user name of player (string) <payload> """ # noqa if player.client is None: player.abort = True del self.wrapper.players[players_name] elif player.client.state != LOBBY and player.client.local: player.abort = True del self.wrapper.players[players_name] if self.wrapper.proxy: self.wrapper.proxy.removestaleclients() if len(self.wrapper.players) == 0: self.wrapper.backups.idle = True def getplayer(self, username): """Returns a player object with the specified name, or False if the user is not logged in/doesn't exist. this getplayer only deals with local players on this server. api.minecraft.getPlayer will deal in all players, including those in proxy and/or other hub servers. """ if username in self.wrapper.players: player = self.wrapper.players[username] if player.client and player.client.state != LOBBY and player.client.local: # noqa return player elif not self.wrapper.proxymode: return player return False def reloadproperties(self): # Read server.properties and extract some information out of it # the PY3.5 ConfigParser seems broken. This way was much more # straightforward and works in both PY2 and PY3 # Load server icon if os.path.exists("%s/server-icon.png" % self.serverpath): with open("%s/server-icon.png" % self.serverpath, "rb") as f: theicon = f.read() iconencoded = base64.standard_b64encode(theicon) self.servericon = b"data:image/png;base64," + iconencoded self.properties = config_to_dict_read( "server.properties", self.serverpath) if self.properties == {}: self.log.warning("File 'server.properties' not found.") return False if "level-name" in self.properties: self.worldname = self.properties["level-name"] else: self.log.warning("No 'level-name=(worldname)' was" " found in the server.properties.") return False self.motd = self.properties["motd"] def console(self, command): """Execute a console command on the server.""" if self.state in (STARTING, STARTED, STOPPING) and self.proc: self.proc.stdin.write("%s\n" % command) self.proc.stdin.flush() else: self.log.debug("Attempted to run console command" " '%s' but the Server is not started.", command) def changestate(self, state, reason=None): """Change the boot state indicator of the server, with a reason message. """ self.state = state if self.state == OFF: self.wrapper.events.callevent( "server.stopped", {"reason": reason}, abortable=False) elif self.state == STARTING: self.wrapper.events.callevent( "server.starting", {"reason": reason}, abortable=False) elif self.state == STARTED: self.wrapper.events.callevent( "server.started", {"reason": reason}, abortable=False) elif self.state == STOPPING: self.wrapper.events.callevent( "server.stopping", {"reason": reason}, abortable=False) self.wrapper.events.callevent( "server.state", {"state": state, "reason": reason}, abortable=False) def doserversaving(self, desiredstate=True): """ :param desiredstate: True = turn serversaving on False = turn serversaving off :return: Future expansion to allow config of server saving state glabally in config. Plan to include a global config option for periodic or continuous server disk saving of the minecraft server. """ if desiredstate: self.console("save-all flush") # flush argument is required self.console("save-on") else: self.console("save-all flush") # flush argument is required self.console("save-off") time.sleep(1) def getservertype(self): if "spigot" in self.config["General"]["command"].lower(): return "spigot" elif "bukkit" in self.config["General"]["command"].lower(): return "bukkit" else: return "vanilla" def server_reload(self): """This is not used yet.. intended to restart a server without kicking players restarts the server quickly. Wrapper "auto-restart" must be set to True. If wrapper is in proxy mode, it will reconnect all clients to the serverconnection. """ if self.state in (STOPPING, OFF): self.log.warning( "The server is not already running... Just use '/start'.") return if self.wrapper.proxymode: # discover who all is playing and store that knowledge # tell the serverconnection to stop processing play packets self.server_stalled = True # stop the server. # Call events to "do stuff" while server is down (write # whilelists, OP files, server properties, etc) # restart the server. if self.wrapper.proxymode: pass # once server is back up, Reconnect stalled/idle # clients back to the serverconnection process. # Do I need to create a new serverconnection, # or can the old one be tricked into continuing?? self.stop_server_command() def __stdout__(self): """handles server output, not lines typed in console.""" while not self.wrapper.haltsig.halt: # noinspection PyBroadException,PyUnusedLocal # this reads the line and puts the line in the # 'self.data' buffer for processing by # readconsole() (inside handle_server) try: data = self.proc.stdout.readline() for line in data.split("\n"): if len(line) < 1: continue self.console_output_data.append(line) except Exception as e: time.sleep(0.1) continue def __stderr__(self): """like __stdout__, handles server output (not lines typed in console).""" while not self.wrapper.haltsig.halt: try: data = self.proc.stderr.readline() if len(data) > 0: for line in data.split("\n"): self.console_output_data.append(line.replace("\r", "")) except Exception as e: time.sleep(0.1) continue def read_ops_file(self, read_super_ops=True): """Keep a list of ops in the server instance to stop reading the disk for it. :rtype: Dictionary """ ops = False # (4 = PROTOCOL_1_7 ) - 1.7.6 or greater use ops.json if self.version_compute > 10700: ops = getjsonfile( "ops", self.serverpath, encodedas=self.encoding ) if not ops: # try for an old "ops.txt" file instead. ops = [] opstext = getfileaslines("ops.txt", self.serverpath) if not opstext: return False for op in opstext: # create a 'fake' ops list from the old pre-1.8 # text line name list notice that the level (an # option not the old list) is set to 1 This will # pass as true, but if the plugin is also # checking op-levels, it may not pass truth. indivop = {"uuid": op, "name": op, "level": 1} ops.append(indivop) # Grant "owner" an op level above 4. required for some wrapper commands if read_super_ops: for eachop in ops: if eachop["name"] in self.ownernames: eachop["level"] = self.ownernames[eachop["name"]] return ops def refresh_ops(self, read_super_ops=True): self.ownernames = config_to_dict_read("superops.txt", ".") if self.ownernames == {}: sample = "<op_player_1>=10\n<op_player_2>=9" with open("superops.txt", "w") as f: f.write(sample) self.operator_list = self.read_ops_file(read_super_ops) def getmemoryusage(self): """Returns allocated memory in bytes. This command currently only works for *NIX based systems. """ if not resource or not os.name == "posix": raise OSError( "Your current OS (%s) does not support" " this command at this time." % os.name) if self.proc is None: self.log.debug("There is no running server to getmemoryusage().") return 0 try: with open("/proc/%d/statm" % self.proc.pid) as f: getbytes = int(f.read().split(" ")[1]) * resource.getpagesize() return getbytes except Exception as e: raise e @staticmethod def getstorageavailable(folder): """Returns the disk space for the working directory in bytes. """ if platform.system() == "Windows": free_bytes = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes)) return free_bytes.value else: st = os.statvfs(folder) return st.f_bavail * st.f_frsize @staticmethod def stripspecial(text): # not sure what this is actually removing... # this must be legacy code of some kind pass a = "" it = iter(range(len(text))) for i in it: char = text[i] if char == "\xc2": try: next(it) next(it) except Exception as e: pass else: a += char return a def readconsole(self, buff): """Internally-used function that parses a particular console line. """ if len(buff) < 1: return # Standardize the line to only include the text (removing # time and log pre-pends) line_words = buff.split(' ')[self.prepends_offset:] # find the actual offset is where server output line # starts (minus date/time and info stamps). # .. and load the proper ops file if "Starting minecraft server version" in buff and \ self.prepends_offset == 0: for place in range(len(line_words)-1): self.prepends_offset = place if line_words[place] == "Starting": break line_words = buff.split(' ')[self.prepends_offset:] self.version = getargs(line_words, 4) semantics = self.version.split(".") release = get_int(getargs(semantics, 0)) major = get_int(getargs(semantics, 1)) minor = get_int(getargs(semantics, 2)) self.version_compute = minor + (major * 100) + (release * 10000) # noqa if len(self.version.split("w")) > 1: # It is a snap shot self.version_compute = 10800 # 1.7.6 (protocol 5) is the cutoff where ops.txt became ops.json if self.version_compute > 10705 and self.protocolVersion < 0: # noqa self.protocolVersion = 5 self.wrapper.api.registerPermission("mc1.7.6", value=True) if self.version_compute < 10702 and self.wrapper.proxymode: self.log.warning("\nProxy mode cannot run because the " "server is a pre-Netty version:\n\n" "http://wiki.vg/Protocol_version_numbers" "#Versions_before_the_Netty_rewrite\n\n" "Server will continue in non-proxy mode.") self.wrapper.disable_proxymode() return self.refresh_ops() if len(line_words) < 1: return # the server attempted to print a blank line if len(line_words[0]) < 1: print('') return # parse or modify the server output section # # # Over-ride OP help console display if "/op <player>" in buff: new_usage = "player> [-s SUPER-OP] [-o OFFLINE] [-l <level>]" message = buff.replace("player>", new_usage) buff = message if "/whitelist <on|off" in buff: new_usage = "/whitelist <on|off|list|add|remvove|reload|offline|online>" # noqa message = new_usage buff = message if "While this makes the game possible to play" in buff: prefix = " ".join(buff.split(' ')[:self.prepends_offset]) if not self.wrapper.wrapper_onlinemode: try: pport = "either port %s or " % self.wrapper.proxy.proxy_port except AttributeError: pport = "" message = ( "%s Since you are running Wrapper in OFFLINE mode, THIS " "COULD BE SERIOUS!\n%s Wrapper is not handling any" " authentication.\n%s This is only ok if this wrapper " "is not accessible from %sport %s" " (I.e., this wrapper is a multiworld for a hub server, or" " you are doing your own authorization via a plugin)." % ( prefix, prefix, prefix, pport, self.server_port)) else: message = ( "%s Since you are running Wrapper in proxy mode, this" " should be ok because Wrapper is handling the" " authentication, PROVIDED no one can access port" " %s from outside your network." % ( prefix, self.server_port)) if self.wrapper.proxymode: buff = message # read port of server and display proxy port, if applicable if "Starting Minecraft server on" in buff: self.server_port = get_int(buff.split(':')[-1:][0]) # check for server console spam before printing to wrapper console server_spaming = False for things in self.spammy_stuff: if things in buff: server_spaming = True # server_spaming setting does not stop it from being parsed below. if not server_spaming: if not self.server_muted: self.wrapper.write_stdout(buff, "server") else: self.queued_lines.append(buff) first_word = getargs(line_words, 0) second_word = getargs(line_words, 1) # be careful about how these elif's are handled! # confirm server start if "Done (" in buff: self._toggle_server_started() self.changestate(STARTED) self.log.info("Server started") if self.wrapper.proxymode: self.log.info("Proxy listening on *:%s", self.wrapper.proxy.proxy_port) # noqa # Getting world name elif "Preparing level" in buff: self.worldname = getargs(line_words, 2).replace('"', "") self.world = World(self.worldname, self) # Player Message elif first_word[0] == "<": # get a name out of <name> name = self.stripspecial(first_word[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) playerobj = self.getplayer(name) if playerobj: self.wrapper.events.callevent("player.message", { "player": self.getplayer(name), "message": message, "original": original }, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> Player chat scrubbed from the console. <description> <abortable> No <abortable> <comments> This event is triggered by console chat which has already been sent. This event returns the player object. if used in a string context, ("%s") it's repr (self.__str__) is self.username (no need to do str(player) or player.username in plugin code). <comments> <payload> "player": playerobject (self.__str__ represents as player.username) "message": <str> type - what the player said in chat. ('hello everyone') "original": The original line of text from the console ('<mcplayer> hello everyone`) <payload> """ # noqa else: self.log.debug("Console has chat from '%s', but wrapper has no " "known logged-in player object by that name.", name) # noqa # Player Login elif second_word == "logged": user_desc = first_word.split("[/") name = user_desc[0] ip_addr = user_desc[1].split(":")[0] eid = get_int(getargs(line_words, 6)) locationtext = getargs(buff.split(" ("), 1)[:-1].split(", ") # spigot versus vanilla # SPIGOT - [12:13:19 INFO]: *******[/] logged in with entity id 123 at ([world]316.86789318152546, 67.12426603789697, -191.9069627257038) # noqa # VANILLA - [23:24:34] [Server thread/INFO]: *******[/127.0.0.1:47434] logged in with entity id 149 at (46.29907483845001, 63.0, -270.1293488726086) # noqa if len(locationtext[0].split("]")) > 1: x_c = get_int(float(locationtext[0].split("]")[1])) else: x_c = get_int(float(locationtext[0])) y_c = get_int(float(locationtext[1])) z_c = get_int(float(locationtext[2])) location = x_c, y_c, z_c self.login(name, eid, location, ip_addr) # Player Logout elif "lost connection" in buff: name = first_word self.logout(name) # player action elif first_word == "*": name = self.stripspecial(second_word) message = self.stripspecial(getargsafter(line_words, 2)) self.wrapper.events.callevent("player.action", { "player": self.getplayer(name), "action": message }, abortable=False) # Player Achievement elif "has just earned the achievement" in buff: name = self.stripspecial(first_word) achievement = getargsafter(line_words, 6) self.wrapper.events.callevent("player.achievement", { "player": name, "achievement": achievement }, abortable=False) # /say command elif getargs( line_words, 0)[0] == "[" and first_word[-1] == "]": if self.getservertype != "vanilla": # Unfortunately, Spigot and Bukkit output things # that conflict with this. return name = self.stripspecial(first_word[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) self.wrapper.events.callevent("server.say", { "player": name, "message": message, "original": original }, abortable=False) # Player Death elif second_word in self.deathprefixes: name = self.stripspecial(first_word) self.wrapper.events.callevent("player.death", { "player": self.getplayer(name), "death": getargsafter(line_words, 1) }, abortable=False) # server lagged elif "Can't keep up!" in buff: skipping_ticks = getargs(line_words, 17) self.wrapper.events.callevent("server.lagged", { "ticks": get_int(skipping_ticks) }, abortable=False) # player teleport elif second_word == "Teleported" and getargs(line_words, 3) == "to": playername = getargs(line_words, 2) # [SurestTexas00: Teleported SapperLeader to 48.49417131908783, 77.67081086259394, -279.88880690937475] # noqa if playername in self.wrapper.players: playerobj = self.getplayer(playername) try: playerobj._position = [ get_int(float(getargs(line_words, 4).split(",")[0])), get_int(float(getargs(line_words, 5).split(",")[0])), get_int(float(getargs(line_words, 6).split("]")[0])), 0, 0 ] except ValueError: pass self.wrapper.events.callevent( "player.teleport", {"player": playerobj}, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> When player teleports. <description> <abortable> No <abortable> <comments> driven from console message "Teleported ___ to ....". <comments> <payload> "player": player object <payload> """ # noqa elif first_word == "Teleported" and getargs(line_words, 2) == "to": playername = second_word # Teleported SurestTexas00 to 48.49417131908783, 77.67081086259394, -279.88880690937475 # noqa if playername in self.wrapper.players: playerobj = self.getplayer(playername) try: playerobj._position = [ get_int(float(getargs(line_words, 3).split(",")[0])), get_int(float(getargs(line_words, 4).split(",")[0])), get_int(float(getargs(line_words, 5))), 0, 0 ] except ValueError: pass self.wrapper.events.callevent( "player.teleport", {"player": playerobj}, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> When player teleports. <description> <abortable> No <abortable> <comments> driven from console message "Teleported ___ to ....". <comments> <payload> "player": player object <payload> """ # noqa # mcserver.py onsecond Event Handlers def reboot_timer(self): rb_mins = self.reboot_minutes rb_mins_warn = self.config["General"]["timed-reboot-warning-minutes"] while not self.wrapper.haltsig.halt: time.sleep(1) timer = rb_mins - rb_mins_warn while self.state in (STARTED, STARTING): timer -= 1 time.sleep(60) if timer > 0: continue if timer + rb_mins_warn > 0: if rb_mins_warn + timer > 1: self.broadcast("&cServer will reboot in %d " "minutes!" % (rb_mins_warn + timer)) else: self.broadcast("&cServer will reboot in %d " "minute!" % (rb_mins_warn + timer)) countdown = 59 timer -= 1 while countdown > 0: time.sleep(1) countdown -= 1 if countdown == 0: if self.wrapper.backups_idle(): self.restart(self.reboot_message) else: self.broadcast( "&cBackup in progress. Server reboot " "delayed for one minute..") countdown = 59 if countdown % 15 == 0: self.broadcast("&cServer will reboot in %d " "seconds" % countdown) if countdown < 6: self.broadcast("&cServer will reboot in %d " "seconds" % countdown) continue if self.wrapper.backups_idle(): self.restart(self.reboot_message) else: self.broadcast( "&cBackup in progress. Server reboot " "delayed..") timer = rb_mins + rb_mins_warn + 1 def eachsecond_web(self): if time.time() - self.lastsizepoll > 120: if self.worldname is None: return True self.lastsizepoll = time.time() size = 0 # os.scandir not in standard library on early py2.7.x systems for i in os.walk( "%s/%s" % (self.serverpath, self.worldname) ): for f in os.listdir(i[0]): size += os.path.getsize(os.path.join(i[0], f)) self.worldsize = size def _console_event(self, payload): """This function is used in conjunction with event handlers to permit a proxy object to make a command call to this server.""" # make commands pass through the command interface. comm_pay = payload["command"].split(" ") if len(comm_pay) > 1: args = comm_pay[1:] else: args = [""] new_payload = {"player": self.wrapper.xplayer, "command": comm_pay[0], "args": args } self.wrapper.commands.playercommand(new_payload)
class Backups(object): def __init__(self, wrapper): self.wrapper = wrapper self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.log = wrapper.log self.api = API(wrapper, "Backups", internal=True) # self.wrapper.backups.idle self.idle = True self.inprogress = False self.backup_interval = self.config["Backups"]["backup-interval"] self.time = time.time() self.backups = [] # allow plugins to shutdown backups via api self.enabled = self.config["Backups"]["enabled"] # only register event if used and tar installed. if self.enabled and self.dotarchecks(): self.api.registerEvent("timer.second", self.eachsecond) self.log.debug("Backups Enabled..") # noinspection PyUnusedLocal def eachsecond(self, payload): # only run backups in server running/starting states if self.wrapper.javaserver.vitals.state in (1, 2) and not self.idle: if time.time() - self.time > self.backup_interval and self.enabled: self.dobackup() def pruneoldbackups(self, filename="IndependentPurge"): if len(self.backups) > self.config["Backups"]["backups-keep"]: self.log.info("Deleting old backups...") while len(self.backups) > self.config["Backups"]["backups-keep"]: backup = self.backups[0] if not self.wrapper.events.callevent( "wrapper.backupDelete", {"file": filename}): # noqa """ eventdoc <group> Backups <group> <description> Called upon deletion of a backup file. <description> <abortable> Yes, return False to abort. <abortable> <comments> <comments> <payload> "file": filename <payload> """ # noqa break try: os.remove( '%s/%s' % (self.config["Backups"]["backup-location"], backup[1])) except Exception as e: self.log.error("Failed to delete backup (%s)", e) self.log.info( "Deleting old backup: %s", datetime.datetime.fromtimestamp(int( backup[0])).strftime('%Y-%m-%d_%H:%M:%S')) # noqa # hink = self.backups[0][1][:] # not used... del self.backups[0] putjsonfile(self.backups, "backups", self.config["Backups"]["backup-location"]) def dotarchecks(self): # Check if tar is installed which = "where" if platform.system() == "Windows" else "which" if not subprocess.call([which, "tar"]) == 0: self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 1, "reasonText": "Tar is not installed. Please install " "tar before trying to make backups." }, abortable=False) """ eventdoc <group> Backups <group> <description> Indicates failure of backup. <description> <abortable> No - informatinal only <abortable> <comments> Reasoncode and text provide more detail about specific problem. 1 - Tar not installed. 2 - Backup file does not exist after the tar operation. 3 - Specified file does not exist. 4 - backups.json is corrupted 5 - unable to create backup directory <comments> <payload> "reasonCode": an integer 1-4 "reasonText": a string description of the failure. <payload> """ self.log.error( "Backups will not work, because tar does not appear " "to be installed!") self.log.error( "If you are on a Linux-based system, please install it through" " your preferred package manager.") self.log.error( "If you are on Windows, you can find GNU/Tar from this link:" " http://goo.gl/SpJSVM") return False else: return True def dobackup(self): self.inprogress = True self.log.debug("Backup starting.") self._settime() if not self._checkforbackupfolder(): self.inprogress = False self.wrapper.events.callevent( "wrapper.backupFailure", { "reasonCode": 5, "reasonText": "Backup location could not be found/created!" }, abortable=False) self.log.warning("") self._getbackups() # populate self.backups self._performbackup() self.log.debug("dobackup() cycle complete.") self.inprogress = False def _checkforbackupfolder(self): if not os.path.exists(self.config["Backups"]["backup-location"]): self.log.warning( "Backup location %s does not exist -- creating target " "location...", self.config["Backups"]["backup-location"]) mkdir_p(self.config["Backups"]["backup-location"]) if not os.path.exists(self.config["Backups"]["backup-location"]): self.log.error("Backup location %s could not be created!", self.config["Backups"]["backup-location"]) return False return True def _performbackup(self): timestamp = int(time.time()) # Turn off server saves... self.wrapper.javaserver.doserversaving(False) # give server time to save time.sleep(1) # Create tar arguments filename = "backup-%s.tar" % datetime.datetime.fromtimestamp( int(timestamp)).strftime("%Y-%m-%d_%H.%M.%S") if self.config["Backups"]["backup-compression"]: filename += ".gz" arguments = [ "tar", "czf", "%s/%s" % (self.config["Backups"]["backup-location"].replace( " ", "\\ "), filename) ] else: arguments = [ "tar", "cfpv", "%s/%s" % (self.config["Backups"]["backup-location"], filename) ] # Process begin Events if not self.wrapper.events.callevent("wrapper.backupBegin", {"file": filename}): # noqa self.log.warning( "A backup was scheduled, but was cancelled by a plugin!") """ eventdoc <group> Backups <group> <description> Indicates a backup is being initiated. <description> <abortable> Yes, return False to abort. <abortable> <comments> A console warning will be issued if a plugin cancels the backup. <comments> <payload> "file": Name of backup file. <payload> """ self.wrapper.javaserver.doserversaving(True) # give server time to save time.sleep(1) return if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&cBacking up... lag may occur!", irc=False) # Do backups serverpath = self.config["General"]["server-directory"] for backupfile in self.config["Backups"]["backup-folders"]: backup_file_and_path = "%s/%s" % (serverpath, backupfile) if os.path.exists(backup_file_and_path): arguments.append(backup_file_and_path) else: self.log.warning( "Backup file '%s' does not exist - canceling backup", backup_file_and_path) self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 3, "reasonText": "Backup file '%s' does not " "exist." % backup_file_and_path }, abortable=False) """ eventdoc <description> internalfunction <description> """ return # perform TAR backup statuscode = os.system(" ".join(arguments)) # TODO add a wrapper properties config item to set save mode of server # restart saves, call finish Events self.wrapper.javaserver.doserversaving(True) self.backups.append((timestamp, filename)) # Prune backups self.pruneoldbackups(filename) # Check for success finalbackup = "%s/%s" % (self.config["Backups"]["backup-location"], filename) if not os.path.exists(finalbackup): self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 2, "reasonText": "Backup file didn't exist after the tar " "command executed - assuming failure." }, abortable=False) """ eventdoc <description> internalfunction <description> """ summary = "backup failed" else: # find size of completed backup file backupsize = os.path.getsize(finalbackup) size_of, units = format_bytes(backupsize) timetook = _secondstohuman(int(time.time()) - timestamp) desc = "were backed up. The operation took" summary = "%s %s %s %s" % (size_of, units, desc, timetook) self.wrapper.events.callevent("wrapper.backupEnd", { "file": filename, "status": statuscode, "summary": summary }, abortable=False) """ eventdoc <group> Backups <group> <description> Indicates a backup is complete. <description> <abortable> No - informational only <abortable> <comments> <comments> <payload> "file": Name of backup file. "status": Status code from TAR "summary": string summary of operation <payload> """ if self.config["Backups"]["backup-notification"]: self.api.minecraft.broadcast("&aBackup cycle complete!", irc=False) self.api.minecraft.broadcast("&a%s" % summary, irc=False) def _getbackups(self): if len(self.backups) == 0 and os.path.exists( self.config["Backups"]["backup-location"] + "/backups.json"): # noqa - long if statement loadcode = getjsonfile("backups", self.config["Backups"]["backup-location"], encodedas=self.encoding) if not loadcode: self.log.error( "NOTE - backups.json was unreadable. It might be corrupted." " Backups will no longer be automatically pruned.") self.wrapper.events.callevent("wrapper.backupFailure", { "reasonCode": 4, "reasonText": "backups.json is corrupted. Please contact" " an administer instantly, as this may be " "critical." }, abortable=False) """ eventdoc <description> internalfunction <description> """ self.backups = [] else: self.backups = loadcode else: if len(os.listdir(self.config["Backups"]["backup-location"])) > 0: # import old backups from previous versions of Wrapper.py backuptimestamps = [] for backupNames in os.listdir( self.config["Backups"]["backup-location"]): # noinspection PyBroadException,PyUnusedLocal try: backuptimestamps.append( int(backupNames[backupNames.find('-') + 1:backupNames.find('.')]) ) # noqa - large one-liner except Exception as e: pass backuptimestamps.sort() for backupI in backuptimestamps: self.backups.append( (int(backupI), "backup-%s.tar" % str(backupI))) def _settime(self): self.time = time.time()
def loadplugin(self, name, available_files): if name in self.plugins_loaded: # Don't try to load a previously errored ( attempted to load..) plugin return False self.log.debug("Reading plugin file %s.py ...", name) # hack to remove previously loaded modules during reloads if name in sys.modules: del sys.modules[name] plugin = importlib.import_module(name) pid = getattr( plugin, 'ID', name ) # from the plugin head.. e.g., 'ID = "com.benbaptist.plugins.essentials"' disabled = getattr(plugin, 'DISABLED', False) dependencies = getattr(plugin, 'DEPENDENCIES', []) if pid in self.wrapper.storage["disabled_plugins"] or disabled: self.log.debug("Plugin '%s' disabled - not loading", name) return True if pid in self.plugins: # Once successfully loaded, further attempts to load the plugin are ignored self.log.debug( "Plugin '%s' is already loaded (probably as a dependency) - not reloading", name) return True # check for unloaded dependencies and develop a list of required dependencies. good_deps = True dep_loads = [] if dependencies: for dep in dependencies: # allow a user to specify name or full filename for dependencies dep_name = dep if dep[-3:] == '.py': dep_name = dep[:-3] if dep_name in available_files: # if the plugin was already loaded, the dependency is satisfied... if dep_name not in self.plugins_loaded: dep_loads.append(dep_name) else: good_deps = False self.log.warn( "Plugin '%s'.py is missing a dependency: '%s.py'", name, dep_name) if not good_deps: self.log.warn( "Plugin '%s'.py failed to load because of missing dependencies.", name) return False # load the required dependencies first. for dependency in dep_loads: if self.loadplugin(dependency, available_files): self.log.debug("Dependency '%s' loaded.", dependency) self.plugins_loaded.append(name) else: self.log.warn("Dependency '%s' could not be loaded.", dependency) self.log.warn( "Plugin '%s'.py failed to load because of missing dependency '%s'.", name, dependency) self.plugins_loaded.append(name) return False # Finally, initialize this plugin self.log.debug("Loading plugin %s...", name) if not getattr(plugin, 'Main', False): self.log.warn( "Plugin '%s' is malformed and missing a class 'Main'", name) self.plugins_loaded.append(name) return False has_onenable = getattr(getattr(plugin, 'Main', False), 'onEnable', False) if not has_onenable: self.log.warn("Plugin '%s' is missing an 'onEnable' method.", name) self.plugins_loaded.append(name) return False main = plugin.Main(API(self.wrapper, name, pid), logging.getLogger(name)) self.plugins[pid] = { "main": main, "good": True, "module": plugin } # "events": {}, "commands": {}} self.plugins[pid]["name"] = getattr(plugin, "NAME", name) self.plugins[pid]["version"] = getattr(plugin, 'VERSION', (0, 1)) self.plugins[pid]["summary"] = getattr(plugin, 'SUMMARY', None) self.plugins[pid]["description"] = getattr(plugin, 'DESCRIPTION', None) self.plugins[pid]["author"] = getattr(plugin, 'AUTHOR', None) self.plugins[pid]["website"] = getattr(plugin, 'WEBSITE', None) self.plugins[pid]["filename"] = "%s.py" % name self.wrapper.commands[pid] = {} self.wrapper.events[pid] = {} self.wrapper.registered_permissions[pid] = {} self.wrapper.help[pid] = {} main.onEnable() self.log.info("Plugin %s loaded...", name) self.plugins_loaded.append(name) return True
class MCServer(object): def __init__(self, wrapper, servervitals): self.log = wrapper.log self.config = wrapper.config self.vitals = servervitals self.encoding = self.config["General"]["encoding"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config["General"]["timed-reboot-minutes"] self.reboot_warn_minutes = self.config["General"]["timed-reboot-warning-minutes"] # noqa # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.vitals.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.lastsizepoll = 0 self.console_output_data = [] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot", "slain"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.world = None # get OPs self.refresh_ops() # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"]: rb = threading.Thread(target=self.reboot_timer, args=()) rb.daemon = True rb.start() if self.config["Web"]["web-enabled"]: wb = threading.Thread(target=self.eachsecond_web, args=()) wb.daemon = True wb.start() # This event is used to allow proxy to make console commands via # callevent() without referencing mcserver.py code (the eventhandler # is passed as an argument to the proxy). self.api.registerEvent("proxy.console", self._console_event) def init(self): """ Start up the listen threads for reading server console output. """ capturethread = threading.Thread(target=self.__stdout__, args=()) capturethread.daemon = True capturethread.start() capturethread = threading.Thread(target=self.__stderr__, args=()) capturethread.daemon = True capturethread.start() def __del__(self): self.vitals.state = 0 def accepteula(self): if os.path.isfile("%s/eula.txt" % self.vitals.serverpath): self.log.debug("Checking EULA agreement...") with open("%s/eula.txt" % self.vitals.serverpath) as f: eula = f.read() # if forced, should be at info level since acceptance # is a legal matter. if "eula=false" in eula: self.log.warning( "EULA agreement was not accepted, accepting on" " your behalf...") set_item("eula", "true", "eula.txt", self.vitals.serverpath) self.log.debug("EULA agreement has been accepted.") return True else: return False def handle_server(self): """ Function that handles booting the server, parsing console output, and such. """ trystart = 0 while not self.wrapper.halt.halt: trystart += 1 self.proc = None # endless loop for not booting the server (while still # allowing handle to run). if not self.boot_server: time.sleep(0.2) trystart = 0 continue self.changestate(STARTING) self.log.info("Starting server...") self.reloadproperties() command = self.args self.proc = subprocess.Popen( command, cwd=self.vitals.serverpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) self.wrapper.players = {} self.accepteula() # Auto accept eula if self.proc.poll() is None and trystart > 3: self.log.error( "Could not start server. check your server.properties," " wrapper.properties and this your startup 'command'" " from wrapper.properties:\n'%s'", " ".join(self.args)) self.changestate(OFF) # halt wrapper self.wrapper.halt.halt = True # exit server_handle break # The server loop while True: # Loop runs continously as long as server console is running time.sleep(0.1) if self.proc.poll() is not None: self.changestate(OFF) trystart = 0 self.boot_server = self.server_autorestart # break out to `while not self.wrapper.halt.halt:` loop # to (possibly) connect to server again. break # is is only reading server console output for line in self.console_output_data: try: self.readconsole(line.replace("\r", "")) except Exception as e: self.log.exception(e) self.console_output_data = [] # code ends here on wrapper.halt.halt and execution returns to # the end of wrapper.start() def _toggle_server_started(self, server_started=True): self.wrapper.storage["ServerStarted"] = server_started self.wrapper.wrapper_storage.save() def start(self): """ Start the Minecraft server """ self.server_autorestart = self.config["General"]["auto-restart"] if self.vitals.state in (STARTED, STARTING): self.log.warning("The server is already running!") return if not self.boot_server: self.boot_server = True else: self.handle_server() self._toggle_server_started() def restart(self, reason=""): """Restart the Minecraft server, and kick people with the specified reason. If server was already stopped, restart it. """ if reason == "": reason = self.restart_message if self.vitals.state in (STOPPING, OFF): self.start() return self.doserversaving() self.stop(reason) def kick_players(self, reasontext): playerlist = copy.copy(self.vitals.players) for player in playerlist: self.kick_player(player, reasontext) def kick_player(self, player, reasontext): if self.wrapper.proxymode: try: playerclient = self.vitals.players[player].client playerclient.notify_disconnect(reasontext) except AttributeError: self.log.warning( "Proxy kick failed - Gould not get client %s.\n" "I'll try using the console..", player) self.console("kick %s %s" % (player, reasontext)) except KeyError: self.log.warning( "Kick failed - No player called %s", player) except Exception as e: self.log.warning( "Kick failed - something else went wrong:" " %s\n%s", player, e,) else: self.console("kick %s %s" % (player, reasontext)) # this sleep is here for Spigot McBans reasons/compatibility. time.sleep(2) def stop(self, reason="", restart_the_server=True): """Stop the Minecraft server from an automatic process. Allow it to restart by default. """ self.doserversaving() self.log.info("Stopping Minecraft server with reason: %s", reason) self.kick_players(reason) self.changestate(STOPPING, reason) self.console("stop") # False will allow this loop to run with no server (and # reboot if permitted). self.boot_server = restart_the_server def stop_server_command(self, reason="", restart_the_server=False): """ Stop the Minecraft server (as a command). By default, do not restart. """ if reason == "": reason = self.stop_message if self.vitals.state == OFF: self.log.warning("The server is not running... :?") return if self.vitals.state == FROZEN: self.log.warning("The server is currently frozen.\n" "To stop it, you must /unfreeze it first") return self.server_autorestart = False self.stop(reason, restart_the_server) self._toggle_server_started(restart_the_server) def kill(self, reason="Killing Server"): """Forcefully kill the server. It will auto-restart if set in the configuration file. """ if self.vitals.state in (STOPPING, OFF): self.log.warning("The server is already dead, my friend...") return self.log.info("Killing Minecraft server with reason: %s", reason) self.changestate(OFF, reason) self.proc.kill() def freeze(self, reason="Server is now frozen. You may disconnect."): """Freeze the server with `kill -STOP`. Can be used to stop the server in an emergency without shutting it down, so it doesn't write corrupted data - e.g. if the disk is full, you can freeze the server, free up some disk space, and then unfreeze 'reason' argument is printed in the chat for all currently-connected players, unless you specify None. This command currently only works for *NIX based systems. """ if self.vitals.state != OFF: if os.name == "posix": self.log.info("Freezing server with reason: %s", reason) self.broadcast("&c%s" % reason) time.sleep(0.5) self.changestate(FROZEN) os.system("kill -STOP %d" % self.proc.pid) else: raise OSError( "Your current OS (%s) does not support this" " command at this time." % os.name) else: raise EnvironmentError( "Server is not started. You may run '/start' to boot it up.") def unfreeze(self): """Unfreeze the server with `kill -CONT`. Counterpart to .freeze(reason) This command currently only works for *NIX based systems. """ if self.vitals.state != OFF: if os.name == "posix": self.log.info("Unfreezing server (ignore any" " messages to type /start)...") self.broadcast("&aServer unfrozen.") self.changestate(STARTED) os.system("kill -CONT %d" % self.proc.pid) else: raise OSError( "Your current OS (%s) does not support this command" " at this time." % os.name) else: raise EnvironmentError( "Server is not started. Please run '/start' to boot it up.") def broadcast(self, message, who="@a"): """Broadcasts the specified message to all clients connected. message can be a JSON chat object, or a string with formatting codes using the § as a prefix. """ if isinstance(message, dict): if self.vitals.version_compute < 10700: self.console("say %s %s" % (who, chattocolorcodes(message))) else: encoding = self.wrapper.encoding self.console("tellraw %s %s" % ( who, json.dumps(message, ensure_ascii=False))) else: temp = processcolorcodes(message) if self.vitals.version_compute < 10700: temp = processcolorcodes(message) self.console("say %s %s" % ( who, chattocolorcodes(temp))) else: self.console("tellraw %s %s" % ( who, json.dumps(processcolorcodes(message)))) def login(self, username, servereid, position, ipaddr): """Called when a player logs in.""" if username not in self.vitals.players: self.vitals.players[username] = Player(username, self.wrapper) # store EID if proxy is not fully connected yet (or is not enabled). self.vitals.players[username].playereid = servereid self.vitals.players[username].loginposition = position if self.vitals.players[username].ipaddress == "127.0.0.0": self.vitals.players[username].ipaddress = ipaddr if self.wrapper.proxy and self.vitals.players[username].client: self.vitals.players[username].client.server_eid = servereid self.vitals.players[username].client.position = position # activate backup status self.wrapper.backups.idle = False player = self.getplayer(username) # proxy will handle the login event if enabled if player and player.client: return self.wrapper.events.callevent( "player.login", {"player": player, "playername": username}, abortable=False ) """ eventdoc <group> core/mcserver.py <group> <description> When player logs into the java MC server. <description> <abortable> No <abortable> <comments> All events in the core/mcserver.py group are collected from the console output, do not require proxy mode, and therefore, also, cannot be aborted. <comments> <payload> "player": player object (if object available -could be False if not) "playername": user name of player (string) <payload> """ def logout(self, players_name): """Called when a player logs out.""" if players_name in self.vitals.players: player = self.vitals.players[players_name] self.wrapper.events.callevent( "player.logout", {"player": player, "playername": players_name}, abortable=True ) """ eventdoc <group> core/mcserver.py <group> <description> When player logs out of the java MC server. <description> <abortable> No - but This will pause long enough for you to deal with the playerobject. <abortable> <comments> All events in the core/mcserver.py group are collected from the console output, do not require proxy mode, and therefore, also, cannot be aborted. <comments> <payload> "player": player object (if object available -could be False if not) "playername": user name of player (string) <payload> """ # noqa if player.client is None: player.abort = True del self.vitals.players[players_name] elif player.client.state != LOBBY and player.client.local: player.abort = True del self.vitals.players[players_name] self.wrapper.proxy.removestaleclients() if len(self.vitals.players) == 0: self.wrapper.backups.idle = True def getplayer(self, username): """Returns a player object with the specified name, or False if the user is not logged in/doesn't exist. this getplayer only deals with local players on this server. api.minecraft.getPlayer will deal in all players, including those in proxy and/or other hub servers. """ if username in self.vitals.players: player = self.vitals.players[username] if player.client and player.client.state != LOBBY and player.client.local: # noqa return player return False def reloadproperties(self): # Read server.properties and extract some information out of it # the PY3.5 ConfigParser seems broken. This way was much more # straightforward and works in both PY2 and PY3 # Load server icon if os.path.exists("%s/server-icon.png" % self.vitals.serverpath): with open("%s/server-icon.png" % self.vitals.serverpath, "rb") as f: theicon = f.read() iconencoded = base64.standard_b64encode(theicon) self.vitals.serverIcon = b"data:image/png;base64," + iconencoded self.vitals.properties = config_to_dict_read( "server.properties", self.vitals.serverpath) if self.vitals.properties == {}: self.log.warning("File 'server.properties' not found.") return False if "level-name" in self.vitals.properties: self.vitals.worldname = self.vitals.properties["level-name"] else: self.log.warning("No 'level-name=(worldname)' was" " found in the server.properties.") return False self.vitals.motd = self.vitals.properties["motd"] if "max-players" in self.vitals.properties: self.vitals.maxPlayers = self.vitals.properties["max-players"] else: self.log.warning( "No 'max-players=(count)' was found in the" " server.properties. The default of '20' will be used.") self.vitals.maxPlayers = 20 self.vitals.onlineMode = self.vitals.properties["online-mode"] def console(self, command): """Execute a console command on the server.""" if self.vitals.state in (STARTING, STARTED, STOPPING) and self.proc: self.proc.stdin.write("%s\n" % command) self.proc.stdin.flush() else: self.log.debug("Attempted to run console command" " '%s' but the Server is not started.", command) def changestate(self, state, reason=None): """Change the boot state indicator of the server, with a reason message. """ self.vitals.state = state if self.vitals.state == OFF: self.wrapper.events.callevent( "server.stopped", {"reason": reason}, abortable=False) elif self.vitals.state == STARTING: self.wrapper.events.callevent( "server.starting", {"reason": reason}, abortable=False) elif self.vitals.state == STARTED: self.wrapper.events.callevent( "server.started", {"reason": reason}, abortable=False) elif self.vitals.state == STOPPING: self.wrapper.events.callevent( "server.stopping", {"reason": reason}, abortable=False) self.wrapper.events.callevent( "server.state", {"state": state, "reason": reason}, abortable=False) def doserversaving(self, desiredstate=True): """ :param desiredstate: True = turn serversaving on False = turn serversaving off :return: Future expansion to allow config of server saving state glabally in config. Plan to include a global config option for periodic or continuous server disk saving of the minecraft server. """ if desiredstate: self.console("save-all flush") # flush argument is required self.console("save-on") else: self.console("save-all flush") # flush argument is required self.console("save-off") time.sleep(1) def getservertype(self): if "spigot" in self.config["General"]["command"].lower(): return "spigot" elif "bukkit" in self.config["General"]["command"].lower(): return "bukkit" else: return "vanilla" def server_reload(self): """This is not used yet.. intended to restart a server without kicking players restarts the server quickly. Wrapper "auto-restart" must be set to True. If wrapper is in proxy mode, it will reconnect all clients to the serverconnection. """ if self.vitals.state in (STOPPING, OFF): self.log.warning( "The server is not already running... Just use '/start'.") return if self.wrapper.proxymode: # discover who all is playing and store that knowledge # tell the serverconnection to stop processing play packets self.server_stalled = True # stop the server. # Call events to "do stuff" while server is down (write # whilelists, OP files, server properties, etc) # restart the server. if self.wrapper.proxymode: pass # once server is back up, Reconnect stalled/idle # clients back to the serverconnection process. # Do I need to create a new serverconnection, # or can the old one be tricked into continuing?? self.stop_server_command() def __stdout__(self): """handles server output, not lines typed in console.""" while not self.wrapper.halt.halt: # noinspection PyBroadException,PyUnusedLocal # this reads the line and puts the line in the # 'self.data' buffer for processing by # readconsole() (inside handle_server) try: data = self.proc.stdout.readline() for line in data.split("\n"): if len(line) < 1: continue self.console_output_data.append(line) except Exception as e: time.sleep(0.1) continue def __stderr__(self): """like __stdout__, handles server output (not lines typed in console).""" while not self.wrapper.halt.halt: try: data = self.proc.stderr.readline() if len(data) > 0: for line in data.split("\n"): self.console_output_data.append(line.replace("\r", "")) except Exception as e: time.sleep(0.1) continue def read_ops_file(self, read_super_ops=True): """Keep a list of ops in the server instance to stop reading the disk for it. :rtype: Dictionary """ ops = False # (4 = PROTOCOL_1_7 ) - 1.7.6 or greater use ops.json if self.vitals.protocolVersion > 4: ops = getjsonfile( "ops", self.vitals.serverpath, encodedas=self.encoding ) if not ops: # try for an old "ops.txt" file instead. ops = [] opstext = getfileaslines("ops.txt", self.vitals.serverpath) if not opstext: return False for op in opstext: # create a 'fake' ops list from the old pre-1.8 # text line name list notice that the level (an # option not the old list) is set to 1 This will # pass as true, but if the plugin is also # checking op-levels, it may not pass truth. indivop = {"uuid": op, "name": op, "level": 1} ops.append(indivop) # Grant "owner" an op level above 4. required for some wrapper commands if read_super_ops: for eachop in ops: if eachop["name"] in self.vitals.ownernames: eachop["level"] = self.vitals.ownernames[eachop["name"]] return ops def refresh_ops(self, read_super_ops=True): self.vitals.ownernames = config_to_dict_read("superops.txt", ".") if self.vitals.ownernames == {}: sample = "<op_player_1>=10\n<op_player_2>=9" with open("superops.txt", "w") as f: f.write(sample) self.vitals.operator_list = self.read_ops_file(read_super_ops) def getmemoryusage(self): """Returns allocated memory in bytes. This command currently only works for *NIX based systems. """ if not resource or not os.name == "posix": raise OSError( "Your current OS (%s) does not support" " this command at this time." % os.name) if self.proc is None: self.log.debug("There is no running server to getmemoryusage().") return 0 try: with open("/proc/%d/statm" % self.proc.pid) as f: getbytes = int(f.read().split(" ")[1]) * resource.getpagesize() return getbytes except Exception as e: raise e @staticmethod def getstorageavailable(folder): """Returns the disk space for the working directory in bytes. """ if platform.system() == "Windows": free_bytes = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(folder), None, None, ctypes.pointer(free_bytes)) return free_bytes.value else: st = os.statvfs(folder) return st.f_bavail * st.f_frsize @staticmethod def stripspecial(text): # not sure what this is actually removing... # this must be legacy code of some kind pass a = "" it = iter(range(len(text))) for i in it: char = text[i] if char == "\xc2": try: next(it) next(it) except Exception as e: pass else: a += char return a def readconsole(self, buff): """Internally-used function that parses a particular console line. """ if len(buff) < 1: return # Standardize the line to only include the text (removing # time and log pre-pends) line_words = buff.split(' ')[self.prepends_offset:] # find the actual offset is where server output line # starts (minus date/time and info stamps). # .. and load the proper ops file if "Starting minecraft server version" in buff and \ self.prepends_offset == 0: for place in range(len(line_words)-1): self.prepends_offset = place if line_words[place] == "Starting": break line_words = buff.split(' ')[self.prepends_offset:] self.vitals.version = getargs(line_words, 4) semantics = self.vitals.version.split(".") release = get_int(getargs(semantics, 0)) major = get_int(getargs(semantics, 1)) minor = get_int(getargs(semantics, 2)) self.vitals.version_compute = minor + (major * 100) + (release * 10000) # noqa # 1.7.6 (protocol 5) is the cutoff where ops.txt became ops.json if self.vitals.version_compute > 10705 and self.vitals.protocolVersion < 0: # noqa self.vitals.protocolVersion = 5 self.wrapper.api.registerPermission("mc1.7.6", value=True) if self.vitals.version_compute < 10702 and self.wrapper.proxymode: self.log.warning("\nProxy mode cannot run because the " "server is a pre-Netty version:\n\n" "http://wiki.vg/Protocol_version_numbers" "#Versions_before_the_Netty_rewrite\n\n" "Server will continue in non-proxy mode.") self.wrapper.disable_proxymode() return self.refresh_ops() if len(line_words) < 1: return # the server attempted to print a blank line if len(line_words[0]) < 1: print('') return # parse or modify the server output section # # # Over-ride OP help console display if "/op <player>" in buff: new_usage = "player> [-s SUPER-OP] [-o OFFLINE] [-l <level>]" message = buff.replace("player>", new_usage) buff = message if "/whitelist <on|off" in buff: new_usage = "/whitelist <on|off|list|add|remvove|reload|offline|online>" # noqa message = new_usage buff = message if "While this makes the game possible to play" in buff: prefix = " ".join(buff.split(' ')[:self.prepends_offset]) if not self.wrapper.wrapper_onlinemode: message = ( "%s Since you are running Wrapper in OFFLINE mode, THIS " "COULD BE SERIOUS!\n%s Wrapper is not handling any" " authentication.\n%s This is only ok if this wrapper " "is not accessible from either port %s or port %s" " (I.e., this wrapper is a multiworld for a hub server, or" " you are doing your own authorization via a plugin)." % ( prefix, prefix, prefix, self.vitals.server_port, self.wrapper.proxy.proxy_port)) else: message = ( "%s Since you are running Wrapper in proxy mode, this" " should be ok because Wrapper is handling the" " authentication, PROVIDED no one can access port" " %s from outside your network." % ( prefix, self.vitals.server_port)) if self.wrapper.proxymode: buff = message # read port of server and display proxy port, if applicable if "Starting Minecraft server on" in buff: self.vitals.server_port = get_int(buff.split(':')[-1:][0]) # check for server console spam before printing to wrapper console server_spaming = False for things in self.vitals.spammy_stuff: if things in buff: server_spaming = True # server_spaming setting does not stop it from being parsed below. if not server_spaming: if not self.server_muted: self.wrapper.write_stdout(buff, "server") else: self.queued_lines.append(buff) first_word = getargs(line_words, 0) second_word = getargs(line_words, 1) # be careful about how these elif's are handled! # confirm server start if "Done (" in buff: self._toggle_server_started() self.changestate(STARTED) self.log.info("Server started") if self.wrapper.proxymode: self.log.info("Proxy listening on *:%s", self.wrapper.proxy.proxy_port) # noqa # Getting world name elif "Preparing level" in buff: self.vitals.worldname = getargs(line_words, 2).replace('"', "") self.world = World(self.vitals.worldname, self) # Player Message elif first_word[0] == "<": # get a name out of <name> name = self.stripspecial(first_word[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) playerobj = self.getplayer(name) if playerobj: self.wrapper.events.callevent("player.message", { "player": self.getplayer(name), "message": message, "original": original }, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> Player chat scrubbed from the console. <description> <abortable> No <abortable> <comments> This event is triggered by console chat which has already been sent. This event returns the player object. if used in a string context, ("%s") it's repr (self.__str__) is self.username (no need to do str(player) or player.username in plugin code). <comments> <payload> "player": playerobject (self.__str__ represents as player.username) "message": <str> type - what the player said in chat. ('hello everyone') "original": The original line of text from the console ('<mcplayer> hello everyone`) <payload> """ # noqa else: self.log.debug("Console has chat from '%s', but wrapper has no " "known logged-in player object by that name.", name) # noqa # Player Login elif second_word == "logged": user_desc = first_word.split("[/") name = user_desc[0] ip_addr = user_desc[1].split(":")[0] eid = get_int(getargs(line_words, 6)) locationtext = getargs(buff.split(" ("), 1)[:-1].split(", ") # spigot versus vanilla # SPIGOT - [12:13:19 INFO]: *******[/] logged in with entity id 123 at ([world]316.86789318152546, 67.12426603789697, -191.9069627257038) # noqa # VANILLA - [23:24:34] [Server thread/INFO]: *******[/127.0.0.1:47434] logged in with entity id 149 at (46.29907483845001, 63.0, -270.1293488726086) # noqa if len(locationtext[0].split("]")) > 1: x_c = get_int(float(locationtext[0].split("]")[1])) else: x_c = get_int(float(locationtext[0])) y_c = get_int(float(locationtext[1])) z_c = get_int(float(locationtext[2])) location = x_c, y_c, z_c self.login(name, eid, location, ip_addr) # Player Logout elif "lost connection" in buff: name = first_word self.logout(name) # player action elif first_word == "*": name = self.stripspecial(second_word) message = self.stripspecial(getargsafter(line_words, 2)) self.wrapper.events.callevent("player.action", { "player": self.getplayer(name), "action": message }, abortable=False) # Player Achievement elif "has just earned the achievement" in buff: name = self.stripspecial(first_word) achievement = getargsafter(line_words, 6) self.wrapper.events.callevent("player.achievement", { "player": name, "achievement": achievement }, abortable=False) # /say command elif getargs( line_words, 0)[0] == "[" and first_word[-1] == "]": if self.getservertype != "vanilla": # Unfortunately, Spigot and Bukkit output things # that conflict with this. return name = self.stripspecial(first_word[1:-1]) message = self.stripspecial(getargsafter(line_words, 1)) original = getargsafter(line_words, 0) self.wrapper.events.callevent("server.say", { "player": name, "message": message, "original": original }, abortable=False) # Player Death elif second_word in self.deathprefixes: name = self.stripspecial(first_word) self.wrapper.events.callevent("player.death", { "player": self.getplayer(name), "death": getargsafter(line_words, 1) }, abortable=False) # server lagged elif "Can't keep up!" in buff: skipping_ticks = getargs(line_words, 17) self.wrapper.events.callevent("server.lagged", { "ticks": get_int(skipping_ticks) }, abortable=False) # player teleport elif second_word == "Teleported" and getargs(line_words, 3) == "to": playername = getargs(line_words, 2) # [SurestTexas00: Teleported SapperLeader to 48.49417131908783, 77.67081086259394, -279.88880690937475] # noqa if playername in self.wrapper.servervitals.players: playerobj = self.getplayer(playername) playerobj._position = [ get_int(float(getargs(line_words, 4).split(",")[0])), get_int(float(getargs(line_words, 5).split(",")[0])), get_int(float(getargs(line_words, 6).split("]")[0])), 0, 0 ] self.wrapper.events.callevent( "player.teleport", {"player": playerobj}, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> When player teleports. <description> <abortable> No <abortable> <comments> driven from console message "Teleported ___ to ....". <comments> <payload> "player": player object <payload> """ # noqa elif first_word == "Teleported" and getargs(line_words, 2) == "to": playername = second_word # Teleported SurestTexas00 to 48.49417131908783, 77.67081086259394, -279.88880690937475 # noqa if playername in self.wrapper.servervitals.players: playerobj = self.getplayer(playername) playerobj._position = [ get_int(float(getargs(line_words, 3).split(",")[0])), get_int(float(getargs(line_words, 4).split(",")[0])), get_int(float(getargs(line_words, 5))), 0, 0 ] self.wrapper.events.callevent( "player.teleport", {"player": playerobj}, abortable=False) """ eventdoc <group> core/mcserver.py <group> <description> When player teleports. <description> <abortable> No <abortable> <comments> driven from console message "Teleported ___ to ....". <comments> <payload> "player": player object <payload> """ # noqa # mcserver.py onsecond Event Handlers def reboot_timer(self): rb_mins = self.reboot_minutes rb_mins_warn = self.config["General"]["timed-reboot-warning-minutes"] while not self.wrapper.halt.halt: time.sleep(1) timer = rb_mins - rb_mins_warn while self.vitals.state in (STARTED, STARTING): timer -= 1 time.sleep(60) if timer > 0: continue if timer + rb_mins_warn > 0: if rb_mins_warn + timer > 1: self.broadcast("&cServer will reboot in %d " "minutes!" % (rb_mins_warn + timer)) else: self.broadcast("&cServer will reboot in %d " "minute!" % (rb_mins_warn + timer)) countdown = 59 timer -= 1 while countdown > 0: time.sleep(1) countdown -= 1 if countdown == 0: if self.wrapper.backups_idle(): self.restart(self.reboot_message) else: self.broadcast( "&cBackup in progress. Server reboot " "delayed for one minute..") countdown = 59 if countdown % 15 == 0: self.broadcast("&cServer will reboot in %d " "seconds" % countdown) if countdown < 6: self.broadcast("&cServer will reboot in %d " "seconds" % countdown) continue if self.wrapper.backups_idle(): self.restart(self.reboot_message) else: self.broadcast( "&cBackup in progress. Server reboot " "delayed..") timer = rb_mins + rb_mins_warn + 1 def eachsecond_web(self): if time.time() - self.lastsizepoll > 120: if self.vitals.worldname is None: return True self.lastsizepoll = time.time() size = 0 # os.scandir not in standard library on early py2.7.x systems for i in os.walk( "%s/%s" % (self.vitals.serverpath, self.vitals.worldname) ): for f in os.listdir(i[0]): size += os.path.getsize(os.path.join(i[0], f)) self.vitals.worldsize = size def _console_event(self, payload): """This function is used in conjunction with event handlers to permit a proxy object to make a command call to this server.""" # make commands pass through the command interface. comm_pay = payload["command"].split(" ") if len(comm_pay) > 1: args = comm_pay[1:] else: args = [""] new_payload = {"player": self.wrapper.xplayer, "command": comm_pay[0], "args": args } self.wrapper.commands.playercommand(new_payload)
def __init__(self, wrapper): self.log = wrapper.log self.config = wrapper.config self.encoding = self.config["General"]["encoding"] self.serverpath = self.config["General"]["server-directory"] self.stop_message = self.config["Misc"]["stop-message"] self.reboot_message = self.config["Misc"]["reboot-message"] self.restart_message = self.config["Misc"]["default-restart-message"] self.reboot_minutes = self.config[ "General"]["timed-reboot-minutes"] self.reboot_warning_minutes = self.config[ "General"]["timed-reboot-warning-minutes"] # These will be used to auto-detect the number of prepend # items in the server output. self.prepends_offset = 0 self.wrapper = wrapper commargs = self.config["General"]["command"].split(" ") self.args = [] for part in commargs: if part[-4:] == ".jar": self.args.append("%s/%s" % (self.serverpath, part)) else: self.args.append(part) self.api = API(wrapper, "Server", internal=True) if "ServerStarted" not in self.wrapper.storage: self._toggle_server_started(False) self.state = OFF self.bootTime = time.time() # False/True - whether server will attempt boot self.boot_server = self.wrapper.storage["ServerStarted"] # whether a stopped server tries rebooting self.server_autorestart = self.config["General"]["auto-restart"] self.proc = None self.rebootWarnings = 0 self.lastsizepoll = 0 self.console_output_data = [] self.spammy_stuff = ["found nothing", "vehicle of", "Wrong location!", "Tried to add entity"] self.server_muted = False self.queued_lines = [] self.server_stalled = False self.deathprefixes = ["fell", "was", "drowned", "blew", "walked", "went", "burned", "hit", "tried", "died", "got", "starved", "suffocated", "withered", "shot"] if not self.wrapper.storage["ServerStarted"]: self.log.warning( "NOTE: Server was in 'STOP' state last time Wrapper.py was" " running. To start the server, run /start.") # Server Information self.players = {} self.player_eids = {} self.worldname = None self.worldSize = 0 self.maxPlayers = 20 # -1 until proxy mode checks the server's MOTD on boot self.protocolVersion = -1 # this is string name of the version, collected by console output self.version = None # a comparable number = x0y0z, where x, y, z = release, # major, minor, of version. self.version_compute = 0 # this port should be hidden from outside traffic. self.server_port = "25564" self.world = None self.entity_control = None self.motd = None # -1 until a player logs on and server sends a time update self.timeofday = -1 self.onlineMode = True self.serverIcon = None # get OPs self.ownernames = {} self.operator_list = [] self.refresh_ops() self.properties = {} # This will be redone on server start. However, it # has to be done immediately to get worldname; otherwise a # "None" folder gets created in the server folder. self.reloadproperties() # don't reg. an unused event. The timer still is running, we # just have not cluttered the events holder with another # registration item. if self.config["General"]["timed-reboot"] or self.config[ "Web"]["web-enabled"]: self.api.registerEvent("timer.second", self.eachsecond)
import api from api.base import API #from api.base import API api = API(wrapper, "Web", internal=True) world = api.minecraft.getWorld()