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);
}
}
}