更新客户端渲染,更新了壳
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
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;
|
||||
|
||||
namespace YMhut.Box.WinUI.Services;
|
||||
|
||||
public sealed class TauriPluginProcessService(AppPaths paths, ILogService? logService = null) : IPluginRuntimeLauncher, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PluginRuntimeSession> _sessions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, Process> _processes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, List<ShellOutputEvent>> _output = new(StringComparer.OrdinalIgnoreCase);
|
||||
private long _sequence;
|
||||
private bool _disposed;
|
||||
|
||||
public async Task<PluginRuntimeSession> 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;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = host,
|
||||
WorkingDirectory = Path.GetDirectoryName(host) ?? AppContext.BaseDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = false
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("--session");
|
||||
startInfo.ArgumentList.Add(sessionId);
|
||||
startInfo.ArgumentList.Add("--plugin-id");
|
||||
startInfo.ArgumentList.Add(request.PluginId);
|
||||
startInfo.ArgumentList.Add("--surface-id");
|
||||
startInfo.ArgumentList.Add(request.SurfaceId);
|
||||
startInfo.ArgumentList.Add("--runtime-kind");
|
||||
startInfo.ArgumentList.Add(request.RuntimeKind.ToString());
|
||||
startInfo.ArgumentList.Add("--plugin-root");
|
||||
startInfo.ArgumentList.Add(request.PluginRoot);
|
||||
startInfo.ArgumentList.Add("--manifest");
|
||||
startInfo.ArgumentList.Add(request.ManifestPath);
|
||||
startInfo.ArgumentList.Add("--entry");
|
||||
startInfo.ArgumentList.Add(request.Entry);
|
||||
if (!string.IsNullOrWhiteSpace(request.CommandId))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--command");
|
||||
startInfo.ArgumentList.Add(request.CommandId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var 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;
|
||||
|
||||
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)
|
||||
{
|
||||
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 (_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);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<PluginRuntimeSession?> GetSessionAsync(string sessionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_sessions.TryGetValue(sessionId, out var session);
|
||||
return Task.FromResult(session);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ShellOutputEvent>> ReadRecentOutputAsync(string sessionId, int take = 200, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!_output.TryGetValue(sessionId, out var lines))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ShellOutputEvent>>([]);
|
||||
}
|
||||
|
||||
lock (lines)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ShellOutputEvent>>(lines.TakeLast(Math.Max(0, take)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
foreach (var sessionId in _processes.Keys.ToArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
StopAsync(sessionId).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteSession(string sessionId, Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
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 string? ResolveHostExecutable()
|
||||
{
|
||||
var candidates = new List<string>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user