diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs index 012b8418..909893e8 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs @@ -12,7 +12,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Runtime /// The bootstrap provenance in agent_started. Fixture lines are the scripts' real /// formats: installer [ts] Bootstrap script version: vX.Y /// (Install-AutopilotMonitor.ps1), loader [ts] === Autopilot Monitor bootstrap loader vX.Y - /// started (user=..., pid=...) === (Start-AutopilotMonitor.ps1). + /// started (user=..., pid=...) === (Start-AutopilotMonitor.ps1), the MSI's downloader before + /// the loader [ts] MSI bootstrap runner started (user=..., pid=...).. /// public sealed class BootstrapProvenanceReaderTests : IDisposable { @@ -79,6 +80,39 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Runtime Assert.Null(p.ScriptVersion); } + [Fact] + public void An_installer_from_before_the_log_rename_is_read_under_its_old_name() + { + Write(BootstrapProvenanceReader.LegacyInstallerLogName, InstallerRun("2.3")); + + var p = BootstrapProvenanceReader.Read(_dir); + + Assert.True(p.ScriptLogPresent); + Assert.Equal("2.3", p.ScriptVersion); + } + + [Fact] + public void With_both_installer_logs_the_most_recently_written_one_counts() + { + Write(BootstrapProvenanceReader.InstallerLogName, InstallerRun("2.6")); + Write(BootstrapProvenanceReader.LegacyInstallerLogName, InstallerRun("2.3")); + File.SetLastWriteTimeUtc(Path.Combine(_dir, BootstrapProvenanceReader.InstallerLogName), DateTime.UtcNow.AddDays(-3)); + File.SetLastWriteTimeUtc(Path.Combine(_dir, BootstrapProvenanceReader.LegacyInstallerLogName), DateTime.UtcNow.AddMinutes(-1)); + + Assert.Equal("2.3", BootstrapProvenanceReader.Read(_dir).ScriptVersion); + } + + [Fact] + public void The_msi_downloader_from_before_the_loader_counts_as_msi_without_version() + { + Write(BootstrapProvenanceReader.LegacyMsiLoaderLogName, "[2026-08-01 09:00:00] MSI bootstrap runner started (user=SECRETUSER, pid=4242)."); + + var p = BootstrapProvenanceReader.Read(_dir); + + Assert.Equal("msi", p.LoaderLog); + Assert.Null(p.LoaderVersion); + } + [Fact] public void The_msi_loader_log_is_told_apart_from_the_intune_loader() { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs index 94060c07..86e1f6d4 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs @@ -13,10 +13,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime /// passing a version to --install would only ever cover installers released after that /// change -- the old installers still assigned in Intune are exactly the ones worth counting. /// - /// bootstrap-script.log: Bootstrap script version: vX.Y, written by every + /// bootstrap-script.log (bootstrap_agent.log up to installer v2.3, before + /// the log rename of 2026-08-18): Bootstrap script version: vX.Y, written by every /// installer run since 2026-04-09. A log without that line means an older installer. - /// bootstrap-loader.log (Intune) / bootstrap-msi.log (MSI): - /// bootstrap loader vX.Y started, written by every loader since v1.0. + /// bootstrap-loader.log (Intune) / bootstrap-msi.log (MSI, + /// msi-bootstrap.log before 2026-08-18): bootstrap loader vX.Y started, written by + /// every loader since v1.0. The MSI's own downloader, which the loader replaced on 2026-09-08, + /// writes no version. /// oobe_bootstrap.log: the OOBE go-script (generated by the backend, no version). /// /// Only the version number is extracted; the loader line also names the account, which is @@ -29,6 +32,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime internal const string MsiLoaderLogName = "bootstrap-msi.log"; internal const string OobeLogName = "oobe_bootstrap.log"; + // Names before the log rename of 2026-08-18; copies of those scripts are still assigned. + internal const string LegacyInstallerLogName = "bootstrap_agent.log"; + internal const string LegacyMsiLoaderLogName = "msi-bootstrap.log"; + // The logs grow across runs; the last run's lines sit at the end. One run writes a few KB. private const int TailBytes = 64 * 1024; @@ -42,22 +49,19 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime { try { - var installerLog = ReadTail(Path.Combine(logDirectory, InstallerLogName)); + var installerLog = ReadTail(MostRecentlyWritten(logDirectory, InstallerLogName, LegacyInstallerLogName)); - // Intune loader and MSI loader write separate files; when a device has both, the - // more recently written one belongs to the bootstrap that ran last. - var loader = new[] { (Name: LoaderLogName, Channel: "loader"), (Name: MsiLoaderLogName, Channel: "msi") } - .Select(l => (l.Channel, Path: Path.Combine(logDirectory, l.Name))) - .Where(l => File.Exists(l.Path)) - .OrderByDescending(l => File.GetLastWriteTimeUtc(l.Path)) - .Select(l => (l.Channel, Version: LastVersion(ReadTail(l.Path), LoaderStartLine))) - .FirstOrDefault(); + // Intune loader and MSI loader write separate files; every name but the Intune + // loader's is the MSI's. + var loaderPath = MostRecentlyWritten(logDirectory, LoaderLogName, MsiLoaderLogName, LegacyMsiLoaderLogName); + var loaderChannel = loaderPath == null ? null + : string.Equals(Path.GetFileName(loaderPath), LoaderLogName, StringComparison.OrdinalIgnoreCase) ? "loader" : "msi"; return new BootstrapProvenance( scriptLogPresent: installerLog != null, scriptVersion: LastVersion(installerLog, InstallerVersionLine), - loaderLog: loader.Channel, - loaderVersion: loader.Version, + loaderLog: loaderChannel, + loaderVersion: LastVersion(ReadTail(loaderPath), LoaderStartLine), oobeLogPresent: File.Exists(Path.Combine(logDirectory, OobeLogName))); } catch @@ -66,6 +70,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime } } + /// + /// The most recently written of the named logs that exist, or null. On a device with + /// several, it belongs to the bootstrap that ran last. + /// + private static string MostRecentlyWritten(string directory, params string[] names) => + names.Select(name => Path.Combine(directory, name)) + .Where(path => File.Exists(path)) + .OrderByDescending(path => File.GetLastWriteTimeUtc(path)) + .FirstOrDefault(); + internal static string LastVersion(string text, Regex line) { if (string.IsNullOrEmpty(text)) return null;