Skip to content

Architecture

knoxxr edited this page Jul 14, 2026 · 1 revision

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.

Projects

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.

Why Core is separate

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.

How the UI talks to the engine

ScanEngine (in Core) is the center. A UI creates one, passing:

  • the observable IPInfoList it 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 (WPF MainNetworkScanner, Avalonia MainWindow), 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).

Concurrency model

DoScanAllRange runs IPs through Parallel.ForEachAsync (up to ScanParallelism = 64) using async ping/port-check. Because results arrive on many worker threads:

  • _items writes are serialized under _itemsLock, exposed as ScanEngine.ItemsSyncRoot.
  • ItemsRefreshNeeded is 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 Config per 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).

Cross-platform seams (Core)

OS branches live behind small helpers so the rest of Core stays portable:

  • ArpResolver — Windows SendARP P/Invoke vs. Unix arp command parsing (Unix needs a prior ping to populate the ARP cache; the scan always pings first).
  • CredentialProtector — DPAPI (Windows) / Keychain security (macOS) / libsecret secret-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 as uint32 so ranges spanning subnet boundaries count/iterate correctly.
  • LocalNetworkInfo — detects the active interface's subnet; AppSettingsStore.LoadScanRanges seeds it as the default range when none are configured.

Making changes across both UIs

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.

Clone this wiki locally