735 lines
18 KiB
TypeScript
735 lines
18 KiB
TypeScript
import grpc from '@grpc/grpc-js';
|
|
import protoLoader from '@grpc/proto-loader';
|
|
import type { PowerAction } from '@source/shared';
|
|
import { PROTO_PATH } from '@source/proto';
|
|
|
|
export interface DaemonNodeConnection {
|
|
fqdn: string;
|
|
grpcPort: number;
|
|
daemonToken: string;
|
|
}
|
|
|
|
export interface DaemonPortMapping {
|
|
host_port: number;
|
|
container_port: number;
|
|
protocol: 'tcp' | 'udp';
|
|
}
|
|
|
|
export interface DaemonCreateServerRequest {
|
|
uuid: string;
|
|
docker_image: string;
|
|
memory_limit: number;
|
|
disk_limit: number;
|
|
cpu_limit: number;
|
|
startup_command: string;
|
|
environment: Record<string, string>;
|
|
ports: DaemonPortMapping[];
|
|
install_plugin_urls: string[];
|
|
}
|
|
|
|
interface DaemonServerResponse {
|
|
uuid: string;
|
|
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;
|
|
}
|
|
|
|
interface EmptyResponse {
|
|
[key: string]: never;
|
|
}
|
|
|
|
interface DaemonFileListResponseRaw {
|
|
files: {
|
|
name: string;
|
|
path: string;
|
|
is_directory: boolean;
|
|
size: number;
|
|
modified_at: number;
|
|
mime_type: string;
|
|
}[];
|
|
}
|
|
|
|
interface DaemonFileContentRaw {
|
|
data: Uint8Array | Buffer;
|
|
mime_type: string;
|
|
}
|
|
|
|
interface DaemonPlayerListRaw {
|
|
players: {
|
|
name: string;
|
|
uuid: string;
|
|
connected_at: number;
|
|
}[];
|
|
max_players: number;
|
|
}
|
|
|
|
interface DaemonBackupResponseRaw {
|
|
backup_id: string;
|
|
size_bytes: number;
|
|
checksum: string;
|
|
success: boolean;
|
|
}
|
|
|
|
export interface DaemonConsoleOutput {
|
|
uuid: string;
|
|
line: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface DaemonConsoleStreamHandle {
|
|
stream: grpc.ClientReadableStream<DaemonConsoleOutput>;
|
|
close: () => void;
|
|
}
|
|
|
|
export interface DaemonFileEntry {
|
|
name: string;
|
|
path: string;
|
|
isDirectory: boolean;
|
|
size: number;
|
|
modifiedAt: number;
|
|
mimeType: string;
|
|
}
|
|
|
|
export interface DaemonPlayersResponse {
|
|
players: Array<{
|
|
name: string;
|
|
id: string;
|
|
connectedAt: number;
|
|
}>;
|
|
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,
|
|
callback: UnaryCallback<DaemonServerResponse>,
|
|
): void;
|
|
deleteServer(
|
|
request: { uuid: string },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<EmptyResponse>,
|
|
): void;
|
|
setPowerState(
|
|
request: { uuid: string; action: number },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<EmptyResponse>,
|
|
): void;
|
|
getServerStatus(
|
|
request: { uuid: string },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<DaemonStatusResponse>,
|
|
): void;
|
|
streamConsole(
|
|
request: { uuid: string },
|
|
metadata: grpc.Metadata,
|
|
): grpc.ClientReadableStream<DaemonConsoleOutput>;
|
|
sendCommand(
|
|
request: { uuid: string; command: string },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<EmptyResponse>,
|
|
): void;
|
|
listFiles(
|
|
request: { uuid: string; path: string },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<DaemonFileListResponseRaw>,
|
|
): void;
|
|
readFile(
|
|
request: { uuid: string; path: string },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<DaemonFileContentRaw>,
|
|
): void;
|
|
writeFile(
|
|
request: { uuid: string; path: string; data: Uint8Array | Buffer },
|
|
metadata: grpc.Metadata,
|
|
callback: UnaryCallback<EmptyResponse>,
|
|
): void;
|
|
deleteFiles(
|
|
request: { uuid: string; paths: string[] },
|
|
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,
|
|
callback: UnaryCallback<DaemonPlayerListRaw>,
|
|
): void;
|
|
}
|
|
|
|
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
|
keepCase: true,
|
|
longs: Number,
|
|
enums: Number,
|
|
defaults: true,
|
|
oneofs: true,
|
|
});
|
|
|
|
const loaded = grpc.loadPackageDefinition(packageDefinition) as {
|
|
gamepanel?: {
|
|
daemon?: {
|
|
DaemonService?: grpc.ServiceClientConstructor;
|
|
};
|
|
};
|
|
};
|
|
|
|
const DaemonServiceCtor = loaded.gamepanel?.daemon?.DaemonService;
|
|
if (!DaemonServiceCtor) {
|
|
throw new Error('Failed to load DaemonService gRPC definition');
|
|
}
|
|
const DaemonService = DaemonServiceCtor;
|
|
|
|
const POWER_ACTIONS: Record<PowerAction, number> = {
|
|
start: 0,
|
|
stop: 1,
|
|
restart: 2,
|
|
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);
|
|
host = parsed.hostname || parsed.host;
|
|
if (!host) throw new Error('Node FQDN has no hostname');
|
|
} catch {
|
|
// Fall through to raw handling below.
|
|
}
|
|
}
|
|
|
|
const withoutPath = host.replace(/\/.*$/, '');
|
|
if (/^\[.+\](?::\d+)?$/.test(withoutPath)) {
|
|
const innerHost = withoutPath
|
|
.replace(/^\[/, '')
|
|
.replace(/\](?::\d+)?$/, '');
|
|
return `[${innerHost}]:${grpcPort}`;
|
|
}
|
|
if (/^[^:]+:\d+$/.test(withoutPath)) {
|
|
const hostOnly = withoutPath.replace(/:\d+$/, '');
|
|
return `${hostOnly}:${grpcPort}`;
|
|
}
|
|
if (withoutPath.includes(':')) return `[${withoutPath}]:${grpcPort}`;
|
|
return `${withoutPath}:${grpcPort}`;
|
|
}
|
|
|
|
function getMetadata(daemonToken: string): grpc.Metadata {
|
|
const metadata = new grpc.Metadata();
|
|
metadata.set('authorization', `Bearer ${daemonToken}`);
|
|
return metadata;
|
|
}
|
|
|
|
function createClient(node: DaemonNodeConnection): DaemonServiceClient {
|
|
const target = buildGrpcTarget(node.fqdn, node.grpcPort);
|
|
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;
|
|
}
|
|
|
|
function waitForReady(client: grpc.Client, timeoutMs: number): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
client.waitForReady(Date.now() + timeoutMs, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function callUnary<TResponse>(
|
|
invoke: (callback: UnaryCallback<TResponse>) => void,
|
|
timeoutMs: number,
|
|
): Promise<TResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
let completed = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (completed) return;
|
|
completed = true;
|
|
reject(new Error(`gRPC request timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
|
|
invoke((error, response) => {
|
|
if (completed) return;
|
|
completed = true;
|
|
clearTimeout(timeout);
|
|
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
resolve(response);
|
|
});
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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,
|
|
): Promise<DaemonServerResponse> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
return await callUnary<DaemonServerResponse>(
|
|
(callback) => client.createServer(request, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonDeleteServer(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
): Promise<void> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
await callUnary<EmptyResponse>(
|
|
(callback) => client.deleteServer({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonSetPowerState(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
action: PowerAction,
|
|
): Promise<void> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
await callUnary<EmptyResponse>(
|
|
(callback) => client.setPowerState({ uuid: serverUuid, action: POWER_ACTIONS[action] }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonGetServerStatus(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
): Promise<DaemonStatusResponse> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
return await callUnary<DaemonStatusResponse>(
|
|
(callback) => client.getServerStatus({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonOpenConsoleStream(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
): Promise<DaemonConsoleStreamHandle> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
const stream = client.streamConsole({ uuid: serverUuid }, getMetadata(node.daemonToken));
|
|
|
|
const close = () => {
|
|
try {
|
|
stream.cancel();
|
|
} catch {
|
|
// no-op
|
|
}
|
|
client.close();
|
|
};
|
|
|
|
stream.on('end', () => client.close());
|
|
stream.on('error', () => client.close());
|
|
|
|
return { stream, close };
|
|
} catch (error) {
|
|
client.close();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function daemonSendCommand(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
command: string,
|
|
): Promise<void> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
await callUnary<EmptyResponse>(
|
|
(callback) => client.sendCommand({ uuid: serverUuid, command }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonListFiles(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
path: string,
|
|
): Promise<DaemonFileEntry[]> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
const response = await callUnary<DaemonFileListResponseRaw>(
|
|
(callback) => client.listFiles({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
|
|
return response.files.map((file) => ({
|
|
name: file.name,
|
|
path: file.path,
|
|
isDirectory: file.is_directory,
|
|
size: Number(file.size),
|
|
modifiedAt: Number(file.modified_at),
|
|
mimeType: file.mime_type,
|
|
}));
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonReadFile(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
path: string,
|
|
): Promise<{ data: Buffer; mimeType: string }> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
const response = await callUnary<DaemonFileContentRaw>(
|
|
(callback) => client.readFile({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
|
|
return {
|
|
data: toBuffer(response.data),
|
|
mimeType: response.mime_type,
|
|
};
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonWriteFile(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
path: string,
|
|
data: string | Buffer,
|
|
): Promise<void> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
await callUnary<EmptyResponse>(
|
|
(callback) =>
|
|
client.writeFile(
|
|
{ uuid: serverUuid, path, data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data },
|
|
getMetadata(node.daemonToken),
|
|
callback,
|
|
),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
export async function daemonDeleteFiles(
|
|
node: DaemonNodeConnection,
|
|
serverUuid: string,
|
|
paths: string[],
|
|
): Promise<void> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
await callUnary<EmptyResponse>(
|
|
(callback) => client.deleteFiles({ uuid: serverUuid, paths }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
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,
|
|
): Promise<DaemonPlayersResponse> {
|
|
const client = createClient(node);
|
|
try {
|
|
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
const response = await callUnary<DaemonPlayerListRaw>(
|
|
(callback) => client.getActivePlayers({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
|
DEFAULT_RPC_TIMEOUT_MS,
|
|
);
|
|
|
|
return {
|
|
players: response.players.map((player) => ({
|
|
name: player.name,
|
|
id: player.uuid,
|
|
connectedAt: Number(player.connected_at),
|
|
})),
|
|
maxPlayers: Number(response.max_players),
|
|
};
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|