Compare commits
No commits in common. "a008bc27fab257cfe6eba7cda494838feed0c0d3" and "eb92d2d36f21650a5ff24b96628a28611c5d5709" have entirely different histories.
a008bc27fa
...
eb92d2d36f
11 changed files with 53 additions and 53 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
__pycache__/
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
18
main.py
|
|
@ -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,
|
||||||
|
|
|
||||||
8
util.py
8
util.py
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue