This commit is contained in:
Malasaur 2025-12-01 23:29:08 +01:00
commit f110d66de9
11 changed files with 53 additions and 53 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,14 @@
from enum import Enum
from typing_extensions import TypedDict from typing_extensions import TypedDict
from typing import NotRequired from typing import NotRequired
class ProcessStatus(Enum):
RUNNING = None
STOPPED = 0
CRASHED = 1
class ServerPlayersList(TypedDict): class ServerPlayersList(TypedDict):
online: int online: int
max: int max: int

View file

@ -4,7 +4,6 @@ from dotenv import dotenv_values
class Config: class Config:
data = dotenv_values() data = dotenv_values()
MINECRAFTD_PASSWORD: str | None = data.get("MINECRAFTD_PASSWORD") 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" START_COMMAND: str = data.get("START_COMMAND") or "python proc.py"
SERVER_HOST: str = data.get("SERVER_HOST") or "localhost" SERVER_HOST: str = data.get("SERVER_HOST") or "localhost"
SERVER_PORT: int = int(data.get("SERVER_PORT") or 25565) SERVER_PORT: int = int(data.get("SERVER_PORT") or 25565)

View file

@ -1,70 +1,56 @@
import shlex import shlex
from collections import deque from collections import deque
from os import setsid
from pathlib import Path from pathlib import Path
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from time import sleep from time import sleep
from typing import Generator from typing import Generator, Literal
from mcrcon import MCRcon from mcrcon import MCRcon
from mcstatus import JavaServer from mcstatus import JavaServer
from psutil import NoSuchProcess, Process
from .classes import ServerStatus from .classes import ProcessStatus, ServerStatus
from .config import Config from .config import Config
class ProcessController: class ProcessController:
def __init__(self): def __init__(self):
self.pid_file: Path = Path(Config.PID_FILE)
self.start_command: list[str] = shlex.split(Config.START_COMMAND) self.start_command: list[str] = shlex.split(Config.START_COMMAND)
self.process: Process | None = None self.process: Popen | None = None
self.last_status: Literal[ProcessStatus.STOPPED, ProcessStatus.CRASHED] = (
if self.pid_file.is_file(): ProcessStatus.STOPPED
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: def start(self) -> None:
"Starts the process." "Start the process."
if self.is_started(): if self.status() == ProcessStatus.RUNNING:
return return
process = Popen( self.process = Popen(
self.start_command, self.start_command,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
preexec_fn=setsid,
) )
self.process = Process(process.pid)
self.pid_file.write_text(str(self.process.pid))
def is_started(self) -> bool: def status(self) -> ProcessStatus:
"Check if the process is running." "Check the process' status."
if self.process: if not self.process:
return self.process.is_running() return self.last_status
return False match self.process.poll():
case None:
def is_crashed(self) -> bool: return ProcessStatus.RUNNING
"Check if the process has crashed." case 0:
return False # TODO self.last_status = ProcessStatus.STOPPED
return ProcessStatus.STOPPED
case _:
self.last_status = ProcessStatus.CRASHED
return ProcessStatus.CRASHED
def kill(self) -> None: def kill(self) -> None:
"Kill the process." "Kill the process."
if self.process: if self.process:
self.process.terminate() self.process.terminate()
self.process = None self.process = None
self.pid_file.unlink() self.last_status = ProcessStatus.STOPPED
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: class ServerController:
@ -102,10 +88,13 @@ class ServerController:
} }
def command(self, command: str) -> str: def command(self, command: str) -> str:
self.rcon.connect() try:
output = self.rcon.command(command) self.rcon.connect()
self.rcon.disconnect() output = self.rcon.command(command)
return output self.rcon.disconnect()
return output
except Exception:
return ""
class MaintainanceController: class MaintainanceController:

18
main.py
View file

@ -4,6 +4,7 @@ from typing import Annotated
from fastapi import FastAPI, Header from fastapi import FastAPI, Header
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from .classes import ProcessStatus
from .controllers import Controllers from .controllers import Controllers
from .models import Models from .models import Models
from .responses import Responses from .responses import Responses
@ -16,7 +17,7 @@ app = FastAPI()
async def start() -> Responses.StartResponse: async def start() -> Responses.StartResponse:
"Starts the Server process." "Starts the Server process."
if Controllers.process.is_started(): if Controllers.process.status() == ProcessStatus.RUNNING:
return {"status": "running"} return {"status": "running"}
Controllers.process.start() Controllers.process.start()
return {"status": "started"} return {"status": "started"}
@ -26,9 +27,10 @@ async def start() -> Responses.StartResponse:
async def status() -> Responses.StatusResponse: async def status() -> Responses.StatusResponse:
"Checks whether the Server is running and returns its information." "Checks whether the Server is running and returns its information."
if not Controllers.process.is_started(): process_status = Controllers.process.status()
if process_status != ProcessStatus.RUNNING:
# Crashed # Crashed
if Controllers.process.is_crashed(): if process_status == ProcessStatus.CRASHED:
return {"status": "crashed"} return {"status": "crashed"}
# Maintainance # Maintainance
if Controllers.maintainance.is_set(): if Controllers.maintainance.is_set():
@ -36,18 +38,18 @@ async def status() -> Responses.StatusResponse:
# Offline # Offline
return {"status": "offline"} return {"status": "offline"}
status = Controllers.server.status() server_status = Controllers.server.status()
# Starting # Starting
if not status["online"]: if not server_status["online"]:
return {"status": "starting"} return {"status": "starting"}
# Online # Online
return { return {
"status": "online", "status": "online",
"motd": status.get("motd", ""), "motd": server_status.get("motd", ""),
"icon": status.get("icon", None), "icon": server_status.get("icon", None),
"players": status.get( "players": server_status.get(
"players", "players",
{ {
"online": 0, "online": 0,

View file

@ -3,6 +3,8 @@ from typing import Callable
from fastapi import HTTPException from fastapi import HTTPException
from minecraftd.classes import ProcessStatus
from .config import Config from .config import Config
from .controllers import Controllers from .controllers import Controllers
@ -28,12 +30,12 @@ async def stop_server(
await sleep(1) await sleep(1)
countdown -= 1 countdown -= 1
# Controllers.server.command("stop") Controllers.server.command("stop")
while timeout > 0 and Controllers.process.is_started(): while timeout > 0 and Controllers.process.status() == ProcessStatus.RUNNING:
await sleep(1) await sleep(1)
timeout -= 1 timeout -= 1
if Controllers.process.is_started(): if Controllers.process.status() == ProcessStatus.RUNNING:
Controllers.process.kill() Controllers.process.kill()
if then: if then: