using System.Diagnostics; using System.IO.Pipes; using System.Text; using YMhut.Box.Core.Api; using YMhut.Box.Core.Data; using YMhut.Box.Core.Logging; using YMhut.Box.Core.Tools; namespace YMhut.Box.WinUI.Services; public sealed class ProcessToolWorkerService : IToolWorkerService, IDisposable { private readonly IApiManager? _apiManager; private readonly IReferenceDataService? _referenceDataService; private readonly ILogService? _logService; private readonly SemaphoreSlim _requestGate = new(1, 1); private readonly SemaphoreSlim _writeGate = new(1, 1); private Process? _process; private NamedPipeServerStream? _pipe; private StreamReader? _reader; private StreamWriter? _writer; private bool _disposed; public ProcessToolWorkerService( IApiManager? apiManager = null, IReferenceDataService? referenceDataService = null, ILogService? logService = null) { _apiManager = apiManager; _referenceDataService = referenceDataService; _logService = logService; AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose(); } public Task RunAsync(Func> work, CancellationToken cancellationToken = default) { return Task.Run(() => work(cancellationToken), cancellationToken); } public async Task ExecuteToolAsync( IToolModule module, string input, IApiManager? apiManager = null, IReferenceDataService? referenceDataService = null, CancellationToken cancellationToken = default, string language = "zh-CN") { await _requestGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { return await ExecuteThroughWorkerAsync(module, input, language, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception exception) { await WriteWorkerLogAsync("Warning", "worker", "Tool worker unavailable; using in-process fallback", exception.Message, CancellationToken.None) .ConfigureAwait(false); return await ToolExecutor.ExecuteAsync( module, input, cancellationToken, apiManager ?? _apiManager, referenceDataService ?? _referenceDataService, language).ConfigureAwait(false); } finally { _requestGate.Release(); } } public void Dispose() { if (_disposed) { return; } _disposed = true; try { if (_writer is not null) { _writer.WriteLine(ToolWorkerProtocol.Serialize(new ToolWorkerMessage(ToolWorkerProtocol.Shutdown))); _writer.Flush(); } } catch { } DisposeWorker(killProcess: false); _requestGate.Dispose(); _writeGate.Dispose(); } private async Task ExecuteThroughWorkerAsync( IToolModule module, string input, string language, CancellationToken cancellationToken) { await EnsureWorkerAsync(cancellationToken).ConfigureAwait(false); var requestId = Guid.NewGuid().ToString("N"); var request = new ToolWorkerMessage( ToolWorkerProtocol.ExecuteTool, requestId, ToolId: module.Id, Input: input, TimeoutMs: 120_000, Language: language); await WriteMessageAsync(request, cancellationToken).ConfigureAwait(false); await WriteWorkerLogAsync("Information", "worker", $"Queued tool in worker: {module.Id}", null, cancellationToken) .ConfigureAwait(false); using var cancellation = cancellationToken.Register(() => { _ = Task.Run(async () => { try { await WriteMessageAsync( new ToolWorkerMessage(ToolWorkerProtocol.Cancel, requestId), CancellationToken.None).ConfigureAwait(false); } catch { } }); }); while (true) { var message = await ReadMessageAsync(cancellationToken).ConfigureAwait(false); if (!string.Equals(message.RequestId, requestId, StringComparison.Ordinal)) { continue; } if (string.Equals(message.Type, ToolWorkerProtocol.Result, StringComparison.Ordinal)) { return new ToolExecutionResult(message.Ok, message.Output ?? string.Empty, message.Error, message.Document); } if (string.Equals(message.Type, ToolWorkerProtocol.Error, StringComparison.Ordinal)) { return ToolExecutionResult.Fail(message.Error ?? "Worker failed to execute the tool."); } } } private async Task EnsureWorkerAsync(CancellationToken cancellationToken) { if (_process is { HasExited: false } && _pipe?.IsConnected == true && _reader is not null && _writer is not null) { return; } DisposeWorker(killProcess: true); var executable = ResolveWorkerExecutable() ?? throw new FileNotFoundException("YMhut.Box.Worker.exe was not found in the application output."); var pipeName = $"YMhutBoxWorker-{Environment.ProcessId}-{Guid.NewGuid():N}"; _pipe = new NamedPipeServerStream( pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); var startInfo = new ProcessStartInfo { FileName = executable, Arguments = $"--pipe {pipeName}", WorkingDirectory = Path.GetDirectoryName(executable) ?? AppContext.BaseDirectory, UseShellExecute = false, CreateNoWindow = true }; _process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start the YMhut tool worker process."); await _pipe.WaitForConnectionAsync(cancellationToken).WaitAsync(TimeSpan.FromSeconds(8), cancellationToken) .ConfigureAwait(false); _reader = new StreamReader(_pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); _writer = new StreamWriter(_pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 4096, leaveOpen: true) { AutoFlush = true }; var ready = await ReadMessageAsync(cancellationToken).ConfigureAwait(false); if (!string.Equals(ready.Type, ToolWorkerProtocol.Ready, StringComparison.Ordinal) || !string.Equals(ready.Version, ToolWorkerProtocol.Version, StringComparison.Ordinal)) { throw new InvalidOperationException("Tool worker did not complete the protocol handshake."); } await WriteWorkerLogAsync("Information", "worker", "Tool worker process started", Path.GetFileName(executable), cancellationToken) .ConfigureAwait(false); } private async Task WriteMessageAsync(ToolWorkerMessage message, CancellationToken cancellationToken) { var writer = _writer ?? throw new InvalidOperationException("Tool worker pipe is not connected."); await _writeGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { await writer.WriteLineAsync(ToolWorkerProtocol.Serialize(message).AsMemory(), cancellationToken).ConfigureAwait(false); await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } finally { _writeGate.Release(); } } private async Task ReadMessageAsync(CancellationToken cancellationToken) { var reader = _reader ?? throw new InvalidOperationException("Tool worker pipe is not connected."); var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line is null) { DisposeWorker(killProcess: true); throw new IOException("Tool worker pipe closed unexpectedly."); } return ToolWorkerProtocol.Deserialize(line) ?? throw new InvalidDataException("Tool worker returned an invalid message."); } private void DisposeWorker(bool killProcess) { try { _reader?.Dispose(); _writer?.Dispose(); _pipe?.Dispose(); } catch { } finally { _reader = null; _writer = null; _pipe = null; } try { if (_process is not null && !_process.HasExited && killProcess) { _process.Kill(entireProcessTree: true); } } catch { } finally { _process?.Dispose(); _process = null; } } private async Task WriteWorkerLogAsync( string level, string category, string message, string? detail, CancellationToken cancellationToken) { if (_logService is null) { return; } await _logService.WriteAsync(level, category, message, detail, cancellationToken).ConfigureAwait(false); } private static string? ResolveWorkerExecutable() { var baseDirectory = AppContext.BaseDirectory; var candidates = new List { Path.Combine(baseDirectory, "worker", "win-x64", "YMhut.Box.Worker.exe"), Path.Combine(baseDirectory, "worker", "YMhut.Box.Worker.exe"), Path.Combine(baseDirectory, "YMhut.Box.Worker.exe") }; var directory = new DirectoryInfo(baseDirectory); while (directory is not null) { candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.Worker", "bin", "Debug", "net10.0", "YMhut.Box.Worker.exe")); candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.Worker", "bin", "Release", "net10.0", "YMhut.Box.Worker.exe")); directory = directory.Parent; } return candidates.FirstOrDefault(File.Exists); } }