106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import { NETWORK_ERROR_CODES } from './request-error.js'
|
|
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'
|
|
| 'request-error'
|
|
const fields: Partial<Record<Event, Record<string, readonly string[]>>> = {
|
|
'output-close': {
|
|
reason: [
|
|
'operator',
|
|
'display',
|
|
'power',
|
|
'fullscreen',
|
|
'renderer',
|
|
'document',
|
|
'geometry',
|
|
'unresponsive',
|
|
],
|
|
},
|
|
'request-error': {
|
|
path: ['/api/games/{id}', '/api/games/{id}/screen-plan', '/api/games/{id}/music', '/api/scenarios/{id}/full'],
|
|
reason: [...NETWORK_ERROR_CODES, 'AUTH', 'HTTP', 'INVALID_JSON', 'SESSION_CHANGED'],
|
|
},
|
|
'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',
|
|
'request-error',
|
|
]
|
|
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')
|
|
}
|
|
}
|
|
}
|