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 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(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(frame.Payload); session.AddLog($"[child:{log?.Level ?? "info"}] {log?.Message}"); break; } case IpcKinds.Progress: { var prog = IpcProtocol.FromJsonElement(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(frame.Payload); session.ProgressPercent = 100; session.Status = "Idle"; session.AddLog($"[child] result: {res?.Message}"); break; } case IpcKinds.Error: { var err = IpcProtocol.FromJsonElement(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); }