diff options
author | Linnnus <[email protected]> | 2025-03-13 12:31:24 +0100 |
---|---|---|
committer | Linnnus <[email protected]> | 2025-03-13 12:31:24 +0100 |
commit | 01948f7e7ce9a57317f69440c9bfe2ac765b5d1e (patch) | |
tree | dc07be2e986acf3a44373a720ea3d5471c694224 | |
parent | 442dddd11e1affa443a9362a4795b9b9958bc0a8 (diff) |
ahmed: Remove minecraft-log-server
8 files changed, 0 insertions, 328 deletions
diff --git a/hosts/ahmed/configuration.nix b/hosts/ahmed/configuration.nix index aab26c3..25eb336 100644 --- a/hosts/ahmed/configuration.nix +++ b/hosts/ahmed/configuration.nix @@ -22,7 +22,6 @@ ./remote-builder ./dyndns ./minecraft - ./minecraft-log-server ./nginx ]; diff --git a/hosts/ahmed/minecraft-log-server/.htpasswd b/hosts/ahmed/minecraft-log-server/.htpasswd deleted file mode 100644 index 5cb2c4c..0000000 --- a/hosts/ahmed/minecraft-log-server/.htpasswd +++ /dev/null @@ -1 +0,0 @@ -Gæst:$2y$05$R1XvCulPi3xjTB31b1MuEugyX.Sk0Bkh1m95.wseA1ydJ3Rq9V.j2 diff --git a/hosts/ahmed/minecraft-log-server/default.nix b/hosts/ahmed/minecraft-log-server/default.nix deleted file mode 100644 index 9ed5e90..0000000 --- a/hosts/ahmed/minecraft-log-server/default.nix +++ /dev/null @@ -1,113 +0,0 @@ -# This module implements a really simple and shitty WSGI app which streams -# journald logs to the client. I don't expect it to hang around for long, so -# it's just quickly hacked together. -# -# FIXME: There's still the issue with broken connections. Perhaps heartbeat would fix. -{ - pkgs, - config, - ... -}: let - # Enable HTTPS stuff. - useACME = true; - - socket-path = "/run/minecraft-log-server.sock"; - - python = pkgs.python3.withPackages (ps: - with ps; [ - gevent - gunicorn - ]); -in { - users.users.minecraft-log-server = { - description = "Runs minecraft-log-server"; - group = "minecraft-log-server"; - isSystemUser = true; - }; - users.groups.minecraft-log-server = {}; - - systemd.sockets.minecraft-log-server = { - description = "Socket where the service of the same name answers HTTP requests."; - - socketConfig = { - ListenStream = socket-path; - - # TODO: wtf apple maps - SocketUser = "nginx"; - SocketGroup = "nginx"; - SocketMode = "600"; - }; - - wantedBy = ["sockets.target"]; - }; - - # See: https://docs.gunicorn.org/en/23.0.0/deploy.html - systemd.services.minecraft-log-server = { - description = "Minecraft log server"; - - serviceConfig = { - # Using a non-sync worker class is super important because we have such long-running connections. - ExecStart = "${python}/bin/gunicorn --worker-class=gevent --chdir ${./.} minecraft_log_server:app"; - - ExecReload = "kill -s HUP $MAINPID"; - KillMode = "mixed"; - - User = config.users.users.minecraft-log-server.uid; - Group = config.users.users.minecraft-log-server.group; - - # gunicorn can let systemd know when it is ready - Type = "notify"; - NotifyAccess = "main"; - - # Harden - ProtectSystem = "strict"; - PrivateTmp = true; - }; - - requires = ["minecraft-log-server.socket"]; # Refuse to start without. - after = ["network.target"]; - }; - - services.nginx = { - virtualHosts."minecraft.linus.onl" = { - enableACME = useACME; - forceSSL = useACME; - - # Let's be safe and pass-word protect it just in case the logs contain some sensitive data. - basicAuthFile = ./.htpasswd; - - # First try resolving files statically, before falling back to the CGI server. - locations."/" = { - alias = "${./public}/"; - index = "index.html"; - tryFiles = "$uri $uri/ @minecraft_log_server"; - }; - - locations."@minecraft_log_server" = { - recommendedProxySettings = true; - - # In addition to the important stuff set indirectly via `recommendedProxySettings` - # (especially `proxy_http_version`), we need these options for SSE. - extraConfig = '' - # Disable buffering. This is crucial for SSE to ensure that - # messages are sent immediately without waiting for a buffer to - # fill. - proxy_buffering off; - - # Disable caching to ensure that all messages are sent and received - # in real-time without being cached by the proxy. - proxy_cache off; - - # Set a long timeout for reading from the proxy to prevent the - # connection from timing out. You may need to adjust this value - # based on your specific requirements. - proxy_read_timeout 86400; - ''; - - proxyPass = "http://unix:${socket-path}:$request_uri"; - }; - }; - }; - - services.cloudflare-dyndns.domains = ["minecraft.linus.onl"]; -} diff --git a/hosts/ahmed/minecraft-log-server/minecraft_log_server.py b/hosts/ahmed/minecraft-log-server/minecraft_log_server.py deleted file mode 100644 index a676626..0000000 --- a/hosts/ahmed/minecraft-log-server/minecraft_log_server.py +++ /dev/null @@ -1,125 +0,0 @@ -import subprocess -import json -import dataclasses -import typing as t -import urllib.parse - [email protected](kw_only=True, slots=True) -class Event: - id: t.Optional[str | bytes] = None - event: t.Optional[str | bytes] = None - data: t.Optional[bytes | bytes] = None - retry: t.Optional[int] = None - - def __post_init__(self): - if (self.id is None and - self.event is None and - self.data is None and - self.retry is None): - raise ValueError("At least one property of event must be non-None: id, event, data, retry") - - def encode(self) -> bytes: - """Returns the on-line representation of this event.""" - - def to_bytes(s: str | bytes | int) -> bytes: - if isinstance(s, str): - return s.encode() - elif isinstance(s, int): - return str(s).encode() - else: - return s - - # We know the result won't be empty because of the invariant that at least one field is non-None. - result = b"" - - if self.id: result += b"id: " + to_bytes(self.id) + b"\n" - if self.event: result += b"event: " + to_bytes(self.event) + b"\n" - if self.data: result += b"data: " + to_bytes(self.data) + b"\n" - if self.retry: result += b"retry: " + to_bytes(self.retry) + b"\n" - - # With this final newline, the encoding will end with two newlines, signifying end of event. - result += b"\n" - - return result - -def app(environ, start_response): - print(f"{environ=} {start_response=}") # NOCOMMIT - path = environ["PATH_INFO"].lstrip("/") - method = environ["REQUEST_METHOD"] - - if method == "GET" and path == "stream": - return send_stream(environ, start_response) - else: - return send_404(environ, start_response) - -def send_stream(environ, start_response): - status = "200 OK" - headers = [("Content-Type", "text/event-stream"), - ("Cache-Control", "no-cache"), - ("X-Accel-Buffering", "no")] - start_response(status, headers) - - # Set the retry rate for when the client looses connection. - retry_event = Event(retry=2_000) - yield retry_event.encode() - - # Figure out if the client is reconnecting. - last_event_id = None - if "HTTP_LAST_EVENT_ID" in environ: - last_event_id = environ["HTTP_LAST_EVENT_ID"] - else: - query = urllib.parse.parse_qs(environ["QUERY_STRING"]) - if "lastEventId" in query: - last_event_id = query["lastEventId"][0] - - # FIXME: We should also send heartbeat events to avoid NGINX killing our connection. - UNITS = [ "minecraft-listen.socket", "minecraft-listen.service", "minecraft-server.socket", - "minecraft-server.service", "minecraft-hook.service", "minecraft-stop.timer", - "minecraft-stop.service" ] - for event in get_log_entries(UNITS, last_event_id): - yield event.encode() - -def get_log_entries(units, last_event_id = None) -> t.Generator[Event, None, None]: - # TODO: We could save some work by only selecting the fields we're interested in with `--fields`. - args = [ - "/run/current-system/sw/bin/journalctl", - # We want a stream - "--follow", - # A JSON line for each entry - "--output=json", - # Use UTC timestamps to avoid tricky timezone issues on the client - "--utc", - # Log entries from any of the units (logical OR) - *(f"--unit={u}" for u in units) - ] - - # Since we use the cursor as the SSE event ID, the client will send the - # last cursor when retrying connections. - if last_event_id: - # If this is such a connection, we can avoid including duplicate entries by - # starting just after the given cursor. - args.append("--after-cursor=" + last_event_id) - else: - # Otherwise this the user has just opened the page and we should give - # them a bit of context for the next lines that appear - args.append("--lines=200") - - try: - process = subprocess.Popen(args, stdout=subprocess.PIPE) - assert process.stdout is not None - for raw_line in process.stdout: - assert raw_line[-2:] == b"}\n", "Raw line ends in single newline" - parsed = json.loads(raw_line) - event = Event(id=parsed["__CURSOR"], - event="entry", - data=raw_line.rstrip(b"\n")) - yield event - except Exception as e: - print("Reading (mega sus) journalctl failed", e) - raise e - -def send_404(environ, start_response): - status = "404 Not Found" - headers = [("Content-type", "text/plain")] - start_response(status, headers) - return [b"The requested resource was not found."] diff --git a/hosts/ahmed/minecraft-log-server/public/index.html b/hosts/ahmed/minecraft-log-server/public/index.html deleted file mode 100644 index 37fdaac..0000000 --- a/hosts/ahmed/minecraft-log-server/public/index.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <script type="module" src="./scripts/plain.js"></script> - <link rel="stylesheet" href="./styles/plain.css"> - </head> - <body> - <p>System events will appear below.</p> - <pre><output id="target"></output></pre> - </body> -</html> diff --git a/hosts/ahmed/minecraft-log-server/public/scripts/plain.js b/hosts/ahmed/minecraft-log-server/public/scripts/plain.js deleted file mode 100644 index c2da55e..0000000 --- a/hosts/ahmed/minecraft-log-server/public/scripts/plain.js +++ /dev/null @@ -1,68 +0,0 @@ -import ReconnectingEventSource from "./reconnecting-eventsource.min.js"; - -function main() { - const sse = new ReconnectingEventSource("/stream", { - // Retry time after browser fails to reconnect (e.g. HTTP 502). - // This is pretty sus, so let's wait a little longer... - max_retry_time: 5_000, - }); - - sse.addEventListener("open", (event) => { - addSpecialMessage("info", "Connection to log server established!"); - }); - - sse.addEventListener("error", (event) => { - console.error("SSE Error: ", event); - addSpecialMessage("error", "Connection to log server lost! Retrying connection..."); - }); - - sse.addEventListener("entry", (event) => { - const line = JSON.parse(event.data); - addEntry(line); - }); -} - -function addEntry(json) { - const $container = document.createElement("span"); - $container.classList.add("regular"); - - const $time = document.createElement("time"); - const timestamp = new Date(+json["__REALTIME_TIMESTAMP"] / 1000); - $time.textContent = `[${timestamp.toISOString()}]: `; - $time.dateTime = timestamp; - $container.append($time) - - const $unit = document.createElement("span"); - $unit.textContent = json["_SYSTEMD_UNIT"]; - $container.append($unit); - $container.append(": ") - - const $message = document.createElement("span"); - $message.textContent = json["MESSAGE"]; - $container.append($message); - - $container.append("\n"); - addToOutput($container); -} - -function addSpecialMessage(klass, message) { - const $message = document.createElement("span"); - $message.classList.add(klass); - $message.textContent = message; - $message.textContent += "\n"; - addToOutput($message); -} - -function addToOutput($elem) { - // TODO: Maybe allow for a little wiggle-room? - const wasAtBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight; - - const $target = document.getElementById("target"); - $target.appendChild($elem); - - if (wasAtBottom) { - window.scrollTo(0, document.body.scrollHeight); - } -} - -main(); diff --git a/hosts/ahmed/minecraft-log-server/public/scripts/reconnecting-eventsource.min.js b/hosts/ahmed/minecraft-log-server/public/scripts/reconnecting-eventsource.min.js deleted file mode 100644 index 344e759..0000000 --- a/hosts/ahmed/minecraft-log-server/public/scripts/reconnecting-eventsource.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// https://github.com/fanout/reconnecting-eventsource -export class EventSourceNotAvailableError extends Error{constructor(){super("EventSource not available.\nConsider loading an EventSource polyfill and making it available globally as EventSource, or passing one in as eventSourceClass to the ReconnectingEventSource constructor.")}}export default class e{_configuration;CONNECTING=0;OPEN=1;CLOSED=2;static CONNECTING=0;static OPEN=1;static CLOSED=2;_eventSource;_lastEventId;_timer;_listeners;_onevent_wrapped;readyState;url;withCredentials;max_retry_time;eventSourceClass;constructor(e,t){if(this._configuration=null!=t?Object.assign({},t):void 0,this.withCredentials=!1,this._eventSource=null,this._lastEventId=null,this._timer=null,this._listeners={},this.url=e.toString(),this.readyState=this.CONNECTING,this.max_retry_time=3e3,this.eventSourceClass=globalThis.EventSource,null!=this._configuration&&(this._configuration.lastEventId&&(this._lastEventId=this._configuration.lastEventId,delete this._configuration.lastEventId),this._configuration.max_retry_time&&(this.max_retry_time=this._configuration.max_retry_time,delete this._configuration.max_retry_time),this._configuration.eventSourceClass&&(this.eventSourceClass=this._configuration.eventSourceClass,delete this._configuration.eventSourceClass)),null==this.eventSourceClass||"function"!=typeof this.eventSourceClass)throw new EventSourceNotAvailableError;this._onevent_wrapped=e=>{this._onevent(e)},this._start()}dispatchEvent(e){throw Error("Method not implemented.")}_start(){let e=this.url;for(let t of(this._lastEventId&&(-1===e.indexOf("?")?e+="?":e+="&",e+="lastEventId="+encodeURIComponent(this._lastEventId)),this._eventSource=new this.eventSourceClass(e,this._configuration),this._eventSource.onopen=e=>{this._onopen(e)},this._eventSource.onerror=e=>{this._onerror(e)},this._eventSource.onmessage=e=>{this.onmessage(e)},Object.keys(this._listeners)))this._eventSource.addEventListener(t,this._onevent_wrapped)}_onopen(e){0===this.readyState&&(this.readyState=1,this.onopen(e))}_onerror(e){if(1===this.readyState&&(this.readyState=0,this.onerror(e)),this._eventSource&&2===this._eventSource.readyState){this._eventSource.close(),this._eventSource=null;let t=Math.round(this.max_retry_time*Math.random());this._timer=setTimeout(()=>this._start(),t)}}_onevent(e){e instanceof MessageEvent&&(this._lastEventId=e.lastEventId);let t=this._listeners[e.type];if(null!=t)for(let s of[...t])s.call(this,e);"message"===e.type&&this.onmessage(e)}onopen(e){}onerror(e){}onmessage(e){}close(){this._timer&&(clearTimeout(this._timer),this._timer=null),this._eventSource&&(this._eventSource.close(),this._eventSource=null),this.readyState=2}addEventListener(e,t,s){e in this._listeners||(this._listeners[e]=[],null!=this._eventSource&&this._eventSource.addEventListener(e,this._onevent_wrapped));let n=this._listeners[e];Array.isArray(n)&&!n.includes(t)&&n.push(t)}removeEventListener(e,t,s){let n=this._listeners[e];if(null!=n){for(;;){let i=n.indexOf(t);if(-1===i)break;n.splice(i,1)}n.length<=0&&(delete this._listeners[e],null!=this._eventSource&&this._eventSource.removeEventListener(e,this._onevent_wrapped))}}}; diff --git a/hosts/ahmed/minecraft-log-server/public/styles/plain.css b/hosts/ahmed/minecraft-log-server/public/styles/plain.css deleted file mode 100644 index a40b612..0000000 --- a/hosts/ahmed/minecraft-log-server/public/styles/plain.css +++ /dev/null @@ -1,7 +0,0 @@ -/* Make special messages stand out */ -.error, .info { font-family: serif; font-style: italic; } -.error { color: red; } -.info { color: blue; } - -/* Make time lower visual priority */ -time { color: grey; } |