Repository navigation
Architecture
The important structural fact: all real logic lives in NetworkScanner.Core — a plain net8.0 library with no UI dependency. The UIs are thin adapters over it.
| Project | Description |
|---|---|
NetworkScanner.Core |
Scan engine, settings/CSV/ini persistence, IP-range math, OS-specific helpers. Shared by both UIs and exercised by the tests. |
NetworkScanner |
Windows-only WPF UI (net8.0-windows..., UseWPF=true). |
NetworkScanner.Avalonia |
Cross-platform Avalonia UI (net8.0). |
NetworkScanner.Cli |
Headless cross-platform console front-end (netscan) over Core. |
NetworkScanner.Tests |
xUnit tests; references Core only. |
OUIConvertor |
Standalone WPF utility that regenerates ouiinfo.ini; not part of the app runtime. |
SetupNetworkScanner |
Legacy WPF .vdproj installer; not built by dotnet. |
The WPF project depends on the Windows Desktop runtime, so anything referencing it only runs on Windows. Extracting UI-independent logic into NetworkScanner.Core means unit tests run on any OS, both UIs share one ScanEngine, and platform-specific pieces branch internally so neither UI needs its own code.
ScanEngine (in Core) is the center. A UI creates one, passing:
- the observable
IPInfoListit binds its grid to, - an
OUIInfo(loaded vendor table), - an
IScanConfigProvider— the decoupling seam. The engine never reads UI controls; it calls this interface for port lists, FTP creds, scan ranges, system name, etc. In both apps the main window implements it (WPFMainNetworkScanner, AvaloniaMainWindow), delegating to the settings views.
The engine reports everything through events; UI handlers marshal them to the UI thread:
Message, ProgressMaxChanged, ProgressChanged, ResultsSummaryChanged, ItemsRefreshNeeded, ScanStarted, ScanFinished.
Start* methods (StartRefreshAllRange, StartSchedulingScan, StartCheck*PortList) are guarded by TryBeginScan (lock + CancellationTokenSource) so only one scan runs at a time, and wrapped in RunTracked which raises ScanStarted/ScanFinished. ScanningStop() cancels the token (the "취소" / Stop button).
DoScanAllRange runs IPs through Parallel.ForEachAsync (up to ScanParallelism = 64) using async ping/port-check. Because results arrive on many worker threads:
-
_itemswrites are serialized under_itemsLock, exposed asScanEngine.ItemsSyncRoot. -
ItemsRefreshNeededis fired outside that lock — firing it inside while a UI handler needs the same lock would deadlock. - Per-IP config (prohibited ports, port-checking flag) is prefetched once before the loop, not read from
Configper IP (that would marshal to the UI thread 64× concurrently). - WPF calls
BindingOperations.EnableCollectionSynchronization(_IPInfoList, engine.ItemsSyncRoot); Avalonia rebuilds its grid from a snapshot copied under that lock. Both UIs use non-blocking dispatch (BeginInvoke/Post) and coalesce rapid refresh requests.
IPInfo does not implement INotifyPropertyChanged — property edits to existing rows are reflected only when the UI is told to refresh (hence ItemsRefreshNeeded).
OS branches live behind small helpers so the rest of Core stays portable:
-
ArpResolver— WindowsSendARPP/Invoke vs. Unixarpcommand parsing (Unix needs a prior ping to populate the ARP cache; the scan always pings first). -
CredentialProtector— DPAPI (Windows) / Keychainsecurity(macOS) / libsecretsecret-tool(Linux), falling back to Base64 obfuscation when unavailable. -
PingTester— logs an ICMP-permission hint once when raw-socket access is denied. -
IPRangeUtil— treats IPv4 asuint32so ranges spanning subnet boundaries count/iterate correctly. -
LocalNetworkInfo— detects the active interface's subnet;AppSettingsStore.LoadScanRangesseeds it as the default range when none are configured.
A UI-facing change almost always needs mirroring in both WPF (NetworkScanner/UC*.xaml[.cs], MainNetworkScanner.xaml[.cs]) and Avalonia (NetworkScanner.Avalonia/Views/*.axaml[.cs], MainWindow.axaml[.cs]). Prefer pushing shared logic into Core so it's tested once and both UIs stay thin.
See Building from Source for build/test commands.