using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Pipes; using System.Text; using System.Text.Json; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; using Windows.Storage.Pickers; using Windows.System; using WinRT.Interop; using YMhut.Box.Core.App; using YMhut.Box.Core.Logging; using YMhut.Box.Core.Platform; using YMhut.Box.Core.Plugins; using YMhut.Box.Core.Plugins.Runtime; using YMhut.Box.Core.Settings; using YMhut.Box.Core.Tools; namespace YMhut.Box.WinUI.Services; public sealed class TauriPluginProcessService( AppPaths paths, IPluginHostProcessService pluginHost, IToolLinkNavigationService linkNavigationService, ISettingsService settingsService, ILogService? logService = null) : IPluginRuntimeLauncher, IDisposable { private static readonly TimeSpan BrokerConnectTimeout = TimeSpan.FromSeconds(12); private readonly ConcurrentDictionary _sessions = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _processes = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary> _output = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _brokers = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _externalOutputs = new(StringComparer.OrdinalIgnoreCase); private long _sequence; private int _snapshotSubscribed; private bool _disposed; public async Task LaunchAsync(PluginRuntimeLaunchRequest request, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var sessionId = Guid.NewGuid().ToString("N"); var startedAt = DateTimeOffset.Now; var host = ResolveHostExecutable(); var session = new PluginRuntimeSession( sessionId, request.PluginId, request.SurfaceId, request.RuntimeKind, null, PluginRuntimeSessionStatus.Starting, startedAt); _sessions[sessionId] = session; _output[sessionId] = []; if (host is null) { session = session with { Status = PluginRuntimeSessionStatus.Failed, EndedAt = DateTimeOffset.Now, LastError = "Tauri plugin host executable was not found." }; _sessions[sessionId] = session; await WriteLogAsync("Error", request.PluginId, "Tauri plugin host missing", session.LastError, cancellationToken).ConfigureAwait(false); return session; } ExternalRuntimeBroker? broker = null; Process? process = null; try { EnsureSnapshotSubscription(); if (!settingsService.Current.PluginDeveloperMode) { throw new UnauthorizedAccessException("The controlled external runtime requires plugin developer mode."); } var snapshot = await pluginHost.GetSnapshotAsync(cancellationToken).ConfigureAwait(false); var pluginDto = snapshot.Plugins.FirstOrDefault(candidate => string.Equals(candidate.Manifest.Id, request.PluginId, StringComparison.OrdinalIgnoreCase)); if (pluginDto is null || !pluginDto.IsValid || !pluginDto.State.Enabled || pluginDto.Manifest.Runtime != PluginRuntimeKind.Tauri) { throw new UnauthorizedAccessException("The Tauri plugin is unavailable, disabled, or invalid."); } var plugin = pluginDto.ToLoadedPlugin(); if (!PluginPermissionPolicy.IsGrantCurrent(plugin.Manifest, plugin.State, PluginPermission.ExternalRuntime) || !string.Equals(plugin.State.ExternalRuntimeConfirmation, plugin.Manifest.Version, StringComparison.Ordinal)) { throw new UnauthorizedAccessException("The external runtime permission or version confirmation is not current."); } var registeredRoot = Path.GetFullPath(plugin.RootPath); var requestedRoot = Path.GetFullPath(request.PluginRoot); var requestedEntry = Path.GetFullPath(request.Entry); if (!string.Equals(registeredRoot.TrimEnd(Path.DirectorySeparatorChar), requestedRoot.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase) || !PluginRegistryService.IsInside(registeredRoot, requestedEntry) || !File.Exists(requestedEntry)) { throw new InvalidDataException("The external runtime launch paths do not match the registered plugin package."); } var allowedOrigins = await ResolveAllowedOriginsAsync(plugin, cancellationToken).ConfigureAwait(false); var origin = PluginExternalWebOrigin.Create(request.PluginId, request.SurfaceId); var protocolName = PluginExternalWebOrigin.ProtocolName(request.PluginId, request.SurfaceId); var bridgeSessionToken = await pluginHost.OpenBridgeSessionAsync( request.PluginId, request.SurfaceId, origin, cancellationToken).ConfigureAwait(false); var pipeName = $"YMhutBoxExternalPlugin-{Environment.ProcessId}-{Guid.NewGuid():N}"; var pipe = new NamedPipeServerStream( pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); broker = new ExternalRuntimeBroker( sessionId, request.PluginId, request.SurfaceId, origin, bridgeSessionToken, RuntimePolicyKey(plugin), pipe); _brokers[sessionId] = broker; var profileRoot = Path.Combine(paths.Cache, "WebView2", "Plugins", request.PluginId, "Tauri", request.SurfaceId); Directory.CreateDirectory(profileRoot); var startInfo = new ProcessStartInfo { FileName = host, WorkingDirectory = Path.GetDirectoryName(host) ?? AppContext.BaseDirectory, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = false }; AddArgument(startInfo, "--session", sessionId); AddArgument(startInfo, "--plugin-id", request.PluginId); AddArgument(startInfo, "--surface-id", request.SurfaceId); AddArgument(startInfo, "--runtime-kind", request.RuntimeKind.ToString()); AddArgument(startInfo, "--plugin-root", registeredRoot); AddArgument(startInfo, "--entry", requestedEntry); AddArgument(startInfo, "--protocol-name", protocolName); AddArgument(startInfo, "--plugin-origin", origin); AddArgument(startInfo, "--profile-root", profileRoot); AddArgument(startInfo, "--broker-pipe", pipeName); AddArgument(startInfo, "--developer-mode", settingsService.Current.PluginDeveloperMode ? "true" : "false"); foreach (var allowedOrigin in allowedOrigins) { AddArgument(startInfo, "--allowed-origin", allowedOrigin); } process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start Tauri plugin host."); process.EnableRaisingEvents = true; process.OutputDataReceived += (_, args) => CaptureOutput(sessionId, ShellOutputStream.Stdout, args.Data); process.ErrorDataReceived += (_, args) => CaptureOutput(sessionId, ShellOutputStream.Stderr, args.Data); process.Exited += (_, _) => CompleteSession(sessionId, process); process.BeginOutputReadLine(); process.BeginErrorReadLine(); _processes[sessionId] = process; await pipe.WaitForConnectionAsync(cancellationToken) .WaitAsync(BrokerConnectTimeout, cancellationToken) .ConfigureAwait(false); broker.Reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); broker.Writer = new StreamWriter(pipe, new UTF8Encoding(false), bufferSize: 4096, leaveOpen: true) { AutoFlush = true }; await broker.Writer.WriteLineAsync(PluginRuntimeProtocol.Serialize(new PluginRuntimeMessage( PluginRuntimeProtocol.Ready, Version: PluginRuntimeProtocol.Version, SessionToken: bridgeSessionToken, Origin: origin))).ConfigureAwait(false); broker.LoopTask = Task.Run(() => RunBrokerAsync(broker), CancellationToken.None); session = session with { ProcessId = process.Id, Status = PluginRuntimeSessionStatus.Running }; _sessions[sessionId] = session; await WriteLogAsync("Information", request.PluginId, "Tauri plugin host launched", $"session={sessionId}; pid={process.Id}", cancellationToken).ConfigureAwait(false); return session; } catch (Exception exception) { if (process is { HasExited: false }) { try { process.Kill(entireProcessTree: true); } catch { } } if (broker is not null) { await CloseBrokerAsync(sessionId, broker).ConfigureAwait(false); } session = session with { Status = PluginRuntimeSessionStatus.Failed, EndedAt = DateTimeOffset.Now, LastError = exception.Message }; _sessions[sessionId] = session; await WriteLogAsync("Error", request.PluginId, "Tauri plugin host launch failed", exception.Message, cancellationToken).ConfigureAwait(false); return session; } } public async Task StopAsync(string sessionId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (_brokers.TryGetValue(sessionId, out var broker)) { await CloseBrokerAsync(sessionId, broker).ConfigureAwait(false); } if (_processes.TryRemove(sessionId, out var process)) { try { if (!process.HasExited) { process.Kill(entireProcessTree: true); } } catch (Exception exception) { CaptureOutput(sessionId, ShellOutputStream.System, $"Stop failed: {exception.Message}"); } finally { process.Dispose(); } } if (_sessions.TryGetValue(sessionId, out var session)) { _sessions[sessionId] = session with { Status = PluginRuntimeSessionStatus.Cancelled, EndedAt = DateTimeOffset.Now }; await WriteLogAsync("Information", session.PluginId, "Tauri plugin host stopped", $"session={sessionId}", cancellationToken).ConfigureAwait(false); } _externalOutputs.TryRemove(sessionId, out _); } public Task GetSessionAsync(string sessionId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); _sessions.TryGetValue(sessionId, out var session); return Task.FromResult(session); } public Task> ReadRecentOutputAsync(string sessionId, int take = 200, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (!_output.TryGetValue(sessionId, out var lines)) { return Task.FromResult>([]); } lock (lines) { return Task.FromResult>(lines.TakeLast(Math.Max(0, take)).ToArray()); } } public void Dispose() { if (_disposed) { return; } _disposed = true; if (Interlocked.Exchange(ref _snapshotSubscribed, 0) != 0) { pluginHost.SnapshotChanged -= PluginHost_SnapshotChanged; } foreach (var sessionId in _processes.Keys.ToArray()) { try { StopAsync(sessionId).GetAwaiter().GetResult(); } catch { } } } private void CompleteSession(string sessionId, Process process) { try { if (_brokers.TryGetValue(sessionId, out var broker)) { _ = CloseBrokerAsync(sessionId, broker); } _externalOutputs.TryRemove(sessionId, out _); int? exitCode = process.HasExited ? process.ExitCode : null; if (_sessions.TryGetValue(sessionId, out var session)) { _sessions[sessionId] = session with { Status = exitCode == 0 ? PluginRuntimeSessionStatus.Exited : PluginRuntimeSessionStatus.Failed, EndedAt = DateTimeOffset.Now, ExitCode = exitCode }; _ = WriteLogAsync( exitCode == 0 ? "Information" : "Error", session.PluginId, "Tauri plugin host exited", $"session={sessionId}; exitCode={exitCode}", CancellationToken.None); } } finally { _processes.TryRemove(sessionId, out _); process.Dispose(); } } private void CaptureOutput(string sessionId, ShellOutputStream stream, string? line) { if (line is null) { return; } var output = new ShellOutputEvent(sessionId, stream, line, DateTimeOffset.Now, Interlocked.Increment(ref _sequence)); var lines = _output.GetOrAdd(sessionId, _ => []); lock (lines) { lines.Add(output); if (lines.Count > 5000) { lines.RemoveRange(0, lines.Count - 5000); } } } private async Task WriteLogAsync(string level, string pluginId, string message, string? detail, CancellationToken cancellationToken) { await (logService?.WriteAsync(level, $"plugin:{pluginId}:runtime", message, detail, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false); } private async Task> ResolveAllowedOriginsAsync(LoadedPlugin plugin, CancellationToken cancellationToken) { if (!PluginPermissionPolicy.IsGrantCurrent(plugin.Manifest, plugin.State, PluginPermission.Http)) { return []; } var origins = new List(); foreach (var value in plugin.Manifest.Network?.AllowedOrigins ?? []) { if (!PluginNetworkPolicy.TryNormalizePublicOrigin(value, allowWebSocket: true, out var origin) || !Uri.TryCreate(origin, UriKind.Absolute, out var uri) || !await PluginNetworkPolicy.ResolvesToPublicAddressAsync(uri.Host, cancellationToken).ConfigureAwait(false)) { throw new UnauthorizedAccessException("A declared external runtime origin did not resolve exclusively to public addresses."); } origins.Add(origin); } return origins.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } private void EnsureSnapshotSubscription() { if (Interlocked.Exchange(ref _snapshotSubscribed, 1) == 0) { pluginHost.SnapshotChanged += PluginHost_SnapshotChanged; } } private void PluginHost_SnapshotChanged(object? sender, PluginSnapshot snapshot) { foreach (var broker in _brokers.Values) { var plugin = snapshot.Plugins.FirstOrDefault(candidate => string.Equals(candidate.Manifest.Id, broker.PluginId, StringComparison.OrdinalIgnoreCase)); if (plugin is null || !plugin.IsValid || RuntimePolicyKey(plugin.ToLoadedPlugin()) != broker.PolicyKey) { _ = StopAsync(broker.SessionId); } } } private static string RuntimePolicyKey(LoadedPlugin plugin) { var state = plugin.State; return JsonSerializer.Serialize(new { plugin.Manifest.Version, plugin.Manifest.Runtime, plugin.Manifest.Permissions, plugin.Manifest.Security, plugin.Manifest.PermissionReasons, plugin.Manifest.Network, plugin.Manifest.Requirements, state.Enabled, Granted = state.GrantedPermissions.OrderBy(value => value).ToArray(), Fingerprints = state.PermissionPolicyFingerprints?.OrderBy(value => value.Key).ToArray(), state.ExternalRuntimeConfirmation }); } private async Task RunBrokerAsync(ExternalRuntimeBroker broker) { try { while (!broker.Cancellation.IsCancellationRequested && broker.Reader is not null) { var line = await broker.Reader.ReadLineAsync(broker.Cancellation.Token).ConfigureAwait(false); if (line is null) { break; } if (Encoding.UTF8.GetByteCount(line) > 512 * 1024) { await WriteBrokerResponseAsync(broker, new PluginRuntimeMessage( PluginRuntimeProtocol.Error, Error: "External runtime message exceeded the size limit.")).ConfigureAwait(false); break; } PluginRuntimeMessage? message; try { message = PluginRuntimeProtocol.Deserialize(line); } catch (JsonException) { message = null; } if (message is null) { await WriteBrokerResponseAsync(broker, new PluginRuntimeMessage( PluginRuntimeProtocol.Error, Error: "External runtime sent an invalid message.")).ConfigureAwait(false); continue; } if (string.Equals(message.Type, PluginRuntimeProtocol.Ping, StringComparison.Ordinal)) { await WriteBrokerResponseAsync(broker, new PluginRuntimeMessage( PluginRuntimeProtocol.Pong, message.RequestId, Version: PluginRuntimeProtocol.Version)).ConfigureAwait(false); continue; } if (!string.Equals(message.Type, PluginRuntimeProtocol.BridgeCall, StringComparison.Ordinal) || message.BridgeRequest is null) { await WriteBrokerResponseAsync(broker, new PluginRuntimeMessage( PluginRuntimeProtocol.Error, message.RequestId, Error: "External runtime message type is not supported.")).ConfigureAwait(false); continue; } var response = await HandleBrokerBridgeCallAsync(broker, message.BridgeRequest).ConfigureAwait(false); await WriteBrokerResponseAsync(broker, new PluginRuntimeMessage( PluginRuntimeProtocol.BridgeCall, message.RequestId, BridgeResponse: response)).ConfigureAwait(false); } } catch (OperationCanceledException) { } catch (Exception exception) { CaptureOutput(broker.SessionId, ShellOutputStream.System, "External runtime broker stopped unexpectedly."); await WriteLogAsync( "Warning", broker.PluginId, "External runtime broker failed", AppLocalizer.SanitizeSensitiveText(exception.Message, 220), CancellationToken.None).ConfigureAwait(false); } finally { await CloseBrokerAsync(broker.SessionId, broker).ConfigureAwait(false); } } private async Task HandleBrokerBridgeCallAsync(ExternalRuntimeBroker broker, PluginBridgeRequest request) { if (!string.Equals(request.PluginId, broker.PluginId, StringComparison.OrdinalIgnoreCase) || !string.Equals(request.SurfaceId, broker.SurfaceId, StringComparison.OrdinalIgnoreCase) || !string.Equals(request.SessionToken, broker.BridgeSessionToken, StringComparison.Ordinal) || !string.Equals(request.Origin, broker.Origin, StringComparison.OrdinalIgnoreCase) || Encoding.UTF8.GetByteCount(request.PayloadJson) > 256 * 1024) { return new PluginBridgeResponse( false, Error: "The external runtime bridge session is invalid.", ErrorCode: PluginBridgeErrorCode.SessionInvalid); } var response = await pluginHost.BridgeCallAsync(request, broker.Cancellation.Token).ConfigureAwait(false); if (!response.Ok) { return response; } try { using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(request.PayloadJson) ? "null" : request.PayloadJson); var payload = document.RootElement.Clone(); var handled = false; object? value = null; if (response.UiAction is not null) { handled = true; value = await ExecuteUiActionAsync(broker, response.UiAction, payload).ConfigureAwait(false); } else if (request.Method is "output.set" or "output.append" or "output.clear") { handled = true; value = UpdateExternalOutput(broker.SessionId, request.Method, payload); } return handled ? response with { ValueJson = JsonSerializer.Serialize(value), UiAction = null } : response with { UiAction = null }; } catch (OperationCanceledException) when (broker.Cancellation.IsCancellationRequested) { return new PluginBridgeResponse(false, Error: "The external runtime session was closed.", ErrorCode: PluginBridgeErrorCode.SessionInvalid); } catch (Exception exception) { await WriteLogAsync( "Warning", broker.PluginId, "External runtime UI action failed", AppLocalizer.SanitizeSensitiveText(exception.Message, 220), CancellationToken.None).ConfigureAwait(false); return new PluginBridgeResponse(false, Error: "The external runtime UI action failed.", ErrorCode: PluginBridgeErrorCode.HostFailure); } } private Task ExecuteUiActionAsync(ExternalRuntimeBroker broker, string uiAction, JsonElement payload) { return RunOnUiThreadAsync(async () => uiAction switch { "clipboard.readText" => await ReadClipboardAsync().ConfigureAwait(true), "clipboard.writeText" => WriteClipboard(payload), "file.openPicker" => await OpenFilePickerAsync().ConfigureAwait(true), "file.savePicker" => await SaveFilePickerAsync(payload).ConfigureAwait(true), "openExternal" => await OpenExternalAsync(broker.PluginId, payload).ConfigureAwait(true), _ => throw new NotSupportedException("The approved UI action is not supported by the external runtime broker.") }); } private object UpdateExternalOutput(string sessionId, string method, JsonElement payload) { if (method == "output.clear") { _externalOutputs.TryRemove(sessionId, out _); CaptureOutput(sessionId, ShellOutputStream.System, "Plugin output cleared."); return true; } var value = JsonValue(payload); var next = method == "output.append" && _externalOutputs.TryGetValue(sessionId, out var current) ? current + value : value; if (next.Length > 256 * 1024) { next = next[(next.Length - (256 * 1024))..]; } _externalOutputs[sessionId] = next; CaptureOutput(sessionId, ShellOutputStream.System, $"Plugin output updated ({next.Length} chars)."); return true; } private static async Task ReadClipboardAsync() { var content = Clipboard.GetContent(); return content.Contains(StandardDataFormats.Text) ? await content.GetTextAsync().AsTask().ConfigureAwait(true) : string.Empty; } private static object WriteClipboard(JsonElement payload) { var package = new DataPackage(); package.SetText(JsonValue(payload)); Clipboard.SetContent(package); return true; } private static async Task OpenFilePickerAsync() { if (App.CurrentWindow is null) { throw new InvalidOperationException("No active window is available for the file picker."); } var picker = new FileOpenPicker(); InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.CurrentWindow)); picker.FileTypeFilter.Add("*"); var file = await picker.PickSingleFileAsync(); if (file is null) { return null; } var properties = await file.GetBasicPropertiesAsync(); if (properties.Size > 2 * 1024 * 1024) { throw new InvalidDataException("Selected plugin input files cannot exceed 2 MiB."); } return new { name = file.Name, content = await FileIO.ReadTextAsync(file) }; } private static async Task SaveFilePickerAsync(JsonElement payload) { if (App.CurrentWindow is null) { throw new InvalidOperationException("No active window is available for the file picker."); } var suggestedName = ReadString(payload, "name") ?? "plugin-output.txt"; var value = ReadString(payload, "value") ?? ReadString(payload, "bytesOrText") ?? ReadString(payload, "text") ?? string.Empty; var extension = Path.GetExtension(suggestedName); if (string.IsNullOrWhiteSpace(extension)) { extension = ".txt"; suggestedName += extension; } var picker = new FileSavePicker { SuggestedFileName = Path.GetFileNameWithoutExtension(suggestedName) }; InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.CurrentWindow)); picker.FileTypeChoices.Add("Plugin output", [extension]); var file = await picker.PickSaveFileAsync(); if (file is null) { return null; } await FileIO.WriteTextAsync(file, value); return new { name = file.Name }; } private async Task OpenExternalAsync(string pluginId, JsonElement payload) { var value = ReadString(payload, "url") ?? JsonValue(payload); var target = payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty("options", out var options) ? ReadString(options, "target") : null; var linkTarget = string.Equals(target, "system", StringComparison.OrdinalIgnoreCase) ? ToolLinkTarget.SystemBrowser : ToolLinkTarget.SafeBrowser; if (linkTarget == ToolLinkTarget.SystemBrowser) { var root = App.CurrentWindow?.Content as FrameworkElement; if (root?.XamlRoot is null) { throw new InvalidOperationException("No active window is available for confirmation."); } var dialog = new ContentDialog { Title = "允许插件打开系统浏览器?", Content = $"插件 {pluginId} 请求打开:\n{value}", PrimaryButtonText = "允许本次", CloseButtonText = "取消", DefaultButton = ContentDialogButton.Close, XamlRoot = root.XamlRoot }; if (await dialog.ShowAsync() != ContentDialogResult.Primary) { throw new UnauthorizedAccessException("The system browser request was cancelled."); } } if (!await linkNavigationService.OpenAsync(value, linkTarget).ConfigureAwait(true)) { throw new InvalidOperationException("The approved external link could not be opened."); } return true; } private static Task RunOnUiThreadAsync(Func> action) { var dispatcher = App.CurrentWindow?.DispatcherQueue ?? throw new InvalidOperationException("No active UI dispatcher is available."); if (dispatcher.HasThreadAccess) { return action(); } var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); if (!dispatcher.TryEnqueue(async () => { try { completion.TrySetResult(await action().ConfigureAwait(true)); } catch (Exception exception) { completion.TrySetException(exception); } })) { completion.TrySetException(new InvalidOperationException("The UI dispatcher rejected the external runtime action.")); } return completion.Task; } private static async Task WriteBrokerResponseAsync(ExternalRuntimeBroker broker, PluginRuntimeMessage message) { if (broker.Writer is null || broker.Cancellation.IsCancellationRequested) { return; } await broker.WriteGate.WaitAsync(broker.Cancellation.Token).ConfigureAwait(false); try { await broker.Writer.WriteLineAsync(PluginRuntimeProtocol.Serialize(message)).ConfigureAwait(false); } finally { broker.WriteGate.Release(); } } private async Task CloseBrokerAsync(string sessionId, ExternalRuntimeBroker broker) { if (Interlocked.Exchange(ref broker.Closed, 1) != 0) { return; } _brokers.TryRemove(sessionId, out _); broker.Cancellation.Cancel(); try { broker.Reader?.Dispose(); broker.Writer?.Dispose(); broker.Pipe.Dispose(); } catch { } try { await pluginHost.CloseBridgeSessionAsync(broker.BridgeSessionToken).ConfigureAwait(false); } catch { } } private static void AddArgument(ProcessStartInfo startInfo, string name, string value) { startInfo.ArgumentList.Add(name); startInfo.ArgumentList.Add(value); } private static string JsonValue(JsonElement element) { return element.ValueKind == JsonValueKind.String ? element.GetString() ?? string.Empty : element.GetRawText(); } private static string? ReadString(JsonElement element, string property) { return element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out var value) ? JsonValue(value) : null; } private string? ResolveHostExecutable() { var candidates = new List { Path.Combine(AppContext.BaseDirectory, "tauri-host", "ymhut-box-plugin-tauri-host.exe"), Path.Combine(AppContext.BaseDirectory, "tauri-host", "YMhut.Box.PluginTauriHost.exe"), Path.Combine(paths.Root, "Runtime", "tauri-host", "ymhut-box-plugin-tauri-host.exe") }; var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null) { candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.PluginTauriHost", "src-tauri", "target", "release", "ymhut-box-plugin-tauri-host.exe")); candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.PluginTauriHost", "src-tauri", "target", "debug", "ymhut-box-plugin-tauri-host.exe")); directory = directory.Parent; } return candidates.FirstOrDefault(File.Exists); } private sealed class ExternalRuntimeBroker( string sessionId, string pluginId, string surfaceId, string origin, string bridgeSessionToken, string policyKey, NamedPipeServerStream pipe) { public string SessionId { get; } = sessionId; public string PluginId { get; } = pluginId; public string SurfaceId { get; } = surfaceId; public string Origin { get; } = origin; public string BridgeSessionToken { get; } = bridgeSessionToken; public string PolicyKey { get; } = policyKey; public NamedPipeServerStream Pipe { get; } = pipe; public CancellationTokenSource Cancellation { get; } = new(); public SemaphoreSlim WriteGate { get; } = new(1, 1); public StreamReader? Reader { get; set; } public StreamWriter? Writer { get; set; } public Task? LoopTask { get; set; } public int Closed; } }