using System.IO; using System.IO.Compression; using System.Reflection; using System.Text; namespace TeamsISO.App.Services; /// /// Gathers logs + config + presets + version metadata into a single .zip the /// operator can attach to a bug report. Surfaced via the "Export diagnostics" /// button in About. /// /// We deliberately do NOT include screenshots or any process/memory dumps — /// that's outside the scope of a v1 support bundle and would raise privacy /// flags. The bundle has only files the user already wrote with their TeamsISO /// usage; nothing here is hidden state. /// public static class DiagnosticsBundle { /// /// Build the bundle and return the path it was written to. /// Throws on disk failure — the caller toasts/dialogs. /// public static string Export() { var ts = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss"); var fileName = $"teamsiso-diagnostics-{ts}.zip"; var outDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var downloads = Path.Combine(outDir, "Downloads"); if (!Directory.Exists(downloads)) downloads = outDir; // fall back to ~/ var outPath = Path.Combine(downloads, fileName); using var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.None); using var zip = new ZipArchive(fs, ZipArchiveMode.Create, leaveOpen: false); WriteEnvironmentTxt(zip); TryCopyDirectory(zip, "logs", LogsDirectory); TryCopyFile(zip, "config.json", AppDataPath("config.json")); TryCopyFile(zip, "presets.json", LocalAppDataPath("presets.json")); TryCopyFile(zip, "window.json", LocalAppDataPath("window.json")); TryCopyFile(zip, "ndi-config.v1.json", NdiConfigPath()); TryCopyFile(zip, "output-name-template.txt", LocalAppDataPath("output-name-template.txt")); return outPath; } private static void WriteEnvironmentTxt(ZipArchive zip) { var asm = typeof(DiagnosticsBundle).Assembly; var version = asm.GetCustomAttribute()?.InformationalVersion ?? asm.GetName().Version?.ToString() ?? "unknown"; var sb = new StringBuilder(); sb.AppendLine("TeamsISO diagnostic bundle"); sb.AppendLine($"Generated: {DateTimeOffset.Now:o}"); sb.AppendLine($"TeamsISO version: {version}"); sb.AppendLine($".NET runtime: {Environment.Version}"); sb.AppendLine($"OS: {Environment.OSVersion}"); sb.AppendLine($"Machine: {Environment.MachineName}"); sb.AppendLine($"User: {Environment.UserName}"); sb.AppendLine($"Process bits: {(Environment.Is64BitProcess ? "64" : "32")}"); sb.AppendLine($"OS bits: {(Environment.Is64BitOperatingSystem ? "64" : "32")}"); sb.AppendLine($"Working set: {Environment.WorkingSet / (1024 * 1024)} MB"); sb.AppendLine(); sb.AppendLine("Files included (when present):"); sb.AppendLine(" logs/ Serilog rolling daily logs"); sb.AppendLine(" config.json Engine settings (framerate, NDI groups, etc.)"); sb.AppendLine(" presets.json Saved operator presets"); sb.AppendLine(" window.json Last main-window placement"); sb.AppendLine(" ndi-config.v1.json NDI Access Manager config (group routing)"); sb.AppendLine(" output-name-template.txt NDI source name template override"); var entry = zip.CreateEntry("environment.txt"); using var w = new StreamWriter(entry.Open(), Encoding.UTF8); w.Write(sb.ToString()); } private static void TryCopyFile(ZipArchive zip, string entryName, string sourcePath) { try { if (!File.Exists(sourcePath)) return; zip.CreateEntryFromFile(sourcePath, entryName); } catch { // One missing or locked file shouldn't kill the rest of the bundle. } } private static void TryCopyDirectory(ZipArchive zip, string prefix, string sourceDir) { if (!Directory.Exists(sourceDir)) return; try { foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories)) { try { var rel = Path.GetRelativePath(sourceDir, file).Replace('\\', '/'); zip.CreateEntryFromFile(file, $"{prefix}/{rel}"); } catch { // Skip locked files (e.g., today's actively-written log). } } } catch { // Permission denied on the dir as a whole; nothing more to do. } } private static string LogsDirectory => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TeamsISO", "Logs"); private static string LocalAppDataPath(string fileName) => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TeamsISO", fileName); private static string AppDataPath(string fileName) => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TeamsISO", fileName); private static string NdiConfigPath() => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "NDI", "ndi-config.v1.json"); }