Compare commits

..

No commits in common. "a008bc27fab257cfe6eba7cda494838feed0c0d3" and "eb92d2d36f21650a5ff24b96628a28611c5d5709" have entirely different histories.

11 changed files with 53 additions and 53 deletions

1
.gitignore vendored
View file

@ -1 +0,0 @@
__pycache__/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,13 +1,6 @@
from enum import Enum
from typing import NotRequired, TypedDict from typing import NotRequired, TypedDict
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,6 +4,7 @@ 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,56 +1,70 @@
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, Literal from typing import Generator
from mcrcon import MCRcon from mcrcon import MCRcon
from mcstatus import JavaServer from mcstatus import JavaServer
from psutil import NoSuchProcess, Process
from .classes import ProcessStatus, ServerStatus from .classes import 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: Popen | None = None self.process: Process | None = None
self.last_status: Literal[ProcessStatus.STOPPED, ProcessStatus.CRASHED] = (
ProcessStatus.STOPPED 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: def start(self) -> None:
"Start the process." "Starts the process."
if self.status() == ProcessStatus.RUNNING: if self.is_started():
return return
self.process = Popen( 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 status(self) -> ProcessStatus: def is_started(self) -> bool:
"Check the process' status." "Check if the process is running."
if not self.process: if self.process:
return self.last_status return self.process.is_running()
match self.process.poll(): return False
case None:
return ProcessStatus.RUNNING def is_crashed(self) -> bool:
case 0: "Check if the process has crashed."
self.last_status = ProcessStatus.STOPPED return False # TODO
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.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: class ServerController:
@ -88,13 +102,10 @@ class ServerController:
} }
def command(self, command: str) -> str: def command(self, command: str) -> str:
try:
self.rcon.connect() self.rcon.connect()
output = self.rcon.command(command) output = self.rcon.command(command)
self.rcon.disconnect() self.rcon.disconnect()
return output return output
except Exception:
return ""
class MaintainanceController: class MaintainanceController:

18
main.py
View file

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

View file

@ -3,8 +3,6 @@ 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
@ -30,12 +28,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.status() == ProcessStatus.RUNNING: while timeout > 0 and Controllers.process.is_started():
await sleep(1) await sleep(1)
timeout -= 1 timeout -= 1
if Controllers.process.status() == ProcessStatus.RUNNING: if Controllers.process.is_started():
Controllers.process.kill() Controllers.process.kill()
if then: if then: