using System.Diagnostics;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Supervisor;
///
/// Production . Spawns OtOpcUa.Driver.FOCAS.Host.exe
/// with the pipe name / allowed-SID / per-spawn shared secret in the environment, waits for
/// the pipe to come up, then connects a and wraps it in an
/// . On best-effort kills the
/// process and closes the IPC stream.
///
public sealed class ProcessHostLauncher : IHostProcessLauncher
{
private readonly ProcessHostLauncherOptions _options;
private Process? _process;
private FocasIpcClient? _ipc;
public ProcessHostLauncher(ProcessHostLauncherOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public bool IsProcessAlive => _process is { HasExited: false };
public async Task LaunchAsync(CancellationToken ct)
{
await TerminateAsync(ct).ConfigureAwait(false);
var secret = _options.SharedSecret ?? Guid.NewGuid().ToString("N");
var psi = new ProcessStartInfo
{
FileName = _options.HostExePath,
Arguments = _options.Arguments ?? string.Empty,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.Environment["OTOPCUA_FOCAS_PIPE"] = _options.PipeName;
psi.Environment["OTOPCUA_ALLOWED_SID"] = _options.AllowedSid;
psi.Environment["OTOPCUA_FOCAS_SECRET"] = secret;
psi.Environment["OTOPCUA_FOCAS_BACKEND"] = _options.Backend;
_process = Process.Start(psi)
?? throw new InvalidOperationException($"Failed to start {_options.HostExePath}");
// Poll for pipe readiness up to the configured connect timeout.
var deadline = DateTime.UtcNow + _options.ConnectTimeout;
while (true)
{
ct.ThrowIfCancellationRequested();
if (_process.HasExited)
throw new InvalidOperationException(
$"FOCAS Host exited before pipe was ready (ExitCode={_process.ExitCode}).");
try
{
_ipc = await FocasIpcClient.ConnectAsync(
_options.PipeName, secret, TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
break;
}
catch (TimeoutException)
{
if (DateTime.UtcNow >= deadline)
throw new TimeoutException(
$"FOCAS Host pipe {_options.PipeName} did not come up within {_options.ConnectTimeout:g}.");
await Task.Delay(TimeSpan.FromMilliseconds(250), ct).ConfigureAwait(false);
}
}
return new IpcFocasClient(_ipc, _options.Series);
}
public async Task TerminateAsync(CancellationToken ct)
{
if (_ipc is not null)
{
try { await _ipc.DisposeAsync().ConfigureAwait(false); }
catch { /* best effort */ }
_ipc = null;
}
if (_process is not null)
{
try
{
if (!_process.HasExited)
{
_process.Kill(entireProcessTree: true);
await _process.WaitForExitAsync(ct).ConfigureAwait(false);
}
}
catch { /* best effort */ }
finally
{
_process.Dispose();
_process = null;
}
}
}
}
public sealed record ProcessHostLauncherOptions(
string HostExePath,
string PipeName,
string AllowedSid)
{
public string? SharedSecret { get; init; }
public string? Arguments { get; init; }
public string Backend { get; init; } = "fwlib32";
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(15);
public FocasCncSeries Series { get; init; } = FocasCncSeries.Unknown;
}