using System.Diagnostics; using System.Text; using Microsoft.Extensions.Options; using UnrealDemoScanner.Web.Infrastructure; namespace UnrealDemoScanner.Web.Services; public sealed class ScannerRunner : IScannerRunner { private readonly ScannerOptions _options; private readonly IWebHostEnvironment _env; private readonly ILogger _logger; public ScannerRunner( IOptions options, IWebHostEnvironment env, ILogger logger) { _options = options.Value; _env = env; _logger = logger; } public async Task RunAsync(string demoPath, CancellationToken cancellationToken) { var scannerPath = ResolveScannerPath(); var timeout = TimeSpan.FromMinutes(Math.Max(1, _options.TimeoutMinutes)); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(timeout); var outputBuilder = new StringBuilder(capacity: 128 * 1024); var stopwatch = Stopwatch.StartNew(); using var process = new Process { StartInfo = new ProcessStartInfo { FileName = scannerPath, Arguments = $"\"{demoPath}\" -skip", WorkingDirectory = Path.GetDirectoryName(scannerPath)!, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }, EnableRaisingEvents = true }; // Allow running older-target scanner binaries (net6) on newer installed runtimes (net8+). process.StartInfo.Environment["DOTNET_ROLL_FORWARD"] = "LatestMajor"; process.OutputDataReceived += (_, args) => { if (args.Data is null) { return; } outputBuilder.AppendLine(args.Data); }; process.ErrorDataReceived += (_, args) => { if (args.Data is null) { return; } outputBuilder.AppendLine(args.Data); }; if (!process.Start()) { throw new InvalidOperationException("Unable to start scanner process."); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); var timedOut = false; try { await process.WaitForExitAsync(timeoutCts.Token); } catch (OperationCanceledException) { timedOut = true; TryKill(process); } stopwatch.Stop(); var exitCode = timedOut ? -1 : process.ExitCode; _logger.LogInformation("Scanner run completed. ExitCode={ExitCode} TimedOut={TimedOut} DurationMs={DurationMs}", exitCode, timedOut, stopwatch.ElapsedMilliseconds); return new ScannerRunResult { ExitCode = exitCode, CombinedOutput = outputBuilder.ToString(), TimedOut = timedOut, DurationMs = stopwatch.ElapsedMilliseconds }; } private string ResolveScannerPath() { var candidates = new List(); if (Path.IsPathRooted(_options.ExecutablePath)) { candidates.Add(_options.ExecutablePath); } else { candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, _options.ExecutablePath))); } // Common local dev output folders. candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\UnrealDemoScanner\bin\Release\net6.0-windows7.0\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\UnrealDemoScanner\bin\Debug\net6.0-windows7.0\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\Release\net6.0-windows7.0\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\Release\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\UnrealDemoScanner\bin\Release\net6.0-windows7.0\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, @"..\UnrealDemoScanner\bin\Debug\net6.0-windows7.0\UnrealDemoScanner.exe"))); candidates.Add(Path.GetFullPath(Path.Combine(_env.ContentRootPath, "UnrealDemoScanner.exe"))); foreach (var candidate in candidates.Distinct(StringComparer.OrdinalIgnoreCase)) { if (File.Exists(candidate)) { return candidate; } } var msg = "Scanner executable not found.\n" + "Build scanner first, for example:\n" + "dotnet build ..\\UnrealDemoScanner\\UnrealDemoScanner.csproj -c Release -p:Platform=x86\n" + "or copy UnrealDemoScanner.exe into UnrealDemoScanner.Web folder.\n" + "Checked paths:\n - " + string.Join("\n - ", candidates.Distinct(StringComparer.OrdinalIgnoreCase)); throw new FileNotFoundException(msg, candidates[0]); } private static void TryKill(Process process) { try { if (!process.HasExited) { process.Kill(entireProcessTree: true); } } catch { // Best effort kill. } } }