import fs from "node:fs"; import path from "node:path"; import AdmZip from "adm-zip"; import { APP_VERSION } from "./constants"; import { getAuditLogPath } from "./audit-log"; import { getDebugSetupCheck } from "./debug-setup"; import { getLogFilePath } from "./logger"; import { getPackageLogPath } from "./package-log"; import { getRenameLogPath } from "./rename-log"; import { getDesktopRenameLogPath } from "./desktop-rename-log"; import { getSessionLogPath } from "./session-log"; import { createStoragePaths, loadHistory, loadSettings } from "./storage"; import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data"; import { getTraceConfig, getTraceConfigPath, getTraceLogPath } from "./trace-log"; import { getCachedWindowsHostDiagnostics, getWindowsHostDiagnostics } from "./windows-host-diagnostics"; import type { DownloadManager } from "./download-manager"; const AI_MANIFEST_FILE = "debug_ai_manifest.json"; function safeReadJson(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown; } catch { return null; } } function addJson(zip: AdmZip, zipPath: string, value: unknown): void { zip.addFile(zipPath, Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8")); } function addFileIfExists(zip: AdmZip, sourcePath: string | null, zipPath: string): void { if (!sourcePath || !fs.existsSync(sourcePath)) { return; } zip.addLocalFile(sourcePath, path.posix.dirname(zipPath), path.posix.basename(zipPath)); } function addDirectoryIfExists(zip: AdmZip, dirPath: string, zipRoot: string): void { if (!fs.existsSync(dirPath)) { return; } const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const zipPath = path.posix.join(zipRoot, entry.name); if (entry.isDirectory()) { addDirectoryIfExists(zip, fullPath, zipPath); continue; } zip.addLocalFile(fullPath, path.posix.dirname(zipPath), path.posix.basename(zipPath)); } } /** Wie addDirectoryIfExists, aber nur Dateien die in den letzten maxAgeMs ms geaendert wurden. */ function addRecentDirectoryFiles(zip: AdmZip, dirPath: string, zipRoot: string, maxAgeMs: number): number { if (!fs.existsSync(dirPath)) { return 0; } const cutoff = Date.now() - maxAgeMs; let added = 0; const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile()) continue; const fullPath = path.join(dirPath, entry.name); try { if (fs.statSync(fullPath).mtimeMs >= cutoff) { zip.addLocalFile(fullPath, zipRoot, entry.name); added += 1; } } catch { /* ignorieren */ } } return added; } function formatTimestampForFileName(date: Date): string { const y = date.getFullYear(); const mo = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); const h = String(date.getHours()).padStart(2, "0"); const mi = String(date.getMinutes()).padStart(2, "0"); const s = String(date.getSeconds()).padStart(2, "0"); return `${y}-${mo}-${d}_${h}-${mi}-${s}`; } export function getSupportBundleDefaultFileName(): string { return `rd-support-bundle-${formatTimestampForFileName(new Date())}.zip`; } type HostDiagnosticsMode = "full" | "cached" | "none"; interface BuildSupportBundleOptions { hostDiagnosticsMode?: HostDiagnosticsMode; } function createDeferredHostDiagnostics(reason: string): unknown { return { collectedAt: new Date().toISOString(), supported: process.platform === "win32", platform: process.platform, crashControl: null, recentKernelPower: [], recentWerKernel: [], recentKernelDump: [], recentAppCrashes: [], recentMinidumps: [], assessmentHints: [ reason ], errors: [] }; } function resolveHostDiagnostics(mode: HostDiagnosticsMode): unknown { if (mode === "none") { return createDeferredHostDiagnostics("Host-Diagnose wurde fuer diesen Bundle-Export deaktiviert."); } if (mode === "cached") { const cached = getCachedWindowsHostDiagnostics(); if (cached) { return cached; } return createDeferredHostDiagnostics("Host-Diagnose wurde uebersprungen, um den Export nicht zu blockieren. Fuer eine Voll-Diagnose /host/diagnostics nutzen."); } return getWindowsHostDiagnostics(); } export function buildSupportBundle(manager: DownloadManager, baseDir: string, options: BuildSupportBundleOptions = {}): Buffer { const zip = new AdmZip(); const hostDiagnosticsMode = options.hostDiagnosticsMode || "full"; const storagePaths = createStoragePaths(baseDir); const settings = loadSettings(storagePaths); const history = loadHistory(storagePaths); const snapshot = manager.getSnapshot(); const packageIds = Object.keys(snapshot.session.packages); const itemIds = Object.keys(snapshot.session.items); const debugSetup = getDebugSetupCheck(baseDir); addJson(zip, "overview/meta.json", { appVersion: APP_VERSION, generatedAt: new Date().toISOString(), runtimeBaseDir: baseDir, packageCount: packageIds.length, itemCount: itemIds.length }); addJson(zip, "overview/status.json", snapshot.session); addJson(zip, "overview/settings.json", buildRedactedSettingsPayload(settings)); addJson(zip, "overview/accounts.json", buildAccountSummary(settings)); addJson(zip, "overview/stats.json", { ...buildStatsPayload(snapshot), allTime: { totalDownloadedAllTime: settings.totalDownloadedAllTime, totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime, totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs } }); addJson(zip, "overview/debug-setup.json", debugSetup); addJson(zip, "overview/self-check.json", debugSetup); addJson(zip, "overview/history.json", { total: history.length, entries: history.map((entry) => summarizeHistoryEntry(entry)) }); addJson(zip, "overview/packages.json", { count: packageIds.length, packages: packageIds.map((packageId) => snapshot.session.packages[packageId]).filter(Boolean) }); addJson(zip, "overview/items.json", { count: itemIds.length, items: itemIds.map((itemId) => snapshot.session.items[itemId]).filter(Boolean) }); addJson(zip, "overview/host-diagnostics.json", resolveHostDiagnostics(hostDiagnosticsMode)); addJson(zip, "overview/trace-config.json", getTraceConfig()); addFileIfExists(zip, path.join(baseDir, AI_MANIFEST_FILE), `runtime/${AI_MANIFEST_FILE}`); addFileIfExists(zip, path.join(baseDir, "debug_host.txt"), "runtime/debug_host.txt"); addFileIfExists(zip, path.join(baseDir, "debug_port.txt"), "runtime/debug_port.txt"); addFileIfExists(zip, getTraceConfigPath(), "runtime/trace_config.json"); addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log"); addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old"); addFileIfExists(zip, getAuditLogPath(), "logs/audit.log"); addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old"); addFileIfExists(zip, getRenameLogPath(), "logs/rename.log"); addFileIfExists(zip, getRenameLogPath() ? `${getRenameLogPath()}.old` : null, "logs/rename.log.old"); addFileIfExists(zip, getDesktopRenameLogPath(), "logs/rename-session-desktop.txt"); addFileIfExists(zip, getSessionLogPath(), "logs/session.log"); addFileIfExists(zip, getTraceLogPath(), "logs/trace.log"); addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old"); // Granulare Per-Item/-Package/-Session-Logs nur der letzten 8h. // Vorher wurden alle logs-Unterordner rekursiv gepackt → tausende Item-Logs // → 200+ MB, unhandlich zum Verschicken. Mit 8h-Fenster bleibt das Bundle // klein genug und enthaelt alles fuer aktuelle Fehler + Rename-Probleme. const SUPPORT_BUNDLE_LOG_WINDOW_MS = 8 * 60 * 60 * 1000; addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs"); addRecentDirectoryFiles(zip, path.join(baseDir, "package-logs"), "logs/package-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS); addRecentDirectoryFiles(zip, path.join(baseDir, "item-logs"), "logs/item-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS); // Live-Logs der aktiven Queue (aktuelle Session) immer vollstaendig mitsichern. for (const packageId of packageIds) { addFileIfExists(zip, manager.getPackageLogPath(packageId) || getPackageLogPath(packageId), `logs/live/package-${packageId}.txt`); } for (const itemId of itemIds) { addFileIfExists(zip, manager.getItemLogPath(itemId), `logs/live/item-${itemId}.txt`); } const aiManifest = safeReadJson(path.join(baseDir, AI_MANIFEST_FILE)); if (aiManifest) { addJson(zip, "overview/ai-manifest.json", aiManifest); } return zip.toBuffer(); }