Migrate existing code
This commit is contained in:
parent
384ff4a6f3
commit
9b49f880a2
30 changed files with 1789 additions and 5 deletions
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue