V6 initial #507

Merged
Sirherobrine23 merged 6 commits from V6_big_changes into main 2023-02-05 02:50:34 +00:00
3 changed files with 315 additions and 20 deletions
Showing only changes of commit 627cb14051 - Show all commits

View File

@@ -0,0 +1,9 @@
import * as Bedrock from "./platform/Bedrock.js";
export default {
Bedrock: Bedrock.default,
};
export {
Bedrock,
}

View File

@@ -1,13 +1,205 @@
import { serverManeger, pathOptions } from "../serverManeger.js";
import { createServerManeger, platformPathID, pathOptions, serverConfig } from "../serverManeger.js";
import { promises as fs, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import coreUtils, { childPromisses } from "@sirherobrine23/coreutils";
import path from "node:path";
import tar from "tar";
import AdmZip from "adm-zip";
import { promisify } from "node:util";
export default async function Bedrock(options?: pathOptions & {variant?: string}) {
options = {...options};
export type bedrockRootOption = pathOptions & {
variant?: "oficial"|"Pocketmine-PMMP"|"Powernukkit"|"Cloudbust"
};
return serverManeger({
const hostArchEmulate = [
"qemu-x86_64-static",
"qemu-x86_64",
"box64"
];
type bedrockVersionJSON = {
version: string,
date: Date,
release?: "stable"|"preview",
url: {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: string
}
}
};
async function getPHPBin(options?: bedrockRootOption) {
options = {variant: "oficial", ...options};
const serverPath = await platformPathID("bedrock", options);
const binFolder = path.join(serverPath.serverPath, "bin");
const files = await coreUtils.extendFs.readdir({folderPath: binFolder});
const file = files.filter((v) => v.endsWith("php.exe")||v.endsWith("php")).at(0);
if (!file) throw new Error("PHP Bin not found");
return file;
}
const oracleBucket = await coreUtils.oracleBucket("sa-saopaulo-1", "bdsFiles", "grwodtg32n4d", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");
export async function installServer(version: string, options?: bedrockRootOption) {
options = {variant: "oficial", ...options};
const serverPath = await platformPathID("bedrock", options);
if (options?.variant === "Pocketmine-PMMP") {
const phpBin = (await oracleBucket.fileList()).filter(({name}) => name.startsWith("/php_bin/")).filter(({name}) => name.includes(process.platform) && name.includes(process.arch)).at(0);
if (!phpBin) throw new Error("PHP Bin not found");
const binFolder = path.join(serverPath.serverPath, "bin");
if (await coreUtils.extendFs.exists(binFolder)) await fs.rm(binFolder, {recursive: true});
await fs.mkdir(binFolder);
await pipeline(await oracleBucket.getFileStream(phpBin.name), createWriteStream(path.join(binFolder, "phpTmp")));
if (phpBin.name.endsWith(".tar.gz")) {
await tar.extract({
file: path.join(binFolder, "phpTmp"),
cwd: binFolder
});
} else if (phpBin.name.endsWith(".zip")) {
await promisify((new AdmZip(path.join(binFolder, "phpTmp"))).extractAllToAsync)(binFolder, true, true);
}
await fs.rm(path.join(binFolder, "phpTmp"));
const phpBinPath = await getPHPBin(options);
await coreUtils.childPromisses.execFile(phpBinPath, ["--version"]);
const rel = await coreUtils.httpRequestGithub.getRelease({owner: "pmmp", repository: "PocketMine-MP", all: true});
const relData = version.trim().toLowerCase() === "latest" ? rel.at(0) : rel.find((v) => v.tag_name === version.trim());
if (!relData) throw new Error("Version not found");
const phpAsset = relData.assets.find((a) => a.name.endsWith(".phar"))?.browser_download_url;
if (!phpAsset) throw new Error("PHP asset not found");
await coreUtils.httpRequestLarge.saveFile({url: phpAsset, filePath: path.join(serverPath.serverPath, "PocketMine-MP.phar")});
return {
version: relData.tag_name,
releaseDate: new Date(relData.published_at),
release: (relData.prerelease ? "preview" : "stable") as "preview"|"stable",
url: phpAsset,
phpBin: phpBin.name,
};
} else if (options?.variant === "Powernukkit") {
const versions = await coreUtils.httpRequest.getJSON<{version: string, mcpeVersion: string, date: string, url: string, variantType: "snapshot"|"stable"}[]>("https://mcpeversion-static.sirherobrine23.org/powernukkit/all.json");
const versionData = version.trim().toLowerCase() === "latest" ? versions.at(-1) : versions.find((v) => v.version === version.trim() || v.mcpeVersion === version.trim());
if (!versionData) throw new Error("Version not found");
const url = versionData.url;
if (!url) throw new Error("Platform not supported");
await coreUtils.httpRequestLarge.saveFile({url, filePath: path.join(serverPath.serverPath, "server.jar")});
return {
version: versionData.version,
mcpeVersion: versionData.mcpeVersion,
variantType: versionData.variantType,
releaseDate: new Date(versionData.date),
url,
};
} else if (options?.variant === "Cloudbust") {
throw new Error("Not implemented");
} else {
const versions = await coreUtils.httpRequest.getJSON<bedrockVersionJSON[]>("https://the-bds-maneger.github.io/BedrockFetch/all.json");
const versionData = version.trim().toLowerCase() === "latest" ? versions.at(-1) : versions.find((v) => v.version === version.trim());
if (!versionData) throw new Error("Version not found");
let currentPlatform = process.platform;
if (currentPlatform === "android") currentPlatform = "linux";
const url = versionData.url[currentPlatform]?.[process.arch];
if (!url) throw new Error("Platform not supported");
await coreUtils.httpRequestLarge.extractZip({url, folderTarget: serverPath.serverPath});
return {
version: versionData.version,
releaseDate: new Date(versionData.date),
release: versionData.release ?? "stable",
url: url
}
}
}
export default startServer;
export async function startServer(options?: bedrockRootOption) {
// Bad fix options
options = {variant: "oficial", ...options};
const serverPath = await platformPathID("bedrock", options);
// Server Object
const serverExec: serverConfig = {
exec: {
exec: "java",
args: []
cwd: serverPath.serverPath,
},
actions: {}
})
};
if (options?.variant === "Pocketmine-PMMP") {
serverExec.exec.exec = await getPHPBin();
serverExec.exec.args = ["PocketMine-MP.phar", "--no-wizard"];
serverExec.actions = {
stopServer(child_process) {
child_process.stdin.write("stop\n");
},
onStart(lineData, fnRegister) {
if (!(lineData.includes("INFO") && lineData.includes("Done") && lineData.includes("help"))) return;
const doneStart = new Date();
fnRegister({
serverAvaible: doneStart,
bootUp: runStart.getTime() - doneStart.getTime()
});
},
};
} else if (options?.variant === "Powernukkit") {
} else if (options?.variant === "Cloudbust") {
} else {
if (process.platform === "win32") serverExec.exec.exec = "bedrock_server.exe";
else if (process.platform === "darwin") throw new Error("MacOS is not supported, run in Docker or Virtual Machine");
else {
serverExec.exec.exec = path.join(serverPath.serverPath, "bedrock_server");
serverExec.exec.env = {
LD_LIBRARY_PATH: serverPath.serverPath
};
}
if ((["android", "linux"]).includes(process.platform) && process.arch !== "x64") {
const exec = serverExec.exec.exec;
serverExec.exec.exec = undefined;
for (const command of hostArchEmulate) {
if (await childPromisses.commandExists(command, true)) {
serverExec.exec.args = [exec];
serverExec.exec.exec = command;
break;
}
if (!serverExec.exec.exec) throw new Error("No emulator found for this platform");
}
}
const startTest = /\[.*\]\s+Server\s+started\./;
// Server actions
serverExec.actions = {
stopServer(child_process) {
child_process.stdin.write("stop\n");
},
onStart(lineData, fnRegister) {if (startTest.test(lineData)) fnRegister({serverAvaible: new Date()});},
playerActions(lineData, fnRegister) {
const playerActionsV1 = /\[.*\]\s+Player\s+((dis|)connected):\s+(.*),\s+xuid:\s+([0-9]+)/;
const newPlayerActions = /\[.*INFO\]\s+Player\s+(Spawned|connected|disconnected):\s+([\s\S\w]+)\s+(xuid:\s+([0-9]+))?/;
const connectTime = new Date();
if (!(newPlayerActions.test(lineData)||playerActionsV1.test(lineData))) return;
let playerName: string, action: string, xuid: string;
if (newPlayerActions.test(lineData)) {
const [, actionV2,, playerNameV2,, xuidV2] = lineData.match(newPlayerActions);
playerName = playerNameV2;
action = actionV2;
xuid = xuidV2;
} else {
const [, actionV1,, playerNameV1, xuidV1] = lineData.match(newPlayerActions);
playerName = playerNameV1;
action = actionV1;
xuid = xuidV1;
}
fnRegister({
player: playerName,
action: action === "Spawned" ? "spawned" : action === "connected" ? "join" : "leave",
actionDate: connectTime,
sessionID: serverPath.id,
more: {
xuid
}
});
},
};
}
const runStart = new Date();
return createServerManeger(serverExec);
}

View File

@@ -5,6 +5,7 @@ import { extendFs } from "@sirherobrine23/coreutils";
import crypto from "node:crypto";
import path from "node:path";
import os from "node:os";
import EventEmitter from "node:events";
export type pathOptions = {
id?: "default"|string,
@@ -63,36 +64,129 @@ export async function platformPathID(platform: "bedrock"|"java", options?: pathO
};
}
export type playerAction = ({action: "join"|"spawned"|"leave"}|{
action: "kick"|"ban",
reason?: string,
by?: string
}) & {
player: string,
actionDate: Date,
sessionID: string
more?: any,
latestAction?: playerAction
}
export type serverConfig = {
exec: {
exec: string,
exec?: string,
args?: string[],
cwd?: string,
env?: NodeJS.ProcessEnv & {[key: string]: string},
},
actions: {
actions?: {
stopServer?: (child_process: ChildProcess) => void,
onStart?: (lineData: string, fnRegister: (data: {serverAvaible: Date, bootUp?: number}) => void) => void,
playerActions?: (lineData: string, fnRegister: (data: {player: string, action: "join" | "leave"}) => void) => void,
onStart?: (lineData: string, fnRegister: (data?: {serverAvaible?: Date, bootUp?: number}) => void) => void,
playerActions?: (lineData: string, fnRegister: (data: playerAction) => void) => void,
}
};
export async function serverManeger(serverOptions: serverConfig) {
declare class serverManeger extends EventEmitter {
on(event: "error", fn: (lineLog: any) => void): this;
once(event: "error", fn: (lineLog: any) => void): this;
emit(event: "error", data: any): boolean;
on(event: "log", fn: (lineLog: string) => void): this;
once(event: "log", fn: (lineLog: string) => void): this;
emit(event: "log", data: string): boolean;
on(event: "rawLog", fn: (raw: any) => void): this;
once(event: "rawLog", fn: (raw: any) => void): this;
emit(event: "rawLog", data: any): boolean;
// Player actions
on(event: "playerAction", fn: (playerAction: playerAction) => void): this;
once(event: "playerAction", fn: (playerAction: playerAction) => void): this;
emit(event: "playerAction", data: playerAction): boolean;
// Server started
on(event: "serverStarted", fn: (data: {serverAvaible: Date, bootUp: number}) => void): this;
once(event: "serverStarted", fn: (data: {serverAvaible: Date, bootUp: number}) => void): this;
emit(event: "serverStarted", data: {serverAvaible: Date, bootUp: number}): boolean;
}
export async function createServerManeger(serverOptions: serverConfig): Promise<serverManeger> {
const serverExec = child_process.execFile(serverOptions.exec.exec, serverOptions.exec.args ?? [], {
cwd: serverOptions.exec.cwd,
windowsHide: true,
maxBuffer: Infinity,
env: {
...process.env,
...serverOptions.exec.env
},
windowsHide: true,
maxBuffer: Infinity
});
const stdoutReadline = readline({input: serverExec.stdout});
const stderrReadline = readline({input: serverExec.stderr});
const playerActions: playerAction[] = [];
const internalEvent = new class serverManeger extends EventEmitter {
stopServer() {
const stopServer = serverOptions.actions?.stopServer ?? ((child_process) => child_process.kill("SIGKILL"));
stopServer(serverExec);
}
return {
serverExec,
stdoutReadline,
stderrReadline
getPlayers() {
return playerActions ?? [];
}
};
serverExec.on("error", internalEvent.emit.bind(internalEvent, "error"));
const stdoutReadline = readline({input: serverExec.stdout});
stdoutReadline.on("line", (line) => internalEvent.emit("log", line));
stdoutReadline.on("error", internalEvent.emit.bind(internalEvent, "error"));
serverExec.stdout.on("data", (data) => internalEvent.emit("rawLog", data));
const stderrReadline = readline({input: serverExec.stderr});
stderrReadline.on("line", (line) => internalEvent.emit("log", line));
stderrReadline.on("error", internalEvent.emit.bind(internalEvent, "error"));
serverExec.stderr.on("data", (data) => internalEvent.emit("rawLog", data));
// Server start
if (serverOptions.actions?.onStart) {
const serverStartFN = serverOptions.actions.onStart;
let lock = false;
const started = new Date();
function register(data?: {serverAvaible?: Date, bootUp?: number}) {
if (lock) return;
const eventData = {
serverAvaible: data?.serverAvaible ?? new Date(),
bootUp: data?.bootUp ?? new Date().getTime() - started.getTime()
};
internalEvent.emit("serverStarted", eventData);
lock = true;
stderrReadline.removeListener("line", register);
stdoutReadline.removeListener("line", register);
// emit and remove new listener for serverStarted
internalEvent.removeAllListeners("serverStarted");
internalEvent.prependListener("serverStarted", () => {
internalEvent.emit("serverStarted", eventData);
internalEvent.removeAllListeners("serverStarted");
});
}
stdoutReadline.on("line", (line) => serverStartFN(line, register));
stderrReadline.on("line", (line) => serverStartFN(line, register));
}
// Player actions
if (serverOptions.actions?.playerActions) {
const playerFn = serverOptions.actions.playerActions;
const registerData = (data: playerAction) => {
const player = playerActions.find((player) => player.player === data.player);
if (!player) playerActions.push(data);
else {
data.latestAction = player;
playerActions[playerActions.indexOf(player)] = data;
}
internalEvent.emit("playerAction", data);
}
stdoutReadline.on("line", (line) => playerFn(line, registerData));
stderrReadline.on("line", (line) => playerFn(line, registerData));
}
return internalEvent;
}