Depot API & v4 (#298)
* feat: nginx + torrential basics & services system * fix: lint + i18n * fix: update torrential to remove openssl * feat: add torrential to Docker build * feat: move to self hosted runner * fix: move off self-hosted runner * fix: update nginx.conf * feat: torrential cache invalidation * fix: update torrential for cache invalidation * feat: integrity check task * fix: lint * feat: move to version ids * fix: client fixes and client-side checks * feat: new depot apis and version id fixes * feat: update torrential * feat: droplet bump and remove unsafe update functions * fix: lint * feat: v4 featureset: emulators, multi-launch commands * fix: lint * fix: mobile ui for game editor * feat: launch options * fix: lint * fix: remove axios, use $fetch * feat: metadata and task api improvements * feat: task actions * fix: slight styling issue * feat: fix style and lints * feat: totp backend routes * feat: oidc groups * fix: update drop-base * feat: creation of passkeys & totp * feat: totp signin * feat: webauthn mfa/signin * feat: launch selecting ui * fix: manually running tasks * feat: update add company game modal to use new SelectorGame * feat: executor selector * fix(docker): update rust to rust nightly for torrential build (#305) * feat: new version ui * feat: move package lookup to build time to allow for deno dev * fix: lint * feat: localisation cleanup * feat: apply localisation cleanup * feat: potential i18n refactor logic * feat: remove args from commands * fix: lint * fix: lockfile --------- Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import type { ChildProcess } from "child_process";
|
||||
import { logger } from "../logging";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
class ServiceManager {
|
||||
private services: Map<string, Service<unknown>> = new Map();
|
||||
|
||||
register(name: string, service: Service<unknown>) {
|
||||
this.services.set(name, service);
|
||||
}
|
||||
|
||||
spin() {
|
||||
for (const service of this.services.values()) {
|
||||
service.spin();
|
||||
}
|
||||
}
|
||||
|
||||
kill() {
|
||||
for (const service of this.services.values()) {
|
||||
service.kill();
|
||||
}
|
||||
}
|
||||
|
||||
healthchecks() {
|
||||
return this.services
|
||||
.entries()
|
||||
.map(([name, service]) => ({ name, healthy: service.serviceHealthy() }))
|
||||
.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
export type Executor = () => ChildProcess;
|
||||
export type Setup = () => Promise<boolean>;
|
||||
export type Healthcheck = () => Promise<boolean>;
|
||||
export class Service<T> {
|
||||
name: string;
|
||||
private executor: Executor;
|
||||
private setup: Setup | undefined;
|
||||
private healthcheck: Healthcheck | undefined;
|
||||
|
||||
private logger: Logger<never>;
|
||||
|
||||
private currentProcess: ChildProcess | undefined;
|
||||
|
||||
private runningHealthcheck: boolean = false;
|
||||
private healthy: boolean = true;
|
||||
private spun: boolean = false;
|
||||
|
||||
private uutils: T;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
executor: Executor,
|
||||
setup?: Setup,
|
||||
healthcheck?: Healthcheck,
|
||||
utils?: T,
|
||||
) {
|
||||
this.name = name;
|
||||
const serviceLogger = logger.child({ name: `service-${name}` });
|
||||
this.logger = serviceLogger;
|
||||
this.executor = executor;
|
||||
this.setup = setup;
|
||||
this.healthcheck = healthcheck;
|
||||
this.uutils = utils!;
|
||||
}
|
||||
|
||||
spin() {
|
||||
if (this.spun) return;
|
||||
this.launch();
|
||||
|
||||
if (this.healthcheck) {
|
||||
setInterval(this.runHealthcheck, 1000 * 60 * 5); // Every 5 minutes
|
||||
}
|
||||
|
||||
this.spun = true;
|
||||
}
|
||||
|
||||
kill() {
|
||||
this.spun = false;
|
||||
this.currentProcess?.kill();
|
||||
}
|
||||
|
||||
register() {
|
||||
serviceManager.register(this.name, this);
|
||||
}
|
||||
|
||||
private async launch() {
|
||||
if (this.currentProcess) return;
|
||||
const disableEnv = `EXTERNAL_SERVICE_${this.name.toUpperCase()}`;
|
||||
if (!process.env[disableEnv]) {
|
||||
const serviceProcess = this.executor();
|
||||
this.logger.info("service launched");
|
||||
serviceProcess.on("close", async (code, signal) => {
|
||||
serviceProcess.kill();
|
||||
this.currentProcess = undefined;
|
||||
this.logger.warn(
|
||||
`service exited with code ${code} (${signal}), restarting...`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
if (this.spun) this.launch();
|
||||
});
|
||||
serviceProcess.stdout?.on("data", (data) =>
|
||||
this.logger.info(data.toString().trim()),
|
||||
);
|
||||
serviceProcess.stderr?.on("data", (data) =>
|
||||
this.logger.error(data.toString().trim()),
|
||||
);
|
||||
this.currentProcess = serviceProcess;
|
||||
}
|
||||
|
||||
if (this.setup) {
|
||||
while (true) {
|
||||
try {
|
||||
const hasSetup = await this.setup();
|
||||
if (hasSetup) break;
|
||||
throw "setup function returned false...";
|
||||
} catch (e) {
|
||||
this.logger.warn(`failed setup, trying again... | ${e}`);
|
||||
await new Promise((r) => setTimeout(r, 7000));
|
||||
}
|
||||
}
|
||||
this.healthy = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async runHealthcheck() {
|
||||
if (!this.healthcheck || !this.currentProcess || this.runningHealthcheck)
|
||||
return;
|
||||
this.runningHealthcheck = true;
|
||||
let fails = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const successful = await this.healthcheck();
|
||||
if (successful) break;
|
||||
} finally {
|
||||
/* empty */
|
||||
}
|
||||
this.healthy = false;
|
||||
fails++;
|
||||
if (fails >= 5) {
|
||||
this.currentProcess.kill();
|
||||
this.runningHealthcheck = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.healthy = true;
|
||||
this.runningHealthcheck = false;
|
||||
}
|
||||
|
||||
serviceHealthy() {
|
||||
return this.healthy;
|
||||
}
|
||||
|
||||
utils() {
|
||||
return this.uutils;
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceManager = new ServiceManager();
|
||||
export default serviceManager;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { spawn } from "child_process";
|
||||
import { Service } from "..";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
export const NGINX_SERVICE = new Service(
|
||||
"nginx",
|
||||
() => {
|
||||
const nginxConfig = path.resolve(
|
||||
process.env.NGINX_CONFIG ?? "./build/nginx.conf",
|
||||
);
|
||||
const nginxPrefix = path.join(systemConfig.getDataFolder(), "nginx");
|
||||
fs.mkdirSync(nginxPrefix, { recursive: true });
|
||||
|
||||
return spawn("nginx", ["-c", nginxConfig, "-p", nginxPrefix]);
|
||||
},
|
||||
undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
async () => await $fetch(`http://127.0.0.1:8080/`),
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { spawn } from "child_process";
|
||||
import { Service } from "..";
|
||||
import fs from "fs";
|
||||
import prisma from "../../db/database";
|
||||
import { logger } from "../../logging";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
|
||||
const INTERNAL_DEPOT_URL = new URL(
|
||||
process.env.INTERNAL_DEPOT_URL ?? "http://localhost:5000",
|
||||
);
|
||||
|
||||
export const TORRENTIAL_SERVICE = new Service(
|
||||
"torrential",
|
||||
() => {
|
||||
const localDir = fs.readdirSync(".");
|
||||
if ("torrential" in localDir) return spawn("./torrential", [], {});
|
||||
|
||||
const envPath = process.env.TORRENTIAL_PATH;
|
||||
if (envPath) return spawn(envPath, [], {});
|
||||
|
||||
return spawn("torrential", [], {});
|
||||
},
|
||||
async () => {
|
||||
const externalUrl = systemConfig.getExternalUrl();
|
||||
const depot = await prisma.depot.upsert({
|
||||
where: {
|
||||
id: "torrential",
|
||||
},
|
||||
update: {
|
||||
endpoint: `${externalUrl}/api/v1/depot`,
|
||||
},
|
||||
create: {
|
||||
id: "torrential",
|
||||
endpoint: `${externalUrl}/api/v1/depot`,
|
||||
},
|
||||
});
|
||||
|
||||
await $fetch(`${INTERNAL_DEPOT_URL.toString()}key`, {
|
||||
method: "POST",
|
||||
body: { key: depot.key },
|
||||
});
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
async () => await $fetch(`${INTERNAL_DEPOT_URL.toString()}healthcheck`),
|
||||
{
|
||||
async invalidate(gameId: string, versionId: string) {
|
||||
try {
|
||||
await $fetch(`${INTERNAL_DEPOT_URL.toString()}invalidate`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
game: gameId,
|
||||
version: versionId,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn("invalidate torrential cache failed with error: " + e);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user