Files
electric-cherry/src/lib/server/fleet/snapshot.ts
T

109 lines
4.7 KiB
TypeScript

import { db } from '../db/index.js';
import { getServer, listServers } from './servers.js';
import { observationReader, serverObservation } from './status.js';
import { config, rconConfigured } from '../config.js';
import { runningVersions } from '../integrations/plutonium.js';
import { listHosts } from '../hosts/index.js';
import type { NativeInstance } from '../backends/native.js';
import { serverOperations } from './overview.js';
import { hostListStatus } from '../../ui/host-overview.js';
import { playerCapacityReader } from './player-capacity.js';
export function fleetSnapshot(serverName?: string) {
const bindings = serverName === undefined ? [] : [serverName];
const rconHost = config.rcon.host;
const hosts = listHosts();
const serverNow = Date.now();
const hostsById = new Map(hosts.map(({ inventory: _inventory, ...host }) => [host.id, host]));
const versions = new Map(
runningVersions(serverName).map((version) => [version.server, version])
);
const instances = new Map(
(
db()
.prepare(
`SELECT * FROM native_instances WHERE retired_at IS NULL ${serverName === undefined ? '' : 'AND server = ?'}`
)
.all(...bindings) as NativeInstance[]
).map((instance) => [instance.server, instance])
);
const holds = new Map<string, { target: string; betaId: number }[]>();
for (const row of db()
.prepare(
`SELECT bsv.server, s.target_rel_path AS target, b.id AS betaId
FROM beta_servers bsv JOIN betas b ON b.id=bsv.beta_id
JOIN beta_server_scripts bs ON bs.beta_id=b.id AND bs.server=bsv.server
JOIN scripts s ON s.id=bs.script_id WHERE bsv.server_deleted_at IS NULL AND b.state IN ('running','ready-promote')
${serverName === undefined ? '' : 'AND bsv.server = ?'}`
)
.all(...bindings) as { server: string; target: string; betaId: number }[]) {
const entries = holds.get(row.server) ?? [];
entries.push({ target: row.target, betaId: row.betaId });
holds.set(row.server, entries);
}
const observe = serverName === undefined ? observationReader() : serverObservation;
const capacity = playerCapacityReader(serverName);
const selected = serverName === undefined ? undefined : getServer(serverName);
const rows =
serverName === undefined
? listServers()
: selected && !selected.destroyed_at
? [selected]
: [];
const servers = rows.map((s) => {
const instance = s.native_instance_id ? instances.get(s.name) : null;
return {
...observe(s),
...capacity(s.name),
host: s.host_id ? (hostsById.get(s.host_id) ?? null) : null,
network: instance
? {
bindAddress: instance.bind_address,
localPort: instance.port,
publicAddress: instance.public_address,
publicPort: instance.public_port
}
: null,
plutonium: versions.get(s.name),
// The host agent owns the game connection.
endpoint: instance
? { host: `agent:${instance.host_id}`, port: instance.port }
: s.game_port
? { host: s.rcon_host ?? rconHost, port: s.game_port }
: null,
held: holds.get(s.name) ?? []
};
});
return {
servers,
serverNow,
// Include hosts without assigned servers. Re-evaluate heartbeat age on
// every existing snapshot refresh rather than adding a navigation poll.
hostAttention: hosts.filter((host) =>
['attention', 'offline'].includes(hostListStatus(host, serverNow).key)
).length,
reservedNames:
serverName === undefined
? (
db()
.prepare(
"SELECT j.server FROM server_creation_jobs j JOIN server_deployments d ON d.id = j.deployment_id WHERE d.state IN ('queued', 'running', 'awaiting-health')"
)
.all() as { server: string }[]
).map((row) => row.server)
: [],
integrations: {
native: true,
hostsReady: hosts.filter((host) => host.readiness === 'ready').length,
hostsTotal: hosts.length,
rcon: rconConfigured(),
rconTimeoutMs: config.rcon.timeoutMs,
defaultRconHost: rconHost
}
};
}
export function serverSnapshot(name: string) {
return { ...fleetSnapshot(name), operations: serverOperations(name) };
}