1
0
Fork 0

Migrate existing code

This commit is contained in:
Jöran Malek 2025-01-29 23:53:57 +01:00
parent 384ff4a6f3
commit 9b49f880a2
30 changed files with 1789 additions and 5 deletions

View file

@ -8,6 +8,13 @@
"libman" "libman"
], ],
"rollForward": false "rollForward": false
},
"dotnet-outdated-tool": {
"version": "4.6.4",
"commands": [
"dotnet-outdated"
],
"rollForward": false
} }
} }
} }

View 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);
}

View 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);
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View 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();
}
}
}

View 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());
}
}

View 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]);
}
}
}

View 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);
}
}

View file

@ -0,0 +1,9 @@
namespace DotNetDDI.Integrations.Kea;
public enum LeaseType : byte
{
NonTempraryIPv6 = 0,
TemporaryIPv6 = 1,
IPv6Prefix = 2,
IPv4 = 3
}

View 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;

View 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;

View 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);
}
}
}

View 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;

View 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
View file

@ -0,0 +1,6 @@
namespace DotNetDDI.Options;
public class DhcpOptions
{
public KeaDhcpOptions? Kea { get; set; }
}

10
Options/KeaDhcpOptions.cs Normal file
View 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);

View 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);

View file

@ -30,8 +30,7 @@ app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
app.MapStaticAssets(); app.UseStaticFiles();
app.MapRazorPages() app.MapRazorPages();
.WithStaticAssets();
app.Run(); app.Run();

View 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;

View 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);
}
}
}
}

View 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);
}
}

View 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]);
}
}
}

View file

@ -0,0 +1,9 @@
using DotNetDDI.Integrations.Kea;
using DotNetDDI.Options;
namespace DotNetDDI.Services.Dhcp;
public interface IDhcpWatcherFactory
{
KeaService KeaService(KeaDhcpOptions options);
}

View 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
View 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
}

View 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();
}
}
}
}

View file

@ -6,14 +6,25 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-netddi-40b2ab4f-2391-4151-8f6d-f213efd89115</UserSecretsId> <UserSecretsId>aspnet-netddi-40b2ab4f-2391-4151-8f6d-f213efd89115</UserSecretsId>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile> <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
<RootNamespace>DotNetDDI</RootNamespace>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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.Diagnostics.EntityFrameworkCore" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Identity.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.AspNetCore.Identity.UI" Version="8.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.12" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.12" /> <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> </ItemGroup>
</Project> </Project>