Core apis #528

Merged
Sirherobrine23 merged 3 commits from coreApis into main 2023-06-12 01:39:52 +00:00
6 changed files with 273 additions and 145 deletions
Showing only changes of commit a4b385d2cb - Show all commits

View File

@ -1 +1,2 @@
export * as listVersion from "./listVersion.js";
export * as listVersion from "./listVersion.js";
export * from "./main.js";

View File

@ -1,20 +1,27 @@
import { http, Github } from "@sirherobrine23/http";
import { versionsStorages } from "../../serverRun.js";
import util from "node:util";
import xml from "xml-js";
export interface mojangInfo {
releaseDate: Date;
release: "oficial" | "beta";
files: { [P in NodeJS.Platform]?: { [A in NodeJS.Architecture]?: string } }
date: Date,
release?: "oficial" | "preview",
url: {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: {
[ext in "tgz" | "zip"]?: string;
}
}
}
}
export const mojangCache = new Map<string, mojangInfo>();
export const mojangCache = new versionsStorages<mojangInfo>();
export async function listMojang() {
const versions = await http.jsonRequestBody<{ version: string, release?: "stable" | "preview", date: string, url: { [P in NodeJS.Platform]?: { [A in NodeJS.Architecture]?: string } } }[]>("https://raw.githubusercontent.com/Sirherobrine23/BedrockFetch/main/versions/all.json");
versions.forEach(rel => mojangCache.has(rel.version) ? null : mojangCache.set(rel.version, {
releaseDate: new Date(rel.release),
release: !rel.release ? "oficial" : rel.release === "preview" ? "beta" : "oficial",
files: rel.url
const versions = await http.jsonRequestBody<({version: string} & mojangInfo)[]>("https://raw.githubusercontent.com/Sirherobrine23/BedrockFetch/main/versions/all.json");
versions.filter(ver => !(mojangCache.has(ver.version))).forEach(rel => mojangCache.set(rel.version, {
date: new Date(rel.date),
release: rel.release,
url: rel.url
}));
}
@ -34,7 +41,7 @@ export interface powernukkitDownload {
url: string;
}
export const powernukkitCache = new Map<string, powernukkitDownload>();
export const powernukkitCache = new versionsStorages<powernukkitDownload>();
export async function listPowernukkitProject() {
const releases = await http.jsonRequestBody<{[k in "releases"|"snapshots"]: powerNukkitRelease[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json");
const releasesData = (Object.keys(releases) as (keyof typeof releases)[]).map(releaseType => releases[releaseType].map(data => ({...data, releaseType}))).flat(1).sort((b, a) => Math.min(1, Math.max(-1, a.releaseTime - b.releaseTime)));
@ -70,8 +77,8 @@ export interface cloudburstDownload {
url: string;
}
export const nukkitCache = new Map<string, cloudburstDownload>();
export const cloudburstCache = new Map<string, cloudburstDownload>();
export const nukkitCache = new versionsStorages<cloudburstDownload>();
export const cloudburstCache = new versionsStorages<cloudburstDownload>();
export async function listCloudburstProject() {
const Projects = [ "Nukkit", "Server" ] as const;
for (const Project of Projects) {
@ -142,7 +149,7 @@ export interface pocketmineDownload {
url: string;
}
export const pocketmineCache = new Map<string, pocketmineDownload>();
export const pocketmineCache = new versionsStorages<pocketmineDownload>();
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
export async function listPocketmineProject() {
const pocketmineReleases = (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url);

View File

@ -1,19 +1,210 @@
import { getCacheVersions } from "./listVersion.js";
import { serverManeger } from "../../serverRun.js";
import semver from "semver";
import { extendsFS } from "@sirherobrine23/extends";
import { http } from "@sirherobrine23/http";
import AdmZip from "adm-zip";
import child_process from "node:child_process";
import { createWriteStream } from "node:fs";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import readline from "node:readline";
import { finished } from "node:stream/promises";
import tar from "tar";
import { bdsFilesBucket } from "../../internalClouds.js";
import { customEvent, defineEvents } from "../../serverRun.js";
import * as bedrockVersions from "./listVersion.js";
export type platforms = "mojang"|"pocketmine"|"powernukkit"|"nukkit"|"cloudburst";
export async function installServer({platform, version}: {platform?: platforms, version?: string} = {}) {
if (!platform) platform = "mojang";
else if ((!(["mojang", "pocketmine", "powernukkit", "nukkit", "cloudburst"]).includes(platform))) throw new Error("Invalid platform");
const versions = getCacheVersions();
const getLatest = (keys: IterableIterator<string>|string[]) => Array.from(keys).sort((b, a) => semver.compare(semver.valid(semver.coerce(a)), semver.valid(semver.coerce(b)))).at(0);
if (!version) version = "latest";
if (platform === "mojang") {
if (version === "latest") version = getLatest(Object.keys(versions.mojang));
const release = versions.mojang[version];
if (!release) throw new Error("Not valid Release");
export type platforms = "mojang" | "pocketmine" | "powernukkit" | "nukkit" | "cloudburst";
export interface bedrockPorts { }
export interface playerInfo {
connected: boolean;
banned: boolean;
historic: {
action: "connected" | "spawned" | "disconnected";
actionDate: Date;
}[];
};
class playerListen extends Map<string, playerInfo> {
constructor() { super(); }
toJSON() {
return Array.from(this.keys()).reduce<{ [playerName: string]: playerInfo }>((acc, player) => {
acc[player] = this.get(player);
return acc;
}, {});
}
updateState(playerName: string, state: playerInfo["historic"][number]["action"]) {
const actionDate = new Date();
if (!(this.has(playerName))) throw new Error("Set Player");
const playerData = super.get(playerName);
if (state === "disconnected") playerData.connected = false; else playerData.connected = true;
playerData.historic.push({ action: state, actionDate });
super.set(playerName, playerData);
}
}
export async function runServer() {}
/**
* Return boolean if Class input is Bedrock class server
* @param event - Bedrock class
* @returns
*/
export function isBedrock(event: Bedrock<any>): event is Bedrock<any> {
return event instanceof Bedrock;
}
export type bedrockEvents = defineEvents<{
logLine(lineString: string): void;
portListen(info: bedrockPorts): void
}>;
export class Bedrock<P extends platforms> extends customEvent<bedrockEvents> {
readonly serverFolder: string;
readonly rootServer: string;
readonly platform: P;
constructor(rootServer: string, platform: P) {
super();
this.platform = platform;
this.rootServer = rootServer;
this.serverFolder = path.join(rootServer, "server");
Object.defineProperty(this, "rootServer", { writable: false });
Object.defineProperty(this, "serverFolder", { writable: false });
if ((!(["mojang", "pocketmine", "powernukkit", "nukkit", "cloudburst"]).includes(platform))) throw new Error("Invalid platform");
Object.defineProperty(this, "platform", { writable: false });
}
async installServer(version: string | number) {
const { platform } = this;
if (!(await extendsFS.exists(this.serverFolder))) await fs.mkdir(this.serverFolder, { recursive: true });
if (platform === "mojang") {
const release = bedrockVersions.mojangCache.get(version);
if (!release) throw new Error("Not valid Release");
const serverURL = release.url[process.platform]?.[process.arch]?.tgz;
if (!serverURL) throw new Error("Current platform not support mojang server");
let backupFiles = [{ file: "allowlist.json", data: "" }, { file: "permissions.json", data: "" }, { file: "server.properties", data: "" }];
backupFiles = await Promise.all(backupFiles.map(async file => { file.data = await fs.readFile(path.join(this.serverFolder, file.file), "utf8").catch(() => ""); return file }));
await finished((await http.streamRequest(serverURL)).pipe(tar.extract({ cwd: this.serverFolder, preserveOwner: true, keep: false })));
await Promise.all(backupFiles.filter(file => !!(file.data.trim())).map(async file => fs.writeFile(path.join(this.serverFolder, file.file), file.data)));
} else if (platform === "pocketmine") {
const release = bedrockVersions.pocketmineCache.get(version);
if (!release) throw new Error("Not valid Release");
await finished(await http.streamRequest(release.url), createWriteStream(path.join(this.serverFolder, "server.phar")));
let phpFiles = (await bdsFilesBucket.listFiles("php_bin/")).map(file => ({ name: file.name.slice(8).toLowerCase(), data: file })).filter(file => file.name.startsWith(`${process.platform}_${process.arch}`));
if (!phpFiles.length) throw new Error("Cannot get php binary to current platform");
const phpFile = phpFiles.sort((b, a) => b.data.Dates.Modified.getTime() - a.data.Dates.Modified.getTime()).at(0);
await fs.rm(path.join(this.serverFolder, "bin"), { recursive: true, force: true });
if (phpFile.name.endsWith(".tar.gz") || phpFile.name.endsWith(".tgz")) await finished((await phpFile.data.getFile()).pipe(tar.extract({ cwd: path.join(this.serverFolder, "bin") })));
else {
const tmpFile = path.join(tmpdir(), Date.now() + "_" + phpFile.name);
await finished((await phpFile.data.getFile()).pipe(createWriteStream(tmpFile)));
await new Promise<void>((done, reject) => (new AdmZip(tmpFile)).extractAllToAsync(path.join(this.serverFolder, "bin"), true, true, err => err ? reject(err) : done()));
await fs.rm(tmpFile, { force: true });
}
} else if (platform === "powernukkit") {
const release = bedrockVersions.powernukkitCache.get(version);
if (!release) throw new Error("Not valid Release");
await finished(await http.streamRequest(release.url), createWriteStream(path.join(this.serverFolder, "server.jar")));
} else if (platform === "cloudburst" || platform === "nukkit") {
const platformVersions = platform === "cloudburst" ? bedrockVersions.cloudburstCache : bedrockVersions.nukkitCache;
const release = platformVersions.get(version);
if (!release) throw new Error("Not valid Release");
await finished(await http.streamRequest(release.url), createWriteStream(path.join(this.serverFolder, "server.jar")));
}
}
ports: bedrockPorts[] = [];
readonly players = new playerListen();
serverProcess?: child_process.ChildProcess;
async runServer() {
const { platform } = this;
if (platform === "nukkit" || platform === "powernukkit" || platform === "cloudburst") {
const serverProcess = this.serverProcess = child_process.spawn("java", [
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:G1NewSizePercent=30",
"-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M",
"-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5",
"-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15",
"-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5",
"-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem",
"-XX:MaxTenuringThreshold=1",
"-Dusing.aikars.flags=https://mcflags.emc.gs",
"-Daikars.new.flags=true",
"-jar", "server.jar",
], {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
serverProcess;
} else if (platform === "pocketmine") {
const phpBin = (await extendsFS.readdirV2(this.serverFolder)).find(file => file.endsWith("php")||file.endsWith("php.exe"));
if (!phpBin) throw new Error("Fist install php binary in server folder");
this.serverProcess = child_process.spawn(phpBin, ["server.phar", "--no-wizard"], {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
} else if (platform === "mojang") {
const fileExec = path.join(this.serverFolder, (await fs.readdir(this.serverFolder)).find(file => file.startsWith("bedrock_server")));
const serverProcess = this.serverProcess = child_process.spawn(fileExec, {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"]
});
serverProcess;
}
([
readline.createInterface(this.serverProcess.stdout),
readline.createInterface(this.serverProcess.stderr)
]).map(inter => inter.on("error", err => this.emit("error", err)).on("line", data => this.emit("logLine", typeof data === "string" ? data : data[0])));
return this.serverProcess;
}
writeLn(data: string|Buffer) {
this.serverProcess.stdin.write(data);
if (typeof data === "string" && !(data.trim().endsWith("\n"))) this.serverProcess.stdin.write("\n");
return this;
}
async stopServer() {
this.writeLn("stop");
return new Promise<{code: number, signal: NodeJS.Signals}>((done, reject) => this.serverProcess.once("error", reject).once("exit", (code, signal) => done({code, signal})));
}
async setPlayerPermission(playername: string, permission: P extends "mojang" ? "operator"|"member"|"visitor" : "admin"|"user") {
if (this.platform === "mojang") {
const permissions: {permission: "operator"|"member"|"visitor", xuid: string}[] = JSON.parse(await fs.readFile(path.join(this.serverFolder, "permissions.json"), "utf8"));
permissions.push({
permission: permission as any,
xuid: playername
});
await fs.writeFile(path.join(this.serverFolder, "permissions.json"), JSON.stringify(permissions));
if (this.serverProcess) this.writeLn("permission reload");
}
}
async allowList(playername: string, options?: {xuid?: string, ignoresPlayerLimit?: boolean}) {
if (this.platform === "mojang") {
const permissions: {ignoresPlayerLimit: boolean, name: string, xuid?: string}[] = JSON.parse(await fs.readFile(path.join(this.serverFolder, "allowlist.json"), "utf8"));
await fs.writeFile(path.join(this.serverFolder, "allowlist.json"), JSON.stringify(permissions));
permissions.push({
name: playername,
ignoresPlayerLimit: options?.ignoresPlayerLimit ?? false,
xuid: options?.xuid
});
if (this.serverProcess) this.writeLn("allowlist reload");
}
}
}

View File

@ -1,6 +1,7 @@
// Bedrock platform
export * as Bedrock from "./platform/bedrock/index.js";
export * as bedrock from "./platform/bedrock/index.js";
export { isBedrock } from "./platform/bedrock/index.js";
// Java platform
export * as Java from "./platform/java/index.js";

View File

@ -1,46 +1,15 @@
import { extendsFS } from "@sirherobrine23/extends";
import child_process from "node:child_process";
import EventEmitter from "node:events";
import { createWriteStream, promises as fs } from "node:fs";
import { createInterface as readline } from "node:readline";
import path from "node:path";
import stream from "node:stream";
import tar from "tar";
export interface ManegerOptions {
rootPath: string;
runServer: {
command: string;
args?: (string | number | boolean)[];
env?: Map<string, string | number | boolean> | { [K: string]: string | number | boolean },
stdio?: child_process.StdioPipeNamed | child_process.StdioPipe[];
};
};
type EventMap = Record<string, (...args: any[]) => void>;
export type EventMap = Record<string, (...args: any[]) => void>;
type EventKey<T extends EventMap> = string & keyof T;
export type defaultEvents = {
"onServerSpawn": (child: child_process.ChildProcess) => void;
}
/**
* Extens
*/
export class serverManeger<T extends EventMap = {}> extends EventEmitter {
#io: ManegerOptions;
#serverStorage: string;
#logStorage: string;
constructor(options: ManegerOptions) {
super({ captureRejections: true });
this.#io = options;
this.#io.rootPath = path.resolve(process.cwd(), this.#io.rootPath);
this.#serverStorage = path.join(this.#io.rootPath, "storage");
this.#logStorage = path.join(this.#io.rootPath, "logs");
export type defineEvents<T extends EventMap> = T;
export class customEvent<T extends EventMap> extends EventEmitter {
constructor() {
super({captureRejections: true});
}
on<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
on<K extends EventKey<defaultEvents>>(eventName: K, fn: defaultEvents[K]): this;
on(eventName: "line", fn: (line: string) => void): this;
on(eventName: "error", fn: (err: Error) => void): this;
on(eventName: string, fn: (...args: any) => void): this {
super.on(eventName, fn);
@ -48,95 +17,42 @@ export class serverManeger<T extends EventMap = {}> extends EventEmitter {
}
once<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
once<K extends EventKey<defaultEvents>>(eventName: K, fn: defaultEvents[K]): this;
once(eventName: "line", fn: (line: string) => void): this;
once(eventName: "error", fn: (err: Error) => void): this;
once(eventName: string, fn: (...args: any) => void): this {
super.once(eventName, fn);
return this;
}
removeListener<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
removeListener(eventName: "error", fn: (err: Error) => void): this;
removeListener(eventName: string, listener: (...args: any[]) => void): this {
super.removeListener(eventName, listener);
return this;
}
off<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
off(eventName: "error", fn: (err: Error) => void): this;
off(eventName: string, listener: (...args: any[]) => void): this {
super.off(eventName, listener);
return this;
}
removeAllListeners<K extends EventKey<T>>(eventName: K): this;
removeAllListeners(event?: string): this {
super.removeAllListeners(event);
return this;
}
emit<K extends EventKey<T>>(eventName: K, ...args: Parameters<T[K]>): boolean;
emit<K extends EventKey<defaultEvents>>(eventName: K, ...args: Parameters<defaultEvents[K]>): boolean;
emit(name: "line", line: string): boolean;
emit(name: "error", err: Error): boolean;
emit(eventName: string, ...args: any): boolean {
return super.emit(eventName, args);
}
};
/** Get root from paths */
getRoot(): string { return this.#io.rootPath; };
/**
* Create tar.gz from server Storage, if server running create "snapshot" from server running state.
*
* @returns - Gzip tar
*/
hotBackup(): stream.Readable {
return tar.create({
gzip: true,
cwd: this.#serverStorage
}, []);
export class versionsStorages<T> extends Map<string, T> {
get(key: string|number): T {
if (typeof key === "number") return super.get(Array.from(this.keys()).at(key));
return super.get(key);
}
#severProcess: child_process.ChildProcess;
getStdout() {
return this.#severProcess.stdout;
}
getStderr() {
return this.#severProcess.stderr;
}
getStdin() {
return this.#severProcess.stdin;
}
/**
* Start server
*
* @returns get from server actions
*/
async startServer() {
if (this.#severProcess) return;
let processEnv: { [k: string]: string } = {};
if (this.#io.runServer.env) {
const { env } = this.#io.runServer;
if (env instanceof Map) {
processEnv = Array.from(env.keys()).reduce<typeof processEnv>((acc, keyName) => {
if (env.get(keyName) !== undefined) acc[keyName] = String(env.get(keyName));
return acc;
}, {});
} else {
processEnv = Object.keys(env).reduce<typeof processEnv>((acc, keyName) => {
if (env[keyName] !== undefined) acc[keyName] = String(env[keyName]);
return acc;
}, {});
}
}
const runDate = new Date();
this.#severProcess = child_process.spawn(this.#io.runServer.command, (this.#io.runServer.args || []).map(String), {
env: { ...process.env, ...processEnv },
cwd: this.#serverStorage,
stdio: this.#io.runServer.stdio,
});
const logPath = path.join(this.#logStorage, String(runDate.getFullYear()), String(runDate.getMonth() + 1), String(runDate.getDate()));
const logpathRoot = createWriteStream(path.join(this.#logStorage, "all.log"));
if (!(await extendsFS.exists(logPath))) await fs.mkdir(logPath, {recursive: true});
if (this.#severProcess.stdout) {
this.#severProcess.stdout.pipe(createWriteStream(path.join(logPath, "stdout.log")));
this.#severProcess.stdout.pipe(logpathRoot);
readline(this.#severProcess.stdout).on("line", line => this.emit("line", line));
}
if (this.#severProcess.stderr) {
this.#severProcess.stderr.pipe(createWriteStream(path.join(logPath, "stderr.log")));
this.#severProcess.stderr.pipe(logpathRoot);
readline(this.#severProcess.stderr).on("line", line => this.emit("line", line));
}
this.emit("onServerSpawn", this.#severProcess);
}
};
}

View File

@ -0,0 +1,12 @@
import path from "node:path";
import { listVersion, Bedrock } from "../src/platform/bedrock/index.js";
import { homedir } from "node:os";
await listVersion.listMojang();
const mojang = new Bedrock(path.join(homedir(), ".bdsmaneger/playgroud/mojang"), "mojang");
const version = Array.from(listVersion.mojangCache.keys()).at(9);
console.log("Installing %s", version);
await mojang.installServer(version);
mojang.on("logLine", (line) => console.log(line[0]));
const pr = await mojang.runServer();
process.stdin.pipe(pr.stdin);