mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
Merge pull request #44 from okieselbach/v2-hotfix-first-run-ghost-detection
fix(agent): V2 hotfix — first-run ghost-detect triggers self-destruct on fresh install
This commit is contained in:
@@ -75,6 +75,99 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
$"Expected hard_kill or reboot_kill, got {summary.ExitType}.");
|
||||
}
|
||||
|
||||
// ================================================================= CheckEnrollmentCompleteMarker
|
||||
|
||||
[Fact]
|
||||
public void CheckEnrollmentCompleteMarker_returns_false_on_first_run_after_install_when_session_exists()
|
||||
{
|
||||
// Regression guard for the first-run-after-install bug: Program.cs must create the
|
||||
// SessionId BEFORE invoking this guard, so a fresh install (Deployed registry marker
|
||||
// written by --install, no prior session) does not get misdiagnosed as a ghost
|
||||
// restart and trigger self-destruct before FetchConfigAsync.
|
||||
using var tmp = new TempDirectory();
|
||||
var logger = NewLogger(tmp.Path);
|
||||
new SessionIdPersistence(tmp.Path).GetOrCreate(logger);
|
||||
var stateDir = Path.Combine(tmp.Path, "State");
|
||||
var factoryCalls = 0;
|
||||
|
||||
var shouldExit = AutopilotMonitor.Agent.V2.Program.CheckEnrollmentCompleteMarker(
|
||||
dataDirectory: tmp.Path,
|
||||
stateDirectory: stateDir,
|
||||
selfDestructOnComplete: true, // worst case — cleanup armed
|
||||
cleanupServiceFactory: () =>
|
||||
{
|
||||
factoryCalls++;
|
||||
throw new InvalidOperationException(
|
||||
"test-harness: cleanup factory must not be invoked in this scenario");
|
||||
},
|
||||
logger: logger,
|
||||
consoleMode: false);
|
||||
|
||||
Assert.False(shouldExit);
|
||||
Assert.Equal(0, factoryCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckEnrollmentCompleteMarker_returns_true_without_firing_cleanup_when_marker_present_and_no_selfdestruct()
|
||||
{
|
||||
// Remote config said "don't self-destruct": agent must exit cleanly but leave the
|
||||
// ProgramData directory intact for post-mortem inspection.
|
||||
using var tmp = new TempDirectory();
|
||||
var logger = NewLogger(tmp.Path);
|
||||
new SessionIdPersistence(tmp.Path).GetOrCreate(logger);
|
||||
var stateDir = Path.Combine(tmp.Path, "State");
|
||||
Directory.CreateDirectory(stateDir);
|
||||
File.WriteAllText(Path.Combine(stateDir, "enrollment-complete.marker"), "previous completion");
|
||||
var factoryCalls = 0;
|
||||
|
||||
var shouldExit = AutopilotMonitor.Agent.V2.Program.CheckEnrollmentCompleteMarker(
|
||||
dataDirectory: tmp.Path,
|
||||
stateDirectory: stateDir,
|
||||
selfDestructOnComplete: false,
|
||||
cleanupServiceFactory: () =>
|
||||
{
|
||||
factoryCalls++;
|
||||
throw new InvalidOperationException(
|
||||
"test-harness: cleanup factory must not be invoked in this scenario");
|
||||
},
|
||||
logger: logger,
|
||||
consoleMode: false);
|
||||
|
||||
Assert.True(shouldExit);
|
||||
Assert.Equal(0, factoryCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckEnrollmentCompleteMarker_fires_cleanup_retry_when_marker_present_and_selfdestruct_armed()
|
||||
{
|
||||
// Scheduled-task-survived-self-destruct retry path. CleanupService actually spawns a
|
||||
// PowerShell cleanup script — we throw from the factory so the real side-effect does
|
||||
// not fire; TryRetryCleanup swallows the exception and logs a warning.
|
||||
using var tmp = new TempDirectory();
|
||||
var logger = NewLogger(tmp.Path);
|
||||
new SessionIdPersistence(tmp.Path).GetOrCreate(logger);
|
||||
var stateDir = Path.Combine(tmp.Path, "State");
|
||||
Directory.CreateDirectory(stateDir);
|
||||
File.WriteAllText(Path.Combine(stateDir, "enrollment-complete.marker"), "previous completion");
|
||||
var factoryCalls = 0;
|
||||
|
||||
var shouldExit = AutopilotMonitor.Agent.V2.Program.CheckEnrollmentCompleteMarker(
|
||||
dataDirectory: tmp.Path,
|
||||
stateDirectory: stateDir,
|
||||
selfDestructOnComplete: true,
|
||||
cleanupServiceFactory: () =>
|
||||
{
|
||||
factoryCalls++;
|
||||
throw new InvalidOperationException(
|
||||
"test-harness: must not spawn real PowerShell cleanup in unit tests");
|
||||
},
|
||||
logger: logger,
|
||||
consoleMode: false);
|
||||
|
||||
Assert.True(shouldExit);
|
||||
Assert.Equal(1, factoryCalls);
|
||||
}
|
||||
|
||||
// ================================================================= Emergency break
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -36,11 +36,11 @@ namespace AutopilotMonitor.Agent.V2
|
||||
/// <item>Load cached <c>remote-config.json</c> → <see cref="SelfUpdater.BackendExpectedSha256"/> + <c>AllowAgentDowngrade</c></item>
|
||||
/// <item><see cref="SelfUpdater.CheckAndApplyUpdateAsync"/> — on success restarts the process; on failure continues with current binary</item>
|
||||
/// <item><see cref="DetectPreviousExit"/> — reads markers + event log to classify last shutdown</item>
|
||||
/// <item><see cref="CheckEnrollmentCompleteMarker"/> — ghost-restart detection + cleanup retry</item>
|
||||
/// <item><see cref="CheckSessionAgeEmergencyBreak"/> — absolute session-age watchdog</item>
|
||||
/// <item>Resolve TenantId (registry → bootstrap-config.json fallback)</item>
|
||||
/// <item>Build <see cref="AgentConfiguration"/> (CLI args + persisted bootstrap / await-enrollment config)</item>
|
||||
/// <item>Get/create SessionId via <see cref="SessionIdPersistence"/></item>
|
||||
/// <item>Get/create SessionId via <see cref="SessionIdPersistence"/> — <b>before</b> the guards, matching Legacy boot order. The ghost-restart guard keys on <c>session.id</c> absence to detect "self-destruct ran but Scheduled Task survived"; creating the session after the guard would misdiagnose every first-run-after-install as a ghost restart and trigger self-destruct before the remote-config fetch.</item>
|
||||
/// <item><see cref="CheckEnrollmentCompleteMarker"/> — ghost-restart detection + cleanup retry</item>
|
||||
/// <item><see cref="CheckSessionAgeEmergencyBreak"/> — absolute session-age watchdog</item>
|
||||
/// <item>(Optional) Wait for MDM certificate in <c>--await-enrollment</c> mode</item>
|
||||
/// <item>Build <see cref="BackendApiClient"/> + <see cref="RemoteConfigService"/> → fetch config</item>
|
||||
/// <item><see cref="BootstrapConfigCleanup.TryDeleteIfCertReadyAsync"/> — H-2 mitigation post-cert</item>
|
||||
@@ -197,13 +197,21 @@ namespace AutopilotMonitor.Agent.V2
|
||||
// --install time may have written a bootstrap config that holds a tenantId even when
|
||||
// the registry is not yet populated (bootstrap token path). Honour that.
|
||||
|
||||
var agentConfig = BuildAgentConfiguration(args, tenantId, sessionId: null, bootstrapConfig, awaitConfig);
|
||||
|
||||
// Create/recover SessionId BEFORE the startup guards. Legacy-parity boot order —
|
||||
// the ghost-restart guard below keys on session.id absence to detect
|
||||
// "self-destruct ran but Scheduled Task survived". Creating the session after the
|
||||
// guard would misdiagnose every first-run-after-install (Deployed registry marker
|
||||
// set by --install, no session.id yet) as a ghost restart and trigger
|
||||
// ExecuteSelfDestruct before the remote-config fetch, deleting the ProgramData
|
||||
// directory on every fresh deployment.
|
||||
if (args.Contains("--new-session"))
|
||||
{
|
||||
new SessionIdPersistence(dataDirectory).Delete(logger);
|
||||
logger.Info("--new-session: cleared persisted SessionId.");
|
||||
}
|
||||
|
||||
var agentConfig = BuildAgentConfiguration(args, tenantId, sessionId: null, bootstrapConfig, awaitConfig);
|
||||
agentConfig.SessionId = new SessionIdPersistence(dataDirectory).GetOrCreate(logger);
|
||||
|
||||
// Build a cleanup-service factory used by guards: instantiated lazily and with the
|
||||
// current agentConfig so command-line overrides (e.g. --no-cleanup) take effect.
|
||||
@@ -226,10 +234,6 @@ namespace AutopilotMonitor.Agent.V2
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Session is safe to start — create/recover SessionId now (AFTER guards so a dead
|
||||
// session is not resurrected before the watchdog can kill it).
|
||||
agentConfig.SessionId = new SessionIdPersistence(dataDirectory).GetOrCreate(logger);
|
||||
|
||||
if (agentConfig.AwaitEnrollment)
|
||||
{
|
||||
logger.Info($"--await-enrollment: polling for MDM certificate (timeout: {agentConfig.AwaitEnrollmentTimeoutMinutes}min).");
|
||||
|
||||
Reference in New Issue
Block a user