using GeoVLog.Core.Crypto; using GeoVLog.Core.Hdf5; using GeoVLog.Core.Manifests; using GeoVLog.Core.Models; using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; using System.Security.Cryptography; using System.ServiceProcess; using System.Threading; namespace GeoVLogSvc { /// /// Windows Service for GeoVLog. Handles sensor logging to HDF5 and manifest encryption on shutdown. /// Supports graceful cleanup via PreShutdown and normal stop. /// [SupportedOSPlatform("windows")] public sealed class GeoVLogService : ServiceBase { private H5LogQueueWorker? _h5Worker; private FlightManifest? _manifest; private string? _manifestPath; private byte[]? _aesKey; private IFlightManifestSerializer? _serializer; private bool _parsingEnabled = false; private readonly object _cleanupLock = new(); private bool _hasCleanedUp = false; private bool _shutdownInitiated = false; private const string EventSourceName = "GeoVLogSvc"; private const string EventLogName = "Application"; /// /// Initializes a new instance of the GeoVLogService. Sets up PreShutdown handling and registers EventLog source. /// public GeoVLogService() { this.ServiceName = EventSourceName; this.CanStop = true; this.CanShutdown = true; this.AutoLog = true; // Enable PreShutdown via reflection (not exposed directly) FieldInfo? cmdField = typeof(ServiceBase).GetField("acceptedCommands", BindingFlags.Instance | BindingFlags.NonPublic); if (cmdField != null) { int currentFlags = (int)cmdField.GetValue(this)!; cmdField.SetValue(this, currentFlags | 0x100); // 0x100 = SERVICE_ACCEPT_PRESHUTDOWN } // Ensure EventLog source exists try { if (!EventLog.SourceExists(EventSourceName)) { EventLog.CreateEventSource(new EventSourceCreationData(EventSourceName, EventLogName)); } } catch (Exception ex) { Console.WriteLine($"[GeoVLog] WARNING: Unable to register EventLog source: {ex.Message}"); } } /// /// Starts the service when running as a console application. /// /// Command line arguments. public void StartConsole(string[] args) => OnStart(args); /// /// Stops the service when running as a console application. /// public void StopConsole() => OnStop(); /// /// Starts the service and initializes logging infrastructure. /// /// Service arguments (unused) protected override void OnStart(string[] args) { try { string logDir = Path.Combine(AppContext.BaseDirectory, "Logs"); Directory.CreateDirectory(logDir); _manifest = new FlightManifest { FlightStartUtc = DateTime.UtcNow }; _manifestPath = Path.Combine(logDir, "manifest.bin"); _aesKey = RandomNumberGenerator.GetBytes(AesGcmHelper.KeySize); _serializer = new FlightManifestProtoSerializer(); var flushOptions = new H5LogFlushOptions { EnableAutoFlush = true, FlushInterval = TimeSpan.FromSeconds(1) }; _h5Worker = new H5LogQueueWorker(logDir, _manifest, _manifestPath, _aesKey, _serializer, flushOptions); _h5Worker.RegisterSensor(SensorId.Gps, "/Sensors/GPS", enableParse: false, schema: SensorSchema.Gps); _h5Worker.RegisterSensor(SensorId.Imu, "/Sensors/IMU", enableParse: false, schema: SensorSchema.Imu); _h5Worker.RegisterSensor(SensorId.Mag1, "/Sensors/MAG1", enableParse: false, schema: SensorSchema.Mag); _h5Worker.RegisterSensor(SensorId.Mag2, "/Sensors/MAG2", enableParse: false, schema: SensorSchema.Mag); _h5Worker.RegisterSensor(SensorId.Vlf, "/Sensors/VLF", enableParse: false, schema: SensorSchema.Vlf); _h5Worker.RegisterSensor(SensorId.RadarAltimeter, "/Sensors/RadarAlt", enableParse: false, schema: SensorSchema.RadarAlt); EventLog.WriteEntry(EventSourceName, "GeoVLogService started. Logging active.", EventLogEntryType.Information); } catch (Exception ex) { EventLog.WriteEntry(EventSourceName, $"GeoVLogService OnStart error: {ex}", EventLogEntryType.Error); throw; } } /// /// Called when service is stopped by SCM or manually. Triggers cleanup. /// protected override void OnStop() { string reason = _shutdownInitiated ? "system shutdown" : "manual stop"; EventLog.WriteEntry(EventSourceName, $"GeoVLogService stopping due to {reason}.", EventLogEntryType.Information); PerformCleanup(); } /// /// Called during system shutdown. Ensures cleanup before termination. /// protected override void OnShutdown() { EventLog.WriteEntry(EventSourceName, "GeoVLogService received system shutdown (OnShutdown).", EventLogEntryType.Information); PerformCleanup(); base.OnShutdown(); } /// /// Handles PreShutdown command (code 15). Ensures early cleanup during shutdown. /// /// The SCM command code protected override void OnCustomCommand(int command) { if (command == 15) // SERVICE_CONTROL_PRESHUTDOWN { _shutdownInitiated = true; EventLog.WriteEntry(EventSourceName, "PreShutdown event received.", EventLogEntryType.Information); PerformCleanup(); try { Stop(); } catch (Exception ex) { EventLog.WriteEntry(EventSourceName, $"Error during Stop(): {ex}", EventLogEntryType.Error); } return; } base.OnCustomCommand(command); } /// /// Enable or disable parsing of sensor messages at runtime. /// public void SetParsingEnabled(bool enable) { _parsingEnabled = enable; _h5Worker?.SetParsingEnabledForAll(enable); } /// /// Flushes HDF5 log, writes and encrypts manifest, clears sensitive data. Runs once. /// private void PerformCleanup() { lock (_cleanupLock) { if (_hasCleanedUp) return; _hasCleanedUp = true; } if (_h5Worker != null) { try { _h5Worker.Dispose(); EventLog.WriteEntry(EventSourceName, "HDF5 worker disposed.", EventLogEntryType.Information); } catch (Exception ex) { EventLog.WriteEntry(EventSourceName, $"Error disposing HDF5: {ex}", EventLogEntryType.Error); } } if (_manifest != null && _aesKey != null && _manifestPath != null && _serializer != null) { try { _manifest.FlightEndUtc = DateTime.UtcNow; ManifestIO.WriteAsync(_manifest, _aesKey, _manifestPath, _serializer).GetAwaiter().GetResult(); EventLog.WriteEntry(EventSourceName, $"Manifest saved and encrypted to {_manifestPath}.", EventLogEntryType.Information); } catch (Exception ex) { EventLog.WriteEntry(EventSourceName, $"Error writing manifest: {ex}", EventLogEntryType.Error); } } if (_aesKey != null) { Array.Clear(_aesKey, 0, _aesKey.Length); } } /// /// Releases any remaining resources. /// /// True if managed resources should be disposed. protected override void Dispose(bool disposing) { base.Dispose(disposing); } } }