Core apis #528

Merged
Sirherobrine23 merged 3 commits from coreApis into main 2023-06-12 01:39:52 +00:00
14 changed files with 514 additions and 995 deletions
Showing only changes of commit 1d94fa451b - Show all commits

View File

@ -1,37 +1,3 @@
# Bds Maneger Core
Basic core to install, update and manage several minecraft servers automatically, depending on a few dependencies, the basic being **Nodejs**.
## Servers supports and TODO
**Bedrock Mojang**:
- [x] Install/Update.
- [ ] Hot backup.
- [x] Start.
- [x] Port Listened.
- [ ] Player connect/disconnect/spawn.
- [ ] Player kick/ban.
**Pocketmine PMMP**:
- [x] Install/Update.
- [ ] Hot backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconnect.
- [ ] Player kick/ban.
**Powernukkit** and **Cloudbust**:
- [x] Install/Update.
- 🚫 Hot backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconnect.
- [ ] Player kick/ban.
**Java Mojang**, **Purpur**, **Paper** and **Spigot**:
- [x] Install/Update.
- 🚫 Hot Backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconect action.
- [ ] Player kick/ban.
Um simples nucleo com exposição de APIs para integração com os Servidores de Minecraft para gerenciamento no Nodejs.

View File

@ -1,7 +1,7 @@
{
"name": "@the-bds-maneger/core",
"version": "6.0.4",
"description": "",
"description": "Core APIs to maneger Minecraft servers platforms",
"main": "src/index.js",
"types": "src/index.d.ts",
"type": "module",
@ -11,24 +11,22 @@
"access": "public"
},
"scripts": {
"prepack": "tsc --build --clean && tsc",
"postpack": "tsc --build --clean",
"build": "tsc --build --clean && tsc"
"prepack": "tsc --build --clean && tsc --build",
"postpack": "tsc --build --clean"
},
"dependencies": {
"@sirherobrine23/cloud": "^3.6.11",
"@sirherobrine23/extends": "^3.6.11",
"@sirherobrine23/http": "^3.6.11",
"adm-zip": "0.5.10",
"sanitize-filename": "^1.6.3",
"semver": "^7.5.1",
"tar": "^6.1.15",
"unzip-stream": "^0.3.1",
"unzipper": "^0.10.14"
"xml-js": "^1.6.11"
},
"devDependencies": {
"@types/adm-zip": "0.5.0",
"@types/semver": "^7.5.0",
"@types/tar": "^6.1.5",
"@types/unzip-stream": "^0.3.1",
"@types/unzipper": "^0.10.6"
"@types/tar": "^6.1.5"
}
}

View File

@ -1,5 +0,0 @@
import { oracleBucket } from "@sirherobrine23/cloud";
/**
* Bucket readonly
*/
export const oracleStorage = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");

View File

@ -0,0 +1,5 @@
import { oracleBucket } from "@sirherobrine23/cloud";
/**
* Bucket readonly
*/
export const bdsFilesBucket = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");

View File

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

View File

@ -0,0 +1,168 @@
import { http, Github } from "@sirherobrine23/http";
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 } }
}
export const mojangCache = new Map<string, 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
}));
}
type powerNukkitRelease = {
version: string;
minecraftVersion: string;
releaseTime: number;
commitId: string;
snapshotBuild?: number;
artefacts: string[];
}
export interface powernukkitDownload {
version: string;
releaseDate: Date;
releaseType: "snapshot"|"oficial";
url: string;
}
export const powernukkitCache = new Map<string, 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)));
for (const data of releasesData) {
if (powernukkitCache.has(data.minecraftVersion)) continue;
const releateDate = new Date(data.releaseTime);
const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
function buildArtefactUrl(data: any, artefactId?: string) {
const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), releateDate.getUTCFullYear().toString().padStart(4, "0") + (releateDate.getUTCMonth() + 1).toString().padStart(2, "0") + releateDate.getUTCDate().toString().padStart(2, "0") + "." + releateDate.getUTCHours().toString().padStart(2, "0") + releateDate.getUTCMinutes().toString().padStart(2, "0") + releateDate.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
if (artefactId == "GIT_SOURCE") {
if (data.commitId) return util.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
} else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
return buildReleaseArtefactUrl(data, artefactId);
}
const artefacts = data.artefacts.reduce<{[key: string]: string}>((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {});
if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) continue;
powernukkitCache.set(data.minecraftVersion, {
version: data.minecraftVersion,
releaseDate: releateDate,
releaseType: data.releaseType === "releases" ? "oficial" : "snapshot",
url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
});
}
}
export interface cloudburstDownload {
releaseDate: Date;
url: string;
}
export const nukkitCache = new Map<string, cloudburstDownload>();
export const cloudburstCache = new Map<string, cloudburstDownload>();
export async function listCloudburstProject() {
const Projects = [ "Nukkit", "Server" ] as const;
for (const Project of Projects) {
const { body: { jobs } } = await http.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/api/json`);
const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
const { body: { builds } } = await http.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/api/json`);
return Promise.all(builds.map(async build => {
const { body: { artifacts, result, timestamp, actions } } = await http.jsonRequest<{result: "SUCCESS", actions: {_class: string, [k: string]: any}[], timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/api/json`);
if (result !== "SUCCESS") return [];
const branchBuild = actions.find(r => typeof r["buildsByBranchName"] === "object");
if (!branch) return [];
const commitID = ((branchBuild?.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.marked?.SHA1 || branchBuild.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.revision?.SHA1) as string|undefined)
let mcpeVersion: string;
if (Project === "Server") {
const json = xml.xml2js((await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Server/${commitID}/pom.xml`)).toString("utf8"), {compact: true});
const info = json["project"].dependencies.dependency.find(dep => dep.groupId._text === "com.nukkitx");
mcpeVersion = info.version._text;
} else {
try {
// https://raw.githubusercontent.com/CloudburstMC/Nukkit/master/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java
const lines = (await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Nukkit/${commitID}/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java`)).toString("utf8").split("\n");
const versions = lines.filter(l => l.trim().startsWith("String") && l.toLowerCase().includes("version")).reduce<{[k: string]: string}>((acc, line) => {
line = line.trim().slice(6).trim();
const i = line.indexOf("=");
const def = line.slice(0, i).trim();
let version = line.slice(i+1).trim().slice(1, -1);
if (version.endsWith("\"")) version = version.slice(0, -1).trim();
if (version.startsWith("v")) version = version.slice(1);
if (!(version.includes("+"))) acc[def] = version;
return acc;
}, {});
mcpeVersion = versions.MINECRAFT_VERSION || versions.MINECRAFT_VERSION_NETWORK;
} catch {
return [];
}
}
if (!mcpeVersion) return [];
return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
buildNumber: build.number,
branch,
mcpeVersion,
releaseDate: new Date(timestamp),
url: `https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
}));
}));
})).then(r => r.flat(2));
for (const build of buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime())) {
if (Project === "Server") {
if (cloudburstCache.has(build.mcpeVersion)) continue;
cloudburstCache.set(build.mcpeVersion, {
releaseDate: build.releaseDate,
url: build.url
});
} else {
if (nukkitCache.has(build.mcpeVersion)) continue;
nukkitCache.set(build.mcpeVersion, {
releaseDate: build.releaseDate,
url: build.url
});
}
}
}
}
export interface pocketmineDownload {
releateDate: Date;
releaseType: "preview" | "oficial";
url: string;
}
export const pocketmineCache = new Map<string, 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);
for (const data of pocketmineReleases) {
if (pocketmineCache.has(data.tag_name)) continue;
const assest = data.assets.find(assert => assert.name.endsWith(".phar"));
pocketmineCache.set(data.tag_name, {
releaseType: data.prerelease ? "preview" : "oficial",
releateDate: new Date(assest.created_at),
url: assest.browser_download_url
});
};
}
export function getCacheVersions() {
return {
mojang: Array.from(mojangCache.keys()).reduce<{[k: string]: mojangInfo}>((acc, key) => {acc[key] = mojangCache.get(key); return acc;}, {}),
pocketmine: Array.from(pocketmineCache.keys()).reduce<{[k: string]: pocketmineDownload}>((acc, key) => {acc[key] = pocketmineCache.get(key); return acc;}, {}),
powernukkit: Array.from(powernukkitCache.keys()).reduce<{[k: string]: powernukkitDownload}>((acc, key) => {acc[key] = powernukkitCache.get(key); return acc;}, {}),
nukkit: Array.from(nukkitCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = nukkitCache.get(key); return acc;}, {}),
cloudburst: Array.from(cloudburstCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = cloudburstCache.get(key); return acc;}, {}),
};
}

View File

@ -0,0 +1,19 @@
import { getCacheVersions } from "./listVersion.js";
import { serverManeger } from "../../serverRun.js";
import semver from "semver";
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 async function runServer() {}

View File

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

View File

@ -0,0 +1,163 @@
import { Github, http } from "@sirherobrine23/http";
import stream from "node:stream";
import semver from "semver";
import path from "path";
import { bdsFilesBucket } from "../../internalClouds.js";
interface baseDownload {
URL: string;
releaseDate: Date;
}
interface mojangInfo extends baseDownload {
release: "oficial" | "snapshot" | "beta" | "alpha";
}
async function PromiseSplit<T extends (any[])>(arrayInput: T, fn: (value: T[number]) => any): Promise<Awaited<ReturnType<typeof fn>>[]> {
const backup = ([]).concat(...arrayInput);
let i = arrayInput.length, b = 1; while (i > 2) { b++; i /= 2 };
const arraySplit = Math.max(2, Math.floor(b));
let result: Awaited<ReturnType<typeof fn>>[] = [];
await Promise.all(Array(arraySplit).fill(async function call() {
return Promise.resolve().then(() => backup.length > 0 ? fn(backup.shift()) : null).then(data => {
result.push(data);
if (backup.length > 0) return call();
return null;
});
}).map(a => a()));
return result.flat(1);
}
export const mojangCache = new Map<string, mojangInfo>();
export async function listMojang() {
const versions = (await http.jsonRequestBody<{ versions: { id: string, releaseTime: string, url: string, type: "snapshot" | "release" | "old_beta" | "old_alpha" }[] }>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).versions;
await PromiseSplit(versions, async version => {
if (mojangCache.has(version.id)) return;
const { downloads: { server } } = await http.jsonRequestBody<{ downloads: { server?: { url: string } } }>(version.url);
if (!server) return;
mojangCache.set(version.id, {
release: version.type === "release" ? "oficial" : version.type === "snapshot" ? "snapshot" : version.type === "old_beta" ? "beta" : "alpha",
releaseDate: new Date(version.releaseTime),
URL: server.url,
});
});
}
export interface spigotDownload {
getServer(): Promise<stream.Readable>;
craftbukkit?(): Promise<stream.Readable>;
}
const spigotCache = new Map<string, spigotDownload>();
export async function listSpigot() {
const spigotFiles = await bdsFilesBucket.listFiles("SpigotBuild/");
for (const file of spigotFiles.filter(file => file.name.slice(12).startsWith("1.")).sort((b, a) => {
const valid = (c: typeof a) => semver.valid(semver.coerce(c.name.slice(12, -4), { loose: true }), { loose: true });
return semver.compare(valid(a), valid(b));
})) {
const version = file.name.slice(12, -4), fixVersion = semver.valid(semver.coerce(version, { loose: true }), { loose: true });
if (spigotCache.has(fixVersion)) continue;
const craftbukkit = spigotFiles.find(file => file.name.slice(12).startsWith(`craftbukkit-${version}.jar`));
spigotCache.set(fixVersion, {
getServer: file.getFile,
craftbukkit: craftbukkit ? craftbukkit.getFile : undefined,
});
}
}
export const paperCache = new Map<string, baseDownload>();
export const velocityCache = new Map<string, baseDownload>();
export const foliaCache = new Map<string, baseDownload>();
export async function listPaperProject() {
const paperProjects = ["paper", "velocity", "folia"] as const;
await Promise.all(paperProjects.map(async projectName => {
const baseURL = new URL(path.posix.join("/v2/projects", projectName), "https://api.papermc.io");
const projectInfo = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
for (const projectVersion of projectInfo.versions) {
if (projectName === "paper" && paperCache.has(projectVersion)) continue;
else if (projectName === "velocity" && velocityCache.has(projectVersion)) continue;
else if (projectName === "folia" && foliaCache.has(projectVersion)) continue;
else {
const versionBase = new URL(path.posix.join(baseURL.pathname, "versions", projectVersion), baseURL);
const builds = await http.jsonRequestBody<{ builds: number[] }>(versionBase);
for (const build of builds.builds) {
const buildURL = new URL(path.posix.join(versionBase.pathname, "builds", String(build)), versionBase);
const downloadInfo = await http.jsonRequestBody<{ time: string; downloads: { application?: { name: string } } }>(buildURL);
if (downloadInfo.downloads.application) {
const downloadURL = new URL(path.posix.join(buildURL.pathname, "downloads", downloadInfo.downloads.application.name), buildURL);
if (projectName === "paper") paperCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
else if (projectName === "velocity") velocityCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
else if (projectName === "folia") foliaCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
break;
}
}
}
}
}));
}
export const purpurCache = new Map<string, baseDownload>();
export async function listPurpurProject() {
const baseURL = new URL("https://api.purpurmc.org/v2/purpur");
const { versions } = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
for (const version of versions) {
if (purpurCache.has(version)) continue;
const infoBase = new URL(path.posix.join(baseURL.pathname, version, "latest"), baseURL);
const relInfo = await http.jsonRequestBody<{ timestamp: number }>(infoBase);
purpurCache.set(version, {
URL: (new URL(path.posix.join(infoBase.pathname, "download"), infoBase)).toString(),
releaseDate: new Date(relInfo.timestamp)
});
}
}
export const glowstoneCache = new Map<string, baseDownload>();
export async function listGlowstoneProject() {
const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
const rels = (await repo.release.getRelease()).filter(rel => rel.assets.some(asset => asset.name.endsWith(".jar")));
rels.forEach(rel => rel.assets.forEach(asset => glowstoneCache.has(rel.tag_name) ? null : glowstoneCache.set(rel.tag_name, {
URL: asset.browser_download_url,
releaseDate: new Date(asset.created_at)
})));
}
export const cuberiteCache = new Map<string, { URL: string[] }>([
[
"win32-x64",
{
URL: ["https://download.cuberite.org/windows-x86_64/Cuberite.zip"]
}
],
[
"win32-ia32",
{
URL: ["https://download.cuberite.org/windows-i386/Cuberite.zip"]
}
]
]);
export async function listCuberite() {
const projects = ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"] as const;
await Promise.all(projects.map(async project => {
const { builds = [] } = await http.jsonRequestBody<{ builds: { number: number, _class: string }[] }>(`https://builds.cuberite.org/job/${project}/api/json`);
for (const job of builds) {
const { artifacts = [], result } = await http.jsonRequestBody<{ result: string, artifacts: { relativePath: string, fileName: string }[] }>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
if (result !== "SUCCESS") continue;
let map = artifacts.filter(file => !file.fileName.endsWith(".sha1")).map(file => `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`);
if (project === "android") {
const serverIndex = map.findIndex(file => file.toLowerCase().endsWith("server.zip"));
const server = map[serverIndex];
delete map[serverIndex];
map = map.filter(Boolean);
for (const file of map) {
const fileURL = new URL(file);
const plat = path.basename(fileURL.pathname).replace(path.extname(fileURL.pathname), "");
if (plat.startsWith("x86_64")) cuberiteCache.set("android-x64", { URL: [server, file] });
else if (plat.startsWith("x86")) cuberiteCache.set("android-ia32", { URL: [server, file] });
else if (plat.startsWith("arm64")) cuberiteCache.set("android-arm64", { URL: [server, file] });
else if (plat.startsWith("arm")) cuberiteCache.set("android-arm", { URL: [server, file] });
}
} else cuberiteCache.set(project, { URL: map });
break;
}
}));
}

