using System.Diagnostics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace ScreenJob.WindowsServiceHost; internal sealed class BackendProcessService : BackgroundService { private readonly ILogger _logger; private readonly ServiceOptions _options; private readonly object _logLock = new(); private Process? _backendProcess; private string _stdoutLogPath = string.Empty; private string _stderrLogPath = string.Empty; public BackendProcessService(ILogger logger, ServiceOptions options) { _logger = logger; _options = options; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Directory.CreateDirectory(_options.LogDirectory); _stdoutLogPath = Path.Combine(_options.LogDirectory, "backend-service.stdout.log"); _stderrLogPath = Path.Combine(_options.LogDirectory, "backend-service.stderr.log"); LogStdOut("Service host starting backend process."); LogStdOut($"Script: {_options.BackendScriptPath}"); LogStdOut($"Working directory: {_options.WorkingDirectory}"); var powershellPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); var startInfo = new ProcessStartInfo { FileName = powershellPath, Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{_options.BackendScriptPath}\"", WorkingDirectory = _options.WorkingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; _backendProcess = new Process { StartInfo = startInfo }; if (!_backendProcess.Start()) { throw new InvalidOperationException("Failed to start backend process."); } LogStdOut($"Backend process started with PID {_backendProcess.Id}."); _logger.LogInformation("Backend process started with PID {Pid}.", _backendProcess.Id); var stdoutPump = PumpStreamAsync(_backendProcess.StandardOutput, LogStdOut, stoppingToken); var stderrPump = PumpStreamAsync(_backendProcess.StandardError, LogStdErr, stoppingToken); try { await _backendProcess.WaitForExitAsync(stoppingToken); var exitCode = _backendProcess.ExitCode; LogStdErr($"Backend process exited unexpectedly with code {exitCode}."); _logger.LogError("Backend process exited unexpectedly with code {ExitCode}.", exitCode); Environment.ExitCode = exitCode == 0 ? 1 : exitCode; throw new InvalidOperationException( $"Backend process ended unexpectedly. Service host exit code: {Environment.ExitCode}."); } catch (OperationCanceledException) { LogStdOut("Service stop requested."); } finally { await Task.WhenAll(stdoutPump, stderrPump); } } public override async Task StopAsync(CancellationToken cancellationToken) { if (_backendProcess is { HasExited: false }) { try { LogStdOut("Stopping backend process."); _backendProcess.Kill(entireProcessTree: true); } catch (Exception ex) { LogStdErr($"Failed to stop backend process cleanly: {ex.Message}"); _logger.LogError(ex, "Failed to stop backend process cleanly."); } } await base.StopAsync(cancellationToken); } private async Task PumpStreamAsync( StreamReader reader, Action sink, CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var line = await reader.ReadLineAsync(); if (line is null) { break; } sink(line); } } private void LogStdOut(string message) { WriteLog(_stdoutLogPath, message); } private void LogStdErr(string message) { WriteLog(_stderrLogPath, message); } private void WriteLog(string path, string message) { var stamp = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); var line = $"[{stamp}] {message}{Environment.NewLine}"; lock (_logLock) { File.AppendAllText(path, line); } } }