Files
2026-07-13 13:13:17 +08:00

296 lines
10 KiB
C#

#nullable enable
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using T3.Core.Utils;
namespace Lib.io.udp;
[Guid("C029B846-B442-458B-933B-653609827B75")]
internal sealed class UdpInput : Instance<UdpInput>, IStatusProvider, ICustomDropdownHolder, IDisposable
{
[Output(Guid = "9C4D3558-1584-422E-A59B-D08D23E45242")]
public readonly Slot<bool> IsListening = new();
[Output(Guid = "8056024D-4581-4328-8547-19B44EA58742", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<string> LastSenderAddress = new();
[Output(Guid = "D938634B-3736-444F-942F-C2D046D06D4D", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<int> LastSenderPort = new();
[Input(Guid = "0944714D-693D-4251-93A6-E22A2DB64F20")]
public readonly InputSlot<bool> Listen = new();
[Input(Guid = "E6589335-4E51-41B0-8777-6A5D54C4F0EE")]
public readonly InputSlot<int> ListLength = new(10);
[Input(Guid = "9E23335A-D63A-4286-930E-C63E86D0E6F0")]
public readonly InputSlot<string> LocalIpAddress = new("0.0.0.0");
[Input(Guid = "2EBE418D-407E-46D8-B274-13B41C52ACCF")]
public readonly InputSlot<int> Port = new(7000);
[Input(Guid = "5E725916-4143-4759-8651-E12185C658D3")]
public readonly InputSlot<bool> PrintToLog = new();
[Output(Guid = "444498A6-972F-4375-A152-A103AC537A1D", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<List<string>> ReceivedLines = new();
[Output(Guid = "21E92723-E786-4556-91F6-31804301509D", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<string> ReceivedString = new();
[Output(Guid = "E4162B57-5586-4513-A551-7C64B95B8A1D", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<bool> WasTrigger = new();
private string? _lastLocalIp;
private int _lastPort;
private bool _printToLog;
private bool _wasListening;
public UdpInput()
{
ReceivedString.UpdateAction = Update;
ReceivedLines.UpdateAction = Update;
LastSenderAddress.UpdateAction = Update;
LastSenderPort.UpdateAction = Update;
WasTrigger.UpdateAction = Update;
IsListening.UpdateAction = Update;
}
public void Dispose()
{
StopListening();
}
private void Update(EvaluationContext context)
{
_printToLog = PrintToLog.GetValue(context);
var shouldListen = Listen.GetValue(context);
var localIp = LocalIpAddress.GetValue(context);
var port = Port.GetValue(context);
var settingsChanged = shouldListen != _wasListening || localIp != _lastLocalIp || port != _lastPort;
if (settingsChanged)
{
StopListening();
if (shouldListen) StartListening();
_wasListening = shouldListen;
_lastLocalIp = localIp;
_lastPort = port;
}
var listLength = ListLength.GetValue(context).Clamp(1, 1000);
var wasTriggered = false;
while (_receivedQueue.TryDequeue(out var msg))
{
ReceivedString.Value = msg.DecodedString;
LastSenderAddress.Value = msg.Source.Address.ToString();
LastSenderPort.Value = msg.Source.Port;
_messageHistory.Add(msg.DecodedString);
wasTriggered = true;
}
while (_messageHistory.Count > listLength)
_messageHistory.RemoveAt(0);
ReceivedLines.Value = _messageHistory;
WasTrigger.Value = wasTriggered;
IsListening.Value = _runListener;
UpdateStatusMessage();
}
private void StartListening()
{
if (_listenerThread is { IsAlive: true }) return;
_runListener = true;
_listenerThread = new Thread(ListenLoop) { IsBackground = true, Name = "UDPInputListener" };
_listenerThread.Start();
if (_printToLog)
{
Log.Debug($"UDP Input: Starting listener thread.", this);
}
}
private void StopListening()
{
if (!_runListener) return;
_runListener = false;
if (_printToLog)
{
Log.Debug("UDP Input: Stopping listener.", this);
}
_udpClient?.Close();
_listenerThread?.Join(200); // Give the thread a moment to shut down
_listenerThread = null;
if (_printToLog)
{
Log.Debug("UDP Input: Listener stopped.", this);
}
}
private void ListenLoop()
{
var port = Port.Value;
var localIpStr = LocalIpAddress.Value;
UdpClient? currentUdpClient = null; // Declare locally
try
{
var listenIp = IPAddress.Any;
if (!string.IsNullOrEmpty(localIpStr) && localIpStr != "0.0.0.0 (Any)" && IPAddress.TryParse(localIpStr, out var parsedIp))
listenIp = parsedIp;
var localEp = new IPEndPoint(listenIp, port);
currentUdpClient = new UdpClient { ExclusiveAddressUse = false };
currentUdpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
currentUdpClient.Client.Bind(localEp);
_udpClient = currentUdpClient;
if (_printToLog)
{
Log.Debug($"UDP Input: Bound to {localEp.Address}:{localEp.Port}. Ready to receive.", this);
}
var remoteEp = new IPEndPoint(IPAddress.Any, 0);
while (_runListener)
{
try
{
if (_udpClient == null) break;
var data = _udpClient.Receive(ref remoteEp);
if (_printToLog)
{
var message = Encoding.UTF8.GetString(data);
Log.Debug($"UDP Input received from {remoteEp.Address}:{remoteEp.Port}: \"{message}\"", this);
}
_receivedQueue.Enqueue(new ReceivedMessage(data, remoteEp));
ReceivedString.DirtyFlag.Invalidate();
}
catch (SocketException ex)
{
if (_runListener)
{
Log.Warning($"UDP Input receive socket error: {ex.Message} (Error Code: {ex.ErrorCode})", this);
}
break;
}
catch (Exception e)
{
if (_runListener)
{
Log.Error($"UDP Input receive error: {e.Message}", this);
}
}
}
}
catch (Exception e)
{
SetStatus($"Failed to bind to {localIpStr}:{port}. Error: {e.Message}", IStatusProvider.StatusLevel.Error);
if (_printToLog)
{
Log.Error($"UDP Input: Failed to bind to {localIpStr}:{port}. Error: {e.Message}", this);
}
}
finally
{
currentUdpClient?.Close();
if (_udpClient == currentUdpClient) _udpClient = null;
}
}
private void UpdateStatusMessage()
{
var localIpDisplay = LocalIpAddress.Value ?? string.Empty;
if (!_runListener) SetStatus("Not listening. Enable 'Listen'.", IStatusProvider.StatusLevel.Notice);
else if (_lastStatusLevel != IStatusProvider.StatusLevel.Error)
SetStatus($"Listening on {localIpDisplay.Split(' ')[0]}:{Port.Value}", IStatusProvider.StatusLevel.Success);
}
#region internal types, state, and providers
private UdpClient? _udpClient;
private Thread? _listenerThread;
private volatile bool _runListener;
private readonly ConcurrentQueue<ReceivedMessage> _receivedQueue = new();
private readonly List<string> _messageHistory = new();
private readonly struct ReceivedMessage
{
public readonly IPEndPoint Source;
public readonly string DecodedString;
public ReceivedMessage(byte[] data, IPEndPoint source)
{
Source = source;
DecodedString = Encoding.UTF8.GetString(data);
}
}
private string? _lastErrorMessage = "Not listening.";
private IStatusProvider.StatusLevel _lastStatusLevel = IStatusProvider.StatusLevel.Notice;
public void SetStatus(string m, IStatusProvider.StatusLevel l)
{
_lastErrorMessage = m;
_lastStatusLevel = l;
}
public IStatusProvider.StatusLevel GetStatusLevel() => _lastStatusLevel;
public string? GetStatusMessage() => _lastErrorMessage;
#region Network Interface Logic
private static List<NetworkAdapterInfo> _networkInterfaces = new();
private static List<NetworkAdapterInfo> GetNetworkInterfaces()
{
var list = new List<NetworkAdapterInfo>();
list.Add(new NetworkAdapterInfo(IPAddress.Any, IPAddress.Any, "Any"));
list.Add(new NetworkAdapterInfo(IPAddress.Loopback, IPAddress.Parse("255.0.0.0"), "Localhost"));
try
{
list.AddRange(from ni in NetworkInterface.GetAllNetworkInterfaces()
where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback
from ip in ni.GetIPProperties().UnicastAddresses
where ip.Address.AddressFamily == AddressFamily.InterNetwork
select new NetworkAdapterInfo(ip.Address, ip.IPv4Mask, ni.Name));
}
catch (Exception e)
{
Log.Warning("Could not enumerate network interfaces: " + e.Message);
}
return list;
}
private sealed record NetworkAdapterInfo(IPAddress IpAddress, IPAddress SubnetMask, string Name)
{
public string DisplayName => $"{Name}: {IpAddress}";
}
#endregion
string ICustomDropdownHolder.GetValueForInput(Guid id) => id == LocalIpAddress.Id ? LocalIpAddress.Value : string.Empty;
IEnumerable<string> ICustomDropdownHolder.GetOptionsForInput(Guid id)
{
if (id == LocalIpAddress.Id)
{
_networkInterfaces = GetNetworkInterfaces();
foreach (var adapter in _networkInterfaces) yield return adapter.DisplayName;
}
}
void ICustomDropdownHolder.HandleResultForInput(Guid id, string? s, bool i)
{
if (string.IsNullOrEmpty(s) || !i || id != LocalIpAddress.Id) return;
var foundAdapter = _networkInterfaces.FirstOrDefault(adapter => adapter.DisplayName == s);
if (foundAdapter != null) LocalIpAddress.SetTypedInputValue(foundAdapter.IpAddress.ToString());
}
#endregion
}