View File

@ -1,4 +1,7 @@
export * from "./serverManeger.js";
export * as serverManeger from "./serverManeger.js";
export * as Bedrock from "./servers/bedrock.js";
export * as Java from "./servers/java.js";
// Bedrock platform
export * as Bedrock from "./platform/bedrock/index.js";
export * as bedrock from "./platform/bedrock/index.js";
// Java platform
export * as Java from "./platform/java/index.js";
export * as java from "./platform/java/index.js";

View File

@ -1,317 +0,0 @@
import readline from "node:readline";
import { createWriteStream } from "node:fs";
import { extendsFS } from "@sirherobrine23/extends";
import { pipeline } from "node:stream/promises";
import { format } from "node:util";
import sanitizeFilename from "sanitize-filename";
import child_process from "node:child_process";
import crypto from "node:crypto";
import stream from "node:stream";
import path from "node:path";
import tar from "tar";
import fs from "node:fs/promises";
import os from "node:os";
// Default bds maneger core
const ENVROOT = process.env.BDSCOREROOT || process.env.bdscoreroot;
export const bdsManegerRoot = ENVROOT ? path.resolve(process.cwd(), ENVROOT) : path.join(os.homedir(), ".bdsmaneger");
if (!(await extendsFS.exists(bdsManegerRoot))) await fs.mkdir(bdsManegerRoot, {recursive: true});
export type withPromise<T> = T|Promise<T>;
export interface manegerOptions {
ID?: string,
newID?: boolean,
};
// only letters and numbers
const idReg = /^[a-zA-Z0-9_]+$/;
export interface serverManegerV1 {
id: string,
rootPath: string,
serverFolder: string,
backup: string,
logs: string,
platform: "java"|"bedrock",
runCommand(options: Omit<runOptions, "cwd">): ReturnType<typeof runServer>
};
/**
* Platform path maneger
*/
export async function serverManeger(platform: serverManegerV1["platform"], options: manegerOptions): Promise<serverManegerV1> {
if (!((["java", "bedrock"]).includes(platform))) throw new TypeError("Invalid platform target!");
if (!options) throw new TypeError("Please add serverManeger options!");
const platformFolder = path.join(bdsManegerRoot, platform);
if ((await fs.readdir(platformFolder).then(a => a.length).catch(() => 0)) === 0 && options.newID === undefined) options.newID = true;
// Create or check if exists
if (options.newID === true) {
while(true) {
options.ID = typeof crypto.randomUUID === "function" ? crypto.randomUUID().split("-").join("_") : crypto.randomBytes(crypto.randomInt(8, 14)).toString("hex");
if (!(idReg.test(options.ID))) continue;
if (!((await fs.readdir(platformFolder).catch(() => [])).includes(options.ID))) break;
}
await fs.mkdir(path.join(platformFolder, options.ID), {recursive: true});
} else {
// Test invalid ID
if (String(options.ID).length > 32) throw new TypeError("options.ID is invalid, is very long!");
if (!(!!options.ID && idReg.test(options.ID))) throw new TypeError("options.ID is invalid");
else if (!((await fs.readdir(platformFolder)).includes(options.ID))) throw new Error("ID not exists")
}
// Folders
const rootPath = path.join(platformFolder, path.posix.resolve("/", sanitizeFilename(options.ID)));
const serverFolder = path.join(rootPath, "server");
const backup = path.join(rootPath, "backups");
const log = path.join(rootPath, "logs");
for await (const p of [
serverFolder,
backup,
log,
]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, {recursive: true});
return {
id: options.ID,
platform,
rootPath,
serverFolder,
backup,
logs: log,
async runCommand(options: Omit<runOptions, "cwd">) {
return runServer({...options, cwd: serverFolder});
}
};
}
export async function listIDs(): Promise<{id: string, platform: "bedrock"|"java", delete: () => Promise<void>}[]> {
const main = [];
for await (const platform of ["bedrock", "java"]) {
try {
const platformFolder = path.join(bdsManegerRoot, platform);
if (!(await extendsFS.exists(platformFolder))) continue;
const IDs = await fs.readdir(platformFolder);
for await (const id of IDs) main.push({
id: id,
platform,
async delete() {
return fs.rm(path.join(platformFolder, id), {recursive: true, force: true});
}
});
} catch {}
}
return main;
}
export type portListen = {
port: number,
protocol: "TCP"|"UDP"|"both",
listenOn?: string,
listenFrom?: "server"|"plugin"
};
export type playerAction = {
playerName: string,
onDate: Date,
action: string,
extra?: any
};
export type runOptions = {
cwd: string,
env?: {[k: string]: string|number|boolean},
command: string,
args?: (string|number|boolean)[],
stdio?: child_process.StdioOptions,
paths: serverManegerV1,
serverActions?: {
stop?(this: serverRun): withPromise<void>,
playerAction?(this: serverRun, lineString: string): withPromise<null|void|playerAction>,
hotBackup?(this: serverRun): withPromise<stream.Readable|void>,
portListen?(this: serverRun, lineString: string): withPromise<void|portListen>,
onAvaible?(this: serverRun, lineString: string): withPromise<void|Date>,
postStart?: ((this: serverRun) => withPromise<void>)[],
}
};
export declare class serverRun extends child_process.ChildProcess {
on(event: string, listener: (...args: any[]) => void): this;
once(event: string, listener: (...args: any[]) => void): this;
on(event: "error", listener: (err: Error) => void): this;
once(event: "error", listener: (err: Error) => void): this;
on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
on(event: "disconnect", listener: () => void): this;
once(event: "disconnect", listener: () => void): this;
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
on(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
once(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
on(event: "spawn", listener: () => void): this;
once(event: "spawn", listener: () => void): this;
on(event: "warning", listener: (data: any) => void): this;
once(event: "warning", listener: (data: any) => void): this;
// BDS Assigns
once(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
on(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
once(event: "player", fn: (playerInfo: playerAction) => void): this;
on(event: "player", fn: (playerInfo: playerAction) => void): this;
once(event: "portListening", fn: (portInfo: portListen) => void): this;
on(event: "portListening", fn: (portInfo: portListen) => void): this;
once(event: "serverAvaible", fn: (date: Date) => void): this;
on(event: "serverAvaible", fn: (date: Date) => void): this;
once(event: "backup", fn: (filePath: string) => void): this;
on(event: "backup", fn: (filePath: string) => void): this;
once(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
on(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
avaibleDate?: Date;
runOptions: runOptions;
portListening: portListen[];
logPath: {stderr: string, stdout: string, merged: string};
playerActions: playerAction[];
stdoutInterface: readline.Interface;
stderrInterface: readline.Interface;
stopServer(): Promise<{code?: number, signal?: NodeJS.Signals}>;
sendCommand(streamPipe: stream.Readable): this;
sendCommand(...args: (string|number|boolean)[]): this;
hotBackup(): this & Promise<Awaited<ReturnType<runOptions["serverActions"]["hotBackup"]>>>;
}
/**
* Run servers globally and hormonally across servers
*/
export async function runServer(options: runOptions): Promise<serverRun> {
if (!options.stdio) options.stdio = ["pipe", "pipe", "pipe"];
const child = child_process.spawn(options.command, [...((options.args ?? []).map(String))], {
// maxBuffer: Infinity,
stdio: options.stdio,
cwd: options.cwd,
env: {
...process.env,
...Object.keys(options.env ?? {}).reduce((acc, a) => {
acc[a] = String(options.env[a]);
return acc;
}, {})
}
}) as serverRun;
child.runOptions = options;
child.portListening = [];
child.playerActions = [];
for (const std of [child.stdout, child.stderr]) if (!std) {
child.kill("SIGKILL");
throw new TypeError("Stdout or Stderr stream disabled, killed process, cannot continue to exec server, check stdio passed to spawn!");
}
// Log Write
const currentDate = new Date();
const baseLog = path.join(options.paths.logs, format("%s_%s_%s_%s-%s-%s", currentDate.getDate(), currentDate.getMonth()+1, currentDate.getFullYear(), currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds()));
await fs.mkdir(baseLog, {recursive: true});
child.logPath = {stdout: path.join(baseLog, "stdout.log"), stderr: path.join(baseLog, "stderr.log"), merged: path.join(baseLog, "server.log")};
const allLog = createWriteStream(child.logPath.merged);
child.stdout.pipe(allLog);
child.stdout.pipe(createWriteStream(child.logPath.stdout));
child.stderr.pipe(allLog);
child.stderr.pipe(createWriteStream(child.logPath.stderr));
// Lines
const stdout = child.stdoutInterface = readline.createInterface(child.stdout).on("line", data => child.emit("line", data, "stdout")).on("error", err => child.emit("error", err));
const stderr = child.stderrInterface = readline.createInterface(child.stderr).on("line", data => child.emit("line", data, "stderr")).on("error", err => child.emit("error", err));
if (typeof options.serverActions?.playerAction === "function") {
for (const std of [stdout, stderr]) std.on("line", async data => {
const playerData = await Promise.resolve(options.serverActions.playerAction.call(child, data) as ReturnType<typeof options.serverActions.playerAction>);
if (!playerData) return;
child.playerActions.push(playerData);
child.emit("player", playerData);
});
}
if (typeof options.serverActions?.portListen === "function") {
for (const std of [stdout, stderr]) std.on("line", async data => {
const portData = await Promise.resolve(options.serverActions.portListen.call(child, data) as ReturnType<typeof options.serverActions.portListen>);
if (!portData) return;
portData.listenFrom ??= "server";
child.portListening.push(portData);
child.emit("portListening", portData);
});
}
child.sendCommand = function (...args) {
if (!child.stdin.writable) {
child.emit("error", new Error("cannot send command to server"));
return child;
};
if (args[0] instanceof stream.Readable) {
args[0].on("data", data => child.stdin.write(data)).once("close", () => child.stdin.write("\n"));
return child;
}
child.stdin.write(args.map(String).join(" ")+"\n");
return child;
}
child.stopServer = async function () {
child.sendCommand("");
const stop = options.serverActions?.stop ?? function () {
child.kill("SIGINT");
const kill = setTimeout(() => {
clearTimeout(kill);
if (child.exitCode !== null) return;
child.kill("SIGKILL");
}, 2500);
};
Promise.resolve().then(() => stop.call(child)).catch(err => child.emit("error", err));
return new Promise((done, reject) => child.once("error", reject).once("exit", (code, signal) => done({code, signal})));
}
child.hotBackup = function hotBackup() {
return Object.assign({}, Promise.resolve().then((async () => {
if (!options.serverActions?.hotBackup) throw new Error("Hot backup disabled to current platform!");
child.emit("backup", "start");
return Promise.resolve(options.serverActions.hotBackup.call(child) as ReturnType<typeof options.serverActions.hotBackup>).then(data => {
child.emit("backup", "success");
return data;
}).catch(err => {
child.emit("backup", "fail");
return Promise.reject(err);
});
})), child);
}
if (typeof options.serverActions?.onAvaible === "function") {
let run = options.serverActions.onAvaible;
for (const std of [stdout, stderr]) std.on("line", async data => {
if (!run) return null;
const avaibleDate = await Promise.resolve(run.call(child, data) as ReturnType<typeof run>);
if (!avaibleDate) return;
child.avaibleDate = avaibleDate;
if (options.serverActions?.postStart) for (const ss of options.serverActions?.postStart) Promise.resolve().then(() => ss.call(child)).catch(err => child.emit("error", err));
});
} else if (options.serverActions?.postStart?.length > 0) child.emit("warning", "no post actions run!");
child.once("close", async () => {
const cDate = new Date();
const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
const backupFile = path.join(options.paths.backup, String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
try {
if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
const ff = await fs.readdir(options.paths.serverFolder);
await pipeline(tar.create({
gzip: true,
cwd: options.paths.serverFolder,
prefix: ""
}, ff), createWriteStream(backupFile));
child.emit("backup", backupFile);
} catch (err) {
if (await extendsFS.exists(backupFile)) await fs.unlink(backupFile);
child.emit("error", err);
}
});
return child;
}

View File

@ -0,0 +1,142 @@
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>;
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");
}
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);
return this;
}
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;
}
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
}, []);
}
#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

