Files
minecraft/dashboard/src/server/scheduler.ts
T

97 lines
2.8 KiB
TypeScript

import cron, { type ScheduledTask } from "node-cron";
import { db } from "./db";
import { notify } from "./notify";
import { powerContainer } from "./docker";
import { rcon } from "./rcon";
import { say } from "./mc";
import { env } from "./env";
import { triggerBackup } from "./backups";
/**
* Scheduled tasks (cron-based): restarts, backups, announcements, arbitrary
* console commands. Definitions live in the panel DB; jobs are (re)loaded
* whenever they change.
*/
export type Schedule = {
id: number;
name: string;
cron: string;
action: "restart" | "backup" | "command" | "announce";
payload: string | null;
enabled: number;
last_run_at: number | null;
last_result: string | null;
};
const g = globalThis as unknown as { __craftdeckJobs?: Map<number, ScheduledTask> };
function jobs(): Map<number, ScheduledTask> {
if (!g.__craftdeckJobs) g.__craftdeckJobs = new Map();
return g.__craftdeckJobs;
}
export function validateCron(expr: string): boolean {
return cron.validate(expr);
}
export async function runScheduleAction(action: Schedule["action"], payload: string | null): Promise<string> {
switch (action) {
case "restart": {
try {
await say("Server restarting in 30 seconds!");
await new Promise((r) => setTimeout(r, 30_000));
} catch {
// server not reachable over RCON — restart anyway
}
await powerContainer(env.mcContainer, "restart");
return "Restarted";
}
case "backup": {
return await triggerBackup();
}
case "command": {
if (!payload) throw new Error("No command configured");
return (await rcon().exec(payload)).slice(0, 500);
}
case "announce": {
if (!payload) throw new Error("No message configured");
await say(payload);
return "Announced";
}
}
}
async function execute(schedule: Schedule) {
let result: string;
try {
result = await runScheduleAction(schedule.action, schedule.payload);
if (schedule.action !== "announce") {
notify("success", `Schedule "${schedule.name}" ran`, result, { discord: false });
}
} catch (err) {
result = `ERROR: ${(err as Error).message}`;
notify("error", `Schedule "${schedule.name}" failed`, (err as Error).message);
}
db()
.prepare("UPDATE schedules SET last_run_at = ?, last_result = ? WHERE id = ?")
.run(Date.now(), result.slice(0, 1000), schedule.id);
}
export function reloadSchedules() {
for (const task of jobs().values()) task.stop();
jobs().clear();
const rows = db().prepare("SELECT * FROM schedules WHERE enabled = 1").all() as unknown as Schedule[];
for (const row of rows) {
if (!cron.validate(row.cron)) continue;
jobs().set(
row.id,
cron.schedule(row.cron, () => void execute(row))
);
}
}
export function startScheduler() {
reloadSchedules();
}