using System.Diagnostics; using System.Net.Sockets; using System.Text; namespace Housing.Services; public sealed class TcpStartSignalWatcher { private readonly StartSignalSettings _settings; public TcpStartSignalWatcher(StartSignalSettings settings) { _settings = settings; } public async Task WaitForStartAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(_settings.Host)) { throw new InvalidOperationException("Hardware.ini [StartSignal] Host 값을 LAN 장비 IP로 설정하세요."); } using var client = new TcpClient(); await ConnectAsync(client, cancellationToken); using var stream = client.GetStream(); var stopwatch = Stopwatch.StartNew(); if (string.IsNullOrWhiteSpace(_settings.ReadCommand)) { await WaitForEventMessageAsync(stream, stopwatch, cancellationToken); return; } await WaitForPolledResponseAsync(stream, stopwatch, cancellationToken); } private async Task ConnectAsync(TcpClient client, CancellationToken cancellationToken) { try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); if (_settings.TimeoutMilliseconds > 0) { timeout.CancelAfter(_settings.TimeoutMilliseconds); } await client.ConnectAsync(_settings.Host, _settings.Port, timeout.Token); } catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException($"LAN 시작 신호 장비 연결 시간 초과: {_settings.Host}:{_settings.Port}", ex); } catch (SocketException ex) { throw new InvalidOperationException($"LAN 시작 신호 장비 연결 실패: {_settings.Host}:{_settings.Port}, {ex.Message}", ex); } } private async Task WaitForEventMessageAsync(NetworkStream stream, Stopwatch stopwatch, CancellationToken cancellationToken) { while (true) { ThrowIfTimedOut(stopwatch, "LAN 메시지 수신 대기"); var response = await ReadResponseAsync(stream, cancellationToken); if (_settings.IsActive(response)) { return; } } } private async Task WaitForPolledResponseAsync(NetworkStream stream, Stopwatch stopwatch, CancellationToken cancellationToken) { if (_settings.RequireInactiveBeforeStart) { while (_settings.IsActive(await QueryAsync(stream, cancellationToken))) { ThrowIfTimedOut(stopwatch, "기존 LAN 시작 상태 해제 대기"); await DelayAsync(cancellationToken); } } while (!_settings.IsActive(await QueryAsync(stream, cancellationToken))) { ThrowIfTimedOut(stopwatch, "LAN 시작 응답 대기"); await DelayAsync(cancellationToken); } } private async Task QueryAsync(NetworkStream stream, CancellationToken cancellationToken) { var command = _settings.ReadCommand.Trim() + _settings.GetTerminator(); var bytes = Encoding.ASCII.GetBytes(command); await stream.WriteAsync(bytes, cancellationToken); await stream.FlushAsync(cancellationToken); return await ReadResponseAsync(stream, cancellationToken); } private async Task ReadResponseAsync(NetworkStream stream, CancellationToken cancellationToken) { var buffer = new byte[1024]; var response = new StringBuilder(); using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(_settings.ReadTimeoutMilliseconds); try { while (true) { var count = await stream.ReadAsync(buffer, timeout.Token); if (count == 0) { break; } var chunk = Encoding.ASCII.GetString(buffer, 0, count); response.Append(chunk); if (chunk.Contains('\n') || chunk.Contains('\r') || !stream.DataAvailable) { break; } } } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { return response.ToString(); } return response.ToString(); } private async Task DelayAsync(CancellationToken cancellationToken) { await Task.Delay(_settings.PollIntervalMilliseconds, cancellationToken); } private void ThrowIfTimedOut(Stopwatch stopwatch, string state) { if (_settings.TimeoutMilliseconds <= 0 || stopwatch.ElapsedMilliseconds <= _settings.TimeoutMilliseconds) { return; } throw new TimeoutException( $"LAN 시작 신호 대기 시간 초과: {state}, 대상={_settings.Host}:{_settings.Port}, 제한={_settings.TimeoutMilliseconds}ms"); } }