From eb92d2d36f21650a5ff24b96628a28611c5d5709 Mon Sep 17 00:00:00 2001 From: Malasaur Date: Sun, 30 Nov 2025 15:47:45 +0100 Subject: [PATCH] Genesis commit --- __init__.py | 0 __pycache__/__init__.cpython-313.pyc | Bin 0 -> 147 bytes __pycache__/classes.cpython-313.pyc | Bin 0 -> 906 bytes __pycache__/config.cpython-313.pyc | Bin 0 -> 1509 bytes __pycache__/controllers.cpython-313.pyc | Bin 0 -> 8753 bytes __pycache__/libminecraftd.cpython-313.pyc | Bin 0 -> 9885 bytes __pycache__/main.cpython-313.pyc | Bin 0 -> 5673 bytes __pycache__/models.cpython-313.pyc | Bin 0 -> 1753 bytes __pycache__/responses.cpython-313.pyc | Bin 0 -> 1506 bytes __pycache__/util.cpython-313.pyc | Bin 0 -> 2810 bytes classes.py | 14 ++ config.py | 14 ++ controllers.py | 155 ++++++++++++++++++++++ main.py | 120 +++++++++++++++++ models.py | 35 +++++ responses.py | 25 ++++ util.py | 45 +++++++ 17 files changed, 408 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/classes.cpython-313.pyc create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/controllers.cpython-313.pyc create mode 100644 __pycache__/libminecraftd.cpython-313.pyc create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 __pycache__/models.cpython-313.pyc create mode 100644 __pycache__/responses.cpython-313.pyc create mode 100644 __pycache__/util.cpython-313.pyc create mode 100644 classes.py create mode 100644 config.py create mode 100644 controllers.py create mode 100644 main.py create mode 100644 models.py create mode 100644 responses.py create mode 100644 util.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..542b8931be1e9e658ddab45c04deb3e4f19c1925 GIT binary patch literal 147 zcmey&%ge<81eqQ>nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABz4P3KO;XkRX;Z| zC$TuOv`F72wK%&Zzd%38DZfNNH#09axhOHMBt<_yJ~J<~BtBlRpz;=nO>TZlX-=wL W5i8ILkQK!s#z$sGM#ds$APWG@Y$4SE literal 0 HcmV?d00001 diff --git a/__pycache__/classes.cpython-313.pyc b/__pycache__/classes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51dccecc3539ebbd1845708a416bac1aabf624b7 GIT binary patch literal 906 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 N30&iE2mp4P{{w|>Qw0D3 literal 0 HcmV?d00001 diff --git a/__pycache__/libminecraftd.cpython-313.pyc b/__pycache__/libminecraftd.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfe03c146c989a1094d37bc4dd9c6c7864f9afb0 GIT binary patch literal 9885 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 P+dZLSgC|0Wk^27wb#PXj literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..437c4ed48df6bc2cd37d9d8487efe44c28d132c4 GIT binary patch literal 5673 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 Se78z*@u!vLOFtNblKTs`bwx@5 literal 0 HcmV?d00001 diff --git a/__pycache__/responses.cpython-313.pyc b/__pycache__/responses.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39cde344c514cb066771dc58f35b58692d375deb GIT binary patch literal 1506 zcmZvc-HY2s5WrW`>ccwuE=|tuB+{ghr^_|lXi

%sgC7VGJ1Kur$c_xwx$30({ zHjJ_?%?_k3Xd553P$@>@PuB5m=|oW$50WU7ZWImj-l$Ky7e&uTalhVKi=zD$MA1*P zB+H`+C_ahSILmUpE5-NlP%Db)=Cm8dMFHufQIVh-JanGokI6gj$N3;>58{3-;t{lW z67jUihwX>o=S6#fmv3%zrIlv{MH++ncgg#y zC{(%~zX44Oi^7F95*NtbNV5Vypyj4oqB~rqB{8ZJStsqoxu8cx3kBu8c67gNct<~$ z?&?eN%73v}ognAssrPZ`{nQt`=iRi=txW|8u>{sltU?XPuB}eJb`-1zG!T|6R~KPT z2!L_xBYo8|y3{V<&op7>VF7&bo@xzE^r5ofu

x^%eZtJ)^ zb~n%XrczIKUOyST+h=^cY;K%hzBO*P-|}`<6uNPtC>k1QOs}9=EK0}$yAaIK0xQt* zZ^Q7pY|Q&jOjPXDAsydYnlKip&QN8uEKQ$WNYG`1JYKMJef#+@<7fK6rT0k-t})9(kkFoFiwhMH;K~*NPMcilqm))0dWaPyrz<#BRN`hDXw_Ta?Al9;ifB;}9cgFY zn|br*d*95w*{x8>kD#19(y9FDLFi|y)Jw1?Y@Gw)8WNGnOri;fFqq1&N!J8RSW9Ln z-4h<-0og5jCcP6J;VjuZ$xrx*52M{kd`aYI{i1I+Ao`&VKpUK8MXrlOXun?!MbH`d z8HNPK@T`BoOKhMz3FWwW;{ysK&X^uOFUv&`_+crZmrmzpkV8kNr>92GrRAcbXzByT zHuVl`YF5d?cmO1WX!*QMbiwpWX=-Nr(ppJ1GTNeQdWkIQ(8U=_LDou!={DwNH4R)i z1rVZ-;M@9`MW_v7;9`b})rO_V2%U0FQ!Ynzgw%FlwO)~#L1Ex;?B<$G!XRFQiF9jHb5x8uC%t&ck#r2$2t z?o0I5agHO=KM$OqWv;P;CkW94ZeLe3LMr1#F#5lU03tpLQOvM4UbBBeRlh0q5TQ*q zY*T7tomYpK;eX5vTjPaw5bXU=5YV_N3I}ZJMPu3hTrZ8zB92gJ1a{u-u)+h+?pE|< zEa%Vpd32Z&cJN48>0`4@JRp4oSLTF7R2{>G#l^g|1XsKh>xNdeyF{6sl}RU-)C^uw z@_8LAhK`FwOUpXQSxYsf1x3waL(_0xQ*#Nwe@Y@!K{miy(vP9oDFerJgT#|~&`Ka+ zj~9tz$XHRW2-xnzS<*+U- z;U`=zyYaYymjlo>d`uV-qtWPch?=BE2gPGT61SU7JE3XEejBCTv{s>VCSL3kc^UUvN_oxkZY&|J4n;7=yNbi3^`-E=?RH@2+eqeK8b|p zD?%>=2|=aPBu@DF=mPp}C*m9L@=X;!QsyHSzO~G^e(~Pg*joSf{WpBq&)@D? zZ##a6Kk>B5yKB8`vFp+GwnKOLw>QJhtIh+q6>e{t+gst<%Urv~MoXD*sqnbW<2!uoFI?a!KKO~Z$|64aYcmS9JoF=9 z^B;N-u>Ijs>@dUJe4QCP8l zCJh_q6gNop5N(Y>1-}8?6KCyFbZkLLF2WNGXXupk3m`J0tNuL7Ob*3aLHQI`SZBDC$`$uVmsZp|7*qbaY%89Zn67O^?h?4T|E4W2q5BUB(&G0*y`~VzP_chST$M zdSR}PorE+x?i@REHQB)4wV{M$lMG{|Rc None: + "Starts the process." + if self.is_started(): + return + + 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 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 + + +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: + try: + status = self.server.status() + except Exception: + return {"online": False} + + players = [] + if status.players.sample: + for player in status.players.sample: + players.append(player.name) + + 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: + self.rcon.connect() + output = self.rcon.command(command) + self.rcon.disconnect() + return output + + +class MaintainanceController: + def __init__(self): + self.mnt_file = Path(Config.MAINTAINANCE_FILE) + + def set(self, reason: str): + self.mnt_file.write_text(reason) + + def is_set(self) -> bool: + return self.mnt_file.is_file() + + def get(self) -> str: + if self.is_set(): + return self.mnt_file.read_text() + return "" + + def unset(self): + if self.is_set(): + self.mnt_file.unlink() + + +class LogsController: + def __init__(self): + self.log_file = Path(Config.LOG_FILE) + + def stream(self) -> Generator[str]: + with self.log_file.open() as f: + f.seek(0, 2) + while True: + line = f.readline() + if line: + yield line + else: + sleep(0.1) + + def tail(self, back: int = 10) -> Generator[str]: + 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() diff --git a/main.py b/main.py new file mode 100644 index 0000000..5f74ce4 --- /dev/null +++ b/main.py @@ -0,0 +1,120 @@ +from asyncio import create_task +from typing import Annotated + +from fastapi import FastAPI, Header +from fastapi.responses import StreamingResponse + +from .controllers import Controllers +from .models import Models +from .responses import Responses +from .util import check_password, stop_server + +app = FastAPI() + + +@app.get("/start") +async def start() -> Responses.StartResponse: + "Starts the Server process." + + if Controllers.process.is_started(): + return {"status": "running"} + Controllers.process.start() + return {"status": "started"} + + +@app.get("/status") +async def status() -> Responses.StatusResponse: + "Checks whether the Server is running and returns its information." + + if not Controllers.process.is_started(): + # Crashed + if Controllers.process.is_crashed(): + return {"status": "crashed"} + # Maintainance + if Controllers.maintainance.is_set(): + return {"status": "maintainance", "reason": Controllers.maintainance.get()} + # Offline + return {"status": "offline"} + + status = Controllers.server.status() + + # Starting + if not status["online"]: + return {"status": "starting"} + + # Online + return { + "status": "online", + "motd": status.get("motd", ""), + "icon": status.get("icon", None), + "players": status.get( + "players", + { + "online": 0, + "max": 20, + "list": [], + }, + ), + } + + +@app.get("/stop") +async def stop(data: Models.StopModel, authorization: Annotated[str, Header()]): + "Stops the Server." + check_password(authorization) + create_task(stop_server("STOPPING", data.countdown, data.reason, data.timeout)) + + +@app.get("/restart") +async def restart(data: Models.RestartModel, authorization: Annotated[str, Header()]): + "Restarts the Server." + check_password(authorization) + create_task( + stop_server( + "RESTARTING", + data.countdown, + data.reason, + data.timeout, + Controllers.process.start, + ) + ) + + +@app.get("/maintainance") +async def maintainance( + data: Models.MaintainanceModel, authorization: Annotated[str, Header()] +): + "Stops the Server and sets it to maintainance status." + check_password(authorization) + create_task( + stop_server( + "STOPPING FOR MAINTAINANCE", + data.countdown, + data.reason, + data.timeout, + Controllers.maintainance.set(data.reason), + ) + ) + + +@app.get("/command") +async def command( + data: Models.CommandModel, authorization: Annotated[str, Header()] +) -> str: + "Runs a command on the Server and returns its output." + check_password(authorization) + return Controllers.server.command(data.command) + + +@app.get("/logs/stream") +async def logs_stream(authorization: Annotated[str, Header()]) -> StreamingResponse: + check_password(authorization) + return StreamingResponse(Controllers.logs.stream(), media_type="text/event-stream") + + +@app.get("/logs/tail") +async def logs_tail( + data: Models.LogsTailModel, authorization: Annotated[str, Header()] +) -> StreamingResponse: + check_password(authorization) + return StreamingResponse(Controllers.logs.tail(data.back)) diff --git a/models.py b/models.py new file mode 100644 index 0000000..3370743 --- /dev/null +++ b/models.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel + + +class StopModel(BaseModel): + countdown: int = 60 + reason: str = "" + timeout: int = 10 + + +class RestartModel(BaseModel): + countdown: int = 60 + reason: str = "" + timeout: int = 10 + + +class MaintainanceModel(BaseModel): + countdown: int = 60 + reason: str = "" + timeout: int = 10 + + +class CommandModel(BaseModel): + command: str + + +class LogsTailModel(BaseModel): + back: int = 10 + + +class Models: + StopModel = StopModel + RestartModel = RestartModel + MaintainanceModel = MaintainanceModel + CommandModel = CommandModel + LogsTailModel = LogsTailModel diff --git a/responses.py b/responses.py new file mode 100644 index 0000000..b9383f3 --- /dev/null +++ b/responses.py @@ -0,0 +1,25 @@ +from typing import Literal, NotRequired, TypedDict + + +class StartResponse(TypedDict): + status: Literal["running", "started"] + + +class StatusResponsePlayers(TypedDict): + online: int + max: int + list: list[str] + + +class StatusResponse(TypedDict): + status: Literal["online", "offline", "crashed", "maintainance", "starting"] + reason: NotRequired[str] + motd: NotRequired[str] + icon: NotRequired[str | None] + players: NotRequired[StatusResponsePlayers] + + +class Responses: + StartResponse = StartResponse + StatusResponsePlayers = StatusResponsePlayers + StatusResponse = StatusResponse diff --git a/util.py b/util.py new file mode 100644 index 0000000..5c7d38a --- /dev/null +++ b/util.py @@ -0,0 +1,45 @@ +from asyncio import sleep +from typing import Callable + +from fastapi import HTTPException + +from .config import Config +from .controllers import Controllers + + +async def stop_server( + action: str, countdown: int, reason: str, timeout: int, then: Callable | None = None +): + """Warns the players, stops the Server, and kills its process if its taking too long. + + Parameters: + action (str): Action to write in the warning ("SERVER IS {action}..."). + countdown (int): Seconds to wait after the warning. + reason: (str): The reason for the Server shutdown. + timeout (int): Seconds to wait before killing the process. + then (Callable | None) (default: None): Function to be called after the Server is stopped. + """ + if countdown: + Controllers.server.command(f"say SERVER IS {action} IN {countdown} SECONDS!!!") + Controllers.server.command(f"say REASON: '{reason}'") + while countdown > 0 and Controllers.server.status().get("players", {}).get( + "online", 0 + ): + await sleep(1) + countdown -= 1 + + # Controllers.server.command("stop") + while timeout > 0 and Controllers.process.is_started(): + await sleep(1) + timeout -= 1 + + if Controllers.process.is_started(): + Controllers.process.kill() + + if then: + then() + + +def check_password(password: str): + if password != Config.MINECRAFTD_PASSWORD: + raise HTTPException(401, "Password is invalid")