dragon-iso/src/TeamsISO.App/Services/DiagnosticsBundle.cs

134 lines
5.6 KiB
C#

using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Text;
namespace TeamsISO.App.Services;
/// <summary>
/// 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.
/// </summary>
public static class DiagnosticsBundle
{
/// <summary>
/// Build the bundle and return the path it was written to.
/// Throws on disk failure — the caller toasts/dialogs.
/// </summary>
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<AssemblyInformationalVersionAttribute>()?.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");
}