mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
Agent architecture changes and fixes
This commit is contained in:
@@ -92,6 +92,12 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
/// </summary>
|
||||
public bool RebootOnComplete { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the log directory is preserved after self-destruct/cleanup.
|
||||
/// All other files (binaries, config, spool, scheduled task) are still removed.
|
||||
/// </summary>
|
||||
public bool KeepLogFile { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable geo-location detection (queries external IP services)
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
private readonly Timer _uploadTimer;
|
||||
private readonly Timer _debounceTimer;
|
||||
private readonly object _timerLock = new object();
|
||||
private readonly ManualResetEventSlim _completionEvent = new(false);
|
||||
private long _eventSequence = 0;
|
||||
private EnrollmentPhase? _lastPhase = null;
|
||||
|
||||
@@ -496,6 +497,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
UploadEventsAsync().Wait(TimeSpan.FromSeconds(10));
|
||||
|
||||
_logger.Info("Monitoring service stopped");
|
||||
|
||||
// Unblock WaitForCompletion() in background/SYSTEM mode
|
||||
_completionEvent.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocks the calling thread until the service stops (used when running as Scheduled Task
|
||||
/// under SYSTEM where there is no interactive Console.ReadLine() to keep the process alive).
|
||||
/// </summary>
|
||||
public void WaitForCompletion()
|
||||
{
|
||||
_completionEvent.Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -778,37 +791,54 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
"AutopilotMonitor"
|
||||
));
|
||||
|
||||
var logDir = Path.GetFullPath(Path.Combine(agentBasePath, "Logs"));
|
||||
var keepLogs = _configuration.KeepLogFile;
|
||||
|
||||
// Create a self-deleting PowerShell script
|
||||
var cleanupScript = $@"
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
|
||||
# Wait for agent process to exit
|
||||
Start-Sleep -Seconds 3
|
||||
Start-Sleep -Seconds 2
|
||||
Wait-Process -Id {currentProcessId} -ErrorAction SilentlyContinue -Timeout 30
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# Remove Scheduled Task
|
||||
Write-Host 'Removing Scheduled Task: {_configuration.ScheduledTaskName}'
|
||||
try {{
|
||||
Stop-ScheduledTask -TaskName '{_configuration.ScheduledTaskName}' -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName '{_configuration.ScheduledTaskName}' -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Write-Host 'Scheduled Task removed successfully'
|
||||
}} catch {{
|
||||
Write-Host ""Failed to remove Scheduled Task: $_""
|
||||
}}
|
||||
}} catch {{ }}
|
||||
|
||||
# Delete AutopilotMonitor folder
|
||||
Write-Host 'Deleting folder: {agentBasePath}'
|
||||
try {{
|
||||
Remove-Item -Path '{agentBasePath}' -Recurse -Force -ErrorAction Stop
|
||||
Write-Host 'Folder deleted successfully'
|
||||
}} catch {{
|
||||
Write-Host ""Failed to delete folder: $_""
|
||||
{(keepLogs ? $@"
|
||||
# Delete everything except Logs directory
|
||||
Get-ChildItem -Path '{agentBasePath}' -Exclude 'Logs' | ForEach-Object {{
|
||||
$dest = $_.FullName + '.del'
|
||||
try {{ Rename-Item -Path $_.FullName -NewName $dest -Force -ErrorAction SilentlyContinue }} catch {{ }}
|
||||
Remove-Item -Path $dest -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}}
|
||||
|
||||
Write-Host 'AutopilotMonitor Agent self-destruct complete'
|
||||
" : $@"
|
||||
# Rename folder then delete. Retry a few times to let the OS release handles.
|
||||
$renamedPath = '{agentBasePath}.del'
|
||||
$renamed = $false
|
||||
for ($i = 1; $i -le 10; $i++) {{
|
||||
try {{
|
||||
Rename-Item -Path '{agentBasePath}' -NewName $renamedPath -Force -ErrorAction Stop
|
||||
$renamed = $true
|
||||
break
|
||||
}} catch {{
|
||||
Start-Sleep -Seconds 2
|
||||
}}
|
||||
}}
|
||||
if ($renamed) {{
|
||||
Remove-Item -Path $renamedPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}} else {{
|
||||
Remove-Item -Path '{agentBasePath}' -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}}
|
||||
")}
|
||||
{(_configuration.RebootOnComplete ? @"
|
||||
# Reboot the device
|
||||
Write-Host 'Initiating reboot...'
|
||||
Restart-Computer -Force
|
||||
Restart-Computer -Force -Delay 10 -Comment 'Autopilot enrollment completed - Autopilot Monitor is rebooting'
|
||||
" : "")}
|
||||
Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue
|
||||
";
|
||||
|
||||
// Write cleanup script to temp location (outside of agent folder)
|
||||
@@ -816,13 +846,21 @@ Restart-Computer -Force
|
||||
File.WriteAllText(tempScriptPath, cleanupScript);
|
||||
_logger.Info($"Cleanup script written to {tempScriptPath}");
|
||||
|
||||
// Launch cleanup script in background (detached process)
|
||||
// Change CWD to temp so this process no longer holds a reference into
|
||||
// the AutopilotMonitor folder tree - Windows won't allow renaming a
|
||||
// directory that any process has as its current working directory.
|
||||
try { Directory.SetCurrentDirectory(Path.GetTempPath()); } catch { }
|
||||
|
||||
// Launch via cmd /c start so the powershell process is created outside the
|
||||
// current Job Object (Scheduled Task job). cmd's 'start' command always
|
||||
// creates a new process group that breaks job inheritance, even under SYSTEM.
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"",
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c start \"\" /b powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetTempPath()
|
||||
};
|
||||
|
||||
Process.Start(psi);
|
||||
@@ -849,27 +887,38 @@ Restart-Computer -Force
|
||||
"AutopilotMonitor"
|
||||
));
|
||||
|
||||
// Create a cleanup PowerShell script
|
||||
var logDir = Path.GetFullPath(Path.Combine(agentBasePath, "Logs"));
|
||||
var keepLogs = _configuration.KeepLogFile;
|
||||
|
||||
// Create a self-deleting cleanup PowerShell script
|
||||
var cleanupScript = $@"
|
||||
# Wait for agent process to exit
|
||||
Start-Sleep -Seconds 3
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
|
||||
# Wait for agent process to exit. Wait-Process may fail if the process is already gone - that is fine.
|
||||
Start-Sleep -Seconds 2
|
||||
Wait-Process -Id {currentProcessId} -ErrorAction SilentlyContinue -Timeout 30
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# Delete AutopilotMonitor folder
|
||||
Write-Host 'Deleting folder: {agentBasePath}'
|
||||
try {{
|
||||
Remove-Item -Path '{agentBasePath}' -Recurse -Force -ErrorAction Stop
|
||||
Write-Host 'Folder deleted successfully'
|
||||
}} catch {{
|
||||
Write-Host ""Failed to delete folder: $_""
|
||||
{(keepLogs ? $@"
|
||||
# Delete everything except Logs directory.
|
||||
# Rename subdirs/files first so locked EXE bytes are released, then remove.
|
||||
Get-ChildItem -Path '{agentBasePath}' -Exclude 'Logs' | ForEach-Object {{
|
||||
$dest = $_.FullName + '.del'
|
||||
try {{ Rename-Item -Path $_.FullName -NewName $dest -Force -ErrorAction SilentlyContinue }} catch {{ }}
|
||||
Remove-Item -Path $dest -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}}
|
||||
|
||||
Write-Host 'AutopilotMonitor Agent cleanup complete'
|
||||
" : $@"
|
||||
# Rename the folder first (works even while EXE bytes are still mapped),
|
||||
# then delete the renamed copy - by then all handles are closed.
|
||||
$renamedPath = '{agentBasePath}.del'
|
||||
try {{ Rename-Item -Path '{agentBasePath}' -NewName $renamedPath -Force -ErrorAction Stop }} catch {{ $renamedPath = '{agentBasePath}' }}
|
||||
Remove-Item -Path $renamedPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
")}
|
||||
{(_configuration.RebootOnComplete ? @"
|
||||
# Reboot the device
|
||||
Write-Host 'Initiating reboot...'
|
||||
Restart-Computer -Force
|
||||
" : "")}
|
||||
# Delete this script
|
||||
Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue
|
||||
";
|
||||
|
||||
// Write cleanup script to temp location
|
||||
@@ -877,11 +926,13 @@ Restart-Computer -Force
|
||||
File.WriteAllText(tempScriptPath, cleanupScript);
|
||||
_logger.Info($"Cleanup script written to {tempScriptPath}");
|
||||
|
||||
// Launch cleanup script
|
||||
// Launch via cmd /c start so the powershell process is created outside the
|
||||
// current Job Object (Scheduled Task job). cmd's 'start' command always
|
||||
// creates a new process group that breaks job inheritance, even under SYSTEM.
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"",
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c start \"\" /b powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{tempScriptPath}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
@@ -907,6 +958,7 @@ Restart-Computer -Force
|
||||
_enrollmentTracker?.Dispose();
|
||||
_gatherRuleExecutor?.Dispose();
|
||||
_remoteConfigService?.Dispose();
|
||||
_completionEvent.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
using System;
|
||||
using System.ServiceProcess;
|
||||
using AutopilotMonitor.Agent.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Agent.Core.Monitoring.Core;
|
||||
|
||||
namespace AutopilotMonitor.Agent
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows Service wrapper for the monitoring agent
|
||||
/// </summary>
|
||||
public class AutopilotMonitorService : ServiceBase
|
||||
{
|
||||
private MonitoringService _monitoringService;
|
||||
private AgentLogger _logger;
|
||||
|
||||
public AutopilotMonitorService()
|
||||
{
|
||||
ServiceName = "AutopilotMonitor";
|
||||
CanStop = true;
|
||||
CanPauseAndContinue = false;
|
||||
AutoLog = true;
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = LoadConfiguration();
|
||||
var logDir = Environment.ExpandEnvironmentVariables(config.LogDirectory);
|
||||
_logger = new AgentLogger(logDir, config.EnableDebugLogging);
|
||||
|
||||
_logger.Info("Windows Service starting");
|
||||
|
||||
_monitoringService = new MonitoringService(config, _logger);
|
||||
_monitoringService.Start();
|
||||
|
||||
_logger.Info("Windows Service started");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error("Failed to start service", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger?.Info("Windows Service stopping");
|
||||
_monitoringService?.Stop();
|
||||
_monitoringService?.Dispose();
|
||||
_logger?.Info("Windows Service stopped");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error("Error stopping service", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private AgentConfiguration LoadConfiguration()
|
||||
{
|
||||
// Load or create persisted session ID
|
||||
var dataDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor");
|
||||
var sessionPersistence = new SessionPersistence(dataDirectory);
|
||||
var sessionId = sessionPersistence.LoadOrCreateSessionId();
|
||||
|
||||
return new AgentConfiguration
|
||||
{
|
||||
//ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "http://localhost:7071",
|
||||
ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "https://autopilotmonitor-func.azurewebsites.net",
|
||||
SessionId = sessionId,
|
||||
TenantId = "default-tenant",
|
||||
SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"),
|
||||
LogDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Logs"),
|
||||
UploadIntervalSeconds = 30,
|
||||
MaxBatchSize = 100,
|
||||
EnableDebugLogging = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using AutopilotMonitor.Agent.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Agent.Core.Monitoring.Core;
|
||||
@@ -22,85 +21,97 @@ namespace AutopilotMonitor.Agent
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if running in console mode (for testing)
|
||||
if (args.Contains("--console") || Environment.UserInteractive)
|
||||
{
|
||||
RunConsole(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
RunService();
|
||||
}
|
||||
// Always run directly - no ServiceBase.Run.
|
||||
// The agent is started by the Scheduled Task (SYSTEM) or manually with --console.
|
||||
// Both paths land here and run identically; --console just enables console output.
|
||||
RunAgent(args);
|
||||
}
|
||||
|
||||
static void RunConsole(string[] args)
|
||||
static void RunAgent(string[] args)
|
||||
{
|
||||
Console.WriteLine("Autopilot Monitor Agent - Console Mode");
|
||||
Console.WriteLine("======================================");
|
||||
Console.WriteLine();
|
||||
var consoleMode = args.Contains("--console") || Environment.UserInteractive;
|
||||
|
||||
if (consoleMode)
|
||||
{
|
||||
Console.WriteLine("Autopilot Monitor Agent");
|
||||
Console.WriteLine("=======================");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = LoadConfiguration(args);
|
||||
var logDir = Environment.ExpandEnvironmentVariables(config.LogDirectory);
|
||||
var logger = new AgentLogger(logDir, enableDebug: true);
|
||||
var logger = new AgentLogger(logDir, enableDebug: config.EnableDebugLogging);
|
||||
|
||||
logger.Info("");
|
||||
logger.Info("============================================================================================================");
|
||||
logger.Info("====================================== Agent starting in console mode ======================================");
|
||||
logger.Info($"============================== Agent starting ({(consoleMode ? "console" : "background/SYSTEM")}) ==============================");
|
||||
logger.Info("============================================================================================================");
|
||||
Console.WriteLine($"Session ID: {config.SessionId}");
|
||||
Console.WriteLine($"Tenant ID: {config.TenantId}");
|
||||
Console.WriteLine($"API URL: {config.ApiBaseUrl}");
|
||||
Console.WriteLine($"Log Directory: {logDir}");
|
||||
|
||||
if (config.EnableSimulator)
|
||||
if (consoleMode)
|
||||
{
|
||||
Console.WriteLine($"Simulator Mode: ENABLED");
|
||||
Console.WriteLine($"Simulate Failure: {(config.SimulateFailure ? "YES" : "NO")}");
|
||||
if (!string.IsNullOrEmpty(config.SimulationLogDirectory))
|
||||
Console.WriteLine($"Session ID: {config.SessionId}");
|
||||
Console.WriteLine($"Tenant ID: {config.TenantId}");
|
||||
Console.WriteLine($"API URL: {config.ApiBaseUrl}");
|
||||
Console.WriteLine($"Log Dir: {logDir}");
|
||||
Console.WriteLine($"Keep Logs: {config.KeepLogFile}");
|
||||
if (config.EnableSimulator)
|
||||
{
|
||||
Console.WriteLine($"Simulation Log Dir: {config.SimulationLogDirectory}");
|
||||
Console.WriteLine($"Simulation Speed: {config.SimulationSpeedFactor}x");
|
||||
Console.WriteLine($"Simulator: ENABLED (Failure={config.SimulateFailure})");
|
||||
if (!string.IsNullOrEmpty(config.SimulationLogDirectory))
|
||||
Console.WriteLine($"Sim Log Dir: {config.SimulationLogDirectory} ({config.SimulationSpeedFactor}x)");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.RebootOnComplete)
|
||||
{
|
||||
Console.WriteLine($"Reboot on Complete: ENABLED");
|
||||
}
|
||||
|
||||
if (config.EnableGeoLocation)
|
||||
{
|
||||
Console.WriteLine($"GeoLocation Detection: ENABLED");
|
||||
}
|
||||
|
||||
if (config.UseClientCertAuth)
|
||||
{
|
||||
Console.WriteLine($"Client Certificate Auth: ENABLED");
|
||||
if (!string.IsNullOrEmpty(config.ClientCertThumbprint))
|
||||
if (config.RebootOnComplete) Console.WriteLine("Reboot: ON COMPLETE");
|
||||
if (config.UseClientCertAuth)
|
||||
{
|
||||
Console.WriteLine($"Certificate Thumbprint: {config.ClientCertThumbprint}");
|
||||
Console.WriteLine($"Cert Auth: ENABLED");
|
||||
if (!string.IsNullOrEmpty(config.ClientCertThumbprint))
|
||||
Console.WriteLine($"Thumbprint: {config.ClientCertThumbprint}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
using (var service = new MonitoringService(config, logger))
|
||||
{
|
||||
service.Start();
|
||||
Console.WriteLine("Agent is running. Press Enter to stop...");
|
||||
Console.ReadLine();
|
||||
service.Stop();
|
||||
|
||||
if (consoleMode)
|
||||
{
|
||||
Console.WriteLine("Agent is running. Press Enter to stop...");
|
||||
Console.ReadLine();
|
||||
service.Stop();
|
||||
Console.WriteLine("Agent stopped.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Running as Scheduled Task under SYSTEM - block until the monitoring
|
||||
// service signals completion via self-destruct / process exit.
|
||||
service.WaitForCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Agent stopped");
|
||||
Console.WriteLine("Agent stopped.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"ERROR: {ex.Message}");
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
// Last-resort: write to a crash file next to the log directory since we
|
||||
// cannot use the Event Log (no traces policy).
|
||||
try
|
||||
{
|
||||
var crashPath = Path.Combine(
|
||||
Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Logs"),
|
||||
$"crash_{DateTime.UtcNow:yyyyMMdd_HHmmss}.log");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(crashPath));
|
||||
File.WriteAllText(crashPath, $"[{DateTime.UtcNow:u}] FATAL: {ex}");
|
||||
}
|
||||
catch { /* nowhere left to log */ }
|
||||
|
||||
if (consoleMode)
|
||||
{
|
||||
Console.WriteLine($"ERROR: {ex.Message}");
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +132,7 @@ namespace AutopilotMonitor.Agent
|
||||
var deviceConfigs = new[]
|
||||
{
|
||||
new { TenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359", SimulateFailure = false, DeviceName = "Device-1" },
|
||||
new { TenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359", SimulateFailure = true, DeviceName = "Device-2" }, // This one will fail
|
||||
new { TenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359", SimulateFailure = true, DeviceName = "Device-2" },
|
||||
new { TenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359", SimulateFailure = false, DeviceName = "Device-3" },
|
||||
new { TenantId = "a53834b7-42bc-46a3-b004-369735c3acf9", SimulateFailure = false, DeviceName = "Device-4" },
|
||||
new { TenantId = "deadbeef-dead-beef-dead-beefdeadbeef", SimulateFailure = false, DeviceName = "Device-5" }
|
||||
@@ -130,7 +141,6 @@ namespace AutopilotMonitor.Agent
|
||||
var tasks = new List<System.Threading.Tasks.Task>();
|
||||
var services = new List<MonitoringService>();
|
||||
|
||||
// Start all 5 instances in parallel
|
||||
for (int i = 0; i < deviceConfigs.Length; i++)
|
||||
{
|
||||
var deviceConfig = deviceConfigs[i];
|
||||
@@ -150,15 +160,13 @@ namespace AutopilotMonitor.Agent
|
||||
SimulateFailure = deviceConfig.SimulateFailure,
|
||||
UseClientCertAuth = baseConfig.UseClientCertAuth,
|
||||
ClientCertThumbprint = baseConfig.ClientCertThumbprint,
|
||||
CleanupOnExit = false, // Don't cleanup in mass rollout mode for easier inspection
|
||||
SelfDestructOnComplete = false, // Don't self-destruct in mass rollout mode
|
||||
RebootOnComplete = false, // Don't reboot in mass rollout mode
|
||||
EnableGeoLocation = false // Disable geo-location to reduce noise
|
||||
CleanupOnExit = false,
|
||||
SelfDestructOnComplete = false,
|
||||
RebootOnComplete = false,
|
||||
EnableGeoLocation = false
|
||||
};
|
||||
|
||||
var logDir = Environment.ExpandEnvironmentVariables(config.LogDirectory);
|
||||
|
||||
// Ensure directories exist
|
||||
Directory.CreateDirectory(config.SpoolDirectory);
|
||||
Directory.CreateDirectory(config.LogDirectory);
|
||||
|
||||
@@ -173,19 +181,13 @@ namespace AutopilotMonitor.Agent
|
||||
var service = new MonitoringService(config, logger);
|
||||
services.Add(service);
|
||||
|
||||
// Start each service in a separate task
|
||||
var task = System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
service.Start();
|
||||
|
||||
// Keep the service running until enrollment completes
|
||||
// The simulator will automatically complete and emit enrollment_complete or enrollment_failed
|
||||
while (true)
|
||||
{
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -201,36 +203,17 @@ namespace AutopilotMonitor.Agent
|
||||
Console.WriteLine("Press Enter to stop all instances...");
|
||||
Console.ReadLine();
|
||||
|
||||
// Stop all services
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Stopping all instances...");
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
service.Stop();
|
||||
service.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping service: {ex.Message}");
|
||||
}
|
||||
try { service.Stop(); service.Dispose(); }
|
||||
catch (Exception ex) { Console.WriteLine($"Error stopping service: {ex.Message}"); }
|
||||
}
|
||||
|
||||
Console.WriteLine("All instances stopped.");
|
||||
}
|
||||
|
||||
static void RunService()
|
||||
{
|
||||
var service = new AutopilotMonitorService();
|
||||
ServiceBase.Run(service);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the Azure AD Tenant ID from the Windows Registry.
|
||||
/// Looks for enrollments with EnrollmentType = 6 (AAD Join).
|
||||
/// </summary>
|
||||
/// <returns>The AAD Tenant ID if found, otherwise null.</returns>
|
||||
static string GetTenantIdFromRegistry()
|
||||
{
|
||||
try
|
||||
@@ -240,113 +223,81 @@ namespace AutopilotMonitor.Agent
|
||||
using (var enrollmentsKey = Registry.LocalMachine.OpenSubKey(enrollmentsKeyPath))
|
||||
{
|
||||
if (enrollmentsKey == null)
|
||||
{
|
||||
Console.WriteLine("Registry key not found: HKLM\\" + enrollmentsKeyPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Iterate through all enrollment GUIDs
|
||||
foreach (var enrollmentGuid in enrollmentsKey.GetSubKeyNames())
|
||||
{
|
||||
using (var enrollmentKey = enrollmentsKey.OpenSubKey(enrollmentGuid))
|
||||
{
|
||||
if (enrollmentKey == null)
|
||||
continue;
|
||||
if (enrollmentKey == null) continue;
|
||||
|
||||
// Check if this is an AAD Join enrollment (EnrollmentType = 6)
|
||||
var enrollmentType = enrollmentKey.GetValue("EnrollmentType");
|
||||
if (enrollmentType != null && Convert.ToInt32(enrollmentType) == 6)
|
||||
{
|
||||
// Try to get AADTenantID
|
||||
var tenantId = enrollmentKey.GetValue("AADTenantID");
|
||||
if (tenantId != null)
|
||||
{
|
||||
var tenantIdString = tenantId.ToString();
|
||||
Console.WriteLine($"Found AAD Tenant ID in registry: {tenantIdString} (Enrollment: {enrollmentGuid})");
|
||||
return tenantIdString;
|
||||
}
|
||||
return tenantId.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("No AAD Join enrollment (EnrollmentType=6) with AADTenantID found in registry.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error reading Tenant ID from registry: {ex.Message}");
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static AgentConfiguration LoadConfiguration(string[] args = null)
|
||||
{
|
||||
// Parse command-line arguments
|
||||
var enableSimulator = args?.Contains("--simulator") ?? false;
|
||||
var simulateFailure = args?.Contains("--simulate-failure") ?? false;
|
||||
var noAuth = args?.Contains("--no-auth") ?? false;
|
||||
var noCleanup = args?.Contains("--no-cleanup") ?? false;
|
||||
var rebootOnComplete = args?.Contains("--reboot-on-complete") ?? false;
|
||||
var disableGeoLocation = args?.Contains("--disable-geolocation") ?? false;
|
||||
var newSession = args?.Contains("--new-session") ?? false;
|
||||
var enableSimulator = args?.Contains("--simulator") ?? false;
|
||||
var simulateFailure = args?.Contains("--simulate-failure") ?? false;
|
||||
var noAuth = args?.Contains("--no-auth") ?? false;
|
||||
var noCleanup = args?.Contains("--no-cleanup") ?? false;
|
||||
var rebootOnComplete = args?.Contains("--reboot-on-complete") ?? false;
|
||||
var disableGeoLoc = args?.Contains("--disable-geolocation") ?? false;
|
||||
var newSession = args?.Contains("--new-session") ?? false;
|
||||
var keepLogFile = args?.Contains("--keep-logfile") ?? false;
|
||||
|
||||
// Parse certificate thumbprint if provided
|
||||
string certThumbprint = null;
|
||||
string tenantIdOverride = null;
|
||||
string imeLogPathOverride = null;
|
||||
string simulationLogDirectory = null;
|
||||
string certThumbprint = null;
|
||||
string tenantIdOverride = null;
|
||||
string imeLogPathOverride = null;
|
||||
string simulationLogDir = null;
|
||||
double simulationSpeedFactor = 50;
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
var thumbprintIndex = Array.IndexOf(args, "--cert-thumbprint");
|
||||
if (thumbprintIndex >= 0 && thumbprintIndex + 1 < args.Length)
|
||||
{
|
||||
certThumbprint = args[thumbprintIndex + 1];
|
||||
}
|
||||
|
||||
var tenantIdIndex = Array.IndexOf(args, "--tenant-id");
|
||||
if (tenantIdIndex >= 0 && tenantIdIndex + 1 < args.Length)
|
||||
{
|
||||
tenantIdOverride = args[tenantIdIndex + 1];
|
||||
}
|
||||
|
||||
var imeLogPathIndex = Array.IndexOf(args, "--ime-log-path");
|
||||
if (imeLogPathIndex >= 0 && imeLogPathIndex + 1 < args.Length)
|
||||
{
|
||||
imeLogPathOverride = args[imeLogPathIndex + 1];
|
||||
Console.WriteLine($"Using custom IME log path: {imeLogPathOverride}");
|
||||
}
|
||||
|
||||
var simLogDirIndex = Array.IndexOf(args, "--simulation-log-dir");
|
||||
if (simLogDirIndex >= 0 && simLogDirIndex + 1 < args.Length)
|
||||
{
|
||||
simulationLogDirectory = args[simLogDirIndex + 1];
|
||||
Console.WriteLine($"Using simulation log directory: {simulationLogDirectory}");
|
||||
}
|
||||
simulationLogDir = args[simLogDirIndex + 1];
|
||||
|
||||
var simSpeedIndex = Array.IndexOf(args, "--simulation-speed-factor");
|
||||
if (simSpeedIndex >= 0 && simSpeedIndex + 1 < args.Length)
|
||||
{
|
||||
if (double.TryParse(args[simSpeedIndex + 1], out var speed))
|
||||
{
|
||||
simulationSpeedFactor = speed;
|
||||
Console.WriteLine($"Using simulation speed factor: {simulationSpeedFactor}x");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults (fallback values if no config file exists)
|
||||
//string apiBaseUrl = "http://localhost:7071";
|
||||
string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net";
|
||||
int uploadIntervalSeconds = 30;
|
||||
bool cleanupOnExit = true;
|
||||
bool selfDestructOnComplete = true;
|
||||
bool rebootOnCompleteConfig = false;
|
||||
bool enableGeoLocationConfig = true;
|
||||
bool useClientCertAuthConfig = true;
|
||||
// Defaults
|
||||
string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net";
|
||||
int uploadIntervalSeconds = 30;
|
||||
bool cleanupOnExit = true;
|
||||
bool selfDestructOnComplete = true;
|
||||
bool rebootOnCompleteConfig = false;
|
||||
bool enableGeoLocationConfig = true;
|
||||
bool useClientCertAuthConfig = true;
|
||||
|
||||
// Try to load configuration from JSON file (written by PowerShell bootstrap script)
|
||||
// Load configuration JSON written by PowerShell bootstrap script
|
||||
var configFilePath = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Config\agent-config.json");
|
||||
if (File.Exists(configFilePath))
|
||||
{
|
||||
@@ -378,91 +329,65 @@ namespace AutopilotMonitor.Agent
|
||||
if (configDict.ContainsKey("useClientCertAuth") && configDict["useClientCertAuth"] != null)
|
||||
useClientCertAuthConfig = Convert.ToBoolean(configDict["useClientCertAuth"]);
|
||||
|
||||
Console.WriteLine($"Loaded configuration from {configFilePath}");
|
||||
if (configDict.ContainsKey("keepLogFile") && configDict["keepLogFile"] != null)
|
||||
keepLogFile = keepLogFile || Convert.ToBoolean(configDict["keepLogFile"]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Failed to load configuration file: {ex.Message}");
|
||||
Console.WriteLine("Using default configuration");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Configuration file not found at {configFilePath}");
|
||||
Console.WriteLine("Using default configuration");
|
||||
catch { /* use defaults */ }
|
||||
}
|
||||
|
||||
// Environment variable overrides config file
|
||||
apiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? apiBaseUrl;
|
||||
|
||||
// Command-line arguments override everything (for --no-cleanup)
|
||||
if (noCleanup)
|
||||
{
|
||||
cleanupOnExit = false;
|
||||
selfDestructOnComplete = false;
|
||||
}
|
||||
|
||||
// Determine TenantId: Command-line arg > Registry > Fallback
|
||||
// Determine TenantId: command-line arg > registry > fallback
|
||||
string tenantId;
|
||||
if (!string.IsNullOrEmpty(tenantIdOverride))
|
||||
{
|
||||
// Command-line argument takes precedence
|
||||
tenantId = tenantIdOverride;
|
||||
Console.WriteLine($"Using Tenant ID from command-line argument: {tenantId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to read from registry
|
||||
tenantId = GetTenantIdFromRegistry();
|
||||
|
||||
if (string.IsNullOrEmpty(tenantId))
|
||||
{
|
||||
// Fallback to default (for testing/development)
|
||||
tenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359";
|
||||
Console.WriteLine($"Using fallback Tenant ID: {tenantId}");
|
||||
}
|
||||
tenantId = "b54dc1af-5320-4f60-b5d4-821e0cf2a359"; // fallback for dev/testing
|
||||
}
|
||||
|
||||
// Load or create persisted session ID
|
||||
var dataDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor");
|
||||
var sessionPersistence = new SessionPersistence(dataDirectory);
|
||||
var dataDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor");
|
||||
var sessionPersist = new SessionPersistence(dataDirectory);
|
||||
|
||||
// Force new session if --new-session flag is set
|
||||
if (newSession)
|
||||
{
|
||||
Console.WriteLine("--new-session flag detected, deleting existing session...");
|
||||
sessionPersistence.DeleteSession();
|
||||
}
|
||||
sessionPersist.DeleteSession();
|
||||
|
||||
var sessionId = sessionPersistence.LoadOrCreateSessionId();
|
||||
|
||||
var sessionStatus = sessionPersistence.SessionExists() && File.GetCreationTime(Path.Combine(dataDirectory, "session.id")) < DateTime.Now.AddMinutes(-1)
|
||||
? "Restored from previous session"
|
||||
: "New session created";
|
||||
Console.WriteLine($"Session ID: {sessionId} ({sessionStatus})");
|
||||
var sessionId = sessionPersist.LoadOrCreateSessionId();
|
||||
|
||||
return new AgentConfiguration
|
||||
{
|
||||
ApiBaseUrl = apiBaseUrl,
|
||||
SessionId = sessionId,
|
||||
TenantId = tenantId,
|
||||
SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"),
|
||||
LogDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Logs"),
|
||||
ApiBaseUrl = apiBaseUrl,
|
||||
SessionId = sessionId,
|
||||
TenantId = tenantId,
|
||||
SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"),
|
||||
LogDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Logs"),
|
||||
UploadIntervalSeconds = uploadIntervalSeconds,
|
||||
MaxBatchSize = 100,
|
||||
EnableDebugLogging = true,
|
||||
EnableSimulator = enableSimulator,
|
||||
SimulateFailure = simulateFailure,
|
||||
UseClientCertAuth = !noAuth && useClientCertAuthConfig,
|
||||
ClientCertThumbprint = certThumbprint,
|
||||
CleanupOnExit = cleanupOnExit,
|
||||
MaxBatchSize = 100,
|
||||
EnableDebugLogging = true,
|
||||
EnableSimulator = enableSimulator,
|
||||
SimulateFailure = simulateFailure,
|
||||
UseClientCertAuth = !noAuth && useClientCertAuthConfig,
|
||||
ClientCertThumbprint = certThumbprint,
|
||||
CleanupOnExit = cleanupOnExit,
|
||||
SelfDestructOnComplete = selfDestructOnComplete,
|
||||
RebootOnComplete = rebootOnComplete || rebootOnCompleteConfig,
|
||||
EnableGeoLocation = !disableGeoLocation && enableGeoLocationConfig,
|
||||
ImeLogPathOverride = imeLogPathOverride,
|
||||
SimulationLogDirectory = simulationLogDirectory,
|
||||
SimulationSpeedFactor = simulationSpeedFactor
|
||||
RebootOnComplete = rebootOnComplete || rebootOnCompleteConfig,
|
||||
EnableGeoLocation = !disableGeoLoc && enableGeoLocationConfig,
|
||||
ImeLogPathOverride = imeLogPathOverride,
|
||||
SimulationLogDirectory = simulationLogDir,
|
||||
SimulationSpeedFactor = simulationSpeedFactor,
|
||||
KeepLogFile = keepLogFile
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user