Files
PatchProbe-Server/PatchProbe.Cli/Collectors/WindowsUpdateCollector.cs
2026-05-25 10:29:38 +08:00

146 lines
5.4 KiB
C#

using Microsoft.Extensions.Logging;
using PatchProbe.Shared.Contracts;
using PatchProbe.Shared.Models;
namespace PatchProbe.Cli.Collectors;
public sealed class WindowsUpdateCollector(ILogger<WindowsUpdateCollector> logger) : ICollector<WindowsUpdateInfo>
{
public Task<WindowsUpdateInfo> CollectAsync(CancellationToken cancellationToken = default)
{
logger.LogInformation("Collecting Windows Update information via WUA COM");
var applicable = new List<ApplicableUpdate>();
var history = new List<UpdateHistoryEntry>();
string? searchError = null;
try
{
var sessionType = Type.GetTypeFromProgID("Microsoft.Update.Session")
?? throw new InvalidOperationException("Microsoft.Update.Session ProgID not found");
dynamic session = Activator.CreateInstance(sessionType)!;
dynamic searcher = session.CreateUpdateSearcher();
logger.LogInformation("Searching for applicable updates (IsInstalled=0 and IsHidden=0)");
dynamic result = searcher.Search("IsInstalled=0 and IsHidden=0");
for (int i = 0; i < result.Updates.Count; i++)
{
dynamic u = result.Updates.Item(i);
string kbId = ExtractKbId(u);
string category = ExtractFirstCategory(u);
applicable.Add(new ApplicableUpdate
{
UpdateId = TryGet<string>(() => u.Identity.UpdateID),
Title = TryGet<string>(() => u.Title),
KbArticleId = kbId,
Category = category,
Severity = TryGet<string>(() => u.MsrcSeverity),
RebootRequired = TryGet<bool>(() => u.RebootRequired),
IsDownloaded = TryGet<bool>(() => u.IsDownloaded),
Description = TryGet<string>(() => u.Description),
SupportUrl = TryGet<string>(() => u.SupportUrl),
});
}
logger.LogInformation("Found {Count} applicable updates", applicable.Count);
int totalHistoryCount = searcher.GetTotalHistoryCount();
int historyLimit = Math.Min(totalHistoryCount, 100);
if (historyLimit > 0)
{
dynamic historyEntries = searcher.QueryHistory(0, historyLimit);
for (int i = 0; i < historyEntries.Count; i++)
{
dynamic e = historyEntries.Item(i);
history.Add(new UpdateHistoryEntry
{
UpdateId = TryGet<string>(() => e.UpdateIdentity.UpdateID),
Title = TryGet<string>(() => e.Title),
KbArticleId = ExtractKbIdFromTitle(TryGet<string>(() => e.Title)),
ResultCode = TryGet<int>(() => (int)e.ResultCode),
HResult = TryGet<int>(() => e.HResult),
Date = TryGetDate(() => e.Date),
Operation = TryGet<int>(() => (int)e.Operation) switch
{
1 => "Installation",
2 => "Uninstallation",
_ => "Unknown"
},
ServerSelection = TryGet<int>(() => (int)e.ServerSelection) switch
{
0 => "Default",
1 => "ManagedServer",
2 => "WindowsUpdate",
3 => "Others",
_ => "Unknown"
},
});
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "WUA search failed");
searchError = ex.Message;
}
return Task.FromResult(new WindowsUpdateInfo
{
ApplicableUpdates = applicable,
History = history,
SearchError = searchError,
});
}
private static string ExtractKbId(dynamic update)
{
try
{
if (update.KBArticleIDs.Count > 0)
return "KB" + update.KBArticleIDs.Item(0);
}
catch { /* fall through to title parse */ }
return ExtractKbIdFromTitle(TryGet<string>(() => update.Title));
}
private static string ExtractFirstCategory(dynamic update)
{
try
{
if (update.Categories.Count > 0)
return update.Categories.Item(0).Name;
}
catch { }
return string.Empty;
}
private static string ExtractKbIdFromTitle(string? title)
{
if (string.IsNullOrEmpty(title)) return string.Empty;
var match = System.Text.RegularExpressions.Regex.Match(title, @"KB\d+", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return match.Success ? match.Value.ToUpperInvariant() : string.Empty;
}
private static T TryGet<T>(Func<T> getter)
{
try { return getter(); }
catch { return default!; }
}
private static DateTimeOffset? TryGetDate(Func<object> getter)
{
try
{
var raw = getter();
if (raw is DateTime dt) return new DateTimeOffset(dt, TimeSpan.Zero);
if (raw is DateTimeOffset dto) return dto;
if (DateTime.TryParse(raw?.ToString(), out var parsed)) return new DateTimeOffset(parsed, TimeSpan.Zero);
}
catch { }
return null;
}
}