527 lines
23 KiB
C#
527 lines
23 KiB
C#
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<string, string>(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<InventoryContext>(options => options.UseSqlServer(dbCon));
|
|
|
|
services.AddHostedService<AppHostService>();
|
|
services.AddHttpClient();
|
|
services.AddSingleton<SystemInfoCollector>(provider =>
|
|
{
|
|
var collector = new SystemInfoCollector();
|
|
collector.Consumer = ConsumerType.AdminTool; // Explicitly set the consumer
|
|
return collector;
|
|
});
|
|
services.AddScoped<DatabaseUpdater>(provider => new DatabaseUpdater(
|
|
provider.GetRequiredService<InventoryContext>(),
|
|
provider.GetRequiredService<SystemInfoCollector>(),
|
|
provider.GetRequiredService<HealthMonitor>(),
|
|
ConsumerType.AdminTool
|
|
));
|
|
services.AddScoped<HealthMonitor>();
|
|
services.AddScoped<SlurpitClient>();
|
|
services.AddScoped<UpdateWorkflow>(provider => new UpdateWorkflow(
|
|
provider.GetRequiredService<DatabaseUpdater>(),
|
|
provider.GetRequiredService<SystemInfoCollector>(),
|
|
provider.GetRequiredService<InventoryContext>(),
|
|
provider.GetRequiredService<SlurpitClient>()
|
|
));
|
|
});
|
|
}
|
|
|
|
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<InventoryContext>();
|
|
await dbContext.Database.MigrateAsync(cancellationToken);
|
|
|
|
var collector = services.GetRequiredService<SystemInfoCollector>();
|
|
var healthMonitor = services.GetRequiredService<HealthMonitor>();
|
|
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<UpdateWorkflow>();
|
|
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<InventoryContext>();
|
|
var collector = scope.ServiceProvider.GetRequiredService<SystemInfoCollector>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|