@ -1,388 +0,0 @@
import fsOld, { promises as fs } from "node:fs";
import coreHttp, { Github } from "@sirherobrine23/http";
import { runOptions, serverManegerV1 } from "../serverManeger.js";
import { oracleStorage } from "../internal.js";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
import extendsFS, { promiseChildProcess } from "@sirherobrine23/extends";
import semver from "semver";
import unzip from "unzipper";
import utils from "node:util";
import path from "node:path";
import tar from "tar";
export interface bedrockOptions {
/**
* Alternative server instead of official Mojang server
*/
altServer?: "mojang"|"pocketmine"|"powernukkit"|"nukkit"|"cloudbust",
};
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
export type bedrockList = {
date: Date,
version: string,
release: "preview"|"stable",
downloads: {
php?: {
installPHP(serverPath: serverManegerV1): Promise<void>,
},
server: {
getServer(): Promise<Readable>,
url?: string,
urls?: {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: string
}
}
[K: string]: any
}
}
};
/**
* List Minecrft bedrock server versions
*
* @param altServer - Alternative server of official Mojang
* @returns
*/
export async function listVersions(altServer?: bedrockOptions["altServer"]): Promise<bedrockList[]> {
if (!altServer) altServer = "mojang";
if (altServer) if (!(["mojang", "cloudbust", "cloudbust", "nukkit", "pocketmine", "powernukkit"]).includes(altServer)) throw new TypeError("Invalid alt server");
if (altServer === "pocketmine") {
return (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url).map(rel => ({
date: new Date(rel.created_at),
version: rel.tag_name,
release: rel.prerelease ? "preview" : "stable",
downloads: {
php: {
async installPHP(serverPath: serverManegerV1) {
const phpFile = (await oracleStorage.listFiles("php_bin")).find(file => file.name.includes(process.platform) && file.name.includes(process.arch));
if (!phpFile) throw new Error(`Unable to find php files for ${process.platform} with architecture ${process.arch}`);
if (phpFile.name.endsWith(".tar.gz")||phpFile.name.endsWith(".tgz")||phpFile.name.endsWith(".tar")) await pipeline(oracleStorage.getFile(phpFile.name), tar.extract({cwd: serverPath.serverFolder}));
else if (phpFile.name.endsWith(".zip")) await pipeline(oracleStorage.getFile(phpFile.name), unzip.Extract({path: serverPath.serverFolder}));
else throw new Error("Found file is not supported!");
return null
},
},
server: {
url: (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url,
async getServer() {
const pharFile = rel.assets.find(assert => assert.name.endsWith(".phar"));
if (!pharFile) throw new Error("Version not includes server file!");
return coreHttp.streamRequest(pharFile.browser_download_url);
}
}
}
}));
} else if (altServer === "powernukkit") {
const releases_version = (await coreHttp.jsonRequest<{[k: string]: {version: string, releaseTime: number, minecraftVersion: string, artefacts: string[], commitId: string, snapshotBuild?: number}[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json")).body;
return Object.keys(releases_version).reduce((acc, key) => acc.concat(releases_version[key]), [] as (typeof releases_version)[string]).map(data => {
const dt = new Date(data.releaseTime);
const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
function buildArtefactUrl(data: any, artefactId?: string) {
const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), dt.getUTCFullYear().toString().padStart(4, "0") + (dt.getUTCMonth() + 1).toString().padStart(2, "0") + dt.getUTCDate().toString().padStart(2, "0") + "." + dt.getUTCHours().toString().padStart(2, "0") + dt.getUTCMinutes().toString().padStart(2, "0") + dt.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
if (artefactId == "GIT_SOURCE") {
if (data.commitId) return utils.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
} else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
else return buildReleaseArtefactUrl(data, artefactId);
return null;
}
const artefacts = data.artefacts.reduce((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {} as {[key: string]: string});
return {
date: dt,
version: data.version,
release: data.snapshotBuild ? "stable" : "preview",
downloads: {
server: {
mcpeVersion: data.minecraftVersion,
url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
async getServer() {
if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) throw new Error("Cannot get server file to the version!");
return coreHttp.streamRequest(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)
},
}
}
};
});
} else if (altServer === "cloudbust"||altServer === "nukkit") {
const { body: { jobs } } = await coreHttp.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/api/json`);
const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
const { body: { builds } } = await coreHttp.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/api/json`);
return Promise.all(builds.map(async build => {
const { body: { artifacts, result, timestamp } } = await coreHttp.jsonRequest<{result: "SUCCESS", timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/api/json`);
if (result !== "SUCCESS") return [];
return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
buildNumber: build.number,
branch,
releaseDate: new Date(timestamp),
url: `https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
}));
}));
})).then(r => r.flat(2));
return buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
date: rel.releaseDate,
release: "preview",
version: `${rel.branch}_${rel.buildNumber}`,
downloads: {
server: {
url: rel.url,
async getServer() {
return coreHttp.streamRequest(rel.url);
},
}
}
}));
} else if (altServer === "mojang") {
return (await coreHttp.jsonRequest<{version: string, date: Date, release?: "stable"|"preview", url: {[platform in NodeJS.Platform]?: {[arch in NodeJS.Architecture]?: string}}}[]>("https://sirherobrine23.github.io/BedrockFetch/all.json")).body.sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version)))).map(rel => ({
version: rel.version,
date: new Date(rel.date),
release: rel.release === "preview" ? "preview" : "stable",
downloads: {
server: {
url: rel.url[process.platform]?.[process.arch],
async getServer() {
const platformURL = (rel.url[process.platform] ?? rel.url["linux"]);
if (!platformURL) throw new Error("Cannot get platform URL");
const arch = platformURL[process.arch] ?? platformURL["x64"];
if (!arch) throw new Error("Cannot get bedrock server to current arch");
return coreHttp.streamRequest(arch);
},
urls: rel.url
}
}
}));
} else throw new Error("Invalid platform");
}
export async function installServer(serverPath: serverManegerV1, options: bedrockOptions & {version?: string, allowBeta?: boolean}) {
const versions = await listVersions(options?.altServer);
if (!options.altServer) options.altServer = "mojang";
if (options.altServer === "pocketmine") {
const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
if (!rel) throw new Error("Version not exsists");
await rel.downloads.php.installPHP(serverPath);
await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.phar")));
return {
...rel,
id: serverPath.id,
};
} else if (options.altServer === "cloudbust" || options.altServer === "powernukkit" || options.altServer === "nukkit") {
if ((["cloudbust", "nukkit"]).includes(options.altServer)) options.version = "latest";
const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
if (!rel) throw new Error("Version not exists");
await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.jar")));
return {
...rel,
id: serverPath.id,
};
} else if (options.altServer === "mojang") {
const bedrockVersion = versions.find(rel => {
if (rel.release === "preview") if (options.allowBeta !== true) return false;
const version = (options.version ?? "latest").trim();
if (version.toLowerCase() === "latest") return true;
return rel.version === version;
});
if (!bedrockVersion) throw new Error("Não existe essa versão");
let downloadUrl = bedrockVersion.downloads.server.url;
if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
if (!downloadUrl) {
for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
if (downloadUrl) break;
if (await promiseChildProcess.commandExists(emu)) downloadUrl = bedrockVersion.downloads.server.urls.linux?.x64;
}
}
}
if (!downloadUrl) throw new Error(`Não existe o URL de download para ${process.platform} na arquitetura ${process.arch}`);
const filesBackup = ["server.properties", "valid_known_packs.json", "permissions.json", "allowlist.json", "whitelist.json"];
const datS = (await Promise.all(filesBackup.map(async f => !await extendsFS.exists(path.join(serverPath.serverFolder, f)) ? null : ({path: f, data: await fs.readFile(path.join(serverPath.serverFolder, f))})))).filter(a => !!a);
await pipeline(await coreHttp.streamRequest(downloadUrl), unzip.Extract({path: serverPath.serverFolder}));
await Promise.all(datS.map(async f => fs.writeFile(f.path, f.data)));
return {
...bedrockVersion,
id: serverPath.id,
};
} else throw new Error("Invalid platform");
}
export async function startServer(maneger: serverManegerV1, options: bedrockOptions) {
if (!options.altServer) options.altServer = "mojang";
if (options.altServer === "powernukkit"||options.altServer === "cloudbust") {
return maneger.runCommand({
command: "java",
args: [
"-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",
],
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop");
},
}
})
} else if (options.altServer === "pocketmine") {
return maneger.runCommand({
command: (await extendsFS.readdir(maneger.serverFolder)).find(file => file.endsWith("php")||file.endsWith("php.exe")),
args: [
"server.phar",
"--no-wizard"
],
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop")
},
}
});
}
if (process.platform === "darwin") throw new Error("Run in docker or podman!");
const run: Omit<runOptions, "cwd"> = {
command: path.join(maneger.serverFolder, "bedrock_server"+(process.platform === "win32" ? ".exe" : "")),
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop");
},
portListen(lineString) {
// [INFO] IPv4 supported, port: 19132
// [2023-03-08 13:01:57 INFO] Listening on IPv4 port: 19132
const ipProtocol = lineString.slice(lineString.indexOf("IPv"), lineString.indexOf("IPv")+4);
if (ipProtocol) {
let port = lineString.slice(lineString.lastIndexOf("port:")+5).trim();
if (port.indexOf(":") !== -1) port = port.slice(0, port.lastIndexOf(":"));
return {
protocol: "UDP",
listenOn: ipProtocol.toLowerCase() === "ipv4" ? "0.0.0.0" : "[::]",
port: Number(port),
};
}
return null;
},
playerAction(lineString) {
lineString = lineString.replace(/^(.*)?\[.*\]/, "").trim();
if (lineString.startsWith("Player")) {
lineString = lineString.replace("Player", "").trim();
// Spawned, disconnected, connected
let action: string;
if (lineString.startsWith("Spawned")) action = "Spawned";
else if (lineString.startsWith("disconnected")) action = "disconnected";
else if (lineString.startsWith("connected")) action = "connected";
if (!action) return null;
lineString = lineString.replace(action, "").trim();
if (lineString.startsWith(":")) lineString = lineString.slice(1).trim();
let playerName = lineString.substring(0, lineString.indexOf("xuid:")-1).trim();
if (!playerName) return null;
if (playerName.endsWith(",")) playerName = playerName.slice(0, playerName.length - 1);
let xuid: string;
if (lineString.indexOf("xuid:") !== -1) {
xuid = lineString.slice(lineString.indexOf("xuid:")+5).trim();
if (!xuid) xuid = null;
}
return {
onDate: new Date(),
action,
playerName,
extra: {
xuid
}
};
};
return null;
},
onAvaible(data) {
// [2023-03-06 21:37:27:699 INFO] Server started.
data = data.replace(/^.*?\[.*\]/, "").trim();
if (data.includes("started") && data.includes("Server")) return new Date();
return null
},
async hotBackup() {
const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
let ok = ff.endsWith(".json");
if (!ok) ok = ff === "server.properties";
if (!ok) ok = ff === "worlds";
return ok;
});
return tar.create({
gzip: true,
cwd: this.runOptions.paths.serverFolder,
prefix: ""
}, ff);
},
postStart: [
async function() {
let breaked = false;
this.once("close", () => breaked = true);
let w: any;
this.once("close", () => w.close());
while(true) {
if (breaked) break;
await new Promise(done => {
this.once("close", done);
w = fsOld.watch(this.runOptions.paths.serverFolder, {recursive: true}, () => {w.close(); done(null);}).on("error", () => {});
});
await new Promise(done => setTimeout(done, 1000));
const cDate = new Date();
const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
const backupFile = path.join(this.runOptions.paths.backup, "hotBackup", String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
let ok = ff.endsWith(".json");
if (!ok) ok = ff === "server.properties";
if (!ok) ok = ff === "worlds";
return ok;
});
const hotTar = tar.create({
gzip: true,
cwd: this.runOptions.paths.serverFolder,
prefix: ""
}, ff);
this.emit("hotBackup", hotTar);
await pipeline(hotTar, fsOld.createWriteStream(backupFile));
}
}
]
}
};
if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
if (await promiseChildProcess.commandExists(emu)) {
run.args = [run.command, ...run.args];
run.command = emu;
break;
}
}
}
return maneger.runCommand(run);
}

