Changed ProcessController to handle Server as child subprocess

This commit is contained in:
Malasaur 2025-12-01 22:43:15 +01:00
parent 9742cafad9
commit a008bc27fa
No known key found for this signature in database
5 changed files with 52 additions and 53 deletions

View file

@ -1,6 +1,13 @@
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

View file

@ -4,7 +4,6 @@ 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)

View file

@ -1,70 +1,56 @@
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
from typing import Generator, Literal
from mcrcon import MCRcon
from mcstatus import JavaServer
from psutil import NoSuchProcess, Process
from .classes import ServerStatus
from .classes import ProcessStatus, 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: 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()
self.process: Popen | None = None
self.last_status: Literal[ProcessStatus.STOPPED, ProcessStatus.CRASHED] = (
ProcessStatus.STOPPED
)
def start(self) -> None:
"Starts the process."
if self.is_started():
"Start the process."
if self.status() == ProcessStatus.RUNNING:
return
process = Popen(
self.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 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 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 kill(self) -> None:
"Kill the process."
if self.process:
self.process.terminate()
self.process = None
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
self.last_status = ProcessStatus.STOPPED
class ServerController:
@ -102,10 +88,13 @@ class ServerController:
}
def command(self, command: str) -> str:
self.rcon.connect()
output = self.rcon.command(command)
self.rcon.disconnect()
return output
try:
self.rcon.connect()
output = self.rcon.command(command)
self.rcon.disconnect()
return output
except Exception:
return ""
class MaintainanceController:

18
main.py
View file

@ -4,6 +4,7 @@ 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
@ -16,7 +17,7 @@ app = FastAPI()
async def start() -> Responses.StartResponse:
"Starts the Server process."
if Controllers.process.is_started():
if Controllers.process.status() == ProcessStatus.RUNNING:
return {"status": "running"}
Controllers.process.start()
return {"status": "started"}
@ -26,9 +27,10 @@ async def start() -> Responses.StartResponse:
async def status() -> Responses.StatusResponse:
"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
if Controllers.process.is_crashed():
if process_status == ProcessStatus.CRASHED:
return {"status": "crashed"}
# Maintainance
if Controllers.maintainance.is_set():
@ -36,18 +38,18 @@ async def status() -> Responses.StatusResponse:
# Offline
return {"status": "offline"}
status = Controllers.server.status()
server_status = Controllers.server.status()
# Starting
if not status["online"]:
if not server_status["online"]:
return {"status": "starting"}
# Online
return {
"status": "online",
"motd": status.get("motd", ""),
"icon": status.get("icon", None),
"players": status.get(
"motd": server_status.get("motd", ""),
"icon": server_status.get("icon", None),
"players": server_status.get(
"players",
{
"online": 0,

View file

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