Files
evening_detective_screen/electron/diagnostics.ts
T
2026-10-17 21:21:03 +07:00

99 lines
3.1 KiB
TypeScript

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<Record<Event, Record<string, readonly string[]>>> = {
'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<string, unknown> = {}) {
if (this.disabled || !events.includes(event)) return
const entry: Record<string, string> = { 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')
}
}
}