/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/desktop/src/main/logging.ts
205 строк
6 KB
Luke Parker
feat(desktop): Add Export Logs (#26262)
21 май 2026, 07:55
Не верифицирован
21 май 2026, 07:55
bea3ca5
Код
Авторство
О чём код?
import { MainLogger } from "electron-log" import log from "electron-log/main.js" import { app, crashReporter, netLog, shell } from "electron" import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs" import { ZipWriter, BlobWriter, BlobReader } from "@zip.js/zip.js" import { dirname, join } from "node:path" import { homedir } from "node:os" const MAX_LOG_AGE_DAYS = 7 const TAIL_LINES = 1000 const EXPORT_WINDOW = 24 * 60 * 60 * 1000 const MAX_EXPORT_FILE_SIZE = 50 * 1024 * 1024 const NET_LOG_SIZE = 20 * 1024 * 1024 let root = "" let run = "" let netLogPath: string | undefined let logger: MainLogger export const getLogger = () => logger export function initLogging() { initRunDirectory() log.transports.file.maxSize = 5 * 1024 * 1024 log.transports.file.resolvePathFn = (_vars, message) => join( run, `${safeLogName(message?.scope ?? (message?.variables?.processType === "renderer" ? "renderer" : "main"))}.log`, ) log.initialize({ preload: false, spyRendererConsole: true }) initConsoleTransport() cleanup() return (logger = log) } export function initCrashReporter() { const dir = join(app.getPath("userData"), "Crashpad") mkdirSync(dir, { recursive: true }) app.setPath("crashDumps", dir) crashReporter.start({ uploadToServer: false, compress: true }) write("crash", "crash reporter started", { path: dir }) } export async function startNetLog() { if (netLog.currentlyLogging) return netLogPath = join(run, "network.netlog") await netLog.startLogging(netLogPath, { captureMode: "default", maxFileSize: NET_LOG_SIZE }) write("network", "net log started", { path: netLogPath }) } export async function exportDebugLogs() { const restartNetLog = netLog.currentlyLogging if (restartNetLog) { await netLog.stopLogging().catch((error) => write("network", "failed to stop net log", { error })) } const output = join(app.getPath("downloads"), `opencode-debug-${stamp()}.zip`) try { write("main", "exporting debug logs", { output }) await writeZip(output, [ { name: "manifest.json", data: Buffer.from(JSON.stringify(manifest(), null, 2)) }, ...collect(root, "desktop"), ...serverLogRoots().flatMap((dir, i) => collect(dir, `server-${i + 1}`)), ...collect(app.getPath("crashDumps"), "crashpad"), ]) shell.showItemInFolder(output) return output } finally { if (restartNetLog) { await startNetLog().catch((error) => write("network", "failed to restart net log", { error })) } } } export function write( name: string, message: string, extra?: Record<string, unknown>, level: "info" | "warn" | "error" = "info", ) { if (!run) return const scoped = log.scope(safeLogName(name)) if (extra !== undefined) { scoped[level](message, extra) return } scoped[level](message) } export function tail(): string { try { const path = log.transports.file.getFile().path const contents = readFileSync(path, "utf8") const lines = contents.split("\n") return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n") } catch { return "" } } function initRunDirectory() { root = join(app.getPath("userData"), "logs") run = join(root, stamp()) mkdirSync(run, { recursive: true }) } function stamp() { return new Date() .toISOString() .replace(/[-:]/g, "") .replace(/\.\d+Z$/, "") } function safeLogName(name: string) { return name.replace(/[^a-z0-9_.-]/gi, "_") || "main" } function cleanup() { const dir = root || dirname(log.transports.file.getFile().path) const cutoff = Date.now() - MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000 for (const entry of readdirSync(dir)) { const file = join(dir, entry) try { const info = statSync(file) if (info.mtimeMs < cutoff) rmSync(file, { recursive: true, force: true }) } catch { continue } } } function manifest() { return { generated: new Date().toISOString(), version: app.getVersion(), name: app.getName(), packaged: app.isPackaged, platform: process.platform, arch: process.arch, versions: process.versions, uptime: process.uptime(), userData: app.getPath("userData"), logs: root, currentRun: run, crashDumps: app.getPath("crashDumps"), serverLogs: serverLogRoots(), netLog: netLogPath, } } function serverLogRoots() { const xdgData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share") return [...new Set([join(xdgData, "opencode", "log"), join(app.getPath("userData"), "opencode", "log")])] } type Entry = { name: string; path?: string; data?: Buffer } function collect(dir: string, prefix: string): Entry[] { if (!existsSync(dir)) return [] const cutoff = Date.now() - EXPORT_WINDOW const result: Entry[] = [] const walk = (current: string) => { for (const entry of readdirSync(current)) { const file = join(current, entry) const info = statSync(file) if (info.isDirectory()) { walk(file) continue } if (info.mtimeMs < cutoff) continue if (info.size > MAX_EXPORT_FILE_SIZE) continue if (file.endsWith(".heapsnapshot")) continue result.push({ name: join(prefix, file.slice(dir.length + 1)).replace(/\\/g, "/"), path: file }) } } walk(dir) return result } async function writeZip(output: string, entries: Entry[]) { const writer = new ZipWriter(new BlobWriter("application/zip")) for (const entry of entries) { const data = entry.data ?? readFileSync(entry.path!) await writer.add(entry.name, new BlobReader(new Blob([new Uint8Array(data)]))) } const zip = await writer.close() writeFileSync(output, Buffer.from(await zip.arrayBuffer())) } function initConsoleTransport() { const write = log.transports.console.writeFn.bind(log.transports.console) log.transports.console.writeFn = (options) => { try { write(options) } catch (err) { if (!isBrokenPipe(err)) throw err log.transports.console.level = false } } } function isBrokenPipe(err: unknown) { return typeof err === "object" && err !== null && "code" in err && err.code === "EPIPE" }