Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,192 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const pkg = require('./package.json');
|
||||
const db = require('./src/database');
|
||||
const ftp = require('./src/ftp');
|
||||
const { initializeDataPaths } = require('./src/platform/data-paths');
|
||||
const { readBuildInfo } = require('./src/platform/build-info');
|
||||
const { createLogger } = require('./src/platform/logger');
|
||||
const { SettingsStore } = require('./src/persistence/settings-store');
|
||||
const { SecretsStore } = require('./src/security/secrets-store');
|
||||
const { createRegistry } = require('./src/server/create-registry');
|
||||
const { createAppServer } = require('./src/server/app-server');
|
||||
|
||||
const appRoot = __dirname;
|
||||
const paths = initializeDataPaths();
|
||||
const logger = createLogger(paths.logs);
|
||||
const settings = new SettingsStore(path.join(paths.config, 'settings.json'));
|
||||
const keyFile = process.env.APP_SECRET_KEY_FILE || path.join(paths.secrets, 'master.key');
|
||||
const secrets = new SecretsStore({ filePath: path.join(paths.secrets, 'secrets.enc.json'), keyFile });
|
||||
|
||||
const capabilities = {
|
||||
platform: 'linux-server', language: { default: 'en', fallback: 'en' },
|
||||
features: {
|
||||
browserUi: { available: true, mode: 'native-web' }, ftp: { available: true },
|
||||
https: { available: true, mode: 'reverse-proxy', configured: Boolean(process.env.CHIMERA_PUBLIC_ORIGIN || process.env.PORKFOLIO_PUBLIC_ORIGIN) },
|
||||
gamesDatabase: { available: true }, transferEvents: { available: true, transport: 'websocket' },
|
||||
systemView: { available: true, mode: 'browser-media-devices', secureContextRequired: true },
|
||||
ffpkgConversion: { available: false, reason: 'Windows worker required.' },
|
||||
ffpfscConversion: { available: true, mode: 'native-worker' },
|
||||
exfatConversion: { available: false, reason: 'Windows worker required.' },
|
||||
arbitrarySqlWrite: { available: false, reason: 'SQL Explorer is read-only in Docker.' },
|
||||
shellAccess: { available: false, reason: 'General shell access is intentionally disabled.' },
|
||||
localGameLibraries: { available: true, roots: ['/games/ps4', '/games/ps5'], readOnly: true },
|
||||
elfArsenal: { available: true, requiresPayload: true, port: 6969 },
|
||||
payloadManager: { available: true, requiresPayload: true, port: 8084 },
|
||||
cheats: { available: true, mode: 'server-cache-and-ftp' },
|
||||
localSaveManager: { available: true, requiresPayload: true, port: 8082 },
|
||||
media: { available: true, mode: 'ftp-cache' },
|
||||
localBackups: { available: true, roots: ['/games/ps4', '/games/ps5', '/game-backups'] },
|
||||
backporkGenerator: { available: true, mode: 'native-worker', pupUnpacker: '/usr/local/bin/ps5-pup-unpacker' },
|
||||
bdjbGenerator: { available: true, mode: 'bundled-linux-sdk' },
|
||||
y2jbGenerator: { available: true, mode: 'server-download' },
|
||||
ecosystemHub: { available: true, mode: 'official-upstream-connectors' },
|
||||
compatibilityEngine: { available: true, automaticExecution: false },
|
||||
consoleHealth: { available: true, scheduled: true, notifications: 'optional-discord' },
|
||||
safeUpdatePlanner: { available: true, dependencyOrder: true, automaticExecution: false },
|
||||
payloadManagerRepository: { available: true, path: '/payloads.json', cors: true, checksums: true },
|
||||
websrvDiscovery: { available: true, endpoint: '/mdns' },
|
||||
homebrewHub: { available: true, requiresPayload: true, backend: 'ps5-payload-dev/websrv', port: 8080 },
|
||||
remoteSources: { available: true, protocols: ['smb', 'nfs', 'sftp', 'ftp', 'webdav', 'http', 'https'], credentialsEncrypted: true },
|
||||
etaHenProfiles: { available: true, snapshots: true, uploadVerification: true },
|
||||
etaHenPlugins: { available: true, usbPriorityAware: true },
|
||||
cheatsV2: { available: true, sources: ['etaHEN/PS5_Cheats', 'GoldHEN/GoldHEN_Cheat_Repository', 'illusionyy/PS-Game-Patch'] },
|
||||
saveVault: { available: true, backend: 'earthonion/garlic-savemgr', port: 8082 },
|
||||
consoleTimeline: { available: true, ports: [9081, 3232], expertShellProxy: false },
|
||||
gameUpdateCenter: { available: true, officialManifestHostsOnly: true, pieceAndPackageHashes: true, automaticInstall: false },
|
||||
payloadSupplyChain: { available: true, localRehash: true, officialSources: true, automaticExecution: false },
|
||||
liveSession: { available: true, backend: 'ps5debug-NG', readOnlyCommands: 5, memoryAccess: false, kernelAccess: false },
|
||||
websrvFileExplorer: { available: true, readOnly: true, fixedRoots: true, smbProfilesOnly: true },
|
||||
remotePlayWake: { available: true, backend: 'chiaki-ng-compatible', registrationKeyEncrypted: true },
|
||||
remotePlay: { available: true, mode: 'paired-native-client-worker', discovery: true, wake: true, sessionOrchestration: true, embeddedVideo: false, hmacWorker: true },
|
||||
itemzflowJobs: { available: true, mode: 'read-only-log-monitor', undocumentedCommands: false },
|
||||
backupReplication: { available: true, target: '/replication', sourceDeletion: false, sha256Verification: true, scheduledOptIn: true },
|
||||
prometheusMetrics: { available: true, path: '/metrics', bearerTokenRequired: true },
|
||||
sessionManagement: { available: true, revoke: true, passwordRotation: true },
|
||||
pacbrewCatalog: { available: true, mode: 'read-only-recipes', install: false },
|
||||
ps5LinuxLab: { available: true, mode: 'read-only-preflight', payloadSend: false, blockDeviceWrite: false },
|
||||
auditCenter: { available: true, filters: true, boundedRetention: true, authenticatedExport: true },
|
||||
recoveryVault: { available: true, schema: 2, sha256: true, externalImport: true, automaticRestore: false },
|
||||
shadowMountPreflight: { available: true, invalidConfigBlocked: true, criticalConfirmation: true },
|
||||
passiveKlog: { available: true, port: 3232, zeroWrite: true, payloadLaunch: false, explicitStart: true },
|
||||
},
|
||||
};
|
||||
|
||||
function backupDatabase() {
|
||||
const chimeraSource = path.join(paths.database, 'chimera.db');
|
||||
const legacySource = path.join(paths.database, 'porkfolio.db');
|
||||
const source = fs.existsSync(chimeraSource) ? chimeraSource : legacySource;
|
||||
if (!fs.existsSync(source)) return;
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
fs.copyFileSync(source, path.join(paths.backups, `chimera-${stamp}.db`));
|
||||
const retention = Math.max(1, Number(settings.get('backup.retention', 14)) || 14);
|
||||
const backups = fs.readdirSync(paths.backups).filter(name => /^chimera-.*\.db$/.test(name)).sort().reverse();
|
||||
for (const stale of backups.slice(retention)) fs.unlinkSync(path.join(paths.backups, stale));
|
||||
}
|
||||
|
||||
let appServer;
|
||||
let registryInstance;
|
||||
let shuttingDown = false;
|
||||
|
||||
function portReachable(host, port, timeoutMs = 1_500) {
|
||||
return new Promise(resolve => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
let settled = false;
|
||||
const finish = reachable => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
socket.destroy();
|
||||
resolve(reachable);
|
||||
};
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.once('connect', () => finish(true));
|
||||
socket.once('timeout', () => finish(false));
|
||||
socket.once('error', () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
async function autoConnectFtp() {
|
||||
if (!settings.get('autoConnect', true)) {
|
||||
logger.info('[FTP] Automatisch verbinden is uitgeschakeld');
|
||||
return { connected: false, reason: 'disabled' };
|
||||
}
|
||||
const host = String(settings.get('ftp.host', '') || '').trim();
|
||||
if (!host) {
|
||||
logger.warn('[FTP] Automatisch verbinden overgeslagen: PS5-host ontbreekt');
|
||||
return { connected: false, reason: 'host-missing' };
|
||||
}
|
||||
const configured = settings.get('ftp.ports', ['1337', '2121', '21']);
|
||||
const preferred = settings.get('ftp.port', '1337');
|
||||
const ports = [...new Set([preferred, ...(Array.isArray(configured) ? configured : [configured])]
|
||||
.map(Number)
|
||||
.filter(port => Number.isInteger(port) && port >= 1 && port <= 65535))];
|
||||
const errors = [];
|
||||
const reachability = new Map(await Promise.all(ports.map(async port => [port, await portReachable(host, port)])));
|
||||
for (const port of ports) {
|
||||
if (!reachability.get(port)) {
|
||||
errors.push({ port, error: 'TCP-poort niet bereikbaar' });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await ftp.connect({
|
||||
host,
|
||||
port,
|
||||
user: String(settings.get('ftp.user', '') || ''),
|
||||
password: String(secrets.get('ftp.password') || ''),
|
||||
});
|
||||
settings.set('ftp.port', String(port));
|
||||
logger.info('[FTP] PS5 automatisch verbonden', { host, port });
|
||||
return { connected: true, host, port };
|
||||
} catch (error) {
|
||||
errors.push({ port, error: error.message });
|
||||
}
|
||||
}
|
||||
logger.warn('[FTP] Automatisch verbinden met de PS5 mislukt', { host, attempts: errors });
|
||||
return { connected: false, reason: 'unreachable', host, attempts: errors };
|
||||
}
|
||||
|
||||
async function shutdown(signal) {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
logger.info('Graceful shutdown gestart', { signal });
|
||||
try { registryInstance?.maintenance?.stopOperations?.(); } catch (_) {}
|
||||
try { await registryInstance?.ps5P15A10Runtime?.stopEngine?.(); } catch (error) { logger.warn('[PS5Upload] diagnostische engine stoppen mislukt', { error: error.message }); }
|
||||
try { await registryInstance?.ps5ProfileRuntime?.stopEngine?.(); } catch (error) { logger.warn('[PS5Upload] productie-engine stoppen mislukt', { error: error.message }); }
|
||||
try { await ftp.disconnect(); } catch (_) {}
|
||||
try { db.close(); } catch (error) { logger.error('Database sluiten mislukt', error); }
|
||||
try { if (appServer) await appServer.close(); } catch (error) { logger.error('HTTP-server sluiten mislukt', error); }
|
||||
logger.info('Graceful shutdown voltooid');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
backupDatabase();
|
||||
db.configure({ dataDir: paths.database, logger });
|
||||
await db.initialize();
|
||||
const registry = createRegistry({ db, ftp, settings, secrets, paths, logger, capabilities, appRoot, broadcast: (channel, ...args) => appServer?.broadcast(channel, ...args) });
|
||||
registryInstance = registry;
|
||||
appServer = createAppServer({ appRoot, paths, settings, secrets, registry, logger, ftp, capabilities, version: pkg.version });
|
||||
const port = Number(process.env.PORT || 6967);
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
await appServer.listen(port, host);
|
||||
logger.info('Chimera server gestart', { host, port, dataRoot: paths.root, version: pkg.version, build: readBuildInfo() });
|
||||
try { await registry.ps5ProfileRuntime.startEngine(); }
|
||||
catch (error) { logger.warn('[PS5Upload] engine autostart geblokkeerd', { code: error.code, error: error.message }); }
|
||||
registry.maintenance.seedPayloadSources();
|
||||
if (settings.get('admin.passwordHash', '')) {
|
||||
await autoConnectFtp();
|
||||
registry.maintenance.startOperations();
|
||||
} else {
|
||||
logger.info('[Operations] Taakplanner wacht tot de eerste configuratie is voltooid');
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM').finally(() => process.exit(0)));
|
||||
process.on('SIGINT', () => shutdown('SIGINT').finally(() => process.exit(0)));
|
||||
process.on('uncaughtException', error => { logger.error('Uncaught exception', error); shutdown('uncaughtException').finally(() => process.exit(1)); });
|
||||
process.on('unhandledRejection', error => logger.error('Unhandled rejection', error));
|
||||
|
||||
if (require.main === module) main().catch(error => { logger.error('Serverstart mislukt', error); process.exitCode = 1; });
|
||||
module.exports = { main, shutdown, capabilities, autoConnectFtp, portReachable };
|
||||
Reference in new issue
Block a user