import { appendFileSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs' import { join } from 'node:path' type Event = | 'app-start' | 'app-stop' | 'startup-error' | 'settings-error' | 'invalid-report' | 'output-open' | 'output-close' | 'media-error' | 'connection' | 'package' const fields: Partial>> = { 'output-close': { reason: [ 'operator', 'display', 'power', 'fullscreen', 'renderer', 'document', 'geometry', 'unresponsive', ], }, 'media-error': { channel: ['main', 'music', 'scene'] }, connection: { status: ['disabled', 'waiting', 'connected', 'offline', 'expired'] }, package: { action: ['download', 'check', 'remove'], status: ['success', 'failed'] }, } const events: readonly string[] = [ 'app-start', 'app-stop', 'startup-error', 'settings-error', 'invalid-report', 'output-open', 'output-close', 'media-error', 'connection', 'package', ] export class DiagnosticLog { private disabled = false constructor( private directory: string, private limit = 1024 * 1024, ) {} record(event: Event, data: Record = {}) { if (this.disabled || !events.includes(event)) return const entry: Record = { time: new Date().toISOString(), event } // Только разрешённые значения: никаких Error.message, URL, токенов или состояния игры. for (const [key, allowed] of Object.entries(fields[event] ?? {})) { if (typeof data[key] !== 'string' || !allowed.includes(data[key])) return entry[key] = data[key] } if (event === 'app-start') { for (const key of ['version', 'electron'] as const) { if (typeof data[key] !== 'string' || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(data[key])) return entry[key] = data[key] } if ( !['linux', 'darwin', 'win32'].includes(String(data.platform)) || !['x64', 'arm64', 'ia32'].includes(String(data.arch)) ) return entry.platform = String(data.platform) entry.arch = String(data.arch) } try { mkdirSync(this.directory, { recursive: true, mode: 0o700 }) const path = join(this.directory, 'diagnostic.jsonl') const line = JSON.stringify(entry) + '\n' let size = 0 try { size = statSync(path).size } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error } if (size + Buffer.byteLength(line) > this.limit) { rmSync(join(this.directory, 'diagnostic.2.jsonl'), { force: true }) const previous = join(this.directory, 'diagnostic.1.jsonl') try { renameSync(previous, join(this.directory, 'diagnostic.2.jsonl')) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error } renameSync(path, previous) } appendFileSync(path, line, { mode: 0o600 }) } catch { this.disabled = true console.error('SCREEN_LOG_UNAVAILABLE') } } }