feat: overhaul server automation, files editor, and CS2 setup workflows
This commit is contained in:
+246
-6
@@ -32,6 +32,21 @@ interface DaemonServerResponse {
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface DaemonNodeStatusRaw {
|
||||
version: string;
|
||||
is_healthy: boolean;
|
||||
uptime_seconds: number;
|
||||
active_servers: number;
|
||||
}
|
||||
|
||||
interface DaemonNodeStatsRaw {
|
||||
cpu_percent: number;
|
||||
memory_used: number;
|
||||
memory_total: number;
|
||||
disk_used: number;
|
||||
disk_total: number;
|
||||
}
|
||||
|
||||
interface DaemonStatusResponse {
|
||||
uuid: string;
|
||||
state: string;
|
||||
@@ -66,6 +81,13 @@ interface DaemonPlayerListRaw {
|
||||
max_players: number;
|
||||
}
|
||||
|
||||
interface DaemonBackupResponseRaw {
|
||||
backup_id: string;
|
||||
size_bytes: number;
|
||||
checksum: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DaemonConsoleOutput {
|
||||
uuid: string;
|
||||
line: string;
|
||||
@@ -95,9 +117,40 @@ export interface DaemonPlayersResponse {
|
||||
maxPlayers: number;
|
||||
}
|
||||
|
||||
export interface DaemonBackupResponse {
|
||||
backupId: string;
|
||||
sizeBytes: number;
|
||||
checksum: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DaemonNodeStatus {
|
||||
version: string;
|
||||
isHealthy: boolean;
|
||||
uptimeSeconds: number;
|
||||
activeServers: number;
|
||||
}
|
||||
|
||||
export interface DaemonNodeStats {
|
||||
cpuPercent: number;
|
||||
memoryUsed: number;
|
||||
memoryTotal: number;
|
||||
diskUsed: number;
|
||||
diskTotal: number;
|
||||
}
|
||||
|
||||
type UnaryCallback<TResponse> = (error: grpc.ServiceError | null, response: TResponse) => void;
|
||||
|
||||
interface DaemonServiceClient extends grpc.Client {
|
||||
getNodeStatus(
|
||||
request: EmptyResponse,
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<DaemonNodeStatusRaw>,
|
||||
): void;
|
||||
streamNodeStats(
|
||||
request: EmptyResponse,
|
||||
metadata: grpc.Metadata,
|
||||
): grpc.ClientReadableStream<DaemonNodeStatsRaw>;
|
||||
createServer(
|
||||
request: DaemonCreateServerRequest,
|
||||
metadata: grpc.Metadata,
|
||||
@@ -147,6 +200,21 @@ interface DaemonServiceClient extends grpc.Client {
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<EmptyResponse>,
|
||||
): void;
|
||||
createBackup(
|
||||
request: { server_uuid: string; backup_id: string; cdn_upload_url?: string },
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<DaemonBackupResponseRaw>,
|
||||
): void;
|
||||
restoreBackup(
|
||||
request: { server_uuid: string; backup_id: string; cdn_download_url?: string },
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<EmptyResponse>,
|
||||
): void;
|
||||
deleteBackup(
|
||||
request: { server_uuid: string; backup_id: string },
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<EmptyResponse>,
|
||||
): void;
|
||||
getActivePlayers(
|
||||
request: { uuid: string },
|
||||
metadata: grpc.Metadata,
|
||||
@@ -183,27 +251,34 @@ const POWER_ACTIONS: Record<PowerAction, number> = {
|
||||
kill: 3,
|
||||
};
|
||||
|
||||
const MAX_GRPC_MESSAGE_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
function buildGrpcTarget(fqdn: string, grpcPort: number): string {
|
||||
const trimmed = fqdn.trim();
|
||||
if (!trimmed) throw new Error('Node FQDN is empty');
|
||||
|
||||
let host = trimmed;
|
||||
if (trimmed.includes('://')) {
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const host = parsed.hostname || parsed.host;
|
||||
host = parsed.hostname || parsed.host;
|
||||
if (!host) throw new Error('Node FQDN has no hostname');
|
||||
if (parsed.port) return `${host}:${parsed.port}`;
|
||||
return `${host}:${grpcPort}`;
|
||||
} catch {
|
||||
// Fall through to raw handling below.
|
||||
}
|
||||
}
|
||||
|
||||
const withoutPath = trimmed.replace(/\/.*$/, '');
|
||||
const withoutPath = host.replace(/\/.*$/, '');
|
||||
if (/^\[.+\](?::\d+)?$/.test(withoutPath)) {
|
||||
return /\]:\d+$/.test(withoutPath) ? withoutPath : `${withoutPath}:${grpcPort}`;
|
||||
const innerHost = withoutPath
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\](?::\d+)?$/, '');
|
||||
return `[${innerHost}]:${grpcPort}`;
|
||||
}
|
||||
if (/^[^:]+:\d+$/.test(withoutPath)) {
|
||||
const hostOnly = withoutPath.replace(/:\d+$/, '');
|
||||
return `${hostOnly}:${grpcPort}`;
|
||||
}
|
||||
if (/^[^:]+:\d+$/.test(withoutPath)) return withoutPath;
|
||||
if (withoutPath.includes(':')) return `[${withoutPath}]:${grpcPort}`;
|
||||
return `${withoutPath}:${grpcPort}`;
|
||||
}
|
||||
@@ -219,6 +294,10 @@ function createClient(node: DaemonNodeConnection): DaemonServiceClient {
|
||||
return new DaemonService(
|
||||
target,
|
||||
grpc.credentials.createInsecure(),
|
||||
{
|
||||
'grpc.max_send_message_length': MAX_GRPC_MESSAGE_BYTES,
|
||||
'grpc.max_receive_message_length': MAX_GRPC_MESSAGE_BYTES,
|
||||
},
|
||||
) as unknown as DaemonServiceClient;
|
||||
}
|
||||
|
||||
@@ -262,6 +341,46 @@ function callUnary<TResponse>(
|
||||
});
|
||||
}
|
||||
|
||||
function readFirstStreamMessage<TMessage>(
|
||||
stream: grpc.ClientReadableStream<TMessage>,
|
||||
timeoutMs: number,
|
||||
): Promise<TMessage> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let completed = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
reject(new Error(`gRPC stream timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
const onData = (message: TMessage) => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
clearTimeout(timeout);
|
||||
resolve(message);
|
||||
};
|
||||
|
||||
const onError = (error: Error) => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('gRPC stream ended before first message'));
|
||||
};
|
||||
|
||||
stream.on('data', onData);
|
||||
stream.on('error', onError);
|
||||
stream.on('end', onEnd);
|
||||
});
|
||||
}
|
||||
|
||||
function toBuffer(data: Uint8Array | Buffer): Buffer {
|
||||
if (Buffer.isBuffer(data)) return data;
|
||||
return Buffer.from(data);
|
||||
@@ -270,6 +389,49 @@ function toBuffer(data: Uint8Array | Buffer): Buffer {
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS = 8_000;
|
||||
const DEFAULT_RPC_TIMEOUT_MS = 20_000;
|
||||
|
||||
export async function daemonGetNodeStatus(
|
||||
node: DaemonNodeConnection,
|
||||
): Promise<DaemonNodeStatus> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const response = await callUnary<DaemonNodeStatusRaw>(
|
||||
(callback) => client.getNodeStatus({}, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
return {
|
||||
version: response.version,
|
||||
isHealthy: response.is_healthy,
|
||||
uptimeSeconds: Number(response.uptime_seconds),
|
||||
activeServers: Number(response.active_servers),
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonGetNodeStats(
|
||||
node: DaemonNodeConnection,
|
||||
): Promise<DaemonNodeStats> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const stream = client.streamNodeStats({}, getMetadata(node.daemonToken));
|
||||
const response = await readFirstStreamMessage(stream, DEFAULT_RPC_TIMEOUT_MS);
|
||||
|
||||
return {
|
||||
cpuPercent: Number(response.cpu_percent),
|
||||
memoryUsed: Number(response.memory_used),
|
||||
memoryTotal: Number(response.memory_total),
|
||||
diskUsed: Number(response.disk_used),
|
||||
diskTotal: Number(response.disk_total),
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonCreateServer(
|
||||
node: DaemonNodeConnection,
|
||||
request: DaemonCreateServerRequest,
|
||||
@@ -468,6 +630,84 @@ export async function daemonDeleteFiles(
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonCreateBackup(
|
||||
node: DaemonNodeConnection,
|
||||
serverUuid: string,
|
||||
backupId: string,
|
||||
): Promise<DaemonBackupResponse> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const response = await callUnary<DaemonBackupResponseRaw>(
|
||||
(callback) =>
|
||||
client.createBackup(
|
||||
{ server_uuid: serverUuid, backup_id: backupId },
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
return {
|
||||
backupId: response.backup_id,
|
||||
sizeBytes: Number(response.size_bytes),
|
||||
checksum: response.checksum,
|
||||
success: response.success,
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonRestoreBackup(
|
||||
node: DaemonNodeConnection,
|
||||
serverUuid: string,
|
||||
backupId: string,
|
||||
cdnPath?: string | null,
|
||||
): Promise<void> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) =>
|
||||
client.restoreBackup(
|
||||
{
|
||||
server_uuid: serverUuid,
|
||||
backup_id: backupId,
|
||||
cdn_download_url: cdnPath ?? '',
|
||||
},
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonDeleteBackup(
|
||||
node: DaemonNodeConnection,
|
||||
serverUuid: string,
|
||||
backupId: string,
|
||||
): Promise<void> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) =>
|
||||
client.deleteBackup(
|
||||
{ server_uuid: serverUuid, backup_id: backupId },
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonGetActivePlayers(
|
||||
node: DaemonNodeConnection,
|
||||
serverUuid: string,
|
||||
|
||||
Reference in New Issue
Block a user