migration
This commit is contained in:
parent
561881c9aa
commit
f133c740e1
55 changed files with 2928 additions and 20 deletions
295
src/Discovery.Core/Services/Scanner.cs
Normal file
295
src/Discovery.Core/Services/Scanner.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
using Discovery.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Discovery.Services;
|
||||
|
||||
public partial class Scanner<T> where T : Target
|
||||
{
|
||||
public ScannerState State => _state;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private readonly ScannerState _state = new();
|
||||
private readonly static Dictionary<string, string>? _vendors = BuildVendorDictionary();
|
||||
|
||||
public async Task<ScannerState> ScanAsync(
|
||||
SynchronizedCollection<T> targets,
|
||||
ScannerOptions options,
|
||||
ScannerEvents events,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// cancel if already running
|
||||
if (_state.Operation == ScanOperation.Scanning && _cts != null)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
return _state;
|
||||
}
|
||||
|
||||
// renew state
|
||||
_state.Operation = ScanOperation.Scanning;
|
||||
_state.Progress = 0;
|
||||
|
||||
// set cts
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var orderedTargets = targets.InternalList.OrderBy(p => p.Index);
|
||||
var start = Stopwatch.GetTimestamp();
|
||||
|
||||
try
|
||||
{
|
||||
var tasks = new List<Task>(targets.InternalList.Count);
|
||||
var con = new SemaphoreSlim(options.Concurrency);
|
||||
|
||||
var pingOptions = new PingOptions
|
||||
{
|
||||
DontFragment = options.DontFragment,
|
||||
Ttl = options.Ttl
|
||||
};
|
||||
|
||||
foreach (var target in targets.InternalList)
|
||||
{
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await con.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
target.Ping = await PingAsync(target.IPAddress, options.PingTimeout, null, pingOptions, cancellationToken).ConfigureAwait(false);
|
||||
target.Mac = await ArpAsync(target.IPAddress, options.ArpTimeout, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (target.Mac != null)
|
||||
{
|
||||
target.Arp = true;
|
||||
target.Vendor = ResolveVendor(target.Mac!);
|
||||
}
|
||||
|
||||
// local resolve fix
|
||||
string? sAddress = null;
|
||||
if (TryGetPrimaryInterface(out var pInterface, out var pAddress))
|
||||
sAddress = pAddress?.ToString();
|
||||
|
||||
if (target.Address == sAddress)
|
||||
{
|
||||
target.Hostname = Dns.GetHostName();
|
||||
}
|
||||
else if (options.Nameservers == null)
|
||||
{
|
||||
target.Hostname = "N/A";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target.Online || (!target.Online && options.ResolveAll))
|
||||
{
|
||||
var result = await ResolveHostnameAsync(target.IPAddress, options.Nameservers, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
target.Hostname = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally
|
||||
{
|
||||
_state.Progress += (100 / (double)targets.InternalList.Count);
|
||||
|
||||
if (events.OnStateUpdate != null)
|
||||
await events.OnStateUpdate(_state);
|
||||
|
||||
con.Release();
|
||||
}
|
||||
}, cancellationToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
_state.Operation = ScanOperation.Idle;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_state.Operation = ScanOperation.Aborted;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_state.Progress = 100;
|
||||
_state.Elapsed = Stopwatch.GetElapsedTime(start);
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
return _state;
|
||||
}
|
||||
|
||||
public async Task CancelAsync()
|
||||
{
|
||||
if (_cts == null)
|
||||
return;
|
||||
|
||||
await _cts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
_state.Operation = ScanOperation.Aborted;
|
||||
}
|
||||
|
||||
public bool TryGetPrimaryInterface(out NetworkInterface? pInterface, out IPAddress? pAddress)
|
||||
{
|
||||
pInterface = null;
|
||||
pAddress = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
|
||||
socket.Connect("8.8.8.8", 65530);
|
||||
|
||||
var ep = socket.LocalEndPoint as IPEndPoint;
|
||||
|
||||
if (ep?.Address == null)
|
||||
return false;
|
||||
|
||||
pAddress = ep.Address;
|
||||
|
||||
pInterface = NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.FirstOrDefault(p => p
|
||||
.GetIPProperties().UnicastAddresses.Any(x => x.Address.ToString() == ep.Address.ToString()));
|
||||
|
||||
if (pInterface == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkInfo? GetPrimaryNetwork()
|
||||
{
|
||||
if (!TryGetPrimaryInterface(out var pInterface, out var pAddress))
|
||||
return null;
|
||||
|
||||
var pUnicast = pInterface!.GetIPProperties().UnicastAddresses.Single(p => p.Address.ToString() == pAddress!.ToString());
|
||||
|
||||
if (!IPNetwork2.TryParse(pAddress!, pUnicast!.IPv4Mask, out var net))
|
||||
return null;
|
||||
|
||||
var nameservers = pInterface!.GetIPProperties().DnsAddresses;
|
||||
|
||||
return new NetworkInfo(pAddress!, net.Network, net.Cidr, [.. nameservers]);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVendorDictionary()
|
||||
{
|
||||
var dictionary = new Dictionary<string, string>();
|
||||
|
||||
using var fileStream = new FileStream("vendors.json", FileMode.Open, FileAccess.Read);
|
||||
|
||||
var vendors = JsonSerializer.Deserialize(
|
||||
fileStream,
|
||||
VendorsJsonContext.Default.VendorArray
|
||||
) ?? throw new InvalidOperationException("vendors.json");
|
||||
|
||||
foreach (var item in vendors)
|
||||
{
|
||||
if (item is null)
|
||||
continue;
|
||||
|
||||
if (item.MacPrefix == null || item.VendorName == null)
|
||||
continue;
|
||||
|
||||
dictionary.Add(item.MacPrefix, item.VendorName);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static async Task<PingReply?> PingAsync(IPAddress address, TimeSpan timeout, byte[]? buffer, PingOptions? options, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
using var ping = new Ping();
|
||||
var reply = await ping.SendPingAsync(address, timeout, buffer, options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
ping.SendAsyncCancel();
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
private static async Task<string?> ArpAsync(IPAddress address, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return null;
|
||||
if (address.AddressFamily != AddressFamily.InterNetwork) return null; // IPv4 only
|
||||
|
||||
// 1) Try ARP cache (GetIpNetTable)
|
||||
if (WinArpInterop.TryGetFromArpCache(address, out var mac))
|
||||
return mac;
|
||||
|
||||
// 2) Active probe on a dedicated thread; still honor the timeout
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
var probeTask = Task.Factory.StartNew(
|
||||
() => WinArpInterop.ProbeWithSendArp(address),
|
||||
linked.Token,
|
||||
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
|
||||
TaskScheduler.Default);
|
||||
|
||||
var completed = await Task.WhenAny(probeTask, Task.Delay(timeout, linked.Token)).ConfigureAwait(false);
|
||||
if (completed != probeTask) return null;
|
||||
|
||||
linked.Cancel(); // cancel the delay if still pending
|
||||
return await probeTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<string?> ResolveHostnameAsync(IPAddress address, List<IPAddress> nameservers, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
string? hostname = null;
|
||||
|
||||
for (int i = 0; i < nameservers.Count; i++)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "nslookup",
|
||||
Arguments = $"{address} {nameservers[i]}",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput
|
||||
.ReadToEndAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var match = NameRegex().Match(output);
|
||||
if (match.Success)
|
||||
hostname = match.Groups["Name"].Value;
|
||||
|
||||
if (hostname != null)
|
||||
break;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
private static string? ResolveVendor(string mac)
|
||||
{
|
||||
if (!_vendors!.TryGetValue(mac[..8].ToUpperInvariant(), out string? vendor))
|
||||
return null;
|
||||
|
||||
return vendor;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"Name:\s*(?<Name>[^\s]+)")]
|
||||
private static partial Regex NameRegex();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue