diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c18dd8d..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ diff --git a/__pycache__/classes.cpython-313.pyc b/__pycache__/classes.cpython-313.pyc index 31e2a2e..51dccec 100644 Binary files a/__pycache__/classes.cpython-313.pyc and b/__pycache__/classes.cpython-313.pyc differ diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc index 18ee267..02188a3 100644 Binary files a/__pycache__/config.cpython-313.pyc and b/__pycache__/config.cpython-313.pyc differ diff --git a/__pycache__/controllers.cpython-313.pyc b/__pycache__/controllers.cpython-313.pyc index e930446..7c0c8f5 100644 Binary files a/__pycache__/controllers.cpython-313.pyc and b/__pycache__/controllers.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index eb6b760..437c4ed 100644 Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/util.cpython-313.pyc b/__pycache__/util.cpython-313.pyc index 8c0432b..702c65c 100644 Binary files a/__pycache__/util.cpython-313.pyc and b/__pycache__/util.cpython-313.pyc differ diff --git a/classes.py b/classes.py index ceca19f..ca48faf 100644 --- a/classes.py +++ b/classes.py @@ -1,13 +1,6 @@ -from enum import Enum from typing import NotRequired, TypedDict -class ProcessStatus(Enum): - RUNNING = None - STOPPED = 0 - CRASHED = 1 - - class ServerPlayersList(TypedDict): online: int max: int diff --git a/config.py b/config.py index 7d57f4e..f90de32 100644 --- a/config.py +++ b/config.py @@ -4,6 +4,7 @@ from dotenv import dotenv_values class Config: data = dotenv_values() MINECRAFTD_PASSWORD: str | None = data.get("MINECRAFTD_PASSWORD") + PID_FILE: str = data.get("PID_FILE") or "server.pid" START_COMMAND: str = data.get("START_COMMAND") or "python proc.py" SERVER_HOST: str = data.get("SERVER_HOST") or "localhost" SERVER_PORT: int = int(data.get("SERVER_PORT") or 25565) diff --git a/controllers.py b/controllers.py index ac87ddb..24ddf33 100644 --- a/controllers.py +++ b/controllers.py @@ -1,56 +1,70 @@ import shlex from collections import deque +from os import setsid from pathlib import Path from subprocess import PIPE, Popen from time import sleep -from typing import Generator, Literal +from typing import Generator from mcrcon import MCRcon from mcstatus import JavaServer +from psutil import NoSuchProcess, Process -from .classes import ProcessStatus, ServerStatus +from .classes import ServerStatus from .config import Config class ProcessController: def __init__(self): + self.pid_file: Path = Path(Config.PID_FILE) self.start_command: list[str] = shlex.split(Config.START_COMMAND) - self.process: Popen | None = None - self.last_status: Literal[ProcessStatus.STOPPED, ProcessStatus.CRASHED] = ( - ProcessStatus.STOPPED - ) + self.process: Process | None = None + + if self.pid_file.is_file(): + pid = int(self.pid_file.read_text()) + if self._is_pid_alive(pid): + self.process = Process(pid) + else: + self.pid_file.unlink() def start(self) -> None: - "Start the process." - if self.status() == ProcessStatus.RUNNING: + "Starts the process." + if self.is_started(): return - self.process = Popen( + process = Popen( self.start_command, stdout=PIPE, stderr=PIPE, + preexec_fn=setsid, ) + self.process = Process(process.pid) + self.pid_file.write_text(str(self.process.pid)) - def status(self) -> ProcessStatus: - "Check the process' status." - if not self.process: - return self.last_status - match self.process.poll(): - case None: - return ProcessStatus.RUNNING - case 0: - self.last_status = ProcessStatus.STOPPED - return ProcessStatus.STOPPED - case _: - self.last_status = ProcessStatus.CRASHED - return ProcessStatus.CRASHED + def is_started(self) -> bool: + "Check if the process is running." + if self.process: + return self.process.is_running() + return False + + def is_crashed(self) -> bool: + "Check if the process has crashed." + return False # TODO def kill(self) -> None: "Kill the process." if self.process: self.process.terminate() self.process = None - self.last_status = ProcessStatus.STOPPED + self.pid_file.unlink() + + def _is_pid_alive(self, pid: int) -> bool: + "Check if a process with the given PID is alive." + try: + p = Process(pid) + return p.is_running() + except NoSuchProcess: + return False class ServerController: @@ -88,13 +102,10 @@ class ServerController: } def command(self, command: str) -> str: - try: - self.rcon.connect() - output = self.rcon.command(command) - self.rcon.disconnect() - return output - except Exception: - return "" + self.rcon.connect() + output = self.rcon.command(command) + self.rcon.disconnect() + return output class MaintainanceController: diff --git a/main.py b/main.py index e8faa57..5f74ce4 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,6 @@ from typing import Annotated from fastapi import FastAPI, Header from fastapi.responses import StreamingResponse -from .classes import ProcessStatus from .controllers import Controllers from .models import Models from .responses import Responses @@ -17,7 +16,7 @@ app = FastAPI() async def start() -> Responses.StartResponse: "Starts the Server process." - if Controllers.process.status() == ProcessStatus.RUNNING: + if Controllers.process.is_started(): return {"status": "running"} Controllers.process.start() return {"status": "started"} @@ -27,10 +26,9 @@ async def start() -> Responses.StartResponse: async def status() -> Responses.StatusResponse: "Checks whether the Server is running and returns its information." - process_status = Controllers.process.status() - if process_status != ProcessStatus.RUNNING: + if not Controllers.process.is_started(): # Crashed - if process_status == ProcessStatus.CRASHED: + if Controllers.process.is_crashed(): return {"status": "crashed"} # Maintainance if Controllers.maintainance.is_set(): @@ -38,18 +36,18 @@ async def status() -> Responses.StatusResponse: # Offline return {"status": "offline"} - server_status = Controllers.server.status() + status = Controllers.server.status() # Starting - if not server_status["online"]: + if not status["online"]: return {"status": "starting"} # Online return { "status": "online", - "motd": server_status.get("motd", ""), - "icon": server_status.get("icon", None), - "players": server_status.get( + "motd": status.get("motd", ""), + "icon": status.get("icon", None), + "players": status.get( "players", { "online": 0, diff --git a/util.py b/util.py index 7a1588f..5c7d38a 100644 --- a/util.py +++ b/util.py @@ -3,8 +3,6 @@ from typing import Callable from fastapi import HTTPException -from minecraftd.classes import ProcessStatus - from .config import Config from .controllers import Controllers @@ -30,12 +28,12 @@ async def stop_server( await sleep(1) countdown -= 1 - Controllers.server.command("stop") - while timeout > 0 and Controllers.process.status() == ProcessStatus.RUNNING: + # Controllers.server.command("stop") + while timeout > 0 and Controllers.process.is_started(): await sleep(1) timeout -= 1 - if Controllers.process.status() == ProcessStatus.RUNNING: + if Controllers.process.is_started(): Controllers.process.kill() if then: