diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d62af21 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +server/ +fabric/ +.vscode/ +mcgrizz.json +players.cache.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf0d16c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM denoland/deno:debian + +WORKDIR /mcgrizz +# USER deno + +COPY . . + +# RUN deno task build +RUN deno cache main.ts +# Install OpenJDK-17 +RUN apt-get update && \ + apt-get install -y openjdk-17-jre ca-certificates-java && \ + apt-get clean && \ + update-ca-certificates -f +# Setup JAVA_HOME -- useful for docker commandline +ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk-amd64/ +RUN export JAVA_HOME + +EXPOSE 8000 +EXPOSE 25565-25575 + +CMD [ "run", "-A", "main.ts" ] \ No newline at end of file diff --git a/deno.json b/deno.json index 73ad739..57057c1 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,8 @@ "start": "tailwind -i ./static/styles/main.css -o ./static/styles/tailwind.css --minify --watch & deno run -A --watch=static/,routes/ dev.ts", "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts", - "update": "deno run -A -r https://fresh.deno.dev/update ." + "update": "deno run -A -r https://fresh.deno.dev/update .", + "clean": "rm -rf ./server && rm ./players.cache.json && rm ./mcgrizz.json" }, "lint": { "rules": { diff --git a/islands/statusManager.tsx b/islands/statusManager.tsx index 96d97a5..2462f1e 100644 --- a/islands/statusManager.tsx +++ b/islands/statusManager.tsx @@ -18,30 +18,43 @@ export function StatusManager( props.onAction && props.onAction(body); }; - const [status, setStatus] = useState(''); + const [status, setStatus] = useState(""); - const getStatus = async () => { - const res = await fetch('/api/manage'); - const body = await res.text(); + const getStatus = () => { + globalThis.statusSource = globalThis.statusSource || new EventSource('/api/manage'); - setStatus(body); - } + globalThis.statusSource.addEventListener('status', (e) => { + setStatus(e.data); + }) + }; useEffect(() => { if (IS_BROWSER) getStatus(); - },[]) + }, []); return (
{!!status && Server is {status}}
- - -
diff --git a/islands/terminal.tsx b/islands/terminal.tsx index 46483e9..a3109ac 100644 --- a/islands/terminal.tsx +++ b/islands/terminal.tsx @@ -4,7 +4,7 @@ import { JSX } from "preact/jsx-runtime"; import { Sockpuppet } from "puppet/client"; export function Terminal(props: { channelId: string }) { - const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com")); + const puppet = useRef(); const [lines, setLines] = useState([]); const divRef = useRef(null); const storeKey = "commandHistory"; @@ -18,15 +18,21 @@ export function Terminal(props: { channelId: string }) { const changeHistoryIndex = (by: number) => setHistoryIndex((i) => i + by); useEffect(() => { - if (!IS_BROWSER) return; - puppet.current.joinChannel(props.channelId, (line) => { - setLines((l) => [...l, line]); + if (!IS_BROWSER || puppet.current) return; + puppet.current = new Sockpuppet("ws://sockpuppet.cyborggrizzly.com", () => { + puppet.current?.joinChannel(props.channelId, (line) => { + if (line === "clear") { + setLines([]); + } else { + setLines((l) => [...l, line]); + } + }); + setTimeout(() => { + const channel = puppet.current?.getChannel(props.channelId); + // console.log(channel) + channel?.send("log"); + }, 200); }); - setTimeout(() => { - const channel = puppet.current.getChannel(props.channelId) - // console.log(channel) - channel?.send("log"); - }, 200); document.addEventListener("keyup", (e) => { switch (e.key) { @@ -50,12 +56,12 @@ export function Terminal(props: { channelId: string }) { const sendCommand = (e: Event) => { e.preventDefault(); - puppet.current.getChannel(props.channelId)?.send( - historyIndex === commandHistory.length - ? command - : commandHistory[historyIndex], + puppet.current?.getChannel(props.channelId)?.send( + commandHistory.at(historyIndex) || command, ); - setCommandHistory((c) => [...c, command]); + setCommandHistory(( + c, + ) => [...c, commandHistory.at(historyIndex) || command]); setHistoryIndex(commandHistory.length + 1); setCommand(""); }; diff --git a/routes/api/manage.ts b/routes/api/manage.ts index f7503a0..dd9f204 100644 --- a/routes/api/manage.ts +++ b/routes/api/manage.ts @@ -25,7 +25,26 @@ export const handler: Handlers = { return new Response("action done"); } }, - GET() { - return new Response(SERVER_STATE.status); + GET(_req, _ctx) { + let listener: (e: CustomEvent) => void; + const body = new ReadableStream({ + async start(controller){ + const event = `event: status\ndata: ${SERVER_STATE.status}\n\n` + controller.enqueue(event); + listener = (e) => { + const event = `event: status\ndata: ${e.detail}\n\n` + controller.enqueue(event); + }; + globalThis.addEventListener('serverstatuschange' as any, listener) + }, + cancel() { + globalThis.removeEventListener('serverstatuschange' as any, listener); + } + }) + + return new Response(body.pipeThrough(new TextEncoderStream()), { headers: { + "Content-Type": "text/event-stream", + "cache-control": "no-cache" + }}); } }; diff --git a/state/serverState.ts b/state/serverState.ts index 1c655bc..07096d3 100644 --- a/state/serverState.ts +++ b/state/serverState.ts @@ -4,7 +4,7 @@ import { Loader } from "../types/mcgrizzconf.ts"; import { getConfFile, updateConfFile } from "../util/confFile.ts"; import { IS_BROWSER } from "$fresh/runtime.ts"; -type MCServerEvent = 'message'; +type MCServerEvent = "message"; type MCServerEventCallback = (msg: string) => void; class ServerState { @@ -19,21 +19,21 @@ class ServerState { private _eulaAccepted = false; private sockpuppet!: Sockpuppet; - private _channelId = "blanaba"; + private _channelId; public get channelId() { return this._channelId; } public get eulaAccepted() { return this._eulaAccepted; } - private stdin!: WritableStreamDefaultWriter; + private stdin!: WritableStreamDefaultWriter; - private _serverType: Loader = 'unset'; + private _serverType: Loader = "unset"; public get serverType(): Loader { return this._serverType; } public set serverType(loader: Loader) { - updateConfFile({loader}); + updateConfFile({ loader }); this._serverType = loader; } @@ -42,26 +42,35 @@ class ServerState { return this._serverVersion; } public set serverVersion(version: string) { - updateConfFile({version}); + updateConfFile({ version }); this._serverVersion = version; } constructor() { + this._channelId = crypto.randomUUID(); + const conf = getConfFile(); this._serverType = conf.loader; this._serverVersion = conf.version; - // if (this.serverType !== 'unset') this._eulaAccepted = checkEULA(); + this._eulaAccepted = checkEULA(); this.sockpuppet = new Sockpuppet( "ws://sockpuppet.cyborggrizzly.com", () => { + this.sockpuppet.createChannel(this._channelId); this.sockpuppet.joinChannel(this.channelId, async (msg) => { - if (msg === 'log' && !IS_BROWSER) { - const log = await Deno.readTextFile('./server/logs/latest.log'); - this.channel?.send(log); - } else - this.sendStdIn(msg); + if (msg === "log" && !IS_BROWSER) { + try { + const log = await Deno.readTextFile("./server/logs/latest.log"); + this.channel?.send(log); + } catch { + 1; + } + } else { + console.log(msg) + this.sendStdIn(msg); + } }); }, ); @@ -79,7 +88,6 @@ class ServerState { await this.stdin.write(msg); } - // "instance" should be moved to a private member once multi-instance support is implemented public startMCServer(instance = "server") { this.command = new Deno.Command("java", { @@ -94,14 +102,23 @@ class ServerState { stdout: "piped", }); + + this.channel?.send('clear'); + this.process = this.command.spawn(); - - const {readable, writable} = new TransformStream(); + + const { readable, writable } = new TransformStream(); readable.pipeTo(this.process.stdin); this.stdin = writable.getWriter(); - + this._status = "running"; + const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status}); + + globalThis.dispatchEvent(statusEvent); this.startStream(); + // this.process.status.then(() => { + // this._status = "stopped"; + // }); } private async startStream() { @@ -120,25 +137,54 @@ class ServerState { } if (done) break; } + + // await this.process.stdout.pipeThrough(new TextDecoderStream()).pipeTo( + // new WritableStream({ + // write: (chunk) => { + // this.channel?.send(chunk); + // const stdoutMsg = new CustomEvent("stdoutmsg", { + // detail: chunk, + // }); + // globalThis.dispatchEvent(stdoutMsg); + // }, + // }), + // ); } public gracefullyStopMCServer() { this._status = "stopped"; this.sendStdIn("stop"); + const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status}); + + globalThis.dispatchEvent(statusEvent); + } public forceStopMCServer() { this._status = "stopped"; this.process.kill(); + const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status}); + + globalThis.dispatchEvent(statusEvent); + } public async restartMCServer() { if (this.status === "running") { await this.sendStdIn("stop"); + const statusEvent = new CustomEvent('serverstatuschange', {detail: 'restarting'}); + + globalThis.dispatchEvent(statusEvent); + // while (true) { await this.process.status; // } } + + const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status}); + + globalThis.dispatchEvent(statusEvent); + this.startMCServer(); } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f26cf54 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["./types.d.ts"] +} \ No newline at end of file diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..ccb59ec --- /dev/null +++ b/types.d.ts @@ -0,0 +1,6 @@ +// deno-lint-ignore-file no-var +export {} + +declare global { + var statusSource: EventSource; +} diff --git a/util/EULA.ts b/util/EULA.ts index 589565b..a97a2ab 100644 --- a/util/EULA.ts +++ b/util/EULA.ts @@ -1,8 +1,14 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; const eulaRegex = /(eula=false)/; -export const checkEULA = (instance = "server") => - !IS_BROWSER && !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); +export const checkEULA = (instance = "server") => { + try { + return !IS_BROWSER && + !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); + } catch { + return false; + } +}; export const acceptEULA = (instance = "server") => { const eula = Deno.readTextFileSync(`./${instance}/eula.txt`);