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; }