using System.Diagnostics; using System.Collections.Concurrent; using MaksIT.UScheduler.Shared.Helpers; using MaksIT.UScheduler.Shared.Extensions; namespace MaksIT.UScheduler.Services; /// /// Service responsible for managing and executing external processes. /// Tracks running processes and provides methods for starting, monitoring, and terminating them. /// public sealed class ProcessService : IProcessService { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly ConcurrentDictionary _runningProcesses = new(); /// /// Initializes a new instance of the class. /// /// The logger instance for this service. /// The logger factory for creating process-specific loggers. public ProcessService( ILogger logger, ILoggerFactory loggerFactory ) { _logger = logger; _loggerFactory = loggerFactory; } /// /// Starts and monitors an external process asynchronously. /// If the process exits with a non-zero code, it will be automatically restarted. /// /// The path to the executable to run. /// Optional command-line arguments to pass to the process. /// Cancellation token to signal when the process should stop. /// A task representing the asynchronous operation. public async Task RunProcessAsync(string processPath, string[]? args, CancellationToken stoppingToken) { // Resolve relative paths against application base directory var resolvedPath = PathHelper.ResolvePath(processPath); var processLogger = _loggerFactory.CreateFolderLogger(resolvedPath); var argsString = args != null ? string.Join(", ", args) : ""; processLogger.LogInformation($"Starting process {resolvedPath} with arguments {argsString}"); Process? process = null; try { if (GetRunningProcesses().Any(x => x.Value.StartInfo.FileName == resolvedPath)) { processLogger.LogInformation($"Process {resolvedPath} is already running"); return; } process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = resolvedPath, WorkingDirectory = Path.GetDirectoryName(resolvedPath), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; if (args != null) { foreach (var arg in args) process.StartInfo.ArgumentList.Add(arg); } process.Start(); _runningProcesses.TryAdd(process.Id, process); processLogger.LogInformation($"Process {resolvedPath} started with ID {process.Id}"); await process.WaitForExitAsync(); if (process.ExitCode != 0 && !stoppingToken.IsCancellationRequested) { processLogger.LogWarning($"Process {resolvedPath} exited with code {process.ExitCode}, restarting..."); await RunProcessAsync(resolvedPath, args, stoppingToken); } else { processLogger.LogInformation($"Process {resolvedPath} completed successfully with exit code {process.ExitCode}"); } } catch (OperationCanceledException) { // When the stopping token is canceled, for example, a call made from services.msc, // we shouldn't exit with a non-zero exit code. In other words, this is expected... processLogger.LogInformation($"Process {resolvedPath} was canceled"); } catch (Exception ex) { processLogger.LogError($"Error running process {resolvedPath}: {ex.Message}"); } finally { if (process != null && _runningProcesses.ContainsKey(process.Id)) { TerminateProcessById(process.Id); processLogger.LogInformation($"Process {resolvedPath} with ID {process.Id} removed from running processes"); } } } /// /// Gets the dictionary of currently running processes. /// /// A concurrent dictionary mapping process IDs to their Process objects. public ConcurrentDictionary GetRunningProcesses() { return _runningProcesses; } /// /// Terminates a running process by its ID. /// Recursively attempts to kill the process until it has exited. /// /// The ID of the process to terminate. public void TerminateProcessById(int processId) { // Check if the process is in the running processes list if (!_runningProcesses.TryGetValue(processId, out var processToTerminate)) { return; } // Kill the process try { processToTerminate.Kill(true); } catch (Exception ex) { _logger.LogError($"Error terminating process {processId}: {ex.Message}"); } // Check if the process has exited if (!processToTerminate.HasExited) { TerminateProcessById(processId); } } /// /// Terminates all currently running processes managed by this service. /// public void TerminateAllProcesses() { _logger.LogInformation("Terminating all running processes"); foreach (var processId in _runningProcesses.Keys.ToList()) { TerminateProcessById(processId); } } }