Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c6f34cd3 |
@@ -26,6 +26,24 @@ let dbBackupPath = '';
|
||||
|
||||
// SQLite files start with this 16-byte header string.
|
||||
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 {
|
||||
return applicationDataSource;
|
||||
@@ -87,18 +105,47 @@ function safeguardDbFile(): Uint8Array | undefined {
|
||||
* 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.
|
||||
*/
|
||||
async function atomicSave(data: Uint8Array): Promise<void> {
|
||||
async function replaceDatabaseFile(tmpPath: string): 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');
|
||||
|
||||
try {
|
||||
await fsp.writeFile(tmpPath, Buffer.from(data));
|
||||
await fsp.rename(tmpPath, dbFilePath);
|
||||
await fsp.writeFile(tmpPath, snapshot);
|
||||
await replaceDatabaseFile(tmpPath);
|
||||
} catch (err) {
|
||||
await fsp.unlink(tmpPath).catch(() => {});
|
||||
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> {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const dbDir = path.join(userDataPath, 'metoyou');
|
||||
|
||||
@@ -49,8 +49,25 @@ const DB_BACKUP = DB_FILE + '.bak';
|
||||
const DATA_DIR = path.dirname(DB_FILE);
|
||||
// SQLite files start with this 16-byte header string.
|
||||
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 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 {
|
||||
if (!fs.existsSync(DB_BACKUP)) {
|
||||
@@ -160,18 +177,47 @@ function resolveSqlJsConfig(): { locateFile: (file: string) => string } {
|
||||
* 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.
|
||||
*/
|
||||
async function atomicSave(data: Uint8Array): Promise<void> {
|
||||
async function replaceDatabaseFile(tmpPath: string): 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');
|
||||
|
||||
try {
|
||||
await fsp.writeFile(tmpPath, Buffer.from(data));
|
||||
await fsp.rename(tmpPath, DB_FILE);
|
||||
await fsp.writeFile(tmpPath, snapshot);
|
||||
await replaceDatabaseFile(tmpPath);
|
||||
} catch (err) {
|
||||
await fsp.unlink(tmpPath).catch(() => {});
|
||||
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 {
|
||||
if (!applicationDataSource?.isInitialized) {
|
||||
throw new Error('DataSource not initialised');
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from './broadcast';
|
||||
import { handleWebSocketMessage } from './handler';
|
||||
|
||||
type IncomingWebSocketMessage = Parameters<typeof handleWebSocketMessage>[1];
|
||||
|
||||
/** How often to ping all connected clients (ms). */
|
||||
const PING_INTERVAL_MS = 30_000;
|
||||
/** Maximum time a client can go without a pong before we consider it dead (ms). */
|
||||
@@ -89,12 +91,20 @@ export function setupWebSocket(server: Server<typeof IncomingMessage, typeof Ser
|
||||
});
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
let message: IncomingWebSocketMessage;
|
||||
|
||||
await handleWebSocketMessage(connectionId, message);
|
||||
try {
|
||||
message = JSON.parse(data.toString()) as IncomingWebSocketMessage;
|
||||
} catch (err) {
|
||||
console.error('Invalid WebSocket message:', err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await handleWebSocketMessage(connectionId, message);
|
||||
} catch (err) {
|
||||
console.error('WebSocket message handler failed:', err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -173,7 +173,8 @@ export class GameActivityService implements OnDestroy {
|
||||
const matchedGame = await this.matchRunningGame(processNames);
|
||||
|
||||
this.ngZone.run(() => this.applyMatchedGame(matchedGame));
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.warn('[GameActivity] Failed to scan running processes', error);
|
||||
return;
|
||||
} finally {
|
||||
this.scanInFlight = false;
|
||||
|
||||
Reference in New Issue
Block a user