fix some bugs

This commit is contained in:
q13x 2024-03-21 01:06:42 -07:00
parent 4b465802d1
commit 26275fa0d7
5 changed files with 93 additions and 9 deletions

View File

@ -15,8 +15,8 @@ export const config: Config = {
cache: { cache: {
useCache: true, useCache: true,
folderName: "skinCache", folderName: "skinCache",
skinCacheLifetime: 60 * 1000, skinCacheLifetime: 60 * 60 * 1000,
skinCachePruneInterval: 5000, skinCachePruneInterval: 10 * 60 * 1000,
}, },
}, },
motd: true motd: true

View File

@ -20,7 +20,8 @@ export default class DiskDB<T extends any> {
public async filter(f: (v: T) => boolean) { public async filter(f: (v: T) => boolean) {
for (const file of await fs.readdir(this.folder)) { for (const file of await fs.readdir(this.folder)) {
if (!f(this.decoder(await fs.readFile(file)))) await fs.rm(file); const fp = path.join(this.folder, file);
if (!f(this.decoder(await fs.readFile(fp)))) await fs.rm(fp);
} }
} }

View File

@ -0,0 +1,63 @@
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
export default class ExponentialBackoffRequestController {
public queue: Task[];
public flushQueueAfterTries: number;
public baseDelay: number;
ended: boolean;
aborted: boolean;
constructor(baseDelay: number = 3000, triesBeforeFlush: number = 10) {
this.flushQueueAfterTries = triesBeforeFlush;
this.baseDelay = baseDelay;
this.queue = [];
this.ended = false;
this.aborted = false;
setTimeout(() => this.tick(), 0);
}
private async tick() {
while (true) {
if (this.ended) break;
for (const task of this.queue) {
if (this.ended || this.aborted) break;
let times = 0,
breakOut = false;
while (true) {
try {
await task();
break;
} catch (err) {
times++;
await wait(this.baseDelay * 2 ** times);
if (times > this.flushQueueAfterTries) {
this.queue.forEach((task) => task(new Error("Controller overload!")));
breakOut = true;
break;
}
}
}
if (breakOut) break;
}
if (this.aborted) this.aborted = false;
this.queue = [];
await wait(1);
}
}
public end() {
this.ended = true;
}
public flush() {
this.aborted = false;
this.queue.forEach((task) => task(new Error("Aborted")));
this.queue = [];
}
public queueTask(task: Task): void {
this.queue.push(task);
}
}
type Task = (err?: object) => void | Promise<void>;

View File

@ -11,6 +11,7 @@ import { Logger } from "../../logger.js";
import fetch from "node-fetch"; import fetch from "node-fetch";
import Jimp from "jimp"; import Jimp from "jimp";
import { ImageEditor } from "./ImageEditor.js"; import { ImageEditor } from "./ImageEditor.js";
import ExponentialBackoffRequestController from "../ratelimit/ExponentialBackoffRequestController.js";
// TODO: convert all functions to use MineProtocol's UUID manipulation functions // TODO: convert all functions to use MineProtocol's UUID manipulation functions
@ -81,7 +82,10 @@ export namespace EaglerSkins {
return new Promise<Buffer>(async (res, rej) => { return new Promise<Buffer>(async (res, rej) => {
const skin = await fetch(skinUrl); const skin = await fetch(skinUrl);
if (skin.status != 200) { if (skin.status != 200) {
rej(`Tried to fetch ${skinUrl}, got HTTP ${skin.status} instead!`); rej({
url: skinUrl,
status: skin.status,
});
return; return;
} else { } else {
res(Buffer.from(await skin.arrayBuffer())); res(Buffer.from(await skin.arrayBuffer()));
@ -89,6 +93,20 @@ export namespace EaglerSkins {
}); });
} }
export function safeDownloadSkin(skinUrl: string, backoff: ExponentialBackoffRequestController): Promise<Buffer> {
return new Promise((res, rej) => {
backoff.queueTask(async (err) => {
if (err) return rej(err);
try {
res(await downloadSkin(skinUrl));
} catch (err) {
if (err.status == 429) throw new Error("Ratelimited!");
else rej("Unexpected HTTP status code: " + err.status);
}
});
});
}
export function readClientDownloadSkinRequestPacket(message: Buffer): ClientDownloadSkinRequest { export function readClientDownloadSkinRequestPacket(message: Buffer): ClientDownloadSkinRequest {
const ret: ClientDownloadSkinRequest = { const ret: ClientDownloadSkinRequest = {
id: null, id: null,

View File

@ -11,11 +11,13 @@ import { SCChannelMessagePacket } from "../packets/channel/SCChannelMessage.js";
import { EaglerSkins } from "./EaglerSkins.js"; import { EaglerSkins } from "./EaglerSkins.js";
import { ImageEditor } from "./ImageEditor.js"; import { ImageEditor } from "./ImageEditor.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
import ExponentialBackoffRequestController from "../ratelimit/ExponentialBackoffRequestController.js";
export class SkinServer { export class SkinServer {
public allowedSkinDomains: string[]; public allowedSkinDomains: string[];
public cache: DiskDB<CachedSkin>; public cache: DiskDB<CachedSkin>;
public proxy: Proxy; public proxy: Proxy;
public backoffController: ExponentialBackoffRequestController;
public usingNative: boolean; public usingNative: boolean;
public usingCache: boolean; public usingCache: boolean;
private _logger: Logger; private _logger: Logger;
@ -29,13 +31,14 @@ export class SkinServer {
cacheFolder, cacheFolder,
(v) => exportCachedSkin(v), (v) => exportCachedSkin(v),
(b) => readCachedSkin(b), (b) => readCachedSkin(b),
(k) => k (k) => k.replaceAll("-", "")
); );
} }
this.proxy = proxy ?? PROXY; this.proxy = proxy ?? PROXY;
this.usingCache = useCache; this.usingCache = useCache;
this.usingNative = native; this.usingNative = native;
this.lifetime = cacheLifetime; this.lifetime = cacheLifetime;
this.backoffController = new ExponentialBackoffRequestController();
this._logger = new Logger("SkinServer"); this._logger = new Logger("SkinServer");
this._logger.info("Started EaglercraftX skin server."); this._logger.info("Started EaglercraftX skin server.");
if (useCache) this.deleteTask = setInterval(async () => await this.cache.filter((ent) => Date.now() < ent.expires), sweepInterval); if (useCache) this.deleteTask = setInterval(async () => await this.cache.filter((ent) => Date.now() < ent.expires), sweepInterval);
@ -88,17 +91,16 @@ export class SkinServer {
skin = null; skin = null;
if (this.usingCache) { if (this.usingCache) {
(cacheHit = await this.cache.get(parsedPacket_1.uuid)), (skin = cacheHit != null ? cacheHit.data : null); (cacheHit = await this.cache.get(parsedPacket_1.uuid)), (skin = cacheHit != null ? cacheHit.data : null);
if (!skin) { if (!skin) {
this._logger.info("cache miss: getting skin"); const fetched = await EaglerSkins.safeDownloadSkin(parsedPacket_1.url, this.backoffController);
const fetched = await EaglerSkins.downloadSkin(parsedPacket_1.url);
skin = fetched; skin = fetched;
await this.cache.set(parsedPacket_1.uuid, { await this.cache.set(parsedPacket_1.uuid, {
uuid: parsedPacket_1.uuid, uuid: parsedPacket_1.uuid,
expires: Date.now() + this.lifetime, expires: Date.now() + this.lifetime,
data: fetched, data: fetched,
}); });
this._logger.info("downloaded skin, saved to cache"); }
} else this._logger.info("hit success: got skin");
} else { } else {
skin = await EaglerSkins.downloadSkin(parsedPacket_1.url); skin = await EaglerSkins.downloadSkin(parsedPacket_1.url);
} }