Hilfe beim Stoppen eines Minecraft-Servers über ein Python-Skript

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Ghostplayer
User
Beiträge: 4
Registriert: Montag 30. Dezember 2024, 22:12

Hallo zusammen,

ich versuche, ein Python-Skript zu erstellen, mit dem ich eine Schnittstelle zur Minecraft-Server-Konsole habe. Der Server wird derzeit wie folgt gestartet:

Code: Alles auswählen

def start_server(server_name, install_dir="servers"):
    global subprocess_server
    server_path = os.path.join(install_dir, server_name)

    # Ensure the start script exists
    run_script = "run.bat" if os.name == 'nt' else "run.sh"
    run_script_path = os.path.join(server_path, run_script)

    if not os.path.exists(run_script_path):
        print(f"Error: {run_script} not found. Ensure the server is correctly installed.")
        return False

    # Start the server
    try:
        if os.name == 'nt':  # Windows
            subprocess_server = subprocess.Popen(
                ["cmd", "/c", "start", "cmd", "/k", run_script],
                cwd=server_path
            )
        else:  # Unix-based systems
            subprocess_server = subprocess.Popen(
                ["bash", run_script],
                cwd=server_path,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )
        print(f"Server '{server_name}' started.")
        return True
    except Exception as e:
        print(f"Error starting the server: {e}")
        return False
Jetzt möchte ich den Server stoppen. Wenn ich jedoch denselben Subprozess über die subprocess_server-Variable aufrufe und versuche, einen "stop"-Befehl auszuführen, passiert nichts.

Hat jemand von euch eine Idee, wie ich das erreichen kann?
Benutzeravatar
sparrow
User
Beiträge: 4525
Registriert: Freitag 17. April 2009, 10:28

Aus welchem Grund ist es nötig hier global zu verwenden? Globale Variablen verwendet man in der Regel nicht. Egal in welcher Programmiersprache. Die Ausnahmen sind selten.

Was ist denn der "stop-Befehl". Zeig bitte, was du versucht hast und was du erreichen möchtest.
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Ghostplayer: Ergänzend zu sparrow: Statt des Fehlerrückgabewertes sollte man eine Ausnahme nicht einfach durch ein `print()` und ein `False` ersetzen, sondern die Ausnahme an dieser Stelle einfach gar nicht behandeln. Und in selbst festgestellten Fehlerfällen selber eine Ausnahme auslösen statt eine `print()`-Ausgabe zu machen und `False` zurück zu geben.

Wenn dieses `True`/`False` weg ist, kann man das `Popen`-Objekt als Ergebnis der Funktion benutzen und schon ist man das ``global`` und die globale Variable an der Stelle los!

Statt der Funktionen aus `os.path` verwendet man in neuem Code besser das `pathlib`-Modul.

Falls Du nicht Yoda bist, sollte `subprocess_server` korrekt `server_subprocess` heissen, denn ein Prozess-Server ist was anderes als ein Server-Prozess.

Je nach Betriebssystem verhalten sich die `Popen`-Objekte recht unterschiedlich: Das eine hat keine Umleitungen der Standardein- und Ausgabe, das andere hat die. Das sollte einheitlich sein.

Unter Unixoiden Systemen sind Skripte über die Dateirechte als ausführbar gekennzeichnet und der Interpreter wird *im* Skript über die She-Bang-Zeile angegeben. Da sollte man nicht selbst eine ``bash`` starten.

Unter Windows sind ".bat"-Dateien auch ohne dieses sogar zweimalige Aufrufen einer ``cmd.exe`` ausführbar. Eventuell ist das ``start.exe`` da drin sogar das Problem, dass Du den Prozess nicht stoppen kannst, denn es kann sein, dass ``start.exe`` sofort beendet wird, nachdem es das Skript gestartet hat, und zwar eben nicht als Kindprozess, sondern eigenständig.

Zwischenstand (ungetestet):

Code: Alles auswählen

import os
import subprocess
from pathlib import Path


def start_server(server_name, install_dir=Path("servers")):
    run_script_file_path = (install_dir / server_name / "run").with_suffix(
        ".bat" if os.name == "nt" else ".sh"
    )
    if not run_script_file_path.exists():
        raise ValueError(
            f"{run_script_file_path.name!r} not found."
            f" Ensure the server is correctly installed."
        )

    server_process = subprocess.Popen(
        [run_script_file_path.name], cwd=run_script_file_path.parent
    )
    #
    # TODO Don't use print for logging.
    #
    print(f"Server {server_name!r} started.")
    return server_process
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Ghostplayer
User
Beiträge: 4
Registriert: Montag 30. Dezember 2024, 22:12

Also der Code zum stoppen war bis jetzt:

Code: Alles auswählen

def stop_mc_server():
    global subprocess_server
    if subprocess_server:
        try:
            print("Stopping Minecraft server...")
            subprocess_server.stdin.write("stop\n")
            subprocess_server.stdin.flush()
            subprocess_server.wait()
            print("Server stopped.")
        except Exception as e:
            print(f"Error stopping the server: {e}")
        finally:
            subprocess_server = None
    else:
        print("No server is currently running.")
Benutzeravatar
sparrow
User
Beiträge: 4525
Registriert: Freitag 17. April 2009, 10:28

Und? Mehr Informationen?
Hat der mal funktioniert? Geht der nun nicht mehr?
Warum global?
Gibt es eine Fehlermeldung?
Falls das mal funktioniert hat: Was hat sich geändert?
Auf welchem OS läuft der Server?
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Ghostplayer: Falls `subprocess_server` einen Wert hat der wahr ist, dann führt das beim gezeigten Code für's starten zu einer Ausnahme und es wird ein Fehlertext ausgegeben. So etwas musst Du schon verraten. Also auch den Fehlertext zeigen, damit wir nicht raten müssen.

Auch hier ist das ``global`` wieder sehr schlecht — die Funktion sollte das `Popen`-Objekt als Argument übergeben bekommen. Und sie sollte nur aufgerufen werden wenn es eines gibt, dann spart man sich den Test.

Die `print()`-Aufrufe sehen wieder nach einem Job für Logging aus.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Ghostplayer
User
Beiträge: 4
Registriert: Montag 30. Dezember 2024, 22:12

Aktuell läuft das Programm nur unter Windows, soll aber später auch auf Linux erweitert werden. Das global stammt daher, dass ich ursprünglich aus Java komme und erst vor Kurzem mit Python angefangen habe.

Es gibt keine Fehlermeldung, aber es passiert schlicht nichts. Laut meinen Print-Ausgaben scheint alles wie erwartet ausgeführt zu werden. Ich vermute, dass das Problem darin liegt, dass subprocess nur eine .bat-Datei ausführt, die wiederum einen Child-Prozess erzeugt.

Ich habe den Code inzwischen angepasst (den überarbeiteten Code sende ich, sobald ich zuhause bin). Mit dieser Änderung kann ich zumindest auf die Konsolen-Ausgabe zugreifen, der Code ist aber weiterhin sehr ähnlich. Das eigentliche Problem bleibt jedoch bestehen: Der stop-Befehl wird nicht ausgeführt.

Viele Grüße! Ich hoffe, das klärt alle offenen Fragen.
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Ghostplayer: Wenn es keine Fehlermeldung gibt, dann wird der Code in der Tat nicht ausgeführt, das heisst die Funktion wird gar nicht erst aufgerufen. Den Code der das aufruft, beziehungsweise *nicht* aufruft, kennen wir ja nicht.

Wenn die Ausgaben zu Deinem Prozess umgeleitet werden, dann musst Du die auch auslesen. *Beide* und *gleichzeitig*, denn wenn man das nacheinander macht, kann es zu Verklemmungen („dead locks“) führen und das ganze Programm bleibt hängen.

Wenn Du von Java kommst, kennst Du ja Klassen, also weisst wie man sich Zustand über Aufrufe hinweg in einem Objekt merkt, also gibt es so gar keine Ausrede ``global`` zu verwenden. 🙂
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Ghostplayer
User
Beiträge: 4
Registriert: Montag 30. Dezember 2024, 22:12

Lösung:

Code: Alles auswählen

