Files
YMhut-box-C-/src/box-winUI/Services/ProcessToolWorkerService.cs
T
QWQLwToo 7ecc6a8923
build-winui / winui (push) Has been cancelled
Add WinUI and core source
2026-06-26 13:27:13 +08:00

308 lines
10 KiB
C#

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<T> RunAsync<T>(Func<CancellationToken, Task<T>> work, CancellationToken cancellationToken = default)
{
return Task.Run(() => work(cancellationToken), cancellationToken);
}
public async Task<ToolExecutionResult> 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<ToolExecutionResult> 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<ToolWorkerMessage> 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<string>
{
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);
}
}