minecraftd/controllers.py
2025-12-17 17:51:22 +01:00

202 lines
6.4 KiB
Python

import shlex
from collections import deque
from pathlib import Path
from subprocess import Popen
from threading import Lock
from time import sleep
from typing import Literal
from mcrcon import MCRcon
from mcstatus import JavaServer
from classes import ProcessStatus, ServerStatus
from config import Config
from logs import logger
class ProcessController:
def __init__(self):
self.start_command: list[str] = shlex.split(
Path(Config.START_COMMAND_FILE).read_text()
)
self.cwd = Config.SERVER_PATH
self.process: Popen | None = None
self.last_status: Literal[ProcessStatus.STOPPED, ProcessStatus.CRASHED] = (
ProcessStatus.STOPPED
)
self._lock = Lock()
def start(self) -> None:
"Start the process."
logger.debug("ProcessController.start()")
with self._lock:
if self.status() == ProcessStatus.RUNNING:
if self.process:
logger.debug(
"ProcessController.start() - Process was already running with PID: %s",
self.process.pid,
)
return
self.process = Popen(
self.start_command,
stdout=None,
stderr=None,
stdin=None,
start_new_session=True,
cwd=self.cwd,
)
logger.info(
"ProcessController.start() - Started process with PID: %s",
self.process.pid,
)
def status(self) -> ProcessStatus:
"Check the process' status."
logger.debug("ProcessController.status()")
if not self.process:
logger.debug("ProcessController.status() => %s", self.last_status)
return self.last_status
match self.process.poll():
case None:
logger.debug("ProcessController.status() => ProcessStatus.RUNNING")
return ProcessStatus.RUNNING
case 0:
logger.debug("ProcessController.status() => ProcessStatus.STOPPED")
self.last_status = ProcessStatus.STOPPED
return ProcessStatus.STOPPED
case _:
logger.debug("ProcessController.status() => ProcessStatus.CRASHED")
self.last_status = ProcessStatus.CRASHED
return ProcessStatus.CRASHED
def kill(self) -> None:
"Kill the process."
logger.debug("ProcessController.kill()")
with self._lock:
if self.process:
pid = self.process.pid
self.process.kill()
code = self.process.wait()
self.process = None
logger.info(
"ProcessController.kill() - Process with PID %s killed with return code: %s",
pid,
code,
)
self.last_status = ProcessStatus.STOPPED
class ServerController:
def __init__(self):
self.server: JavaServer = JavaServer(
Config.SERVER_HOST,
Config.SERVER_PORT,
)
self.rcon = MCRcon(
Config.SERVER_HOST,
Config.SERVER_RCON_PASSWORD,
Config.SERVER_RCON_PORT,
)
def status(self) -> ServerStatus:
logger.debug("ServerController.status()")
try:
status = self.server.status()
except Exception:
logger.debug("ServerController.status() - Server is offline")
return {"online": False}
players = []
if status.players.sample:
for player in status.players.sample:
players.append(player.name)
logger.debug("ServerController.status() - Server is online")
return {
"online": True,
"icon": status.icon,
"motd": status.motd.to_html(),
"players": {
"online": status.players.online,
"max": status.players.max,
"list": players,
},
}
def command(self, command: str) -> str:
logger.debug('ServerController.command(command="%s")', command)
try:
self.rcon.connect()
output = self.rcon.command(command)
self.rcon.disconnect()
logger.info('ServerController.command(command="%s") => %s', command, output)
return output
except Exception:
logger.exception(
'ServerController.command(command="%s") - Command execution failed',
command,
)
return ""
class MaintainanceController:
def __init__(self):
self.mnt_file = Path(Config.MAINTAINANCE_FILE)
def set(self, reason: str):
logger.debug('MaintainanceController.set(reason="%s")', reason)
self.mnt_file.write_text(reason)
def is_set(self) -> bool:
logger.debug("MaintainanceController.is_set() => %s", self.mnt_file.is_file())
return self.mnt_file.is_file()
def get(self) -> str:
if self.is_set():
logger.debug(
"MaintainanceController.get() => %s", self.mnt_file.read_text()
)
return self.mnt_file.read_text()
logger.debug("MaintainanceController.get() => ")
return ""
def unset(self):
logger.debug("MaintainanceController.unset()")
if self.is_set():
self.mnt_file.unlink()
logger.debug("MaintainanceController.unset() - Maintainance file was unset")
class LogsController:
def __init__(self):
self.log_file = Path(Config.LOG_FILE)
def stream(self):
logger.debug("LogsController.stream()")
i = 0
with self.log_file.open() as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
logger.debug("LogsController.stream() - Yielding line %s", i)
i += 1
yield line
else:
sleep(0.1)
def tail(self, back: int = 10):
logger.debug("LogsController.tail(back=%s)", back)
with self.log_file.open() as f:
for line in deque(f, maxlen=back):
yield line
class Controllers:
process = ProcessController()
server = ServerController()
maintainance = MaintainanceController()
logs = LogsController()