Files
windows/Network/MdnsPublisher.cs
T

86 lines
3.7 KiB
C#

using System.Net;
using Makaretu.Dns;
namespace MusicBridge.Agent.Network;
// All lifecycle calls run on the console loop, not on network-change callback threads.
internal sealed class MdnsPublisher(string fingerprint, int port) : IDisposable
{
private MulticastService? multicast;
private ServiceDiscovery? discovery;
private ServiceProfile? profile;
private string addressesKey = "";
private DateTimeOffset nextAnnouncement;
private DateTimeOffset lastRefresh;
public string Status { get; private set; } = "mDNS: ожидание локальной сети.";
public static ServiceProfile CreateProfile(string fingerprint, int port, IPAddress[] addresses)
{
if (port is < 1 or > 65535 || addresses.Length == 0 || addresses.Any(a => !LocalAddresses.IsEligible(a)))
throw new ArgumentException("Неверный порт или адрес объявления mDNS.");
var descriptor = DiscoveryDescriptor.Create(fingerprint);
var result = new ServiceProfile(descriptor.InstanceName, "_musicbridge._tcp", (ushort)port, addresses);
foreach (var pair in descriptor.TxtRecords()) result.AddProperty(pair.Key, pair.Value);
foreach (var record in result.Resources) record.TTL = TimeSpan.FromSeconds(120);
return result;
}
public void Refresh(IPAddress[] addresses)
{
try
{
// Rebind after a long pause (sleep/resume), even if Windows kept the same IP.
var now = DateTimeOffset.UtcNow;
if (lastRefresh != default && (now - lastRefresh > TimeSpan.FromSeconds(20) || now < lastRefresh)) Stop();
lastRefresh = now;
var key = string.Join(",", addresses.Select(a => a.ToString()));
if (discovery is null || key != addressesKey)
{
Stop();
if (addresses.Length == 0) { Status = "mDNS: нет адреса домашней сети."; return; }
multicast = new MulticastService(nics => nics.Where(nic => nic.GetIPProperties().UnicastAddresses.Any(a => addresses.Contains(a.Address))))
{
UseIpv4 = true, UseIpv6 = false
};
profile = CreateProfile(fingerprint, port, addresses);
discovery = new ServiceDiscovery(multicast);
discovery.Advertise(profile);
multicast.Start();
addressesKey = key;
nextAnnouncement = DateTimeOffset.MinValue;
}
if (DateTimeOffset.UtcNow >= nextAnnouncement)
{
var message = new Makaretu.Dns.Message { QR = true, AA = true };
message.Answers.Add(new PTRRecord
{
Name = profile!.QualifiedServiceName, DomainName = profile.FullyQualifiedName, TTL = TimeSpan.FromSeconds(120)
});
foreach (var record in profile.Resources) message.Answers.Add(record);
multicast!.SendAnswer(message, checkDuplicate: false);
nextAnnouncement = DateTimeOffset.UtcNow.AddSeconds(30);
}
Status = $"mDNS: объявляется {profile!.FullyQualifiedName}";
}
catch (Exception ex)
{
Stop();
Status = $"mDNS недоступен: {ex.Message}";
}
}
private void Stop()
{
try { if (profile is not null) discovery?.Unadvertise(profile); }
catch (Exception) { /* Best-effort goodbye when a network has disappeared. */ }
discovery?.Dispose();
multicast?.Dispose();
discovery = null;
multicast = null;
profile = null;
addressesKey = "";
}
public void Dispose() => Stop();
}