1 Commits

Author SHA1 Message Date
Myx
53389ed3ad feat: Add game activity status (Experimental)
All checks were successful
Queue Release Build / prepare (push) Successful in 23s
Deploy Web Apps / deploy (push) Successful in 5m54s
Queue Release Build / build-windows (push) Successful in 16m19s
Queue Release Build / build-linux (push) Successful in 30m13s
Queue Release Build / finalize (push) Successful in 47s
2026-04-27 05:46:33 +02:00
4 changed files with 9 additions and 113 deletions

View File

@@ -26,24 +26,6 @@ let dbBackupPath = '';
// SQLite files start with this 16-byte header string. // SQLite files start with this 16-byte header string.
const SQLITE_MAGIC = 'SQLite format 3\0'; const SQLITE_MAGIC = 'SQLite format 3\0';
const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600];
const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
let saveQueue: Promise<void> = Promise.resolve();
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableSaveError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = (error as NodeJS.ErrnoException).code;
return typeof code === 'string' && RETRYABLE_SAVE_ERROR_CODES.has(code);
}
export function getDataSource(): DataSource | undefined { export function getDataSource(): DataSource | undefined {
return applicationDataSource; return applicationDataSource;
@@ -105,47 +87,18 @@ function safeguardDbFile(): Uint8Array | undefined {
* then rename it over the real file. rename() is atomic on the same * then rename it over the real file. rename() is atomic on the same
* filesystem, so a crash mid-write can never leave a half-written DB. * filesystem, so a crash mid-write can never leave a half-written DB.
*/ */
async function replaceDatabaseFile(tmpPath: string): Promise<void> { async function atomicSave(data: Uint8Array): Promise<void> {
for (let attempt = 0; ; attempt++) {
try {
await fsp.rename(tmpPath, dbFilePath);
return;
} catch (error) {
const delay = SAVE_RETRY_DELAYS_MS[attempt];
if (!isRetryableSaveError(error) || delay === undefined) {
throw error;
}
await wait(delay);
}
}
}
async function writeDatabaseSnapshot(snapshot: Buffer): Promise<void> {
const tmpPath = dbFilePath + '.tmp-' + randomBytes(6).toString('hex'); const tmpPath = dbFilePath + '.tmp-' + randomBytes(6).toString('hex');
try { try {
await fsp.writeFile(tmpPath, snapshot); await fsp.writeFile(tmpPath, Buffer.from(data));
await replaceDatabaseFile(tmpPath); await fsp.rename(tmpPath, dbFilePath);
} catch (err) { } catch (err) {
await fsp.unlink(tmpPath).catch(() => {}); await fsp.unlink(tmpPath).catch(() => {});
throw err; throw err;
} }
} }
async function atomicSave(data: Uint8Array): Promise<void> {
const snapshot = Buffer.from(data);
const saveTask = saveQueue.then(
() => writeDatabaseSnapshot(snapshot),
() => writeDatabaseSnapshot(snapshot)
);
saveQueue = saveTask.catch(() => {});
return saveTask;
}
export async function initializeDatabase(): Promise<void> { export async function initializeDatabase(): Promise<void> {
const userDataPath = app.getPath('userData'); const userDataPath = app.getPath('userData');
const dbDir = path.join(userDataPath, 'metoyou'); const dbDir = path.join(userDataPath, 'metoyou');

View File

@@ -49,25 +49,8 @@ const DB_BACKUP = DB_FILE + '.bak';
const DATA_DIR = path.dirname(DB_FILE); const DATA_DIR = path.dirname(DB_FILE);
// SQLite files start with this 16-byte header string. // SQLite files start with this 16-byte header string.
const SQLITE_MAGIC = 'SQLite format 3\0'; const SQLITE_MAGIC = 'SQLite format 3\0';
const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600];
const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
let applicationDataSource: DataSource | undefined; let applicationDataSource: DataSource | undefined;
let saveQueue: Promise<void> = Promise.resolve();
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableSaveError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = (error as NodeJS.ErrnoException).code;
return typeof code === 'string' && RETRYABLE_SAVE_ERROR_CODES.has(code);
}
function restoreFromBackup(reason: string): Uint8Array | undefined { function restoreFromBackup(reason: string): Uint8Array | undefined {
if (!fs.existsSync(DB_BACKUP)) { if (!fs.existsSync(DB_BACKUP)) {
@@ -177,47 +160,18 @@ function resolveSqlJsConfig(): { locateFile: (file: string) => string } {
* then rename it over the real file. rename() is atomic on the same * then rename it over the real file. rename() is atomic on the same
* filesystem, so a crash mid-write can never leave a half-written DB. * filesystem, so a crash mid-write can never leave a half-written DB.
*/ */
async function replaceDatabaseFile(tmpPath: string): Promise<void> { async function atomicSave(data: Uint8Array): Promise<void> {
for (let attempt = 0; ; attempt++) {
try {
await fsp.rename(tmpPath, DB_FILE);
return;
} catch (error) {
const delay = SAVE_RETRY_DELAYS_MS[attempt];
if (!isRetryableSaveError(error) || delay === undefined) {
throw error;
}
await wait(delay);
}
}
}
async function writeDatabaseSnapshot(snapshot: Buffer): Promise<void> {
const tmpPath = DB_FILE + '.tmp-' + randomBytes(6).toString('hex'); const tmpPath = DB_FILE + '.tmp-' + randomBytes(6).toString('hex');
try { try {
await fsp.writeFile(tmpPath, snapshot); await fsp.writeFile(tmpPath, Buffer.from(data));
await replaceDatabaseFile(tmpPath); await fsp.rename(tmpPath, DB_FILE);
} catch (err) { } catch (err) {
await fsp.unlink(tmpPath).catch(() => {}); await fsp.unlink(tmpPath).catch(() => {});
throw err; throw err;
} }
} }
async function atomicSave(data: Uint8Array): Promise<void> {
const snapshot = Buffer.from(data);
const saveTask = saveQueue.then(
() => writeDatabaseSnapshot(snapshot),
() => writeDatabaseSnapshot(snapshot)
);
saveQueue = saveTask.catch(() => {});
return saveTask;
}
export function getDataSource(): DataSource { export function getDataSource(): DataSource {
if (!applicationDataSource?.isInitialized) { if (!applicationDataSource?.isInitialized) {
throw new Error('DataSource not initialised'); throw new Error('DataSource not initialised');

View File

@@ -13,8 +13,6 @@ import {
} from './broadcast'; } from './broadcast';
import { handleWebSocketMessage } from './handler'; import { handleWebSocketMessage } from './handler';
type IncomingWebSocketMessage = Parameters<typeof handleWebSocketMessage>[1];
/** How often to ping all connected clients (ms). */ /** How often to ping all connected clients (ms). */
const PING_INTERVAL_MS = 30_000; const PING_INTERVAL_MS = 30_000;
/** Maximum time a client can go without a pong before we consider it dead (ms). */ /** Maximum time a client can go without a pong before we consider it dead (ms). */
@@ -91,20 +89,12 @@ export function setupWebSocket(server: Server<typeof IncomingMessage, typeof Ser
}); });
ws.on('message', async (data) => { ws.on('message', async (data) => {
let message: IncomingWebSocketMessage;
try { try {
message = JSON.parse(data.toString()) as IncomingWebSocketMessage; const message = JSON.parse(data.toString());
} catch (err) {
console.error('Invalid WebSocket message:', err);
return;
}
try {
await handleWebSocketMessage(connectionId, message); await handleWebSocketMessage(connectionId, message);
} catch (err) { } catch (err) {
console.error('WebSocket message handler failed:', err); console.error('Invalid WebSocket message:', err);
} }
}); });

View File

@@ -173,8 +173,7 @@ export class GameActivityService implements OnDestroy {
const matchedGame = await this.matchRunningGame(processNames); const matchedGame = await this.matchRunningGame(processNames);
this.ngZone.run(() => this.applyMatchedGame(matchedGame)); this.ngZone.run(() => this.applyMatchedGame(matchedGame));
} catch (error) { } catch {
console.warn('[GameActivity] Failed to scan running processes', error);
return; return;
} finally { } finally {
this.scanInFlight = false; this.scanInFlight = false;