コード例 #1
0
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"])
コード例 #2
0
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"])
コード例 #3
0
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
コード例 #4
0
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)
コード例 #5
0
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.')
コード例 #6
0
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()
コード例 #7
0
ファイル: irc.py プロジェクト: benbaptist/minecraft-wrapper
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.')
コード例 #8
0
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)
コード例 #9
0
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()
コード例 #10
0
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()
コード例 #11
0
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()
コード例 #12
0
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()