Initial commit
This commit is contained in:
297
ParentWpf/MainViewModel.cs
Normal file
297
ParentWpf/MainViewModel.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using CommIpc;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
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)
|
||||
{
|
||||
// Prefer spawning the built exe. If not found, fall back to running the dll via dotnet.
|
||||
string? solutionRoot = TryFindSolutionRoot(AppContext.BaseDirectory);
|
||||
if (solutionRoot is null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find solution root (CommTester.slnx).");
|
||||
}
|
||||
|
||||
string debugExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.exe");
|
||||
string releaseExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.exe");
|
||||
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 fileName;
|
||||
string arguments;
|
||||
if (File.Exists(debugExe))
|
||||
{
|
||||
fileName = debugExe;
|
||||
arguments = $"--pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(releaseExe))
|
||||
{
|
||||
fileName = releaseExe;
|
||||
arguments = $"--pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(debugDll))
|
||||
{
|
||||
fileName = "dotnet";
|
||||
arguments = $"\"{debugDll}\" --pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(releaseDll))
|
||||
{
|
||||
fileName = "dotnet";
|
||||
arguments = $"\"{releaseDll}\" --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 Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
HandleIncomingFrame(session, frame);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
session.Status = "Error";
|
||||
session.AddLog($"[parent] Receive loop error: {ex.Message}");
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Application.Current.Dispatcher.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);
|
||||
}
|
||||
Reference in New Issue
Block a user