basic docker config, fixes several critical faults, allows status manager to get status via sse

This commit is contained in:
Emmaline Autumn 2023-10-07 12:19:43 -06:00
parent 2487529aaf
commit 6944cbb9f7
10 changed files with 172 additions and 45 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
server/
fabric/
.vscode/
mcgrizz.json
players.cache.json

22
Dockerfile Normal file
View File

@ -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" ]

View File

@ -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", "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", "build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts", "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": { "lint": {
"rules": { "rules": {

View File

@ -18,30 +18,43 @@ export function StatusManager(
props.onAction && props.onAction(body); props.onAction && props.onAction(body);
}; };
const [status, setStatus] = useState(''); const [status, setStatus] = useState("");
const getStatus = async () => { const getStatus = () => {
const res = await fetch('/api/manage'); globalThis.statusSource = globalThis.statusSource || new EventSource('/api/manage');
const body = await res.text();
setStatus(body); globalThis.statusSource.addEventListener('status', (e) => {
} setStatus(e.data);
})
};
useEffect(() => { useEffect(() => {
if (IS_BROWSER) getStatus(); if (IS_BROWSER) getStatus();
},[]) }, []);
return ( return (
<div {...props}> <div {...props}>
{!!status && <small>Server is {status}</small>} {!!status && <small>Server is {status}</small>}
<div class="flex gap-4"> <div class="flex gap-4">
<Button color="wasabi" disabled={!status || status === 'running'} onClick={() => sendCommand(ManageAction.start)}> <Button
color="wasabi"
disabled={!status || status === "running"}
onClick={() => sendCommand(ManageAction.start)}
>
Start Start
</Button> </Button>
<Button color="fire" disabled={!status || status === 'stopped'} onClick={() => sendCommand(ManageAction.stop)}> <Button
color="fire"
disabled={!status || status === "stopped"}
onClick={() => sendCommand(ManageAction.stop)}
>
Stop Stop
</Button> </Button>
<Button color="sky" disabled={!status} onClick={() => sendCommand(ManageAction.restart)}> <Button
color="sky"
disabled={!status}
onClick={() => sendCommand(ManageAction.restart)}
>
Restart Restart
</Button> </Button>
</div> </div>

View File

@ -4,7 +4,7 @@ import { JSX } from "preact/jsx-runtime";
import { Sockpuppet } from "puppet/client"; import { Sockpuppet } from "puppet/client";
export function Terminal(props: { channelId: string }) { export function Terminal(props: { channelId: string }) {
const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com")); const puppet = useRef<Sockpuppet>();
const [lines, setLines] = useState<string[]>([]); const [lines, setLines] = useState<string[]>([]);
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
const storeKey = "commandHistory"; const storeKey = "commandHistory";
@ -18,15 +18,21 @@ export function Terminal(props: { channelId: string }) {
const changeHistoryIndex = (by: number) => setHistoryIndex((i) => i + by); const changeHistoryIndex = (by: number) => setHistoryIndex((i) => i + by);
useEffect(() => { useEffect(() => {
if (!IS_BROWSER) return; if (!IS_BROWSER || puppet.current) return;
puppet.current.joinChannel(props.channelId, (line) => { puppet.current = new Sockpuppet("ws://sockpuppet.cyborggrizzly.com", () => {
puppet.current?.joinChannel(props.channelId, (line) => {
if (line === "clear") {
setLines([]);
} else {
setLines((l) => [...l, line]); setLines((l) => [...l, line]);
}
}); });
setTimeout(() => { setTimeout(() => {
const channel = puppet.current.getChannel(props.channelId) const channel = puppet.current?.getChannel(props.channelId);
// console.log(channel) // console.log(channel)
channel?.send("log"); channel?.send("log");
}, 200); }, 200);
});
document.addEventListener("keyup", (e) => { document.addEventListener("keyup", (e) => {
switch (e.key) { switch (e.key) {
@ -50,12 +56,12 @@ export function Terminal(props: { channelId: string }) {
const sendCommand = (e: Event) => { const sendCommand = (e: Event) => {
e.preventDefault(); e.preventDefault();
puppet.current.getChannel(props.channelId)?.send( puppet.current?.getChannel(props.channelId)?.send(
historyIndex === commandHistory.length commandHistory.at(historyIndex) || command,
? command
: commandHistory[historyIndex],
); );
setCommandHistory((c) => [...c, command]); setCommandHistory((
c,
) => [...c, commandHistory.at(historyIndex) || command]);
setHistoryIndex(commandHistory.length + 1); setHistoryIndex(commandHistory.length + 1);
setCommand(""); setCommand("");
}; };

View File

@ -25,7 +25,26 @@ export const handler: Handlers = {
return new Response("action done"); return new Response("action done");
} }
}, },
GET() { GET(_req, _ctx) {
return new Response(SERVER_STATE.status); 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"
}});
} }
}; };

View File

@ -4,7 +4,7 @@ import { Loader } from "../types/mcgrizzconf.ts";
import { getConfFile, updateConfFile } from "../util/confFile.ts"; import { getConfFile, updateConfFile } from "../util/confFile.ts";
import { IS_BROWSER } from "$fresh/runtime.ts"; import { IS_BROWSER } from "$fresh/runtime.ts";
type MCServerEvent = 'message'; type MCServerEvent = "message";
type MCServerEventCallback = (msg: string) => void; type MCServerEventCallback = (msg: string) => void;
class ServerState { class ServerState {
@ -19,7 +19,7 @@ class ServerState {
private _eulaAccepted = false; private _eulaAccepted = false;
private sockpuppet!: Sockpuppet; private sockpuppet!: Sockpuppet;
private _channelId = "blanaba"; private _channelId;
public get channelId() { public get channelId() {
return this._channelId; return this._channelId;
} }
@ -28,12 +28,12 @@ class ServerState {
} }
private stdin!: WritableStreamDefaultWriter; private stdin!: WritableStreamDefaultWriter;
private _serverType: Loader = 'unset'; private _serverType: Loader = "unset";
public get serverType(): Loader { public get serverType(): Loader {
return this._serverType; return this._serverType;
} }
public set serverType(loader: Loader) { public set serverType(loader: Loader) {
updateConfFile({loader}); updateConfFile({ loader });
this._serverType = loader; this._serverType = loader;
} }
@ -42,26 +42,35 @@ class ServerState {
return this._serverVersion; return this._serverVersion;
} }
public set serverVersion(version: string) { public set serverVersion(version: string) {
updateConfFile({version}); updateConfFile({ version });
this._serverVersion = version; this._serverVersion = version;
} }
constructor() { constructor() {
this._channelId = crypto.randomUUID();
const conf = getConfFile(); const conf = getConfFile();
this._serverType = conf.loader; this._serverType = conf.loader;
this._serverVersion = conf.version; this._serverVersion = conf.version;
// if (this.serverType !== 'unset') this._eulaAccepted = checkEULA(); this._eulaAccepted = checkEULA();
this.sockpuppet = new Sockpuppet( this.sockpuppet = new Sockpuppet(
"ws://sockpuppet.cyborggrizzly.com", "ws://sockpuppet.cyborggrizzly.com",
() => { () => {
this.sockpuppet.createChannel(this._channelId);
this.sockpuppet.joinChannel(this.channelId, async (msg) => { this.sockpuppet.joinChannel(this.channelId, async (msg) => {
if (msg === 'log' && !IS_BROWSER) { if (msg === "log" && !IS_BROWSER) {
const log = await Deno.readTextFile('./server/logs/latest.log'); try {
const log = await Deno.readTextFile("./server/logs/latest.log");
this.channel?.send(log); this.channel?.send(log);
} else } catch {
1;
}
} else {
console.log(msg)
this.sendStdIn(msg); this.sendStdIn(msg);
}
}); });
}, },
); );
@ -79,7 +88,6 @@ class ServerState {
await this.stdin.write(msg); await this.stdin.write(msg);
} }
// "instance" should be moved to a private member once multi-instance support is implemented // "instance" should be moved to a private member once multi-instance support is implemented
public startMCServer(instance = "server") { public startMCServer(instance = "server") {
this.command = new Deno.Command("java", { this.command = new Deno.Command("java", {
@ -94,14 +102,23 @@ class ServerState {
stdout: "piped", stdout: "piped",
}); });
this.channel?.send('clear');
this.process = this.command.spawn(); this.process = this.command.spawn();
const {readable, writable} = new TransformStream(); const { readable, writable } = new TransformStream();
readable.pipeTo(this.process.stdin); readable.pipeTo(this.process.stdin);
this.stdin = writable.getWriter(); this.stdin = writable.getWriter();
this._status = "running"; this._status = "running";
const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status});
globalThis.dispatchEvent(statusEvent);
this.startStream(); this.startStream();
// this.process.status.then(() => {
// this._status = "stopped";
// });
} }
private async startStream() { private async startStream() {
@ -120,25 +137,54 @@ class ServerState {
} }
if (done) break; 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() { public gracefullyStopMCServer() {
this._status = "stopped"; this._status = "stopped";
this.sendStdIn("stop"); this.sendStdIn("stop");
const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status});
globalThis.dispatchEvent(statusEvent);
} }
public forceStopMCServer() { public forceStopMCServer() {
this._status = "stopped"; this._status = "stopped";
this.process.kill(); this.process.kill();
const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status});
globalThis.dispatchEvent(statusEvent);
} }
public async restartMCServer() { public async restartMCServer() {
if (this.status === "running") { if (this.status === "running") {
await this.sendStdIn("stop"); await this.sendStdIn("stop");
const statusEvent = new CustomEvent('serverstatuschange', {detail: 'restarting'});
globalThis.dispatchEvent(statusEvent);
// while (true) { // while (true) {
await this.process.status; await this.process.status;
// } // }
} }
const statusEvent = new CustomEvent('serverstatuschange', {detail: this._status});
globalThis.dispatchEvent(statusEvent);
this.startMCServer(); this.startMCServer();
} }

3
tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"include": ["./types.d.ts"]
}

6
types.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
// deno-lint-ignore-file no-var
export {}
declare global {
var statusSource: EventSource;
}

View File

@ -1,8 +1,14 @@
import { IS_BROWSER } from "$fresh/runtime.ts"; import { IS_BROWSER } from "$fresh/runtime.ts";
const eulaRegex = /(eula=false)/; const eulaRegex = /(eula=false)/;
export const checkEULA = (instance = "server") => export const checkEULA = (instance = "server") => {
!IS_BROWSER && !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); try {
return !IS_BROWSER &&
!eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`));
} catch {
return false;
}
};
export const acceptEULA = (instance = "server") => { export const acceptEULA = (instance = "server") => {
const eula = Deno.readTextFileSync(`./${instance}/eula.txt`); const eula = Deno.readTextFileSync(`./${instance}/eula.txt`);