Genesis commit
This commit is contained in:
commit
0736d1fe08
4 changed files with 181 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
__pycache__/
|
||||
venv/
|
||||
.env
|
||||
program.py
|
||||
test.py
|
||||
71
handler.py
Normal file
71
handler.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from typing import AsyncIterator, List
|
||||
from asyncio.subprocess import PIPE
|
||||
import asyncio
|
||||
|
||||
|
||||
class ServerHandler:
|
||||
def __init__(self, argv: List[str]):
|
||||
self.argv = argv
|
||||
self._proc: asyncio.subprocess.Process | None = None
|
||||
self._stdout_queue: asyncio.Queue[str | None] = asyncio.Queue()
|
||||
self._reader_task: asyncio.Task | None = None
|
||||
self._stderr_task: asyncio.Task | None = None
|
||||
self._watcher_task: asyncio.Task | None = None
|
||||
self._stop_event = asyncio.Event()
|
||||
|
||||
async def start(self) -> None:
|
||||
if self._proc and self._proc.returncode is None:
|
||||
return
|
||||
|
||||
self._stop_event = asyncio.Event()
|
||||
self._stdout_queue = asyncio.Queue()
|
||||
|
||||
self._proc = await asyncio.create_subprocess_exec(
|
||||
*self.argv,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
self._reader_task = loop.create_task(
|
||||
self._read_stream(self._proc.stdout))
|
||||
self._stderr_task = loop.create_task(
|
||||
self._read_stream(self._proc.stderr))
|
||||
self._watcher_task = loop.create_task(self._watch_process())
|
||||
|
||||
async def _read_stream(self, stream: asyncio.StreamReader | None) -> None:
|
||||
if stream is None:
|
||||
return
|
||||
try:
|
||||
while not stream.at_eof():
|
||||
line_bytes = await stream.readline()
|
||||
if not line_bytes:
|
||||
break
|
||||
|
||||
try:
|
||||
line = line_bytes.decode(errors="replace").rstrip("\n")
|
||||
except Exception:
|
||||
line = line_bytes.decode("utf-8", "replace").rstrip("\n")
|
||||
|
||||
print(line)
|
||||
await self._stdout_queue.put(line)
|
||||
except asyncio.CancelledError:
|
||||
...
|
||||
finally:
|
||||
await self._stdout_queue.put(None)
|
||||
|
||||
async def _watch_process(self) -> None:
|
||||
if self._proc is not None:
|
||||
await self._proc.wait()
|
||||
self._stop_event.set()
|
||||
for t in (self._reader_task, self._stderr_task):
|
||||
if t and not t.done():
|
||||
t.cancel()
|
||||
await self._stdout_queue.put(None)
|
||||
|
||||
async def reader(self) -> AsyncIterator[str]:
|
||||
while True:
|
||||
item = await self._stdout_queue.get()
|
||||
if item is None:
|
||||
break
|
||||
yield item
|
||||
38
lib.py
Normal file
38
lib.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from typing import AsyncIterator
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
|
||||
|
||||
class CommandModel(BaseModel):
|
||||
password: str
|
||||
cmd: str
|
||||
|
||||
|
||||
class LogsModel(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
def sse_event(data: str, event: str | None = None, id: str | None = None) -> bytes:
|
||||
parts = []
|
||||
if id is not None:
|
||||
parts.append(f"id: {id}")
|
||||
if event is not None:
|
||||
parts.append(f"event: {event}")
|
||||
for line in data.rstrip("\n").split("\n"):
|
||||
parts.append(f"data: {line}")
|
||||
parts.append("")
|
||||
return ("\n".join(parts) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
async def sse_from_iterator(iterator: AsyncIterator[str], keep_alive: float = 15):
|
||||
ait = iterator.__aiter__()
|
||||
while True:
|
||||
try:
|
||||
item = await asyncio.wait_for(ait.__anext__(), timeout=keep_alive)
|
||||
except asyncio.TimeoutError:
|
||||
yield b": keep-alive\n\n"
|
||||
continue
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
yield sse_event(item)
|
||||
yield sse_event("stream closed", event="close")
|
||||
67
main.py
Normal file
67
main.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from fastapi.responses import StreamingResponse
|
||||
from fastapi import HTTPException
|
||||
from mcstatus import JavaServer
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI
|
||||
from mcrcon import MCRcon
|
||||
from os import environ
|
||||
|
||||
from lib import CommandModel, LogsModel, sse_from_iterator
|
||||
from handler import ServerHandler
|
||||
|
||||
load_dotenv()
|
||||
MCSMGR_PASSWORD = environ.get("MCSMGR_PASSWORD", "SuperSecretPassword")
|
||||
MINECRAFT_SERVER_COMMAND = environ.get(
|
||||
"MINECRAFT_SERVER_COMMAND", "java -jar fabric.jar nogui")
|
||||
MINECRAFT_SERVER_ADDRESS = environ.get("MINECRAFT_SERVER_ADDRESS", "localhost")
|
||||
MINECRAFT_SERVER_RCON_PASSWORD = environ.get(
|
||||
"MINECRAFT_SERVER_RCON_PASSWORD", "SuperSecretPassword")
|
||||
|
||||
app = FastAPI()
|
||||
handler = ServerHandler(MINECRAFT_SERVER_COMMAND.split())
|
||||
server = JavaServer(MINECRAFT_SERVER_ADDRESS)
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
async def status():
|
||||
try:
|
||||
info = server.status()
|
||||
except:
|
||||
return {"online": False}
|
||||
|
||||
return {
|
||||
"online": True,
|
||||
"latency": info.latency,
|
||||
"motd": info.motd,
|
||||
"players": {
|
||||
"list": info.players.sample,
|
||||
"online": info.players.online,
|
||||
"max": info.players.max,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/command")
|
||||
async def command(data: CommandModel):
|
||||
if data.password != MCSMGR_PASSWORD:
|
||||
raise HTTPException(403, "Invalid password")
|
||||
|
||||
try:
|
||||
with MCRcon(MINECRAFT_SERVER_ADDRESS, MINECRAFT_SERVER_RCON_PASSWORD) as mcr:
|
||||
return mcr.command(data.cmd)
|
||||
except:
|
||||
raise HTTPException(500, "Unable to reach Server")
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
async def start():
|
||||
await handler.start()
|
||||
|
||||
|
||||
@app.get("/logs")
|
||||
async def logs(data: LogsModel):
|
||||
if data.password != MCSMGR_PASSWORD:
|
||||
raise HTTPException(401, "Invalid password")
|
||||
|
||||
generator = sse_from_iterator(handler.reader())
|
||||
return StreamingResponse(generator, media_type="text/event-stream")
|
||||
Loading…
Add table
Add a link
Reference in a new issue