Example #1
0
def notified_set_status(instance: str,
                        action: str,
                        message: str = '',
                        persistent: bool = False) -> None:
    """Notifies the Players on the Server if applicable and sets the Service Status.

    Arguments:
        instance (str): The name of the instance.
        action (str): The systemd action to apply to the service. Can be "start", "restart", "stop".

    Keyword Arguments:
        message (str): A message relayed to Server Chat, e.g. reason the Server is shutting down.
        persistent (bool): If True, the Server will not start after a Machine reboot (default: {False})
        restart (bool): If True, persistent wil be ignored and the server wil be restarted (default: {False})
    """
    if action not in ("start", "restart", "stop"):
        raise ValueError(f"Invalid action '{action}'")

    unit = service.get_unit(instance)
    if persistent and action != "restart":
        enable = (action == "start")
        service.set_persistence(unit, enable)

    if action in ("stop", "restart"):
        msgcol = "6" if action == "restart" else "4"
        msg = f"say ยง{msgcol}Server {action} pending"
        msg += f": {message}" if message else "."
        try:
            proc.mc_exec(instance, shlex.split(msg))
        except ConnectionError:
            pass
    service.set_status(unit, action)
Example #2
0
def update(instance: str,
           source: str,
           literal_url: bool = False,
           restart: bool = False) -> None:
    """Change the Jar File of a server.

    Stops the Server if necessary, deletes the old Jar File and copies the new one, starts the Server again.

    Arguments:
        instance (str): The Instance ID.
        source (str): The Type ID or URL of the new minecraft server Jar.
        literal_url (bool): Determines if the TypeID is a literal URL. Default: False
        allow_restart (bool): Allows a Server restart if the Server is running. Default: False
    """
    jar_src, version = web.pull(source, literal_url)
    jar_dest = storage.get_instance_path(instance) / "server.jar"
    storage.symlink(jar_src, jar_dest)

    additions = ''
    unit = service.get_unit(instance)
    if service.is_active(unit) and restart:
        notified_set_status(instance, "restart",
                            f"Updating to Version {version}")
    else:
        additions = " Manual restart required."
    print(f"Update successful.{additions}")
Example #3
0
def list_instances(filter_str: str = '') -> None:
    """Print a list of all instances.

    Output a table of all instances with their respective Name, Server Version String, Status and persistence.

    Keyword Arguments:
        filter_str (str): Filter the list by instance name. (default: {''})
    """
    base_path = storage.get_instance_path(bare=True)
    server_paths = base_path.iterdir()
    servers = [x.name for x in server_paths]

    name_col_width = str(len(max(servers, key=len)) + 1)
    template = "{:" + name_col_width + "} {:<6} {:20} {:14} {:10} {:10}"
    title = template.format("Name", "Port", "Server Version", "Player Count",
                            "Status", "Persistent")

    print(title)
    for name in servers:
        if filter_str in name:
            cfg = config.get_properties(base_path / name / "server.properties")
            port = int(cfg.get("server-port"))
            server = MinecraftServer('localhost', port)
            status_info = status.get_simple_status(server)

            unit = service.get_unit(name)
            state = get_online_state(unit, status_info.get("proto"))

            player_ratio = f"{status_info.get('online')}/{cfg.get('max-players')}"
            contents = template.format(name, port, status_info.get("version"),
                                       player_ratio, state.capitalize(),
                                       str(service.is_enabled(unit)))
            print(contents)
Example #4
0
def rename(instance: str, new_name: str) -> None:
    """Rename a server instance.

    A server instance is renamed. The server has to be stopped and disabled, so no invalid service links can occur.

    Arguments:
        instance (str): Current name of the Instance.
        new_name (str): New name of the Instance.
    """
    unit = service.get_unit(instance)
    if (service.is_enabled(unit) or service.is_active(unit)):
        raise OSError("The server is still persistent and/or running.")
    server_path = storage.get_instance_path(instance)
    server_path.rename(server_path.parent / new_name)
Example #5
0
def mc_exec(instance: str,
            command: list,
            pollrate: float = 0.2,
            max_retries: int = 24,
            max_flush_retries: int = 4) -> None:
    """Execute a command on the console of a server.

    Uses the 'stuff' command of screen to pass the minecraft command to the server.
    Return Values are read from 'latest.log' shortly after the command is executed.
    The logfile is read every <timeout> seconds. If nothing is appended to the Log after the set amount of <retries>,
    the function exits. If there were already some lines received, the function tries <flush_retries> times before exiting.
    Like this, the function will more likely give an output, and will exit faster if an output was already returned.

    Arguments:
        instance (str): The name of the instance.
        command (list): A list of the individual parts of the command executed on the server console.

    Keyword Arguments:
        pollrate (float): The polling interval between log reads/checks. (default: {0.2})
        max_retries (int): The amount of retries when no lines have been pushed to console. (default: {25})
        max_flush_retries (int): The amount of retries when some lines have been pushed to console. (default: {10})
    """
    unit = service.get_unit(instance)
    if not service.is_active(unit):
        raise OSError("The Server is not running.")
    elif not status.is_ready(instance):
        raise ConnectionError("The Server is starting up.")

    log_path = storage.get_instance_path(instance) / "logs/latest.log"

    with open(log_path) as log_file:
        old_count = sum(1 for line in log_file) - 1

        jar_cmd = " ".join(command)
        # Use ^U^Y to cut and paste Text already in the Session
        cmd = shlex.split(
            f"screen -p 0 -S mc-{instance} -X stuff '^U{jar_cmd}^M^Y'")
        proc = sproc.Popen(cmd, preexec_fn=perms.demote())  # nopep8 pylint: disable=subprocess-popen-preexec-fn
        proc.wait()

        i = 0
        while i < max_retries:
            i += 1
            time.sleep(pollrate)
            log_file.seek(0)
            for j, line in enumerate(log_file):
                if j > old_count:
                    i = max_retries - max_flush_retries
                    print(line.rstrip())
                    old_count += 1
Example #6
0
def attach(instance: str) -> None:
    """Attach to the console of a server.

    Launches screen to reattach to the screen session of the server.

    Arguments:
        instance (str): The name of the instance.
    """
    unit = service.get_unit(instance)
    if not service.is_active(unit):
        raise OSError("The Server is not running.")
    cmd = shlex.split(f"screen -r mc-{instance}")
    proc = sproc.Popen(cmd)
    proc.wait()
    clear()
Example #7
0
def remove(instance: str, force: bool = False) -> None:
    """Remove an instance from disk.

    Arguments:
        instance (str): The name of the Instance to be deleted.

    Keyword Arguments:
        force (bool): Delete the Instance without a prompt. (default: {False})
    """
    del_path = get_instance_path(instance)
    if not del_path.exists():
        raise FileNotFoundError(f"Instance Path not found: {del_path}.")
    unit = service.get_unit(instance)
    if (service.is_enabled(unit) or service.is_active(unit)):
        raise OSError("The server is still running and/or persistent.")

    prompt_msg = f"Are you absolutely sure you want to remove the Instance '{instance}'?"
    do_remove = force or visuals.bool_selector(prompt_msg)

    if do_remove:
        remove_all(del_path)
