fix reboot, metrics, teams notification

This commit is contained in:
Oliver Kieselbach
2026-02-16 19:12:02 +01:00
parent 7550eec276
commit ae15d798fc
3 changed files with 47 additions and 9 deletions
@@ -556,8 +556,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
});
// Only trigger self-destruct if configured
if (_configuration.SelfDestructOnComplete)
// Trigger self-destruct or reboot if configured
if (_configuration.SelfDestructOnComplete || _configuration.RebootOnComplete)
{
Task.Run(() => HandleEnrollmentComplete());
return; // Don't continue with normal event processing
@@ -752,7 +752,28 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
else if (_configuration.RebootOnComplete)
{
_logger.Info("RebootOnComplete enabled - initiating reboot");
_logger.Info($"RebootOnComplete enabled - initiating reboot in {_configuration.RebootDelaySeconds}s");
// Emit reboot event into the timeline before shutdown
EmitEvent(new EnrollmentEvent
{
SessionId = _configuration.SessionId,
TenantId = _configuration.TenantId,
Timestamp = DateTime.UtcNow,
EventType = "reboot_triggered",
Severity = EventSeverity.Info,
Source = "MonitoringService",
Phase = EnrollmentPhase.Complete,
Message = $"Reboot configured — triggering reboot in {_configuration.RebootDelaySeconds} seconds",
Data = new Dictionary<string, object>
{
{ "rebootDelaySeconds", _configuration.RebootDelaySeconds }
}
});
// Final upload to ensure the reboot event is sent
await UploadEventsAsync();
var psi = new ProcessStartInfo
{
FileName = "shutdown.exe",
@@ -136,10 +136,13 @@ namespace AutopilotMonitor.Functions.Functions
}
// Update session status based on events
// statusTransitioned = true only when this is the FIRST time the session reaches Succeeded/Failed
// (UpdateSessionStatusAsync returns false if already in a terminal state — idempotency guard)
bool statusTransitioned = false;
if (completionEvent != null)
{
// Enrollment completed successfully - use event timestamp for accurate duration
await _storageService.UpdateSessionStatusAsync(
statusTransitioned = await _storageService.UpdateSessionStatusAsync(
request.TenantId,
request.SessionId,
SessionStatus.Succeeded,
@@ -147,7 +150,7 @@ namespace AutopilotMonitor.Functions.Functions
completedAt: completionEvent.Timestamp,
earliestEventTimestamp: earliestEventTimestamp
);
_logger.LogInformation($"{sessionPrefix} Status: Succeeded");
_logger.LogInformation("{SessionPrefix} Status: Succeeded (transitioned={Transitioned})", sessionPrefix, statusTransitioned);
}
else if (failureEvent != null)
{
@@ -156,7 +159,7 @@ namespace AutopilotMonitor.Functions.Functions
? $"{failureEvent.Message} ({failureEvent.Data["errorCode"]})"
: failureEvent.Message;
await _storageService.UpdateSessionStatusAsync(
statusTransitioned = await _storageService.UpdateSessionStatusAsync(
request.TenantId,
request.SessionId,
SessionStatus.Failed,
@@ -165,7 +168,7 @@ namespace AutopilotMonitor.Functions.Functions
completedAt: failureEvent.Timestamp,
earliestEventTimestamp: earliestEventTimestamp
);
_logger.LogWarning($"{sessionPrefix} Status: Failed - {failureReason}");
_logger.LogWarning("{SessionPrefix} Status: Failed - {FailureReason} (transitioned={Transitioned})", sessionPrefix, failureReason, statusTransitioned);
}
else if (lastPhaseChangeEvent != null)
{
@@ -228,7 +231,8 @@ namespace AutopilotMonitor.Functions.Functions
var updatedSession = await _storageService.GetSessionAsync(request.TenantId, request.SessionId);
// Send Teams notification on enrollment completion (fire-and-forget, non-fatal)
if (completionEvent != null || failureEvent != null)
// Only send when statusTransitioned=true to prevent duplicates on retry/double-upload
if (statusTransitioned && (completionEvent != null || failureEvent != null))
{
var tenantConfig = await _configService.GetConfigurationAsync(request.TenantId);
if (!string.IsNullOrEmpty(tenantConfig.TeamsWebhookUrl))
@@ -343,7 +343,8 @@ namespace AutopilotMonitor.Functions.Services
var sessions = new List<SessionSummary>();
// Query all sessions without tenant filter
var query = tableClient.QueryAsync<TableEntity>(maxPerPage: maxResults);
// maxPerPage is capped at 1000 (Azure Table Storage limit); pagination is automatic
var query = tableClient.QueryAsync<TableEntity>(maxPerPage: Math.Min(maxResults, 1000));
await foreach (var entity in query)
{
@@ -404,6 +405,18 @@ namespace AutopilotMonitor.Functions.Services
var entity = await tableClient.GetEntityAsync<TableEntity>(tenantId, sessionId);
var session = entity.Value;
// Idempotency: if the session is already in a terminal state (Succeeded/Failed),
// do not overwrite it with another terminal state to prevent duplicate notifications.
if (status == SessionStatus.Succeeded || status == SessionStatus.Failed)
{
var existingStatus = session.GetString("Status");
if (existingStatus == SessionStatus.Succeeded.ToString() || existingStatus == SessionStatus.Failed.ToString())
{
_logger.LogInformation($"Session {sessionId} already in terminal state '{existingStatus}', skipping status update to '{status}'");
return false;
}
}
// Update status
session["Status"] = status.ToString();