her sey
This commit is contained in:
+2
-1
@@ -23,7 +23,8 @@ Thumbs.db
|
||||
apps/daemon/target/
|
||||
|
||||
# Database
|
||||
packages/database/drizzle/
|
||||
packages/database/drizzle/*
|
||||
!packages/database/drizzle/0007_satisfactory_game.sql
|
||||
|
||||
# Common JS/TS
|
||||
coverage/
|
||||
|
||||
@@ -157,8 +157,10 @@ source-gamepanel/
|
||||
| Minecraft: Bedrock Edition | `itzg/minecraft-bedrock-server` | 19132 | `.properties` | — |
|
||||
| Terraria | `ryshe/terraria` | 7777 | keyvalue | — |
|
||||
| Rust | `didstopia/rust-server` | 28015 | — | — |
|
||||
| Satisfactory | `wolveix/satisfactory-server` | 7777 + 8888/tcp | — | — |
|
||||
| FiveM | `spritsail/fivem:stable` | 30120 | `server.cfg` (keyvalue-style) | — |
|
||||
|
||||
Adding new games requires only a database seed entry — no code changes needed.
|
||||
Many games can be added with a database seed entry alone. Some images still need small daemon-side tweaks for mount paths, port protocols, or config parsing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ function parseKeyValue(content: string): ConfigEntry[] {
|
||||
const entries: ConfigEntry[] = [];
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('//')) continue;
|
||||
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) continue;
|
||||
|
||||
// Match: key "value" or key value
|
||||
const match = trimmed.match(/^(\S+)\s+"([^"]*)"/) || trimmed.match(/^(\S+)\s+(.*)/);
|
||||
@@ -210,7 +210,7 @@ function serializeKeyValue(entries: ConfigEntry[], originalContent?: string): st
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('//')) {
|
||||
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
+61
-26
@@ -200,6 +200,11 @@ interface DaemonServiceClient extends grpc.Client {
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<DaemonManagedDatabaseCredentialsRaw>,
|
||||
): void;
|
||||
importDatabaseSql(
|
||||
request: { database_name: string; sql: string },
|
||||
metadata: grpc.Metadata,
|
||||
callback: UnaryCallback<EmptyResponse>,
|
||||
): void;
|
||||
updateDatabasePassword(
|
||||
request: { username: string; password: string },
|
||||
metadata: grpc.Metadata,
|
||||
@@ -319,9 +324,7 @@ function buildGrpcTarget(fqdn: string, grpcPort: number): string {
|
||||
|
||||
const withoutPath = host.replace(/\/.*$/, '');
|
||||
if (/^\[.+\](?::\d+)?$/.test(withoutPath)) {
|
||||
const innerHost = withoutPath
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\](?::\d+)?$/, '');
|
||||
const innerHost = withoutPath.replace(/^\[/, '').replace(/\](?::\d+)?$/, '');
|
||||
return `[${innerHost}]:${grpcPort}`;
|
||||
}
|
||||
if (/^[^:]+:\d+$/.test(withoutPath)) {
|
||||
@@ -340,14 +343,10 @@ function getMetadata(daemonToken: string): grpc.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;
|
||||
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> {
|
||||
@@ -444,9 +443,7 @@ interface DaemonRequestTimeoutOptions {
|
||||
rpcTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export async function daemonGetNodeStatus(
|
||||
node: DaemonNodeConnection,
|
||||
): Promise<DaemonNodeStatus> {
|
||||
export async function daemonGetNodeStatus(node: DaemonNodeConnection): Promise<DaemonNodeStatus> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
@@ -466,9 +463,7 @@ export async function daemonGetNodeStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonGetNodeStats(
|
||||
node: DaemonNodeConnection,
|
||||
): Promise<DaemonNodeStats> {
|
||||
export async function daemonGetNodeStats(node: DaemonNodeConnection): Promise<DaemonNodeStats> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
@@ -511,7 +506,8 @@ export async function daemonDeleteServer(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) => client.deleteServer({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.deleteServer({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
@@ -593,6 +589,30 @@ export async function daemonUpdateDatabasePassword(
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonImportDatabaseSql(
|
||||
node: DaemonNodeConnection,
|
||||
request: { databaseName: string; sql: string },
|
||||
): Promise<void> {
|
||||
const client = createClient(node);
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) =>
|
||||
client.importDatabaseSql(
|
||||
{
|
||||
database_name: request.databaseName,
|
||||
sql: request.sql,
|
||||
},
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function daemonDeleteDatabase(
|
||||
node: DaemonNodeConnection,
|
||||
request: { databaseName: string; username: string },
|
||||
@@ -626,7 +646,12 @@ export async function daemonSetPowerState(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) => client.setPowerState({ uuid: serverUuid, action: POWER_ACTIONS[action] }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.setPowerState(
|
||||
{ uuid: serverUuid, action: POWER_ACTIONS[action] },
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
POWER_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
@@ -643,7 +668,8 @@ export async function daemonGetServerStatus(
|
||||
try {
|
||||
await waitForReady(client, timeouts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
return await callUnary<DaemonStatusResponse>(
|
||||
(callback) => client.getServerStatus({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.getServerStatus({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
timeouts.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
@@ -688,7 +714,8 @@ export async function daemonSendCommand(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) => client.sendCommand({ uuid: serverUuid, command }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.sendCommand({ uuid: serverUuid, command }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
@@ -705,7 +732,8 @@ export async function daemonListFiles(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const response = await callUnary<DaemonFileListResponseRaw>(
|
||||
(callback) => client.listFiles({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.listFiles({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
@@ -731,7 +759,8 @@ export async function daemonReadFile(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const response = await callUnary<DaemonFileContentRaw>(
|
||||
(callback) => client.readFile({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.readFile({ uuid: serverUuid, path }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
@@ -756,7 +785,11 @@ export async function daemonWriteFile(
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) =>
|
||||
client.writeFile(
|
||||
{ uuid: serverUuid, path, data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data },
|
||||
{
|
||||
uuid: serverUuid,
|
||||
path,
|
||||
data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data,
|
||||
},
|
||||
getMetadata(node.daemonToken),
|
||||
callback,
|
||||
),
|
||||
@@ -776,7 +809,8 @@ export async function daemonDeleteFiles(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
await callUnary<EmptyResponse>(
|
||||
(callback) => client.deleteFiles({ uuid: serverUuid, paths }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.deleteFiles({ uuid: serverUuid, paths }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
} finally {
|
||||
@@ -870,7 +904,8 @@ export async function daemonGetActivePlayers(
|
||||
try {
|
||||
await waitForReady(client, DEFAULT_CONNECT_TIMEOUT_MS);
|
||||
const response = await callUnary<DaemonPlayerListRaw>(
|
||||
(callback) => client.getActivePlayers({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
(callback) =>
|
||||
client.getActivePlayers({ uuid: serverUuid }, getMetadata(node.daemonToken), callback),
|
||||
DEFAULT_RPC_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { gunzipSync } from 'node:zlib';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { and, asc, eq } from 'drizzle-orm';
|
||||
import * as tar from 'tar-stream';
|
||||
import type { Headers } from 'tar-stream';
|
||||
import * as unzipper from 'unzipper';
|
||||
import { serverDatabases, servers } from '@source/database';
|
||||
import {
|
||||
daemonCreateDatabase,
|
||||
daemonDeleteDatabase,
|
||||
daemonDeleteFiles,
|
||||
daemonImportDatabaseSql,
|
||||
daemonReadFile,
|
||||
daemonWriteFile,
|
||||
type DaemonNodeConnection,
|
||||
} from './daemon.js';
|
||||
|
||||
const GITHUB_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
|
||||
const URL_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
|
||||
const QBCORE_DATABASE_NAME = 'qbcore';
|
||||
const FIVE_M_QBCORE_MARKER_PATH = '/.gamepanel/fivem-qbcore.json';
|
||||
const FIVE_M_INTERNAL_PORT = 30120;
|
||||
const QBCORE_SQL_URL =
|
||||
'https://raw.githubusercontent.com/qbcore-framework/txAdminRecipe/main/qbcore.sql';
|
||||
const OXMYSQL_ZIP_URL =
|
||||
'https://github.com/overextended/oxmysql/releases/download/v2.12.0/oxmysql.zip';
|
||||
const MENUV_ZIP_URL = 'https://github.com/ThymonA/menuv/releases/download/v1.4.1/menuv_v1.4.1.zip';
|
||||
|
||||
interface ExtractedFile {
|
||||
path: string;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
interface ManagedServerDatabaseRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
databaseName: string;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
phpMyAdminUrl: string | null;
|
||||
}
|
||||
|
||||
interface FivemProvisionContext {
|
||||
node: DaemonNodeConnection;
|
||||
serverDescription?: string | null;
|
||||
serverId: string;
|
||||
serverName: string;
|
||||
serverUuid: string;
|
||||
}
|
||||
|
||||
interface GitHubArchiveResource {
|
||||
destination: string;
|
||||
owner: string;
|
||||
ref: string;
|
||||
repo: string;
|
||||
subpath?: string;
|
||||
}
|
||||
|
||||
interface RemoteArchiveResource {
|
||||
collapseTopLevelDirectory?: boolean;
|
||||
destination: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const FIVEM_GITHUB_RESOURCES: GitHubArchiveResource[] = [
|
||||
{
|
||||
owner: 'citizenfx',
|
||||
repo: 'cfx-server-data',
|
||||
ref: 'master',
|
||||
destination: '/resources/[cfx-default]',
|
||||
subpath: 'resources',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'bob74_ipl',
|
||||
ref: 'master',
|
||||
destination: '/resources/[standalone]/bob74_ipl',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'safecracker',
|
||||
ref: 'main',
|
||||
destination: '/resources/[standalone]/safecracker',
|
||||
},
|
||||
{
|
||||
owner: 'citizenfx',
|
||||
repo: 'screenshot-basic',
|
||||
ref: 'master',
|
||||
destination: '/resources/[standalone]/screenshot-basic',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'progressbar',
|
||||
ref: 'main',
|
||||
destination: '/resources/[standalone]/progressbar',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'interact-sound',
|
||||
ref: 'master',
|
||||
destination: '/resources/[standalone]/interact-sound',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'connectqueue',
|
||||
ref: 'master',
|
||||
destination: '/resources/[standalone]/connectqueue',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'PolyZone',
|
||||
ref: 'master',
|
||||
destination: '/resources/[standalone]/PolyZone',
|
||||
},
|
||||
{
|
||||
owner: 'AvarianKnight',
|
||||
repo: 'pma-voice',
|
||||
ref: 'main',
|
||||
destination: '/resources/[voice]/pma-voice',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'qb-radio',
|
||||
ref: 'main',
|
||||
destination: '/resources/[voice]/qb-radio',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'hospital_map',
|
||||
ref: 'main',
|
||||
destination: '/resources/[defaultmaps]/hospital_map',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'dealer_map',
|
||||
ref: 'main',
|
||||
destination: '/resources/[defaultmaps]/dealer_map',
|
||||
},
|
||||
{
|
||||
owner: 'qbcore-framework',
|
||||
repo: 'prison_map',
|
||||
ref: 'main',
|
||||
destination: '/resources/[defaultmaps]/prison_map',
|
||||
},
|
||||
...[
|
||||
'qb-core',
|
||||
'qb-scoreboard',
|
||||
'qb-adminmenu',
|
||||
'qb-multicharacter',
|
||||
'qb-target',
|
||||
'qb-vehiclesales',
|
||||
'qb-vehicleshop',
|
||||
'qb-houserobbery',
|
||||
'qb-prison',
|
||||
'qb-hud',
|
||||
'qb-management',
|
||||
'qb-weed',
|
||||
'qb-lapraces',
|
||||
'qb-inventory',
|
||||
'qb-houses',
|
||||
'qb-garages',
|
||||
'qb-ambulancejob',
|
||||
'qb-radialmenu',
|
||||
'qb-crypto',
|
||||
'qb-weathersync',
|
||||
'qb-policejob',
|
||||
'qb-apartments',
|
||||
'qb-vehiclekeys',
|
||||
'qb-mechanicjob',
|
||||
'qb-phone',
|
||||
'qb-vineyard',
|
||||
'qb-weapons',
|
||||
'qb-scrapyard',
|
||||
'qb-towjob',
|
||||
'qb-streetraces',
|
||||
'qb-storerobbery',
|
||||
'qb-spawn',
|
||||
'qb-smallresources',
|
||||
'qb-recyclejob',
|
||||
'qb-crafting',
|
||||
'qb-diving',
|
||||
'qb-cityhall',
|
||||
'qb-truckrobbery',
|
||||
'qb-pawnshop',
|
||||
'qb-minigames',
|
||||
'qb-taxijob',
|
||||
'qb-busjob',
|
||||
'qb-newsjob',
|
||||
'qb-fuel',
|
||||
'qb-jewelery',
|
||||
'qb-bankrobbery',
|
||||
'qb-banking',
|
||||
'qb-clothing',
|
||||
'qb-hotdogjob',
|
||||
'qb-doorlock',
|
||||
'qb-garbagejob',
|
||||
'qb-drugs',
|
||||
'qb-shops',
|
||||
'qb-interior',
|
||||
'qb-menu',
|
||||
'qb-input',
|
||||
'qb-loading',
|
||||
].map((repo) => ({
|
||||
owner: 'qbcore-framework',
|
||||
repo,
|
||||
ref: 'main',
|
||||
destination: `/resources/[qb]/${repo}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const FIVEM_REMOTE_ARCHIVES: RemoteArchiveResource[] = [
|
||||
{
|
||||
url: OXMYSQL_ZIP_URL,
|
||||
destination: '/resources/[standalone]/oxmysql',
|
||||
collapseTopLevelDirectory: true,
|
||||
},
|
||||
{
|
||||
url: MENUV_ZIP_URL,
|
||||
destination: '/resources/[standalone]/menuv',
|
||||
collapseTopLevelDirectory: true,
|
||||
},
|
||||
];
|
||||
|
||||
function normalizePathSegments(path: string): string[] {
|
||||
return path
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.filter((segment) => segment && segment !== '.' && segment !== '..');
|
||||
}
|
||||
|
||||
function normalizeArchivePath(path: string): string | null {
|
||||
const segments = normalizePathSegments(path);
|
||||
if (segments.length === 0) return null;
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
function joinServerPath(base: string, relative: string): string {
|
||||
const baseSegments = normalizePathSegments(base);
|
||||
const relativeSegments = normalizePathSegments(relative);
|
||||
return `/${[...baseSegments, ...relativeSegments].join('/')}`.replace(/\/{2,}/g, '/');
|
||||
}
|
||||
|
||||
function stripSharedTopLevelDirectory(files: ExtractedFile[]): ExtractedFile[] {
|
||||
if (files.length === 0) return files;
|
||||
|
||||
const firstSegments = new Set<string>();
|
||||
for (const file of files) {
|
||||
const [first] = normalizePathSegments(file.path);
|
||||
if (!first) return files;
|
||||
firstSegments.add(first);
|
||||
if (firstSegments.size > 1) {
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
.map((file) => {
|
||||
const segments = normalizePathSegments(file.path).slice(1);
|
||||
if (segments.length === 0) return null;
|
||||
return {
|
||||
path: segments.join('/'),
|
||||
data: file.data,
|
||||
};
|
||||
})
|
||||
.filter((file): file is ExtractedFile => file !== null);
|
||||
}
|
||||
|
||||
function filterFilesBySubpath(files: ExtractedFile[], subpath: string): ExtractedFile[] {
|
||||
const prefix = normalizePathSegments(subpath).join('/');
|
||||
if (!prefix) return files;
|
||||
|
||||
const normalizedPrefix = `${prefix}/`;
|
||||
return files
|
||||
.map((file) => {
|
||||
if (file.path === prefix) return null;
|
||||
if (!file.path.startsWith(normalizedPrefix)) return null;
|
||||
return {
|
||||
path: file.path.slice(normalizedPrefix.length),
|
||||
data: file.data,
|
||||
};
|
||||
})
|
||||
.filter((file): file is ExtractedFile => file !== null && file.path.length > 0);
|
||||
}
|
||||
|
||||
async function downloadBinary(
|
||||
url: string,
|
||||
maxBytes: number,
|
||||
headers: Record<string, string> = {},
|
||||
): Promise<Buffer> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'SourceGamePanel/1.0',
|
||||
...headers,
|
||||
},
|
||||
redirect: 'follow',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed (${response.status}): ${url}`);
|
||||
}
|
||||
|
||||
const contentLength = Number(response.headers.get('content-length') ?? '0');
|
||||
if (contentLength > maxBytes) {
|
||||
throw new Error(`Download exceeds size limit (${contentLength} > ${maxBytes})`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
if (buffer.length === 0) {
|
||||
throw new Error(`Downloaded archive is empty: ${url}`);
|
||||
}
|
||||
if (buffer.length > maxBytes) {
|
||||
throw new Error(`Download exceeds size limit (${buffer.length} > ${maxBytes})`);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function downloadText(url: string, headers: Record<string, string> = {}): Promise<string> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'SourceGamePanel/1.0',
|
||||
...headers,
|
||||
},
|
||||
redirect: 'follow',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Text download failed (${response.status}): ${url}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text.trim()) {
|
||||
throw new Error(`Downloaded text is empty: ${url}`);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function extractZipFiles(buffer: Buffer): Promise<ExtractedFile[]> {
|
||||
const archive = await unzipper.Open.buffer(buffer);
|
||||
const files: ExtractedFile[] = [];
|
||||
|
||||
for (const entry of archive.files) {
|
||||
if (entry.type !== 'File') continue;
|
||||
const normalized = normalizeArchivePath(entry.path);
|
||||
if (!normalized) continue;
|
||||
|
||||
files.push({
|
||||
path: normalized,
|
||||
data: await entry.buffer(),
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function extractTarFiles(buffer: Buffer): Promise<ExtractedFile[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extract = tar.extract();
|
||||
const files: ExtractedFile[] = [];
|
||||
|
||||
extract.on('entry', (header: Headers, stream, next) => {
|
||||
const type = header.type ?? 'file';
|
||||
const normalized = normalizeArchivePath(header.name);
|
||||
const isFileType = type === 'file' || type === 'contiguous-file';
|
||||
|
||||
if (!isFileType || !normalized) {
|
||||
stream.resume();
|
||||
stream.on('end', next);
|
||||
stream.on('error', reject);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
stream.on('end', () => {
|
||||
files.push({ path: normalized, data: Buffer.concat(chunks) });
|
||||
next();
|
||||
});
|
||||
stream.on('error', reject);
|
||||
});
|
||||
|
||||
extract.on('finish', () => resolve(files));
|
||||
extract.on('error', reject);
|
||||
extract.end(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
async function extractArchive(buffer: Buffer, url: string): Promise<ExtractedFile[]> {
|
||||
const normalizedUrl = url.toLowerCase();
|
||||
if (normalizedUrl.endsWith('.zip')) {
|
||||
return extractZipFiles(buffer);
|
||||
}
|
||||
if (normalizedUrl.endsWith('.tar.gz') || normalizedUrl.endsWith('.tgz')) {
|
||||
return extractTarFiles(gunzipSync(buffer));
|
||||
}
|
||||
if (normalizedUrl.endsWith('.tar')) {
|
||||
return extractTarFiles(buffer);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported archive type: ${url}`);
|
||||
}
|
||||
|
||||
async function writeFilesToServer(
|
||||
node: DaemonNodeConnection,
|
||||
serverUuid: string,
|
||||
destination: string,
|
||||
files: ExtractedFile[],
|
||||
): Promise<void> {
|
||||
for (const file of files) {
|
||||
await daemonWriteFile(node, serverUuid, joinServerPath(destination, file.path), file.data);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeCfgValue(value: string): string {
|
||||
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function buildMysqlConnectionString(database: ManagedServerDatabaseRecord): string {
|
||||
return `mysql://${encodeURIComponent(database.username)}:${encodeURIComponent(database.password)}@${database.host}:${database.port}/${encodeURIComponent(database.databaseName)}?charset=utf8mb4`;
|
||||
}
|
||||
|
||||
function renderFivemServerConfig(
|
||||
serverName: string,
|
||||
description: string | null | undefined,
|
||||
database: ManagedServerDatabaseRecord,
|
||||
): string {
|
||||
const safeServerName = escapeCfgValue(serverName.trim() || 'QBCore Server');
|
||||
const safeProjectDescription = escapeCfgValue(
|
||||
description?.trim() || 'QBCore server provisioned by Source GamePanel.',
|
||||
);
|
||||
const rconPassword = randomBytes(16).toString('hex');
|
||||
const mysqlConnectionString = escapeCfgValue(buildMysqlConnectionString(database));
|
||||
|
||||
return `# Generated by Source GamePanel
|
||||
# QBCore resources and base dependencies are installed automatically.
|
||||
|
||||
endpoint_add_tcp "0.0.0.0:${FIVE_M_INTERNAL_PORT}"
|
||||
endpoint_add_udp "0.0.0.0:${FIVE_M_INTERNAL_PORT}"
|
||||
|
||||
sv_maxclients "32"
|
||||
sv_hostname "${safeServerName}"
|
||||
sets sv_projectName "[QBCore] ${safeServerName}"
|
||||
sets sv_projectDesc "${safeProjectDescription}"
|
||||
sets locale "en-US"
|
||||
sets tags "qbcore, qb-core, roleplay, source-gamepanel"
|
||||
set steam_webApiKey "none"
|
||||
set resources_useSystemChat "true"
|
||||
set mysql_connection_string "${mysqlConnectionString}"
|
||||
|
||||
setr qb_locale "en"
|
||||
setr UseTarget "false"
|
||||
setr voice_useNativeAudio "true"
|
||||
setr voice_useSendingRangeOnly "true"
|
||||
setr voice_defaultCycle "GRAVE"
|
||||
setr voice_defaultVolume "0.3"
|
||||
setr voice_enableRadioAnim "1"
|
||||
setr voice_syncData "1"
|
||||
|
||||
sv_scriptHookAllowed "0"
|
||||
sv_endpointprivacy "true"
|
||||
rcon_password "${rconPassword}"
|
||||
|
||||
ensure mapmanager
|
||||
ensure chat
|
||||
ensure spawnmanager
|
||||
ensure sessionmanager
|
||||
ensure basic-gamemode
|
||||
ensure hardcap
|
||||
ensure baseevents
|
||||
|
||||
ensure qb-core
|
||||
ensure [qb]
|
||||
ensure [standalone]
|
||||
ensure [voice]
|
||||
ensure [defaultmaps]
|
||||
|
||||
add_ace group.admin command allow
|
||||
add_ace group.admin command.quit deny
|
||||
add_ace resource.qb-core command allow
|
||||
add_ace qbcore.god command allow
|
||||
add_principal qbcore.god group.admin
|
||||
add_principal qbcore.god qbcore.admin
|
||||
add_principal qbcore.admin qbcore.mod
|
||||
`;
|
||||
}
|
||||
|
||||
function isMissingFileError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
message.includes('No such file or directory') ||
|
||||
message.includes('Server responded with NOT_FOUND') ||
|
||||
message.includes('status code 404')
|
||||
);
|
||||
}
|
||||
|
||||
export function isFivemQbCoreGame(gameSlug: string): boolean {
|
||||
return gameSlug.trim().toLowerCase() === 'fivem';
|
||||
}
|
||||
|
||||
export async function ensureFivemQbCoreDatabase(
|
||||
app: FastifyInstance,
|
||||
context: Pick<FivemProvisionContext, 'node' | 'serverId' | 'serverUuid'>,
|
||||
): Promise<ManagedServerDatabaseRecord> {
|
||||
const existing = await app.db
|
||||
.select({
|
||||
id: serverDatabases.id,
|
||||
name: serverDatabases.name,
|
||||
databaseName: serverDatabases.databaseName,
|
||||
username: serverDatabases.username,
|
||||
password: serverDatabases.password,
|
||||
host: serverDatabases.host,
|
||||
port: serverDatabases.port,
|
||||
phpMyAdminUrl: serverDatabases.phpMyAdminUrl,
|
||||
})
|
||||
.from(serverDatabases)
|
||||
.where(eq(serverDatabases.serverId, context.serverId))
|
||||
.orderBy(asc(serverDatabases.createdAt));
|
||||
|
||||
const preferred =
|
||||
existing.find((database) => database.name.trim().toLowerCase() === QBCORE_DATABASE_NAME) ??
|
||||
existing[0];
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
const managedDatabase = await daemonCreateDatabase(context.node, {
|
||||
serverUuid: context.serverUuid,
|
||||
name: QBCORE_DATABASE_NAME,
|
||||
});
|
||||
|
||||
try {
|
||||
const [created] = await app.db
|
||||
.insert(serverDatabases)
|
||||
.values({
|
||||
serverId: context.serverId,
|
||||
name: QBCORE_DATABASE_NAME,
|
||||
databaseName: managedDatabase.databaseName,
|
||||
username: managedDatabase.username,
|
||||
password: managedDatabase.password,
|
||||
host: managedDatabase.host,
|
||||
port: managedDatabase.port,
|
||||
phpMyAdminUrl: managedDatabase.phpMyAdminUrl,
|
||||
})
|
||||
.returning({
|
||||
id: serverDatabases.id,
|
||||
name: serverDatabases.name,
|
||||
databaseName: serverDatabases.databaseName,
|
||||
username: serverDatabases.username,
|
||||
password: serverDatabases.password,
|
||||
host: serverDatabases.host,
|
||||
port: serverDatabases.port,
|
||||
phpMyAdminUrl: serverDatabases.phpMyAdminUrl,
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
throw new Error('Failed to persist managed database metadata');
|
||||
}
|
||||
|
||||
return created;
|
||||
} catch (error) {
|
||||
try {
|
||||
await daemonDeleteDatabase(context.node, {
|
||||
databaseName: managedDatabase.databaseName,
|
||||
username: managedDatabase.username,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
app.log.error(
|
||||
{
|
||||
cleanupError,
|
||||
databaseName: managedDatabase.databaseName,
|
||||
serverId: context.serverId,
|
||||
serverUuid: context.serverUuid,
|
||||
},
|
||||
'Failed to roll back managed MySQL database after metadata save failure',
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFivemQbCoreDatabase(
|
||||
app: FastifyInstance,
|
||||
context: Pick<FivemProvisionContext, 'node' | 'serverId'>,
|
||||
): Promise<void> {
|
||||
const [database] = await app.db
|
||||
.select({
|
||||
id: serverDatabases.id,
|
||||
databaseName: serverDatabases.databaseName,
|
||||
username: serverDatabases.username,
|
||||
})
|
||||
.from(serverDatabases)
|
||||
.where(
|
||||
and(
|
||||
eq(serverDatabases.serverId, context.serverId),
|
||||
eq(serverDatabases.name, QBCORE_DATABASE_NAME),
|
||||
),
|
||||
);
|
||||
|
||||
if (!database) return;
|
||||
|
||||
await daemonDeleteDatabase(context.node, {
|
||||
databaseName: database.databaseName,
|
||||
username: database.username,
|
||||
});
|
||||
await app.db.delete(serverDatabases).where(eq(serverDatabases.id, database.id));
|
||||
}
|
||||
|
||||
async function installGitHubResource(
|
||||
app: FastifyInstance,
|
||||
context: FivemProvisionContext,
|
||||
resource: GitHubArchiveResource,
|
||||
): Promise<void> {
|
||||
const archiveUrl = `https://codeload.github.com/${resource.owner}/${resource.repo}/tar.gz/refs/heads/${encodeURIComponent(resource.ref)}`;
|
||||
const archive = await downloadBinary(archiveUrl, GITHUB_ARCHIVE_MAX_BYTES);
|
||||
let files = await extractTarFiles(gunzipSync(archive));
|
||||
files = stripSharedTopLevelDirectory(files);
|
||||
if (resource.subpath) {
|
||||
files = filterFilesBySubpath(files, resource.subpath);
|
||||
}
|
||||
if (files.length === 0) {
|
||||
throw new Error(
|
||||
`GitHub archive had no files: ${resource.owner}/${resource.repo}@${resource.ref}`,
|
||||
);
|
||||
}
|
||||
|
||||
await writeFilesToServer(context.node, context.serverUuid, resource.destination, files);
|
||||
|
||||
app.log.info(
|
||||
{
|
||||
destination: resource.destination,
|
||||
filesWritten: files.length,
|
||||
repo: `${resource.owner}/${resource.repo}`,
|
||||
serverId: context.serverId,
|
||||
serverUuid: context.serverUuid,
|
||||
},
|
||||
'Installed FiveM GitHub resource',
|
||||
);
|
||||
}
|
||||
|
||||
async function installRemoteArchive(
|
||||
app: FastifyInstance,
|
||||
context: FivemProvisionContext,
|
||||
resource: RemoteArchiveResource,
|
||||
): Promise<void> {
|
||||
const archive = await downloadBinary(resource.url, URL_ARCHIVE_MAX_BYTES);
|
||||
let files = await extractArchive(archive, resource.url);
|
||||
if (resource.collapseTopLevelDirectory) {
|
||||
files = stripSharedTopLevelDirectory(files);
|
||||
}
|
||||
if (files.length === 0) {
|
||||
throw new Error(`Remote archive had no files: ${resource.url}`);
|
||||
}
|
||||
|
||||
await writeFilesToServer(context.node, context.serverUuid, resource.destination, files);
|
||||
|
||||
app.log.info(
|
||||
{
|
||||
destination: resource.destination,
|
||||
filesWritten: files.length,
|
||||
serverId: context.serverId,
|
||||
serverUuid: context.serverUuid,
|
||||
url: resource.url,
|
||||
},
|
||||
'Installed FiveM remote archive',
|
||||
);
|
||||
}
|
||||
|
||||
export async function provisionFivemQbCoreServer(
|
||||
app: FastifyInstance,
|
||||
context: FivemProvisionContext,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await daemonReadFile(context.node, context.serverUuid, FIVE_M_QBCORE_MARKER_PATH);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isMissingFileError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const database = await ensureFivemQbCoreDatabase(app, context);
|
||||
const qbCoreSql = await downloadText(QBCORE_SQL_URL);
|
||||
|
||||
await daemonImportDatabaseSql(context.node, {
|
||||
databaseName: database.databaseName,
|
||||
sql: qbCoreSql,
|
||||
});
|
||||
|
||||
for (const resource of FIVEM_GITHUB_RESOURCES) {
|
||||
await installGitHubResource(app, context, resource);
|
||||
}
|
||||
|
||||
for (const resource of FIVEM_REMOTE_ARCHIVES) {
|
||||
await installRemoteArchive(app, context, resource);
|
||||
}
|
||||
|
||||
try {
|
||||
await daemonDeleteFiles(context.node, context.serverUuid, [
|
||||
'/resources/[cfx-default]/[gameplay]/chat',
|
||||
]);
|
||||
} catch (error) {
|
||||
if (!isMissingFileError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await daemonWriteFile(
|
||||
context.node,
|
||||
context.serverUuid,
|
||||
'/server.cfg',
|
||||
renderFivemServerConfig(context.serverName, context.serverDescription, database),
|
||||
);
|
||||
|
||||
await daemonWriteFile(
|
||||
context.node,
|
||||
context.serverUuid,
|
||||
FIVE_M_QBCORE_MARKER_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
installedAt: new Date().toISOString(),
|
||||
manifestVersion: 1,
|
||||
resourceCount: FIVEM_GITHUB_RESOURCES.length + FIVEM_REMOTE_ARCHIVES.length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
await app.db
|
||||
.update(servers)
|
||||
.set({ updatedAt: new Date() })
|
||||
.where(eq(servers.id, context.serverId));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ export const CreateServerSchema = {
|
||||
diskLimit: Type.Number({ minimum: 256 * 1024 * 1024 }), // min 256MB
|
||||
cpuLimit: Type.Optional(Type.Number({ minimum: 10, maximum: 10000, default: 100 })),
|
||||
allocationId: Type.String({ format: 'uuid' }),
|
||||
additionalAllocationIds: Type.Optional(Type.Array(Type.String({ format: 'uuid' }))),
|
||||
environment: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
startupOverride: Type.Optional(Type.String()),
|
||||
}),
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use bollard::container::{
|
||||
AttachContainerOptions, Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions,
|
||||
StopContainerOptions, StatsOptions, Stats,
|
||||
AttachContainerOptions, Config, CreateContainerOptions, ListContainersOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions,
|
||||
StopContainerOptions, StatsOptions, Stats, UploadToContainerOptions,
|
||||
};
|
||||
use bollard::image::CreateImageOptions;
|
||||
use bollard::models::{HostConfig, PortBinding};
|
||||
use bollard::models::{HostConfig, MountPointTypeEnum, PortBinding};
|
||||
use futures::StreamExt;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::docker::DockerManager;
|
||||
use crate::server::ServerSpec;
|
||||
use crate::server::state::ServerState;
|
||||
|
||||
/// Container name prefix for all managed game servers.
|
||||
const CONTAINER_PREFIX: &str = "gp_";
|
||||
const SATISFACTORY_RUN_SH: &str = include_str!("../game/satisfactory_run.sh");
|
||||
|
||||
pub fn container_name(server_uuid: &str) -> String {
|
||||
format!("{}{}", CONTAINER_PREFIX, server_uuid)
|
||||
}
|
||||
|
||||
fn uuid_from_container_name(name: &str) -> Option<String> {
|
||||
let trimmed = name.trim_start_matches('/');
|
||||
trimmed
|
||||
.strip_prefix(CONTAINER_PREFIX)
|
||||
.filter(|uuid| !uuid.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn container_data_path_for_image(image: &str) -> &'static str {
|
||||
let normalized = image.to_ascii_lowercase();
|
||||
if normalized.contains("cm2network/cs2") || normalized.contains("joedwards32/cs2") {
|
||||
@@ -29,9 +41,30 @@ fn container_data_path_for_image(image: &str) -> &'static str {
|
||||
if normalized.contains("cm2network/csgo") {
|
||||
return "/home/steam/csgo-dedicated";
|
||||
}
|
||||
if normalized.contains("spritsail/fivem") {
|
||||
return "/config";
|
||||
}
|
||||
if normalized.contains("wolveix/satisfactory-server") {
|
||||
return "/config";
|
||||
}
|
||||
"/data"
|
||||
}
|
||||
|
||||
fn is_wolveix_satisfactory_image(image: &str) -> bool {
|
||||
image
|
||||
.to_ascii_lowercase()
|
||||
.contains("wolveix/satisfactory-server")
|
||||
}
|
||||
|
||||
fn server_state_from_container_status(status: &str) -> ServerState {
|
||||
match status {
|
||||
"running" | "restarting" | "paused" => ServerState::Running,
|
||||
"created" | "exited" => ServerState::Stopped,
|
||||
"dead" => ServerState::Error,
|
||||
_ => ServerState::Error,
|
||||
}
|
||||
}
|
||||
|
||||
impl DockerManager {
|
||||
async fn attach_command_stream(
|
||||
&self,
|
||||
@@ -136,6 +169,33 @@ impl DockerManager {
|
||||
Err(anyhow::anyhow!("exec command timeout"))
|
||||
}
|
||||
|
||||
async fn patch_satisfactory_run_script(&self, container_name: &str) -> Result<()> {
|
||||
let mut archive = Vec::new();
|
||||
{
|
||||
let mut builder = tar::Builder::new(&mut archive);
|
||||
let bytes = SATISFACTORY_RUN_SH.as_bytes();
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o755);
|
||||
header.set_cksum();
|
||||
builder.append_data(&mut header, "run.sh", Cursor::new(bytes))?;
|
||||
builder.finish()?;
|
||||
}
|
||||
|
||||
self.client()
|
||||
.upload_to_container(
|
||||
container_name,
|
||||
Some(UploadToContainerOptions {
|
||||
path: "/home/steam",
|
||||
no_overwrite_dir_non_dir: "false",
|
||||
}),
|
||||
archive.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn rcon_command(&self, server_uuid: &str, command: &str) -> Result<String> {
|
||||
let name = container_name(server_uuid);
|
||||
self.run_exec(&name, vec!["rcon-cli".to_string(), command.to_string()])
|
||||
@@ -248,6 +308,10 @@ impl DockerManager {
|
||||
let options = CreateContainerOptions { name: name.as_str(), platform: None };
|
||||
let response = self.client().create_container(Some(options), config).await?;
|
||||
|
||||
if is_wolveix_satisfactory_image(&spec.docker_image) {
|
||||
self.patch_satisfactory_run_script(&name).await?;
|
||||
}
|
||||
|
||||
info!(container_id = %response.id, uuid = %spec.uuid, "Container created");
|
||||
Ok(response.id)
|
||||
}
|
||||
@@ -380,6 +444,162 @@ impl DockerManager {
|
||||
Ok((image, env_map))
|
||||
}
|
||||
|
||||
/// Discover existing managed containers and rebuild in-memory server specs.
|
||||
pub async fn recover_managed_server_specs(&self, data_root: &Path) -> Result<Vec<ServerSpec>> {
|
||||
let containers = self
|
||||
.client()
|
||||
.list_containers(Some(ListContainersOptions::<String> {
|
||||
all: true,
|
||||
..Default::default()
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let mut recovered = Vec::new();
|
||||
|
||||
for container in containers {
|
||||
let uuid = container
|
||||
.names
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find_map(|name| uuid_from_container_name(name));
|
||||
|
||||
let Some(uuid) = uuid else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let info = self.client().inspect_container(&container_name(&uuid), None).await?;
|
||||
|
||||
let image = info
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.image.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let data_mount_path = info
|
||||
.mounts
|
||||
.as_ref()
|
||||
.and_then(|mounts| {
|
||||
mounts.iter().find_map(|mount| {
|
||||
if mount.typ != Some(MountPointTypeEnum::BIND) {
|
||||
return None;
|
||||
}
|
||||
mount.source.as_ref().map(PathBuf::from)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| data_root.join(&uuid));
|
||||
|
||||
let data_destination = info
|
||||
.mounts
|
||||
.as_ref()
|
||||
.and_then(|mounts| {
|
||||
mounts.iter().find_map(|mount| {
|
||||
if mount.typ != Some(MountPointTypeEnum::BIND) {
|
||||
return None;
|
||||
}
|
||||
mount.destination.clone()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| container_data_path_for_image(&image).to_string());
|
||||
|
||||
let startup_command = info
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|cfg| {
|
||||
let working_dir = cfg.working_dir.as_deref().unwrap_or_default();
|
||||
if working_dir != data_destination {
|
||||
return None;
|
||||
}
|
||||
cfg.cmd.as_ref().map(|cmd| cmd.join(" "))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut environment = HashMap::new();
|
||||
if let Some(env_vars) = info
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.env.clone())
|
||||
{
|
||||
for entry in env_vars {
|
||||
if let Some((key, value)) = entry.split_once('=') {
|
||||
environment.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ports = info
|
||||
.host_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.port_bindings.as_ref())
|
||||
.map(|bindings| {
|
||||
bindings
|
||||
.iter()
|
||||
.flat_map(|(container_port, host_bindings)| {
|
||||
let (container_port, protocol) = match container_port.split_once('/') {
|
||||
Some((port, protocol)) => (port, protocol),
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let Ok(container_port_num) = container_port.parse::<u16>() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
host_bindings
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|binding| {
|
||||
let host_port = binding.host_port.as_deref()?.parse::<u16>().ok()?;
|
||||
Some(crate::server::PortMap {
|
||||
host_port,
|
||||
container_port: container_port_num,
|
||||
protocol: protocol.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let memory_limit = info
|
||||
.host_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.memory)
|
||||
.unwrap_or_default();
|
||||
|
||||
let cpu_limit = info
|
||||
.host_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.nano_cpus)
|
||||
.map(|nano_cpus| (nano_cpus / 10_000_000) as i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let state = info
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|state| state.status.as_ref())
|
||||
.map(|status| server_state_from_container_status(&format!("{status:?}").to_lowercase()))
|
||||
.unwrap_or(ServerState::Error);
|
||||
|
||||
recovered.push(ServerSpec {
|
||||
uuid,
|
||||
docker_image: image,
|
||||
memory_limit,
|
||||
disk_limit: 0,
|
||||
cpu_limit,
|
||||
startup_command,
|
||||
environment,
|
||||
ports,
|
||||
data_path: data_mount_path,
|
||||
state,
|
||||
container_id: info.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(recovered)
|
||||
}
|
||||
|
||||
/// Stream container logs (stdout + stderr). Returns an owned stream.
|
||||
pub fn stream_logs(
|
||||
self: &Arc<Self>,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
NUMCHECK='^[0-9]+$'
|
||||
MSGWARNING="\033[0;33mWARNING:\033[0m"
|
||||
|
||||
if ! [[ "$SERVERGAMEPORT" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid server port given: %s\n" "$SERVERGAMEPORT"
|
||||
SERVERGAMEPORT="7777"
|
||||
fi
|
||||
printf "Setting server port to %s\n" "$SERVERGAMEPORT"
|
||||
|
||||
if ! [[ "$SERVERMESSAGINGPORT" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid messaging port given: %s\n" "$SERVERMESSAGINGPORT"
|
||||
SERVERMESSAGINGPORT="8888"
|
||||
fi
|
||||
printf "Setting messaging port to %s\n" "$SERVERMESSAGINGPORT"
|
||||
|
||||
if ! [[ "$AUTOSAVENUM" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid autosave number given: %s\n" "$AUTOSAVENUM"
|
||||
AUTOSAVENUM="5"
|
||||
fi
|
||||
printf "Setting autosave number to %s\n" "$AUTOSAVENUM"
|
||||
|
||||
if ! [[ "$MAXOBJECTS" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid max objects number given: %s\n" "$MAXOBJECTS"
|
||||
MAXOBJECTS="2162688"
|
||||
fi
|
||||
printf "Setting max objects to %s\n" "$MAXOBJECTS"
|
||||
|
||||
if ! [[ "$MAXTICKRATE" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid max tick rate number given: %s\n" "$MAXTICKRATE"
|
||||
MAXTICKRATE="30"
|
||||
fi
|
||||
printf "Setting max tick rate to %s\n" "$MAXTICKRATE"
|
||||
|
||||
[[ "${SERVERSTREAMING,,}" == "true" ]] && SERVERSTREAMING="1" || SERVERSTREAMING="0"
|
||||
printf "Setting server streaming to %s\n" "$SERVERSTREAMING"
|
||||
|
||||
if ! [[ "$TIMEOUT" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid timeout number given: %s\n" "$TIMEOUT"
|
||||
TIMEOUT="30"
|
||||
fi
|
||||
printf "Setting timeout to %s\n" "$TIMEOUT"
|
||||
|
||||
if ! [[ "$MAXPLAYERS" =~ $NUMCHECK ]]; then
|
||||
printf "Invalid max players given: %s\n" "$MAXPLAYERS"
|
||||
MAXPLAYERS="4"
|
||||
fi
|
||||
printf "Setting max players to %s\n" "$MAXPLAYERS"
|
||||
|
||||
if [[ "${DISABLESEASONALEVENTS,,}" == "true" ]]; then
|
||||
printf "Disabling seasonal events\n"
|
||||
DISABLESEASONALEVENTS="-DisableSeasonalEvents"
|
||||
else
|
||||
DISABLESEASONALEVENTS=""
|
||||
fi
|
||||
|
||||
if [[ "$MULTIHOME" != "" ]]; then
|
||||
if [[ "$MULTIHOME" == "::" ]]; then
|
||||
printf "Multihome will accept IPv4 and IPv6 connections\n"
|
||||
fi
|
||||
printf "Setting multihome to %s\n" "$MULTIHOME"
|
||||
MULTIHOME="-multihome=$MULTIHOME"
|
||||
fi
|
||||
|
||||
ini_args=(
|
||||
"-ini:Engine:[/Script/FactoryGame.FGSaveSession]:mNumRotatingAutosaves=$AUTOSAVENUM"
|
||||
"-ini:Engine:[/Script/Engine.GarbageCollectionSettings]:gc.MaxObjectsInEditor=$MAXOBJECTS"
|
||||
"-ini:Engine:[/Script/OnlineSubsystemUtils.IpNetDriver]:LanServerMaxTickRate=$MAXTICKRATE"
|
||||
"-ini:Engine:[/Script/OnlineSubsystemUtils.IpNetDriver]:NetServerMaxTickRate=$MAXTICKRATE"
|
||||
"-ini:Engine:[/Script/OnlineSubsystemUtils.IpNetDriver]:ConnectionTimeout=$TIMEOUT"
|
||||
"-ini:Engine:[/Script/OnlineSubsystemUtils.IpNetDriver]:InitialConnectTimeout=$TIMEOUT"
|
||||
"-ini:Engine:[ConsoleVariables]:wp.Runtime.EnableServerStreaming=$SERVERSTREAMING"
|
||||
"-ini:Game:[/Script/Engine.GameSession]:ConnectionTimeout=$TIMEOUT"
|
||||
"-ini:Game:[/Script/Engine.GameSession]:InitialConnectTimeout=$TIMEOUT"
|
||||
"-ini:Game:[/Script/Engine.GameSession]:MaxPlayers=$MAXPLAYERS"
|
||||
"-ini:GameUserSettings:[/Script/Engine.GameSession]:MaxPlayers=$MAXPLAYERS"
|
||||
"$DISABLESEASONALEVENTS"
|
||||
"$MULTIHOME"
|
||||
)
|
||||
|
||||
if [[ "${SKIPUPDATE,,}" != "false" ]] && [ ! -f "/config/gamefiles/FactoryServer.sh" ]; then
|
||||
printf "%s Skip update is set, but no game files exist. Updating anyway\n" "$MSGWARNING"
|
||||
SKIPUPDATE="false"
|
||||
fi
|
||||
|
||||
if [[ "${SKIPUPDATE,,}" != "true" ]]; then
|
||||
STEAMBETAPASSWORD=""
|
||||
|
||||
if [[ -n "${STEAMBETAID}" ]]; then
|
||||
printf "STEAMBETAID is set. Using beta ID: %s\n" "$STEAMBETAID"
|
||||
STEAMBETAFLAG="$STEAMBETAID"
|
||||
if [[ -n "${STEAMBETAKEY}" ]]; then
|
||||
STEAMBETAPASSWORD="-betapassword $STEAMBETAKEY"
|
||||
printf "Beta password provided\n"
|
||||
fi
|
||||
elif [[ "${STEAMBETA,,}" == "true" ]]; then
|
||||
printf "Experimental flag is set. Experimental will be downloaded instead of Early Access.\n"
|
||||
STEAMBETAFLAG="experimental"
|
||||
else
|
||||
STEAMBETAFLAG=""
|
||||
fi
|
||||
|
||||
STORAGEAVAILABLE=$(stat -f -c "%a*%S" .)
|
||||
STORAGEAVAILABLE=$((STORAGEAVAILABLE/1024/1024/1024))
|
||||
printf "Checking available storage: %sGB detected\n" "$STORAGEAVAILABLE"
|
||||
|
||||
if [[ "$STORAGEAVAILABLE" -lt 8 ]]; then
|
||||
printf "You have less than 8GB (%sGB detected) of available storage to download the game.\nIf this is a fresh install, it will probably fail.\n" "$STORAGEAVAILABLE"
|
||||
fi
|
||||
|
||||
printf "\nDownloading the latest version of the game...\n"
|
||||
if [ -f "/config/gamefiles/steamapps/appmanifest_1690800.acf" ]; then
|
||||
printf "\nRemoving the app manifest to force Steam to check for an update...\n"
|
||||
rm "/config/gamefiles/steamapps/appmanifest_1690800.acf" || true
|
||||
fi
|
||||
|
||||
if [[ -n "$STEAMBETAFLAG" ]]; then
|
||||
steamcmd +force_install_dir /config/gamefiles +login anonymous +app_update "$STEAMAPPID" -beta "$STEAMBETAFLAG" $STEAMBETAPASSWORD validate +quit
|
||||
else
|
||||
steamcmd +force_install_dir /config/gamefiles +login anonymous +app_update "$STEAMAPPID" validate +quit
|
||||
fi
|
||||
|
||||
cp -r /home/steam/.steam/steam/logs/* "/config/logs/steam" || printf "Failed to store Steam logs\n"
|
||||
else
|
||||
printf "Skipping update as flag is set\n"
|
||||
fi
|
||||
|
||||
printf "Launching game server\n\n"
|
||||
|
||||
cp -r "/config/saved/server/." "/config/backups/" 2>/dev/null || true
|
||||
cp -r "${GAMESAVESDIR}/server/." "/config/backups" 2>/dev/null || true
|
||||
rm -rf "$GAMESAVESDIR"
|
||||
ln -sf "/config/saved" "$GAMESAVESDIR"
|
||||
|
||||
if [ ! -f "/config/gamefiles/FactoryServer.sh" ]; then
|
||||
printf "FactoryServer launch script is missing.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd /config/gamefiles || exit 1
|
||||
|
||||
chmod +x FactoryServer.sh || true
|
||||
./FactoryServer.sh -Port="$SERVERGAMEPORT" -ReliablePort="$SERVERMESSAGINGPORT" -ExternalReliablePort="$SERVERMESSAGINGPORT" "${ini_args[@]}" "$@" &
|
||||
|
||||
sleep 2
|
||||
satisfactory_pid=$(ps --ppid ${!} o pid=)
|
||||
|
||||
shutdown() {
|
||||
printf "\nReceived SIGINT. Shutting down.\n"
|
||||
kill -INT $satisfactory_pid 2>/dev/null
|
||||
}
|
||||
trap shutdown SIGINT SIGTERM
|
||||
|
||||
wait
|
||||
@@ -303,6 +303,28 @@ impl DaemonService for DaemonServiceImpl {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn import_database_sql(
|
||||
&self,
|
||||
request: Request<ImportDatabaseSqlRequest>,
|
||||
) -> Result<Response<Empty>, Status> {
|
||||
self.check_auth(&request)?;
|
||||
let req = request.into_inner();
|
||||
|
||||
if req.database_name.trim().is_empty() {
|
||||
return Err(Status::invalid_argument("Database name is required"));
|
||||
}
|
||||
if req.sql.trim().is_empty() {
|
||||
return Err(Status::invalid_argument("SQL payload is required"));
|
||||
}
|
||||
|
||||
self.managed_mysql
|
||||
.import_sql(req.database_name.trim(), &req.sql)
|
||||
.await
|
||||
.map_err(Status::from)?;
|
||||
|
||||
Ok(Response::new(Empty {}))
|
||||
}
|
||||
|
||||
async fn update_database_password(
|
||||
&self,
|
||||
request: Request<UpdateDatabasePasswordRequest>,
|
||||
|
||||
@@ -49,6 +49,9 @@ async fn main() -> Result<()> {
|
||||
let server_manager = Arc::new(ServerManager::new(docker, &config));
|
||||
info!("Server manager initialized");
|
||||
|
||||
let recovered_servers = server_manager.recover_existing_servers().await?;
|
||||
info!(recovered_servers, "Recovered managed servers from Docker");
|
||||
|
||||
// Initialize shared command dispatcher (single command pipeline for all games/sources)
|
||||
let command_dispatcher = Arc::new(CommandDispatcher::new(server_manager.clone()));
|
||||
info!("Command dispatcher initialized");
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::io::ErrorKind;
|
||||
use std::process::Stdio;
|
||||
|
||||
use reqwest::Url;
|
||||
use thiserror::Error;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
use tonic::Status;
|
||||
use uuid::Uuid;
|
||||
@@ -164,6 +166,28 @@ impl ManagedMysqlManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn import_sql(
|
||||
&self,
|
||||
database_name: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ManagedMysqlError> {
|
||||
let config = self.config.as_ref().ok_or(ManagedMysqlError::NotConfigured)?;
|
||||
let database_name = database_name.trim();
|
||||
if database_name.is_empty() {
|
||||
return Err(ManagedMysqlError::CommandFailed(
|
||||
"Database name is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if sql.trim().is_empty() {
|
||||
return Err(ManagedMysqlError::CommandFailed(
|
||||
"SQL payload is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.run_sql_script(config, database_name, sql).await
|
||||
}
|
||||
|
||||
pub async fn delete_database(
|
||||
&self,
|
||||
database_name: &str,
|
||||
@@ -242,6 +266,76 @@ impl ManagedMysqlManager {
|
||||
|
||||
Err(ManagedMysqlError::ClientMissing)
|
||||
}
|
||||
|
||||
async fn run_sql_script(
|
||||
&self,
|
||||
config: &ManagedMysqlRuntimeConfig,
|
||||
database_name: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ManagedMysqlError> {
|
||||
let binaries = match config.client_bin.as_deref() {
|
||||
Some(bin) if !bin.trim().is_empty() => vec![bin.to_string()],
|
||||
_ => vec!["mariadb".to_string(), "mysql".to_string()],
|
||||
};
|
||||
|
||||
let mut missing_binary = false;
|
||||
|
||||
for binary in binaries {
|
||||
let child = Command::new(&binary)
|
||||
.args([
|
||||
"--protocol=TCP",
|
||||
"--batch",
|
||||
"--skip-column-names",
|
||||
"-h",
|
||||
&config.admin_host,
|
||||
"-P",
|
||||
&config.admin_port.to_string(),
|
||||
"-u",
|
||||
&config.admin_username,
|
||||
database_name,
|
||||
])
|
||||
.env("MYSQL_PWD", &config.admin_password)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Ok(mut child) => {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(sql.as_bytes()).await?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let message = if !stderr.is_empty() {
|
||||
stderr
|
||||
} else if !stdout.is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
format!("{} exited with status {}", binary, output.status)
|
||||
};
|
||||
return Err(ManagedMysqlError::CommandFailed(message));
|
||||
}
|
||||
Err(error) if error.kind() == ErrorKind::NotFound => {
|
||||
missing_binary = true;
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(ManagedMysqlError::Io(error)),
|
||||
}
|
||||
}
|
||||
|
||||
if missing_binary {
|
||||
return Err(ManagedMysqlError::ClientMissing);
|
||||
}
|
||||
|
||||
Err(ManagedMysqlError::ClientMissing)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_runtime_config(
|
||||
|
||||
@@ -49,6 +49,32 @@ impl ServerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild in-memory server specs from existing managed Docker containers.
|
||||
pub async fn recover_existing_servers(&self) -> Result<usize, DaemonError> {
|
||||
let recovered = self
|
||||
.docker
|
||||
.recover_managed_server_specs(&self.data_root)
|
||||
.await
|
||||
.map_err(|error| DaemonError::Internal(format!("Failed to recover managed containers: {}", error)))?;
|
||||
|
||||
let recovered_count = recovered.len();
|
||||
let mut servers = self.servers.write().await;
|
||||
servers.clear();
|
||||
|
||||
for spec in recovered {
|
||||
self.ensure_server_data_dir(&spec.data_path).await?;
|
||||
info!(
|
||||
uuid = %spec.uuid,
|
||||
state = %spec.state,
|
||||
image = %spec.docker_image,
|
||||
"Recovered managed server from Docker runtime"
|
||||
);
|
||||
servers.insert(spec.uuid.clone(), spec);
|
||||
}
|
||||
|
||||
Ok(recovered_count)
|
||||
}
|
||||
|
||||
/// Get server spec by UUID.
|
||||
pub async fn get_server(&self, uuid: &str) -> Result<ServerSpec, DaemonError> {
|
||||
let servers = self.servers.read().await;
|
||||
@@ -200,6 +226,10 @@ impl ServerManager {
|
||||
})?;
|
||||
}
|
||||
|
||||
self.docker.pull_image(&desired_spec.docker_image).await.map_err(|e| {
|
||||
DaemonError::Internal(format!("Failed to pull updated image during server update: {}", e))
|
||||
})?;
|
||||
|
||||
match self.docker.create_container(&desired_spec).await {
|
||||
Ok(container_id) => {
|
||||
desired_spec.container_id = Some(container_id);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -7,15 +7,34 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface Game {
|
||||
environmentVars?: GameEnvironmentVar[];
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
dockerImage: string;
|
||||
}
|
||||
|
||||
interface GameEnvironmentVar {
|
||||
key: string;
|
||||
label?: string;
|
||||
default?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
inputType?: 'text' | 'boolean';
|
||||
composeInto?: string;
|
||||
enabledLabel?: string;
|
||||
disabledLabel?: string;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -36,6 +55,28 @@ interface PaginatedResponse<T> {
|
||||
meta: { total: number };
|
||||
}
|
||||
|
||||
interface AdditionalPortRequirement {
|
||||
key: string;
|
||||
label: string;
|
||||
defaultPort: number;
|
||||
protocols: Array<'tcp' | 'udp'>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function additionalPortRequirementsForGame(gameSlug: string): AdditionalPortRequirement[] {
|
||||
if (gameSlug.trim().toLowerCase() !== 'satisfactory') return [];
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'satisfactory-messaging',
|
||||
label: 'Messaging Port',
|
||||
defaultPort: 8888,
|
||||
protocols: ['tcp'],
|
||||
description: 'Required by the Satisfactory server messaging API.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function CreateServerPage() {
|
||||
const { orgId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -46,9 +87,13 @@ export function CreateServerPage() {
|
||||
const [gameId, setGameId] = useState('');
|
||||
const [nodeId, setNodeId] = useState('');
|
||||
const [allocationId, setAllocationId] = useState('');
|
||||
const [additionalAllocationIds, setAdditionalAllocationIds] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [memoryLimit, setMemoryLimit] = useState(1024);
|
||||
const [diskLimit, setDiskLimit] = useState(5120);
|
||||
const [cpuLimit, setCpuLimit] = useState(100);
|
||||
const [environment, setEnvironment] = useState<Record<string, string>>({});
|
||||
|
||||
const { data: gamesData } = useQuery({
|
||||
queryKey: ['games'],
|
||||
@@ -63,9 +108,7 @@ export function CreateServerPage() {
|
||||
const { data: allocationsData } = useQuery({
|
||||
queryKey: ['allocations', orgId, nodeId],
|
||||
queryFn: () =>
|
||||
api.get<PaginatedResponse<Allocation>>(
|
||||
`/organizations/${orgId}/nodes/${nodeId}/allocations`,
|
||||
),
|
||||
api.get<PaginatedResponse<Allocation>>(`/organizations/${orgId}/nodes/${nodeId}/allocations`),
|
||||
enabled: !!nodeId,
|
||||
});
|
||||
|
||||
@@ -81,8 +124,60 @@ export function CreateServerPage() {
|
||||
|
||||
const games = gamesData?.data ?? [];
|
||||
const nodes = nodesData?.data ?? [];
|
||||
const activeGame = games.find((game) => game.id === gameId);
|
||||
const additionalPortRequirements = activeGame
|
||||
? additionalPortRequirementsForGame(activeGame.slug)
|
||||
: [];
|
||||
const visibleEnvironmentVars = (activeGame?.environmentVars ?? []).filter((variable) => {
|
||||
const key = variable.key?.trim();
|
||||
return Boolean(key) && !variable.composeInto?.trim();
|
||||
});
|
||||
const missingRequiredEnvironment = visibleEnvironmentVars.some((variable) => {
|
||||
const key = variable.key.trim();
|
||||
const currentValue = environment[key] ?? String(variable.default ?? '');
|
||||
return Boolean(variable.required) && !currentValue.trim();
|
||||
});
|
||||
const missingRequiredAdditionalPorts = additionalPortRequirements.some(
|
||||
(requirement) => !additionalAllocationIds[requirement.key],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeGame) {
|
||||
setEnvironment({});
|
||||
return;
|
||||
}
|
||||
|
||||
setEnvironment((current) => {
|
||||
const next: Record<string, string> = {};
|
||||
for (const variable of activeGame.environmentVars ?? []) {
|
||||
const key = variable.key?.trim();
|
||||
if (!key || variable.composeInto?.trim()) continue;
|
||||
next[key] = current[key] ?? String(variable.default ?? '');
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setAdditionalAllocationIds({});
|
||||
if (activeGame.slug.trim().toLowerCase() === 'satisfactory') {
|
||||
setMemoryLimit((current) => Math.max(current, 8192));
|
||||
setDiskLimit((current) => Math.max(current, 12288));
|
||||
}
|
||||
}, [activeGame?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setAdditionalAllocationIds((current) => {
|
||||
const allowedKeys = new Set(additionalPortRequirements.map((requirement) => requirement.key));
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(current).filter(([key]) => allowedKeys.has(key)),
|
||||
);
|
||||
return next;
|
||||
});
|
||||
}, [activeGame?.slug]);
|
||||
|
||||
const handleCreate = () => {
|
||||
const environmentPayload = Object.fromEntries(
|
||||
Object.entries(environment).filter(([, value]) => value.trim() !== ''),
|
||||
);
|
||||
|
||||
createMutation.mutate({
|
||||
name,
|
||||
description: description || undefined,
|
||||
@@ -92,9 +187,30 @@ export function CreateServerPage() {
|
||||
memoryLimit: memoryLimit * 1024 * 1024,
|
||||
diskLimit: diskLimit * 1024 * 1024,
|
||||
cpuLimit,
|
||||
additionalAllocationIds:
|
||||
additionalPortRequirements.length > 0
|
||||
? additionalPortRequirements
|
||||
.map((requirement) => additionalAllocationIds[requirement.key])
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
environment: Object.keys(environmentPayload).length > 0 ? environmentPayload : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const allocationOptionsForRequirement = (requirementKey: string) => {
|
||||
const selectedByOtherRequirements = new Set(
|
||||
Object.entries(additionalAllocationIds)
|
||||
.filter(([key]) => key !== requirementKey)
|
||||
.map(([, id]) => id)
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
return freeAllocations.filter(
|
||||
(allocation) =>
|
||||
allocation.id !== allocationId && !selectedByOtherRequirements.has(allocation.id),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
@@ -149,7 +265,69 @@ export function CreateServerPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={() => setStep(2)} disabled={!name || !gameId}>
|
||||
{visibleEnvironmentVars.map((variable) => {
|
||||
const key = variable.key.trim();
|
||||
const label = variable.label?.trim() || key;
|
||||
const value = environment[key] ?? String(variable.default ?? '');
|
||||
const isSecret =
|
||||
key.toLowerCase().includes('password') || key.toLowerCase().includes('license');
|
||||
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label>
|
||||
{label}
|
||||
{variable.required ? ' *' : ''}
|
||||
</Label>
|
||||
{variable.inputType === 'boolean' ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={value === 'true' ? 'default' : 'outline'}
|
||||
onClick={() =>
|
||||
setEnvironment((current) => ({
|
||||
...current,
|
||||
[key]: 'true',
|
||||
}))
|
||||
}
|
||||
>
|
||||
{variable.enabledLabel ?? 'Enabled'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={value === 'false' ? 'secondary' : 'outline'}
|
||||
onClick={() =>
|
||||
setEnvironment((current) => ({
|
||||
...current,
|
||||
[key]: 'false',
|
||||
}))
|
||||
}
|
||||
>
|
||||
{variable.disabledLabel ?? 'Disabled'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
type={isSecret ? 'password' : 'text'}
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setEnvironment((current) => ({
|
||||
...current,
|
||||
[key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={variable.description || label}
|
||||
/>
|
||||
)}
|
||||
{variable.description && (
|
||||
<p className="text-xs text-muted-foreground">{variable.description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!name || !gameId || missingRequiredEnvironment}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</CardContent>
|
||||
@@ -170,6 +348,7 @@ export function CreateServerPage() {
|
||||
onValueChange={(v) => {
|
||||
setNodeId(v);
|
||||
setAllocationId('');
|
||||
setAdditionalAllocationIds({});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -187,7 +366,17 @@ export function CreateServerPage() {
|
||||
{nodeId && (
|
||||
<div className="space-y-2">
|
||||
<Label>Port Allocation</Label>
|
||||
<Select value={allocationId} onValueChange={setAllocationId}>
|
||||
<Select
|
||||
value={allocationId}
|
||||
onValueChange={(value) => {
|
||||
setAllocationId(value);
|
||||
setAdditionalAllocationIds((current) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(current).filter(([, selectedId]) => selectedId !== value),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a port" />
|
||||
</SelectTrigger>
|
||||
@@ -204,11 +393,53 @@ export function CreateServerPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{nodeId &&
|
||||
additionalPortRequirements.map((requirement) => {
|
||||
const options = allocationOptionsForRequirement(requirement.key);
|
||||
const protocols = requirement.protocols.join('/').toUpperCase();
|
||||
|
||||
return (
|
||||
<div key={requirement.key} className="space-y-2">
|
||||
<Label>
|
||||
{requirement.label} ({protocols})
|
||||
</Label>
|
||||
<Select
|
||||
value={additionalAllocationIds[requirement.key] ?? ''}
|
||||
onValueChange={(value) =>
|
||||
setAdditionalAllocationIds((current) => ({
|
||||
...current,
|
||||
[requirement.key]: value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`Select port ${requirement.defaultPort}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((allocation) => (
|
||||
<SelectItem key={allocation.id} value={allocation.id}>
|
||||
{allocation.ip}:{allocation.port}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{requirement.description}</p>
|
||||
{options.length === 0 && (
|
||||
<p className="text-sm text-destructive">
|
||||
No free allocation available for this port
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setStep(1)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => setStep(3)} disabled={!nodeId || !allocationId}>
|
||||
<Button
|
||||
onClick={() => setStep(3)}
|
||||
disabled={!nodeId || !allocationId || missingRequiredAdditionalPorts}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
=== CONDUIT CRASH REPORT ==========================================
|
||||
Framework : Conduit 0.1.0-dev
|
||||
Time : 2026-06-13 11:34:58 (local)
|
||||
Process : pid=36
|
||||
Signal : 11 (Segmentation fault), fault address (nil)
|
||||
|
||||
--- ATTRIBUTION ---------------------------------------------------
|
||||
1. owner=crash_test_plugin callsite=conduit_crash_test
|
||||
>>> Most likely culprit: 'crash_test_plugin' (in 'conduit_crash_test')
|
||||
|
||||
--- STACK TRACE ---------------------------------------------------
|
||||
#00 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xbfb64) [0x7fdf9e694b64]
|
||||
#01 /lib/x86_64-linux-gnu/libc.so.6(+0x3c050) [0x7fdfefa51050]
|
||||
#02 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xbf66c) [0x7fdf9e69466c]
|
||||
#03 /home/steam/cs2-dedicated/game/bin/linuxsteamrt64/libtier0.so(+0x1e0902) [0x7fdfef4ca902]
|
||||
|
||||
--- RECENT EVENTS (oldest first) ----------------------------------
|
||||
[-1767.736s] [INFO] perf: heartbeat: 1921 frames, avg 2.22 ms, max 20.85 ms (last 30 s)
|
||||
[-1737.735s] [INFO] perf: heartbeat: 1920 frames, avg 2.16 ms, max 5.31 ms (last 30 s)
|
||||
[-1707.720s] [INFO] perf: heartbeat: 1921 frames, avg 2.12 ms, max 4.39 ms (last 30 s)
|
||||
[-1677.719s] [INFO] perf: heartbeat: 1920 frames, avg 2.20 ms, max 5.59 ms (last 30 s)
|
||||
[-1647.706s] [INFO] perf: heartbeat: 1921 frames, avg 2.39 ms, max 23.56 ms (last 30 s)
|
||||
[-1617.705s] [INFO] perf: heartbeat: 1920 frames, avg 2.71 ms, max 7.46 ms (last 30 s)
|
||||
[-1587.691s] [INFO] perf: heartbeat: 1921 frames, avg 2.37 ms, max 8.58 ms (last 30 s)
|
||||
[-1557.680s] [INFO] perf: heartbeat: 1921 frames, avg 2.33 ms, max 4.84 ms (last 30 s)
|
||||
[-1527.679s] [INFO] perf: heartbeat: 1920 frames, avg 2.25 ms, max 5.09 ms (last 30 s)
|
||||
[-1497.674s] [INFO] perf: heartbeat: 1920 frames, avg 2.18 ms, max 19.43 ms (last 30 s)
|
||||
[-1467.673s] [INFO] perf: heartbeat: 1920 frames, avg 2.30 ms, max 6.87 ms (last 30 s)
|
||||
[-1437.669s] [INFO] perf: heartbeat: 1920 frames, avg 2.23 ms, max 34.46 ms (last 30 s)
|
||||
[-1407.668s] [INFO] perf: heartbeat: 1920 frames, avg 2.45 ms, max 6.17 ms (last 30 s)
|
||||
[-1377.653s] [INFO] perf: heartbeat: 1921 frames, avg 2.46 ms, max 6.25 ms (last 30 s)
|
||||
[-1347.653s] [INFO] perf: heartbeat: 1920 frames, avg 2.53 ms, max 5.44 ms (last 30 s)
|
||||
[-1317.644s] [INFO] perf: heartbeat: 1921 frames, avg 2.51 ms, max 20.77 ms (last 30 s)
|
||||
[-1287.631s] [INFO] perf: heartbeat: 1921 frames, avg 2.46 ms, max 6.45 ms (last 30 s)
|
||||
[-1257.614s] [INFO] perf: heartbeat: 1921 frames, avg 2.57 ms, max 5.90 ms (last 30 s)
|
||||
[-1227.600s] [INFO] perf: heartbeat: 1921 frames, avg 2.54 ms, max 5.79 ms (last 30 s)
|
||||
[-1197.583s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 6.94 ms (last 30 s)
|
||||
[-1167.578s] [INFO] perf: heartbeat: 1920 frames, avg 2.46 ms, max 20.36 ms (last 30 s)
|
||||
[-1137.562s] [INFO] perf: heartbeat: 1921 frames, avg 2.47 ms, max 7.86 ms (last 30 s)
|
||||
[-1107.548s] [INFO] perf: heartbeat: 1921 frames, avg 2.53 ms, max 6.38 ms (last 30 s)
|
||||
[-1077.547s] [INFO] perf: heartbeat: 1920 frames, avg 2.54 ms, max 4.09 ms (last 30 s)
|
||||
[-1047.543s] [INFO] perf: heartbeat: 1920 frames, avg 2.61 ms, max 33.69 ms (last 30 s)
|
||||
[-1017.532s] [INFO] perf: heartbeat: 1921 frames, avg 2.44 ms, max 5.97 ms (last 30 s)
|
||||
[- 987.516s] [INFO] perf: heartbeat: 1921 frames, avg 2.59 ms, max 5.71 ms (last 30 s)
|
||||
[- 957.501s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 7.43 ms (last 30 s)
|
||||
[- 927.501s] [INFO] perf: heartbeat: 1920 frames, avg 2.60 ms, max 9.17 ms (last 30 s)
|
||||
[- 897.490s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 24.84 ms (last 30 s)
|
||||
[- 867.475s] [INFO] perf: heartbeat: 1921 frames, avg 2.58 ms, max 6.20 ms (last 30 s)
|
||||
[- 837.475s] [INFO] perf: heartbeat: 1920 frames, avg 2.74 ms, max 5.57 ms (last 30 s)
|
||||
[- 807.475s] [INFO] perf: heartbeat: 1920 frames, avg 2.71 ms, max 6.15 ms (last 30 s)
|
||||
[- 777.460s] [INFO] perf: heartbeat: 1921 frames, avg 2.70 ms, max 5.47 ms (last 30 s)
|
||||
[- 747.449s] [INFO] perf: heartbeat: 1920 frames, avg 2.68 ms, max 24.22 ms (last 30 s)
|
||||
[- 717.434s] [INFO] perf: heartbeat: 1921 frames, avg 2.80 ms, max 8.64 ms (last 30 s)
|
||||
[- 687.419s] [INFO] perf: heartbeat: 1921 frames, avg 2.65 ms, max 5.28 ms (last 30 s)
|
||||
[- 657.418s] [INFO] perf: heartbeat: 1920 frames, avg 2.66 ms, max 6.34 ms (last 30 s)
|
||||
[- 627.416s] [INFO] perf: heartbeat: 1920 frames, avg 2.51 ms, max 31.89 ms (last 30 s)
|
||||
[- 597.416s] [INFO] perf: heartbeat: 1920 frames, avg 2.61 ms, max 6.72 ms (last 30 s)
|
||||
[- 567.400s] [INFO] perf: heartbeat: 1921 frames, avg 2.67 ms, max 6.53 ms (last 30 s)
|
||||
[- 537.384s] [INFO] perf: heartbeat: 1921 frames, avg 2.66 ms, max 6.66 ms (last 30 s)
|
||||
[- 507.369s] [INFO] perf: heartbeat: 1921 frames, avg 2.55 ms, max 5.30 ms (last 30 s)
|
||||
[- 477.365s] [INFO] perf: heartbeat: 1921 frames, avg 2.50 ms, max 34.05 ms (last 30 s)
|
||||
[- 447.364s] [INFO] perf: heartbeat: 1920 frames, avg 2.59 ms, max 5.42 ms (last 30 s)
|
||||
[- 417.349s] [INFO] perf: heartbeat: 1921 frames, avg 2.66 ms, max 6.55 ms (last 30 s)
|
||||
[- 387.334s] [INFO] perf: heartbeat: 1921 frames, avg 2.34 ms, max 4.63 ms (last 30 s)
|
||||
[- 357.334s] [INFO] perf: heartbeat: 1920 frames, avg 2.77 ms, max 6.65 ms (last 30 s)
|
||||
[- 327.332s] [INFO] perf: heartbeat: 1920 frames, avg 2.74 ms, max 26.14 ms (last 30 s)
|
||||
[- 312.170s] detour 'detour_self_test' installed at 0x7fdf9e693cd0
|
||||
[- 312.170s] [INFO] hooks: detour 'detour_self_test' installed at 0x7fdf9e693cd0
|
||||
[- 312.169s] detour 'detour_self_test' removed
|
||||
[- 312.169s] [INFO] hooks: detour 'detour_self_test' removed
|
||||
[- 297.318s] [INFO] perf: heartbeat: 1921 frames, avg 2.73 ms, max 6.96 ms (last 30 s)
|
||||
[- 267.308s] [INFO] perf: heartbeat: 1920 frames, avg 2.58 ms, max 6.68 ms (last 30 s)
|
||||
[- 237.308s] [INFO] perf: heartbeat: 1920 frames, avg 2.68 ms, max 7.08 ms (last 30 s)
|
||||
[- 207.296s] [INFO] perf: heartbeat: 1921 frames, avg 2.50 ms, max 25.68 ms (last 30 s)
|
||||
[- 177.293s] [INFO] perf: heartbeat: 1920 frames, avg 2.51 ms, max 5.12 ms (last 30 s)
|
||||
[- 147.280s] [INFO] perf: heartbeat: 1921 frames, avg 2.65 ms, max 5.80 ms (last 30 s)
|
||||
[- 117.280s] [INFO] perf: heartbeat: 1920 frames, avg 2.64 ms, max 6.24 ms (last 30 s)
|
||||
[- 87.265s] [INFO] perf: heartbeat: 1921 frames, avg 2.78 ms, max 5.95 ms (last 30 s)
|
||||
[- 57.263s] [INFO] perf: heartbeat: 1920 frames, avg 2.66 ms, max 31.12 ms (last 30 s)
|
||||
[- 27.248s] [INFO] perf: heartbeat: 1921 frames, avg 2.72 ms, max 7.34 ms (last 30 s)
|
||||
[- 0.001s] [WARN] core: conduit_crash_test invoked — crashing deliberately
|
||||
===================================================================
|
||||
@@ -0,0 +1,374 @@
|
||||
# Conduit Linux Accumulated Re-run - 2026-06-15
|
||||
|
||||
## Scope
|
||||
|
||||
- Pulled all accumulated commits from `origin/main`.
|
||||
- Inspected the changed files and current `docs/linux-bringup.md`.
|
||||
- Rebuilt Conduit on Linux with GCC 12.
|
||||
- Deployed to CS2 server `Test Sunucusu`.
|
||||
- Verified the new timer, chat interception, usermessage/chat-send, and console adopt paths.
|
||||
- Cleaned temporary test subscriptions/timers/commands.
|
||||
- Satisfactory server was not modified.
|
||||
|
||||
## Git
|
||||
|
||||
Previous local HEAD before this run:
|
||||
|
||||
```text
|
||||
bb58461 Phase 2: dynamic console commands + convar access
|
||||
```
|
||||
|
||||
Pulled commits:
|
||||
|
||||
```text
|
||||
43855ff PLAN: chat ships plain (white) via UTIL_SayTextFilter; color is a follow-up
|
||||
8afbeec chat: send via UTIL_SayTextFilter again (plain text works; SayText2 shows nothing)
|
||||
faded69 docs: refresh README (Phase 2 status + what works) and PLAN chat-send entry
|
||||
18e7970 chat colors: prefer UTIL_SayText2Filter (renders color codes)
|
||||
1743147 PLAN: chat send visually verified on Windows (msgType 0 = chat); colors pending
|
||||
4b3d322 PLAN: correct the chat-send entry (native UTIL_SayTextFilter, SDK drift finding)
|
||||
4ba6019 chat sending: call native UTIL_SayTextFilter instead of building the usermessage
|
||||
a4af862 Phase 2: usermessages + chat sending (SayText2)
|
||||
6985830 Phase 2: chat interception (say / say_team)
|
||||
a375e05 Phase 2: game-thread timers driven by GameFrame
|
||||
62e4ca0 Close out Linux verification of the Commands & ConVars slice
|
||||
15cc25e console: track adopted convars under their owner
|
||||
```
|
||||
|
||||
Diff summary:
|
||||
|
||||
```text
|
||||
PLAN.md | 8 +-
|
||||
README.md | 17 +++-
|
||||
core/CMakeLists.txt | 3 +
|
||||
core/src/conduit/chat.cpp | 165 ++++++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/chat.h | 46 +++++++++++
|
||||
core/src/conduit/commands.cpp | 131 ++++++++++++++++++++++++++++++
|
||||
core/src/conduit/console.cpp | 23 +++++-
|
||||
core/src/conduit/timers.cpp | 181 ++++++++++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/timers.h | 47 +++++++++++
|
||||
core/src/conduit/usermsg.cpp | 147 ++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/usermsg.h | 23 ++++++
|
||||
core/src/plugin.cpp | 16 ++++
|
||||
docs/linux-bringup.md | 76 +++++++++++++++---
|
||||
gamedata/core.json | 15 +++-
|
||||
```
|
||||
|
||||
Working tree after the run:
|
||||
|
||||
```text
|
||||
## main...origin/main
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Command:
|
||||
|
||||
```sh
|
||||
cmake -S /root/codex/Conduit -B /root/codex/Conduit/build/linux-rerun -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12
|
||||
cmake --build /root/codex/Conduit/build/linux-rerun
|
||||
```
|
||||
|
||||
Result: build and link succeeded.
|
||||
|
||||
```text
|
||||
-- Configuring done
|
||||
-- Generating done
|
||||
-- Build files have been written to: /root/codex/Conduit/build/linux-rerun
|
||||
[1/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/timers.cpp.o
|
||||
[2/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/commands.cpp.o
|
||||
[3/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/usermsg.cpp.o
|
||||
[4/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/console.cpp.o
|
||||
[5/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/chat.cpp.o
|
||||
[6/7] Building CXX object core/CMakeFiles/conduit.dir/src/plugin.cpp.o
|
||||
[7/7] Linking CXX shared library package/addons/conduit/bin/conduit.so
|
||||
```
|
||||
|
||||
Only warning observed:
|
||||
|
||||
```text
|
||||
/root/codex/Conduit/core/src/conduit/commands.cpp:521:13: warning: compound assignment with volatile-qualified left operand is deprecated [-Wvolatile]
|
||||
```
|
||||
|
||||
No failed CMake/Ninja output exists because the build passed.
|
||||
|
||||
## Deploy And Load
|
||||
|
||||
Deployed:
|
||||
|
||||
```text
|
||||
/root/codex/Conduit/build/linux-rerun/package/.
|
||||
```
|
||||
|
||||
To:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/
|
||||
```
|
||||
|
||||
Load checks:
|
||||
|
||||
```text
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 73.6 s
|
||||
game frames : 3983
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260615.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
```
|
||||
|
||||
Gamedata and usermessage function resolution:
|
||||
|
||||
```text
|
||||
--- conduit_gamedata ---
|
||||
gamedata: 1 files, 2 signatures, 0 offsets
|
||||
[ok] UTIL_SayText2Filter server 0x7fd2de250b30 (core.json)
|
||||
[ok] UTIL_SayTextFilter server 0x7fd2de2508d0 (core.json)
|
||||
|
||||
--- conduit_usermsg ---
|
||||
[Conduit] [INFO] usermsg: chat sending live via UTIL_SayTextFilter (0x7fd2de2508d0) - plain text, no color yet
|
||||
usermessages: chat sending live (SayText2 resolved, SayText resolved)
|
||||
```
|
||||
|
||||
Startup log:
|
||||
|
||||
```text
|
||||
2026-06-15 10:50:26 [INFO] gamedata: 1 files loaded: 2 signatures, 0 offsets, all valid
|
||||
2026-06-15 10:50:27 [INFO] entity: armed via CGameEntitySystem vtable 0x7fd2dec4ffd8
|
||||
2026-06-15 10:50:27 [INFO] gameevents: armed via CGameEventManager vtable 0x7fd2dec50f50
|
||||
2026-06-15 10:50:27 [INFO] chat: ready - hook installs on first subscriber
|
||||
2026-06-15 10:50:27 [INFO] core: Conduit 0.1.0-dev loaded - crash reporter armed, profiler on
|
||||
2026-06-15 10:50:27 [INFO] gameevents: game event manager connected (0x7fd2defd0460)
|
||||
2026-06-15 10:50:37 [INFO] core: first GameFrame observed - hook dispatch confirmed
|
||||
```
|
||||
|
||||
Entity system still resolves on Linux at `GameResourceServiceServerV001+0x50`:
|
||||
|
||||
```text
|
||||
--- conduit_entity ---
|
||||
[Conduit] [INFO] entity: entity system connected (0x7fd2da446000) via GameResourceServiceServerV001+0x50 - vtable matches CGameEntitySystem
|
||||
entity system: connected
|
||||
slot 0 cs_player_controller team=2 pawn=player health=100
|
||||
slot 1 cs_player_controller team=3 pawn=player health=100
|
||||
```
|
||||
|
||||
## Timer Tests
|
||||
|
||||
Schedule/list:
|
||||
|
||||
```text
|
||||
--- conduit_timer_after 2 ---
|
||||
[Conduit] one-shot timer #1 scheduled in 2.00 s
|
||||
--- conduit_timer_every 1 ---
|
||||
[Conduit] repeating timer #2 every 1.00 s (stops after 5)
|
||||
--- conduit_timers ---
|
||||
timers: 2 active
|
||||
#1 owner=timer_test once next in 1938 ms
|
||||
#2 owner=timer_test repeat next in 969 ms
|
||||
```
|
||||
|
||||
Log block:
|
||||
|
||||
```text
|
||||
2026-06-15 10:51:55 [INFO] timer_test: repeat timer tick 1
|
||||
2026-06-15 10:51:56 [INFO] timer_test: one-shot timer fired
|
||||
2026-06-15 10:51:56 [INFO] timer_test: repeat timer tick 2
|
||||
2026-06-15 10:51:57 [INFO] timer_test: repeat timer tick 3
|
||||
2026-06-15 10:51:58 [INFO] timer_test: repeat timer tick 4
|
||||
2026-06-15 10:51:59 [INFO] timer_test: repeat timer tick 5
|
||||
2026-06-15 10:51:59 [INFO] timer_test: repeat timer self-cancelled after 5 ticks
|
||||
```
|
||||
|
||||
After firing:
|
||||
|
||||
```text
|
||||
--- conduit_timers ---
|
||||
timers: 0 active
|
||||
--- conduit_prof ---
|
||||
timer_test timer 6 71.5us 131.1us 131.1us 101.4us 429.0us
|
||||
```
|
||||
|
||||
Group cancel:
|
||||
|
||||
```text
|
||||
--- conduit_timer_every 1 ---
|
||||
[Conduit] repeating timer #3 every 1.00 s (stops after 5)
|
||||
--- conduit_timer_after 30 ---
|
||||
[Conduit] one-shot timer #4 scheduled in 30.00 s
|
||||
--- conduit_timers ---
|
||||
timers: 2 active
|
||||
#3 owner=timer_test repeat next in 937 ms
|
||||
#4 owner=timer_test once next in 29969 ms
|
||||
--- conduit_timer_stop ---
|
||||
[Conduit] cancelled 2 test timer(s)
|
||||
--- conduit_timers ---
|
||||
timers: 0 active
|
||||
```
|
||||
|
||||
Result: timers passed, including self-cancel from inside callback and owner group cancel.
|
||||
|
||||
## Chat Interception Tests
|
||||
|
||||
RCON output:
|
||||
|
||||
```text
|
||||
--- conduit_chat ---
|
||||
chat: 0 subscriber(s), interception hook off
|
||||
--- conduit_chat_listen ---
|
||||
[Conduit] [INFO] chat: 'chat_test' subscribed to chat
|
||||
[Conduit] listening to chat
|
||||
--- conduit_chat ---
|
||||
chat: 1 subscriber(s), interception hook on
|
||||
owner=chat_test hits=0
|
||||
--- say "hello from server" ---
|
||||
[Conduit] [INFO] chat_test: say slot=-1: "hello from server"
|
||||
[All Chat][Console (0)]: "hello from server"
|
||||
L 06/15/2026 - 10:53:03: "Console<0>" say ""hello from server""
|
||||
--- say "this has badword inside" ---
|
||||
[Conduit] [INFO] chat_test: say slot=-1: "this has badword inside" [SUPPRESSED]
|
||||
--- say_team "x" ---
|
||||
[Conduit] [INFO] chat_test: say_team slot=-1: "x"
|
||||
--- conduit_prof ---
|
||||
chat_test chat 3 97.0us 131.1us 131.1us 127.4us 290.9us
|
||||
```
|
||||
|
||||
Conduit log:
|
||||
|
||||
```text
|
||||
2026-06-15 10:53:03 [INFO] chat: 'chat_test' subscribed to chat
|
||||
2026-06-15 10:53:03 [INFO] chat_test: say slot=-1: "hello from server"
|
||||
2026-06-15 10:53:03 [INFO] chat_test: say slot=-1: "this has badword inside" [SUPPRESSED]
|
||||
2026-06-15 10:53:03 [INFO] chat_test: say_team slot=-1: "x"
|
||||
```
|
||||
|
||||
The suppressed `badword` message did not echo as `[All Chat]` in the RCON output.
|
||||
|
||||
Cleanup:
|
||||
|
||||
```text
|
||||
--- conduit_chat_stop ---
|
||||
[Conduit] removed 1 chat subscriber(s)
|
||||
--- conduit_chat ---
|
||||
chat: 0 subscriber(s), interception hook off
|
||||
--- say "after stop visible" ---
|
||||
[All Chat][Console (0)]: "after stop visible"
|
||||
L 06/15/2026 - 10:53:20: "Console<0>" say ""after stop visible""
|
||||
--- conduit_chat ---
|
||||
chat: 0 subscriber(s), interception hook off
|
||||
```
|
||||
|
||||
Result: chat interception, suppression, say_team distinction, profiler scope, and hook removal all passed.
|
||||
|
||||
## Chat Sending / UserMessages
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
--- conduit_usermsg ---
|
||||
usermessages: chat sending live (SayText2 resolved, SayText resolved)
|
||||
--- conduit_say linux broadcast test ---
|
||||
[Conduit] PrintToAll sent
|
||||
--- conduit_say_player 0 private_ping ---
|
||||
[Conduit] PrintToPlayer(0) sent
|
||||
--- conduit_usermsg ---
|
||||
usermessages: chat sending live (SayText2 resolved, SayText resolved)
|
||||
```
|
||||
|
||||
Result: Linux signatures resolve and native send calls return `sent`. This verifies the server-side path and no crash. I cannot visually confirm client rendering from this environment; per latest plan this path sends plain white text through `UTIL_SayTextFilter`.
|
||||
|
||||
## Console Adopt Fix
|
||||
|
||||
This re-tested the previously observed adopted-convar listing issue.
|
||||
|
||||
```text
|
||||
--- conduit_console ---
|
||||
[Conduit] [INFO] console: command 'conduit_demo' registered (owner 'console_demo')
|
||||
[Conduit] [INFO] console: convar 'conduit_demo_value' created (owner 'console_demo')
|
||||
console: ready, 1 command(s), 1 plugin convar(s)
|
||||
cmd conduit_demo owner=console_demo
|
||||
cvar conduit_demo_value owner=console_demo value=7
|
||||
--- conduit_cvar conduit_demo_value 42 ---
|
||||
[Conduit] set 'conduit_demo_value' = '42'
|
||||
[Conduit] conduit_demo_value = 42 (int=42 float=42.000 bool=1)
|
||||
--- conduit_concmd_stop ---
|
||||
[Conduit] removed 2 demo registration(s)
|
||||
--- conduit_console ---
|
||||
[Conduit] [INFO] console: command 'conduit_demo' registered (owner 'console_demo')
|
||||
[Conduit] [INFO] console: convar 'conduit_demo_value' already registered - adopting (owner 'console_demo')
|
||||
console: ready, 1 command(s), 1 plugin convar(s)
|
||||
cmd conduit_demo owner=console_demo
|
||||
cvar conduit_demo_value owner=console_demo value=42
|
||||
```
|
||||
|
||||
Final cleanup:
|
||||
|
||||
```text
|
||||
--- conduit_concmd_stop ---
|
||||
[Conduit] removed 2 demo registration(s)
|
||||
--- conduit_demo x ---
|
||||
|
||||
```
|
||||
|
||||
Result: adopt tracking is fixed. Adopted convar now appears under `1 plugin convar(s)` and owner cleanup removes 2 registrations.
|
||||
|
||||
## Final Runtime State
|
||||
|
||||
```text
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 248.6 s
|
||||
game frames : 15184
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260615.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
--- conduit_timers ---
|
||||
timers: 0 active
|
||||
--- conduit_chat ---
|
||||
chat: 0 subscriber(s), interception hook off
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 0 subscription(s), 0 event(s) registered, FireEvent detour off
|
||||
--- conduit_usermsg ---
|
||||
usermessages: chat sending live (SayText2 resolved, SayText resolved)
|
||||
```
|
||||
|
||||
Final profiler:
|
||||
|
||||
```text
|
||||
engine GameFrame 13848 2.77ms 4.19ms 8.39ms 39.34ms 38.34s
|
||||
timer_test timer 6 71.5us 131.1us 131.1us 101.4us 429.0us
|
||||
chat_test chat 3 97.0us 131.1us 131.1us 127.4us 290.9us
|
||||
core SchemaFindField 3 13.2us 8.2us 32.8us 31.7us 39.5us
|
||||
```
|
||||
|
||||
## Final Server State
|
||||
|
||||
Docker:
|
||||
|
||||
```text
|
||||
/gp_c2b47a90 running running=true exit=0 oom=false
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
Panel DB:
|
||||
|
||||
```text
|
||||
uuid | name | status | slug | port
|
||||
----------+----------------+---------+--------------+-------
|
||||
c2b47a90 | Test Sunucusu | running | cs2 | 27015
|
||||
d8f13411 | MukemmelSunucu | stopped | satisfactory | 7777
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- No Conduit/Metamod/signal-specific load failure or crash was observed.
|
||||
- Server console contained ordinary CS2/Steam/map warnings and warmup long-frame messages.
|
||||
- No watchdog stall block appeared during this run.
|
||||
- CS2 panel status was `stopped` after Docker restart; I updated only the CS2 row back to `running`. Satisfactory remained `stopped`.
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
# Conduit Linux Bring-up Report - 2026-06-13
|
||||
|
||||
Target server:
|
||||
- CS2 container: `gp_c2b47a90`
|
||||
- CS2 server UUID: `c2b47a90`
|
||||
- CS2 name: `Test Sunucusu`
|
||||
- Satisfactory container: `gp_d8f13411` (left untouched)
|
||||
|
||||
Artifacts copied next to this report:
|
||||
- `conduit-20260613.log`
|
||||
- `conduit-crash-20260613-113458.txt`
|
||||
|
||||
Final service state after crash test recovery:
|
||||
|
||||
```text
|
||||
/gp_c2b47a90 running running=true exit=0 oom=false
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
Panel DB state after recovery:
|
||||
|
||||
```text
|
||||
uuid | name | status | slug | port
|
||||
----------+----------------+---------+--------------+-------
|
||||
c2b47a90 | Test Sunucusu | running | cs2 | 27015
|
||||
d8f13411 | MukemmelSunucu | stopped | satisfactory | 7777
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
## 1. Build Result
|
||||
|
||||
Final build succeeded using GCC 12 in `build/linux-gcc12`.
|
||||
|
||||
Final link output:
|
||||
|
||||
```text
|
||||
[27/27] Linking CXX shared library package/addons/conduit/bin/conduit.so
|
||||
```
|
||||
|
||||
Build/deploy issues encountered and fixed:
|
||||
- Ubuntu `clang++` was 14.0.0 and rejected `-std=c++23`.
|
||||
- `hl2sdk` headers needed POSIX aliases for `stricmp`/`strcmpi` during this Linux build.
|
||||
- `crash_handler_linux.cpp` needed `cstdarg` visible for `va_start`/`va_end`; handled via build flag.
|
||||
- `watchdog.cpp` had a Linux compile error because glibc `SIGRTMIN` is runtime, not constexpr. Local source change:
|
||||
|
||||
```diff
|
||||
-constexpr int kSampleSignal = SIGRTMIN + 4;
|
||||
+const int kSampleSignal = SIGRTMIN + 4;
|
||||
```
|
||||
|
||||
- First deployed build failed to load in Metamod with protobuf ABI mismatch:
|
||||
|
||||
```text
|
||||
[META] Failed to load plugin addons/conduit/bin/conduit: /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so: undefined symbol: _ZNK6google8protobuf7Message11GetTypeNameB5cxx11Ev
|
||||
```
|
||||
|
||||
Rebuilt with `_GLIBCXX_USE_CXX11_ABI=0`; Conduit then loaded successfully.
|
||||
|
||||
Final configure command used:
|
||||
|
||||
```sh
|
||||
cmake -S /root/codex/Conduit -B /root/codex/Conduit/build/linux-gcc12 -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12 \
|
||||
"-DCMAKE_CXX_FLAGS=-include strings.h -include cstdarg -Dstricmp=strcasecmp -Dstrcmpi=strcasecmp -D_GLIBCXX_USE_CXX11_ABI=0"
|
||||
```
|
||||
|
||||
## 2. meta list
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
```
|
||||
|
||||
## 3. conduit_status + conduit_prof
|
||||
|
||||
```text
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 3384.6 s
|
||||
game frames : 215969
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260613.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 215971 2.36ms 4.19ms 4.19ms 34.46ms 510.40s
|
||||
stall_test_plugin conduit_stall_test 1 1.50s 2.15s 2.15s 1.50s 1.50s
|
||||
core SchemaFindField 2 98.4us 262.1us 262.1us 164.3us 196.7us
|
||||
```
|
||||
|
||||
After the deliberate crash test and restart, Conduit loaded again:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 130.2 s
|
||||
game frames : 7760
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260613.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
```
|
||||
|
||||
## 4. conduit_schema
|
||||
|
||||
```text
|
||||
--- conduit_schema CCSPlayerPawn m_iHealth ---
|
||||
CCSPlayerPawn::m_iHealth -> offset 0x5B0 (1456), type int32
|
||||
--- conduit_schema CBaseEntity m_iTeamNum ---
|
||||
CBaseEntity::m_iTeamNum -> offset 0x624 (1572), type uint8
|
||||
```
|
||||
|
||||
Comparison with Windows reference from the bring-up doc:
|
||||
- `CCSPlayerPawn::m_iHealth`: Linux `0x5B0`, Windows expected `0x2D0` -> mismatch.
|
||||
- `CBaseEntity::m_iTeamNum`: Linux `0x624`, Windows expected `0x344` -> mismatch.
|
||||
|
||||
## 5. conduit_stall_test 1500 Watchdog Block
|
||||
|
||||
RCON command:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_stall_test 1500 ---
|
||||
[Conduit] stalling the game thread for 1500 ms...
|
||||
```
|
||||
|
||||
Conduit log block:
|
||||
|
||||
```text
|
||||
2026-06-13 10:35:31 [WARN] watchdog: game thread has not advanced a frame for 591 ms (outside GameFrame)
|
||||
2026-06-13 10:35:31 [WARN] watchdog: context 1: owner=stall_test_plugin callsite=conduit_stall_test
|
||||
2026-06-13 10:35:31 [WARN] watchdog: >>> most likely culprit: 'stall_test_plugin' (in 'conduit_stall_test')
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #00 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xc95c9) [0x7fdf9e69e5c9]
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #01 /lib/x86_64-linux-gnu/libc.so.6(+0x3c050) [0x7fdfefa51050]
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #02 /lib/x86_64-linux-gnu/libc.so.6(clock_nanosleep+0x65) [0x7fdfefae4545]
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #03 /lib/x86_64-linux-gnu/libc.so.6(nanosleep+0x13) [0x7fdfefae8e53]
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #04 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xbf703) [0x7fdf9e694703]
|
||||
2026-06-13 10:35:31 [WARN] watchdog: #05 /home/steam/cs2-dedicated/game/bin/linuxsteamrt64/libtier0.so(+0x1e0902) [0x7fdfef4ca902]
|
||||
2026-06-13 10:35:32 [INFO] watchdog: game thread recovered after a ~1586 ms stall
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Attribution worked.
|
||||
- Stack sample landed.
|
||||
- Symbol quality is partial: `libconduit.so(+0x...)` offsets are present, but not function names such as `conduit_stall_test`.
|
||||
|
||||
## 6. conduit_crash_test Report
|
||||
|
||||
RCON command result:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_crash_test ---
|
||||
(no response: connection closed)
|
||||
```
|
||||
|
||||
Generated file:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/addons/conduit/crashes/conduit-crash-20260613-113458.txt
|
||||
```
|
||||
|
||||
Crash report content:
|
||||
|
||||
```text
|
||||
=== CONDUIT CRASH REPORT ==========================================
|
||||
Framework : Conduit 0.1.0-dev
|
||||
Time : 2026-06-13 11:34:58 (local)
|
||||
Process : pid=36
|
||||
Signal : 11 (Segmentation fault), fault address (nil)
|
||||
|
||||
--- ATTRIBUTION ---------------------------------------------------
|
||||
1. owner=crash_test_plugin callsite=conduit_crash_test
|
||||
>>> Most likely culprit: 'crash_test_plugin' (in 'conduit_crash_test')
|
||||
|
||||
--- STACK TRACE ---------------------------------------------------
|
||||
#00 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xbfb64) [0x7fdf9e694b64]
|
||||
#01 /lib/x86_64-linux-gnu/libc.so.6(+0x3c050) [0x7fdfefa51050]
|
||||
#02 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xbf66c) [0x7fdf9e69466c]
|
||||
#03 /home/steam/cs2-dedicated/game/bin/linuxsteamrt64/libtier0.so(+0x1e0902) [0x7fdfef4ca902]
|
||||
|
||||
--- RECENT EVENTS (oldest first) ----------------------------------
|
||||
[-1767.736s] [INFO] perf: heartbeat: 1921 frames, avg 2.22 ms, max 20.85 ms (last 30 s)
|
||||
[-1737.735s] [INFO] perf: heartbeat: 1920 frames, avg 2.16 ms, max 5.31 ms (last 30 s)
|
||||
[-1707.720s] [INFO] perf: heartbeat: 1921 frames, avg 2.12 ms, max 4.39 ms (last 30 s)
|
||||
[-1677.719s] [INFO] perf: heartbeat: 1920 frames, avg 2.20 ms, max 5.59 ms (last 30 s)
|
||||
[-1647.706s] [INFO] perf: heartbeat: 1921 frames, avg 2.39 ms, max 23.56 ms (last 30 s)
|
||||
[-1617.705s] [INFO] perf: heartbeat: 1920 frames, avg 2.71 ms, max 7.46 ms (last 30 s)
|
||||
[-1587.691s] [INFO] perf: heartbeat: 1921 frames, avg 2.37 ms, max 8.58 ms (last 30 s)
|
||||
[-1557.680s] [INFO] perf: heartbeat: 1921 frames, avg 2.33 ms, max 4.84 ms (last 30 s)
|
||||
[-1527.679s] [INFO] perf: heartbeat: 1920 frames, avg 2.25 ms, max 5.09 ms (last 30 s)
|
||||
[-1497.674s] [INFO] perf: heartbeat: 1920 frames, avg 2.18 ms, max 19.43 ms (last 30 s)
|
||||
[-1467.673s] [INFO] perf: heartbeat: 1920 frames, avg 2.30 ms, max 6.87 ms (last 30 s)
|
||||
[-1437.669s] [INFO] perf: heartbeat: 1920 frames, avg 2.23 ms, max 34.46 ms (last 30 s)
|
||||
[-1407.668s] [INFO] perf: heartbeat: 1920 frames, avg 2.45 ms, max 6.17 ms (last 30 s)
|
||||
[-1377.653s] [INFO] perf: heartbeat: 1921 frames, avg 2.46 ms, max 6.25 ms (last 30 s)
|
||||
[-1347.653s] [INFO] perf: heartbeat: 1920 frames, avg 2.53 ms, max 5.44 ms (last 30 s)
|
||||
[-1317.644s] [INFO] perf: heartbeat: 1921 frames, avg 2.51 ms, max 20.77 ms (last 30 s)
|
||||
[-1287.631s] [INFO] perf: heartbeat: 1921 frames, avg 2.46 ms, max 6.45 ms (last 30 s)
|
||||
[-1257.614s] [INFO] perf: heartbeat: 1921 frames, avg 2.57 ms, max 5.90 ms (last 30 s)
|
||||
[-1227.600s] [INFO] perf: heartbeat: 1921 frames, avg 2.54 ms, max 5.79 ms (last 30 s)
|
||||
[-1197.583s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 6.94 ms (last 30 s)
|
||||
[-1167.578s] [INFO] perf: heartbeat: 1920 frames, avg 2.46 ms, max 20.36 ms (last 30 s)
|
||||
[-1137.562s] [INFO] perf: heartbeat: 1921 frames, avg 2.47 ms, max 7.86 ms (last 30 s)
|
||||
[-1107.548s] [INFO] perf: heartbeat: 1921 frames, avg 2.53 ms, max 6.38 ms (last 30 s)
|
||||
[-1077.547s] [INFO] perf: heartbeat: 1920 frames, avg 2.54 ms, max 4.09 ms (last 30 s)
|
||||
[-1047.543s] [INFO] perf: heartbeat: 1920 frames, avg 2.61 ms, max 33.69 ms (last 30 s)
|
||||
[-1017.532s] [INFO] perf: heartbeat: 1921 frames, avg 2.44 ms, max 5.97 ms (last 30 s)
|
||||
[- 987.516s] [INFO] perf: heartbeat: 1921 frames, avg 2.59 ms, max 5.71 ms (last 30 s)
|
||||
[- 957.501s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 7.43 ms (last 30 s)
|
||||
[- 927.501s] [INFO] perf: heartbeat: 1920 frames, avg 2.60 ms, max 9.17 ms (last 30 s)
|
||||
[- 897.490s] [INFO] perf: heartbeat: 1921 frames, avg 2.56 ms, max 24.84 ms (last 30 s)
|
||||
[- 867.475s] [INFO] perf: heartbeat: 1921 frames, avg 2.58 ms, max 6.20 ms (last 30 s)
|
||||
[- 837.475s] [INFO] perf: heartbeat: 1920 frames, avg 2.74 ms, max 5.57 ms (last 30 s)
|
||||
[- 807.475s] [INFO] perf: heartbeat: 1920 frames, avg 2.71 ms, max 6.15 ms (last 30 s)
|
||||
[- 777.460s] [INFO] perf: heartbeat: 1921 frames, avg 2.70 ms, max 5.47 ms (last 30 s)
|
||||
[- 747.449s] [INFO] perf: heartbeat: 1920 frames, avg 2.68 ms, max 24.22 ms (last 30 s)
|
||||
[- 717.434s] [INFO] perf: heartbeat: 1921 frames, avg 2.80 ms, max 8.64 ms (last 30 s)
|
||||
[- 687.419s] [INFO] perf: heartbeat: 1921 frames, avg 2.65 ms, max 5.28 ms (last 30 s)
|
||||
[- 657.418s] [INFO] perf: heartbeat: 1920 frames, avg 2.66 ms, max 6.34 ms (last 30 s)
|
||||
[- 627.416s] [INFO] perf: heartbeat: 1920 frames, avg 2.51 ms, max 31.89 ms (last 30 s)
|
||||
[- 597.416s] [INFO] perf: heartbeat: 1920 frames, avg 2.61 ms, max 6.72 ms (last 30 s)
|
||||
[- 567.400s] [INFO] perf: heartbeat: 1921 frames, avg 2.67 ms, max 6.53 ms (last 30 s)
|
||||
[- 537.384s] [INFO] perf: heartbeat: 1921 frames, avg 2.66 ms, max 6.66 ms (last 30 s)
|
||||
[- 507.369s] [INFO] perf: heartbeat: 1921 frames, avg 2.55 ms, max 5.30 ms (last 30 s)
|
||||
[- 477.365s] [INFO] perf: heartbeat: 1921 frames, avg 2.50 ms, max 34.05 ms (last 30 s)
|
||||
[- 447.364s] [INFO] perf: heartbeat: 1920 frames, avg 2.59 ms, max 5.42 ms (last 30 s)
|
||||
[- 417.349s] [INFO] perf: heartbeat: 1921 frames, avg 2.66 ms, max 6.55 ms (last 30 s)
|
||||
[- 387.334s] [INFO] perf: heartbeat: 1921 frames, avg 2.34 ms, max 4.63 ms (last 30 s)
|
||||
[- 357.334s] [INFO] perf: heartbeat: 1920 frames, avg 2.77 ms, max 6.65 ms (last 30 s)
|
||||
[- 327.332s] [INFO] perf: heartbeat: 1920 frames, avg 2.74 ms, max 26.14 ms (last 30 s)
|
||||
[- 312.170s] detour 'detour_self_test' installed at 0x7fdf9e693cd0
|
||||
[- 312.170s] [INFO] hooks: detour 'detour_self_test' installed at 0x7fdf9e693cd0
|
||||
[- 312.169s] detour 'detour_self_test' removed
|
||||
[- 312.169s] [INFO] hooks: detour 'detour_self_test' removed
|
||||
[- 297.318s] [INFO] perf: heartbeat: 1921 frames, avg 2.73 ms, max 6.96 ms (last 30 s)
|
||||
[- 267.308s] [INFO] perf: heartbeat: 1920 frames, avg 2.58 ms, max 6.68 ms (last 30 s)
|
||||
[- 237.308s] [INFO] perf: heartbeat: 1920 frames, avg 2.68 ms, max 7.08 ms (last 30 s)
|
||||
[- 207.296s] [INFO] perf: heartbeat: 1921 frames, avg 2.50 ms, max 25.68 ms (last 30 s)
|
||||
[- 177.293s] [INFO] perf: heartbeat: 1920 frames, avg 2.51 ms, max 5.12 ms (last 30 s)
|
||||
[- 147.280s] [INFO] perf: heartbeat: 1921 frames, avg 2.65 ms, max 5.80 ms (last 30 s)
|
||||
[- 117.280s] [INFO] perf: heartbeat: 1920 frames, avg 2.64 ms, max 6.24 ms (last 30 s)
|
||||
[- 87.265s] [INFO] perf: heartbeat: 1921 frames, avg 2.78 ms, max 5.95 ms (last 30 s)
|
||||
[- 57.263s] [INFO] perf: heartbeat: 1920 frames, avg 2.66 ms, max 31.12 ms (last 30 s)
|
||||
[- 27.248s] [INFO] perf: heartbeat: 1921 frames, avg 2.72 ms, max 7.34 ms (last 30 s)
|
||||
[- 0.001s] [WARN] core: conduit_crash_test invoked — crashing deliberately
|
||||
===================================================================
|
||||
```
|
||||
|
||||
## 7. Conduit Log File
|
||||
|
||||
Original log file:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/addons/conduit/logs/conduit-20260613.log
|
||||
```
|
||||
|
||||
Copied artifact:
|
||||
|
||||
```text
|
||||
/root/codex/source-gamepanel/conduit-bringup-artifacts/conduit-20260613.log
|
||||
```
|
||||
|
||||
Relevant load lines:
|
||||
|
||||
```text
|
||||
2026-06-13 10:33:21 [INFO] gamedata: 1 files loaded: 0 signatures, 0 offsets, all valid
|
||||
2026-06-13 10:33:21 [INFO] core: Conduit 0.1.0-dev loaded — crash reporter armed, profiler on
|
||||
2026-06-13 10:33:29 [INFO] core: first GameFrame observed — hook dispatch confirmed
|
||||
```
|
||||
|
||||
## 8. Console / Metamod / Signal Warnings
|
||||
|
||||
Important console findings:
|
||||
|
||||
1. Before the ABI rebuild, Metamod failed to load Conduit:
|
||||
|
||||
```text
|
||||
[META] Failed to load plugin addons/conduit/bin/conduit: /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so: undefined symbol: _ZNK6google8protobuf7Message11GetTypeNameB5cxx11Ev
|
||||
[META] Loaded 0 plugins.
|
||||
```
|
||||
|
||||
This was fixed by rebuilding with `_GLIBCXX_USE_CXX11_ABI=0`.
|
||||
|
||||
2. After the successful build, no current Metamod load failure was observed. `meta list` reports Conduit loaded.
|
||||
|
||||
3. Signal/watchdog behavior:
|
||||
- No signal clash symptoms observed.
|
||||
- No `(stack sample timed out)` observed.
|
||||
- Stack sample appeared, but symbol names are mostly raw offsets for `libconduit.so`.
|
||||
|
||||
4. Crash test console tail showed the expected deliberate crash:
|
||||
|
||||
```text
|
||||
post-hook: noop
|
||||
entry.sh: line 192: 36 Segmentation fault (core dumped) ./cs2 -dedicated -ip 0.0.0.0 -port 27015 -console -usercon -maxplayers 16 +game_type 0 +game_mode 1 +mapgroup mg_active +map de_dust2 +rcon_password changeme +sv_lan 0 +tv_port 27020 -insecure
|
||||
```
|
||||
|
||||
Other engine/game warnings seen, not clearly Conduit-specific:
|
||||
|
||||
```text
|
||||
Failed loading resource "maps/prefabs/misc/terrorist_team_intro_variant2/world_visibility.vvis_c" (ERROR_FILEOPEN: File not found)
|
||||
Failed to write backup_round15.txt!
|
||||
UNEXPECTED LONG FRAME DETECTED
|
||||
```
|
||||
|
||||
## Additional Findings
|
||||
|
||||
`conduit_dumpbytes` found `server!CreateInterface`:
|
||||
|
||||
```text
|
||||
[Conduit] server!CreateInterface @ 0x7fdfaa682220
|
||||
55 48 89 E5 41 55 49 89 F5 41 54 53 48 83 EC 08
|
||||
```
|
||||
|
||||
But scanning the same pattern failed:
|
||||
|
||||
```text
|
||||
[Conduit] no match in server (285312 bytes scanned)
|
||||
```
|
||||
|
||||
This looks like a Linux memscan bug or module range issue: export lookup can locate `CreateInterface`, but `ScanPattern` does not find the bytes at that address.
|
||||
|
||||
`conduit_detour_test` passed:
|
||||
|
||||
```text
|
||||
[Conduit] detour test: PASS (before=42 during=43 after=42; expected 42/43/42)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
# Conduit Linux Console/ConVar Re-run - 2026-06-14
|
||||
|
||||
## Scope
|
||||
|
||||
- Pulled latest Conduit commits from `origin/main`.
|
||||
- Inspected the dynamic console command / convar diff and `docs/linux-bringup.md` test steps.
|
||||
- Rebuilt on Linux with GCC 12.
|
||||
- Deployed to CS2 server `Test Sunucusu`.
|
||||
- Verified dynamic command registration, dynamic command dispatch under profiler scope, plugin convar read/write, engine convar read/write, owner cleanup, and final server state.
|
||||
- Satisfactory server was not modified.
|
||||
|
||||
## Git
|
||||
|
||||
Latest commits:
|
||||
|
||||
```text
|
||||
bb58461 (HEAD -> main, origin/main, origin/HEAD) Phase 2: dynamic console commands + convar access
|
||||
a3bd08f Close out Linux verification of the Entity + Player slice
|
||||
de4bda2 Phase 2: schema-backed Entity + Player access, with event bridge
|
||||
4b79d33 Close out Linux verification of the Phase 2 GameEvents slice
|
||||
a5c1c13 docs: Linux re-test steps for the Phase 2 GameEvents slice
|
||||
ffb57f3 Phase 2 start: dispatch core + GameEvents, with an RTTI vtable finder
|
||||
```
|
||||
|
||||
Pulled range:
|
||||
|
||||
```text
|
||||
Updating de4bda2..bb58461
|
||||
Fast-forward
|
||||
PLAN.md | 4 +-
|
||||
core/CMakeLists.txt | 1 +
|
||||
core/src/conduit/commands.cpp | 52 ++++++++
|
||||
core/src/conduit/console.cpp | 302 ++++++++++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/console.h | 78 +++++++++++
|
||||
core/src/plugin.cpp | 5 +
|
||||
docs/linux-bringup.md | 58 ++++++--
|
||||
7 files changed, 491 insertions(+), 9 deletions(-)
|
||||
create mode 100644 core/src/conduit/console.cpp
|
||||
create mode 100644 core/src/conduit/console.h
|
||||
```
|
||||
|
||||
Working tree after the run:
|
||||
|
||||
```text
|
||||
## main...origin/main
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Command:
|
||||
|
||||
```sh
|
||||
cmake -S /root/codex/Conduit -B /root/codex/Conduit/build/linux-rerun -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12
|
||||
cmake --build /root/codex/Conduit/build/linux-rerun
|
||||
```
|
||||
|
||||
Result: build and link succeeded.
|
||||
|
||||
```text
|
||||
-- Configuring done
|
||||
-- Generating done
|
||||
-- Build files have been written to: /root/codex/Conduit/build/linux-rerun
|
||||
[1/4] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/commands.cpp.o
|
||||
[2/4] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/console.cpp.o
|
||||
[3/4] Building CXX object core/CMakeFiles/conduit.dir/src/plugin.cpp.o
|
||||
[4/4] Linking CXX shared library package/addons/conduit/bin/conduit.so
|
||||
```
|
||||
|
||||
Only warning observed: the existing volatile compound-assignment warning in `commands.cpp`.
|
||||
|
||||
No failed CMake/Ninja output exists because the build passed. The Linux link path for `CConVar` / `ConVarRefAbstract` symbols passed.
|
||||
|
||||
## Deploy
|
||||
|
||||
Package source:
|
||||
|
||||
```text
|
||||
/root/codex/Conduit/build/linux-rerun/package/.
|
||||
```
|
||||
|
||||
Destination:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/
|
||||
```
|
||||
|
||||
CS2 container restarted:
|
||||
|
||||
```text
|
||||
gp_c2b47a90
|
||||
```
|
||||
|
||||
Satisfactory was not touched:
|
||||
|
||||
```text
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
## Load Check
|
||||
|
||||
RCON:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 47.4 s
|
||||
game frames : 2251
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
```
|
||||
|
||||
Startup log:
|
||||
|
||||
```text
|
||||
2026-06-14 14:19:24 [INFO] gamedata: 1 files loaded: 0 signatures, 0 offsets, all valid
|
||||
2026-06-14 14:19:25 [INFO] entity: armed via CGameEntitySystem vtable 0x7f4c8640ffd8 - entity system not created yet, will resolve on first use after map load
|
||||
2026-06-14 14:19:25 [INFO] gameevents: armed via CGameEventManager vtable 0x7f4c86410f50 - instance captured on next LoadEventsFromFile
|
||||
2026-06-14 14:19:25 [INFO] core: Conduit 0.1.0-dev loaded - crash reporter armed, profiler on
|
||||
2026-06-14 14:19:25 [INFO] gameevents: game event manager connected (0x7f4c86790460) - event listening live
|
||||
2026-06-14 14:19:36 [INFO] core: first GameFrame observed - hook dispatch confirmed
|
||||
```
|
||||
|
||||
Result: plugin loaded, frames advanced, no Conduit/Metamod load failure.
|
||||
|
||||
## Dynamic Command and Profiler
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_console
|
||||
conduit_demo alpha bravo charlie
|
||||
conduit_prof
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_console ---
|
||||
[Conduit] [INFO] console: command 'conduit_demo' registered (owner 'console_demo')
|
||||
[Conduit] [INFO] console: convar 'conduit_demo_value' created (owner 'console_demo')
|
||||
console: ready, 1 command(s), 1 plugin convar(s)
|
||||
cmd conduit_demo owner=console_demo
|
||||
cvar conduit_demo_value owner=console_demo value=7
|
||||
--- conduit_demo alpha bravo charlie ---
|
||||
[Conduit] [INFO] console_demo: conduit_demo invoked: argc=4 args='alpha bravo charlie' issuer=server(-1)
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 3409 2.53ms 4.19ms 8.39ms 47.48ms 8.62s
|
||||
console_demo conduit_demo 1 63.1us 65.5us 65.5us 63.1us 63.1us
|
||||
```
|
||||
|
||||
Result: dynamic command dispatch works and is profiled under `console_demo / conduit_demo`.
|
||||
|
||||
## ConVar Tests
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_cvar conduit_demo_value
|
||||
conduit_cvar conduit_demo_value 42
|
||||
conduit_cvar conduit_demo_value
|
||||
conduit_cvar sv_cheats
|
||||
conduit_cvar sv_gravity
|
||||
conduit_cvar sv_gravity 700
|
||||
conduit_cvar sv_gravity
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_cvar conduit_demo_value ---
|
||||
[Conduit] conduit_demo_value = 7 (int=7 float=7.000 bool=1)
|
||||
--- conduit_cvar conduit_demo_value 42 ---
|
||||
[Conduit] set 'conduit_demo_value' = '42'
|
||||
[Conduit] conduit_demo_value = 42 (int=42 float=42.000 bool=1)
|
||||
--- conduit_cvar conduit_demo_value ---
|
||||
[Conduit] conduit_demo_value = 42 (int=42 float=42.000 bool=1)
|
||||
--- conduit_cvar sv_cheats ---
|
||||
[Conduit] sv_cheats = false (int=0 float=0.000 bool=0)
|
||||
--- conduit_cvar sv_gravity ---
|
||||
[Conduit] sv_gravity = 800.000000 (int=800 float=800.000 bool=1)
|
||||
--- conduit_cvar sv_gravity 700 ---
|
||||
L 06/14/2026 - 14:20:43: server_cvar: "sv_gravity" "700.000000"
|
||||
[Conduit] set 'sv_gravity' = '700'
|
||||
[Conduit] sv_gravity = 700.000000 (int=700 float=700.000 bool=1)
|
||||
--- conduit_cvar sv_gravity ---
|
||||
[Conduit] sv_gravity = 700.000000 (int=700 float=700.000 bool=1)
|
||||
```
|
||||
|
||||
Result: plugin-owned convar read/write passed. Engine convar read/write passed.
|
||||
|
||||
`sv_gravity` was restored:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_cvar sv_gravity 800 ---
|
||||
L 06/14/2026 - 14:20:56: server_cvar: "sv_gravity" "800.000000"
|
||||
[Conduit] set 'sv_gravity' = '800'
|
||||
[Conduit] sv_gravity = 800.000000 (int=800 float=800.000 bool=1)
|
||||
--- conduit_cvar sv_gravity ---
|
||||
[Conduit] sv_gravity = 800.000000 (int=800 float=800.000 bool=1)
|
||||
```
|
||||
|
||||
## Owner Cleanup
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_concmd_stop
|
||||
conduit_demo x
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
--- conduit_concmd_stop ---
|
||||
[Conduit] removed 2 demo registration(s)
|
||||
--- conduit_demo x ---
|
||||
|
||||
```
|
||||
|
||||
Result: `conduit_demo x` produced no handler line after cleanup, so the command was removed from the engine.
|
||||
|
||||
I then called `conduit_console` again to probe the persistence/adopt path:
|
||||
|
||||
```text
|
||||
--- conduit_console ---
|
||||
[Conduit] [INFO] console: command 'conduit_demo' registered (owner 'console_demo')
|
||||
[Conduit] [INFO] console: convar 'conduit_demo_value' already registered - adopting (owner 'console_demo')
|
||||
console: ready, 1 command(s), 0 plugin convar(s)
|
||||
cmd conduit_demo owner=console_demo
|
||||
```
|
||||
|
||||
The persisted convar value remained available:
|
||||
|
||||
```text
|
||||
--- conduit_cvar conduit_demo_value ---
|
||||
[Conduit] conduit_demo_value = 42 (int=42 float=42.000 bool=1)
|
||||
```
|
||||
|
||||
Final cleanup after the re-register:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_cvar conduit_demo_value ---
|
||||
[Conduit] conduit_demo_value = 42 (int=42 float=42.000 bool=1)
|
||||
--- conduit_concmd_stop ---
|
||||
[Conduit] removed 1 demo registration(s)
|
||||
--- conduit_demo x ---
|
||||
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 115.6 s
|
||||
game frames : 6615
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 6617 2.34ms 4.19ms 8.39ms 47.48ms 15.50s
|
||||
console_demo conduit_demo 1 63.1us 65.5us 65.5us 63.1us 63.1us
|
||||
```
|
||||
|
||||
## Finding
|
||||
|
||||
The adopt path works for name-based access: `conduit_demo_value` stayed at `42` and could be read after re-registration.
|
||||
|
||||
Potential mismatch: after adoption, `conduit_console` reported `0 plugin convar(s)` and did not list `conduit_demo_value`, despite logging `already registered - adopting`. That means the existing convar is not represented in `g_convars`, so owner cleanup removed only the command on the second cleanup (`removed 1 demo registration(s)`). If the intended behavior is "adopted convars are tracked and listed as plugin-owned", this needs a small fix. If "adopt" only means "treat existing engine convar as accessible by name", the runtime behavior is consistent.
|
||||
|
||||
## Final State
|
||||
|
||||
Docker:
|
||||
|
||||
```text
|
||||
/gp_c2b47a90 running running=true exit=0 oom=false
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
Panel DB:
|
||||
|
||||
```text
|
||||
uuid | name | status | slug | port
|
||||
----------+----------------+---------+--------------+-------
|
||||
c2b47a90 | Test Sunucusu | running | cs2 | 27015
|
||||
d8f13411 | MukemmelSunucu | stopped | satisfactory | 7777
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
No Conduit/Metamod/signal-specific load failure or crash was observed. Server console had ordinary CS2/Steam/map warnings only.
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# Conduit Linux Entity Re-run - 2026-06-14
|
||||
|
||||
## Scope
|
||||
|
||||
- Pulled latest Conduit commits from `origin/main`.
|
||||
- Inspected the Phase 2 entity/event bridge diff and updated `docs/linux-bringup.md` steps.
|
||||
- Rebuilt on Linux with GCC 12.
|
||||
- Deployed to CS2 server `Test Sunucusu`.
|
||||
- Verified normal startup arming, lazy entity-system resolution, Linux RTTI vtable lookup, event-to-entity bridge, schema-backed health read/write, cleanup, and final server state.
|
||||
- Satisfactory server was not modified.
|
||||
|
||||
## Git
|
||||
|
||||
Latest commits:
|
||||
|
||||
```text
|
||||
de4bda2 (HEAD -> main, origin/main, origin/HEAD) Phase 2: schema-backed Entity + Player access, with event bridge
|
||||
4b79d33 Close out Linux verification of the Phase 2 GameEvents slice
|
||||
a5c1c13 docs: Linux re-test steps for the Phase 2 GameEvents slice
|
||||
ffb57f3 Phase 2 start: dispatch core + GameEvents, with an RTTI vtable finder
|
||||
4078ab7 PLAN: close the Linux memscan question — M1 fully done on both platforms
|
||||
```
|
||||
|
||||
Pulled range:
|
||||
|
||||
```text
|
||||
Updating a5c1c13..de4bda2
|
||||
Fast-forward
|
||||
PLAN.md | 4 +-
|
||||
core/CMakeLists.txt | 1 +
|
||||
core/src/conduit/commands.cpp | 61 ++++++-
|
||||
core/src/conduit/entity.cpp | 380 ++++++++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/entity.h | 89 ++++++++++
|
||||
core/src/conduit/gameevents.cpp | 9 +
|
||||
core/src/conduit/gameevents.h | 9 +
|
||||
core/src/conduit/memscan.cpp | 52 ++++++
|
||||
core/src/conduit/memscan.h | 6 +
|
||||
core/src/plugin.cpp | 5 +
|
||||
docs/linux-bringup.md | 53 +++++-
|
||||
```
|
||||
|
||||
Working tree after run:
|
||||
|
||||
```text
|
||||
## main...origin/main
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Command:
|
||||
|
||||
```sh
|
||||
cmake -S /root/codex/Conduit -B /root/codex/Conduit/build/linux-rerun -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12
|
||||
cmake --build /root/codex/Conduit/build/linux-rerun
|
||||
```
|
||||
|
||||
Result: build succeeded.
|
||||
|
||||
```text
|
||||
-- Configuring done
|
||||
-- Generating done
|
||||
-- Build files have been written to: /root/codex/Conduit/build/linux-rerun
|
||||
[1/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/memscan.cpp.o
|
||||
[2/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/commands.cpp.o
|
||||
/root/codex/Conduit/core/src/conduit/commands.cpp: In function ‘int {anonymous}::DetourTestTarget(int)’:
|
||||
/root/codex/Conduit/core/src/conduit/commands.cpp:338:13: warning: compound assignment with ‘volatile’-qualified left operand is deprecated [-Wvolatile]
|
||||
338 | acc += x;
|
||||
| ~~~~^~~~
|
||||
[3/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/gamedata.cpp.o
|
||||
[4/7] Building CXX object core/CMakeFiles/conduit.dir/src/plugin.cpp.o
|
||||
[5/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/entity.cpp.o
|
||||
[6/7] Building CXX object core/CMakeFiles/conduit.dir/src/conduit/gameevents.cpp.o
|
||||
[7/7] Linking CXX shared library package/addons/conduit/bin/conduit.so
|
||||
```
|
||||
|
||||
No failed CMake/Ninja output exists because the build passed.
|
||||
|
||||
## Deploy
|
||||
|
||||
Package source:
|
||||
|
||||
```text
|
||||
/root/codex/Conduit/build/linux-rerun/package/.
|
||||
```
|
||||
|
||||
Destination:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/
|
||||
```
|
||||
|
||||
CS2 container restarted:
|
||||
|
||||
```text
|
||||
gp_c2b47a90
|
||||
```
|
||||
|
||||
Satisfactory container state during deploy:
|
||||
|
||||
```text
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
## Load Check
|
||||
|
||||
RCON:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 45.8 s
|
||||
game frames : 2220
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
```
|
||||
|
||||
Startup log:
|
||||
|
||||
```text
|
||||
2026-06-14 13:48:50 [INFO] gamedata: 1 files loaded: 0 signatures, 0 offsets, all valid
|
||||
2026-06-14 13:48:50 [INFO] entity: armed via CGameEntitySystem vtable 0x7f2eed759fd8 — entity system not created yet, will resolve on first use after map load
|
||||
2026-06-14 13:48:51 [INFO] gameevents: armed via CGameEventManager vtable 0x7f2eed75af50 — instance captured on next LoadEventsFromFile
|
||||
2026-06-14 13:48:51 [INFO] core: Conduit 0.1.0-dev loaded — crash reporter armed, profiler on
|
||||
2026-06-14 13:48:51 [INFO] gameevents: game event manager connected (0x7f2eedada460) — event listening live
|
||||
2026-06-14 13:49:01 [INFO] core: first GameFrame observed — hook dispatch confirmed
|
||||
```
|
||||
|
||||
Result: normal startup did not crash. Entity acquisition armed at load and deferred resolution until map/entity system was available.
|
||||
|
||||
## Entity RTTI and Lazy Resolve
|
||||
|
||||
RCON:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_vtable server CGameEntitySystem ---
|
||||
[Conduit] CGameEntitySystem vtable @ 0x7f2eed759fd8 first method -> 0x7f2eec92aec0
|
||||
--- conduit_entity ---
|
||||
[Conduit] [INFO] entity: entity system connected (0x7f2ee9124000) via GameResourceServiceServerV001+0x50 — vtable matches CGameEntitySystem
|
||||
entity system: connected
|
||||
slot 0 cs_player_controller team=2 pawn=player health=100
|
||||
slot 1 cs_player_controller team=3 pawn=player health=100
|
||||
```
|
||||
|
||||
Result: Linux Itanium RTTI path found `CGameEntitySystem`. Lazy resource-service scan found the entity system at `GameResourceServiceServerV001+0x50`. This differs from Windows `+0x58`, as expected, and was found by vtable matching.
|
||||
|
||||
## Event to Entity Bridge
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_event_stop
|
||||
conduit_event_listen player_spawn
|
||||
mp_warmup_end
|
||||
mp_restartgame 1
|
||||
```
|
||||
|
||||
RCON / console-reported Conduit lines:
|
||||
|
||||
```text
|
||||
[Conduit] [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (pre)
|
||||
[Conduit] [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (post)
|
||||
[Conduit] listening to 'player_spawn' (pre+post) — watch the log
|
||||
[Conduit] [INFO] event_test: [pre] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
[Conduit] [INFO] event_test: [post] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
[Conduit] [INFO] event_test: [pre] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
[Conduit] [INFO] event_test: [post] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
```
|
||||
|
||||
Conduit log:
|
||||
|
||||
```text
|
||||
2026-06-14 13:50:11 [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (pre)
|
||||
2026-06-14 13:50:11 [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (post)
|
||||
2026-06-14 13:50:11 [INFO] event_test: [pre] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
2026-06-14 13:50:11 [INFO] event_test: [post] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
2026-06-14 13:50:11 [INFO] event_test: [pre] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
2026-06-14 13:50:11 [INFO] event_test: [post] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
2026-06-14 13:50:12 [INFO] event_test: [pre] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
2026-06-14 13:50:12 [INFO] event_test: [post] player_spawn userid=0 pawn=player idx=256 health=100 team=2
|
||||
2026-06-14 13:50:12 [INFO] event_test: [pre] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
2026-06-14 13:50:12 [INFO] event_test: [post] player_spawn userid=1 pawn=player idx=260 health=100 team=3
|
||||
```
|
||||
|
||||
Result: event carried player reference resolved to live pawn entities in both pre and post phases. Schema-backed reads returned health/team.
|
||||
|
||||
## Health Read / Write
|
||||
|
||||
RCON:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_ent_health 0 ---
|
||||
[Conduit] slot 0 pawn=player health=100 team=2
|
||||
--- conduit_ent_health 0 45 ---
|
||||
[Conduit] slot 0 health 100 -> 45 (read back 45; networked write)
|
||||
--- conduit_ent_health 0 ---
|
||||
[Conduit] slot 0 pawn=player health=45 team=2
|
||||
```
|
||||
|
||||
Cleanup / restore:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_ent_health 0 100 ---
|
||||
[Conduit] slot 0 health 45 -> 100 (read back 100; networked write)
|
||||
--- conduit_ent_health 0 ---
|
||||
[Conduit] slot 0 pawn=player health=100 team=2
|
||||
--- conduit_event_stop ---
|
||||
[Conduit] removed 2 test subscription(s)
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 0 subscription(s), 1 event(s) registered, FireEvent detour off
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 6851 2.44ms 4.19ms 8.39ms 33.34ms 16.74s
|
||||
event_test player_spawn 8 42.6us 65.5us 131.1us 77.7us 341.2us
|
||||
core SchemaFindField 3 12.7us 8.2us 32.8us 29.3us 38.0us
|
||||
```
|
||||
|
||||
Result: schema-backed setter wrote `m_iHealth`, readback returned the written value, then health was restored to 100.
|
||||
|
||||
## Final State
|
||||
|
||||
RCON:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 162.5 s
|
||||
game frames : 9648
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 0 subscription(s), 1 event(s) registered, FireEvent detour off
|
||||
```
|
||||
|
||||
Docker:
|
||||
|
||||
```text
|
||||
/gp_c2b47a90 running running=true exit=0 oom=false
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
Panel DB:
|
||||
|
||||
```text
|
||||
uuid | name | status | slug | port
|
||||
----------+----------------+---------+--------------+-------
|
||||
c2b47a90 | Test Sunucusu | running | cs2 | 27015
|
||||
d8f13411 | MukemmelSunucu | stopped | satisfactory | 7777
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
## Odd Warnings / Notes
|
||||
|
||||
No Conduit/Metamod/signal load failure or crash was observed.
|
||||
|
||||
The `conduit_vtable` / `conduit_entity` window produced one watchdog sample:
|
||||
|
||||
```text
|
||||
2026-06-14 13:49:57 [WARN] watchdog: game thread has not advanced a frame for 521 ms (outside GameFrame)
|
||||
2026-06-14 13:49:57 [WARN] watchdog: no Conduit context on the game thread — the stall is in engine/game code or an untracked path
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #00 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcff99) [0x7f2ee4b0ef99]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #01 /lib/x86_64-linux-gnu/libc.so.6(+0x3c050) [0x7f2f31cc2050]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #02 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcd020) [0x7f2ee4b0c020]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #03 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcd160) [0x7f2ee4b0c160]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #04 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcd523) [0x7f2ee4b0c523]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #05 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcd60f) [0x7f2ee4b0c60f]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #06 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xcdecd) [0x7f2ee4b0cecd]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #07 /home/steam/cs2-dedicated/game/csgo/addons/conduit/bin/conduit.so(+0xc10d5) [0x7f2ee4b000d5]
|
||||
2026-06-14 13:49:57 [WARN] watchdog: #08 /home/steam/cs2-dedicated/game/bin/linuxsteamrt64/libtier0.so(+0x1e0902) [0x7f2f3173b902]
|
||||
2026-06-14 13:49:57 [INFO] entity: entity system connected (0x7f2ee9124000) via GameResourceServiceServerV001+0x50 — vtable matches CGameEntitySystem
|
||||
2026-06-14 13:49:57 [INFO] watchdog: game thread recovered after a ~816 ms stall
|
||||
```
|
||||
|
||||
This looks tied to the diagnostic RTTI/entity scan command window. The server recovered and continued through later event and health tests.
|
||||
|
||||
Server console also printed ordinary CS2 map/resource/round warnings such as missing lightmap resources and `Failed to write backup_round00.txt!`; I did not see those tied to Conduit/Metamod/signal failure.
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
# Conduit Linux GameEvents Re-run - 2026-06-14
|
||||
|
||||
## Scope
|
||||
|
||||
- Pulled latest Conduit commits from `origin/main`.
|
||||
- Inspected the new Phase 2 dispatch/GameEvents diff.
|
||||
- Rebuilt clean on Linux with GCC 12.
|
||||
- Deployed the resulting package to CS2 server `Test Sunucusu`.
|
||||
- Verified Linux RTTI vtable lookup, GameEvent manager connection, pre/post dispatch, cancellation, profiler accounting, and cleanup.
|
||||
- Did not touch the Satisfactory server files/container.
|
||||
|
||||
## Git
|
||||
|
||||
Latest commits after pull:
|
||||
|
||||
```text
|
||||
a5c1c13 (HEAD -> main, origin/main, origin/HEAD) docs: Linux re-test steps for the Phase 2 GameEvents slice
|
||||
ffb57f3 Phase 2 start: dispatch core + GameEvents, with an RTTI vtable finder
|
||||
4078ab7 PLAN: close the Linux memscan question - M1 fully done on both platforms
|
||||
```
|
||||
|
||||
Diff summary inspected:
|
||||
|
||||
```text
|
||||
PLAN.md | 4 +-
|
||||
core/CMakeLists.txt | 1 +
|
||||
core/src/conduit/commands.cpp | 85 ++++++++++++
|
||||
core/src/conduit/gameevents.cpp | 294 ++++++++++++++++++++++++++++++++++++++++
|
||||
core/src/conduit/gameevents.h | 73 ++++++++++
|
||||
core/src/conduit/memscan.cpp | 237 ++++++++++++++++++++++++++++++--
|
||||
core/src/conduit/memscan.h | 7 +
|
||||
core/src/plugin.cpp | 5 +
|
||||
docs/linux-bringup.md | 24 ++++
|
||||
```
|
||||
|
||||
Working tree after the run:
|
||||
|
||||
```text
|
||||
## main...origin/main
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Build command used:
|
||||
|
||||
```sh
|
||||
cmake -S /root/codex/Conduit -B /root/codex/Conduit/build/linux-rerun -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12
|
||||
cmake --build /root/codex/Conduit/build/linux-rerun
|
||||
```
|
||||
|
||||
Result: build succeeded.
|
||||
|
||||
Final build line:
|
||||
|
||||
```text
|
||||
[30/30] Linking CXX shared library package/addons/conduit/bin/conduit.so
|
||||
```
|
||||
|
||||
Warnings only:
|
||||
|
||||
```text
|
||||
- protobuf syntax warnings
|
||||
- paths.cpp snprintf truncation warnings
|
||||
- commands.cpp volatile compound assignment deprecated
|
||||
- log.cpp snprintf truncation warning
|
||||
- C-only warning for C++ flags on Zydis.c
|
||||
```
|
||||
|
||||
No full cmake/ninja failure output exists because the build did not fail.
|
||||
|
||||
## Deploy
|
||||
|
||||
Deployed package:
|
||||
|
||||
```text
|
||||
/root/codex/Conduit/build/linux-rerun/package/.
|
||||
```
|
||||
|
||||
Destination:
|
||||
|
||||
```text
|
||||
/var/lib/gamepanel/servers/c2b47a90/game/csgo/
|
||||
```
|
||||
|
||||
Server restarted:
|
||||
|
||||
```text
|
||||
gp_c2b47a90
|
||||
```
|
||||
|
||||
Satisfactory server was not modified.
|
||||
|
||||
## Initial Load
|
||||
|
||||
`meta list` and `conduit_status` after deploy:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- meta list ---
|
||||
Listing 1 plugin:
|
||||
[01] Conduit (0.1.0-dev) by Conduit Contributors
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 88.9 s
|
||||
game frames : 5066
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
```
|
||||
|
||||
Startup log lines:
|
||||
|
||||
```text
|
||||
2026-06-14 11:34:50 [INFO] gamedata: 1 files loaded: 0 signatures, 0 offsets, all valid
|
||||
2026-06-14 11:34:50 [INFO] gameevents: armed via CGameEventManager vtable 0x7f47c7ae2f50 — instance captured on next LoadEventsFromFile
|
||||
2026-06-14 11:34:50 [INFO] core: Conduit 0.1.0-dev loaded — crash reporter armed, profiler on
|
||||
2026-06-14 11:34:50 [INFO] gameevents: game event manager connected (0x7f47c7e62460) — event listening live
|
||||
2026-06-14 11:35:00 [INFO] core: first GameFrame observed — hook dispatch confirmed
|
||||
```
|
||||
|
||||
## RTTI Vtable Checks
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_vtable server CGameEventManager
|
||||
conduit_vtable server CCSPlayerController
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_vtable server CGameEventManager ---
|
||||
[Conduit] CGameEventManager vtable @ 0x7f47c7ae2f50 first method -> 0x7f47c6d0aa80
|
||||
--- conduit_vtable server CCSPlayerController ---
|
||||
[Conduit] CCSPlayerController vtable @ 0x7f47c7ace5a8 first method -> 0x7f47c6b169a0
|
||||
```
|
||||
|
||||
Result: Linux Itanium RTTI lookup passed for both classes.
|
||||
|
||||
## Listen Test
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_event_stop
|
||||
conduit_event_listen player_spawn
|
||||
mp_restartgame 1
|
||||
```
|
||||
|
||||
RCON output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_event_stop ---
|
||||
[Conduit] removed 0 test subscription(s)
|
||||
--- conduit_event_listen player_spawn ---
|
||||
[Conduit] [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (pre)
|
||||
[Conduit] [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (post)
|
||||
[Conduit] listening to 'player_spawn' (pre+post) — watch the log
|
||||
--- mp_restartgame 1 ---
|
||||
```
|
||||
|
||||
Log evidence:
|
||||
|
||||
```text
|
||||
2026-06-14 11:55:18 [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (pre)
|
||||
2026-06-14 11:55:18 [INFO] gameevents: 'event_test' subscribed to 'player_spawn' (post)
|
||||
2026-06-14 11:55:20 [INFO] event_test: [pre] player_spawn userid=0 attacker=-1
|
||||
2026-06-14 11:55:20 [INFO] event_test: [post] player_spawn userid=0 attacker=-1
|
||||
2026-06-14 11:55:20 [INFO] event_test: [pre] player_spawn userid=1 attacker=-1
|
||||
2026-06-14 11:55:20 [INFO] event_test: [post] player_spawn userid=1 attacker=-1
|
||||
```
|
||||
|
||||
`conduit_events` and `conduit_prof` after listen:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 2 subscription(s), 1 event(s) registered, FireEvent detour on
|
||||
event_test player_spawn pre hits=2
|
||||
event_test player_spawn post hits=2
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 80323 2.28ms 4.19ms 4.19ms 27.49ms 183.11s
|
||||
event_test player_spawn 4 38.1us 65.5us 65.5us 54.9us 152.6us
|
||||
```
|
||||
|
||||
Result: pre and post dispatch both fired.
|
||||
|
||||
## Cancel Test
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_event_cancel player_spawn
|
||||
mp_restartgame 1
|
||||
```
|
||||
|
||||
RCON output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_event_cancel player_spawn ---
|
||||
[Conduit] [INFO] gameevents: 'event_cancel_test' subscribed to 'player_spawn' (pre)
|
||||
[Conduit] will cancel every 'player_spawn' until conduit_event_stop
|
||||
--- mp_restartgame 1 ---
|
||||
```
|
||||
|
||||
Log evidence:
|
||||
|
||||
```text
|
||||
2026-06-14 12:02:21 [INFO] gameevents: 'event_cancel_test' subscribed to 'player_spawn' (pre)
|
||||
2026-06-14 12:02:22 [INFO] event_test: [pre] player_spawn userid=0 attacker=-1
|
||||
2026-06-14 12:02:22 [WARN] event_test: cancelling 'player_spawn' (pre)
|
||||
2026-06-14 12:02:22 [INFO] event_test: [pre] player_spawn userid=1 attacker=-1
|
||||
2026-06-14 12:02:22 [WARN] event_test: cancelling 'player_spawn' (pre)
|
||||
```
|
||||
|
||||
There were no new `[post] player_spawn` lines after cancellation.
|
||||
|
||||
`conduit_events` and `conduit_prof` after cancel:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 3 subscription(s), 1 event(s) registered, FireEvent detour on
|
||||
event_test player_spawn pre hits=4
|
||||
event_test player_spawn post hits=2
|
||||
event_cancel_test player_spawn pre hits=2
|
||||
--- conduit_prof ---
|
||||
owner callsite calls avg p50 p99 max total
|
||||
engine GameFrame 107531 2.25ms 4.19ms 4.19ms 32.27ms 242.28s
|
||||
event_test player_spawn 6 47.1us 65.5us 131.1us 76.9us 282.5us
|
||||
event_cancel_test player_spawn 2 14.5us 16.4us 16.4us 14.9us 28.9us
|
||||
```
|
||||
|
||||
Result: cancel path passed; post stayed at `hits=2` while pre advanced.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
conduit_event_stop
|
||||
conduit_events
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_event_stop ---
|
||||
[Conduit] removed 3 test subscription(s)
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 0 subscription(s), 1 event(s) registered, FireEvent detour off
|
||||
```
|
||||
|
||||
Final status:
|
||||
|
||||
```text
|
||||
rcon: authenticated
|
||||
--- conduit_status ---
|
||||
Conduit 0.1.0-dev
|
||||
uptime : 2228.6 s
|
||||
game frames : 141868
|
||||
log file : /home/steam/cs2-dedicated/game/csgo/addons/conduit/logs/conduit-20260614.log
|
||||
crash report: armed (use conduit_crash_test to verify)
|
||||
--- conduit_events ---
|
||||
game events: manager connected, 0 subscription(s), 1 event(s) registered, FireEvent detour off
|
||||
```
|
||||
|
||||
## Container and Panel State
|
||||
|
||||
Docker:
|
||||
|
||||
```text
|
||||
/gp_c2b47a90 running running=true exit=0 oom=false
|
||||
/gp_d8f13411 exited running=false exit=130 oom=false
|
||||
```
|
||||
|
||||
Panel DB:
|
||||
|
||||
```text
|
||||
uuid | name | status | slug | port
|
||||
----------+----------------+---------+--------------+-------
|
||||
c2b47a90 | Test Sunucusu | running | cs2 | 27015
|
||||
d8f13411 | MukemmelSunucu | stopped | satisfactory | 7777
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
## Odd Warnings
|
||||
|
||||
During the RTTI/vtable/memscan command window, the log briefly reported watchdog stalls outside Conduit context:
|
||||
|
||||
```text
|
||||
2026-06-14 11:50:54 [WARN] watchdog: game thread has not advanced a frame for 509 ms (outside GameFrame)
|
||||
2026-06-14 11:50:54 [WARN] watchdog: no Conduit context on the game thread — the stall is in engine/game code or an untracked path
|
||||
2026-06-14 11:51:04 [WARN] watchdog: game thread has not advanced a frame for 585 ms (outside GameFrame)
|
||||
2026-06-14 11:51:04 [WARN] watchdog: no Conduit context on the game thread — the stall is in engine/game code or an untracked path
|
||||
```
|
||||
|
||||
The server recovered and continued through 141k+ frames. I did not see Conduit/Metamod/signal load failures or crashes in this run.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
INSERT INTO "games" (
|
||||
"slug",
|
||||
"name",
|
||||
"docker_image",
|
||||
"default_port",
|
||||
"config_files",
|
||||
"automation_rules",
|
||||
"startup_command",
|
||||
"stop_command",
|
||||
"environment_vars",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
VALUES (
|
||||
'satisfactory',
|
||||
'Satisfactory',
|
||||
'wolveix/satisfactory-server:latest',
|
||||
7777,
|
||||
'[]'::jsonb,
|
||||
'[]'::jsonb,
|
||||
'',
|
||||
'quit',
|
||||
'[
|
||||
{
|
||||
"key": "MAXPLAYERS",
|
||||
"default": "4",
|
||||
"description": "Maximum player count",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "STEAMBETA",
|
||||
"label": "Branch",
|
||||
"default": "false",
|
||||
"description": "Use the experimental branch instead of stable",
|
||||
"required": false,
|
||||
"inputType": "boolean",
|
||||
"enabledLabel": "Experimental",
|
||||
"disabledLabel": "Stable"
|
||||
},
|
||||
{
|
||||
"key": "AUTOSAVENUM",
|
||||
"default": "5",
|
||||
"description": "Number of rotating autosaves",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "MAXTICKRATE",
|
||||
"default": "30",
|
||||
"description": "Maximum simulation tick rate",
|
||||
"required": false
|
||||
}
|
||||
]'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT ("slug") DO UPDATE
|
||||
SET
|
||||
"name" = EXCLUDED."name",
|
||||
"docker_image" = EXCLUDED."docker_image",
|
||||
"default_port" = EXCLUDED."default_port",
|
||||
"config_files" = EXCLUDED."config_files",
|
||||
"automation_rules" = EXCLUDED."automation_rules",
|
||||
"startup_command" = EXCLUDED."startup_command",
|
||||
"stop_command" = EXCLUDED."stop_command",
|
||||
"environment_vars" = EXCLUDED."environment_vars",
|
||||
"updated_at" = now();
|
||||
@@ -50,6 +50,13 @@
|
||||
"when": 1772900000000,
|
||||
"tag": "0006_cs2_servername_branding",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1773000000000,
|
||||
"tag": "0007_satisfactory_game",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createDb } from './client';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { games } from './schema/games';
|
||||
import { users } from './schema/users';
|
||||
|
||||
@@ -292,8 +293,18 @@ async function seed() {
|
||||
description: 'Steam Workshop map id to launch',
|
||||
required: false,
|
||||
},
|
||||
{ key: 'CS2_GAMETYPE', default: '0', description: 'Game type numeric value', required: false },
|
||||
{ key: 'CS2_GAMEMODE', default: '1', description: 'Game mode numeric value', required: false },
|
||||
{
|
||||
key: 'CS2_GAMETYPE',
|
||||
default: '0',
|
||||
description: 'Game type numeric value',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'CS2_GAMEMODE',
|
||||
default: '1',
|
||||
description: 'Game mode numeric value',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'CS2_ADDITIONAL_ARGS',
|
||||
default: '',
|
||||
@@ -340,7 +351,12 @@ async function seed() {
|
||||
],
|
||||
environmentVars: [
|
||||
{ key: 'EULA', default: 'TRUE', description: 'Accept Minecraft EULA', required: true },
|
||||
{ key: 'VERSION', default: 'LATEST', description: 'Bedrock server version', required: true },
|
||||
{
|
||||
key: 'VERSION',
|
||||
default: 'LATEST',
|
||||
description: 'Bedrock server version',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -377,15 +393,138 @@ async function seed() {
|
||||
stopCommand: 'quit',
|
||||
configFiles: [],
|
||||
environmentVars: [
|
||||
{ key: 'RUST_SERVER_NAME', default: 'My Rust Server', description: 'Server name', required: true },
|
||||
{ key: 'RUST_SERVER_MAXPLAYERS', default: '50', description: 'Max players', required: false },
|
||||
{ key: 'RUST_SERVER_IDENTITY', default: 'default', description: 'Server identity', required: false },
|
||||
{
|
||||
key: 'RUST_SERVER_NAME',
|
||||
default: 'My Rust Server',
|
||||
description: 'Server name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'RUST_SERVER_MAXPLAYERS',
|
||||
default: '50',
|
||||
description: 'Max players',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'RUST_SERVER_IDENTITY',
|
||||
default: 'default',
|
||||
description: 'Server identity',
|
||||
required: false,
|
||||
},
|
||||
{ key: 'RUST_RCON_PASSWORD', default: '', description: 'RCON password', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'satisfactory',
|
||||
name: 'Satisfactory',
|
||||
dockerImage: 'wolveix/satisfactory-server:latest',
|
||||
defaultPort: 7777,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
configFiles: [],
|
||||
automationRules: [],
|
||||
environmentVars: [
|
||||
{
|
||||
key: 'MAXPLAYERS',
|
||||
default: '4',
|
||||
description: 'Maximum player count',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'STEAMBETA',
|
||||
label: 'Branch',
|
||||
default: 'false',
|
||||
description: 'Use the experimental branch instead of stable',
|
||||
required: false,
|
||||
inputType: 'boolean',
|
||||
enabledLabel: 'Experimental',
|
||||
disabledLabel: 'Stable',
|
||||
},
|
||||
{
|
||||
key: 'AUTOSAVENUM',
|
||||
default: '5',
|
||||
description: 'Number of rotating autosaves',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'MAXTICKRATE',
|
||||
default: '30',
|
||||
description: 'Maximum simulation tick rate',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'fivem',
|
||||
name: 'FiveM',
|
||||
dockerImage: 'spritsail/fivem:latest',
|
||||
defaultPort: 30120,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.cfg',
|
||||
parser: 'keyvalue',
|
||||
editableKeys: [
|
||||
'endpoint_add_tcp',
|
||||
'endpoint_add_udp',
|
||||
'sv_hostname',
|
||||
'sv_scriptHookAllowed',
|
||||
'rcon_password',
|
||||
'sv_endpointprivacy',
|
||||
'sv_maxclients',
|
||||
],
|
||||
},
|
||||
],
|
||||
automationRules: [],
|
||||
environmentVars: [
|
||||
{
|
||||
key: 'LICENSE_KEY',
|
||||
default: '',
|
||||
description: 'Cfx.re server license key required to start FXServer',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
.onConflictDoNothing();
|
||||
|
||||
await db
|
||||
.update(games)
|
||||
.set({
|
||||
name: 'FiveM',
|
||||
dockerImage: 'spritsail/fivem:latest',
|
||||
defaultPort: 30120,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.cfg',
|
||||
parser: 'keyvalue',
|
||||
editableKeys: [
|
||||
'endpoint_add_tcp',
|
||||
'endpoint_add_udp',
|
||||
'sv_hostname',
|
||||
'sv_scriptHookAllowed',
|
||||
'rcon_password',
|
||||
'sv_endpointprivacy',
|
||||
'sv_maxclients',
|
||||
],
|
||||
},
|
||||
],
|
||||
automationRules: [],
|
||||
environmentVars: [
|
||||
{
|
||||
key: 'LICENSE_KEY',
|
||||
default: '',
|
||||
description: 'Cfx.re server license key required to start FXServer',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(games.slug, 'fivem'));
|
||||
|
||||
console.log('Seed completed successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ message CreateDatabaseRequest {
|
||||
string password = 3;
|
||||
}
|
||||
|
||||
message ImportDatabaseSqlRequest {
|
||||
string database_name = 1;
|
||||
string sql = 2;
|
||||
}
|
||||
|
||||
message UpdateDatabasePasswordRequest {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
@@ -252,6 +257,7 @@ service DaemonService {
|
||||
rpc DeleteServer(ServerIdentifier) returns (Empty);
|
||||
rpc ReinstallServer(ServerIdentifier) returns (Empty);
|
||||
rpc CreateDatabase(CreateDatabaseRequest) returns (ManagedDatabaseCredentials);
|
||||
rpc ImportDatabaseSql(ImportDatabaseSqlRequest) returns (Empty);
|
||||
rpc UpdateDatabasePassword(UpdateDatabasePasswordRequest) returns (Empty);
|
||||
rpc DeleteDatabase(DeleteDatabaseRequest) returns (Empty);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user