def start_forge_server(server_name, install_dir="servers", min_memory="4G", max_memory="7G"):
    global global_forge_server_process
    try:
        server_path = os.path.join(install_dir, server_name)
        win_args_path = os.path.join(server_path, "libraries/net/minecraftforge/forge/1.20.2-48.1.0/win_args.txt")
        if not os.path.exists(win_args_path):
            raise FileNotFoundError("Required win_args.txt file not found in the server directory.")

        with open(win_args_path, "r") as file:
            win_args = file.read().strip().split()

        command = ["java", f"-Xms{min_memory}", f"-Xmx{max_memory}"] + win_args + ["nogui"]
        process = subprocess.Popen(command, cwd=server_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        global_forge_server_process = process

        def enqueue_output(out, log_entries):
            for line in iter(out.readline, ''):
                log_entries.append(line)
                if 'Done (' in line:
                    global server_status, status_color
                    server_status = "Eingeschaltet"
                    status_color = "on"
            out.close()

        threading.Thread(target=enqueue_output, args=(process.stdout, log_entries)).start()
        threading.Thread(target=enqueue_output, args=(process.stderr, log_entries)).start()
    except Exception as e:
        print(f"Error starting the server: {e}")
        
def send_command_to_server(command):
    global global_forge_server_process
    if global_forge_server_process and global_forge_server_process.stdin:
        try:
            global_forge_server_process.stdin.write(command + "\n")
            global_forge_server_process.stdin.flush()
        except Exception as e:
            print(f"Fehler beim Senden des Befehls: {e}")
PS: Ich weiß, dass Logging ist noch nicht gut und auch global sollte ich dort nicht verwenden. Daher ist es auch noch wip und nicht fertig.
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Ghostplayer: Da ist dreimal ``global`` drin und eine globale Variable ohne dieses Schlüsselwort — das ist keine Lösung, sondern ein Problem. Und das ist nichts was man irgendwann mal gerade biegt, so einen Unsinn sollte man gar nicht erst anfangen. Denn entweder macht man das dann am Ende doch nie ordentlich, oder stellt *dann* erst fest was für ein Chaos man da angerichtet hat, das sich gar nicht mehr so einfach ordentlich machen lässt.

Wenn man `os.path.join()` verwendet und dann doch wieder in Teile hart einen Pfadtrenner schreibt, macht das irgendwie keinen Sinn. Und `pathlib` hatte ich ja schon erwähnt.

Man prüft nicht ob eine Datei existiert bevor man sie öffnet. Dieser Test ist im öffnen schon enthalten, und zwischen dem unnötigen Extratext und dem Öffnen kann es sich das ja schon wieder geändert haben. Du löst ja sogar genau die Ausnahme aus die sowieso schon ausgelöst wird.

Und das hatte ich ja auch schon erwähnt: Man behandelt keine Ausnahmen in dem man sie einfach durch Print-Ausgaben ersetzt und dann so weitermacht als wäre nix passiert. Das führt doch dann nur zu Folgeausnahmen und Aufrufer haben gar keine Möglichkeit auf Ausnahmefälle zu reagieren.

Beim öffnen von Textdateien sollte man die Kodierung angeben.

Ein `strip()` vor einem ``split()`` ist unnötig:

Code: Alles auswählen

In [15]: " a b    c d \n".split()
Out[15]: ['a', 'b', 'c', 'd']
``for line in iter(file.readline, "")`` ist recht umständlich für ``for line in file:``.

Bei Threads möchte man in der Regel die `daemon`-Option haben.

Zum stoppen möchte man nicht einfach nur den "stop"-Befehl senden, denn man muss ja auch den Prozess sauber abräumen und den anderen Zustand entsprechend setzen.

Zwischenstand (ungetestet):

Code: Alles auswählen

import subprocess
from pathlib import Path
from threading import Thread


class Server:
    def __init__(self, name, install_path=Path("servers")):
        self.path = install_path / name
        self.process = None
        self.log_entries = []
        self.is_on = False

    def _process_output(self, file):
        with file:
            for line in file:
                self.log_entries.append(line)
                if "Done (" in line:
                    self.is_on = True

    def start(self, min_memory="4G", max_memory="7G"):
        arguments = (
            (
                self.path
                / "libraries"
                / "net"
                / "minecraftforge"
                / "forge"
                / "1.20.2-48.1.0"
                / "win_args.txt"
            )
            .read_text(encoding="utf-8")
            .split()
        )
        self.process = subprocess.Popen(
            [
                "java",
                f"-Xms{min_memory}",
                f"-Xmx{max_memory}",
                *arguments,
                "nogui",
            ],
            cwd=self.path,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        for file in [self.process.stdout, self.process.stderr]:
            Thread(
                target=self._process_output, args=[file], daemon=True
            ).start()

    def send_command(self, command):
        self.process.stdin.write(command + "\n")
        self.process.stdin.flush()

    def stop(self):
        self.send_command("stop")  # ???
        return_code = self.process.wait()
        self.process = None
        self.is_on = False
        return return_code
Wird `log_entries` nicht recht schnell unschön gross?
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Antworten