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 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
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 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 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 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 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)
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()
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 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 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()