using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Principal; using System.Text.Json; using System.Threading.Tasks; using Inventory.Core; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using DotNetEnv; namespace Inventory.AdminTool { class Program { static async Task Main(string[] args) { if (OperatingSystem.IsWindows() && !IsAdministrator()) { Console.WriteLine("This tool requires administrator privileges to collect all system information."); Console.WriteLine("Attempting to restart with elevated permissions..."); try { var exeName = Process.GetCurrentProcess().MainModule.FileName; var startInfo = new ProcessStartInfo(exeName) { Verb = "runas", Arguments = string.Join(" ", args), UseShellExecute = true }; Process.Start(startInfo); return; // Exit the non-elevated instance } catch (Exception ex) { Console.WriteLine($"Failed to restart with admin privileges. Please run your terminal as an administrator and try again. Error: {ex.Message}"); return; } } var host = CreateHostBuilder(args).Build(); await host.RunAsync(); } private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureHostConfiguration(config => { #if DEBUG config.AddInMemoryCollection(new[] { new KeyValuePair(HostDefaults.EnvironmentKey, "Development") }); #endif }) .ConfigureLogging(logging => { // Suppress informational logs from Entity Framework Core logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning); }) .ConfigureAppConfiguration((context, config) => { // Setup secrets here, after the default configuration and environment are established. // This ensures Secrets are populated before ConfigureServices is called. EnvironmentBuilder.SetupEnvironment(context.HostingEnvironment); }) .ConfigureServices((context, services) => { var dbCon = Secrets.DbConnectionString; if (string.IsNullOrWhiteSpace(dbCon)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("FATAL ERROR: DB_CONNECTION_STRING is not configured."); Console.WriteLine("Ensure it is in your .env file or Vault. The application cannot start."); Console.ResetColor(); } services.AddDbContext(options => options.UseSqlServer(dbCon)); services.AddHostedService(); services.AddHttpClient(); services.AddSingleton(provider => { var collector = new SystemInfoCollector(); collector.Consumer = ConsumerType.AdminTool; // Explicitly set the consumer return collector; }); services.AddScoped(provider => new DatabaseUpdater( provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), ConsumerType.AdminTool )); services.AddScoped(); services.AddScoped(); services.AddScoped(provider => new UpdateWorkflow( provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService() )); }); } private static bool IsAdministrator() { if (!OperatingSystem.IsWindows()) { // On non-Windows systems, we can assume root access is handled differently (e.g., via sudo). return true; } var identity = WindowsIdentity.GetCurrent(); var principal = new WindowsPrincipal(identity); return principal.IsInRole(WindowsBuiltInRole.Administrator); } } public class AppHostService : IHostedService { private readonly IHostApplicationLifetime _lifetime; private readonly IServiceProvider _serviceProvider; public AppHostService(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime) { _lifetime = lifetime; _serviceProvider = serviceProvider; } public async Task StartAsync(CancellationToken cancellationToken) { bool errorOccurred = false; try { using var scope = _serviceProvider.CreateScope(); var services = scope.ServiceProvider; // Apply migrations on startup var dbContext = services.GetRequiredService(); await dbContext.Database.MigrateAsync(cancellationToken); var collector = services.GetRequiredService(); var healthMonitor = services.GetRequiredService(); DisplaySystemInfo(collector, healthMonitor); await HandleLocationUpdate(services); Console.Write("\nDo you want to force an upsert now? (y/n): "); var response = Console.ReadLine(); if (response?.Equals("y", StringComparison.OrdinalIgnoreCase) == true) { var updateWorkflow = services.GetRequiredService(); await updateWorkflow.Run(); Console.WriteLine("Upsert completed successfully."); if (OperatingSystem.IsWindows()) { Console.WriteLine("Registering and starting the Inventory Agent service..."); RegisterAndStartService(); } } } catch (Exception ex) { errorOccurred = true; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\n\nAn unexpected error occurred:"); Console.WriteLine(ex.ToString()); Console.ResetColor(); } finally { Console.WriteLine(errorOccurred ? "\nAn error occurred. The application will remain open. Press any key to exit." : "\nExecution finished. Press any key to exit."); Console.ReadKey(); } _lifetime.StopApplication(); } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } private static string FormatBytes(ulong bytes) { if (bytes == 0) return "0 B"; const int scale = 1024; string[] orders = { "B", "KB", "MB", "GB", "TB", "PB" }; var orderIndex = (int)Math.Floor(Math.Log(bytes, scale)); if (orderIndex >= orders.Length) orderIndex = orders.Length - 1; double num = Math.Round(bytes / Math.Pow(scale, orderIndex), 2); return $"{num} {orders[orderIndex]}"; } private void DisplaySystemInfo(SystemInfoCollector collector, HealthMonitor healthMonitor) { Console.WriteLine("--- System Information ---"); Console.WriteLine($"Computer Name: {collector.GetComputerName()}"); Console.WriteLine($"Device Type: {collector.GetDeviceType()}"); Console.WriteLine($"Serial Number: {collector.GetSystemSerialNumber()}"); Console.WriteLine($"Motherboard Serial Number: {collector.GetMotherboardSerialNumber()}"); Console.WriteLine($"System UUID: {collector.GetSystemUUID()}"); Console.WriteLine($"Processor: {collector.GetProcessor()}"); Console.WriteLine($"RAM: {collector.GetTotalRAM()}"); Console.WriteLine($"OS Version: {collector.GetOSVersion()}"); Console.WriteLine($"OS Install Date: {collector.GetOSInstallDate()}"); Console.WriteLine($"OS License Key: {collector.GetOSLicenseKey()}"); var (ips, mac) = collector.GetNetworkInfo(); Console.WriteLine("IP Addresses:"); if (ips.Any()) { foreach (var ip in ips) { Console.WriteLine($" - {ip}"); } } Console.WriteLine($"MAC Address: {mac}"); Console.WriteLine($"Optical Drive: {collector.HasOpticalDrive()}"); Console.WriteLine("\n--- GPUs ---"); var gpus = collector.GetGPUs(); if (gpus.Any()) { gpus.ForEach(gpu => Console.WriteLine($" - {gpu}")); } else { Console.WriteLine(" No GPUs found."); } Console.WriteLine("\n--- Storage Devices ---"); var storage = collector.GetStorage(); if (storage.Any()) { foreach (var drive in storage) { Console.WriteLine($" - Model: {drive.Model ?? "N/A"}"); Console.WriteLine($" Serial: {drive.SerialNumber ?? "N/A"}"); Console.WriteLine($" Type: {drive.MediaType} ({drive.InterfaceType})"); Console.WriteLine($" Size: {FormatBytes(drive.Size)}"); } } else { Console.WriteLine(" No storage devices found."); } Console.WriteLine("\n--- Monitors ---"); var monitors = collector.GetMonitors(); if (monitors.Any()) { foreach (var monitor in monitors) { Console.WriteLine($" - Model: {monitor.Model ?? "N/A"}"); Console.WriteLine($" Serial: {monitor.SerialNumber ?? "N/A"}"); Console.WriteLine($" PnPDeviceID: {monitor.PnPDeviceID ?? "N/A"}"); } } else { Console.WriteLine(" No monitors found."); } Console.WriteLine("\n--- Printers ---"); var printers = collector.GetPrinters(); if (printers.Any()) { foreach (var printer in printers) { string type = printer.IsNetwork ? "Network" : "Local"; Console.WriteLine($" - Name: {printer.Name ?? "N/A"} ({type})"); Console.WriteLine($" Driver: {printer.DriverName ?? "N/A"}"); Console.WriteLine($" Port: {printer.PortName ?? "N/A"}"); Console.WriteLine($" Serial: {printer.SerialNumber ?? "N/A"}"); Console.WriteLine($" Host: {printer.HostName ?? "N/A"}"); } } else { Console.WriteLine(" No physical printers found."); } Console.WriteLine("\n--- Local Admins ---"); var admins = collector.GetLocalAdmins(); if (admins.Any()) { foreach (var admin in admins.OrderBy(a => a.Source).ThenBy(a => a.Name)) { string status = admin.IsEnabled == false ? " (Disabled)" : (admin.IsLockedOut == true ? " (Locked)" : ""); Console.WriteLine($" - {admin.Name} [{admin.AccountType} / {admin.Source}]{status}"); } } else { Console.WriteLine(" Could not retrieve local administrators."); } Console.WriteLine("\n--- Health Metrics (Requires Admin) ---"); try { var report = healthMonitor.CollectHealthMetrics(); Console.WriteLine($"CPU Temp: {(report.CpuTemp.HasValue ? $"{report.CpuTemp.Value}°C" : "N/A")}"); Console.WriteLine($"CPU Load: {(report.CpuLoad.HasValue ? $"{Math.Round(report.CpuLoad.Value, 2)}%" : "N/A")}"); Console.WriteLine($"CPU Power: {(report.CpuPower.HasValue ? $"{Math.Round(report.CpuPower.Value, 2)}W" : "N/A")}"); Console.WriteLine($"GPU Temp: {(report.GpuTemp.HasValue ? $"{report.GpuTemp.Value}°C" : "N/A")}"); Console.WriteLine($"GPU Load: {(report.GpuLoad.HasValue ? $"{Math.Round(report.GpuLoad.Value, 2)}%" : "N/A")}"); Console.WriteLine($"GPU Power: {(report.GpuPower.HasValue ? $"{Math.Round(report.GpuPower.Value, 2)}W" : "N/A")}"); Console.WriteLine($"GPU Clock: {(report.GpuClock.HasValue ? $"{Math.Round(report.GpuClock.Value, 0)}MHz" : "N/A")}"); Console.WriteLine($"RAM Load: {(report.RamLoad.HasValue ? $"{Math.Round(report.RamLoad.Value, 2)}%" : "N/A")}"); Console.WriteLine($"Battery Health: {(report.BatteryHealth.HasValue ? $"{report.BatteryHealth.Value}%" : "N/A")}"); Console.WriteLine("Fan Speeds:"); if (report.FanSpeeds.Any()) { foreach (var fan in report.FanSpeeds) { Console.WriteLine($" - {fan.Name}: {(fan.Rpm.HasValue ? $"{Math.Round(fan.Rpm.Value, 0)} RPM" : "N/A")}"); } } Console.WriteLine("Drive Health:"); if (report.DriveHealth.Any()) { foreach (var driveHealth in report.DriveHealth) { string failing = driveHealth.IsFailing == true ? " - PREDICTING FAILURE!" : ""; string health = driveHealth.HealthPercentage.HasValue ? $"Health: {driveHealth.HealthPercentage}% | " : ""; Console.WriteLine($" - {driveHealth.Model}: {health}Temp: {(driveHealth.Temperature.HasValue ? $"{driveHealth.Temperature.Value}°C" : "N/A")}{failing}"); } } else { Console.WriteLine(" No drive health sensors found."); } } catch (Exception ex) { Console.WriteLine($"Could not retrieve health metrics. Ensure the tool is run as Administrator. Error: {ex.Message}"); } Console.WriteLine("--------------------------"); } private async Task HandleLocationUpdate(IServiceProvider services) { using var scope = services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var collector = scope.ServiceProvider.GetRequiredService(); string? localIdentifier = GetLocalIdentifier(collector); if (string.IsNullOrWhiteSpace(localIdentifier)) { Console.WriteLine("\nCould not determine a unique identifier for this machine. Cannot update location."); return; } var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.HardwareIdentifier == localIdentifier); Console.WriteLine("\n--- Location Information ---"); Console.WriteLine($"Current Location: {device?.Location ?? "Not set"}"); Console.Write("Enter new location (leave blank to keep current): "); string? newLocation = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(newLocation)) { if (device == null) { // This case is unlikely if the update workflow runs first, but good to have. device = new Device { HardwareIdentifier = localIdentifier }; dbContext.Devices.Add(device); } device.Location = newLocation; await dbContext.SaveChangesAsync(); Console.WriteLine($"Location updated to '{newLocation}'."); } } private string? GetLocalIdentifier(SystemInfoCollector collector) { // This logic is duplicated from DatabaseUpdater to be used here. var identifiers = new[] { collector.GetMotherboardSerialNumber(), collector.GetSystemUUID(), collector.GetNetworkInfo().MACAddress }; return identifiers.FirstOrDefault(id => !string.IsNullOrWhiteSpace(id) && !id.Equals("To be filled by O.E.M.", StringComparison.OrdinalIgnoreCase)); } private void RegisterAndStartService() { const string serviceName = "InventoryAgent"; const string serviceDisplayName = "Inventory Agent"; try { var exePath = GetAgentExePath(); if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"FATAL: Inventory.Agent.exe not found. Searched at: {exePath}"); Console.ResetColor(); return; } Console.WriteLine($"Found Inventory.Agent.exe at: {exePath}"); // Stop and delete the service if it exists ExecuteCommand("sc.exe", $"stop {serviceName}", ignoreErrors: true); ExecuteCommand("sc.exe", $"delete {serviceName}", ignoreErrors: true); // Wait a moment for the service to be fully deleted System.Threading.Thread.Sleep(2000); // Create the service string createCommand = $"create {serviceName} binPath=\"{exePath}\" start=auto obj=LocalSystem DisplayName=\"{serviceDisplayName}\""; ExecuteCommand("sc.exe", createCommand); // Start the service ExecuteCommand("sc.exe", $"start {serviceName}"); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Service registered and started successfully."); Console.ResetColor(); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"An error occurred during service registration: {ex.Message}"); Console.ResetColor(); } } private string GetAgentExePath() { // This method robustly finds the Inventory.Agent.exe by searching upwards from the // current executable's location. This works for both deployed/installed structures // (e.g., .../publish/AdminTool/ and .../publish/Agent/) and development environments // (e.g., .../Inventory.AdminTool/bin/Debug/ and .../Inventory.Agent/bin/Debug/). try { string currentExePath = Process.GetCurrentProcess().MainModule.FileName; DirectoryInfo? startDir = Directory.GetParent(currentExePath); // Search up a maximum of 5 levels to find a directory that contains the "Agent" sub-folder. // This handles the "published" or "installed" application structure. DirectoryInfo? currentDir = startDir; for (int i = 0; i < 5 && currentDir != null; i++, currentDir = currentDir.Parent) { var agentDir = Path.Combine(currentDir.FullName, "Agent"); var agentExePath = Path.Combine(agentDir, "Inventory.Agent.exe"); if (File.Exists(agentExePath)) { return agentExePath; } } // Fallback for development: Search up to find a directory (likely the solution root) // and then search down for the agent executable in its project output folder. currentDir = startDir; for (int i = 0; i < 5 && currentDir != null; i++, currentDir = currentDir.Parent) { // Search for the exe in any subdirectory from this level. var agentFiles = Directory.GetFiles(currentDir.FullName, "Inventory.Agent.exe", SearchOption.AllDirectories); var agentExePath = agentFiles.FirstOrDefault(p => !p.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar)); if (agentExePath != null) { return agentExePath; } } return null; // Return null if not found after all attempts. } catch (Exception ex) { Console.WriteLine($"Error determining Agent path: {ex.Message}"); return null; } } private void ExecuteCommand(string fileName, string arguments, bool ignoreErrors = false) { var process = new Process { StartInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, } }; process.Start(); string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0 && !ignoreErrors) { throw new Exception($"Command '{fileName} {arguments}' failed with exit code {process.ExitCode}. Output: {output}. Error: {error}"); } if(!string.IsNullOrWhiteSpace(output)) { Console.WriteLine(output); } if(!string.IsNullOrWhiteSpace(error) && !ignoreErrors) { Console.WriteLine(error); } } } }