View File

@ -1,237 +0,0 @@
import { serverManegerV1 } from "../serverManeger.js";
import { oracleStorage } from "../internal.js";
import { extendsFS } from "@sirherobrine23/extends";
import { pipeline } from "node:stream/promises";
import coreHttp, { Github } from "@sirherobrine23/http";
import semver from "semver";
import stream from "node:stream";
import utils from "node:util";
import path from "node:path";
import fs from "node:fs";
export interface javaOptions {
/**
* Alternative server instead of official Mojang server
*/
altServer?: "mojang"|"spigot"|"paper"|"purpur"|"glowstone"|"folia"|"cuberite"
};
export type javaList = {
version: string,
release: "stable"|"snapshot",
date?: Date,
platform?: NodeJS.Platform;
architecture?: NodeJS.Architecture;
getFile: {
fileName: string;
stream(): Promise<stream.Readable>;
fileURL?: string;
}[],
};
export async function listVersions(altServer?: javaOptions["altServer"]): Promise<javaList[]> {
if (!altServer) altServer = "mojang";
if (altServer) if(!(["mojang", "paper", "folia", "purpur", "spigot", "glowstone", "cuberite"]).includes(altServer)) throw new TypeError("Invalid alt server!");
if (altServer === "purpur") {
return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>("https://api.purpurmc.org/v2/purpur")).body.versions.map(async (version): Promise<javaList> => ({
version,
release: "stable",
date: new Date((await coreHttp.jsonRequest<{timestamp: number}>(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest", version))).body.timestamp),
getFile: [
{
fileName: "server.jar",
fileURL: utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version),
async stream() {return coreHttp.streamRequest(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version));},
}
],
})))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "paper" || altServer === "folia") {
const uriPATH = ["/v2/projects", altServer];
return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>(new URL(uriPATH.join("/"), "https://api.papermc.io"))).body.versions.map(async (version): Promise<javaList> => {
const build = (await coreHttp.jsonRequest<{builds: number[]}>(new URL(uriPATH.concat(["versions", version]).join("/"), "https://api.papermc.io"))).body.builds.at(-1);
const data = (await coreHttp.jsonRequest<{time: string, downloads: {[k: string]: {name: string, sha256: string}}}>(new URL(uriPATH.concat(["versions", version, "builds", build.toString()]).join("/"), "https://api.papermc.io"))).body;
const fileUrl = new URL(uriPATH.concat(["versions", version, "builds", build.toString(), "downloads", data.downloads["application"].name]).join("/"), "https://api.papermc.io");
return {
version,
date: new Date(data.time),
release: "stable",
getFile: [{
fileURL: fileUrl.toString(),
fileName: "server.jar",
async stream() {return coreHttp.streamRequest(fileUrl);}
}]
}
}))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "spigot") {
const fileList = await oracleStorage.listFiles("SpigotBuild");
return fileList.filter(f => f.name.endsWith(".jar") && !f.name.includes("craftbukkit-")).map((file): javaList => {
let version: string;
if (!(version = semver.valid(semver.coerce(file.name.replace("SpigotBuild/", "").replace(".jar", ""))))) return null;
const craftBuckit = ([...fileList]).reverse().find(file => file.name.startsWith("SpigotBuild/craftbukkit-"+version) || file.name.startsWith("SpigotBuild/craftbukkit-"+(version.endsWith(".0") ? version.slice(0, -2) : version)));
return {
release: "stable",
version,
getFile: [
...(craftBuckit ? [{
fileName: path.basename(craftBuckit.name),
async stream() {
return craftBuckit.getFile();
},
}] : []),
{
fileName: "server.jar",
async stream() {
return file.getFile();
},
},
],
};
}).filter(rel => !!rel).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "glowstone") {
const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
const rels = await repo.release.getRelease();
return rels.filter(rel => !!rel.assets.find(asset => asset.name.endsWith(".jar"))).map(rel => ({
version: rel.tag_name,
release: "stable",
fileUrl: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
getFile: [{
fileURL: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url);
},
}]
}));
} else if (altServer === "cuberite") {
const buildFiles: {buildNumber: number; project: string; releaseDate: Date; url: string;}[] = [];
for (const project of ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"]) {
const { builds = [] } = await coreHttp.jsonRequestBody<{builds: {number: number, _class: string}[]}>(`https://builds.cuberite.org/job/${project}/api/json`);
await Promise.all(builds.slice(0, 16).map(async job => {
const { artifacts = [], result, timestamp } = await coreHttp.jsonRequestBody<{timestamp: number, result: string, artifacts: {relativePath: string, fileName: string}[]}>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
if (result !== "SUCCESS") return;
artifacts.filter(file => !file.fileName.endsWith(".sha1")).forEach(file => buildFiles.push({
project,
url: `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`,
buildNumber: job.number,
releaseDate: new Date(timestamp),
}));
}));
}
return buildFiles.concat([
{
buildNumber: -1,
project: "windows-x86_64",
releaseDate: new Date(),
url: "https://download.cuberite.org/windows-x86_64/Cuberite.zip",
},
{
buildNumber: -1,
project: "windows-x86",
releaseDate: new Date(),
url: "https://download.cuberite.org/windows-i386/Cuberite.zip",
},
]).sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
date: rel.releaseDate,
version: `${rel.project}_${rel.buildNumber}`,
platform: rel.project.startsWith("windows") ? "win32" : rel.project.startsWith("linux") ? "linux" : rel.project.startsWith("android") ? "android" : rel.project.startsWith("darwin") ? "darwin" : undefined,
architecture: rel.project.endsWith("x86_64") ? "x64" : rel.project.endsWith("i386") ? "ia32" : rel.project.endsWith("armhf") ? "arm" : rel.project.endsWith("aarch64") ? "arm64" : undefined,
release: "stable",
fileUrl: rel.url,
getFile: [{
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(rel.url);
},
}]
}));
} else if (altServer === "mojang") {
return (await Promise.all((await coreHttp.jsonRequest<{versions: {id: string, releaseTime: string, url: string, type: "snapshot"|"release"}[]}>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).body.versions.map(async (data): Promise<javaList> => {
const fileURL = (await coreHttp.jsonRequest<{downloads: {[k: string]: {size: number, url: string}}}>(data.url)).body.downloads?.["server"]?.url;
if (!fileURL) return null;
return {
version: data.id,
date: new Date(data.releaseTime),
release: data.type === "snapshot" ? "snapshot" : "stable",
getFile: [{
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(fileURL);
},
}],
};
}))).filter(a => !!a);
} else throw new Error("Invalid platform");
}
export async function installServer(serverPath: serverManegerV1, options: javaOptions & {version?: string, allowBeta?: boolean}) {
if (!options.altServer) options.altServer = "mojang";
const version = (await listVersions(options.altServer)).filter(rel => rel.release === "stable" ? true : !!options.allowBeta).find(rel => (!options.version || options.version === "latest" || rel.version === options.version));
if (!version) throw new Error("The specified version does not exist!");
for (const file of version.getFile) await pipeline(await file.stream(), fs.createWriteStream(path.join(serverPath.serverFolder, file.fileName)));
await fs.promises.writeFile(path.join(serverPath.serverFolder, "eula.txt"), "eula=true\n");
return {
id: serverPath.id,
version: version.version,
release: version.release,
date: version.date,
};
}
export async function startServer(serverPath: serverManegerV1, options: javaOptions) {
if (!options.altServer) options.altServer = "mojang";
// Java server
if (await extendsFS.exists(path.join(serverPath.serverFolder, "server.jar"))) {
return serverPath.runCommand({
command: "java",
args: [
"-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",
"--nogui",
// Save world in worlds folder
"--universe", "worlds",
// ...extraArgs,
],
paths: serverPath,
serverActions: {
stop() {
this.sendCommand("stop");
},
portListen(lineString) {
// [21:19:18] [Server thread/INFO]: Starting Minecraft server on *:25565
lineString = lineString.replace(/^.*?\[.*\]:/, "").trim();
if (lineString.startsWith("Starting Minecraft server on")) {
if (lineString.lastIndexOf(":") === -1) return null;
const port = Number(lineString.slice(lineString.lastIndexOf(":")+1));
return {
port,
protocol: "TCP",
listenFrom: "server"
};
}
return null;
},
}
});
}
throw new Error("install server or check if server installed correctly!");
}