-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathReportManager.cs
More file actions
126 lines (117 loc) · 5.29 KB
/
Copy pathReportManager.cs
File metadata and controls
126 lines (117 loc) · 5.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Win32;
using BootstrapMate.Core;
namespace BootstrapMate
{
/// <summary>
/// Posts a vendor-neutral run summary to an optional reporting endpoint when a
/// bootstrap run completes, turning "did this PC provision cleanly?" into a
/// fleet-dashboard query instead of an RDP/registry expedition.
///
/// The payload is plain JSON and intentionally NOT specific to any one backend
/// (ReportMate, MunkiReport, a custom collector, …). Windows counterpart of the
/// macOS ReportManager; emits the same schema.
/// </summary>
public static class ReportManager
{
/// <summary>
/// Build and POST the run summary to the configured reporting endpoint, if any.
/// Best-effort: failures are logged and never abort the run.
/// </summary>
public static async Task SendRunSummaryAsync(bool success, DateTime startTimeUtc, string version, string manifestUrl)
{
var config = ConfigManager.Instance.Config;
var url = config.ReportingUrl;
if (string.IsNullOrWhiteSpace(url))
return;
try
{
// Use the manifest URL actually used for this run (which may come
// from --url), not whatever happens to be in config.
var payload = BuildPayload(success, startTimeUtc, DateTime.UtcNow, version, manifestUrl);
var json = JsonSerializer.Serialize(payload);
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", $"BootstrapMate/{version}");
if (!string.IsNullOrWhiteSpace(config.ReportingHeader))
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", config.ReportingHeader);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
Logger.Info($"Posting run summary to reporting endpoint: {url}");
using var response = await httpClient.PostAsync(url, content);
if (response.IsSuccessStatusCode)
Logger.Info($"Run summary reported (HTTP {(int)response.StatusCode})");
else
Logger.Warning($"Reporting endpoint returned HTTP {(int)response.StatusCode}");
}
catch (Exception ex)
{
Logger.Warning($"Reporting POST failed: {ex.Message}");
}
}
/// <summary>Assemble the vendor-neutral summary. Mirrors the macOS schema.</summary>
public static Dictionary<string, object?> BuildPayload(
bool success, DateTime startTimeUtc, DateTime endTimeUtc, string version, string manifestUrl)
{
return new Dictionary<string, object?>
{
["tool"] = "BootstrapMate",
["platform"] = "Windows",
["schemaVersion"] = 1,
["version"] = version,
["runId"] = StatusManager.GetCurrentRunId(),
["success"] = success,
["startTime"] = startTimeUtc.ToString("o"),
["endTime"] = endTimeUtc.ToString("o"),
["durationSeconds"] = (int)Math.Round((endTimeUtc - startTimeUtc).TotalSeconds),
["architecture"] = CurrentArchitecture(),
["hostname"] = Environment.MachineName,
["serialNumber"] = SerialNumber() ?? "",
["manifestUrl"] = manifestUrl,
["phases"] = CollectPhases()
};
}
private static Dictionary<string, object?> CollectPhases()
{
var phases = new Dictionary<string, object?>();
foreach (var phase in new[] { InstallationPhase.SetupAssistant, InstallationPhase.Userland })
{
var status = StatusManager.GetPhaseStatus(phase);
phases[phase.ToString()] = new Dictionary<string, object?>
{
["stage"] = status.Stage.ToString(),
["exitCode"] = status.ExitCode,
["startTime"] = status.StartTime,
["completionTime"] = status.CompletionTime,
["lastError"] = status.LastError
};
}
return phases;
}
private static string CurrentArchitecture()
=> RuntimeInformation.OSArchitecture switch
{
Architecture.Arm64 => "ARM64",
Architecture.X64 => "X64",
Architecture.X86 => "X86",
var other => other.ToString().ToUpperInvariant()
};
/// <summary>Best-effort hardware serial number from the SMBIOS registry key.</summary>
private static string? SerialNumber()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS");
return key?.GetValue("SystemSerialNumber") as string;
}
catch
{
return null;
}
}
}
}