using System.Diagnostics; using System.Runtime.InteropServices; namespace Sonex.Library.WorkersCore; public static partial class Worker { public static bool EnsureWindowsServiceExists( string? serviceName = null, string? displayName = null, string startMode = "auto", string? description = null, string launchArguments = "--service") { var result = EnsureWindowsServiceExistsDetailed( serviceName, displayName, startMode, description, launchArguments); return result.Success; } public static ServiceRegistrationResult EnsureWindowsServiceExistsDetailed( string? serviceName = null, string? displayName = null, string startMode = "auto", string? description = null, string launchArguments = "--service") { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return ServiceRegistrationResult.Failed("Windows service registration is only supported on Windows."); string resolvedServiceName = string.IsNullOrWhiteSpace(serviceName) ? Name : serviceName.Trim(); if (string.IsNullOrWhiteSpace(resolvedServiceName)) return ServiceRegistrationResult.Failed("Service name cannot be empty."); string normalizedStartMode = NormalizeServiceStartMode(startMode); string? resolvedDescription = ResolveServiceDescription(description); if (ServiceExists(resolvedServiceName)) { var existingServiceConfig = ApplyServiceConfiguration( resolvedServiceName, normalizedStartMode, resolvedDescription); if (!existingServiceConfig.Success) { string warningMessage = $"Service '{resolvedServiceName}' exists, but configuration update failed. {existingServiceConfig.ErrorMessage}"; WriteServiceConsoleLine(warningMessage); return ServiceRegistrationResult.Warning(warningMessage); } return ServiceRegistrationResult.Completed($"Service '{resolvedServiceName}' already exists."); } string? exePath = Environment.ProcessPath; if (string.IsNullOrWhiteSpace(exePath)) return ServiceRegistrationResult.Failed("Unable to resolve executable path from Environment.ProcessPath."); string resolvedDisplayName = string.IsNullOrWhiteSpace(displayName) ? (string.IsNullOrWhiteSpace(Title) ? resolvedServiceName : Title.Trim()) : displayName.Trim(); string quotedBinPath = $"\\\"{exePath.Trim()}\\\" {launchArguments}".Trim(); string createCommandArguments = BuildCreateServiceCommandArguments( resolvedServiceName, quotedBinPath, normalizedStartMode, resolvedDisplayName); var createResult = RunScCommand(createCommandArguments); if (createResult.ExitCode != 0) { string errorMessage = BuildCommandFailureMessage( $"Service registration failed for '{resolvedServiceName}'.", createResult); WriteServiceConsoleLine(errorMessage); WriteServiceConsoleLine("To create the service manually, run this command in an Administrator console:"); WriteServiceConsoleLine($"sc.exe {createCommandArguments}"); if (!string.IsNullOrWhiteSpace(resolvedDescription)) { WriteServiceConsoleLine("Then set service description (Administrator console):"); WriteServiceConsoleLine($"sc.exe {BuildDescriptionCommandArguments(resolvedServiceName, resolvedDescription)}"); } return ServiceRegistrationResult.Failed(errorMessage); } var createdServiceConfig = ApplyServiceConfiguration( resolvedServiceName, normalizedStartMode, resolvedDescription); if (!createdServiceConfig.Success) { string warningMessage = $"Service '{resolvedServiceName}' was created, but configuration update failed. {createdServiceConfig.ErrorMessage}"; WriteServiceConsoleLine(warningMessage); return ServiceRegistrationResult.Warning(warningMessage); } if (!ServiceExists(resolvedServiceName)) { const string serviceMissingAfterCreateMessage = "Service creation returned success, but service is not queryable afterwards."; string errorMessage = $"Service '{resolvedServiceName}' registration failed. {serviceMissingAfterCreateMessage}"; WriteServiceConsoleLine(errorMessage); return ServiceRegistrationResult.Failed(errorMessage); } return ServiceRegistrationResult.Completed($"Service '{resolvedServiceName}' was created successfully."); } private static bool ServiceExists(string serviceName) { var queryResult = RunScCommand($"query \"{serviceName}\""); return queryResult.ExitCode == 0; } private static string NormalizeServiceStartMode(string startMode) { string token = string.IsNullOrWhiteSpace(startMode) ? "auto" : startMode.Trim().ToLowerInvariant(); return token switch { "auto" => "auto", "automatic" => "auto", "delayed-auto" => "delayed-auto", "demand" => "demand", "manual" => "demand", "disabled" => "disabled", _ => "auto" }; } private static ServiceConfigurationResult ApplyServiceConfiguration( string serviceName, string normalizedStartMode, string? description) { string startModeCommandArguments = BuildConfigStartModeCommandArguments(serviceName, normalizedStartMode); var startModeResult = RunScCommand(startModeCommandArguments); if (startModeResult.ExitCode != 0) { string errorMessage = BuildCommandFailureMessage( $"Service start mode update failed for '{serviceName}'.", startModeResult); WriteServiceConsoleLine("To update start mode manually, run this command in an Administrator console:"); WriteServiceConsoleLine($"sc.exe {startModeCommandArguments}"); return ServiceConfigurationResult.Failed(errorMessage); } if (!string.IsNullOrWhiteSpace(description)) { string descriptionCommandArguments = BuildDescriptionCommandArguments(serviceName, description); var descriptionResult = RunScCommand(descriptionCommandArguments); if (descriptionResult.ExitCode != 0) { string errorMessage = BuildCommandFailureMessage( $"Service description update failed for '{serviceName}'.", descriptionResult); WriteServiceConsoleLine("To update description manually, run this command in an Administrator console:"); WriteServiceConsoleLine($"sc.exe {descriptionCommandArguments}"); return ServiceConfigurationResult.Failed(errorMessage); } } return ServiceConfigurationResult.Completed(); } private static string BuildCreateServiceCommandArguments( string serviceName, string quotedBinPath, string normalizedStartMode, string displayName) { return $"create \"{serviceName}\" " + $"binPath= \"{quotedBinPath}\" " + $"start= {normalizedStartMode} " + $"DisplayName= \"{displayName}\""; } private static string BuildConfigStartModeCommandArguments(string serviceName, string normalizedStartMode) { return $"config \"{serviceName}\" start= {normalizedStartMode}"; } private static string BuildDescriptionCommandArguments(string serviceName, string description) { return $"description \"{serviceName}\" \"{description.Trim()}\""; } private static string? ResolveServiceDescription(string? explicitDescription) { if (!string.IsNullOrWhiteSpace(explicitDescription)) return explicitDescription.Trim(); if (!string.IsNullOrWhiteSpace(Description)) return Description.Trim(); return null; } private static ServiceCommandExecutionResult RunScCommand(string arguments) { var startInfo = new ProcessStartInfo { FileName = "sc.exe", Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var process = Process.Start(startInfo); if (process is null) { return new ServiceCommandExecutionResult { ExitCode = -1, ErrorOutput = "Unable to start sc.exe process." }; } string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); return new ServiceCommandExecutionResult { ExitCode = process.ExitCode, Output = output, ErrorOutput = error }; } private static string BuildCommandFailureMessage( string context, ServiceCommandExecutionResult commandResult) { string details = BuildCommandDetails(commandResult); return $"{context} {details}"; } private static string BuildCommandDetails(ServiceCommandExecutionResult commandResult) { string output = NormalizeCommandOutput(commandResult.Output); string error = NormalizeCommandOutput(commandResult.ErrorOutput); return $"ExitCode={commandResult.ExitCode}. " + $"StdOut={output}. " + $"StdErr={error}."; } private static string NormalizeCommandOutput(string value) { if (string.IsNullOrWhiteSpace(value)) return "(empty)"; return value .Replace("\r", " ") .Replace("\n", " ") .Trim(); } private static void WriteServiceConsoleLine(string message) { Console.Error.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [WorkerCore] {message}"); } private sealed class ServiceCommandExecutionResult { public int ExitCode { get; init; } public string Output { get; init; } = string.Empty; public string ErrorOutput { get; init; } = string.Empty; } private sealed class ServiceConfigurationResult { public bool Success { get; init; } public string ErrorMessage { get; init; } = string.Empty; public static ServiceConfigurationResult Completed() { return new ServiceConfigurationResult { Success = true }; } public static ServiceConfigurationResult Failed(string errorMessage) { return new ServiceConfigurationResult { Success = false, ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? "Unknown service configuration error." : errorMessage.Trim() }; } } public sealed class ServiceRegistrationResult { public bool Success { get; init; } public bool IsWarning { get; init; } public string Message { get; init; } = string.Empty; public static ServiceRegistrationResult Completed(string message) { return new ServiceRegistrationResult { Success = true, IsWarning = false, Message = message }; } public static ServiceRegistrationResult Warning(string message) { return new ServiceRegistrationResult { Success = true, IsWarning = true, Message = message }; } public static ServiceRegistrationResult Failed(string message) { return new ServiceRegistrationResult { Success = false, IsWarning = false, Message = message }; } } }