Migrate existing code
This commit is contained in:
parent
384ff4a6f3
commit
9b49f880a2
30 changed files with 1789 additions and 5 deletions
|
|
@ -8,6 +8,13 @@
|
|||
"libman"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"dotnet-outdated-tool": {
|
||||
"version": "4.6.4",
|
||||
"commands": [
|
||||
"dotnet-outdated"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public interface IKeaDhcpLeaseHandler
|
||||
{
|
||||
DhcpLeaseChange? Handle(in SepReader.Row row);
|
||||
}
|
||||
12
Integrations/Kea/IKeaFactory.cs
Normal file
12
Integrations/Kea/IKeaFactory.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using DotNetDDI.Options;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public interface IKeaFactory
|
||||
{
|
||||
KeaDhcp4LeaseHandler CreateHandler4();
|
||||
|
||||
KeaDhcp6LeaseHandler CreateHandler6();
|
||||
|
||||
KeaDhcpLeaseWatcher CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options);
|
||||
}
|
||||
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
using Cell = System.ReadOnlySpan<char>;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
using Lease = KeaDhcp4Lease;
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file4.h
|
||||
public record struct KeaDhcp4Lease(
|
||||
IPAddress Address,
|
||||
PhysicalAddress HWAddr,
|
||||
string? ClientId,
|
||||
TimeSpan ValidLifetime,
|
||||
DateTimeOffset Expire,
|
||||
uint SubnetId,
|
||||
bool FqdnFwd,
|
||||
bool FqdnRev,
|
||||
string Hostname,
|
||||
uint State,
|
||||
string? UserContext,
|
||||
uint PoolId)
|
||||
{
|
||||
public static Lease? Parse(in SepReader.Row row)
|
||||
{
|
||||
Lease result = new();
|
||||
for (int i = 0; i < row.ColCount; i++)
|
||||
{
|
||||
if (Parse(ref result, i, row[i].Span) == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||
{
|
||||
return column switch
|
||||
{
|
||||
0 => ToIPAddress(ref lease, span),
|
||||
1 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||
2 => ToClientId(ref lease, span),
|
||||
3 => ToValidLifetime(ref lease, span),
|
||||
4 => ToExpire(ref lease, span),
|
||||
5 => ToSubnetId(ref lease, span),
|
||||
6 => ToFqdnFwd(ref lease, span),
|
||||
7 => ToFqdnRev(ref lease, span),
|
||||
8 => ToHostname(ref lease, span),
|
||||
9 => ToState(ref lease, span),
|
||||
10 => ToUserContext(ref lease, span),
|
||||
11 => ToPoolId(ref lease, span),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
static bool ToIPAddress(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = IPAddress.TryParse(span, out var address);
|
||||
lease.Address = address!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = PhysicalAddress.TryParse(span, out var hwaddr);
|
||||
lease.HWAddr = hwaddr!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToClientId(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.ClientId = span.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var validLifetime);
|
||||
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToExpire(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ulong.TryParse(span, out var expire);
|
||||
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var subnetId);
|
||||
lease.SubnetId = subnetId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||
lease.FqdnFwd = fqdnFwd != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnRev);
|
||||
lease.FqdnRev = fqdnRev != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHostname(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToState(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var state);
|
||||
lease.State = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var poolId);
|
||||
lease.PoolId = poolId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaDhcp4LeaseHandler : IKeaDhcpLeaseHandler
|
||||
{
|
||||
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||
{
|
||||
if (KeaDhcp4Lease.Parse(row) is not { } lease)
|
||||
{
|
||||
goto exitNull;
|
||||
}
|
||||
|
||||
if (lease.State != 0)
|
||||
{
|
||||
goto exitNull;
|
||||
}
|
||||
|
||||
DhcpLeaseIdentifier identifier = lease.ClientId switch
|
||||
{
|
||||
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||
};
|
||||
|
||||
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||
|
||||
exitNull:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
using Cell = System.ReadOnlySpan<char>;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
using Lease = KeaDhcp6Lease;
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file6.h
|
||||
public record struct KeaDhcp6Lease(
|
||||
IPAddress Address,
|
||||
string DUId,
|
||||
TimeSpan ValidLifetime,
|
||||
DateTimeOffset Expire,
|
||||
uint SubnetId,
|
||||
uint PrefLifetime,
|
||||
LeaseType LeaseType,
|
||||
uint IAId,
|
||||
byte PrefixLen,
|
||||
bool FqdnFwd,
|
||||
bool FqdnRev,
|
||||
string Hostname,
|
||||
PhysicalAddress HWAddr,
|
||||
uint State,
|
||||
string? UserContext,
|
||||
ushort? HWType,
|
||||
uint? HWAddrSource,
|
||||
uint PoolId)
|
||||
{
|
||||
public static Lease? Parse(in SepReader.Row row)
|
||||
{
|
||||
Lease result = new();
|
||||
for (int i = 0; i < row.ColCount; i++)
|
||||
{
|
||||
if (Parse(ref result, i, row[i].Span) == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||
{
|
||||
return column switch
|
||||
{
|
||||
0 => ToAddress(ref lease, span),
|
||||
1 => ToDUId(ref lease, span),
|
||||
2 => ToValidLifetime(ref lease, span),
|
||||
3 => ToExpire(ref lease, span),
|
||||
4 => ToSubnetId(ref lease, span),
|
||||
5 => ToPrefLifetime(ref lease, span),
|
||||
6 => ToLeaseType(ref lease, span),
|
||||
7 => ToIAId(ref lease, span),
|
||||
8 => ToPrefixLen(ref lease, span),
|
||||
9 => ToFqdnFwd(ref lease, span),
|
||||
10 => ToFqdnRev(ref lease, span),
|
||||
11 => ToHostname(ref lease, span),
|
||||
12 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||
13 => ToState(ref lease, span),
|
||||
14 when !span.IsWhiteSpace() => ToUserContext(ref lease, span),
|
||||
15 => ToHWType(ref lease, span),
|
||||
16 => ToHWAddrSource(ref lease, span),
|
||||
17 => ToPoolId(ref lease, span),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
static bool ToAddress(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = IPAddress.TryParse(span, out var address);
|
||||
lease.Address = address!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToDUId(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.DUId = span.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var validLifetime);
|
||||
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToExpire(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ulong.TryParse(span, out var expire);
|
||||
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var subnetId);
|
||||
lease.SubnetId = subnetId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPrefLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var prefLifetime);
|
||||
lease.PrefLifetime = prefLifetime;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToLeaseType(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var leaseType);
|
||||
lease.LeaseType = (LeaseType)leaseType;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToIAId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var iaId);
|
||||
lease.IAId = iaId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPrefixLen(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var prefixLen);
|
||||
lease.PrefixLen = prefixLen;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||
lease.FqdnFwd = fqdnFwd != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnRev);
|
||||
lease.FqdnRev = fqdnRev != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHostname(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = PhysicalAddress.TryParse(span, out var hwAddr);
|
||||
lease.HWAddr = hwAddr!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToState(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var state);
|
||||
lease.State = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToHWType(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ushort.TryParse(span, out var hwType);
|
||||
lease.HWType = hwType;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHWAddrSource(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var hwAddrSource);
|
||||
lease.HWAddrSource = hwAddrSource;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var poolId);
|
||||
lease.PoolId = poolId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaDhcp6LeaseHandler : IKeaDhcpLeaseHandler
|
||||
{
|
||||
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||
{
|
||||
if (KeaDhcp6Lease.Parse(row) is not { } lease)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DhcpLeaseIdentifier identifier = lease.DUId switch
|
||||
{
|
||||
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||
};
|
||||
|
||||
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||
}
|
||||
}
|
||||
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Buffers;
|
||||
using System.Globalization;
|
||||
|
||||
using DotNext.Buffers;
|
||||
|
||||
using CommunityToolkit.HighPerformance.Buffers;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public static class KeaDhcpLease
|
||||
{
|
||||
private static ReadOnlySpan<char> EscapeTag => ['&', '#', 'x'];
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/util/csv_file.cc#L479
|
||||
public static string Unescape(in ReadOnlySpan<char> text)
|
||||
{
|
||||
return text.IndexOf(EscapeTag) switch
|
||||
{
|
||||
-1 => text.ToString(),
|
||||
int i => SlowPath(i, text)
|
||||
};
|
||||
|
||||
static string SlowPath(int esc_pos, in ReadOnlySpan<char> text)
|
||||
{
|
||||
SpanReader<char> reader = new(text);
|
||||
using ArrayPoolBufferWriter<char> writer = new(text.Length);
|
||||
while (reader.RemainingCount > 0)
|
||||
{
|
||||
writer.Write(reader.Read(esc_pos));
|
||||
reader.Advance(EscapeTag.Length);
|
||||
|
||||
bool converted = false;
|
||||
char escapedChar = default;
|
||||
if (reader.RemainingCount >= 2)
|
||||
{
|
||||
if (byte.TryParse(reader.RemainingSpan[..2], NumberStyles.AllowHexSpecifier, null, out var b))
|
||||
{
|
||||
converted = true;
|
||||
escapedChar = (char)b;
|
||||
reader.Advance(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (converted)
|
||||
{
|
||||
writer.Write([escapedChar]);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write(EscapeTag);
|
||||
}
|
||||
|
||||
esc_pos = reader.RemainingSpan.IndexOf(EscapeTag);
|
||||
if (esc_pos == -1)
|
||||
{
|
||||
writer.Write(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
return writer.WrittenSpan.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public sealed class KeaDhcpLeaseWatcher : IHostedService
|
||||
{
|
||||
private static readonly FileStreamOptions LeaseFileStreamOptions = new()
|
||||
{
|
||||
Access = FileAccess.Read,
|
||||
Mode = FileMode.Open,
|
||||
Options = FileOptions.SequentialScan | FileOptions.Asynchronous,
|
||||
Share = (FileShare)7,
|
||||
};
|
||||
private static readonly SepReaderOptions MemfileReader = Sep.New(',').Reader(o => o with
|
||||
{
|
||||
DisableColCountCheck = true,
|
||||
DisableFastFloat = true
|
||||
});
|
||||
|
||||
private readonly Decoder _decoder;
|
||||
private readonly FileSystemWatcher _fsw;
|
||||
private readonly IKeaDhcpLeaseHandler _handler;
|
||||
private readonly string _leaseFile;
|
||||
private readonly Pipe _pipe;
|
||||
private readonly DhcpLeaseQueue _queue;
|
||||
private Channel<FileSystemEventArgs>? _eventChannel;
|
||||
private Task? _executeTask;
|
||||
private CancellationTokenSource? _stoppingCts;
|
||||
|
||||
private KeaDhcpServerOptions Options { get; }
|
||||
|
||||
public KeaDhcpLeaseWatcher(KeaDhcpServerOptions options, IKeaDhcpLeaseHandler handler, DhcpLeaseQueue queue)
|
||||
{
|
||||
Options = options = options with { Leases = PathEx.ExpandPath(options.Leases) };
|
||||
_handler = handler;
|
||||
_queue = queue;
|
||||
|
||||
var leases = options.Leases.AsSpan();
|
||||
if (leases.IsWhiteSpace())
|
||||
{
|
||||
throw new ArgumentException($"{nameof(options.Leases)} must not be empty.", nameof(options));
|
||||
}
|
||||
|
||||
var leaseFile = Path.GetFileName(leases);
|
||||
var leaseDirectory = Path.GetDirectoryName(leases).ToString();
|
||||
if (!Directory.Exists(leaseDirectory))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(options.Leases)} must point to a file in an existing path.", nameof(options));
|
||||
}
|
||||
|
||||
_decoder = Encoding.UTF8.GetDecoder();
|
||||
_leaseFile = leaseFile.ToString();
|
||||
_fsw = new(leaseDirectory, _leaseFile);
|
||||
_fsw.Changed += OnLeaseChanged;
|
||||
_fsw.Error += OnLeaseError;
|
||||
_pipe = new();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_executeTask = Reading(_stoppingCts.Token);
|
||||
return _executeTask is { IsCompleted: true } completed ? completed : Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_executeTask is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fsw.EnableRaisingEvents = false;
|
||||
try
|
||||
{
|
||||
_stoppingCts!.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
using (cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource))
|
||||
{
|
||||
await Task.WhenAny(_executeTask, taskCompletionSource.Task).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Reading(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using CancellationTokenSource loopCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||
var loopToken = loopCts.Token;
|
||||
_eventChannel = Channel.CreateUnbounded<FileSystemEventArgs>();
|
||||
_fsw.EnableRaisingEvents = true;
|
||||
using AutoResetEvent resetEvent = new(false);
|
||||
Task reader = Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
for (
|
||||
EventArgs readArgs = EventArgs.Empty;
|
||||
!loopToken.IsCancellationRequested;
|
||||
readArgs = await _eventChannel.Reader.ReadAsync(loopToken))
|
||||
{
|
||||
// Guard for Deleted and renamed away events,
|
||||
// both have to stop this reader immediately.
|
||||
// Just wait for the file being created/renamed to _leaseFile.
|
||||
// Described in [The LFC Process](https://kea.readthedocs.io/en/latest/arm/lfc.html#kea-lfc)
|
||||
switch (readArgs)
|
||||
{
|
||||
case FileSystemEventArgs { ChangeType: WatcherChangeTypes.Deleted }:
|
||||
case RenamedEventArgs renamed when renamed.OldName == _leaseFile:
|
||||
loopCts.Cancel();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader is not { IsCompleted: false })
|
||||
{
|
||||
// In any case that the reader failed (for whatever reason)
|
||||
// restart now.
|
||||
// Incoming event could be Changed/Created/Renamed
|
||||
// This doesn't care, as we already lost the file handle.
|
||||
reader = FileReader(resetEvent, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
resetEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_eventChannel.Writer.TryComplete();
|
||||
if (reader is { IsCompleted: false })
|
||||
{
|
||||
await reader.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
}
|
||||
|
||||
_pipe.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FileReader(AutoResetEvent waitHandle, CancellationToken stoppingToken)
|
||||
{
|
||||
SepReader? reader = null;
|
||||
try
|
||||
{
|
||||
PipeWriter writer = _pipe.Writer;
|
||||
using (stoppingToken.Register(s => ((PipeWriter)s!).Complete(null), writer))
|
||||
{
|
||||
using var file = new FileStream(Options.Leases, LeaseFileStreamOptions);
|
||||
|
||||
bool awaitLineFeed = false;
|
||||
int newLinesEncountered = 0;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
for (; newLinesEncountered > 0; newLinesEncountered--)
|
||||
{
|
||||
if (reader is null)
|
||||
{
|
||||
// LongRunning, force spawning a thread
|
||||
// As this may block for a long time.
|
||||
reader = await Task.Factory.StartNew(
|
||||
s => MemfileReader.From((Stream)s!),
|
||||
_pipe.Reader.AsStream(),
|
||||
stoppingToken,
|
||||
TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default)
|
||||
.ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!reader.MoveNext())
|
||||
{
|
||||
// TODO Error state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_handler.Handle(reader.Current) is not { } lease)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await _queue.Write(lease, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var memory = writer.GetMemory();
|
||||
int read = await file.ReadAsync(memory, stoppingToken);
|
||||
if (read > 0)
|
||||
{
|
||||
CountNewLines(_decoder, memory[..read], ref newLinesEncountered, ref awaitLineFeed);
|
||||
writer.Advance(read);
|
||||
await writer.FlushAsync(stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await waitHandle.WaitOneAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader?.Dispose();
|
||||
}
|
||||
|
||||
static void CountNewLines(Decoder decoder, in Memory<byte> memory, ref int newLinesEncountered, ref bool awaitLineFeed)
|
||||
{
|
||||
Span<char> buffer = stackalloc char[128];
|
||||
bool completed = false;
|
||||
ReadOnlySequence<byte> sequence = new(memory);
|
||||
var reader = new SequenceReader<byte>(sequence);
|
||||
while (!reader.End)
|
||||
{
|
||||
decoder.Convert(reader.UnreadSpan, buffer, false, out var bytesUsed, out var charsUsed, out completed);
|
||||
reader.Advance(bytesUsed);
|
||||
foreach (ref readonly char c in buffer[..charsUsed])
|
||||
{
|
||||
if (awaitLineFeed || c == '\n')
|
||||
{
|
||||
newLinesEncountered++;
|
||||
awaitLineFeed = false;
|
||||
}
|
||||
else if (c == '\r')
|
||||
{
|
||||
awaitLineFeed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLeaseChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (_eventChannel?.Writer is not { } writer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2012
|
||||
var task = writer.WriteAsync(e, CancellationToken.None);
|
||||
#pragma warning restore
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void OnLeaseError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
_eventChannel?.Writer.Complete(e.GetException());
|
||||
}
|
||||
}
|
||||
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public static class KeaFactoryServices
|
||||
{
|
||||
public static IServiceCollection AddKeaFactory(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IKeaFactory, KeaFactory>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private class KeaFactory(IServiceProvider services) : IKeaFactory
|
||||
{
|
||||
private ObjectFactory<KeaDhcp4LeaseHandler>? _cachedCreateHandler4;
|
||||
private ObjectFactory<KeaDhcp6LeaseHandler>? _cachedCreateHandler6;
|
||||
private ObjectFactory<KeaDhcpLeaseWatcher>? _cachedCreateWatcher;
|
||||
|
||||
KeaDhcp4LeaseHandler IKeaFactory.CreateHandler4()
|
||||
{
|
||||
_cachedCreateHandler4 ??= ActivatorUtilities.CreateFactory<KeaDhcp4LeaseHandler>([]);
|
||||
return _cachedCreateHandler4(services, null);
|
||||
}
|
||||
|
||||
KeaDhcp6LeaseHandler IKeaFactory.CreateHandler6()
|
||||
{
|
||||
_cachedCreateHandler6 ??= ActivatorUtilities.CreateFactory<KeaDhcp6LeaseHandler>([]);
|
||||
return _cachedCreateHandler6(services, null);
|
||||
}
|
||||
|
||||
KeaDhcpLeaseWatcher IKeaFactory.CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options)
|
||||
{
|
||||
_cachedCreateWatcher ??= ActivatorUtilities.CreateFactory<KeaDhcpLeaseWatcher>([typeof(IKeaDhcpLeaseHandler), typeof(KeaDhcpServerOptions)]);
|
||||
return _cachedCreateWatcher(services, [handler, options]);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Integrations/Kea/KeaService.cs
Normal file
54
Integrations/Kea/KeaService.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System.Collections.Immutable;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaService : IHostedService
|
||||
{
|
||||
private readonly ImmutableArray<IHostedService> _services;
|
||||
|
||||
public KeaService(KeaDhcpOptions options, IKeaFactory factory)
|
||||
{
|
||||
var services = ImmutableArray.CreateBuilder<IHostedService>();
|
||||
|
||||
if (options.Dhcp4 is { } dhcp4Options)
|
||||
{
|
||||
services.Add(factory.CreateWatcher(factory.CreateHandler4(), dhcp4Options));
|
||||
}
|
||||
|
||||
if (options.Dhcp6 is { } dhcp6Options)
|
||||
{
|
||||
services.Add(factory.CreateWatcher(factory.CreateHandler6(), dhcp6Options));
|
||||
}
|
||||
|
||||
_services = services.DrainToImmutable();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var waitTask = Task.WhenAll(tasks);
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
using var registration = cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource);
|
||||
await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
9
Integrations/Kea/LeaseType.cs
Normal file
9
Integrations/Kea/LeaseType.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public enum LeaseType : byte
|
||||
{
|
||||
NonTempraryIPv6 = 0,
|
||||
TemporaryIPv6 = 1,
|
||||
IPv6Prefix = 2,
|
||||
IPv4 = 3
|
||||
}
|
||||
12
Integrations/PowerDns/Methods.cs
Normal file
12
Integrations/PowerDns/Methods.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public interface IMethod;
|
||||
|
||||
public record MethodBase(string Method);
|
||||
|
||||
public record Method<TParam>(string Method, TParam Parameters) : MethodBase(Method)
|
||||
where TParam : MethodParameters;
|
||||
|
||||
public record InitializeMethod(InitializeParameters Parameters) : Method<InitializeParameters>("initialize", Parameters), IMethod;
|
||||
|
||||
public record LookupMethod(LookupParameters Parameters) : Method<LookupParameters>("lookup", Parameters), IMethod;
|
||||
55
Integrations/PowerDns/Parameters.cs
Normal file
55
Integrations/PowerDns/Parameters.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public record class Parameters;
|
||||
|
||||
[JsonDerivedType(typeof(InitializeParameters))]
|
||||
[JsonDerivedType(typeof(LookupParameters))]
|
||||
public record class MethodParameters : Parameters
|
||||
{
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement> AdditionalProperties { get; set; } = [];
|
||||
|
||||
protected override bool PrintMembers(StringBuilder builder)
|
||||
{
|
||||
if (base.PrintMembers(builder))
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
builder.Append("AdditionalProperties = [");
|
||||
bool append = false;
|
||||
foreach (var kv in AdditionalProperties)
|
||||
{
|
||||
if (append)
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
append = true;
|
||||
builder.Append(kv.Key);
|
||||
builder.Append(" = ");
|
||||
builder.Append(kv.Value);
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public record class InitializeParameters(
|
||||
[property: JsonPropertyName("command")] string Command,
|
||||
[property: JsonPropertyName("timeout")] int Timeout
|
||||
) : MethodParameters;
|
||||
|
||||
public record class LookupParameters(
|
||||
[property: JsonPropertyName("qtype")] string Qtype,
|
||||
[property: JsonPropertyName("qname")] string Qname,
|
||||
[property: JsonPropertyName("remote")] string Remote,
|
||||
[property: JsonPropertyName("local")] string Local,
|
||||
[property: JsonPropertyName("real-remote")] string RealRemote,
|
||||
[property: JsonPropertyName("zone-id")] int ZoneId
|
||||
) : MethodParameters;
|
||||
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
using CommunityToolkit.HighPerformance;
|
||||
using CommunityToolkit.HighPerformance.Buffers;
|
||||
|
||||
using DotNetDDI.Services.Dns;
|
||||
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public class PowerDnsHandler : ConnectionHandler
|
||||
{
|
||||
delegate MethodBase? HandlerConverter(in JsonElement element);
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, HandlerConverter> Converters;
|
||||
|
||||
private readonly ILogger<PowerDnsHandler> _logger;
|
||||
private readonly DnsRepository _repository;
|
||||
|
||||
static PowerDnsHandler()
|
||||
{
|
||||
Dictionary<string, HandlerConverter> converters = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["initialize"] = new(ToInitialize),
|
||||
["lookup"] = new(ToLookup)
|
||||
};
|
||||
|
||||
Converters = converters.AsReadOnly();
|
||||
|
||||
static InitializeMethod ToInitialize(in JsonElement element)
|
||||
{
|
||||
return new(element.Deserialize(PowerDnsSerializerContext.Default.InitializeParameters)!);
|
||||
}
|
||||
|
||||
static LookupMethod ToLookup(in JsonElement element)
|
||||
{
|
||||
return new(element.Deserialize(PowerDnsSerializerContext.Default.LookupParameters)!);
|
||||
}
|
||||
}
|
||||
|
||||
public PowerDnsHandler(DnsRepository repository, ILogger<PowerDnsHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync(ConnectionContext connection)
|
||||
{
|
||||
var input = connection.Transport.Input;
|
||||
JsonReaderState state = default;
|
||||
using ArrayPoolBufferWriter<byte> json = new();
|
||||
using ArrayPoolBufferWriter<byte> buffer = new();
|
||||
using var writer = connection.Transport.Output.AsStream();
|
||||
while (!connection.ConnectionClosed.IsCancellationRequested)
|
||||
{
|
||||
if (await ReadAsync(input, connection.ConnectionClosed) is not { IsCanceled: false } read)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var memory in read.Buffer)
|
||||
{
|
||||
buffer.Write(memory.Span);
|
||||
if (!ConsumeJson(buffer, json, ref state))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Reply reply = BoolReply.False;
|
||||
try
|
||||
{
|
||||
MethodBase method;
|
||||
try
|
||||
{
|
||||
using var jsonDocument = JsonDocument.Parse(json.WrittenMemory);
|
||||
var root = jsonDocument.RootElement;
|
||||
if (!root.TryGetProperty("method", out var methodElement))
|
||||
{
|
||||
_logger.LogWarning("Json Document missing required property method: {document}", jsonDocument);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Parse(methodElement, root.GetProperty("parameters")) is not { } methodLocal)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
method = methodLocal;
|
||||
}
|
||||
finally
|
||||
{
|
||||
json.Clear();
|
||||
state = default;
|
||||
}
|
||||
|
||||
reply = await Handle(method, connection.ConnectionClosed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(writer, reply, PowerDnsSerializerContext.Default.Reply, connection.ConnectionClosed)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
input.AdvanceTo(read.Buffer.End);
|
||||
}
|
||||
|
||||
static async ValueTask<ReadResult?> ReadAsync(PipeReader reader, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ConsumeJson(ArrayPoolBufferWriter<byte> inflight, ArrayPoolBufferWriter<byte> json, ref JsonReaderState state)
|
||||
{
|
||||
bool final = false;
|
||||
Utf8JsonReader reader = new(inflight.WrittenSpan, false, state);
|
||||
while (!final && reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == 0)
|
||||
{
|
||||
final = true;
|
||||
}
|
||||
}
|
||||
|
||||
state = reader.CurrentState;
|
||||
int consumed = (int)reader.BytesConsumed;
|
||||
if (consumed > 0)
|
||||
{
|
||||
json.Write(inflight.WrittenSpan[..consumed]);
|
||||
|
||||
Span<byte> buffer = default;
|
||||
var remaining = inflight.WrittenCount - consumed;
|
||||
if (remaining > 0)
|
||||
{
|
||||
buffer = inflight.GetSpan(remaining)[..remaining];
|
||||
inflight.WrittenSpan[consumed..].CopyTo(buffer);
|
||||
}
|
||||
|
||||
// clear only clears up until WrittenCount
|
||||
// thus data after write-head is safe
|
||||
inflight.Clear();
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
inflight.Write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return final;
|
||||
}
|
||||
|
||||
static MethodBase? Parse(in JsonElement element, in JsonElement parameters)
|
||||
{
|
||||
HandlerConverter? converter = default;
|
||||
return element.GetString() switch
|
||||
{
|
||||
null => null,
|
||||
{ } methodName when !Converters.TryGetValue(methodName, out converter) => new Method<MethodParameters>(methodName, parameters.Deserialize(PowerDnsSerializerContext.Default.MethodParameters)!),
|
||||
_ => converter(parameters)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask<Reply> Handle(MethodBase method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return method switch
|
||||
{
|
||||
InitializeMethod { Parameters: { } init } => HandleInitialize(init),
|
||||
LookupMethod { Parameters: { } lookup } => HandleLookup(lookup),
|
||||
|
||||
_ => LogUnhandled(_logger, method)
|
||||
};
|
||||
|
||||
static ValueTask<Reply> LogUnhandled(ILogger logger, MethodBase method)
|
||||
{
|
||||
logger.LogWarning("Unhandled {Method}", method);
|
||||
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask<Reply> HandleInitialize(InitializeParameters parameters)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("Handling {parameters}", parameters);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<Reply>(BoolReply.True);
|
||||
}
|
||||
|
||||
private ValueTask<Reply> HandleLookup(LookupParameters parameters)
|
||||
{
|
||||
AddressFamily? qtype = parameters.Qtype.ToUpperInvariant() switch
|
||||
{
|
||||
"ANY" => AddressFamily.Unknown,
|
||||
"A" => AddressFamily.InterNetwork,
|
||||
"AAAA" => AddressFamily.InterNetworkV6,
|
||||
_ => default(AddressFamily?)
|
||||
};
|
||||
|
||||
if (qtype is null)
|
||||
{
|
||||
_logger.LogWarning("Unhandled QType {QType}", parameters.Qtype);
|
||||
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||
}
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("Lookup {key} in {family}", parameters.Qname, parameters.Qtype);
|
||||
}
|
||||
|
||||
return FindByName(((AddressFamily)qtype, parameters.Qname.AsMemory()), _repository, _logger);
|
||||
|
||||
static async ValueTask<Reply> FindByName((AddressFamily Family, ReadOnlyMemory<char> Qname) query, DnsRepository repository, ILogger logger)
|
||||
{
|
||||
QueryResult[] records = [];
|
||||
|
||||
var qname = query.Qname.Trim().TrimEnd(".");
|
||||
if (qname.Span.IsWhiteSpace())
|
||||
{
|
||||
goto exitEmpty;
|
||||
}
|
||||
|
||||
var results = await repository.FindAsync(record =>
|
||||
{
|
||||
if ((record.RecordType & query.Family) != record.RecordType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return qname.Span.Equals(record.FQDN, StringComparison.OrdinalIgnoreCase);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
goto exitEmpty;
|
||||
}
|
||||
|
||||
records = new QueryResult[results.Count];
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
DnsRecord record = results[i];
|
||||
#pragma warning disable CS8509 // RecordType is by convention InterNetwork or InterNetworkV6
|
||||
records[i] = new(record.RecordType switch
|
||||
#pragma warning restore
|
||||
{
|
||||
AddressFamily.InterNetwork => "A",
|
||||
AddressFamily.InterNetworkV6 => "AAAA"
|
||||
}, record.FQDN, record.Address.ToString(), (int)record.Lifetime.TotalSeconds);
|
||||
}
|
||||
|
||||
exitEmpty:
|
||||
return new LookupReply(records);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
[JsonSerializable(typeof(Reply))]
|
||||
[JsonSerializable(typeof(MethodParameters))]
|
||||
[JsonSourceGenerationOptions(
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
UseStringEnumConverter = true,
|
||||
WriteIndented = false
|
||||
)]
|
||||
internal partial class PowerDnsSerializerContext : JsonSerializerContext;
|
||||
39
Integrations/PowerDns/Replies.cs
Normal file
39
Integrations/PowerDns/Replies.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
[JsonDerivedType(typeof(BoolReply))]
|
||||
[JsonDerivedType(typeof(LookupReply))]
|
||||
public abstract class Reply;
|
||||
|
||||
public abstract class Reply<T>(T result) : Reply
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public T Result => result;
|
||||
}
|
||||
|
||||
public class BoolReply(bool result) : Reply<bool>(result)
|
||||
{
|
||||
public static BoolReply False { get; } = new BoolReply(false);
|
||||
|
||||
public static BoolReply True { get; } = new BoolReply(true);
|
||||
}
|
||||
|
||||
public class LookupReply(QueryResult[] result) : Reply<QueryResult[]>(result);
|
||||
|
||||
public record QueryResult(
|
||||
[property: JsonPropertyName("qtype")] string QType,
|
||||
[property: JsonPropertyName("qname")] string QName,
|
||||
[property: JsonPropertyName("content")] string Content,
|
||||
[property: JsonPropertyName("ttl")] int TTL
|
||||
)
|
||||
{
|
||||
[JsonPropertyName("auth")]
|
||||
public bool? Auth { get; init; } = default;
|
||||
|
||||
[JsonPropertyName("domain_id")]
|
||||
public int? DomainId { get; init; } = default;
|
||||
|
||||
[JsonPropertyName("scopeMask")]
|
||||
public int? ScopeMask { get; init; } = default;
|
||||
}
|
||||
6
Options/DhcpOptions.cs
Normal file
6
Options/DhcpOptions.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace DotNetDDI.Options;
|
||||
|
||||
public class DhcpOptions
|
||||
{
|
||||
public KeaDhcpOptions? Kea { get; set; }
|
||||
}
|
||||
10
Options/KeaDhcpOptions.cs
Normal file
10
Options/KeaDhcpOptions.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace DotNetDDI.Options;
|
||||
|
||||
public class KeaDhcpOptions
|
||||
{
|
||||
public KeaDhcpServerOptions? Dhcp4 { get; set; }
|
||||
|
||||
public KeaDhcpServerOptions? Dhcp6 { get; set; }
|
||||
}
|
||||
|
||||
public record class KeaDhcpServerOptions(string Leases);
|
||||
10
Options/PowerDnsOptions.cs
Normal file
10
Options/PowerDnsOptions.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace DotNetDDI.Options;
|
||||
|
||||
public class PowerDnsOptions
|
||||
{
|
||||
public PowerDnsListenerOptions Listener { get; init; } = default!;
|
||||
|
||||
public bool UniqueHostnames { get; init; } = true;
|
||||
}
|
||||
|
||||
public record class PowerDnsListenerOptions(string Socket);
|
||||
|
|
@ -30,8 +30,7 @@ app.UseRouting();
|
|||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorPages()
|
||||
.WithStaticAssets();
|
||||
app.UseStaticFiles();
|
||||
app.MapRazorPages();
|
||||
|
||||
app.Run();
|
||||
|
|
|
|||
36
Services/Dhcp/DhcpLeaseQueue.cs
Normal file
36
Services/Dhcp/DhcpLeaseQueue.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace DotNetDDI.Services.Dhcp;
|
||||
|
||||
public class DhcpLeaseQueue
|
||||
{
|
||||
private readonly Channel<DhcpLeaseChange> _pipe;
|
||||
private readonly ChannelReader<DhcpLeaseChange> _reader;
|
||||
private readonly ChannelWriter<DhcpLeaseChange> _writer;
|
||||
|
||||
public ref readonly ChannelReader<DhcpLeaseChange> Reader => ref _reader;
|
||||
|
||||
public DhcpLeaseQueue()
|
||||
{
|
||||
_pipe = Channel.CreateUnbounded<DhcpLeaseChange>();
|
||||
_reader = _pipe.Reader;
|
||||
_writer = _pipe.Writer;
|
||||
}
|
||||
|
||||
public ValueTask Write(DhcpLeaseChange change, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _writer.WriteAsync(change, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct DhcpLeaseChange(IPAddress Address, string FQDN, DhcpLeaseIdentifier Identifier, TimeSpan Lifetime)
|
||||
{
|
||||
public AddressFamily LeaseType { get; } = Address.AddressFamily;
|
||||
}
|
||||
|
||||
public record DhcpLeaseIdentifier;
|
||||
public record DhcpLeaseClientIdentifier(string ClientId) : DhcpLeaseIdentifier;
|
||||
public record DhcpLeaseHWAddrIdentifier(PhysicalAddress HWAddr) : DhcpLeaseIdentifier;
|
||||
44
Services/Dhcp/DhcpQueueWorker.cs
Normal file
44
Services/Dhcp/DhcpQueueWorker.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Threading.Channels;
|
||||
|
||||
using DotNetDDI.Services.Dns;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DotNetDDI.Services.Dhcp;
|
||||
|
||||
public class DhcpQueueWorker : BackgroundService
|
||||
{
|
||||
private readonly ChannelReader<DhcpLeaseChange> _channelReader;
|
||||
private readonly DnsRepository _repository;
|
||||
|
||||
public DhcpQueueWorker(DhcpLeaseQueue queue, DnsRepository repository)
|
||||
{
|
||||
_channelReader = queue.Reader;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (await _channelReader.WaitToReadAsync(stoppingToken).ConfigureAwait(false))
|
||||
{
|
||||
while (_channelReader.TryRead(out var lease))
|
||||
{
|
||||
DnsRecordIdentifier identifier = lease.Identifier switch
|
||||
{
|
||||
DhcpLeaseClientIdentifier clientId => new DnsRecordClientIdentifier(clientId.ClientId),
|
||||
DhcpLeaseHWAddrIdentifier hwAddr => new DnsRecordHWAddrIdentifier(hwAddr.HWAddr),
|
||||
_ => throw new ArgumentException(nameof(lease.Identifier))
|
||||
};
|
||||
|
||||
TimeSpan lifetime = lease.Lifetime.TotalSeconds switch
|
||||
{
|
||||
<= 1800 => TimeSpan.FromSeconds(600),
|
||||
>= 10800 => TimeSpan.FromSeconds(3600),
|
||||
{ } seconds => TimeSpan.FromSeconds(seconds / 3)
|
||||
};
|
||||
|
||||
await _repository.Record(new DnsRecord(lease.Address, lease.FQDN, identifier, lifetime), stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Services/Dhcp/DhcpWatcher.cs
Normal file
50
Services/Dhcp/DhcpWatcher.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Collections.Immutable;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DotNetDDI.Services.Dhcp;
|
||||
|
||||
public class DhcpWatcher : IHostedService
|
||||
{
|
||||
private readonly ImmutableArray<IHostedService> _services;
|
||||
|
||||
public DhcpWatcher(IOptions<DhcpOptions> options, IDhcpWatcherFactory factory)
|
||||
{
|
||||
var dhcpOptions = options.Value;
|
||||
var services = ImmutableArray.CreateBuilder<IHostedService>();
|
||||
if (dhcpOptions.Kea is { } keaOptions)
|
||||
{
|
||||
services.Add(factory.KeaService(keaOptions));
|
||||
}
|
||||
|
||||
_services = services.DrainToImmutable();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var waitTask = Task.WhenAll(tasks);
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
using var registration = cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource);
|
||||
await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
26
Services/Dhcp/DhcpWatcherFactoryServices.cs
Normal file
26
Services/Dhcp/DhcpWatcherFactoryServices.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using DotNetDDI.Integrations.Kea;
|
||||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DotNetDDI.Services.Dhcp;
|
||||
|
||||
public static class DhcpWatcherFactoryServices
|
||||
{
|
||||
public static IServiceCollection AddDhcpWatcherFactory(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IDhcpWatcherFactory, DhcpWatcherFactory>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private class DhcpWatcherFactory(IServiceProvider services) : IDhcpWatcherFactory
|
||||
{
|
||||
private ObjectFactory<KeaService>? _cachedKeaService;
|
||||
|
||||
KeaService IDhcpWatcherFactory.KeaService(KeaDhcpOptions options)
|
||||
{
|
||||
_cachedKeaService ??= ActivatorUtilities.CreateFactory<KeaService>([typeof(KeaDhcpOptions)]);
|
||||
return _cachedKeaService(services, [options]);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Services/Dhcp/IDhcpWatcherFactory.cs
Normal file
9
Services/Dhcp/IDhcpWatcherFactory.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using DotNetDDI.Integrations.Kea;
|
||||
using DotNetDDI.Options;
|
||||
|
||||
namespace DotNetDDI.Services.Dhcp;
|
||||
|
||||
public interface IDhcpWatcherFactory
|
||||
{
|
||||
KeaService KeaService(KeaDhcpOptions options);
|
||||
}
|
||||
157
Services/Dns/DnsRepository.cs
Normal file
157
Services/Dns/DnsRepository.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Timeout = System.Threading.Timeout;
|
||||
|
||||
namespace DotNetDDI.Services.Dns;
|
||||
|
||||
public class DnsRepository
|
||||
{
|
||||
private static ReadOnlySpan<int> Lifetimes => [600, 3600];
|
||||
|
||||
private readonly PowerDnsOptions _options;
|
||||
private readonly ReaderWriterLockSlim _recordLock = new();
|
||||
private readonly List<DnsRecord> _records = [];
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
|
||||
public DnsRepository(IOptions<PowerDnsOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public List<DnsRecord> Find(Predicate<DnsRecord> query)
|
||||
{
|
||||
bool enteredLock = false;
|
||||
try
|
||||
{
|
||||
enteredLock = _recordLock.TryEnterReadLock(Timeout.Infinite);
|
||||
return _records.FindAll(query);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (enteredLock)
|
||||
{
|
||||
_recordLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<DnsRecord>> FindAsync(Predicate<DnsRecord> query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.Factory.StartNew(state => Find((Predicate<DnsRecord>)state!), query,
|
||||
cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public async ValueTask Record(DnsRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool entered = false;
|
||||
try
|
||||
{
|
||||
entered = await _syncLock.WaitAsync(Timeout.Infinite, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||
RecordContinuation(record);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (entered)
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
void RecordContinuation(DnsRecord record)
|
||||
{
|
||||
var search = Matches(record);
|
||||
bool entered = false;
|
||||
try
|
||||
{
|
||||
entered = _recordLock.TryEnterWriteLock(Timeout.Infinite);
|
||||
|
||||
if (search.First is { } node)
|
||||
{
|
||||
search.RemoveFirst();
|
||||
_records[node.Value] = record;
|
||||
}
|
||||
else
|
||||
{
|
||||
_records.Add(record);
|
||||
}
|
||||
|
||||
while (search.Last is { } replace)
|
||||
{
|
||||
search.RemoveLast();
|
||||
var last = _records.Count - 1;
|
||||
if (replace.Value < last)
|
||||
{
|
||||
_records[replace.Value] = _records[last];
|
||||
}
|
||||
|
||||
_records.RemoveAt(last);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (entered)
|
||||
{
|
||||
_recordLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LinkedList<int> Matches(DnsRecord query)
|
||||
{
|
||||
LinkedList<int> list = [];
|
||||
|
||||
for (int i = 0; i < _records.Count; i++)
|
||||
{
|
||||
var record = _records[i];
|
||||
if (record.RecordType != query.RecordType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ((record.Identifier, query.Identifier))
|
||||
{
|
||||
case (
|
||||
DnsRecordClientIdentifier { ClientId: { } recordClientId },
|
||||
DnsRecordClientIdentifier { ClientId: { } queryClientId }
|
||||
) when StringComparer.InvariantCultureIgnoreCase.Equals(recordClientId, queryClientId):
|
||||
|
||||
case (
|
||||
DnsRecordHWAddrIdentifier { HWAddr: { } recordHWAddr },
|
||||
DnsRecordHWAddrIdentifier { HWAddr: { } queryHWAddr }
|
||||
) when EqualityComparer<PhysicalAddress>.Default.Equals(recordHWAddr, queryHWAddr):
|
||||
|
||||
list.AddLast(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EqualityComparer<IPAddress>.Default.Equals(record.Address, query.Address))
|
||||
{
|
||||
list.AddLast(i);
|
||||
}
|
||||
else if (_options.UniqueHostnames && StringComparer.OrdinalIgnoreCase.Equals(record.FQDN, query.FQDN))
|
||||
{
|
||||
list.AddLast(i);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Remove duplication
|
||||
public record DnsRecordIdentifier;
|
||||
public record DnsRecordClientIdentifier(string ClientId) : DnsRecordIdentifier;
|
||||
public record DnsRecordHWAddrIdentifier(PhysicalAddress HWAddr) : DnsRecordIdentifier;
|
||||
// /TODO
|
||||
|
||||
public record DnsRecord(IPAddress Address, string FQDN, DnsRecordIdentifier Identifier, TimeSpan Lifetime)
|
||||
{
|
||||
public AddressFamily RecordType { get; } = Address.AddressFamily;
|
||||
}
|
||||
113
System/IO/Paths.cs
Normal file
113
System/IO/Paths.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#if !(LINUX || MACOS || WINDOWS)
|
||||
#define TARGET_ANY
|
||||
#endif
|
||||
|
||||
#if TARGET_ANY || LINUX
|
||||
#define TARGET_LINUX
|
||||
#endif
|
||||
#if TARGET_ANY || MACOS
|
||||
#define TARGET_MACOS
|
||||
#endif
|
||||
#if TARGET_ANY || WINDOWS
|
||||
#define TARGET_WINDOWS
|
||||
#endif
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
using CommunityToolkit.HighPerformance.Buffers;
|
||||
|
||||
namespace System.IO;
|
||||
|
||||
public static class PathEx
|
||||
{
|
||||
public static string ExpandPath(string path)
|
||||
{
|
||||
#if TARGET_ANY
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
#endif
|
||||
#if TARGET_LINUX || TARGET_MACOS
|
||||
return ExpandPathUnixImpl(path);
|
||||
#endif
|
||||
#if TARGET_ANY
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
#endif
|
||||
#if TARGET_WINDOWS
|
||||
return ExpandPathWindowsImpl(path);
|
||||
#endif
|
||||
#if TARGET_ANY
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if TARGET_LINUX || TARGET_MACOS
|
||||
private static string ExpandPathUnixImpl(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var reader = new SequenceReader<char>(new(path.AsMemory()));
|
||||
if (!reader.TryReadTo(out ReadOnlySpan<char> read, '$'))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
using ArrayPoolBufferWriter<char> result = new();
|
||||
while (true)
|
||||
{
|
||||
result.Write(read);
|
||||
|
||||
int skip = 0;
|
||||
if (reader.UnreadSpan[0] == '{' && reader.UnreadSpan.IndexOf('}') is not -1 and int s)
|
||||
{
|
||||
read = reader.UnreadSpan[1..s];
|
||||
skip = s + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var propertyReader = new SequenceReader<char>(reader.UnreadSequence);
|
||||
while (!propertyReader.End && propertyReader.UnreadSpan[0] is char c && (c == '_' || char.IsAsciiLetterOrDigit(c)))
|
||||
{
|
||||
propertyReader.Advance(1);
|
||||
}
|
||||
|
||||
read = reader.UnreadSpan[..(int)propertyReader.Consumed];
|
||||
skip = read.Length;
|
||||
}
|
||||
|
||||
if (skip != 0 && Environment.GetEnvironmentVariable(read.ToString()) is { } env)
|
||||
{
|
||||
result.Write(env);
|
||||
reader.Advance(skip);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Write(['$']);
|
||||
}
|
||||
|
||||
if (!reader.TryReadTo(out read, '$'))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.Write(reader.UnreadSpan);
|
||||
return result.WrittenSpan.ToString();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_WINDOWS
|
||||
private static string ExpandPathWindowsImpl(string path)
|
||||
{
|
||||
return Environment.ExpandEnvironmentVariables(path);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
67
System/Threading/Tasks/AsyncWaitHandle.cs
Normal file
67
System/Threading/Tasks/AsyncWaitHandle.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
namespace System.Threading.Tasks;
|
||||
|
||||
public static class AsyncWaitHandle
|
||||
{
|
||||
public static Task WaitOneAsync(this WaitHandle waitHandle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
WaitOneAsyncState state = new(waitHandle, cancellationToken);
|
||||
return state.WaitOneAsync();
|
||||
}
|
||||
|
||||
private class WaitOneAsyncState : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenRegistration _cancellationTokenRegistration;
|
||||
private readonly RegisteredWaitHandle _registeredWaitHandle;
|
||||
private readonly TaskCompletionSource _tcs;
|
||||
private bool _disposed;
|
||||
|
||||
public WaitOneAsyncState(WaitHandle waitHandle, CancellationToken cancellationToken)
|
||||
{
|
||||
_tcs = new();
|
||||
_cancellationTokenRegistration = cancellationToken.Register((state, token) => ((WaitOneAsyncState)state!).Canceled(token), this);
|
||||
_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitHandle, (state, timeout) => ((WaitOneAsyncState)state!).Signaled(timeout), this, Timeout.Infinite, true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_registeredWaitHandle.Unregister(default);
|
||||
_cancellationTokenRegistration.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public Task WaitOneAsync()
|
||||
{
|
||||
const TaskContinuationOptions options = TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously;
|
||||
return _tcs.Task.ContinueWith((upstream, state) => ((WaitOneAsyncState)state!).Continuation(upstream), this, options).Unwrap();
|
||||
}
|
||||
|
||||
private void Canceled(CancellationToken token)
|
||||
{
|
||||
_tcs.SetCanceled(token);
|
||||
}
|
||||
|
||||
private Task Continuation(Task task)
|
||||
{
|
||||
Dispose();
|
||||
return task;
|
||||
}
|
||||
|
||||
private void Signaled(bool timeout)
|
||||
{
|
||||
if (timeout)
|
||||
{
|
||||
Canceled(CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tcs.SetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,25 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-netddi-40b2ab4f-2391-4151-8f6d-f213efd89115</UserSecretsId>
|
||||
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||
<RootNamespace>DotNetDDI</RootNamespace>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
||||
<PackageReference Include="DotNext" Version="5.18.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.12" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.1" />
|
||||
<PackageReference Include="Sep" Version="0.8.0" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue