Files
CommTester/ParentAvalonia/MainViewModel.cs

288 lines
9.1 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using Avalonia.Threading;
using CommIpc;
namespace ParentAvalonia;
public sealed class MainViewModel : NotifyBase, IAsyncDisposable
{
private ChildSession? _selectedChild;
private bool _childrenStarted;
private readonly CancellationTokenSource _shutdownCts = new();
public ObservableCollection<ChildSession> Children { get; } = new();
public ChildSession? SelectedChild
{
get => _selectedChild;
set => SetField(ref _selectedChild, value);
}
public async Task StartChildrenAsync(int count)
{
if (_childrenStarted)
{
return;
}
_childrenStarted = true;
for (int i = 1; i <= count; i++)
{
await StartChildAsync(i, _shutdownCts.Token);
}
}
public async Task PingSelectedAsync()
{
if (SelectedChild is null)
{
return;
}
string corr = Guid.NewGuid().ToString("N");
await SelectedChild.SendAsync(
new IpcFrame(IpcKinds.Ping, CorrelationId: corr, Payload: IpcProtocol.ToJsonElement(new { from = "parent" })),
SelectedChild.LifetimeCts.Token
);
SelectedChild.AddLog($"[parent] -> ping ({corr})");
}
public async Task StartWorkSelectedAsync(int steps = 30, int delayMs = 120)
{
if (SelectedChild is null)
{
return;
}
string corr = Guid.NewGuid().ToString("N");
SelectedChild.CurrentWorkId = corr;
SelectedChild.ProgressPercent = 0;
await SelectedChild.SendAsync(
new IpcFrame(IpcKinds.StartWork, CorrelationId: corr, Payload: IpcProtocol.ToJsonElement(new { steps, delayMs })),
SelectedChild.LifetimeCts.Token
);
SelectedChild.AddLog($"[parent] -> startWork (corr={corr}, steps={steps}, delayMs={delayMs})");
}
public async Task CancelWorkSelectedAsync()
{
if (SelectedChild is null)
{
return;
}
string corr = SelectedChild.CurrentWorkId ?? Guid.NewGuid().ToString("N");
await SelectedChild.SendAsync(new IpcFrame(IpcKinds.CancelWork, CorrelationId: corr), SelectedChild.LifetimeCts.Token);
SelectedChild.AddLog($"[parent] -> cancelWork (corr={corr})");
}
private async Task StartChildAsync(int childId, CancellationToken cancellationToken)
{
string pipeName = PipeName.ForChild(childId, Environment.ProcessId);
var server = new NamedPipeServerStream(
pipeName: pipeName,
direction: PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous
);
Task waitForConnection = server.WaitForConnectionAsync(cancellationToken);
Process process = StartChildProcess(childId, pipeName);
var session = new ChildSession(childId, pipeName, server, process);
Children.Add(session);
SelectedChild ??= session;
session.AddLog($"[parent] Starting {process.StartInfo.FileName} {process.StartInfo.Arguments}");
await waitForConnection;
session.IsConnected = true;
session.Status = "Connected";
session.AddLog($"[parent] Connected to {PipeName.Describe(pipeName)}");
_ = Task.Run(() => ReceiveLoopAsync(session), _shutdownCts.Token);
}
private static Process StartChildProcess(int childId, string pipeName)
{
// Cross-platform friendly: prefer running the built dll via 'dotnet'.
string? solutionRoot = TryFindSolutionRoot(AppContext.BaseDirectory);
if (solutionRoot is null)
{
throw new InvalidOperationException("Could not find solution root (CommTester.slnx).");
}
string debugDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.dll");
string releaseDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.dll");
string childDll =
File.Exists(debugDll) ? debugDll
: File.Exists(releaseDll) ? releaseDll
: string.Empty;
string fileName;
string arguments;
if (!string.IsNullOrWhiteSpace(childDll))
{
fileName = "dotnet";
arguments = $"\"{childDll}\" --pipe \"{pipeName}\" --id {childId}";
}
else
{
// Last-resort: run via project (slower but robust in fresh checkouts).
string csproj = Path.Combine(solutionRoot, "ChildWorker", "ChildWorker.csproj");
fileName = "dotnet";
arguments = $"run --project \"{csproj}\" -- --pipe \"{pipeName}\" --id {childId}";
}
var psi = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
};
var p = new Process { StartInfo = psi, EnableRaisingEvents = true };
p.Start();
return p;
}
private static string? TryFindSolutionRoot(string baseDirectory)
{
var dir = new DirectoryInfo(baseDirectory);
for (int i = 0; i < 10 && dir is not null; i++)
{
string slnx = Path.Combine(dir.FullName, "CommTester.slnx");
if (File.Exists(slnx))
{
return dir.FullName;
}
dir = dir.Parent;
}
return null;
}
private async Task ReceiveLoopAsync(ChildSession session)
{
try
{
while (!session.LifetimeCts.IsCancellationRequested)
{
IpcFrame? frame = await IpcProtocol.ReadFrameAsync(session.Pipe, cancellationToken: session.LifetimeCts.Token);
if (frame is null)
{
break;
}
await Dispatcher.UIThread.InvokeAsync(() => HandleIncomingFrame(session, frame));
}
}
catch (OperationCanceledException)
{
// ignore
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
session.Status = "Error";
session.AddLog($"[parent] Receive loop error: {ex.Message}");
});
}
finally
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
session.IsConnected = false;
session.Status = "Disconnected";
session.AddLog("[parent] Disconnected.");
});
}
}
private static void HandleIncomingFrame(ChildSession session, IpcFrame frame)
{
switch (frame.Kind)
{
case IpcKinds.Hello:
{
var hello = IpcProtocol.FromJsonElement<HelloPayload>(frame.Payload);
session.AddLog($"[child] hello: id={hello?.ChildId}, pid={hello?.Pid}");
break;
}
case IpcKinds.Pong:
session.AddLog($"[child] pong ({frame.CorrelationId})");
break;
case IpcKinds.Log:
{
var log = IpcProtocol.FromJsonElement<LogPayload>(frame.Payload);
session.AddLog($"[child:{log?.Level ?? "info"}] {log?.Message}");
break;
}
case IpcKinds.Progress:
{
var prog = IpcProtocol.FromJsonElement<ProgressPayload>(frame.Payload);
if (prog is not null)
{
session.ProgressPercent = prog.Percent;
session.Status = $"Working ({prog.Step}/{prog.Total})";
}
break;
}
case IpcKinds.Result:
{
var res = IpcProtocol.FromJsonElement<ResultPayload>(frame.Payload);
session.ProgressPercent = 100;
session.Status = "Idle";
session.AddLog($"[child] result: {res?.Message}");
break;
}
case IpcKinds.Error:
{
var err = IpcProtocol.FromJsonElement<ErrorPayload>(frame.Payload);
session.Status = "Error";
session.AddLog($"[child] error: {err?.Message}");
break;
}
default:
session.AddLog($"[child] {frame.Kind} ({frame.CorrelationId})");
break;
}
}
public async ValueTask DisposeAsync()
{
_shutdownCts.Cancel();
foreach (var child in Children.ToArray())
{
try
{
await child.DisposeAsync();
}
catch { }
}
_shutdownCts.Dispose();
}
private sealed record HelloPayload(int ChildId, int Pid);
private sealed record LogPayload(string Level, string Message);
private sealed record ProgressPayload(int Step, int Total, double Percent);
private sealed record ResultPayload(string Message);
private sealed record ErrorPayload(string Message);
}