Example #8
0
def collect_server_data(instance: str) -> dict:
    """Collect Data about the server and return it as a dict.

    Args:
        instance (str): The instance of which the data should be collected.

    Returns:
        dict: A dict containing various Information about the server.
    """
    instance_path = storage.get_instance_path(instance)
    if not instance_path.exists():
        raise FileNotFoundError(f"Instance not found: {instance_path}.")

    properties = config.get_properties(instance_path / "server.properties")
    try:
        envinfo = config.get_properties(instance_path /
                                        CFGVARS.get('system', 'env_file'))
    except FileNotFoundError:
        envinfo = None

    port = properties.get("server-port")
    server = MinecraftServer('localhost', int(port))
    status_info = status.get_simple_status(server)

    files = storage.get_child_paths(instance_path)
    total_size = sum(x.stat().st_size for x in files)

    unit = service.get_unit(instance)
    state = get_online_state(unit, status_info.get("proto"))

    cmdvars = {
        k: v
        for k, v in (x.decode().split("=") for x in unit.Service.Environment)
    }
    cmd = " ".join(x.decode() for x in unit.Service.ExecStart[0][1])
    resolved_cmd = cmd.replace("${", "{").format(**cmdvars)

    jar_path = instance_path / cmdvars.get("JARFILE", "server.jar")
    resolved_jar_path = storage.get_real_abspath(jar_path)
    type_id = None
    if resolved_jar_path != jar_path:
        type_id = storage.get_type_id(resolved_jar_path)

    try:
        with open(instance_path / "whitelist.json") as wlist_hnd:
            whitelist = json.load(wlist_hnd)
    except FileNotFoundError:
        whitelist = None

    try:
        plugins = plugin.get_plugins(instance)
    except FileNotFoundError:
        plugins = None

    data = {
        "instance_name": instance,
        "instance_path": str(instance_path),
        "total_file_size": total_size,
        "status": {
            "players_online": status_info.get('online'),
            "players_max": properties.get('max-players', '?'),
            "protocol_name": status_info.get('proto'),
            "protocol_version": status_info.get('version'),
        },
        "service": {
            "description": unit.Unit.Description.decode(),
            "unit_file_state": unit.UnitFileState.decode(),
            "state": state,
            "main_pid": unit.Service.MainPID,
            "start_command": resolved_cmd,
            "memory_usage": unit.Service.MemoryCurrent,
            "env": dict(cmdvars),
        },
        "type_id": type_id,
        "config": {
            "server.properties": properties,
            "whitelist": whitelist,
            "env_file": envinfo
        },
        "plugins": plugins,
    }

    return data
Example #9
0
def configure(instance: str,
              editor: str,
              properties: list = None,
              edit_paths: list = None,
              memory: str = None,
              restart: bool = False) -> None:
    """Edits configurations, restarts the server if forced, and swaps in the new configurations.

    Args:
        instance (str): The Instance ID.
        editor (str): A Path to an Editor Binary.
        properties (list): The Properties to be changed in the server.properties File.
        edit_paths (list): The Paths to be edited interactively with the specified Editor.
        memory (str): Update the Memory Allocation. Can be appended by K, M or G, to signal Kilo- Mega- or Gigabytes.
        restart (bool, optional): Stops the server, applies changes and starts it again when set to true.
        Defaults to False.
    """
    if not any((properties, edit_paths, memory)):
        raise ValueError("No properties or files to edit specified.")

    instance_path = storage.get_instance_path(instance)
    paths = {}

    if properties:
        properties_path = instance_path / "server.properties"
        tmp_path = storage.tmpcopy(properties_path)
        properties_dict = config.properties_to_dict(properties)
        config.set_properties(tmp_path, properties_dict)
        paths.update({properties_path: tmp_path})

    if memory:
        env_path = instance_path / CFGVARS.get('system', 'env_file')
        tmp_path = storage.tmpcopy(env_path)
        config.set_properties(tmp_path, {"MEM": memory})
        paths.update({env_path: tmp_path})

    if edit_paths:
        for file_path in edit_paths:
            # Check if a Temporary File of the Config already exists
            if file_path not in paths.keys():
                abspath = instance_path / file_path
                tmp_path = storage.tmpcopy(abspath)
                proc.edit(tmp_path, editor)
                if storage.get_file_hash(tmp_path) != storage.get_file_hash(
                        abspath):
                    paths.update({abspath: tmp_path})
                else:
                    tmp_path.unlink()
            else:
                proc.edit(paths[file_path], editor)

    unit = service.get_unit(instance)
    do_restart = service.is_active(unit) and len(paths) > 0 and restart
    if do_restart:
        notified_set_status(instance, "stop",
                            "Reconfiguring and restarting Server.")

    for dst, src in paths.items():
        storage.move(src, dst)

    if do_restart:
        notified_set_status(instance, "start")