using System.Net; using System.Net.Sockets; using System.Text; using System.Text.Json; namespace WirelessTextSyncer.Windows.Services; public sealed class DiscoveryResponderService : IDisposable { public const int DiscoveryPort = 8182; public const string DiscoveryRequestType = "wirelessTextSyncer.discovery"; public const string ServiceResponseType = "wirelessTextSyncer.service"; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private readonly Func hostProvider; private readonly Func portProvider; private readonly string deviceName; private readonly CancellationTokenSource cancellation = new(); private UdpClient? udpClient; private Task? listenTask; public DiscoveryResponderService(Func hostProvider, Func portProvider, string? deviceName = null) { this.hostProvider = hostProvider; this.portProvider = portProvider; this.deviceName = string.IsNullOrWhiteSpace(deviceName) ? Environment.MachineName : deviceName; } public void Start() { if (udpClient is not null) { return; } udpClient = new UdpClient(AddressFamily.InterNetwork); udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); udpClient.EnableBroadcast = true; udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, DiscoveryPort)); listenTask = Task.Run(ListenAsync); AppLogger.Info($"Discovery responder listening on UDP {DiscoveryPort}."); } public void Dispose() { cancellation.Cancel(); udpClient?.Dispose(); try { listenTask?.Wait(TimeSpan.FromSeconds(1)); } catch (AggregateException) { } cancellation.Dispose(); } public string BuildResponseJson() { return JsonSerializer.Serialize( new DiscoveryResponse(ServiceResponseType, 1, deviceName, hostProvider(), portProvider()), JsonOptions); } private async Task ListenAsync() { var token = cancellation.Token; while (!token.IsCancellationRequested && udpClient is not null) { try { var result = await udpClient.ReceiveAsync(token); if (!IsDiscoveryRequest(result.Buffer)) { continue; } var responseBytes = Encoding.UTF8.GetBytes(BuildResponseJson()); await udpClient.SendAsync(responseBytes, result.RemoteEndPoint, token); AppLogger.Info($"Answered discovery request from {result.RemoteEndPoint}."); } catch (OperationCanceledException) { break; } catch (ObjectDisposedException) { break; } catch (Exception exception) { AppLogger.Error("Discovery responder failed to process a packet.", exception); } } } private static bool IsDiscoveryRequest(byte[] bytes) { try { var request = JsonSerializer.Deserialize(bytes, JsonOptions); return request?.Type == DiscoveryRequestType && request.Version == 1; } catch (JsonException) { return false; } } private sealed record DiscoveryRequest(string Type, int Version); private sealed record DiscoveryResponse(string Type, int Version, string Name, string Host, int Port); }