From 8d95a616a0b2bf37ed88ab273c5b40a0b7b416f6 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 7 Aug 2023 16:50:22 -0300 Subject: [PATCH 01/41] initial commit --- .gitignore | 272 ++++++++++++++++++ .../Commands/TestConnectionAsyncCommand.cs | 57 ++++ src/PSNetScanners/PSNetScanners.csproj | 15 + 3 files changed, 344 insertions(+) create mode 100644 .gitignore create mode 100644 src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs create mode 100644 src/PSNetScanners/PSNetScanners.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80a49e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,272 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +benchmarks/ +BenchmarkDotNet.Artifacts/ +tools/dotnet + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +### Custom entries ### +output/ +tools/Modules +test.settings.json +tests/integration/.vagrant +tests/integration/cert_setup diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs new file mode 100644 index 0000000..204afff --- /dev/null +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace PSNetScanners; + +[Cmdlet(VerbsDiagnostic.Test, "ConnectionAsync")] +public sealed class TestConnectionAsyncCommand : PSCmdlet +{ + private readonly List _tasks = new(); + + private byte[] _buffer; + + private readonly PingOptions _pingOptions = new(); + + private TimeSpan? _timeOut; + + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] + public string[] Address { get; set; } = null!; + + [Parameter] + [ValidateRange(1, int.MaxValue)] + public int BufferSize { get; set; } = 32; + + [Parameter] + [ValidateRange(1, int.MaxValue)] + public double? TimeOutSeconds { get; set; } + + [Parameter] + public SwitchParameter DontFragment { get; set; } + + protected override void BeginProcessing() + { + _buffer = Enumerable.Repeat((byte)'A', BufferSize).ToArray(); + _pingOptions.DontFragment = DontFragment.IsPresent; + + if (TimeOutSeconds is not null) + { + _timeOut = TimeSpan.FromSeconds((double)TimeOutSeconds); + } + } + + protected override void ProcessRecord() + { + foreach (string addr in Address) + { + Ping ping = new(); + ping.PingCompleted += (sender, e) => + { + // e. + }; + } + } +} \ No newline at end of file diff --git a/src/PSNetScanners/PSNetScanners.csproj b/src/PSNetScanners/PSNetScanners.csproj new file mode 100644 index 0000000..590f7dc --- /dev/null +++ b/src/PSNetScanners/PSNetScanners.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + PSNetScanners + enable + true + latest + + + + + + + \ No newline at end of file From 9af2e65e06c7c6df535d66805efdec680759c766 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Jul 2024 19:02:05 -0300 Subject: [PATCH 02/41] a bit of progress --- .github/workflows/ci.yml | 145 ++++++++++++++ .gitignore | 5 + .markdownlint.json | 8 + .vscode/extensions.json | 9 + .vscode/launch.json | 45 +++++ .vscode/settings.json | 34 ++++ .vscode/tasks.json | 49 +++++ LICENSE | 21 ++ README.md | 1 + build.ps1 | 51 +++++ module/PSNetScanners.psd1 | 138 ++++++++++++++ src/PSNetScanners/CancellationTask.cs | 28 +++ .../Commands/TestConnectionAsyncCommand.cs | 27 ++- src/PSNetScanners/DnsAsync.cs | 66 +++++++ src/PSNetScanners/PingAsync.cs | 54 ++++++ tools/InvokeBuild.ps1 | 97 ++++++++++ tools/PesterTest.ps1 | 34 ++++ tools/ProjectBuilder/Documentation.cs | 12 ++ tools/ProjectBuilder/Extensions.cs | 30 +++ tools/ProjectBuilder/Module.cs | 163 ++++++++++++++++ tools/ProjectBuilder/Pester.cs | 115 +++++++++++ tools/ProjectBuilder/Project.cs | 73 +++++++ tools/ProjectBuilder/ProjectBuilder.csproj | 14 ++ tools/ProjectBuilder/ProjectInfo.cs | 180 ++++++++++++++++++ tools/ProjectBuilder/Types.cs | 11 ++ tools/prompt.ps1 | 1 + tools/requiredModules.psd1 | 6 + 27 files changed, 1407 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .markdownlint.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 LICENSE create mode 100644 build.ps1 create mode 100644 module/PSNetScanners.psd1 create mode 100644 src/PSNetScanners/CancellationTask.cs create mode 100644 src/PSNetScanners/DnsAsync.cs create mode 100644 src/PSNetScanners/PingAsync.cs create mode 100644 tools/InvokeBuild.ps1 create mode 100644 tools/PesterTest.ps1 create mode 100644 tools/ProjectBuilder/Documentation.cs create mode 100644 tools/ProjectBuilder/Extensions.cs create mode 100644 tools/ProjectBuilder/Module.cs create mode 100644 tools/ProjectBuilder/Pester.cs create mode 100644 tools/ProjectBuilder/Project.cs create mode 100644 tools/ProjectBuilder/ProjectBuilder.csproj create mode 100644 tools/ProjectBuilder/ProjectInfo.cs create mode 100644 tools/ProjectBuilder/Types.cs create mode 100644 tools/prompt.ps1 create mode 100644 tools/requiredModules.psd1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..17fa78e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: PSParallelPipeline Workflow +on: + push: + branches: + - main + + pull_request: + branches: + - main + + release: + types: + - published + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: true + BUILD_CONFIGURATION: ${{ fromJSON('["Debug", "Release"]')[startsWith(github.ref, 'refs/tags/v')] }} + +jobs: + build: + name: build + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build module - Debug + shell: pwsh + run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build + if: ${{ env.BUILD_CONFIGURATION == 'Debug' }} + + - name: Build module - Publish + shell: pwsh + run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build + if: ${{ env.BUILD_CONFIGURATION == 'Release' }} + + - name: Capture PowerShell Module + uses: actions/upload-artifact@v4 + with: + name: PSModule + path: output/*.nupkg + + test: + name: test + needs: + - build + runs-on: ${{ matrix.info.os }} + strategy: + fail-fast: false + matrix: + info: + - name: PS-5.1 + psversion: '5.1' + os: windows-latest + - name: PS-7_Windows + psversion: '7' + os: windows-latest + - name: PS-7_Linux + psversion: '7' + os: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Restore Built PowerShell Module + uses: actions/download-artifact@v4 + with: + name: PSModule + path: output + + - name: Install Built PowerShell Module + shell: pwsh + run: | + $manifestItem = Get-Item ([IO.Path]::Combine('module', '*.psd1')) + $moduleName = $manifestItem.BaseName + $manifest = Test-ModuleManifest -Path $manifestItem.FullName -ErrorAction SilentlyContinue -WarningAction Ignore + + $destPath = [IO.Path]::Combine('output', $moduleName, $manifest.Version) + if (-not (Test-Path -LiteralPath $destPath)) { + New-Item -Path $destPath -ItemType Directory | Out-Null + } + + Get-ChildItem output/*.nupkg | Rename-Item -NewName { $_.Name -replace '.nupkg', '.zip' } + + Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop + + - name: Run Tests - Windows PowerShell + if: ${{ matrix.info.psversion == '5.1' }} + shell: powershell + run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test + + - name: Run Tests - PowerShell + if: ${{ matrix.info.psversion != '5.1' }} + shell: pwsh + run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Unit Test Results (${{ matrix.info.name }}) + path: ./output/TestResults/Pester.xml + + - name: Upload Coverage Results + if: always() && !startsWith(github.ref, 'refs/tags/v') + uses: actions/upload-artifact@v4 + with: + name: Coverage Results (${{ matrix.info.name }}) + path: ./output/TestResults/Coverage.xml + + - name: Upload Coverage to codecov + if: always() && !startsWith(github.ref, 'refs/tags/v') + uses: codecov/codecov-action@v4 + with: + files: ./output/TestResults/Coverage.xml + flags: ${{ matrix.info.name }} + token: ${{ secrets.CODECOV_TOKEN }} + + publish: + name: publish + if: startsWith(github.ref, 'refs/tags/v') + needs: + - build + - test + runs-on: windows-latest + steps: + - name: Restore Built PowerShell Module + uses: actions/download-artifact@v4 + with: + name: PSModule + path: ./ + + - name: Publish to Gallery + if: github.event_name == 'release' + shell: pwsh + run: >- + dotnet nuget push '*.nupkg' + --api-key $env:PSGALLERY_TOKEN + --source 'https://www.powershellgallery.com/api/v2/package' + --no-symbols + env: + PSGALLERY_TOKEN: ${{ secrets.PSGALLERY_TOKEN }} diff --git a/.gitignore b/.gitignore index 80a49e0..480bc3e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ tools/dotnet *.user *.userosscache *.sln.docstates +*.zip # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -270,3 +271,7 @@ tools/Modules test.settings.json tests/integration/.vagrant tests/integration/cert_setup +tools/ProjectBuilder/output +tools/ProjectBuilder/bin +tools/ProjectBuilder/debug +tools/ProjectBuilder/obj diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..4e3f7c3 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,8 @@ +{ + "default": true, + "no-hard-tabs": true, + "no-duplicate-heading": false, + "line-length": false, + "no-inline-html": false, + "ul-indent": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4dafc5c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "formulahendry.dotnet-test-explorer", + "ms-dotnettools.csharp", + "ms-vscode.powershell", + ], +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ff1143b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "PowerShell launch", + "type": "coreclr", + "request": "launch", + "program": "pwsh", + "args": [ + "-NoExit", + "-NoProfile", + "-Command", + ". ./tools/prompt.ps1;", + "Import-Module ./output/PSNetScanners" + ], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "externalTerminal", + }, + { + "name": "PowerShell Launch Current File", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "cwd": "${workspaceFolder}" + }, + { + "name": ".NET FullCLR Attach", + "type": "clr", + "request": "attach", + "processId": "${command:pickProcess}", + "justMyCode": true, + }, + { + "name": ".NET CoreCLR Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}", + "justMyCode": true, + }, + ], +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0e226ee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + "cSpell.enableFiletypes": [ + "!powershell" + ], + //-------- Files configuration -------- + // When enabled, will trim trailing whitespace when you save a file. + "files.trimTrailingWhitespace": true, + // When enabled, insert a final new line at the end of the file when saving it. + "files.insertFinalNewline": true, + "search.exclude": { + "Release": true, + "tools/ResGen": true, + "tools/dotnet": true, + }, + "json.schemas": [ + { + "fileMatch": [ + "/test.settings.json" + ], + "url": "./tests/settings.schema.json" + } + ], + "editor.rulers": [ + 120, + ], + //-------- PowerShell configuration -------- + // Binary modules cannot be unloaded so running in separate processes solves that problem + //"powershell.debugging.createTemporaryIntegratedConsole": true, + // We use Pester v5 so we don't need the legacy code lens + "powershell.pester.useLegacyCodeLens": false, + "cSpell.words": [ + "pwsh" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..bbde0bc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,49 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "pwsh", + "type": "shell", + "args": [ + "-File", + "${workspaceFolder}/build.ps1" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "update docs", + "command": "pwsh", + "type": "shell", + "args": [ + "-Command", + "Import-Module ${workspaceFolder}/output/PSNetScanners; Import-Module ${workspaceFolder}/tools/Modules/platyPS; Update-MarkdownHelpModule ${workspaceFolder}/docs/en-US -AlphabeticParamsOrder -RefreshModulePage -UpdateInputOutput" + ], + "problemMatcher": [], + "dependsOn": [ + "build" + ] + }, + { + "label": "test", + "command": "pwsh", + "type": "shell", + "args": [ + "-File", + "${workspaceFolder}/build.ps1", + "-Task", + "Test" + ], + "problemMatcher": [], + "dependsOn": [ + "build" + ] + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd35bd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Santiago Squarzon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 96dee1c..90834a1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # PowerShell Network Scanners ## DESCRIPTION + Two PowerShell scripts designed to scan Network IP Ranges or hostname using [`Runspace`](https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace?view=powershellsdk-7.0.0) for faster execution. And two standalone functions using async techniques for ICMP and TCP scanning. | Name | Description | diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..650b40b --- /dev/null +++ b/build.ps1 @@ -0,0 +1,51 @@ +[CmdletBinding()] +param( + [Parameter()] + [ValidateSet('Debug', 'Release')] + [string] $Configuration = 'Debug', + + [Parameter()] + [ValidateSet('Build', 'Test')] + [string[]] $Task = 'Build' +) + +$prev = $ErrorActionPreference +$ErrorActionPreference = 'Stop' + +if (-not ('ProjectBuilder.ProjectInfo' -as [type])) { + try { + $builderPath = [IO.Path]::Combine($PSScriptRoot, 'tools', 'ProjectBuilder') + Push-Location $builderPath + + dotnet @( + 'publish' + '--configuration', 'Release' + '-o', 'output' + '--framework', 'netstandard2.0' + '--verbosity', 'q' + '-nologo' + ) + + if ($LASTEXITCODE) { + throw "Failed to compiled 'ProjectBuilder'" + } + + $dll = [IO.Path]::Combine($builderPath, 'output', 'ProjectBuilder.dll') + Add-Type -Path $dll + } + finally { + Pop-Location + } +} + +$projectInfo = [ProjectBuilder.ProjectInfo]::Create($PSScriptRoot, $Configuration) +$projectInfo.GetRequirements() | Import-Module -DisableNameChecking -Force + +$ErrorActionPreference = $prev + +$invokeBuildSplat = @{ + Task = $Task + File = Convert-Path ([IO.Path]::Combine($PSScriptRoot, 'tools', 'InvokeBuild.ps1')) + ProjectInfo = $projectInfo +} +Invoke-Build @invokeBuildSplat diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 new file mode 100644 index 0000000..7b876f1 --- /dev/null +++ b/module/PSNetScanners.psd1 @@ -0,0 +1,138 @@ +# +# Module manifest for module 'PSNetScanners' +# +# Generated by: Santiago Squarzon +# +# Generated on: 10/07/2024 +# + +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'bin/netstandard2.0/PSNetScanners.dll' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'eec20b34-11d9-4085-af34-519c5503beb2' + + # Author of this module + Author = 'Santiago Squarzon' + + # Company or vendor of this module + CompanyName = 'Unknown' + + # Copyright statement for this module + Copyright = '(c) Santiago Squarzon. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'tcp and icmp async scanners for powershell' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @('Invoke-PingAsync') + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'parallel' + 'concurrency' + 'runspace' + 'parallel-processing' + 'powershell' + 'multithreading' + 'ping' + 'async' + 'tcp' + 'icmp' + ) + + # A URL to the license for this module. + LicenseUri = 'https://github.com/santisq/PSNetScanners/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/santisq/PSNetScanners' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} diff --git a/src/PSNetScanners/CancellationTask.cs b/src/PSNetScanners/CancellationTask.cs new file mode 100644 index 0000000..b0f855a --- /dev/null +++ b/src/PSNetScanners/CancellationTask.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PSNetScanners; + +internal sealed class Cancellation : IDisposable +{ + private readonly CancellationTokenSource _cts; + + internal Task Task { get; } + + internal Cancellation(int timeout) + { + _cts = new CancellationTokenSource(timeout); + Task = Task.Delay(Timeout.Infinite, _cts.Token); + } + + internal void Cancel() => _cts.Cancel(); + + internal CancellationTokenRegistration Register(Action action) => + _cts.Token.Register(action); + + public void Dispose() + { + _cts.Dispose(); + } +} \ No newline at end of file diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs index 204afff..a0f4e7f 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -1,8 +1,9 @@ -using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Net.NetworkInformation; +using System.Threading; using System.Threading.Tasks; namespace PSNetScanners; @@ -12,11 +13,13 @@ public sealed class TestConnectionAsyncCommand : PSCmdlet { private readonly List _tasks = new(); - private byte[] _buffer; + private byte[] _buffer = null!; + + private int _taskCount; private readonly PingOptions _pingOptions = new(); - private TimeSpan? _timeOut; + private readonly BlockingCollection _output = new(); [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] public string[] Address { get; set; } = null!; @@ -27,7 +30,7 @@ public sealed class TestConnectionAsyncCommand : PSCmdlet [Parameter] [ValidateRange(1, int.MaxValue)] - public double? TimeOutSeconds { get; set; } + public int TimeOutSeconds { get; set; } = 10; [Parameter] public SwitchParameter DontFragment { get; set; } @@ -36,11 +39,6 @@ protected override void BeginProcessing() { _buffer = Enumerable.Repeat((byte)'A', BufferSize).ToArray(); _pingOptions.DontFragment = DontFragment.IsPresent; - - if (TimeOutSeconds is not null) - { - _timeOut = TimeSpan.FromSeconds((double)TimeOutSeconds); - } } protected override void ProcessRecord() @@ -48,9 +46,18 @@ protected override void ProcessRecord() foreach (string addr in Address) { Ping ping = new(); + ping.SendPingAsync( + hostNameOrAddress: addr, + timeout: TimeOutSeconds * 1000, + buffer: _buffer, + options: _pingOptions); + + ping.Disposed += (sender, e) => Interlocked.Decrement(ref _taskCount); + ping.PingCompleted += (sender, e) => { - // e. + _output.Add(e.Reply); + ((AutoResetEvent)e.UserState).Set(); }; } } diff --git a/src/PSNetScanners/DnsAsync.cs b/src/PSNetScanners/DnsAsync.cs new file mode 100644 index 0000000..4133f9f --- /dev/null +++ b/src/PSNetScanners/DnsAsync.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Threading.Tasks; + +namespace PSNetScanners; + +public enum Status +{ + Success, + Timeout, + Error +} + +public abstract class DnsResult +{ + public string HostName { get; } = string.Empty; + + public Status Status { get; } + + public bool IsSuccess { get; } + + protected DnsResult(string hostname) => + (HostName, IsSuccess) = (hostname, true); + + protected DnsResult(Status status) => Status = status; + + public override string ToString() => HostName; +} + +public class DnsSuccess(string hostname) : + DnsResult(hostname) +{ + +} + +public class DnsFailure(Status status, string error) : + DnsResult(status) +{ + public string ErrorMessage { get; } = error; +} + +public sealed class DnsAsync +{ + private readonly Task _cancellation; + + internal DnsAsync(Cancellation cancellation) => + _cancellation = cancellation.Task; + + internal async Task GetHostEntryAsync( + string host, + Cancellation cancellation) + { + Task task = Task.WhenAny(Dns.GetHostEntryAsync(host), _cancellation); + if (task == cancellation.Task) + { + return new DnsFailure(Status.Timeout, _cancellation.Exception.Message); + } + + if (task.Status is not TaskStatus.RanToCompletion) + { + return new DnsFailure(Status.Error, task.Exception.Message); + } + + IPHostEntry entry = await (Task)task; + return new DnsSuccess(entry.HostName); + } +} diff --git a/src/PSNetScanners/PingAsync.cs b/src/PSNetScanners/PingAsync.cs new file mode 100644 index 0000000..2323cb4 --- /dev/null +++ b/src/PSNetScanners/PingAsync.cs @@ -0,0 +1,54 @@ +// using System; +// using System.Collections.Generic; +// using System.Net; +// using System.Net.NetworkInformation; +// using System.Threading; +// using System.Threading.Tasks; + +// namespace PSNetScanners; + +// public sealed class PingAsync +// { +// private readonly Ping _ping; + +// private readonly PingOptions _options; + +// private readonly int _timeOut; + +// private readonly byte[] _buffer; + +// private readonly string _target; + +// internal PingAsync( +// PingOptions options, +// string target, +// int timeOut, +// byte[] buffer) +// { +// _ping = new Ping(); +// _options = options; +// _target = target; +// _timeOut = timeOut; +// _buffer = buffer; +// } + +// internal Task Run() => Task.Run(async () => +// { + +// CancellationTokenSource source = new(); +// Task myCancellationTask = Task.Run(async () => +// { +// while (true) +// { +// source.Token.ThrowIfCancellationRequested(); +// await Task.Delay(200); +// } +// }); +// Task entryTask = Dns.GetHostEntryAsync(_target); + +// await Task.WhenAny(myCancellationTask, entryTask); + +// Task pingTask = _ping.SendPingAsync(_target, _timeOut, _buffer, _options); +// Task done = await Task.WhenAny(dnsTask, pingTask); +// }); +// } \ No newline at end of file diff --git a/tools/InvokeBuild.ps1 b/tools/InvokeBuild.ps1 new file mode 100644 index 0000000..4ece54d --- /dev/null +++ b/tools/InvokeBuild.ps1 @@ -0,0 +1,97 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ProjectBuilder.ProjectInfo] $ProjectInfo +) + +task Clean { + $ProjectInfo.CleanRelease() +} + +task BuildDocs { + $helpParams = $ProjectInfo.Documentation.GetParams() + $null = New-ExternalHelp @helpParams +} + +task BuildManaged { + $arguments = $ProjectInfo.GetBuildArgs() + Push-Location -LiteralPath $ProjectInfo.Project.Source.FullName + + try { + foreach ($framework in $ProjectInfo.Project.TargetFrameworks) { + Write-Host "Compiling for $framework" + dotnet @arguments --framework $framework + + if ($LASTEXITCODE) { + throw "Failed to compiled code for $framework" + } + } + } + finally { + Pop-Location + } +} + +task CopyToRelease { + $ProjectInfo.Module.CopyToRelease() + $ProjectInfo.Project.CopyToRelease() +} + +task Package { + $ProjectInfo.Project.ClearNugetPackage() + $repoParams = $ProjectInfo.Project.GetPSRepoParams() + + if (Get-PSRepository -Name $repoParams.Name -ErrorAction SilentlyContinue) { + Unregister-PSRepository -Name $repoParams.Name + } + + Register-PSRepository @repoParams + try { + $publishModuleSplat = @{ + Path = $ProjectInfo.Project.Release + Repository = $repoParams.Name + } + Publish-Module @publishModuleSplat + } + finally { + Unregister-PSRepository -Name $repoParams.Name + } +} + +task Analyze { + if (-not $ProjectInfo.AnalyzerPath) { + Write-Host 'No Analyzer Settings found, skipping' + return + } + + $pssaSplat = $ProjectInfo.GetAnalyzerParams() + $results = Invoke-ScriptAnalyzer @pssaSplat + + if ($results) { + $results | Out-String + throw 'Failed PsScriptAnalyzer tests, build failed' + } +} + +task PesterTests { + if (-not $ProjectInfo.Pester.PesterScript) { + Write-Host 'No Pester tests found, skipping' + return + } + + $ProjectInfo.Pester.ClearResultFile() + + if (-not (dotnet tool list --global | Select-String coverlet.console -SimpleMatch)) { + Write-Host 'Installing dotnet tool coverlet.console' -ForegroundColor Yellow + dotnet tool install --global coverlet.console + } + + coverlet $ProjectInfo.Pester.GetTestArgs($PSVersionTable.PSVersion) + + if ($LASTEXITCODE) { + throw 'Pester failed tests' + } +} + +task Build -Jobs Clean, BuildManaged, CopyToRelease, BuildDocs, Package +task Test -Jobs BuildManaged, Analyze, PesterTests diff --git a/tools/PesterTest.ps1 b/tools/PesterTest.ps1 new file mode 100644 index 0000000..1497703 --- /dev/null +++ b/tools/PesterTest.ps1 @@ -0,0 +1,34 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [String] $TestPath, + + [Parameter(Mandatory)] + [String] $OutputFile +) + +$ErrorActionPreference = 'Stop' + +Get-ChildItem ([IO.Path]::Combine($PSScriptRoot, 'Modules')) -Directory | + Import-Module -Name { $_.FullName } -Force -DisableNameChecking + +[PSCustomObject] $PSVersionTable | Select-Object *, @{ + Name = 'Architecture' + Expression = { + switch ([IntPtr]::Size) { + 4 { 'x86' } + 8 { 'x64' } + default { 'Unknown' } + } + } +} | Format-List | Out-Host + +$configuration = [PesterConfiguration]::Default +$configuration.Output.Verbosity = 'Detailed' +$configuration.Run.Path = $TestPath +$configuration.Run.Throw = $true +$configuration.TestResult.Enabled = $true +$configuration.TestResult.OutputPath = $OutputFile +$configuration.TestResult.OutputFormat = 'NUnitXml' + +Invoke-Pester -Configuration $configuration -WarningAction Ignore diff --git a/tools/ProjectBuilder/Documentation.cs b/tools/ProjectBuilder/Documentation.cs new file mode 100644 index 0000000..1ba3e01 --- /dev/null +++ b/tools/ProjectBuilder/Documentation.cs @@ -0,0 +1,12 @@ +using System.Collections; + +namespace ProjectBuilder; + +public record struct Documentation(string Source, string Output) +{ + public readonly Hashtable GetParams() => new() + { + ["Path"] = Source, + ["OutputPath"] = Output + }; +} diff --git a/tools/ProjectBuilder/Extensions.cs b/tools/ProjectBuilder/Extensions.cs new file mode 100644 index 0000000..cc2c0fe --- /dev/null +++ b/tools/ProjectBuilder/Extensions.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +namespace ProjectBuilder; + +internal static class Extensions +{ + internal static void CopyRecursive(this DirectoryInfo source, string? destination) + { + if (destination is null) + { + throw new ArgumentNullException($"Destination path is null.", nameof(destination)); + } + + if (!Directory.Exists(destination)) + { + Directory.CreateDirectory(destination); + } + + foreach (DirectoryInfo dir in source.EnumerateDirectories("*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(dir.FullName.Replace(source.FullName, destination)); + } + + foreach (FileInfo file in source.EnumerateFiles("*", SearchOption.AllDirectories)) + { + file.CopyTo(file.FullName.Replace(source.FullName, destination)); + } + } +} diff --git a/tools/ProjectBuilder/Module.cs b/tools/ProjectBuilder/Module.cs new file mode 100644 index 0000000..a2ff7cf --- /dev/null +++ b/tools/ProjectBuilder/Module.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Net.Http; +using System.Threading.Tasks; + +namespace ProjectBuilder; + +public sealed class Module +{ + public DirectoryInfo Root { get; } + + public FileInfo? Manifest { get; internal set; } + + public Version? Version { get; internal set; } + + public string Name { get; } + + public string PreRequisitePath { get; } + + private string? Release { get => _info.Project.Release; } + + private readonly UriBuilder _builder = new(_base); + + private const string _base = "https://www.powershellgallery.com"; + + private const string _path = "api/v2/package/{0}/{1}"; + + private readonly ProjectInfo _info; + + private Hashtable? _req; + + internal Module( + DirectoryInfo directory, + string name, + ProjectInfo info) + { + Root = directory; + Name = name; + PreRequisitePath = InitPrerequisitePath(Root); + _info = info; + } + + public void CopyToRelease() => Root.CopyRecursive(Release); + + internal IEnumerable GetRequirements(string path) + { + _req ??= ImportRequirements(path); + + if (_req is { Count: 0 }) + { + return []; + } + + List modules = new(_req.Count); + foreach (DictionaryEntry entry in _req) + { + modules.Add(new ModuleDownload + { + Module = entry.Key.ToString(), + Version = LanguagePrimitives.ConvertTo(entry.Value) + }); + } + + return DownloadModules([.. modules]); + } + + private static Hashtable ImportRequirements(string path) + { + using PowerShell powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace); + return powerShell + .AddCommand("Import-PowerShellDataFile") + .AddArgument(path) + .Invoke() + .FirstOrDefault(); + } + + private string[] DownloadModules(ModuleDownload[] modules) + { + List> tasks = new(modules.Length); + List output = new(modules.Length); + + foreach ((string module, Version version) in modules) + { + string destination = GetDestination(module); + string modulePath = GetModulePath(module); + + if (Directory.Exists(modulePath)) + { + output.Add(modulePath); + continue; + } + + Console.WriteLine($"Installing build pre-req '{module}'"); + _builder.Path = string.Format(_path, module, version); + Task task = GetModuleAsync( + uri: _builder.Uri.ToString(), + destination: destination, + expandPath: modulePath); + tasks.Add(task); + } + + output.AddRange(WaitTask(tasks)); + return [.. output]; + } + + private static string[] WaitTask(List> tasks) => + WaitTaskAsync(tasks).GetAwaiter().GetResult(); + + private static void ExpandArchive(string source, string destination) => + ZipFile.ExtractToDirectory(source, destination); + + private static async Task WaitTaskAsync( + List> tasks) + { + List completedTasks = new(tasks.Count); + while (tasks.Count > 0) + { + Task awaiter = await Task.WhenAny(tasks); + tasks.Remove(awaiter); + string module = await awaiter; + completedTasks.Add(module); + } + return [.. completedTasks]; + } + + private string GetDestination(string module) => + Path.Combine(PreRequisitePath, Path.ChangeExtension(module, "zip")); + + private string GetModulePath(string module) => + Path.Combine(PreRequisitePath, module); + + private static async Task GetModuleAsync( + string uri, + string destination, + string expandPath) + { + using (FileStream fs = File.Create(destination)) + { + using HttpClient client = new(); + using Stream stream = await client.GetStreamAsync(uri); + await stream.CopyToAsync(fs); + } + + ExpandArchive(destination, expandPath); + File.Delete(destination); + return expandPath; + } + + private static string InitPrerequisitePath(DirectoryInfo root) + { + string path = Path.Combine(root.Parent.FullName, "tools", "Modules"); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + return path; + } +} diff --git a/tools/ProjectBuilder/Pester.cs b/tools/ProjectBuilder/Pester.cs new file mode 100644 index 0000000..fccaac1 --- /dev/null +++ b/tools/ProjectBuilder/Pester.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace ProjectBuilder; + +public sealed class Pester +{ + public string? PesterScript + { + get + { + _pesterPath ??= Path.Combine( + _info.Root.FullName, + "tools", + "PesterTest.ps1"); + + if (_testsExist = File.Exists(_pesterPath)) + { + return _pesterPath; + } + + return null; + } + } + + public string? ResultPath { get => _testsExist ? Path.Combine(_info.Project.Build, "TestResults") : null; } + + public string? ResultFile { get => _testsExist ? Path.Combine(ResultPath, "Pester.xml") : null; } + + private readonly ProjectInfo _info; + + private string? _pesterPath; + + private bool _testsExist; + + internal Pester(ProjectInfo info) => _info = info; + + private void CreateResultPath() + { + if (!Directory.Exists(ResultPath)) + { + Directory.CreateDirectory(ResultPath); + } + } + + public void ClearResultFile() + { + if (File.Exists(ResultFile)) + { + File.Delete(ResultFile); + } + } + + public string[] GetTestArgs(Version version) + { + CreateResultPath(); + + List arguments = [ + "-NoProfile", + "-NonInteractive", + ]; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + arguments.AddRange([ "-ExecutionPolicy", "Bypass" ]); + } + + arguments.AddRange([ + "-File", PesterScript!, + "-TestPath", Path.Combine(_info.Root.FullName, "tests"), + "-OutputFile", ResultFile! + ]); + + Regex re = new("^|$", RegexOptions.Compiled); + string targetArgs = re.Replace(string.Join("\" \"", [.. arguments]), "\""); + string pwsh = Regex.Replace(Environment.GetCommandLineArgs().First(), @"\.dll$", string.Empty); + string unitCoveragePath = Path.Combine(ResultPath, "UnitCoverage.json"); + string watchFolder = Path.Combine(_info.Project.Release, "bin", _info.Project.TestFramework); + string sourceMappingFile = Path.Combine(ResultPath, "CoverageSourceMapping.txt"); + + if (version is not { Major: >= 7, Minor: > 0 }) + { + targetArgs = re.Replace(targetArgs, "\""); + watchFolder = re.Replace(watchFolder, "\""); + } + + arguments.Clear(); + arguments.AddRange([ + watchFolder, + "--target", pwsh, + "--targetargs", targetArgs, + "--output", Path.Combine(ResultPath, "Coverage.xml"), + "--format", "cobertura" + ]); + + if (File.Exists(unitCoveragePath)) + { + arguments.AddRange([ "--merge-with", unitCoveragePath ]); + } + + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is "true") + { + arguments.AddRange([ "--source-mapping-file", sourceMappingFile ]); + File.WriteAllText( + sourceMappingFile, + $"|{_info.Root.FullName}{Path.DirectorySeparatorChar}=/_/"); + } + + return [.. arguments]; + } +} diff --git a/tools/ProjectBuilder/Project.cs b/tools/ProjectBuilder/Project.cs new file mode 100644 index 0000000..831c292 --- /dev/null +++ b/tools/ProjectBuilder/Project.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; + +namespace ProjectBuilder; + +public sealed class Project +{ + public DirectoryInfo Source { get; } + + public string Build { get; } + + public string? Release { get; internal set; } + + public string[]? TargetFrameworks { get; internal set; } + + public string? TestFramework { get => TargetFrameworks.FirstOrDefault(); } + + private Configuration Configuration { get => _info.Configuration; } + + private readonly ProjectInfo _info; + + internal Project(DirectoryInfo source, string build, ProjectInfo info) + { + Source = source; + Build = build; + _info = info; + } + + public void CopyToRelease() + { + if (TargetFrameworks is null) + { + throw new ArgumentNullException( + "TargetFrameworks is null.", + nameof(TargetFrameworks)); + } + + foreach (string framework in TargetFrameworks) + { + DirectoryInfo buildFolder = new(Path.Combine( + Source.FullName, + "bin", + Configuration.ToString(), + framework, + "publish")); + + string binFolder = Path.Combine(Release, "bin", framework); + buildFolder.CopyRecursive(binFolder); + } + } + + public Hashtable GetPSRepoParams() => new() + { + ["Name"] = "LocalRepo", + ["SourceLocation"] = Build, + ["PublishLocation"] = Build, + ["InstallationPolicy"] = "Trusted" + }; + + public void ClearNugetPackage() + { + string nugetPath = Path.Combine( + Build, + $"{_info.Module.Name}.{_info.Module.Version}.nupkg"); + + if (File.Exists(nugetPath)) + { + File.Delete(nugetPath); + } + } +} diff --git a/tools/ProjectBuilder/ProjectBuilder.csproj b/tools/ProjectBuilder/ProjectBuilder.csproj new file mode 100644 index 0000000..6a1a2a5 --- /dev/null +++ b/tools/ProjectBuilder/ProjectBuilder.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + enable + latest + ProjectBuilder + + + + + + + diff --git a/tools/ProjectBuilder/ProjectInfo.cs b/tools/ProjectBuilder/ProjectInfo.cs new file mode 100644 index 0000000..b1f7b2c --- /dev/null +++ b/tools/ProjectBuilder/ProjectInfo.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Xml; + +namespace ProjectBuilder; + +public sealed class ProjectInfo +{ + public DirectoryInfo Root { get; } + + public Module Module { get; } + + public Configuration Configuration { get; internal set; } + + public Documentation Documentation { get; internal set; } + + public Project Project { get; } + + public Pester Pester { get; } + + public string? AnalyzerPath + { + get + { + _analyzerPath ??= Path.Combine( + Root.FullName, + "ScriptAnalyzerSettings.psd1"); + + if (File.Exists(_analyzerPath)) + { + return _analyzerPath; + } + + return null; + } + } + + private string? _analyzerPath; + + private ProjectInfo(string path) + { + Root = AssertDirectory(path); + + Module = new Module( + directory: AssertDirectory(GetModulePath(path)), + name: Path.GetFileNameWithoutExtension(path), + info: this); + + Project = new Project( + source: AssertDirectory(GetSourcePath(path, Module.Name)), + build: GetBuildPath(path), + info: this); + + Pester = new(this); + } + + public static ProjectInfo Create( + string path, + Configuration configuration) + { + ProjectInfo builder = new(path) + { + Configuration = configuration + }; + builder.Module.Manifest = GetManifest(builder); + builder.Module.Version = GetManifestVersion(builder); + builder.Project.Release = GetReleasePath( + builder.Project.Build, + builder.Module.Name, + builder.Module.Version!); + builder.Project.TargetFrameworks = GetTargetFrameworks(GetProjectFile(builder)); + builder.Documentation = new Documentation + { + Source = Path.Combine(builder.Root.FullName, "docs", "en-US"), + Output = Path.Combine(builder.Project.Release, "en-US") + }; + + return builder; + } + + public IEnumerable GetRequirements() + { + string req = Path.Combine(Root.FullName, "tools", "requiredModules.psd1"); + if (!File.Exists(req)) + { + return []; + } + return Module.GetRequirements(req); + } + + public void CleanRelease() + { + if (Directory.Exists(Project.Release)) + { + Directory.Delete(Project.Release, recursive: true); + } + Directory.CreateDirectory(Project.Release); + } + + public string[] GetBuildArgs() => + [ + "publish", + "--configuration", Configuration.ToString(), + "--verbosity", "q", + "-nologo", + $"-p:Version={Module.Version}" + ]; + + public Hashtable GetAnalyzerParams() => new() + { + ["Path"] = Project.Release, + ["Settings"] = AnalyzerPath, + ["Recurse"] = true, + ["ErrorAction"] = "SilentlyContinue" + }; + + private static string[] GetTargetFrameworks(string path) + { + XmlDocument xmlDocument = new(); + xmlDocument.Load(path); + return xmlDocument + .SelectSingleNode("Project/PropertyGroup/TargetFrameworks") + .InnerText + .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + private static string GetBuildPath(string path) => + Path.Combine(path, "output"); + + private static string GetSourcePath(string path, string moduleName) => + Path.Combine(path, "src", moduleName); + + private static string GetModulePath(string path) => + Path.Combine(path, "module"); + + private static string GetReleasePath( + string buildPath, + string moduleName, + Version version) => Path.Combine( + buildPath, + moduleName, + LanguagePrimitives.ConvertTo(version)); + + private static DirectoryInfo AssertDirectory(string path) + { + DirectoryInfo directory = new(path); + return directory.Exists ? directory + : throw new ArgumentException( + $"Path '{path}' could not be found or is not a Directory.", + nameof(path)); + } + + private static FileInfo GetManifest(ProjectInfo builder) => + builder.Module.Root.EnumerateFiles("*.psd1").FirstOrDefault() + ?? throw new FileNotFoundException( + $"Manifest file could not be found in '{builder.Root.FullName}'"); + + private static string GetProjectFile(ProjectInfo builder) => + builder.Project.Source.EnumerateFiles("*.csproj").FirstOrDefault()?.FullName + ?? throw new FileNotFoundException( + $"Project file could not be found in ''{builder.Project.Source.FullName}'"); + + private static Version? GetManifestVersion(ProjectInfo builder) + { + using PowerShell powershell = PowerShell.Create(RunspaceMode.CurrentRunspace); + Hashtable? moduleInfo = powershell + .AddCommand("Import-PowerShellDataFile") + .AddArgument(builder.Module.Manifest?.FullName) + .Invoke() + .FirstOrDefault(); + + return powershell.HadErrors + ? throw powershell.Streams.Error.First().Exception + : LanguagePrimitives.ConvertTo(moduleInfo?["ModuleVersion"]); + } +} diff --git a/tools/ProjectBuilder/Types.cs b/tools/ProjectBuilder/Types.cs new file mode 100644 index 0000000..3106122 --- /dev/null +++ b/tools/ProjectBuilder/Types.cs @@ -0,0 +1,11 @@ +using System; + +namespace ProjectBuilder; + +public enum Configuration +{ + Debug, + Release +} + +internal record struct ModuleDownload(string Module, Version Version); diff --git a/tools/prompt.ps1 b/tools/prompt.ps1 new file mode 100644 index 0000000..483e08b --- /dev/null +++ b/tools/prompt.ps1 @@ -0,0 +1 @@ +function prompt { "PS $($PWD.Path -replace '.+(?=\\)', '..')$('>' * ($nestedPromptLevel + 1)) " } diff --git a/tools/requiredModules.psd1 b/tools/requiredModules.psd1 new file mode 100644 index 0000000..a486014 --- /dev/null +++ b/tools/requiredModules.psd1 @@ -0,0 +1,6 @@ +@{ + InvokeBuild = '5.11.2' + platyPS = '0.14.2' + PSScriptAnalyzer = '1.22.0' + Pester = '5.6.0' +} From fd9c0f23bea591779f40f63bdcfabfeaaeade3ad Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 13 Jul 2024 16:03:29 -0300 Subject: [PATCH 03/41] some progress with pingasync --- .vscode/settings.json | 24 ++++- module/PSNetScanners.Format.ps1xml | 51 ++++++++++ module/PSNetScanners.psd1 | 2 +- src/PSNetScanners/CancellationTask.cs | 19 ++-- src/PSNetScanners/DnsAsync.cs | 68 ++++---------- src/PSNetScanners/DnsResult.cs | 48 ++++++++++ src/PSNetScanners/PingAsync.cs | 128 +++++++++++++++----------- tools/InvokeBuild.ps1 | 6 +- 8 files changed, 230 insertions(+), 116 deletions(-) create mode 100644 module/PSNetScanners.Format.ps1xml create mode 100644 src/PSNetScanners/DnsResult.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 0e226ee..c7f8f57 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,5 +30,27 @@ "powershell.pester.useLegacyCodeLens": false, "cSpell.words": [ "pwsh" - ] + ], + "xml.fileAssociations": [ + { + "systemId": "https://raw.githubusercontent.com/PowerShell/PowerShell/master/src/Schemas/Format.xsd", + "pattern": "**/*.Format.ps1xml" + }, + { + "systemId": "https://raw.githubusercontent.com/PowerShell/PowerShell/master/src/Schemas/Types.xsd", + "pattern": "**/*.Types.ps1xml" + } + ], + "[powershell]": { + "files.encoding": "utf8bom", + "editor.tabSize": 4, + "editor.detectIndentation": false, + "editor.autoIndent": "full" + }, + "[csharp]": { + "editor.maxTokenizationLineLength": 2500, + "editor.tabSize": 4, + "editor.detectIndentation": false, + "editor.autoIndent": "full", + } } diff --git a/module/PSNetScanners.Format.ps1xml b/module/PSNetScanners.Format.ps1xml new file mode 100644 index 0000000..9e46aaa --- /dev/null +++ b/module/PSNetScanners.Format.ps1xml @@ -0,0 +1,51 @@ + + + + + DnsViewSuccess + + PSNetScanners.DnsSuccess + + + + + + + Status + + + HostName + + + AddressList + + + Aliases + + + + + + + + DnsViewFailure + + PSNetScanners.DnsFailure + + + + + + + Status + + + Exception + + + + + + + + diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 index 7b876f1..7c06665 100644 --- a/module/PSNetScanners.psd1 +++ b/module/PSNetScanners.psd1 @@ -62,7 +62,7 @@ # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module - # FormatsToProcess = @() + FormatsToProcess = @('PSNetScanners.Format.ps1xml') # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() diff --git a/src/PSNetScanners/CancellationTask.cs b/src/PSNetScanners/CancellationTask.cs index b0f855a..e86bfd9 100644 --- a/src/PSNetScanners/CancellationTask.cs +++ b/src/PSNetScanners/CancellationTask.cs @@ -4,25 +4,26 @@ namespace PSNetScanners; -internal sealed class Cancellation : IDisposable +public sealed class Cancellation : IDisposable { private readonly CancellationTokenSource _cts; + internal bool IsCancellationRequested { get => _cts.IsCancellationRequested; } + internal Task Task { get; } - internal Cancellation(int timeout) + public Cancellation(int? timeout) { - _cts = new CancellationTokenSource(timeout); + _cts = new CancellationTokenSource(timeout ?? -1); Task = Task.Delay(Timeout.Infinite, _cts.Token); } - internal void Cancel() => _cts.Cancel(); + public void Cancel() => _cts.Cancel(); internal CancellationTokenRegistration Register(Action action) => _cts.Token.Register(action); - public void Dispose() - { - _cts.Dispose(); - } -} \ No newline at end of file + internal void ThrowIfCancellationRequested() => _cts.Token.ThrowIfCancellationRequested(); + + public void Dispose() => _cts.Dispose(); +} diff --git a/src/PSNetScanners/DnsAsync.cs b/src/PSNetScanners/DnsAsync.cs index 4133f9f..f260e67 100644 --- a/src/PSNetScanners/DnsAsync.cs +++ b/src/PSNetScanners/DnsAsync.cs @@ -1,66 +1,36 @@ +using System; using System.Net; using System.Threading.Tasks; namespace PSNetScanners; -public enum Status +internal static class DnsAsync { - Success, - Timeout, - Error -} - -public abstract class DnsResult -{ - public string HostName { get; } = string.Empty; - - public Status Status { get; } - - public bool IsSuccess { get; } - - protected DnsResult(string hostname) => - (HostName, IsSuccess) = (hostname, true); - - protected DnsResult(Status status) => Status = status; - - public override string ToString() => HostName; -} - -public class DnsSuccess(string hostname) : - DnsResult(hostname) -{ - -} - -public class DnsFailure(Status status, string error) : - DnsResult(status) -{ - public string ErrorMessage { get; } = error; -} - -public sealed class DnsAsync -{ - private readonly Task _cancellation; - - internal DnsAsync(Cancellation cancellation) => - _cancellation = cancellation.Task; - - internal async Task GetHostEntryAsync( + internal static async Task GetHostEntryAsync( string host, Cancellation cancellation) { - Task task = Task.WhenAny(Dns.GetHostEntryAsync(host), _cancellation); - if (task == cancellation.Task) + if (cancellation.IsCancellationRequested) { - return new DnsFailure(Status.Timeout, _cancellation.Exception.Message); + return new DnsFailure(DnsStatus.Cancelled); } - if (task.Status is not TaskStatus.RanToCompletion) + Task taskEntry = Dns.GetHostEntryAsync(host); + Task task = await Task.WhenAny(taskEntry, cancellation.Task); + + if (task == cancellation.Task) { - return new DnsFailure(Status.Error, task.Exception.Message); + return new DnsFailure(DnsStatus.Timeout); } - IPHostEntry entry = await (Task)task; - return new DnsSuccess(entry.HostName); + try + { + IPHostEntry entry = await taskEntry; + return new DnsSuccess(entry); + } + catch (Exception exception) + { + return new DnsFailure(DnsStatus.Error, exception); + } } } diff --git a/src/PSNetScanners/DnsResult.cs b/src/PSNetScanners/DnsResult.cs new file mode 100644 index 0000000..7fcec26 --- /dev/null +++ b/src/PSNetScanners/DnsResult.cs @@ -0,0 +1,48 @@ +using System; +using System.Net; + +namespace PSNetScanners; + +public enum DnsStatus +{ + Success, + Timeout, + Cancelled, + Error +} + +public abstract class DnsResult(DnsStatus status) +{ + public DnsStatus Status { get; } = status; +} + +public sealed class DnsSuccess : DnsResult +{ + public string HostName { get => _entry.HostName; } + + public IPAddress[] AddressList { get => _entry.AddressList; } + + public string[] Aliases { get => _entry.Aliases; } + + private readonly IPHostEntry _entry; + + internal DnsSuccess(IPHostEntry entry) : base(DnsStatus.Success) + { + _entry = entry; + } + + public override string ToString() => HostName; +} + +public class DnsFailure : DnsResult +{ + public Exception? Exception { get; } + + internal DnsFailure(DnsStatus status, Exception? error = null) + : base(status) + { + Exception = error; + } + + public override string ToString() => Exception?.Message ?? Status.ToString(); +} diff --git a/src/PSNetScanners/PingAsync.cs b/src/PSNetScanners/PingAsync.cs index 2323cb4..8e8b0c0 100644 --- a/src/PSNetScanners/PingAsync.cs +++ b/src/PSNetScanners/PingAsync.cs @@ -1,54 +1,74 @@ -// using System; -// using System.Collections.Generic; -// using System.Net; -// using System.Net.NetworkInformation; -// using System.Threading; -// using System.Threading.Tasks; - -// namespace PSNetScanners; - -// public sealed class PingAsync -// { -// private readonly Ping _ping; - -// private readonly PingOptions _options; - -// private readonly int _timeOut; - -// private readonly byte[] _buffer; - -// private readonly string _target; - -// internal PingAsync( -// PingOptions options, -// string target, -// int timeOut, -// byte[] buffer) -// { -// _ping = new Ping(); -// _options = options; -// _target = target; -// _timeOut = timeOut; -// _buffer = buffer; -// } - -// internal Task Run() => Task.Run(async () => -// { - -// CancellationTokenSource source = new(); -// Task myCancellationTask = Task.Run(async () => -// { -// while (true) -// { -// source.Token.ThrowIfCancellationRequested(); -// await Task.Delay(200); -// } -// }); -// Task entryTask = Dns.GetHostEntryAsync(_target); - -// await Task.WhenAny(myCancellationTask, entryTask); - -// Task pingTask = _ping.SendPingAsync(_target, _timeOut, _buffer, _options); -// Task done = await Task.WhenAny(dnsTask, pingTask); -// }); -// } \ No newline at end of file +using System.Net; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace PSNetScanners; + +public sealed class PingResult +{ + private string? _displayAddress; + + private IPAddress? _address; + + public string Source { get; } + + public string Destination { get; } + + public IPAddress? Address + { + get => _address ??= Status is IPStatus.Success ? Reply?.Address : null; + } + + public string DisplayAddress + { + get => _displayAddress ??= Address?.ToString() ?? "*"; + } + + public long? Latency { get => Reply?.RoundtripTime; } + + public IPStatus Status { get => Reply?.Status ?? IPStatus.TimedOut; } + + public DnsResult? DnsResult { get; private set; } + + public PingReply? Reply { get; private set; } + + private PingResult(string source, string target) + { + Source = source; + Destination = target; + } + + public static async Task CreateAsync( + string source, + string target, + Cancellation cancellation) + { + return new PingResult(source, target) + { + Reply = await PingAsync(target, cancellation), + DnsResult = await DnsAsync.GetHostEntryAsync(target, cancellation) + }; + } + + private static async Task PingAsync( + string target, + Cancellation cancellation) + { + if (cancellation.IsCancellationRequested) + { + return null; + } + + using Ping ping = new(); + Task pingTask = ping.SendPingAsync(target); + Task task = await Task.WhenAny(pingTask, cancellation.Task); + + if (task == cancellation.Task) + { + return null; + } + + PingReply reply = await pingTask; + return reply; + } +} diff --git a/tools/InvokeBuild.ps1 b/tools/InvokeBuild.ps1 index 4ece54d..dd81671 100644 --- a/tools/InvokeBuild.ps1 +++ b/tools/InvokeBuild.ps1 @@ -9,8 +9,10 @@ task Clean { } task BuildDocs { - $helpParams = $ProjectInfo.Documentation.GetParams() - $null = New-ExternalHelp @helpParams + if (Test-Path $ProjectInfo.Documentation.Source) { + $helpParams = $ProjectInfo.Documentation.GetParams() + $null = New-ExternalHelp @helpParams + } } task BuildManaged { From 42c86108771705b1e4cc66ccab1efe6d3125e81c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 14 Jul 2024 21:20:34 -0300 Subject: [PATCH 04/41] some progress with pingasync --- module/PSNetScanners.psd1 | 2 +- .../{CancellationTask.cs => Cancellation.cs} | 13 +- .../Commands/TestConnectionAsyncCommand.cs | 115 ++++++++++++------ src/PSNetScanners/DnsAsync.cs | 9 +- src/PSNetScanners/Enums.cs | 7 ++ src/PSNetScanners/ExceptionHelpers.cs | 35 ++++++ src/PSNetScanners/PingAsync.cs | 23 ++-- src/PSNetScanners/PingWorker.cs | 101 +++++++++++++++ src/PSNetScanners/Structs.cs | 26 ++++ 9 files changed, 274 insertions(+), 57 deletions(-) rename src/PSNetScanners/{CancellationTask.cs => Cancellation.cs} (62%) create mode 100644 src/PSNetScanners/Enums.cs create mode 100644 src/PSNetScanners/ExceptionHelpers.cs create mode 100644 src/PSNetScanners/PingWorker.cs create mode 100644 src/PSNetScanners/Structs.cs diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 index 7c06665..7f3bc23 100644 --- a/module/PSNetScanners.psd1 +++ b/module/PSNetScanners.psd1 @@ -71,7 +71,7 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @('Invoke-PingAsync') + CmdletsToExport = @('Test-ConnectionAsync') # Variables to export from this module VariablesToExport = @() diff --git a/src/PSNetScanners/CancellationTask.cs b/src/PSNetScanners/Cancellation.cs similarity index 62% rename from src/PSNetScanners/CancellationTask.cs rename to src/PSNetScanners/Cancellation.cs index e86bfd9..05df74a 100644 --- a/src/PSNetScanners/CancellationTask.cs +++ b/src/PSNetScanners/Cancellation.cs @@ -4,24 +4,23 @@ namespace PSNetScanners; -public sealed class Cancellation : IDisposable +internal sealed class Cancellation : IDisposable { private readonly CancellationTokenSource _cts; + internal CancellationToken Token { get => _cts.Token; } + internal bool IsCancellationRequested { get => _cts.IsCancellationRequested; } internal Task Task { get; } - public Cancellation(int? timeout) + internal Cancellation(int timeout) { - _cts = new CancellationTokenSource(timeout ?? -1); + _cts = new CancellationTokenSource(timeout > 0 ? timeout * 1000 : -1); Task = Task.Delay(Timeout.Infinite, _cts.Token); } - public void Cancel() => _cts.Cancel(); - - internal CancellationTokenRegistration Register(Action action) => - _cts.Token.Register(action); + internal void Cancel() => _cts.Cancel(); internal void ThrowIfCancellationRequested() => _cts.Token.ThrowIfCancellationRequested(); diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs index a0f4e7f..6e06958 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -1,64 +1,107 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System; using System.Management.Automation; -using System.Net.NetworkInformation; -using System.Threading; -using System.Threading.Tasks; namespace PSNetScanners; [Cmdlet(VerbsDiagnostic.Test, "ConnectionAsync")] -public sealed class TestConnectionAsyncCommand : PSCmdlet +public sealed class TestConnectionAsyncCommand : PSCmdlet, IDisposable { - private readonly List _tasks = new(); - - private byte[] _buffer = null!; - - private int _taskCount; - - private readonly PingOptions _pingOptions = new(); - - private readonly BlockingCollection _output = new(); - [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] public string[] Address { get; set; } = null!; [Parameter] [ValidateRange(1, int.MaxValue)] - public int BufferSize { get; set; } = 32; + public int TimeoutSeconds { get; set; } [Parameter] - [ValidateRange(1, int.MaxValue)] - public int TimeOutSeconds { get; set; } = 10; + public TimeSpan? TaskTimeout { get; set; } [Parameter] - public SwitchParameter DontFragment { get; set; } + [ValidateRange(1, 65500)] + public int BufferSize { get; set; } = 32; + + private PingWorker? _worker; protected override void BeginProcessing() { - _buffer = Enumerable.Repeat((byte)'A', BufferSize).ToArray(); - _pingOptions.DontFragment = DontFragment.IsPresent; + TaskTimeout.ValidateTimeout(this); + _worker = new PingWorker(BufferSize, TaskTimeout, TimeoutSeconds); } protected override void ProcessRecord() { - foreach (string addr in Address) + if (_worker is null) { - Ping ping = new(); - ping.SendPingAsync( - hostNameOrAddress: addr, - timeout: TimeOutSeconds * 1000, - buffer: _buffer, - options: _pingOptions); + return; + } - ping.Disposed += (sender, e) => Interlocked.Decrement(ref _taskCount); + try + { + foreach (string addr in Address) + { + _worker.Enqueue(addr); + } + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + _worker.Cancel(); + _worker.Wait(); + throw; + } + catch (OperationCanceledException exception) + { + _worker.Wait(); + exception.WriteTimeoutError(this); + } + } - ping.PingCompleted += (sender, e) => + protected override void EndProcessing() + { + if (_worker is null) + { + return; + } + + try + { + _worker.CompleteAdding(); + foreach (Output data in _worker.GetOutput()) { - _output.Add(e.Reply); - ((AutoResetEvent)e.UserState).Set(); - }; + Process(data); + } + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + _worker.Cancel(); + _worker.Wait(); + throw; + } + catch (OperationCanceledException exception) + { + _worker.Wait(); + exception.WriteTimeoutError(this); + } + } + + private void Process(Output output) + { + switch (output.Type) + { + case Type.Success: + WriteObject((PingResult)output.Data); + break; + + case Type.Error: + WriteError((ErrorRecord)output.Data); + break; } } -} \ No newline at end of file + + protected override void StopProcessing() => _worker?.Cancel(); + + public void Dispose() + { + _worker?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/PSNetScanners/DnsAsync.cs b/src/PSNetScanners/DnsAsync.cs index f260e67..7713169 100644 --- a/src/PSNetScanners/DnsAsync.cs +++ b/src/PSNetScanners/DnsAsync.cs @@ -8,17 +8,18 @@ internal static class DnsAsync { internal static async Task GetHostEntryAsync( string host, - Cancellation cancellation) + TaskOptions options) { - if (cancellation.IsCancellationRequested) + if (options.Cancellation.IsCancellationRequested) { return new DnsFailure(DnsStatus.Cancelled); } Task taskEntry = Dns.GetHostEntryAsync(host); - Task task = await Task.WhenAny(taskEntry, cancellation.Task); + Task delayTask = options.GetTimeoutDelay(); + Task task = await Task.WhenAny(taskEntry, options.CancelTask, delayTask); - if (task == cancellation.Task) + if (task == options.CancelTask || task == delayTask) { return new DnsFailure(DnsStatus.Timeout); } diff --git a/src/PSNetScanners/Enums.cs b/src/PSNetScanners/Enums.cs new file mode 100644 index 0000000..183b346 --- /dev/null +++ b/src/PSNetScanners/Enums.cs @@ -0,0 +1,7 @@ +namespace PSNetScanners; + +internal enum Type +{ + Success, + Error +} diff --git a/src/PSNetScanners/ExceptionHelpers.cs b/src/PSNetScanners/ExceptionHelpers.cs new file mode 100644 index 0000000..4c2e701 --- /dev/null +++ b/src/PSNetScanners/ExceptionHelpers.cs @@ -0,0 +1,35 @@ +using System; +using System.Management.Automation; + +namespace PSNetScanners; + +internal static class ExceptionHelpers +{ + internal static void WriteTimeoutError(this Exception exception, PSCmdlet cmdlet) => + cmdlet.WriteError(new ErrorRecord( + new TimeoutException("Timeout has been reached.", exception), + "TimeOutReached", + ErrorCategory.OperationTimeout, + cmdlet)); + + internal static ErrorRecord CreateProcessing(this Exception exception, object context) => + new(exception, errorId: "ProcessingTaskFailure", ErrorCategory.ConnectionError, context); + + internal static void WriteUnspecifiedError(this Exception exception, PSCmdlet cmdlet) => + cmdlet.WriteError(new ErrorRecord( + exception, "UnspecifiedCmdletError", ErrorCategory.NotSpecified, cmdlet)); + + internal static void ValidateTimeout(this TimeSpan? timeSpan, PSCmdlet cmdlet) + { + if (timeSpan <= TimeSpan.Zero) + { + ErrorRecord error = new( + new ArgumentOutOfRangeException("TaskTimeout must be a TimeSpan above 0."), + "InvalidTimeout", + ErrorCategory.InvalidArgument, + cmdlet); + + cmdlet.ThrowTerminatingError(error); + } + } +} diff --git a/src/PSNetScanners/PingAsync.cs b/src/PSNetScanners/PingAsync.cs index 8e8b0c0..3a0bc58 100644 --- a/src/PSNetScanners/PingAsync.cs +++ b/src/PSNetScanners/PingAsync.cs @@ -38,32 +38,37 @@ private PingResult(string source, string target) Destination = target; } - public static async Task CreateAsync( + internal static async Task CreateAsync( string source, string target, - Cancellation cancellation) + TaskOptions pingOptions) { return new PingResult(source, target) { - Reply = await PingAsync(target, cancellation), - DnsResult = await DnsAsync.GetHostEntryAsync(target, cancellation) + Reply = await PingAsync(target, pingOptions), + DnsResult = await DnsAsync.GetHostEntryAsync(target, pingOptions) }; } private static async Task PingAsync( string target, - Cancellation cancellation) + TaskOptions pingOptions) { - if (cancellation.IsCancellationRequested) + if (pingOptions.Cancellation.IsCancellationRequested) { return null; } using Ping ping = new(); - Task pingTask = ping.SendPingAsync(target); - Task task = await Task.WhenAny(pingTask, cancellation.Task); - if (task == cancellation.Task) + Task pingTask = ping.SendPingAsync( + hostNameOrAddress: target, + timeout: pingOptions.TaskTimeout, + buffer: pingOptions.Buffer); + + Task task = await Task.WhenAny(pingTask, pingOptions.CancelTask); + + if (task == pingOptions.CancelTask) { return null; } diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs new file mode 100644 index 0000000..6a51ecb --- /dev/null +++ b/src/PSNetScanners/PingWorker.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace PSNetScanners; + +internal sealed class PingWorker : IDisposable +{ + private readonly BlockingCollection _inputQueue = []; + + private readonly BlockingCollection _outputQueue = []; + + private readonly Task _worker; + + private CancellationToken Token { get => _cancellation.Token; } + + private readonly TaskOptions _options; + + private readonly Cancellation _cancellation; + + internal PingWorker( + int bufferSize, + TimeSpan? pingTimeout, + int timeoutSeconds) + { + _cancellation = new Cancellation(timeoutSeconds); + _options = new TaskOptions + { + Buffer = Encoding.ASCII.GetBytes(new string('A', bufferSize)), + Timeout = pingTimeout ?? TimeSpan.FromMilliseconds(500), + Cancellation = _cancellation + }; + + _worker = Task.Run(Start, Token); + } + + internal void Cancel() => _cancellation.Cancel(); + + internal void Wait() => _worker.GetAwaiter().GetResult(); + + internal void Enqueue(string item) => _inputQueue.Add(item, Token); + + internal void CompleteAdding() => _inputQueue.CompleteAdding(); + + internal IEnumerable GetOutput() => _outputQueue.GetConsumingEnumerable(Token); + + internal void ThrowIfCancellationRequested() => _cancellation.ThrowIfCancellationRequested(); + + private async Task Start() + { + string source = Dns.GetHostName(); + List> tasks = []; + + while (!_inputQueue.IsCompleted) + { + if (_inputQueue.TryTake(out string host, 0, Token)) + { + tasks.Add(PingResult.CreateAsync( + source: source, + target: host, + pingOptions: _options)); + } + } + + while (tasks.Count > 0) + { + Task task = await Task.WhenAny(tasks); + tasks.Remove(task); + + try + { + PingResult result = await task; + _outputQueue.Add(Output.CreateSuccess(result), Token); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + ErrorRecord error = exception.CreateProcessing(this); + _outputQueue.Add(Output.CreateError(error), Token); + } + } + + _outputQueue.CompleteAdding(); + } + + public void Dispose() + { + _inputQueue.Dispose(); + _outputQueue.Dispose(); + _cancellation.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/PSNetScanners/Structs.cs b/src/PSNetScanners/Structs.cs new file mode 100644 index 0000000..41dffde --- /dev/null +++ b/src/PSNetScanners/Structs.cs @@ -0,0 +1,26 @@ +using System; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PSNetScanners; + +internal record struct TaskOptions( + Cancellation Cancellation, + TimeSpan Timeout, + byte[] Buffer) +{ + internal readonly int TaskTimeout { get => Timeout.Milliseconds; } + + internal readonly Task GetTimeoutDelay() => Task.Delay(Timeout); + + internal readonly Task CancelTask { get => Cancellation.Task; } +} + +internal record struct Output(Type Type, object Data) +{ + internal static Output CreateSuccess(PingResult Data) => + new(Type.Success, Data); + + internal static Output CreateError(ErrorRecord error) => + new(Type.Error, error); +} From 08db1fd4703ed4076cd315110b39b01085ede14e Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 15 Jul 2024 16:08:25 -0300 Subject: [PATCH 05/41] done with pingasync --- src/PSNetScanners/Cancellation.cs | 6 +- .../Commands/TestConnectionAsyncCommand.cs | 33 ++++--- src/PSNetScanners/DnsAsync.cs | 37 -------- src/PSNetScanners/DnsResult.cs | 12 +-- src/PSNetScanners/ExceptionHelpers.cs | 14 --- src/PSNetScanners/PingAsync.cs | 79 ----------------- src/PSNetScanners/PingResult.cs | 85 +++++++++++++++++++ src/PSNetScanners/PingWorker.cs | 42 +++++---- src/PSNetScanners/Structs.cs | 7 +- 9 files changed, 138 insertions(+), 177 deletions(-) delete mode 100644 src/PSNetScanners/DnsAsync.cs delete mode 100644 src/PSNetScanners/PingAsync.cs create mode 100644 src/PSNetScanners/PingResult.cs diff --git a/src/PSNetScanners/Cancellation.cs b/src/PSNetScanners/Cancellation.cs index 05df74a..5df9b7a 100644 --- a/src/PSNetScanners/Cancellation.cs +++ b/src/PSNetScanners/Cancellation.cs @@ -14,15 +14,13 @@ internal sealed class Cancellation : IDisposable internal Task Task { get; } - internal Cancellation(int timeout) + internal Cancellation() { - _cts = new CancellationTokenSource(timeout > 0 ? timeout * 1000 : -1); + _cts = new CancellationTokenSource(); Task = Task.Delay(Timeout.Infinite, _cts.Token); } internal void Cancel() => _cts.Cancel(); - internal void ThrowIfCancellationRequested() => _cts.Token.ThrowIfCancellationRequested(); - public void Dispose() => _cts.Dispose(); } diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs index 6e06958..6a1ed73 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -4,28 +4,32 @@ namespace PSNetScanners; [Cmdlet(VerbsDiagnostic.Test, "ConnectionAsync")] +[OutputType(typeof(PingResult))] public sealed class TestConnectionAsyncCommand : PSCmdlet, IDisposable { [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] public string[] Address { get; set; } = null!; [Parameter] - [ValidateRange(1, int.MaxValue)] - public int TimeoutSeconds { get; set; } - - [Parameter] - public TimeSpan? TaskTimeout { get; set; } + [ValidateRange(200, int.MaxValue)] + public int? TaskTimeoutMilliseconds { get; set; } [Parameter] [ValidateRange(1, 65500)] public int BufferSize { get; set; } = 32; + [Parameter] + [ValidateRange(1, int.MaxValue)] + public int ThrottleLimit { get; set; } = 50; + private PingWorker? _worker; protected override void BeginProcessing() { - TaskTimeout.ValidateTimeout(this); - _worker = new PingWorker(BufferSize, TaskTimeout, TimeoutSeconds); + _worker = new PingWorker( + BufferSize, + TaskTimeoutMilliseconds, + ThrottleLimit); } protected override void ProcessRecord() @@ -41,6 +45,11 @@ protected override void ProcessRecord() { _worker.Enqueue(addr); } + + while (_worker.TryTake(out Output data)) + { + Process(data); + } } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -48,11 +57,6 @@ protected override void ProcessRecord() _worker.Wait(); throw; } - catch (OperationCanceledException exception) - { - _worker.Wait(); - exception.WriteTimeoutError(this); - } } protected override void EndProcessing() @@ -76,11 +80,6 @@ protected override void EndProcessing() _worker.Wait(); throw; } - catch (OperationCanceledException exception) - { - _worker.Wait(); - exception.WriteTimeoutError(this); - } } private void Process(Output output) diff --git a/src/PSNetScanners/DnsAsync.cs b/src/PSNetScanners/DnsAsync.cs deleted file mode 100644 index 7713169..0000000 --- a/src/PSNetScanners/DnsAsync.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; - -namespace PSNetScanners; - -internal static class DnsAsync -{ - internal static async Task GetHostEntryAsync( - string host, - TaskOptions options) - { - if (options.Cancellation.IsCancellationRequested) - { - return new DnsFailure(DnsStatus.Cancelled); - } - - Task taskEntry = Dns.GetHostEntryAsync(host); - Task delayTask = options.GetTimeoutDelay(); - Task task = await Task.WhenAny(taskEntry, options.CancelTask, delayTask); - - if (task == options.CancelTask || task == delayTask) - { - return new DnsFailure(DnsStatus.Timeout); - } - - try - { - IPHostEntry entry = await taskEntry; - return new DnsSuccess(entry); - } - catch (Exception exception) - { - return new DnsFailure(DnsStatus.Error, exception); - } - } -} diff --git a/src/PSNetScanners/DnsResult.cs b/src/PSNetScanners/DnsResult.cs index 7fcec26..8353c90 100644 --- a/src/PSNetScanners/DnsResult.cs +++ b/src/PSNetScanners/DnsResult.cs @@ -7,7 +7,6 @@ public enum DnsStatus { Success, Timeout, - Cancelled, Error } @@ -36,13 +35,16 @@ internal DnsSuccess(IPHostEntry entry) : base(DnsStatus.Success) public class DnsFailure : DnsResult { - public Exception? Exception { get; } + public Exception Exception { get; } - internal DnsFailure(DnsStatus status, Exception? error = null) + internal DnsFailure(DnsStatus status, Exception exception) : base(status) { - Exception = error; + Exception = exception; } - public override string ToString() => Exception?.Message ?? Status.ToString(); + internal static DnsFailure CreateTimeout() => + new(DnsStatus.Timeout, new TimeoutException()); + + public override string ToString() => Exception.Message; } diff --git a/src/PSNetScanners/ExceptionHelpers.cs b/src/PSNetScanners/ExceptionHelpers.cs index 4c2e701..0295258 100644 --- a/src/PSNetScanners/ExceptionHelpers.cs +++ b/src/PSNetScanners/ExceptionHelpers.cs @@ -18,18 +18,4 @@ internal static ErrorRecord CreateProcessing(this Exception exception, object co internal static void WriteUnspecifiedError(this Exception exception, PSCmdlet cmdlet) => cmdlet.WriteError(new ErrorRecord( exception, "UnspecifiedCmdletError", ErrorCategory.NotSpecified, cmdlet)); - - internal static void ValidateTimeout(this TimeSpan? timeSpan, PSCmdlet cmdlet) - { - if (timeSpan <= TimeSpan.Zero) - { - ErrorRecord error = new( - new ArgumentOutOfRangeException("TaskTimeout must be a TimeSpan above 0."), - "InvalidTimeout", - ErrorCategory.InvalidArgument, - cmdlet); - - cmdlet.ThrowTerminatingError(error); - } - } } diff --git a/src/PSNetScanners/PingAsync.cs b/src/PSNetScanners/PingAsync.cs deleted file mode 100644 index 3a0bc58..0000000 --- a/src/PSNetScanners/PingAsync.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Net; -using System.Net.NetworkInformation; -using System.Threading.Tasks; - -namespace PSNetScanners; - -public sealed class PingResult -{ - private string? _displayAddress; - - private IPAddress? _address; - - public string Source { get; } - - public string Destination { get; } - - public IPAddress? Address - { - get => _address ??= Status is IPStatus.Success ? Reply?.Address : null; - } - - public string DisplayAddress - { - get => _displayAddress ??= Address?.ToString() ?? "*"; - } - - public long? Latency { get => Reply?.RoundtripTime; } - - public IPStatus Status { get => Reply?.Status ?? IPStatus.TimedOut; } - - public DnsResult? DnsResult { get; private set; } - - public PingReply? Reply { get; private set; } - - private PingResult(string source, string target) - { - Source = source; - Destination = target; - } - - internal static async Task CreateAsync( - string source, - string target, - TaskOptions pingOptions) - { - return new PingResult(source, target) - { - Reply = await PingAsync(target, pingOptions), - DnsResult = await DnsAsync.GetHostEntryAsync(target, pingOptions) - }; - } - - private static async Task PingAsync( - string target, - TaskOptions pingOptions) - { - if (pingOptions.Cancellation.IsCancellationRequested) - { - return null; - } - - using Ping ping = new(); - - Task pingTask = ping.SendPingAsync( - hostNameOrAddress: target, - timeout: pingOptions.TaskTimeout, - buffer: pingOptions.Buffer); - - Task task = await Task.WhenAny(pingTask, pingOptions.CancelTask); - - if (task == pingOptions.CancelTask) - { - return null; - } - - PingReply reply = await pingTask; - return reply; - } -} diff --git a/src/PSNetScanners/PingResult.cs b/src/PSNetScanners/PingResult.cs new file mode 100644 index 0000000..e195cc5 --- /dev/null +++ b/src/PSNetScanners/PingResult.cs @@ -0,0 +1,85 @@ +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace PSNetScanners; + +public sealed class PingResult +{ + public string Source { get; } + + public string Destination { get; } + + public IPAddress? Address { get; } + + public string DisplayAddress { get; } + + public long Latency { get; } + + public IPStatus Status { get; } + + public DnsResult DnsResult { get; } + + public PingReply? Reply { get; } + + private PingResult( + string source, + string destination, + DnsResult dns, + PingReply? reply = null) + { + Source = source; + Destination = destination; + DnsResult = dns; + Reply = reply; + Status = reply?.Status ?? IPStatus.TimedOut; + Address = Status is IPStatus.Success ? reply?.Address : IPAddress.None; + Latency = reply?.RoundtripTime ?? 0; + DisplayAddress = Address?.ToString() ?? "*"; + } + + internal static async Task CreateAsync( + string source, + string destination, + TaskOptions options) + { + using Ping ping = new(); + Task dnsTask = Dns.GetHostEntryAsync(destination); + Task pingTask = ping.SendPingAsync( + destination, + options.TaskTimeout, + options.Buffer); + + Task result = options.TaskTimeout == 4000 + ? await Task.WhenAny(options.CancelTask, dnsTask, pingTask) + : await Task.WhenAny( + options.CancelTask, dnsTask, pingTask, + Task.Delay(options.TaskTimeout)); + + if (result != dnsTask && result != pingTask) + { + return new PingResult( + source: source, + destination: destination, + dns: DnsFailure.CreateTimeout()); + } + + DnsResult dnsResult; + try + { + IPHostEntry entry = await dnsTask; + dnsResult = new DnsSuccess(entry); + } + catch (Exception exception) + { + dnsResult = new DnsFailure(DnsStatus.Error, exception); + } + + return new PingResult( + source: source, + destination: destination, + dns: dnsResult, + reply: await pingTask); + } +} diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 6a51ecb..6e60ede 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Management.Automation; using System.Net; +using System.Net.NetworkInformation; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,17 +24,17 @@ internal sealed class PingWorker : IDisposable private readonly Cancellation _cancellation; - internal PingWorker( - int bufferSize, - TimeSpan? pingTimeout, - int timeoutSeconds) + private readonly int _throttle; + + internal PingWorker(int bufferSize, int? taskTimeout, int throttle) { - _cancellation = new Cancellation(timeoutSeconds); + _cancellation = new Cancellation(); + _throttle = throttle; _options = new TaskOptions { Buffer = Encoding.ASCII.GetBytes(new string('A', bufferSize)), - Timeout = pingTimeout ?? TimeSpan.FromMilliseconds(500), - Cancellation = _cancellation + Cancellation = _cancellation, + TaskTimeout = taskTimeout ?? 4000 }; _worker = Task.Run(Start, Token); @@ -49,7 +50,7 @@ internal PingWorker( internal IEnumerable GetOutput() => _outputQueue.GetConsumingEnumerable(Token); - internal void ThrowIfCancellationRequested() => _cancellation.ThrowIfCancellationRequested(); + internal bool TryTake(out Output result) => _outputQueue.TryTake(out result, 0, Token); private async Task Start() { @@ -62,12 +63,24 @@ private async Task Start() { tasks.Add(PingResult.CreateAsync( source: source, - target: host, - pingOptions: _options)); + destination: host, + options: _options)); + + if (tasks.Count == _throttle) + { + await ProcessOne(tasks); + } } } while (tasks.Count > 0) + { + await ProcessOne(tasks); + } + + _outputQueue.CompleteAdding(); + + async Task ProcessOne(List> tasks) { Task task = await Task.WhenAny(tasks); tasks.Remove(task); @@ -77,18 +90,17 @@ private async Task Start() PingResult result = await task; _outputQueue.Add(Output.CreateSuccess(result), Token); } - catch (OperationCanceledException) + catch (PingException exception) { - throw; + ErrorRecord error = exception.InnerException.CreateProcessing(task); + _outputQueue.Add(Output.CreateError(error), Token); } catch (Exception exception) { - ErrorRecord error = exception.CreateProcessing(this); + ErrorRecord error = exception.CreateProcessing(task); _outputQueue.Add(Output.CreateError(error), Token); } } - - _outputQueue.CompleteAdding(); } public void Dispose() diff --git a/src/PSNetScanners/Structs.cs b/src/PSNetScanners/Structs.cs index 41dffde..74ba8ef 100644 --- a/src/PSNetScanners/Structs.cs +++ b/src/PSNetScanners/Structs.cs @@ -1,4 +1,3 @@ -using System; using System.Management.Automation; using System.Threading.Tasks; @@ -6,13 +5,9 @@ namespace PSNetScanners; internal record struct TaskOptions( Cancellation Cancellation, - TimeSpan Timeout, + int TaskTimeout, byte[] Buffer) { - internal readonly int TaskTimeout { get => Timeout.Milliseconds; } - - internal readonly Task GetTimeoutDelay() => Task.Delay(Timeout); - internal readonly Task CancelTask { get => Cancellation.Task; } } From 2a0afdf06cfd48432eb20c8ba17eac2c37e8ecd9 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Tue, 16 Jul 2024 00:34:54 -0300 Subject: [PATCH 06/41] adding base classes to use in tcp command --- src/PSNetScanners/AbstractWorker.cs | 30 +++++++++++ src/PSNetScanners/AbstractWorker_T.cs | 30 +++++++++++ src/PSNetScanners/Cancellation.cs | 2 - .../Commands/TestConnectionAsyncCommand.cs | 34 ++----------- src/PSNetScanners/PSNetScannerCommandBase.cs | 34 +++++++++++++ src/PSNetScanners/PingWorker.cs | 50 ++++++------------- 6 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 src/PSNetScanners/AbstractWorker.cs create mode 100644 src/PSNetScanners/AbstractWorker_T.cs create mode 100644 src/PSNetScanners/PSNetScannerCommandBase.cs diff --git a/src/PSNetScanners/AbstractWorker.cs b/src/PSNetScanners/AbstractWorker.cs new file mode 100644 index 0000000..c620ee1 --- /dev/null +++ b/src/PSNetScanners/AbstractWorker.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PSNetScanners; + +internal abstract class WorkerBase(int throttle) : IDisposable +{ + protected abstract CancellationToken Token { get; } + + protected abstract Task Worker { get; } + + protected readonly int _throttle = throttle; + + protected bool _disposed; + + protected abstract Task Start(); + + internal abstract void Cancel(); + + internal void Wait() => Worker.GetAwaiter().GetResult(); + + protected abstract void Dispose(bool disposing); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/PSNetScanners/AbstractWorker_T.cs b/src/PSNetScanners/AbstractWorker_T.cs new file mode 100644 index 0000000..e763b06 --- /dev/null +++ b/src/PSNetScanners/AbstractWorker_T.cs @@ -0,0 +1,30 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace PSNetScanners; + +internal abstract class WorkerBase(int throttle) : WorkerBase(throttle) +{ + protected virtual BlockingCollection InputQueue { get; } = []; + + protected virtual BlockingCollection OutputQueue { get; } = []; + + internal void Enqueue(TInput item) => InputQueue.Add(item, Token); + + internal void CompleteAdding() => InputQueue.CompleteAdding(); + + internal virtual IEnumerable GetOutput() => OutputQueue.GetConsumingEnumerable(Token); + + internal bool TryTake(out TOutput result) => OutputQueue.TryTake(out result, 0, Token); + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + InputQueue.Dispose(); + OutputQueue.Dispose(); + } + + _disposed = true; + } +} diff --git a/src/PSNetScanners/Cancellation.cs b/src/PSNetScanners/Cancellation.cs index 5df9b7a..4e89599 100644 --- a/src/PSNetScanners/Cancellation.cs +++ b/src/PSNetScanners/Cancellation.cs @@ -10,8 +10,6 @@ internal sealed class Cancellation : IDisposable internal CancellationToken Token { get => _cts.Token; } - internal bool IsCancellationRequested { get => _cts.IsCancellationRequested; } - internal Task Task { get; } internal Cancellation() diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs index 6a1ed73..dceb6a6 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -5,11 +5,8 @@ namespace PSNetScanners; [Cmdlet(VerbsDiagnostic.Test, "ConnectionAsync")] [OutputType(typeof(PingResult))] -public sealed class TestConnectionAsyncCommand : PSCmdlet, IDisposable +public sealed class TestConnectionAsyncCommand : PSNetScannerCommandBase, IDisposable { - [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] - public string[] Address { get; set; } = null!; - [Parameter] [ValidateRange(200, int.MaxValue)] public int? TaskTimeoutMilliseconds { get; set; } @@ -18,11 +15,7 @@ public sealed class TestConnectionAsyncCommand : PSCmdlet, IDisposable [ValidateRange(1, 65500)] public int BufferSize { get; set; } = 32; - [Parameter] - [ValidateRange(1, int.MaxValue)] - public int ThrottleLimit { get; set; } = 50; - - private PingWorker? _worker; + internal PingWorker? _worker; protected override void BeginProcessing() { @@ -53,8 +46,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - _worker.Cancel(); - _worker.Wait(); + StopHandle(_worker); throw; } } @@ -73,31 +65,15 @@ protected override void EndProcessing() { Process(data); } + _worker.Wait(); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - _worker.Cancel(); - _worker.Wait(); + StopHandle(_worker); throw; } } - private void Process(Output output) - { - switch (output.Type) - { - case Type.Success: - WriteObject((PingResult)output.Data); - break; - - case Type.Error: - WriteError((ErrorRecord)output.Data); - break; - } - } - - protected override void StopProcessing() => _worker?.Cancel(); - public void Dispose() { _worker?.Dispose(); diff --git a/src/PSNetScanners/PSNetScannerCommandBase.cs b/src/PSNetScanners/PSNetScannerCommandBase.cs new file mode 100644 index 0000000..af87db3 --- /dev/null +++ b/src/PSNetScanners/PSNetScannerCommandBase.cs @@ -0,0 +1,34 @@ +using System; +using System.Management.Automation; + +namespace PSNetScanners; + +public abstract class PSNetScannerCommandBase : PSCmdlet +{ + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] + public string[] Address { get; set; } = null!; + + [Parameter] + [ValidateRange(1, int.MaxValue)] + public int ThrottleLimit { get; set; } = 50; + + internal static void StopHandle(WorkerBase worker) + { + worker.Cancel(); + worker.Wait(); + } + + internal void Process(Output output) + { + switch (output.Type) + { + case Type.Success: + WriteObject(output.Data); + break; + + case Type.Error: + WriteError((ErrorRecord)output.Data); + break; + } + } +} diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 6e60ede..3183c19 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Management.Automation; using System.Net; @@ -10,56 +9,39 @@ namespace PSNetScanners; -internal sealed class PingWorker : IDisposable +internal sealed class PingWorker : WorkerBase { - private readonly BlockingCollection _inputQueue = []; + protected override CancellationToken Token { get => _cancellation.Token; } - private readonly BlockingCollection _outputQueue = []; - - private readonly Task _worker; - - private CancellationToken Token { get => _cancellation.Token; } + protected override Task Worker { get; } private readonly TaskOptions _options; private readonly Cancellation _cancellation; - private readonly int _throttle; - internal PingWorker(int bufferSize, int? taskTimeout, int throttle) + : base(throttle) { _cancellation = new Cancellation(); - _throttle = throttle; _options = new TaskOptions { Buffer = Encoding.ASCII.GetBytes(new string('A', bufferSize)), Cancellation = _cancellation, TaskTimeout = taskTimeout ?? 4000 }; - - _worker = Task.Run(Start, Token); + Worker = Task.Run(Start, Token); } - internal void Cancel() => _cancellation.Cancel(); - - internal void Wait() => _worker.GetAwaiter().GetResult(); - - internal void Enqueue(string item) => _inputQueue.Add(item, Token); - - internal void CompleteAdding() => _inputQueue.CompleteAdding(); - - internal IEnumerable GetOutput() => _outputQueue.GetConsumingEnumerable(Token); - - internal bool TryTake(out Output result) => _outputQueue.TryTake(out result, 0, Token); + internal override void Cancel() => _cancellation.Cancel(); - private async Task Start() + protected override async Task Start() { string source = Dns.GetHostName(); List> tasks = []; - while (!_inputQueue.IsCompleted) + while (!InputQueue.IsCompleted) { - if (_inputQueue.TryTake(out string host, 0, Token)) + if (InputQueue.TryTake(out string host, 0, Token)) { tasks.Add(PingResult.CreateAsync( source: source, @@ -78,7 +60,7 @@ private async Task Start() await ProcessOne(tasks); } - _outputQueue.CompleteAdding(); + OutputQueue.CompleteAdding(); async Task ProcessOne(List> tasks) { @@ -88,26 +70,24 @@ async Task ProcessOne(List> tasks) try { PingResult result = await task; - _outputQueue.Add(Output.CreateSuccess(result), Token); + OutputQueue.Add(Output.CreateSuccess(result), Token); } catch (PingException exception) { ErrorRecord error = exception.InnerException.CreateProcessing(task); - _outputQueue.Add(Output.CreateError(error), Token); + OutputQueue.Add(Output.CreateError(error), Token); } catch (Exception exception) { ErrorRecord error = exception.CreateProcessing(task); - _outputQueue.Add(Output.CreateError(error), Token); + OutputQueue.Add(Output.CreateError(error), Token); } } } - public void Dispose() + protected override void Dispose(bool disposing) { - _inputQueue.Dispose(); - _outputQueue.Dispose(); + base.Dispose(disposing); _cancellation.Dispose(); - GC.SuppressFinalize(this); } } From 339639c784e207b1646a83ab4835a5dc77ad7ff1 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Tue, 16 Jul 2024 11:59:03 -0300 Subject: [PATCH 07/41] adding a few pester tests. updating readme --- .github/workflows/ci.yml | 2 +- Network-IPScanner.ps1 | 117 ----------------- Network-TCPScanner.ps1 | 109 ---------------- README.md | 12 +- Test-ICMPConnectionAsync.ps1 | 118 ------------------ Test-TCPConnectionAsync.ps1 | 113 ----------------- .../{ => Abstractions}/AbstractWorker.cs | 0 .../{ => Abstractions}/AbstractWorker_T.cs | 0 .../PSNetScannerCommandBase.cs | 8 +- .../Commands/TestConnectionAsyncCommand.cs | 4 +- tests/PingAsync.tests.ps1 | 30 +++++ tests/common.psm1 | 1 + tools/PesterTest.ps1 | 2 - 13 files changed, 51 insertions(+), 465 deletions(-) delete mode 100644 Network-IPScanner.ps1 delete mode 100644 Network-TCPScanner.ps1 delete mode 100644 Test-ICMPConnectionAsync.ps1 delete mode 100644 Test-TCPConnectionAsync.ps1 rename src/PSNetScanners/{ => Abstractions}/AbstractWorker.cs (100%) rename src/PSNetScanners/{ => Abstractions}/AbstractWorker_T.cs (100%) rename src/PSNetScanners/{ => Abstractions}/PSNetScannerCommandBase.cs (77%) create mode 100644 tests/PingAsync.tests.ps1 create mode 100644 tests/common.psm1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17fa78e..7280b99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: PSParallelPipeline Workflow +name: PSNetScanners Workflow on: push: branches: diff --git a/Network-IPScanner.ps1 b/Network-IPScanner.ps1 deleted file mode 100644 index aa6c036..0000000 --- a/Network-IPScanner.ps1 +++ /dev/null @@ -1,117 +0,0 @@ -$ErrorActionPreference = 'Stop' - -# Define List of IPs / Hosts to Ping -# $list = 'host1', 'host2', 'host3' -$list = 1..254 | ForEach-Object { - "192.168.0.$_" -} - -function Test-ICMPConnection { - [cmdletbinding(DefaultParameterSetName = 'DefaultParams')] - param( - [parameter(Mandatory, ValueFromPipeline, Position = 0)] - [string[]] $Address, - [parameter(ParameterSetName = 'DefaultParams', Position = 1)] - [int] $Count = 4, - [parameter(ParameterSetName = 'DefaultParams', Position = 2)] - [int] $TimeOut = 1000, - [parameter(ParameterSetName = 'Quiet', Position = 1)] - [switch] $Quiet, - [parameter(ParameterSetName = 'DefaultParams', Position = 3)] - [string] $Buffer = 'aaaaaaaaaa' - ) - - begin { - $ping = [System.Net.NetworkInformation.Ping]::new() - $options = [System.Net.NetworkInformation.PingOptions]::new() - $data = [System.Text.Encoding]::Unicode.GetBytes($Buffer) - $hostname = $env:COMPUTERNAME - $options.DontFragment = $true - } - process { - foreach($i in $Address) { - if($Quiet.IsPresent) { - return [bool]$ping.Send($i, $TimeOut, $data, $options).RoundtripTime - } - - $resolver = try { - [System.Net.Dns]::GetHostEntry($i).HostName - } - catch { '*' } - - 1..$Count | ForEach-Object { - $response = $ping.Send($i, $TimeOut, $data, $options) - $latency = ( - '*', [string]::Format('{0} ms', $response.RoundtripTime) - )[$response.Status -eq 'Success'] - - [pscustomobject]@{ - Ping = $_ - Source = $hostname - Address = $response.Address - Destination = $resolver - Latency = $latency - Status = $response.Status - } - } - } - } - end { - $ping.ForEach('Dispose') - } -} - -# Store the function Definition -$funcDef = ${function:Test-ICMPConnection}.ToString() -$scriptBlock = { - param([string] $ip, [string] $funcDef) - - # Load the function in this Scope - ${function:Test-ICMPConnection} = $funcDef - - # Define which arguments will be used for Pinger - # Default Values are: - # - # -Count 1 - # -TimeOut 1000 (Milliseconds) - # -Buffer 'aaaaaaaaaa' (10 bytes) - # -Quiet:$false - - Test-ICMPConnection -Address $ip -Count 1 -} - -& { - try { - # Change this value for tweaking - $Threshold = 100 - $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threshold) - $RunspacePool.Open() - - $runspaces = foreach($ip in $list) { - $params = @{ - IP = $ip - funcDef = $funcDef - } - - $psinstance = [powershell]::Create().AddScript($scriptBlock).AddParameters($params) - $psinstance.RunspacePool = $RunspacePool - - [pscustomobject]@{ - Instance = $psinstance - Handle = $psinstance.BeginInvoke() - } - } - - foreach($r in $runspaces) { - $r.Instance.EndInvoke($r.Handle) - $r.Instance.foreach('Dispose') - } - } - catch { - Write-Warning $_.Exception.Message - } - finally { - $runspaces.foreach('Clear') - $RunspacePool.foreach('Dispose') - } -} | Format-Table -AutoSize diff --git a/Network-TCPScanner.ps1 b/Network-TCPScanner.ps1 deleted file mode 100644 index d226b3d..0000000 --- a/Network-TCPScanner.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -$Threshold = 100 # => Number of threads running -[int[]] $PortsToScan = 80, 443, 125, 8080 -[string[]] $HostsToScan = 'google.com', 'cisco.com', 'amazon.com' - -function Test-TCPConnectionAsync { - [cmdletbinding()] - param( - [parameter(Mandatory, Valuefrompipeline)] - [string[]] $Target, - - [parameter(Mandatory, Position = 1)] - [ValidateRange(1, 65535)] - [int[]] $Port, - - # 1 second minimum, reasonable for TCP connection - [parameter(Position = 2)] - [ValidateRange(1000, [int]::MaxValue)] - [int] $TimeOut = 1200 - ) - - begin { - $timer = [System.Diagnostics.Stopwatch]::StartNew() - $tasks = [System.Collections.Generic.List[System.Collections.Specialized.OrderedDictionary]]::new() - } - process { - foreach($t in $Target) { - foreach($i in $Port) { - if($tasks.Count -eq 62) { - Wait-Tasks - } - - $tcp = [System.Net.Sockets.TcpClient]::new() - $tasks.Add([ordered]@{ - Instance = $tcp - Task = $tcp.ConnectAsync($t, $i) - Output = [ordered]@{ - Source = $env:COMPUTERNAME - Destination = $t - Port = $i - } - }) - } - } - } - end { - do { - $id = [System.Threading.Tasks.Task]::WaitAny($tasks.Task, 200) - if($id -eq -1) { - continue - } - $instance, $task, $output = $tasks[$id][$tasks[$id].PSBase.Keys] - $output['Success'] = $task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion - $instance.ForEach('Dispose') # Avoid any throws here - $tasks.RemoveAt($id) - [pscustomobject] $output - } while($tasks -and $timer.ElapsedMilliseconds -le $timeout) - - foreach($t in $tasks) { - $instance, $task, $output = $t[$t.PSBase.Keys] - $output['Success'] = $task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion - $instance.ForEach('Dispose') # Avoid any throws here - [pscustomobject] $output - } - } -} - -& { - try { - # Store function definition - $funcDef = ${function:Test-TCPConnectionAsync}.ToString() - $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threshold) - $RunspacePool.Open() - $scriptBlock = { - param([string] $hostname, [int[]] $ports, [string] $func) - - # Load the function in this Scope - ${function:Test-TCPConnectionAsync} = $func - Test-TCPConnectionAsync -Target $hostname -Port $ports -TimeOut 2000 - } - - $runspaces = foreach($i in $HostsToScan) { - $params = @{ - hostname = $i - ports = $PortsToScan - func = $funcDef - } - - $psinstance = [powershell]::Create().AddScript($scriptBlock).AddParameters($params) - $psinstance.RunspacePool = $RunspacePool - - [pscustomobject]@{ - Instance = $psinstance - Handle = $psinstance.BeginInvoke() - } - } - - foreach($r in $runspaces) { - $r.Instance.EndInvoke($r.Handle) - $r.Instance.foreach('Dispose') - } - } - catch { - Write-Warning $_.Exception.Message - } - finally { - $runspaces.foreach('Clear') - $RunspacePool.foreach('Dispose') - } -} diff --git a/README.md b/README.md index 90834a1..c9f58b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ -# PowerShell Network Scanners +

PSNetScanners

+
+PowerShell ICMP and TCP async scanners +

+ +[![build](https://github.com/santisq/PSNetScanners/actions/workflows/ci.yml/badge.svg)](https://github.com/santisq/PSNetScanners/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/santisq/PSNetScanners/branch/main/graph/badge.svg?token=b51IOhpLfQ)](https://codecov.io/gh/santisq/PSNetScanners) +[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/PSNetScanners?label=gallery)](https://www.powershellgallery.com/packages/PSNetScanners) +[![LICENSE](https://img.shields.io/github/license/santisq/PSNetScanners)](https://github.com/santisq/PSNetScanners/blob/main/LICENSE) + +
## DESCRIPTION diff --git a/Test-ICMPConnectionAsync.ps1 b/Test-ICMPConnectionAsync.ps1 deleted file mode 100644 index 5c12878..0000000 --- a/Test-ICMPConnectionAsync.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -using namespace System.Threading.Tasks -using namespace System.Collections.Generic -using namespace System.Net.NetworkInformation -using namespace System.Net -using namespace System.Diagnostics - -function Test-ICMPConnectionAsync { - [cmdletbinding()] - param( - [parameter(Mandatory, ValueFromPipeline, Position = 0)] - [string[]] $Address, - - [parameter(Position = 1)] - [ValidateRange(1, [int]::MaxValue)] - [int] $TimeOut = 10, # In seconds! - - [parameter(Position = 2)] - [ValidateRange(1, [int]::MaxValue)] - [int] $BufferSize = 32 - ) - - begin { - $tasks = [List[hashtable]]::new() - $timer = [Stopwatch]::StartNew() - $data = [byte[]] [char[]] 'A' * $BufferSize - $options = [PingOptions]::new() - $TimeOut = [timespan]::FromSeconds($TimeOut).TotalMilliseconds - $options.DontFragment = $true - - $outObject = { - [pscustomobject]@{ - Source = $env:COMPUTERNAME - Target = $target - Address = $response.Address.IPAddressToString - DnsName = $dnsresol - Latency = $latency - Status = $response.Status - } - } - } - process { - foreach($addr in $Address) { - $ping = [Ping]::new() - $tasks.Add(@{ - Target = $addr - Instance = $ping - PingTask = $ping.SendPingAsync($addr, $TimeOut, $data, $options) - DnsTask = [Dns]::GetHostEntryAsync($addr) - }) - } - } - end { - while($tasks -and $timer.ElapsedMilliseconds -le $timeout) { - $id = [Task]::WaitAny($tasks.PingTask, 200) - if($id -eq -1) { - continue - } - $target, $instance, $ping, $dns = $tasks[$id]['Target', 'Instance', 'PingTask', 'DnsTask'] - - try { - $response = $ping.GetAwaiter().GetResult() - $latency = [string]::Format('{0} ms', $response.RoundtripTime) - $ping.Dispose() - } - catch { - $latency = '*' - $response = @{ - Address = @{ IPAddressToString = '*' } - Status = $_.Exception.InnerException.InnerException.Message - } - } - - try { - $dnsresol = $dns.GetAwaiter().GetResult().HostName - $dns.Dispose() - } - catch { - $dnsresol = $_.Exception.InnerException.Message - } - - & $outObject - $instance.Dispose() - $tasks.RemoveAt($id) - } - - foreach($task in $tasks) { - $target, $instance, $ping, $dns = $task['Target', 'Instance', 'PingTask', 'DnsTask'] - - try { - $response = $ping.GetAwaiter().GetResult() - $latency = [string]::Format('{0} ms', $response.RoundtripTime) - $ping.Dispose() - } - catch { - $latency = '*' - $response = @{ - Address = '*' - Status = $_.Exception.InnerException.InnerException.Message - } - } - - try { - $dnsresol = $dns.GetAwaiter().GetResult().HostName - $dns.Dispose() - } - catch { - $dnsresol = $_.Exception.InnerException.Message - } - - & $outObject - $instance.Dispose() - } - } -} - -'amazon.com', 'google.com', 'facebook.com' | - Test-ICMPConnectionAsync -TimeOut 5 | - Format-Table -AutoSize \ No newline at end of file diff --git a/Test-TCPConnectionAsync.ps1 b/Test-TCPConnectionAsync.ps1 deleted file mode 100644 index 40744de..0000000 --- a/Test-TCPConnectionAsync.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -using namespace System.Diagnostics -using namespace System.Collections.Generic -using namespace System.Net.Sockets -using namespace System.Threading.Tasks - -<# -.DESCRIPTION -PowerShell Function that leverages the ConnectAsync(...) Method from the TcpClient Class to send the async TCP connection requests. - -.EXAMPLE -'google.com', 'cisco.com', 'amazon.com' | Test-TCPConnectionAsync 80, 443, 8080, 389, 636 - -.EXAMPLE -@' -Target,Port -google.com,80 -google.com,443 -google.com,8080 -google.com,389 -google.com,636 -cisco.com,80 -cisco.com,443 -cisco.com,8080 -cisco.com,389 -cisco.com,636 -amazon.com,80 -amazon.com,443 -amazon.com,8080 -amazon.com,389 -amazon.com,636 -'@ | ConvertFrom-Csv | Test-TCPConnectionAsync -#> - -function Test-TCPConnectionAsync { - [cmdletbinding()] - param( - [parameter(Mandatory, Valuefrompipeline, ValueFromPipelineByPropertyName)] - [alias('ComputerName', 'HostName', 'Host', 'Server')] - [string[]] $Target, - - [parameter(Mandatory, ValueFromPipelineByPropertyName)] - [ValidateRange(1, 65535)] - [int[]] $Port, - - [parameter()] - [ValidateRange(5, [int]::MaxValue)] - [int] $TimeOut = 5, # In seconds! - - [parameter()] - [switch] $IPv6 - ) - - begin { - $timer = [Stopwatch]::StartNew() - $queue = [List[hashtable]]::new() - $TimeOut = [timespan]::FromSeconds($TimeOut).TotalMilliseconds - if($IPv6.IsPresent) { - $newTcp = { [TCPClient]::new([AddressFamily]::InterNetworkV6) } - return - } - $newTcp = { [TCPClient]::new() } - } - process { - foreach($item in $Target) { - foreach($i in $Port) { - $tcp = & $newTcp - $queue.Add(@{ - Instance = $tcp - Task = $tcp.ConnectAsync($item, $i) - Output = [ordered]@{ - Source = $env:COMPUTERNAME - Destination = $item - Port = $i - } - }) - } - } - } - end { - while($queue -and $timer.ElapsedMilliseconds -le $timeout) { - try { - $id = [Task]::WaitAny($queue.Task, 200) - if($id -eq -1) { - continue - } - $instance, $task, $output = $queue[$id]['Instance', 'Task', 'Output'] - if($instance) { - $instance.Dispose() - } - $output['Success'] = $task.Status -eq [TaskStatus]::RanToCompletion - $queue.RemoveAt($id) - [pscustomobject] $output - } - catch { - $PSCmdlet.WriteError($_) - } - } - - foreach($item in $queue) { - try { - $instance, $task, $output = $item['Instance', 'Task', 'Output'] - $output['Success'] = $task.Status -eq [TaskStatus]::RanToCompletion - if($instance) { - $instance.Dispose() - } - [pscustomobject] $output - } - catch { - $PSCmdlet.WriteError($_) - } - } - } -} \ No newline at end of file diff --git a/src/PSNetScanners/AbstractWorker.cs b/src/PSNetScanners/Abstractions/AbstractWorker.cs similarity index 100% rename from src/PSNetScanners/AbstractWorker.cs rename to src/PSNetScanners/Abstractions/AbstractWorker.cs diff --git a/src/PSNetScanners/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs similarity index 100% rename from src/PSNetScanners/AbstractWorker_T.cs rename to src/PSNetScanners/Abstractions/AbstractWorker_T.cs diff --git a/src/PSNetScanners/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs similarity index 77% rename from src/PSNetScanners/PSNetScannerCommandBase.cs rename to src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index af87db3..fa9b027 100644 --- a/src/PSNetScanners/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -5,8 +5,12 @@ namespace PSNetScanners; public abstract class PSNetScannerCommandBase : PSCmdlet { - [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] - public string[] Address { get; set; } = null!; + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + public string[] Target { get; set; } = null!; [Parameter] [ValidateRange(1, int.MaxValue)] diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs index dceb6a6..65b0044 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs @@ -15,7 +15,7 @@ public sealed class TestConnectionAsyncCommand : PSNetScannerCommandBase, IDispo [ValidateRange(1, 65500)] public int BufferSize { get; set; } = 32; - internal PingWorker? _worker; + private PingWorker? _worker; protected override void BeginProcessing() { @@ -34,7 +34,7 @@ protected override void ProcessRecord() try { - foreach (string addr in Address) + foreach (string addr in Target) { _worker.Enqueue(addr); } diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 new file mode 100644 index 0000000..086b47b --- /dev/null +++ b/tests/PingAsync.tests.ps1 @@ -0,0 +1,30 @@ +using namespace System.IO + +$moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName +$manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) + +Import-Module $manifestPath +Import-Module ([Path]::Combine($PSScriptRoot, 'common.psm1')) + +Describe TestConnectionAsyncCommand { + Context 'Output Streams' { + It 'Success' { + Test-ConnectionAsync -Target github.com | + Should -BeOfType ([PSNetScanners.PingResult]) + } + It 'Error' { + { Test-ConnectionAsync -Target doesNotExist.com -ErrorAction Stop } | + Should -Throw + + { Test-ConnectionAsync -Target noSuchAddress -ErrorAction Stop } | + Should -Throw + } + } + Context 'DnsResult Type' { + It 'DnsSuccess' { + $result = Test-ConnectionAsync google.com + $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsSuccess]) + $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Success) + } + } +} diff --git a/tests/common.psm1 b/tests/common.psm1 new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/tests/common.psm1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/PesterTest.ps1 b/tools/PesterTest.ps1 index 1497703..5646712 100644 --- a/tools/PesterTest.ps1 +++ b/tools/PesterTest.ps1 @@ -7,8 +7,6 @@ param ( [String] $OutputFile ) -$ErrorActionPreference = 'Stop' - Get-ChildItem ([IO.Path]::Combine($PSScriptRoot, 'Modules')) -Directory | Import-Module -Name { $_.FullName } -Force -DisableNameChecking From 85e676500fd4bef92fa0e1b791ef005f871380c5 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Tue, 16 Jul 2024 14:55:33 -0300 Subject: [PATCH 08/41] adding a more pester tests. --- tests/PingAsync.tests.ps1 | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 086b47b..68915f7 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -26,5 +26,41 @@ Describe TestConnectionAsyncCommand { $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsSuccess]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Success) } + It 'DnsFailure' { + $result = Test-ConnectionAsync 127.0.0.2 + $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) + $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) + } + } + Context 'TestConnectAsyncCommand' { + It 'Parallel Pings' { + Measure-Command { + 1..254 | ForEach-Object { "127.0.0.$_" } | Test-ConnectionAsync + } | ForEach-Object TotalMinutes | Should -BeLessThan 2 + } + It 'Stops processing early' { + Measure-Command { + 1..254 | ForEach-Object { "127.0.0.$_" } | + Test-ConnectionAsync | + Select-Object -First 10 + } | ForEach-Object TotalSeconds | Should -BeLessThan 10 + } + } + Context 'Parameters' { + It 'TaskTimeoutMilliseconds' { + { 1..254 | ForEach-Object { "127.0.0.$_" } | + Test-ConnectionAsync -TaskTimeoutMilliseconds 200 -ErrorAction Stop } | + Should -Not -Throw + } + It 'ThrottleLimit' { + { 1..254 | ForEach-Object { "127.0.0.$_" } | + Test-ConnectionAsync -ThrottleLimit 300 -ErrorAction Stop } | + Should -Not -Throw + } + It 'BufferSize' { + { 1..254 | ForEach-Object { "127.0.0.$_" } | + Test-ConnectionAsync -BufferSize 1 -ErrorAction Stop } | + Should -Not -Throw + } } } From cc5118777fc7cf6d951dfcfa48ace04278d16b87 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Tue, 16 Jul 2024 18:25:32 -0300 Subject: [PATCH 09/41] adding more parameters and refactoring logic to ping async command. --- module/PSNetScanners.psd1 | 2 +- .../Abstractions/PSNetScannerCommandBase.cs | 3 +- ...syncCommand.cs => TestPingAsyncCommand.cs} | 33 +++++++-- src/PSNetScanners/PingResult.cs | 68 +++++++++++++------ src/PSNetScanners/PingWorker.cs | 17 ++--- src/PSNetScanners/Structs.cs | 13 ++-- tests/PingAsync.tests.ps1 | 4 +- 7 files changed, 90 insertions(+), 50 deletions(-) rename src/PSNetScanners/Commands/{TestConnectionAsyncCommand.cs => TestPingAsyncCommand.cs} (63%) diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 index 7f3bc23..e5b4a21 100644 --- a/module/PSNetScanners.psd1 +++ b/module/PSNetScanners.psd1 @@ -71,7 +71,7 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @('Test-ConnectionAsync') + CmdletsToExport = @('Test-PingAsync') # Variables to export from this module VariablesToExport = @() diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index fa9b027..9ab82f5 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -1,4 +1,3 @@ -using System; using System.Management.Automation; namespace PSNetScanners; @@ -10,10 +9,12 @@ public abstract class PSNetScannerCommandBase : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Alias(["Address", "Host"])] public string[] Target { get; set; } = null!; [Parameter] [ValidateRange(1, int.MaxValue)] + [Alias("ttl")] public int ThrottleLimit { get; set; } = 50; internal static void StopHandle(WorkerBase worker) diff --git a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs similarity index 63% rename from src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs rename to src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 65b0044..e42c58d 100644 --- a/src/PSNetScanners/Commands/TestConnectionAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -1,28 +1,47 @@ using System; using System.Management.Automation; +using System.Net.NetworkInformation; +using System.Text; namespace PSNetScanners; -[Cmdlet(VerbsDiagnostic.Test, "ConnectionAsync")] +[Cmdlet(VerbsDiagnostic.Test, "PingAsync")] [OutputType(typeof(PingResult))] -public sealed class TestConnectionAsyncCommand : PSNetScannerCommandBase, IDisposable +public sealed class TestPingAsyncCommand : PSNetScannerCommandBase, IDisposable { [Parameter] [ValidateRange(200, int.MaxValue)] + [Alias("ms")] public int? TaskTimeoutMilliseconds { get; set; } [Parameter] [ValidateRange(1, 65500)] + [Alias("bfs")] public int BufferSize { get; set; } = 32; + [Parameter] + public SwitchParameter ResolveDns { get; set; } + + [Parameter] + public int Ttl { get; set; } = 128; + + [Parameter] + public SwitchParameter DontFragment { get; set; } + private PingWorker? _worker; protected override void BeginProcessing() { - _worker = new PingWorker( - BufferSize, - TaskTimeoutMilliseconds, - ThrottleLimit); + PingAsyncOptions options = new() + { + PingOptions = new PingOptions(Ttl, DontFragment.IsPresent), + Buffer = Encoding.ASCII.GetBytes(new string('A', BufferSize)), + TaskTimeout = TaskTimeoutMilliseconds ?? 4000, + ThrottleLimit = ThrottleLimit, + ResolveDns = ResolveDns.IsPresent + }; + + _worker = new PingWorker(options); } protected override void ProcessRecord() @@ -74,6 +93,8 @@ protected override void EndProcessing() } } + protected override void StopProcessing() => _worker?.Cancel(); + public void Dispose() { _worker?.Dispose(); diff --git a/src/PSNetScanners/PingResult.cs b/src/PSNetScanners/PingResult.cs index e195cc5..8436198 100644 --- a/src/PSNetScanners/PingResult.cs +++ b/src/PSNetScanners/PingResult.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; @@ -19,14 +20,14 @@ public sealed class PingResult public IPStatus Status { get; } - public DnsResult DnsResult { get; } + public DnsResult? DnsResult { get; } public PingReply? Reply { get; } private PingResult( string source, string destination, - DnsResult dns, + DnsResult? dns = null, PingReply? reply = null) { Source = source; @@ -34,7 +35,7 @@ private PingResult( DnsResult = dns; Reply = reply; Status = reply?.Status ?? IPStatus.TimedOut; - Address = Status is IPStatus.Success ? reply?.Address : IPAddress.None; + Address = Status is IPStatus.Success ? reply?.Address : null; Latency = reply?.RoundtripTime ?? 0; DisplayAddress = Address?.ToString() ?? "*"; } @@ -42,20 +43,27 @@ private PingResult( internal static async Task CreateAsync( string source, string destination, - TaskOptions options) + PingAsyncOptions options, + Task cancelTask) { using Ping ping = new(); - Task dnsTask = Dns.GetHostEntryAsync(destination); Task pingTask = ping.SendPingAsync( - destination, - options.TaskTimeout, - options.Buffer); + hostNameOrAddress: destination, + timeout: options.TaskTimeout, + buffer: options.Buffer, + options: options.PingOptions); - Task result = options.TaskTimeout == 4000 - ? await Task.WhenAny(options.CancelTask, dnsTask, pingTask) - : await Task.WhenAny( - options.CancelTask, dnsTask, pingTask, - Task.Delay(options.TaskTimeout)); + if (!options.ResolveDns) + { + return new PingResult( + source: source, + destination: destination, + reply: await pingTask); + } + + Task dnsTask = Dns.GetHostEntryAsync(destination); + List tasks = [pingTask, cancelTask, dnsTask]; + Task result = await WaitOneAsync(options, tasks); if (result != dnsTask && result != pingTask) { @@ -65,21 +73,37 @@ internal static async Task CreateAsync( dns: DnsFailure.CreateTimeout()); } - DnsResult dnsResult; + return new PingResult( + source: source, + destination: destination, + dns: await GetDnsResult(dnsTask), + reply: await pingTask); + } + + private static async Task WaitOneAsync( + PingAsyncOptions options, + List tasks) + { + if (options.TaskTimeout == 4000) + { + return await Task.WhenAny(tasks); + } + + tasks.Add(Task.Delay(options.TaskTimeout)); + return await Task.WhenAny(tasks); + } + + private static async Task GetDnsResult( + Task dnsTask) + { try { IPHostEntry entry = await dnsTask; - dnsResult = new DnsSuccess(entry); + return new DnsSuccess(entry); } catch (Exception exception) { - dnsResult = new DnsFailure(DnsStatus.Error, exception); + return new DnsFailure(DnsStatus.Error, exception); } - - return new PingResult( - source: source, - destination: destination, - dns: dnsResult, - reply: await pingTask); } } diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 3183c19..20cc8b9 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -3,7 +3,6 @@ using System.Management.Automation; using System.Net; using System.Net.NetworkInformation; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -15,20 +14,15 @@ internal sealed class PingWorker : WorkerBase protected override Task Worker { get; } - private readonly TaskOptions _options; + private readonly PingAsyncOptions _options; private readonly Cancellation _cancellation; - internal PingWorker(int bufferSize, int? taskTimeout, int throttle) - : base(throttle) + internal PingWorker(PingAsyncOptions options) + : base(options.ThrottleLimit) { _cancellation = new Cancellation(); - _options = new TaskOptions - { - Buffer = Encoding.ASCII.GetBytes(new string('A', bufferSize)), - Cancellation = _cancellation, - TaskTimeout = taskTimeout ?? 4000 - }; + _options = options; Worker = Task.Run(Start, Token); } @@ -46,7 +40,8 @@ protected override async Task Start() tasks.Add(PingResult.CreateAsync( source: source, destination: host, - options: _options)); + options: _options, + cancelTask: _cancellation.Task)); if (tasks.Count == _throttle) { diff --git a/src/PSNetScanners/Structs.cs b/src/PSNetScanners/Structs.cs index 74ba8ef..024a7cb 100644 --- a/src/PSNetScanners/Structs.cs +++ b/src/PSNetScanners/Structs.cs @@ -1,15 +1,14 @@ using System.Management.Automation; -using System.Threading.Tasks; +using System.Net.NetworkInformation; namespace PSNetScanners; -internal record struct TaskOptions( - Cancellation Cancellation, +internal record struct PingAsyncOptions( + PingOptions PingOptions, + int ThrottleLimit, int TaskTimeout, - byte[] Buffer) -{ - internal readonly Task CancelTask { get => Cancellation.Task; } -} + byte[] Buffer, + bool ResolveDns); internal record struct Output(Type Type, object Data) { diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 68915f7..193f467 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -6,7 +6,7 @@ $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) Import-Module $manifestPath Import-Module ([Path]::Combine($PSScriptRoot, 'common.psm1')) -Describe TestConnectionAsyncCommand { +Describe TestPingAsyncCommand { Context 'Output Streams' { It 'Success' { Test-ConnectionAsync -Target github.com | @@ -32,7 +32,7 @@ Describe TestConnectionAsyncCommand { $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } } - Context 'TestConnectAsyncCommand' { + Context 'Test-PingAsync' { It 'Parallel Pings' { Measure-Command { 1..254 | ForEach-Object { "127.0.0.$_" } | Test-ConnectionAsync From 788a094cec7cbf5916f2d8dc4e61a59b7c803cec Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 17 Jul 2024 23:38:07 -0300 Subject: [PATCH 10/41] almost done with testtcpasync --- module/PSNetScanners.psd1 | 7 +- .../Abstractions/AbstractWorker.cs | 3 + .../Abstractions/AbstractWorker_T.cs | 14 ++- .../Abstractions/PSNetScannerCommandBase.cs | 14 ++- .../Commands/TestPingAsyncCommand.cs | 10 +- .../Commands/TestTcpAsyncCommand.cs | 91 +++++++++++++++++++ src/PSNetScanners/DnsResult.cs | 7 -- src/PSNetScanners/Enums.cs | 14 +++ src/PSNetScanners/PingWorker.cs | 47 +++++----- src/PSNetScanners/Structs.cs | 36 +++++++- src/PSNetScanners/TcpResult.cs | 70 ++++++++++++++ src/PSNetScanners/TcpWorker.cs | 73 +++++++++++++++ tests/PingAsync.tests.ps1 | 36 +++++--- 13 files changed, 364 insertions(+), 58 deletions(-) create mode 100644 src/PSNetScanners/Commands/TestTcpAsyncCommand.cs create mode 100644 src/PSNetScanners/TcpResult.cs create mode 100644 src/PSNetScanners/TcpWorker.cs diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 index e5b4a21..990d3da 100644 --- a/module/PSNetScanners.psd1 +++ b/module/PSNetScanners.psd1 @@ -62,7 +62,7 @@ # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @('PSNetScanners.Format.ps1xml') + FormatsToProcess = @('PSNetScanners.Format.ps1xml') # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() @@ -71,7 +71,10 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @('Test-PingAsync') + CmdletsToExport = @( + 'Test-PingAsync' + 'Test-TcpAsync' + ) # Variables to export from this module VariablesToExport = @() diff --git a/src/PSNetScanners/Abstractions/AbstractWorker.cs b/src/PSNetScanners/Abstractions/AbstractWorker.cs index c620ee1..2286d0e 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,8 @@ internal abstract class WorkerBase(int throttle) : IDisposable protected abstract Task Worker { get; } + internal string Source { get; } = Dns.GetHostName(); + protected readonly int _throttle = throttle; protected bool _disposed; diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index e763b06..04380c0 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -1,9 +1,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading.Tasks; namespace PSNetScanners; -internal abstract class WorkerBase(int throttle) : WorkerBase(throttle) +internal abstract class WorkerBase(int throttle) : + WorkerBase(throttle) { protected virtual BlockingCollection InputQueue { get; } = []; @@ -17,6 +19,16 @@ internal abstract class WorkerBase(int throttle) : WorkerBase(t internal bool TryTake(out TOutput result) => OutputQueue.TryTake(out result, 0, Token); + protected static async Task> WaitOne( + List> tasks) + { + Task task = await Task.WhenAny(tasks); + tasks.Remove(task); + return task; + } + + protected abstract Task ProcessTaskAsync(Task task); + protected override void Dispose(bool disposing) { if (!_disposed) diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index 9ab82f5..5a0461a 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -9,14 +9,24 @@ public abstract class PSNetScannerCommandBase : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0)] - [Alias(["Address", "Host"])] + [Alias([ + "ComputerName", + "HostName", + "Host", + "Server", + "Address"])] public string[] Target { get; set; } = null!; [Parameter] [ValidateRange(1, int.MaxValue)] - [Alias("ttl")] + [Alias("tl")] public int ThrottleLimit { get; set; } = 50; + [Parameter] + [ValidateRange(200, int.MaxValue)] + [Alias("ms")] + public int? TaskTimeoutMilliseconds { get; set; } + internal static void StopHandle(WorkerBase worker) { worker.Cancel(); diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index e42c58d..c8117b4 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -9,17 +9,13 @@ namespace PSNetScanners; [OutputType(typeof(PingResult))] public sealed class TestPingAsyncCommand : PSNetScannerCommandBase, IDisposable { - [Parameter] - [ValidateRange(200, int.MaxValue)] - [Alias("ms")] - public int? TaskTimeoutMilliseconds { get; set; } - [Parameter] [ValidateRange(1, 65500)] [Alias("bfs")] public int BufferSize { get; set; } = 32; [Parameter] + [Alias("dns")] public SwitchParameter ResolveDns { get; set; } [Parameter] @@ -53,9 +49,9 @@ protected override void ProcessRecord() try { - foreach (string addr in Target) + foreach (string address in Target) { - _worker.Enqueue(addr); + _worker.Enqueue(address); } while (_worker.TryTake(out Output data)) diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs new file mode 100644 index 0000000..88a7eaf --- /dev/null +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -0,0 +1,91 @@ +using System; +using System.Management.Automation; +using System.Net; + +namespace PSNetScanners; + + +[Cmdlet(VerbsDiagnostic.Test, "TcpAsync")] +[OutputType(typeof(TcpResult))] +public sealed class TestTcpAsyncCommand : PSNetScannerCommandBase, IDisposable +{ + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true, + Position = 1)] + [ValidateRange(IPEndPoint.MinPort, IPEndPoint.MaxPort)] + [Alias("p")] + public int[] Port { get; set; } = null!; + + private TcpWorker? _worker; + + protected override void BeginProcessing() + { + _worker = new TcpWorker( + throttle: ThrottleLimit, + timeout: TaskTimeoutMilliseconds ?? 4000); + } + + protected override void ProcessRecord() + { + if (_worker is null) + { + return; + } + + try + { + foreach (string address in Target) + { + foreach (int port in Port) + { + _worker.Enqueue(new TcpInput( + source: _worker.Source, + target: address, + port: port)); + } + } + + while (_worker.TryTake(out Output data)) + { + Process(data); + } + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + StopHandle(_worker); + throw; + } + } + + protected override void EndProcessing() + { + if (_worker is null) + { + return; + } + + try + { + _worker.CompleteAdding(); + foreach (Output data in _worker.GetOutput()) + { + Process(data); + } + _worker.Wait(); + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + StopHandle(_worker); + throw; + } + } + + protected override void StopProcessing() => _worker?.Cancel(); + + public void Dispose() + { + _worker?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/PSNetScanners/DnsResult.cs b/src/PSNetScanners/DnsResult.cs index 8353c90..0795929 100644 --- a/src/PSNetScanners/DnsResult.cs +++ b/src/PSNetScanners/DnsResult.cs @@ -3,13 +3,6 @@ namespace PSNetScanners; -public enum DnsStatus -{ - Success, - Timeout, - Error -} - public abstract class DnsResult(DnsStatus status) { public DnsStatus Status { get; } = status; diff --git a/src/PSNetScanners/Enums.cs b/src/PSNetScanners/Enums.cs index 183b346..9ab3dd9 100644 --- a/src/PSNetScanners/Enums.cs +++ b/src/PSNetScanners/Enums.cs @@ -5,3 +5,17 @@ internal enum Type Success, Error } + +public enum DnsStatus +{ + Success, + Timeout, + Error +} + +public enum TcpStatus +{ + Success, + Timeout, + Error +} diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 20cc8b9..82f8652 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Management.Automation; -using System.Net; using System.Net.NetworkInformation; using System.Threading; using System.Threading.Tasks; namespace PSNetScanners; -internal sealed class PingWorker : WorkerBase +internal sealed class PingWorker : WorkerBase { protected override CancellationToken Token { get => _cancellation.Token; } @@ -30,7 +29,6 @@ internal PingWorker(PingAsyncOptions options) protected override async Task Start() { - string source = Dns.GetHostName(); List> tasks = []; while (!InputQueue.IsCompleted) @@ -38,45 +36,44 @@ protected override async Task Start() if (InputQueue.TryTake(out string host, 0, Token)) { tasks.Add(PingResult.CreateAsync( - source: source, + source: Source, destination: host, options: _options, cancelTask: _cancellation.Task)); if (tasks.Count == _throttle) { - await ProcessOne(tasks); + Task result = await WaitOne(tasks); + await ProcessTaskAsync(result); } } } while (tasks.Count > 0) { - await ProcessOne(tasks); + Task result = await WaitOne(tasks); + await ProcessTaskAsync(result); } OutputQueue.CompleteAdding(); + } - async Task ProcessOne(List> tasks) + protected override async Task ProcessTaskAsync(Task task) + { + try { - Task task = await Task.WhenAny(tasks); - tasks.Remove(task); - - try - { - PingResult result = await task; - OutputQueue.Add(Output.CreateSuccess(result), Token); - } - catch (PingException exception) - { - ErrorRecord error = exception.InnerException.CreateProcessing(task); - OutputQueue.Add(Output.CreateError(error), Token); - } - catch (Exception exception) - { - ErrorRecord error = exception.CreateProcessing(task); - OutputQueue.Add(Output.CreateError(error), Token); - } + PingResult result = await task; + OutputQueue.Add(Output.CreateSuccess(result), Token); + } + catch (PingException exception) + { + ErrorRecord error = exception.InnerException.CreateProcessing(task); + OutputQueue.Add(Output.CreateError(error), Token); + } + catch (Exception exception) + { + ErrorRecord error = exception.CreateProcessing(task); + OutputQueue.Add(Output.CreateError(error), Token); } } diff --git a/src/PSNetScanners/Structs.cs b/src/PSNetScanners/Structs.cs index 024a7cb..56eb98d 100644 --- a/src/PSNetScanners/Structs.cs +++ b/src/PSNetScanners/Structs.cs @@ -1,5 +1,7 @@ using System.Management.Automation; +using System.Net; using System.Net.NetworkInformation; +using System.Net.Sockets; namespace PSNetScanners; @@ -12,9 +14,41 @@ internal record struct PingAsyncOptions( internal record struct Output(Type Type, object Data) { - internal static Output CreateSuccess(PingResult Data) => + internal static Output CreateSuccess(object Data) => new(Type.Success, Data); internal static Output CreateError(ErrorRecord error) => new(Type.Error, error); } + +internal readonly record struct TcpInput +{ + internal string Source { get; } + + internal string Target { get; } + + internal int Port { get; } + + internal AddressFamily AddressFamily { get; } + + internal TcpInput(string source, string target, int port) + { + Source = source; + Target = target; + Port = port; + AddressFamily = IPAddress.TryParse(target, out IPAddress ip) + ? ip.AddressFamily + : AddressFamily.InterNetwork; + } + + internal void Deconstruct( + out string source, + out string target, + out int port) + { + source = Source; + target = Target; + port = Port; + } + +} diff --git a/src/PSNetScanners/TcpResult.cs b/src/PSNetScanners/TcpResult.cs new file mode 100644 index 0000000..6195986 --- /dev/null +++ b/src/PSNetScanners/TcpResult.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace PSNetScanners; + +public sealed class TcpResult +{ + public string Source { get; } + + public string Destination { get; } + + public int Port { get; } + + public TcpStatus Status { get; } + + public Exception? Details { get; } + + private TcpResult( + TcpInput input, + TcpStatus status, + Exception? details = null) + { + (Source, Destination, Port) = input; + Status = status; + Details = details; + } + + private static TcpResult CreateSuccess(TcpInput input) => + new(input, TcpStatus.Success); + + private static TcpResult CreateTimeout(TcpInput input) => + new(input, TcpStatus.Timeout, new TimeoutException()); + + private static TcpResult CreateError(TcpInput input, Exception exception) => + new(input, TcpStatus.Error, exception); + + internal static async Task CreateAsync( + TcpInput input, + Task cancelTask, + int timeout) + { + try + { + using TcpClient tcp = new(input.AddressFamily); + Task tcpTask = tcp.ConnectAsync(input.Target, input.Port); + List tasks = [tcpTask, cancelTask]; + + if (timeout != 4000) + { + tasks.Add(Task.Delay(timeout)); + } + + Task result = await Task.WhenAny(tasks); + + if (result == tcpTask) + { + await tcpTask; + return CreateSuccess(input); + } + + return CreateTimeout(input); + } + catch (Exception exception) + { + return CreateError(input, exception); + } + } +} diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs new file mode 100644 index 0000000..d173b98 --- /dev/null +++ b/src/PSNetScanners/TcpWorker.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; + +namespace PSNetScanners; + +internal sealed class TcpWorker : WorkerBase +{ + protected override CancellationToken Token { get => _cancellation.Token; } + + protected override Task Worker { get; } + + private readonly int _timeout; + + private readonly Cancellation _cancellation; + + internal TcpWorker(int throttle, int timeout) : base(throttle) + { + _timeout = timeout; + _cancellation = new Cancellation(); + Worker = Task.Run(Start, Token); + } + + protected override async Task Start() + { + List> tasks = []; + while (!InputQueue.IsCompleted) + { + if (InputQueue.TryTake(out TcpInput input, 0, Token)) + { + tasks.Add(TcpResult.CreateAsync( + input: input, + cancelTask: _cancellation.Task, + timeout: _timeout)); + } + + if (tasks.Count == _throttle) + { + Task result = await WaitOne(tasks); + await ProcessTaskAsync(result); + } + } + + while (tasks.Count > 0) + { + Task result = await WaitOne(tasks); + await ProcessTaskAsync(result); + } + + OutputQueue.CompleteAdding(); + } + + protected override async Task ProcessTaskAsync(Task task) + { + try + { + TcpResult result = await task; + OutputQueue.Add(Output.CreateSuccess(result), Token); + } + catch (Exception exception) + { + ErrorRecord error = exception.CreateProcessing(task); + OutputQueue.Add(Output.CreateError(error), Token); + } + } + + internal override void Cancel() + { + throw new System.NotImplementedException(); + } +} diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 193f467..1cd93f6 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -9,25 +9,25 @@ Import-Module ([Path]::Combine($PSScriptRoot, 'common.psm1')) Describe TestPingAsyncCommand { Context 'Output Streams' { It 'Success' { - Test-ConnectionAsync -Target github.com | + Test-PingAsync -Target github.com | Should -BeOfType ([PSNetScanners.PingResult]) } It 'Error' { - { Test-ConnectionAsync -Target doesNotExist.com -ErrorAction Stop } | + { Test-PingAsync -Target doesNotExist.com -ErrorAction Stop } | Should -Throw - { Test-ConnectionAsync -Target noSuchAddress -ErrorAction Stop } | + { Test-PingAsync -Target noSuchAddress -ErrorAction Stop } | Should -Throw } } Context 'DnsResult Type' { It 'DnsSuccess' { - $result = Test-ConnectionAsync google.com + $result = Test-PingAsync google.com -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsSuccess]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Success) } It 'DnsFailure' { - $result = Test-ConnectionAsync 127.0.0.2 + $result = Test-PingAsync 127.0.0.2 -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } @@ -35,31 +35,41 @@ Describe TestPingAsyncCommand { Context 'Test-PingAsync' { It 'Parallel Pings' { Measure-Command { - 1..254 | ForEach-Object { "127.0.0.$_" } | Test-ConnectionAsync + 1..254 | ForEach-Object { "127.0.0.$_" } | Test-PingAsync } | ForEach-Object TotalMinutes | Should -BeLessThan 2 } It 'Stops processing early' { Measure-Command { 1..254 | ForEach-Object { "127.0.0.$_" } | - Test-ConnectionAsync | + Test-PingAsync | Select-Object -First 10 } | ForEach-Object TotalSeconds | Should -BeLessThan 10 } } Context 'Parameters' { It 'TaskTimeoutMilliseconds' { - { 1..254 | ForEach-Object { "127.0.0.$_" } | - Test-ConnectionAsync -TaskTimeoutMilliseconds 200 -ErrorAction Stop } | + { 1..20 | ForEach-Object { "127.0.0.$_" } | + Test-PingAsync -TaskTimeoutMilliseconds 200 -ErrorAction Stop } | Should -Not -Throw } It 'ThrottleLimit' { - { 1..254 | ForEach-Object { "127.0.0.$_" } | - Test-ConnectionAsync -ThrottleLimit 300 -ErrorAction Stop } | + { 1..20 | ForEach-Object { "127.0.0.$_" } | + Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop } | Should -Not -Throw } It 'BufferSize' { - { 1..254 | ForEach-Object { "127.0.0.$_" } | - Test-ConnectionAsync -BufferSize 1 -ErrorAction Stop } | + { 1..20 | ForEach-Object { "127.0.0.$_" } | + Test-PingAsync -BufferSize 1 -ErrorAction Stop } | + Should -Not -Throw + } + It 'Ttl' { + { 1..20 | ForEach-Object { "127.0.0.$_" } | + Test-PingAsync -Ttl 1 -ErrorAction Stop } | + Should -Not -Throw + } + It 'DontFragment' { + { 1..20 | ForEach-Object { "127.0.0.$_" } | + Test-PingAsync -DontFragment -ErrorAction Stop } | Should -Not -Throw } } From 2a3a9a314c512d4a59f35bddc35be611b4fbc726 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 12:26:05 -0300 Subject: [PATCH 11/41] updating parameter name --- src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs | 4 ++-- src/PSNetScanners/Commands/TestPingAsyncCommand.cs | 6 +++--- src/PSNetScanners/Commands/TestTcpAsyncCommand.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index 5a0461a..ac7c7a7 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -24,8 +24,8 @@ public abstract class PSNetScannerCommandBase : PSCmdlet [Parameter] [ValidateRange(200, int.MaxValue)] - [Alias("ms")] - public int? TaskTimeoutMilliseconds { get; set; } + [Alias(["timeout", "to", "ct"])] + public int? ConnectionTimeout { get; set; } internal static void StopHandle(WorkerBase worker) { diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index c8117b4..16c6c77 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -32,7 +32,7 @@ protected override void BeginProcessing() { PingOptions = new PingOptions(Ttl, DontFragment.IsPresent), Buffer = Encoding.ASCII.GetBytes(new string('A', BufferSize)), - TaskTimeout = TaskTimeoutMilliseconds ?? 4000, + TaskTimeout = ConnectionTimeout ?? 4000, ThrottleLimit = ThrottleLimit, ResolveDns = ResolveDns.IsPresent }; @@ -61,7 +61,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopHandle(_worker); + StopAction(_worker); throw; } } @@ -84,7 +84,7 @@ protected override void EndProcessing() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopHandle(_worker); + StopAction(_worker); throw; } } diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index 88a7eaf..e20d58e 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -23,7 +23,7 @@ protected override void BeginProcessing() { _worker = new TcpWorker( throttle: ThrottleLimit, - timeout: TaskTimeoutMilliseconds ?? 4000); + timeout: ConnectionTimeout ?? 4000); } protected override void ProcessRecord() From dbf636015c2676c528aabdd4b3b7e79d5e6e05f9 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 12:27:00 -0300 Subject: [PATCH 12/41] updating parameter name --- src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs | 2 +- src/PSNetScanners/Commands/TestPingAsyncCommand.cs | 4 ++-- src/PSNetScanners/Commands/TestTcpAsyncCommand.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index ac7c7a7..dfac458 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -27,7 +27,7 @@ public abstract class PSNetScannerCommandBase : PSCmdlet [Alias(["timeout", "to", "ct"])] public int? ConnectionTimeout { get; set; } - internal static void StopHandle(WorkerBase worker) + internal static void StopWorker(WorkerBase worker) { worker.Cancel(); worker.Wait(); diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 16c6c77..7d9c843 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -61,7 +61,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopAction(_worker); + StopWorker(_worker); throw; } } @@ -84,7 +84,7 @@ protected override void EndProcessing() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopAction(_worker); + StopWorker(_worker); throw; } } diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index e20d58e..df2a4d9 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -53,7 +53,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopHandle(_worker); + StopWorker(_worker); throw; } } @@ -76,7 +76,7 @@ protected override void EndProcessing() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopHandle(_worker); + StopWorker(_worker); throw; } } From c66c84d935e3426c71c76361ba992ddd2cb4649c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 13:59:41 -0300 Subject: [PATCH 13/41] tidying up --- .../Abstractions/AbstractWorker.cs | 13 +++-- .../Abstractions/AbstractWorker_T.cs | 4 +- .../Abstractions/PSNetScannerCommandBase.cs | 6 --- .../Commands/TestPingAsyncCommand.cs | 4 +- .../Commands/TestTcpAsyncCommand.cs | 4 +- src/PSNetScanners/PingWorker.cs | 9 +--- src/PSNetScanners/TcpWorker.cs | 13 +---- tests/PingAsync.tests.ps1 | 51 ++++++++++++------- tests/common.psm1 | 6 ++- 9 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/PSNetScanners/Abstractions/AbstractWorker.cs b/src/PSNetScanners/Abstractions/AbstractWorker.cs index 2286d0e..79fa0a5 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker.cs @@ -5,21 +5,28 @@ namespace PSNetScanners; -internal abstract class WorkerBase(int throttle) : IDisposable +internal abstract class WorkerBase(int throttle, Cancellation cancellation) + : IDisposable { - protected abstract CancellationToken Token { get; } + protected CancellationToken Token { get => _cancellation.Token; } protected abstract Task Worker { get; } internal string Source { get; } = Dns.GetHostName(); + protected readonly Cancellation _cancellation = cancellation; + protected readonly int _throttle = throttle; protected bool _disposed; protected abstract Task Start(); - internal abstract void Cancel(); + internal void Cancel() + { + _cancellation.Cancel(); + Wait(); + } internal void Wait() => Worker.GetAwaiter().GetResult(); diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index 04380c0..528acc0 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -4,8 +4,8 @@ namespace PSNetScanners; -internal abstract class WorkerBase(int throttle) : - WorkerBase(throttle) +internal abstract class WorkerBase(int throttle, Cancellation cancellation) : + WorkerBase(throttle, cancellation) { protected virtual BlockingCollection InputQueue { get; } = []; diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index dfac458..54081e0 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -27,12 +27,6 @@ public abstract class PSNetScannerCommandBase : PSCmdlet [Alias(["timeout", "to", "ct"])] public int? ConnectionTimeout { get; set; } - internal static void StopWorker(WorkerBase worker) - { - worker.Cancel(); - worker.Wait(); - } - internal void Process(Output output) { switch (output.Type) diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 7d9c843..2899aa7 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -61,7 +61,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopWorker(_worker); + _worker.Cancel(); throw; } } @@ -84,7 +84,7 @@ protected override void EndProcessing() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopWorker(_worker); + _worker.Cancel(); throw; } } diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index df2a4d9..6df09ca 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -53,7 +53,7 @@ protected override void ProcessRecord() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopWorker(_worker); + _worker.Cancel(); throw; } } @@ -76,7 +76,7 @@ protected override void EndProcessing() } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { - StopWorker(_worker); + _worker.Cancel(); throw; } } diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 82f8652..5cbb17d 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -9,24 +9,17 @@ namespace PSNetScanners; internal sealed class PingWorker : WorkerBase { - protected override CancellationToken Token { get => _cancellation.Token; } - protected override Task Worker { get; } private readonly PingAsyncOptions _options; - private readonly Cancellation _cancellation; - internal PingWorker(PingAsyncOptions options) - : base(options.ThrottleLimit) + : base(options.ThrottleLimit, new Cancellation()) { - _cancellation = new Cancellation(); _options = options; Worker = Task.Run(Start, Token); } - internal override void Cancel() => _cancellation.Cancel(); - protected override async Task Start() { List> tasks = []; diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index d173b98..6a97da6 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -8,18 +8,14 @@ namespace PSNetScanners; internal sealed class TcpWorker : WorkerBase { - protected override CancellationToken Token { get => _cancellation.Token; } - protected override Task Worker { get; } private readonly int _timeout; - private readonly Cancellation _cancellation; - - internal TcpWorker(int throttle, int timeout) : base(throttle) + internal TcpWorker(int throttle, int timeout) + : base(throttle, new Cancellation()) { _timeout = timeout; - _cancellation = new Cancellation(); Worker = Task.Run(Start, Token); } @@ -65,9 +61,4 @@ protected override async Task ProcessTaskAsync(Task task) OutputQueue.Add(Output.CreateError(error), Token); } } - - internal override void Cancel() - { - throw new System.NotImplementedException(); - } } diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 1cd93f6..538e106 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -12,6 +12,7 @@ Describe TestPingAsyncCommand { Test-PingAsync -Target github.com | Should -BeOfType ([PSNetScanners.PingResult]) } + It 'Error' { { Test-PingAsync -Target doesNotExist.com -ErrorAction Stop } | Should -Throw @@ -20,56 +21,68 @@ Describe TestPingAsyncCommand { Should -Throw } } + Context 'DnsResult Type' { It 'DnsSuccess' { $result = Test-PingAsync google.com -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsSuccess]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Success) } + It 'DnsFailure' { $result = Test-PingAsync 127.0.0.2 -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } } + Context 'Test-PingAsync' { + BeforeAll { + $range = makeiprange 127.0.0 1 255 + $range | Out-Null + } + It 'Parallel Pings' { - Measure-Command { - 1..254 | ForEach-Object { "127.0.0.$_" } | Test-PingAsync - } | ForEach-Object TotalMinutes | Should -BeLessThan 2 + Measure-Command { $range | Test-PingAsync } | + ForEach-Object TotalMinutes | + Should -BeLessThan 2 } + It 'Stops processing early' { - Measure-Command { - 1..254 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync | - Select-Object -First 10 - } | ForEach-Object TotalSeconds | Should -BeLessThan 10 + Measure-Command { $range | Test-PingAsync | Select-Object -First 10 } | + ForEach-Object TotalSeconds | + Should -BeLessThan 10 } } + Context 'Parameters' { - It 'TaskTimeoutMilliseconds' { - { 1..20 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync -TaskTimeoutMilliseconds 200 -ErrorAction Stop } | + BeforeAll { + $range = makeiprange 127.0.0 1 20 + $range | Out-Null + } + + It 'ConnectionTimeout' { + { $range | Test-PingAsync -ConnectionTimeout 200 -ErrorAction Stop } | Should -Not -Throw } + It 'ThrottleLimit' { - { 1..20 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop } | + { $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop } | Should -Not -Throw } + It 'BufferSize' { - { 1..20 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync -BufferSize 1 -ErrorAction Stop } | + { $range | Test-PingAsync -BufferSize 1 -ErrorAction Stop } | Should -Not -Throw } + It 'Ttl' { - { 1..20 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync -Ttl 1 -ErrorAction Stop } | + { $range | Test-PingAsync -Ttl 1 -ErrorAction Stop } | Should -Not -Throw } + It 'DontFragment' { - { 1..20 | ForEach-Object { "127.0.0.$_" } | - Test-PingAsync -DontFragment -ErrorAction Stop } | + { $range | Test-PingAsync -DontFragment -ErrorAction Stop } | Should -Not -Throw } } diff --git a/tests/common.psm1 b/tests/common.psm1 index 5f28270..abbafa6 100644 --- a/tests/common.psm1 +++ b/tests/common.psm1 @@ -1 +1,5 @@ - \ No newline at end of file +function makeiprange { + param([string] $ip, [int] $start, [int] $end) + + $start..$end | ForEach-Object { "$ip.$_" } +} From 991314bbbe6c3843de5133a7199f14b28164335e Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 15:30:33 -0300 Subject: [PATCH 14/41] setcap cap_net_raw=eip for linux worker --- tests/PingAsync.tests.ps1 | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 538e106..f705cc5 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -1,5 +1,9 @@ using namespace System.IO +if ($IsLinux) { + sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh +} + $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) @@ -15,10 +19,10 @@ Describe TestPingAsyncCommand { It 'Error' { { Test-PingAsync -Target doesNotExist.com -ErrorAction Stop } | - Should -Throw + Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) { Test-PingAsync -Target noSuchAddress -ErrorAction Stop } | - Should -Throw + Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) } } @@ -62,28 +66,28 @@ Describe TestPingAsyncCommand { } It 'ConnectionTimeout' { - { $range | Test-PingAsync -ConnectionTimeout 200 -ErrorAction Stop } | - Should -Not -Throw + $range | Test-PingAsync -ConnectionTimeout 200 -ErrorAction Stop | + Should -HaveCount 20 } It 'ThrottleLimit' { - { $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop } | - Should -Not -Throw + $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop | + Should -HaveCount 20 } It 'BufferSize' { - { $range | Test-PingAsync -BufferSize 1 -ErrorAction Stop } | - Should -Not -Throw + $range | Test-PingAsync -BufferSize 1 -ErrorAction Stop | + Should -HaveCount 20 } It 'Ttl' { - { $range | Test-PingAsync -Ttl 1 -ErrorAction Stop } | - Should -Not -Throw + $range | Test-PingAsync -Ttl 1 -ErrorAction Stop | + Should -HaveCount 20 } It 'DontFragment' { - { $range | Test-PingAsync -DontFragment -ErrorAction Stop } | - Should -Not -Throw + $range | Test-PingAsync -DontFragment -ErrorAction Stop | + Should -HaveCount 20 } } } From 044e0b1b58f6fd17eeb4010b5c6ffb5fcd06ba10 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 15:53:43 -0300 Subject: [PATCH 15/41] damnit linux --- .github/workflows/ci.yml | 11 ++++++++--- src/PSNetScanners/Abstractions/AbstractWorker_T.cs | 2 +- src/PSNetScanners/PingWorker.cs | 5 ++--- src/PSNetScanners/TcpWorker.cs | 5 ++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7280b99..6c90c44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,16 +87,21 @@ jobs: Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop - - name: Run Tests - Windows PowerShell + - name: Run Tests - PowerShell 5.1 if: ${{ matrix.info.psversion == '5.1' }} shell: powershell run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test - - name: Run Tests - PowerShell - if: ${{ matrix.info.psversion != '5.1' }} + - name: Run Tests - PowerShell 7 Windows + if: ${{ matrix.info.name == 'PS-7_Windows' }} shell: pwsh run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test + - name: Run Tests - PowerShell 7 Linux + if: ${{ matrix.info.name == 'PS-7_Linux' }} + shell: pwsh + run: sudo ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test + - name: Upload Test Results if: always() uses: actions/upload-artifact@v4 diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index 528acc0..50f7272 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -19,7 +19,7 @@ internal abstract class WorkerBase(int throttle, Cance internal bool TryTake(out TOutput result) => OutputQueue.TryTake(out result, 0, Token); - protected static async Task> WaitOne( + protected static async Task> WaitOneAsync( List> tasks) { Task task = await Task.WhenAny(tasks); diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 5cbb17d..d1c3807 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Management.Automation; using System.Net.NetworkInformation; -using System.Threading; using System.Threading.Tasks; namespace PSNetScanners; @@ -36,7 +35,7 @@ protected override async Task Start() if (tasks.Count == _throttle) { - Task result = await WaitOne(tasks); + Task result = await WaitOneAsync(tasks); await ProcessTaskAsync(result); } } @@ -44,7 +43,7 @@ protected override async Task Start() while (tasks.Count > 0) { - Task result = await WaitOne(tasks); + Task result = await WaitOneAsync(tasks); await ProcessTaskAsync(result); } diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index 6a97da6..c0eb487 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Management.Automation; -using System.Threading; using System.Threading.Tasks; namespace PSNetScanners; @@ -34,14 +33,14 @@ protected override async Task Start() if (tasks.Count == _throttle) { - Task result = await WaitOne(tasks); + Task result = await WaitOneAsync(tasks); await ProcessTaskAsync(result); } } while (tasks.Count > 0) { - Task result = await WaitOne(tasks); + Task result = await WaitOneAsync(tasks); await ProcessTaskAsync(result); } From b36e3101fdb443673c618b13f6397a4083b97f93 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 18:12:43 -0300 Subject: [PATCH 16/41] damnit linux --- .github/workflows/ci.yml | 1 - build.ps1 | 4 ++++ tests/PingAsync.tests.ps1 | 31 +++++++++++++++++++++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c90c44..f1055e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,6 @@ jobs: } Get-ChildItem output/*.nupkg | Rename-Item -NewName { $_.Name -replace '.nupkg', '.zip' } - Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop - name: Run Tests - PowerShell 5.1 diff --git a/build.ps1 b/build.ps1 index 650b40b..5d2a70e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -38,6 +38,10 @@ if (-not ('ProjectBuilder.ProjectInfo' -as [type])) { } } +if ($IsLinux) { + sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh +} + $projectInfo = [ProjectBuilder.ProjectInfo]::Create($PSScriptRoot, $Configuration) $projectInfo.GetRequirements() | Import-Module -DisableNameChecking -Force diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index f705cc5..e914a7b 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -1,9 +1,5 @@ using namespace System.IO -if ($IsLinux) { - sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh -} - $moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName $manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) @@ -40,6 +36,30 @@ Describe TestPingAsyncCommand { } } + Context 'PingResult Type' { + BeforeAll { + $ping = Test-PingAsync google.com + $ping | Out-Null + } + + It 'Source' { + $ping.Source | Should -BeNullOrEmpty + $ping.Source | Should -BeOfType ([string]) + } + + It 'Destination' { + $ping.Destination | Should -BeNullOrEmpty + $ping.Destination | Should -BeOfType ([string]) + } + + It 'DisplayAddress' { + $ping.DisplayAddress | Should -BeNullOrEmpty + $ping.DisplayAddress | Should -BeOfType ([string]) + } + + + } + Context 'Test-PingAsync' { BeforeAll { $range = makeiprange 127.0.0 1 255 @@ -68,6 +88,9 @@ Describe TestPingAsyncCommand { It 'ConnectionTimeout' { $range | Test-PingAsync -ConnectionTimeout 200 -ErrorAction Stop | Should -HaveCount 20 + + $range | Test-PingAsync -ConnectionTimeout 200 -ResolveDns -ErrorAction Stop | + Should -HaveCount 20 } It 'ThrottleLimit' { From 31970d64210be53c5a97feffb5c01272d7d0eb50 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 18:21:27 -0300 Subject: [PATCH 17/41] damnit linux --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1055e9..003db0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,11 @@ jobs: shell: powershell run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test + - name: setcap + if: ${{ matrix.info.name == 'PS-7_Linux' }} + shell: pwsh + run: sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh + - name: Run Tests - PowerShell 7 Windows if: ${{ matrix.info.name == 'PS-7_Windows' }} shell: pwsh From 858f265dba0e83ad27636e61be1fbd44b7b91201 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 18:25:37 -0300 Subject: [PATCH 18/41] damnit linux --- .github/workflows/ci.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 003db0f..b44c224 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,16 +96,11 @@ jobs: shell: pwsh run: sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh - - name: Run Tests - PowerShell 7 Windows - if: ${{ matrix.info.name == 'PS-7_Windows' }} + - name: Run Tests - PowerShell 7 + if: ${{ matrix.info.psversion != '5.1' }} shell: pwsh run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test - - name: Run Tests - PowerShell 7 Linux - if: ${{ matrix.info.name == 'PS-7_Linux' }} - shell: pwsh - run: sudo ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test - - name: Upload Test Results if: always() uses: actions/upload-artifact@v4 From 5342b194e16151060c5e44ef498428dfa529750a Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 18:43:04 -0300 Subject: [PATCH 19/41] ok we're good now :) --- build.ps1 | 4 ---- tests/PingAsync.tests.ps1 | 13 +++++-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/build.ps1 b/build.ps1 index 5d2a70e..650b40b 100644 --- a/build.ps1 +++ b/build.ps1 @@ -38,10 +38,6 @@ if (-not ('ProjectBuilder.ProjectInfo' -as [type])) { } } -if ($IsLinux) { - sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh -} - $projectInfo = [ProjectBuilder.ProjectInfo]::Create($PSScriptRoot, $Configuration) $projectInfo.GetRequirements() | Import-Module -DisableNameChecking -Force diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index e914a7b..a8ab439 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -14,10 +14,7 @@ Describe TestPingAsyncCommand { } It 'Error' { - { Test-PingAsync -Target doesNotExist.com -ErrorAction Stop } | - Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) - - { Test-PingAsync -Target noSuchAddress -ErrorAction Stop } | + { Test-PingAsync -Target ([guid]::NewGuid()) -ErrorAction Stop } | Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) } } @@ -30,7 +27,7 @@ Describe TestPingAsyncCommand { } It 'DnsFailure' { - $result = Test-PingAsync 127.0.0.2 -ResolveDns + $result = Test-PingAsync 255.255.255.255 -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } @@ -43,17 +40,17 @@ Describe TestPingAsyncCommand { } It 'Source' { - $ping.Source | Should -BeNullOrEmpty + $ping.Source | Should -Not -BeNullOrEmpty $ping.Source | Should -BeOfType ([string]) } It 'Destination' { - $ping.Destination | Should -BeNullOrEmpty + $ping.Destination | Should -Not -BeNullOrEmpty $ping.Destination | Should -BeOfType ([string]) } It 'DisplayAddress' { - $ping.DisplayAddress | Should -BeNullOrEmpty + $ping.DisplayAddress | Should -Not -BeNullOrEmpty $ping.DisplayAddress | Should -BeOfType ([string]) } From 7fa7deedce934256d8206379551fa7ae3a79b7dc Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 22:56:02 -0300 Subject: [PATCH 20/41] ok we're good now :) --- tests/PingAsync.tests.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index a8ab439..e679ea6 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -27,7 +27,7 @@ Describe TestPingAsyncCommand { } It 'DnsFailure' { - $result = Test-PingAsync 255.255.255.255 -ResolveDns + $result = Test-PingAsync 127.0.0.255 -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } @@ -53,8 +53,6 @@ Describe TestPingAsyncCommand { $ping.DisplayAddress | Should -Not -BeNullOrEmpty $ping.DisplayAddress | Should -BeOfType ([string]) } - - } Context 'Test-PingAsync' { From 0987a61cafa3705ffc1348f6164d38cf435728b1 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 23:22:31 -0300 Subject: [PATCH 21/41] ok we're good now :) --- tests/PingAsync.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index e679ea6..ccfa129 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -14,7 +14,7 @@ Describe TestPingAsyncCommand { } It 'Error' { - { Test-PingAsync -Target ([guid]::NewGuid()) -ErrorAction Stop } | + { Test-PingAsync -Target 192.0.2.10 -ErrorAction Stop } | Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) } } From 5105fb8c3f0129c099e6c6cf3ca07a74ba206849 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 23:28:44 -0300 Subject: [PATCH 22/41] ok we're good now :) --- tests/PingAsync.tests.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index ccfa129..583bbde 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -14,7 +14,7 @@ Describe TestPingAsyncCommand { } It 'Error' { - { Test-PingAsync -Target 192.0.2.10 -ErrorAction Stop } | + { Test-PingAsync -Target "$([guid]::NewGuid()).com" -ErrorAction Stop } | Should -Throw -ExceptionType ([System.Net.Sockets.SocketException]) } } @@ -27,7 +27,10 @@ Describe TestPingAsyncCommand { } It 'DnsFailure' { - $result = Test-PingAsync 127.0.0.255 -ResolveDns + $result = makeiprange 127.0.0 1 255 | + Test-PingAsync -ResolveDns | + Where-Object { $_.DnsResult.Status -eq [PSNetScanners.DnsStatus]::Error } | + Select-Object -First 1 $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsFailure]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Error) } From 544deec03e774d564462ff2e5f46d2806498eab8 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 23:33:31 -0300 Subject: [PATCH 23/41] ok we're good now :) --- tests/PingAsync.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 583bbde..f9cbcc3 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -27,7 +27,7 @@ Describe TestPingAsyncCommand { } It 'DnsFailure' { - $result = makeiprange 127.0.0 1 255 | + $result = makeiprange 10.0.0 1 255 | Test-PingAsync -ResolveDns | Where-Object { $_.DnsResult.Status -eq [PSNetScanners.DnsStatus]::Error } | Select-Object -First 1 From 019ecc7e8668b121db51c80630cd3b3a1412f805 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Jul 2024 23:55:53 -0300 Subject: [PATCH 24/41] ok we're good now :) --- .../Abstractions/AbstractWorker_T.cs | 9 +- .../Commands/TestPingAsyncCommand.cs | 10 +- .../Commands/TestTcpAsyncCommand.cs | 10 +- src/PSNetScanners/Dbg/Dbg.cs | 11 ++ src/PSNetScanners/Dbg/Nullable.cs | 150 ++++++++++++++++++ src/PSNetScanners/ExceptionHelpers.cs | 11 -- src/PSNetScanners/PingWorker.cs | 6 +- src/PSNetScanners/TcpWorker.cs | 6 +- 8 files changed, 174 insertions(+), 39 deletions(-) create mode 100644 src/PSNetScanners/Dbg/Dbg.cs create mode 100644 src/PSNetScanners/Dbg/Nullable.cs diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index 50f7272..54d3a5c 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -1,11 +1,12 @@ +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; namespace PSNetScanners; -internal abstract class WorkerBase(int throttle, Cancellation cancellation) : - WorkerBase(throttle, cancellation) +internal abstract class WorkerBase(int throttle, Cancellation cancellation) + : WorkerBase(throttle, cancellation) { protected virtual BlockingCollection InputQueue { get; } = []; @@ -19,12 +20,12 @@ internal abstract class WorkerBase(int throttle, Cance internal bool TryTake(out TOutput result) => OutputQueue.TryTake(out result, 0, Token); - protected static async Task> WaitOneAsync( + protected async Task ProcessOneAsync( List> tasks) { Task task = await Task.WhenAny(tasks); tasks.Remove(task); - return task; + await ProcessTaskAsync(task); } protected abstract Task ProcessTaskAsync(Task task); diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 2899aa7..577f094 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -42,10 +42,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - if (_worker is null) - { - return; - } + Dbg.Assert(_worker is not null); try { @@ -68,10 +65,7 @@ protected override void ProcessRecord() protected override void EndProcessing() { - if (_worker is null) - { - return; - } + Dbg.Assert(_worker is not null); try { diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index 6df09ca..e74dd6a 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -28,10 +28,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - if (_worker is null) - { - return; - } + Dbg.Assert(_worker is not null); try { @@ -60,10 +57,7 @@ protected override void ProcessRecord() protected override void EndProcessing() { - if (_worker is null) - { - return; - } + Dbg.Assert(_worker is not null); try { diff --git a/src/PSNetScanners/Dbg/Dbg.cs b/src/PSNetScanners/Dbg/Dbg.cs new file mode 100644 index 0000000..9bd540d --- /dev/null +++ b/src/PSNetScanners/Dbg/Dbg.cs @@ -0,0 +1,11 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace PSNetScanners; + +internal static class Dbg +{ + [Conditional("DEBUG")] + public static void Assert([DoesNotReturnIf(false)] bool condition) => + Debug.Assert(condition); +} diff --git a/src/PSNetScanners/Dbg/Nullable.cs b/src/PSNetScanners/Dbg/Nullable.cs new file mode 100644 index 0000000..ad74d3b --- /dev/null +++ b/src/PSNetScanners/Dbg/Nullable.cs @@ -0,0 +1,150 @@ +#if !NETCOREAPP + +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies that null is allowed as an input even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class AllowNullAttribute : Attribute { } + +/// Specifies that null is disallowed as an input even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DisallowNullAttribute : Attribute { } + +/// Specifies that an output may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullAttribute : Attribute { } + +/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullAttribute : Attribute { } + +/// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that the output will be non-null if the named parameter is non-null. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } +} + +/// Applied to a method that will never return under any circumstance. +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnAttribute : Attribute { } + +/// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } +} + +#endif diff --git a/src/PSNetScanners/ExceptionHelpers.cs b/src/PSNetScanners/ExceptionHelpers.cs index 0295258..6ee189d 100644 --- a/src/PSNetScanners/ExceptionHelpers.cs +++ b/src/PSNetScanners/ExceptionHelpers.cs @@ -5,17 +5,6 @@ namespace PSNetScanners; internal static class ExceptionHelpers { - internal static void WriteTimeoutError(this Exception exception, PSCmdlet cmdlet) => - cmdlet.WriteError(new ErrorRecord( - new TimeoutException("Timeout has been reached.", exception), - "TimeOutReached", - ErrorCategory.OperationTimeout, - cmdlet)); - internal static ErrorRecord CreateProcessing(this Exception exception, object context) => new(exception, errorId: "ProcessingTaskFailure", ErrorCategory.ConnectionError, context); - - internal static void WriteUnspecifiedError(this Exception exception, PSCmdlet cmdlet) => - cmdlet.WriteError(new ErrorRecord( - exception, "UnspecifiedCmdletError", ErrorCategory.NotSpecified, cmdlet)); } diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index d1c3807..dd8abb6 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -35,16 +35,14 @@ protected override async Task Start() if (tasks.Count == _throttle) { - Task result = await WaitOneAsync(tasks); - await ProcessTaskAsync(result); + await ProcessOneAsync(tasks); } } } while (tasks.Count > 0) { - Task result = await WaitOneAsync(tasks); - await ProcessTaskAsync(result); + await ProcessOneAsync(tasks); } OutputQueue.CompleteAdding(); diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index c0eb487..cbb3b7a 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -33,15 +33,13 @@ protected override async Task Start() if (tasks.Count == _throttle) { - Task result = await WaitOneAsync(tasks); - await ProcessTaskAsync(result); + await ProcessOneAsync(tasks); } } while (tasks.Count > 0) { - Task result = await WaitOneAsync(tasks); - await ProcessTaskAsync(result); + await ProcessOneAsync(tasks); } OutputQueue.CompleteAdding(); From 212040e630f1af06ebde970de561d702403cbd8e Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 19 Jul 2024 00:22:54 -0300 Subject: [PATCH 25/41] adding tcp tests --- module/PSNetScanners.psd1 | 5 +- .../Commands/TestPingAsyncCommand.cs | 1 + .../Commands/TestTcpAsyncCommand.cs | 2 +- tests/TcpAsync.tests.ps1 | 81 +++++++++++++++++++ tests/common.psm1 | 23 ++++++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/TcpAsync.tests.ps1 diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1 index 990d3da..a4453af 100644 --- a/module/PSNetScanners.psd1 +++ b/module/PSNetScanners.psd1 @@ -80,7 +80,10 @@ VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() + AliasesToExport = @( + 'pingasync' + 'tcpasync' + ) # DSC resources to export from this module # DscResourcesToExport = @() diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 577f094..13c0dbf 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -7,6 +7,7 @@ namespace PSNetScanners; [Cmdlet(VerbsDiagnostic.Test, "PingAsync")] [OutputType(typeof(PingResult))] +[Alias("pingasync")] public sealed class TestPingAsyncCommand : PSNetScannerCommandBase, IDisposable { [Parameter] diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index e74dd6a..bb0febc 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -4,9 +4,9 @@ namespace PSNetScanners; - [Cmdlet(VerbsDiagnostic.Test, "TcpAsync")] [OutputType(typeof(TcpResult))] +[Alias("tcpasync")] public sealed class TestTcpAsyncCommand : PSNetScannerCommandBase, IDisposable { [Parameter( diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 new file mode 100644 index 0000000..0bdbacb --- /dev/null +++ b/tests/TcpAsync.tests.ps1 @@ -0,0 +1,81 @@ +using namespace System.IO + +$moduleName = (Get-Item ([Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName +$manifestPath = [Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) + +Import-Module $manifestPath +Import-Module ([Path]::Combine($PSScriptRoot, 'common.psm1')) + +Describe TestPingAsyncCommand { + Context 'Output Streams' { + It 'Success' { + Test-TcpAsync -Target github.com -Port 80 | + Should -BeOfType ([PSNetScanners.TcpResult]) + } + } + + Context 'TcpResult Type' { + BeforeAll { + $result = Test-TcpAsync -Target github.com -Port 80 + $result | Out-Host + } + + It 'Source' { + $result.Source | Should -Not -BeNullOrEmpty + $result.Source | Should -BeOfType ([string]) + } + + It 'Destination' { + $result.Destination | Should -Not -BeNullOrEmpty + $result.Destination | Should -BeOfType ([string]) + } + + It 'Port' { + $result.Port | Should -Not -BeNullOrEmpty + $result.Port | Should -BeOfType ([int]) + } + + It 'Status' { + $result.Status | Should -BeExactly ([PSNetScanners.TcpStatus]::Success) + } + + It 'Details' { + $result = Test-TcpAsync -Target 192.1.1.1 -Port 80 + $result.Details | Should -BeOfType ([System.Net.Sockets.SocketException]) + } + } + + Context 'Test-TcpAsync' { + It 'Parallel Tcp Tests' { + Measure-Command { $targets | Test-TcpAsync } | + ForEach-Object TotalSeconds | + Should -BeLessOrEqual 30 + } + + It 'Stops processing early' { + Measure-Command { $targets | Test-TcpAsync | Select-Object -First 5 } | + ForEach-Object TotalSeconds | + Should -BeLessOrEqual 1 + } + } + + Context 'Parameters' { + It 'ThrottleLimit' { + $result = $targets | Test-TcpAsync -ThrottleLimit $targets.Count + $result | Should -HaveCount $targets.Count + $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) + $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Error) + } + + It 'ConnectionTimeout' { + $result = $targets | Test-TcpAsync -ThrottleLimit $targets.Count -ConnectionTimeout 200 + $result | Should -HaveCount $targets.Count + $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) + $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Timeout) + $result | + Where-Object Status -EQ Timeout | + ForEach-Object Details | + Should -BeOfType ([System.TimeoutException]) + } + } +} diff --git a/tests/common.psm1 b/tests/common.psm1 index abbafa6..eccf0f3 100644 --- a/tests/common.psm1 +++ b/tests/common.psm1 @@ -3,3 +3,26 @@ $start..$end | ForEach-Object { "$ip.$_" } } + +$targets = @' +Target,Port +google.com,80 +google.com,443 +google.com,8080 +google.com,389 +google.com,636 +github.com,80 +github.com,8080 +cisco.com,80 +cisco.com,443 +cisco.com,8080 +cisco.com,389 +cisco.com,636 +amazon.com,80 +amazon.com,443 +amazon.com,8080 +amazon.com,389 +amazon.com,636 +'@ | ConvertFrom-Csv + +Export-ModuleMember -Function * -Variable * From 1d241e952276df950c9f28c9b9b722231f3e876e Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 19 Jul 2024 00:55:28 -0300 Subject: [PATCH 26/41] changing timeout for linux worker --- tests/TcpAsync.tests.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index 0bdbacb..469e63f 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -47,9 +47,15 @@ Describe TestPingAsyncCommand { Context 'Test-TcpAsync' { It 'Parallel Tcp Tests' { + $timeout = 30 + if ($IsLinux) { + # seems to be much slower in linux + $timeout = 150 + } + Measure-Command { $targets | Test-TcpAsync } | ForEach-Object TotalSeconds | - Should -BeLessOrEqual 30 + Should -BeLessOrEqual $timeout } It 'Stops processing early' { @@ -61,7 +67,7 @@ Describe TestPingAsyncCommand { Context 'Parameters' { It 'ThrottleLimit' { - $result = $targets | Test-TcpAsync -ThrottleLimit $targets.Count + $result = $targets | Test-TcpAsync -ThrottleLimit 3 $result | Should -HaveCount $targets.Count $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Error) From 0ab8fc42879de44ac7bcac498d5b42d0c49657e0 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 19 Jul 2024 01:38:46 -0300 Subject: [PATCH 27/41] changing timeout for test --- src/PSNetScanners/PSNetScanners.csproj | 11 ++++++++++- tests/TcpAsync.tests.ps1 | 8 +------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PSNetScanners/PSNetScanners.csproj b/src/PSNetScanners/PSNetScanners.csproj index 590f7dc..2f16263 100644 --- a/src/PSNetScanners/PSNetScanners.csproj +++ b/src/PSNetScanners/PSNetScanners.csproj @@ -12,4 +12,13 @@ - \ No newline at end of file + + true + true + + + + + + + diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index 469e63f..c28b292 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -47,15 +47,9 @@ Describe TestPingAsyncCommand { Context 'Test-TcpAsync' { It 'Parallel Tcp Tests' { - $timeout = 30 - if ($IsLinux) { - # seems to be much slower in linux - $timeout = 150 - } - Measure-Command { $targets | Test-TcpAsync } | ForEach-Object TotalSeconds | - Should -BeLessOrEqual $timeout + Should -BeLessOrEqual 150 } It 'Stops processing early' { From 73bc8e61c56b10a68c01c803393a0145aec35fbf Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 20 Jul 2024 11:26:01 -0300 Subject: [PATCH 28/41] adding default timeout delay task to tcp command --- src/PSNetScanners/TcpResult.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PSNetScanners/TcpResult.cs b/src/PSNetScanners/TcpResult.cs index 6195986..736c846 100644 --- a/src/PSNetScanners/TcpResult.cs +++ b/src/PSNetScanners/TcpResult.cs @@ -45,13 +45,7 @@ internal static async Task CreateAsync( { using TcpClient tcp = new(input.AddressFamily); Task tcpTask = tcp.ConnectAsync(input.Target, input.Port); - List tasks = [tcpTask, cancelTask]; - - if (timeout != 4000) - { - tasks.Add(Task.Delay(timeout)); - } - + List tasks = [tcpTask, cancelTask, Task.Delay(timeout)]; Task result = await Task.WhenAny(tasks); if (result == tcpTask) From d1d6b8b3f23b9e8588fc3ac07d0ca868382c1b44 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 20 Jul 2024 11:38:22 -0300 Subject: [PATCH 29/41] incrementing timeout for tests --- tests/TcpAsync.tests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index c28b292..d0722cf 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -17,7 +17,7 @@ Describe TestPingAsyncCommand { Context 'TcpResult Type' { BeforeAll { $result = Test-TcpAsync -Target github.com -Port 80 - $result | Out-Host + $result | Out-Null } It 'Source' { @@ -40,7 +40,7 @@ Describe TestPingAsyncCommand { } It 'Details' { - $result = Test-TcpAsync -Target 192.1.1.1 -Port 80 + $result = Test-TcpAsync -Target github.com -Port 8080 -ConnectionTimeout 30000 $result.Details | Should -BeOfType ([System.Net.Sockets.SocketException]) } } @@ -61,7 +61,7 @@ Describe TestPingAsyncCommand { Context 'Parameters' { It 'ThrottleLimit' { - $result = $targets | Test-TcpAsync -ThrottleLimit 3 + $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout 30000 $result | Should -HaveCount $targets.Count $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Error) From 42ef14f3e0f60e070eee115452058d9bb932990f Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 20 Jul 2024 12:44:41 -0300 Subject: [PATCH 30/41] changing logic for connectiontimeout --- src/PSNetScanners/Abstractions/AbstractWorker_T.cs | 1 + .../Abstractions/PSNetScannerCommandBase.cs | 2 +- src/PSNetScanners/PingResult.cs | 9 +++++---- src/PSNetScanners/PingWorker.cs | 2 +- src/PSNetScanners/TcpResult.cs | 10 ++++++++-- src/PSNetScanners/TcpWorker.cs | 2 +- tests/TcpAsync.tests.ps1 | 4 ++-- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index 54d3a5c..ef3355c 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -34,6 +34,7 @@ protected override void Dispose(bool disposing) { if (!_disposed) { + _cancellation.Cancel(); InputQueue.Dispose(); OutputQueue.Dispose(); } diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index 54081e0..508615d 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -23,7 +23,7 @@ public abstract class PSNetScannerCommandBase : PSCmdlet public int ThrottleLimit { get; set; } = 50; [Parameter] - [ValidateRange(200, int.MaxValue)] + [ValidateRange(-1, int.MaxValue)] [Alias(["timeout", "to", "ct"])] public int? ConnectionTimeout { get; set; } diff --git a/src/PSNetScanners/PingResult.cs b/src/PSNetScanners/PingResult.cs index 8436198..d184797 100644 --- a/src/PSNetScanners/PingResult.cs +++ b/src/PSNetScanners/PingResult.cs @@ -44,7 +44,7 @@ internal static async Task CreateAsync( string source, string destination, PingAsyncOptions options, - Task cancelTask) + Cancellation cancellation) { using Ping ping = new(); Task pingTask = ping.SendPingAsync( @@ -62,8 +62,8 @@ internal static async Task CreateAsync( } Task dnsTask = Dns.GetHostEntryAsync(destination); - List tasks = [pingTask, cancelTask, dnsTask]; - Task result = await WaitOneAsync(options, tasks); + List tasks = [pingTask, cancellation.Task, dnsTask]; + Task result = await WaitOneAsync(options, cancellation, tasks); if (result != dnsTask && result != pingTask) { @@ -82,6 +82,7 @@ internal static async Task CreateAsync( private static async Task WaitOneAsync( PingAsyncOptions options, + Cancellation cancellation, List tasks) { if (options.TaskTimeout == 4000) @@ -89,7 +90,7 @@ private static async Task WaitOneAsync( return await Task.WhenAny(tasks); } - tasks.Add(Task.Delay(options.TaskTimeout)); + tasks.Add(Task.Delay(options.TaskTimeout, cancellation.Token)); return await Task.WhenAny(tasks); } diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index dd8abb6..67ea687 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -31,7 +31,7 @@ protected override async Task Start() source: Source, destination: host, options: _options, - cancelTask: _cancellation.Task)); + cancellation: _cancellation)); if (tasks.Count == _throttle) { diff --git a/src/PSNetScanners/TcpResult.cs b/src/PSNetScanners/TcpResult.cs index 736c846..02f9da4 100644 --- a/src/PSNetScanners/TcpResult.cs +++ b/src/PSNetScanners/TcpResult.cs @@ -38,14 +38,20 @@ private static TcpResult CreateError(TcpInput input, Exception exception) => internal static async Task CreateAsync( TcpInput input, - Task cancelTask, + Cancellation cancellation, int timeout) { try { using TcpClient tcp = new(input.AddressFamily); Task tcpTask = tcp.ConnectAsync(input.Target, input.Port); - List tasks = [tcpTask, cancelTask, Task.Delay(timeout)]; + List tasks = [tcpTask, cancellation.Task]; + + if (timeout != -1) + { + tasks.Add(Task.Delay(timeout, cancellation.Token)); + } + Task result = await Task.WhenAny(tasks); if (result == tcpTask) diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index cbb3b7a..77f789f 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -27,7 +27,7 @@ protected override async Task Start() { tasks.Add(TcpResult.CreateAsync( input: input, - cancelTask: _cancellation.Task, + cancellation: _cancellation, timeout: _timeout)); } diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index d0722cf..cbaa8de 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -40,7 +40,7 @@ Describe TestPingAsyncCommand { } It 'Details' { - $result = Test-TcpAsync -Target github.com -Port 8080 -ConnectionTimeout 30000 + $result = Test-TcpAsync -Target github.com -Port 8080 -ConnectionTimeout -1 $result.Details | Should -BeOfType ([System.Net.Sockets.SocketException]) } } @@ -61,7 +61,7 @@ Describe TestPingAsyncCommand { Context 'Parameters' { It 'ThrottleLimit' { - $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout 30000 + $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout -1 $result | Should -HaveCount $targets.Count $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Error) From cb73d691ab16037b07d088e19e32c3ff24e1f4c0 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 20 Jul 2024 15:08:49 -0300 Subject: [PATCH 31/41] few changes with enum tcp status --- module/PSNetScanners.Format.ps1xml | 52 +++++++++++++++++++ .../Abstractions/AbstractWorker_T.cs | 1 - src/PSNetScanners/Enums.cs | 6 +-- src/PSNetScanners/Internal/_Format.cs | 14 +++++ src/PSNetScanners/TcpResult.cs | 6 +-- tests/TcpAsync.tests.ps1 | 10 ++-- 6 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/PSNetScanners/Internal/_Format.cs diff --git a/module/PSNetScanners.Format.ps1xml b/module/PSNetScanners.Format.ps1xml index 9e46aaa..45ce092 100644 --- a/module/PSNetScanners.Format.ps1xml +++ b/module/PSNetScanners.Format.ps1xml @@ -47,5 +47,57 @@ + + PingAsyncView + + PSNetScanners.PingResult + + + + + + + + + + + + + + + + + + + + + + + + + + + Source + + + Destination + + + DisplayAddress + + + [PSNetScanners.Internal._Format]::FormatLatency($_.Latency) + + + Status + + + DnsResult + + + + + + diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index ef3355c..e3960bf 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/PSNetScanners/Enums.cs b/src/PSNetScanners/Enums.cs index 9ab3dd9..e4113eb 100644 --- a/src/PSNetScanners/Enums.cs +++ b/src/PSNetScanners/Enums.cs @@ -15,7 +15,7 @@ public enum DnsStatus public enum TcpStatus { - Success, - Timeout, - Error + Opened, + TimedOut, + Closed } diff --git a/src/PSNetScanners/Internal/_Format.cs b/src/PSNetScanners/Internal/_Format.cs new file mode 100644 index 0000000..e4ea2f3 --- /dev/null +++ b/src/PSNetScanners/Internal/_Format.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using System.Management.Automation; + +namespace PSNetScanners.Internal; + +#pragma warning disable IDE1006 + +[EditorBrowsable(EditorBrowsableState.Never)] +public static class _Format +{ + [Hidden, EditorBrowsable(EditorBrowsableState.Never)] + public static string FormatLatency(long latency) => + string.Format("{0} ms", latency); +} diff --git a/src/PSNetScanners/TcpResult.cs b/src/PSNetScanners/TcpResult.cs index 02f9da4..0afe0d8 100644 --- a/src/PSNetScanners/TcpResult.cs +++ b/src/PSNetScanners/TcpResult.cs @@ -28,13 +28,13 @@ private TcpResult( } private static TcpResult CreateSuccess(TcpInput input) => - new(input, TcpStatus.Success); + new(input, TcpStatus.Opened); private static TcpResult CreateTimeout(TcpInput input) => - new(input, TcpStatus.Timeout, new TimeoutException()); + new(input, TcpStatus.TimedOut, new TimeoutException()); private static TcpResult CreateError(TcpInput input, Exception exception) => - new(input, TcpStatus.Error, exception); + new(input, TcpStatus.Closed, exception); internal static async Task CreateAsync( TcpInput input, diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index cbaa8de..eeef790 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -36,7 +36,7 @@ Describe TestPingAsyncCommand { } It 'Status' { - $result.Status | Should -BeExactly ([PSNetScanners.TcpStatus]::Success) + $result.Status | Should -BeExactly ([PSNetScanners.TcpStatus]::Opened) } It 'Details' { @@ -63,15 +63,15 @@ Describe TestPingAsyncCommand { It 'ThrottleLimit' { $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout -1 $result | Should -HaveCount $targets.Count - $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) - $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Error) + $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Opened) + $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Closed) } It 'ConnectionTimeout' { $result = $targets | Test-TcpAsync -ThrottleLimit $targets.Count -ConnectionTimeout 200 $result | Should -HaveCount $targets.Count - $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Success) - $result.Status | Should -Contain ([PSNetScanners.DnsStatus]::Timeout) + $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Opened) + $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::TimedOut) $result | Where-Object Status -EQ Timeout | ForEach-Object Details | From 453bf18fa8e7c68637748ef414a70a17dc24d637 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 20 Jul 2024 15:25:20 -0300 Subject: [PATCH 32/41] few changes with enum tcp status --- tests/TcpAsync.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index eeef790..29b058a 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -73,7 +73,7 @@ Describe TestPingAsyncCommand { $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Opened) $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::TimedOut) $result | - Where-Object Status -EQ Timeout | + Where-Object Status -EQ TimedOut | ForEach-Object Details | Should -BeOfType ([System.TimeoutException]) } From 82fd06349a0ac029e55266f5e3a0c7b8c0342818 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 14:17:08 -0300 Subject: [PATCH 33/41] few more tests --- module/PSNetScanners.Format.ps1xml | 2 +- src/PSNetScanners/Internal/_Format.cs | 4 ++-- src/PSNetScanners/TcpWorker.cs | 8 ++++---- tests/PingAsync.tests.ps1 | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/module/PSNetScanners.Format.ps1xml b/module/PSNetScanners.Format.ps1xml index 45ce092..91dfc15 100644 --- a/module/PSNetScanners.Format.ps1xml +++ b/module/PSNetScanners.Format.ps1xml @@ -86,7 +86,7 @@ DisplayAddress - [PSNetScanners.Internal._Format]::FormatLatency($_.Latency) + [PSNetScanners.Internal._Format]::FormatLatency($_) Status diff --git a/src/PSNetScanners/Internal/_Format.cs b/src/PSNetScanners/Internal/_Format.cs index e4ea2f3..de7ed5c 100644 --- a/src/PSNetScanners/Internal/_Format.cs +++ b/src/PSNetScanners/Internal/_Format.cs @@ -9,6 +9,6 @@ namespace PSNetScanners.Internal; public static class _Format { [Hidden, EditorBrowsable(EditorBrowsableState.Never)] - public static string FormatLatency(long latency) => - string.Format("{0} ms", latency); + public static string FormatLatency(PingResult pingResult) => + string.Format("{0} ms", pingResult.Latency); } diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index 77f789f..c79f36c 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -29,11 +29,11 @@ protected override async Task Start() input: input, cancellation: _cancellation, timeout: _timeout)); - } - if (tasks.Count == _throttle) - { - await ProcessOneAsync(tasks); + if (tasks.Count == _throttle) + { + await ProcessOneAsync(tasks); + } } } diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index f9cbcc3..8db1752 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -92,6 +92,12 @@ Describe TestPingAsyncCommand { } It 'ThrottleLimit' { + $targets.Target | + Sort-Object -Unique | + Test-PingAsync -ThrottleLimit 1 | + ForEach-Object Status | + Should -Be ([System.Net.NetworkInformation.IPStatus]::Success) + $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop | Should -HaveCount 20 } @@ -111,4 +117,16 @@ Describe TestPingAsyncCommand { Should -HaveCount 20 } } + + Context 'Formatting' { + BeforeAll { + $ping = Test-PingAsync google.com + $ping | Out-Null + } + + It 'Format Latency' { + [PSNetScanners.Internal._Format]::FormatLatency($ping) | + Should -Not -BeNullOrEmpty + } + } } From 1bb56a308f7edc491f61eef2af5866b61ff91f99 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 15:53:41 -0300 Subject: [PATCH 34/41] few more tests --- .../Abstractions/PSNetScannerCommandBase.cs | 2 +- src/PSNetScanners/Cancellation.cs | 2 ++ src/PSNetScanners/PingResult.cs | 31 ++++++++----------- src/PSNetScanners/TcpResult.cs | 13 +++----- tests/PingAsync.tests.ps1 | 7 ++--- tests/TcpAsync.tests.ps1 | 4 +-- 6 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index 508615d..8f96ab6 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -23,7 +23,7 @@ public abstract class PSNetScannerCommandBase : PSCmdlet public int ThrottleLimit { get; set; } = 50; [Parameter] - [ValidateRange(-1, int.MaxValue)] + [ValidateRange(1, int.MaxValue)] [Alias(["timeout", "to", "ct"])] public int? ConnectionTimeout { get; set; } diff --git a/src/PSNetScanners/Cancellation.cs b/src/PSNetScanners/Cancellation.cs index 4e89599..8cb41e1 100644 --- a/src/PSNetScanners/Cancellation.cs +++ b/src/PSNetScanners/Cancellation.cs @@ -18,6 +18,8 @@ internal Cancellation() Task = Task.Delay(Timeout.Infinite, _cts.Token); } + internal Task GetTimeoutTask(int timeout) => Task.Delay(timeout, Token); + internal void Cancel() => _cts.Cancel(); public void Dispose() => _cts.Dispose(); diff --git a/src/PSNetScanners/PingResult.cs b/src/PSNetScanners/PingResult.cs index d184797..9ff7c35 100644 --- a/src/PSNetScanners/PingResult.cs +++ b/src/PSNetScanners/PingResult.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; @@ -61,9 +60,8 @@ internal static async Task CreateAsync( reply: await pingTask); } - Task dnsTask = Dns.GetHostEntryAsync(destination); - List tasks = [pingTask, cancellation.Task, dnsTask]; - Task result = await WaitOneAsync(options, cancellation, tasks); + Task dnsTask = GetDnsAsync(destination, options, cancellation); + Task result = await Task.WhenAny(pingTask, cancellation.Task, dnsTask); if (result != dnsTask && result != pingTask) { @@ -76,30 +74,27 @@ internal static async Task CreateAsync( return new PingResult( source: source, destination: destination, - dns: await GetDnsResult(dnsTask), + dns: await dnsTask, reply: await pingTask); } - private static async Task WaitOneAsync( + private static async Task GetDnsAsync( + string destination, PingAsyncOptions options, - Cancellation cancellation, - List tasks) + Cancellation cancellation) { - if (options.TaskTimeout == 4000) + Task dns = Dns.GetHostEntryAsync(destination); + Task timeout = cancellation.GetTimeoutTask(options.TaskTimeout); + Task result = await Task.WhenAny(dns, timeout); + + if (result == timeout) { - return await Task.WhenAny(tasks); + return DnsFailure.CreateTimeout(); } - tasks.Add(Task.Delay(options.TaskTimeout, cancellation.Token)); - return await Task.WhenAny(tasks); - } - - private static async Task GetDnsResult( - Task dnsTask) - { try { - IPHostEntry entry = await dnsTask; + IPHostEntry entry = await dns; return new DnsSuccess(entry); } catch (Exception exception) diff --git a/src/PSNetScanners/TcpResult.cs b/src/PSNetScanners/TcpResult.cs index 0afe0d8..1b6ce26 100644 --- a/src/PSNetScanners/TcpResult.cs +++ b/src/PSNetScanners/TcpResult.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Net.Sockets; using System.Threading.Tasks; @@ -45,14 +44,10 @@ internal static async Task CreateAsync( { using TcpClient tcp = new(input.AddressFamily); Task tcpTask = tcp.ConnectAsync(input.Target, input.Port); - List tasks = [tcpTask, cancellation.Task]; - - if (timeout != -1) - { - tasks.Add(Task.Delay(timeout, cancellation.Token)); - } - - Task result = await Task.WhenAny(tasks); + Task result = await Task.WhenAny( + tcpTask, + cancellation.Task, + cancellation.GetTimeoutTask(timeout)); if (result == tcpTask) { diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 8db1752..ef2b55c 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -92,11 +92,8 @@ Describe TestPingAsyncCommand { } It 'ThrottleLimit' { - $targets.Target | - Sort-Object -Unique | - Test-PingAsync -ThrottleLimit 1 | - ForEach-Object Status | - Should -Be ([System.Net.NetworkInformation.IPStatus]::Success) + $targets.Target | Sort-Object -Unique | Test-PingAsync -ThrottleLimit 1 | + Should -HaveCount 4 $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop | Should -HaveCount 20 diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index 29b058a..19951a6 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -40,7 +40,7 @@ Describe TestPingAsyncCommand { } It 'Details' { - $result = Test-TcpAsync -Target github.com -Port 8080 -ConnectionTimeout -1 + $result = Test-TcpAsync -Target github.com -Port 8080 -ConnectionTimeout ([int]::MaxValue) $result.Details | Should -BeOfType ([System.Net.Sockets.SocketException]) } } @@ -61,7 +61,7 @@ Describe TestPingAsyncCommand { Context 'Parameters' { It 'ThrottleLimit' { - $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout -1 + $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout ([int]::MaxValue) $result | Should -HaveCount $targets.Count $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Opened) $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Closed) From d9e74de8f3b152d8f9ec8b52c54e9c09d6bf1662 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 16:19:51 -0300 Subject: [PATCH 35/41] few more tests --- tests/PingAsync.tests.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index ef2b55c..fe96411 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -56,6 +56,18 @@ Describe TestPingAsyncCommand { $ping.DisplayAddress | Should -Not -BeNullOrEmpty $ping.DisplayAddress | Should -BeOfType ([string]) } + + It 'Status' { + $ping.Status | Should -BeOfType ([System.Net.NetworkInformation.IPStatus]) + } + + It 'Address' { + $ping.Address | Should -BeOfType ([ipaddress]) + } + + It 'Latency' { + $ping.Latency | Should -BeOfType ([long]) + } } Context 'Test-PingAsync' { From 4c110df1506d5e475686ba7fc06bcfce42ab4303 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 16:23:28 -0300 Subject: [PATCH 36/41] few more tests --- tests/TcpAsync.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TcpAsync.tests.ps1 b/tests/TcpAsync.tests.ps1 index 19951a6..a616400 100644 --- a/tests/TcpAsync.tests.ps1 +++ b/tests/TcpAsync.tests.ps1 @@ -61,7 +61,7 @@ Describe TestPingAsyncCommand { Context 'Parameters' { It 'ThrottleLimit' { - $result = $targets | Test-TcpAsync -ThrottleLimit 3 -ConnectionTimeout ([int]::MaxValue) + $result = $targets | Test-TcpAsync -ThrottleLimit 1 -ConnectionTimeout ([int]::MaxValue) $result | Should -HaveCount $targets.Count $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Opened) $result.Status | Should -Contain ([PSNetScanners.TcpStatus]::Closed) From f704f3b4915ba664ee53f154997b9cbca73b89e5 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 17:19:10 -0300 Subject: [PATCH 37/41] few changes. adding formatting for tcp async --- module/PSNetScanners.Format.ps1xml | 47 ++++++++++++++++++++++++++- src/PSNetScanners/Internal/_Format.cs | 3 +- src/PSNetScanners/PingResult.cs | 35 +++++++++++++++----- tests/PingAsync.tests.ps1 | 14 +------- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/module/PSNetScanners.Format.ps1xml b/module/PSNetScanners.Format.ps1xml index 91dfc15..0279642 100644 --- a/module/PSNetScanners.Format.ps1xml +++ b/module/PSNetScanners.Format.ps1xml @@ -65,6 +65,7 @@ + Right @@ -86,7 +87,7 @@ DisplayAddress - [PSNetScanners.Internal._Format]::FormatLatency($_) + [PSNetScanners.Internal._Format]::GetFormattedLatency($_) Status @@ -99,5 +100,49 @@ + + TcpAsyncView + + PSNetScanners.TcpResult + + + + + + 17 + + + + 17 + + + + 7 + + + + 15 + + + + + + + Source + + + Destination + + + Port + + + Status + + + + + + diff --git a/src/PSNetScanners/Internal/_Format.cs b/src/PSNetScanners/Internal/_Format.cs index de7ed5c..adad67e 100644 --- a/src/PSNetScanners/Internal/_Format.cs +++ b/src/PSNetScanners/Internal/_Format.cs @@ -9,6 +9,5 @@ namespace PSNetScanners.Internal; public static class _Format { [Hidden, EditorBrowsable(EditorBrowsableState.Never)] - public static string FormatLatency(PingResult pingResult) => - string.Format("{0} ms", pingResult.Latency); + public static string GetFormattedLatency(PingResult pingResult) => pingResult._displayLatency; } diff --git a/src/PSNetScanners/PingResult.cs b/src/PSNetScanners/PingResult.cs index 9ff7c35..a0ab85d 100644 --- a/src/PSNetScanners/PingResult.cs +++ b/src/PSNetScanners/PingResult.cs @@ -7,17 +7,39 @@ namespace PSNetScanners; public sealed class PingResult { + private IPStatus? _status; + + private IPAddress? _ip; + + private long? _latency; + + private string? _displayAddress; + + internal readonly string _displayLatency; + public string Source { get; } public string Destination { get; } - public IPAddress? Address { get; } + public IPAddress? Address + { + get => _ip ??= Status is IPStatus.Success ? Reply?.Address : null; + } - public string DisplayAddress { get; } + public string DisplayAddress + { + get => _displayAddress ??= Address?.ToString() ?? "*"; + } - public long Latency { get; } + public long Latency + { + get => _latency ??= Reply?.RoundtripTime ?? 0; + } - public IPStatus Status { get; } + public IPStatus Status + { + get => _status ??= Reply?.Status ?? IPStatus.TimedOut; + } public DnsResult? DnsResult { get; } @@ -33,10 +55,7 @@ private PingResult( Destination = destination; DnsResult = dns; Reply = reply; - Status = reply?.Status ?? IPStatus.TimedOut; - Address = Status is IPStatus.Success ? reply?.Address : null; - Latency = reply?.RoundtripTime ?? 0; - DisplayAddress = Address?.ToString() ?? "*"; + _displayLatency = Status is IPStatus.Success ? $"{Latency} ms" : "*"; } internal static async Task CreateAsync( diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index fe96411..0eec3d0 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -38,7 +38,7 @@ Describe TestPingAsyncCommand { Context 'PingResult Type' { BeforeAll { - $ping = Test-PingAsync google.com + $ping = Test-PingAsync 127.0.0.1 $ping | Out-Null } @@ -126,16 +126,4 @@ Describe TestPingAsyncCommand { Should -HaveCount 20 } } - - Context 'Formatting' { - BeforeAll { - $ping = Test-PingAsync google.com - $ping | Out-Null - } - - It 'Format Latency' { - [PSNetScanners.Internal._Format]::FormatLatency($ping) | - Should -Not -BeNullOrEmpty - } - } } From 258183ff2aa60a80937b98971a8e5d40ff18daaa Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 17:40:57 -0300 Subject: [PATCH 38/41] few changes. adding formatting for tcp async --- tests/PingAsync.tests.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 0eec3d0..8fe63d6 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -104,8 +104,8 @@ Describe TestPingAsyncCommand { } It 'ThrottleLimit' { - $targets.Target | Sort-Object -Unique | Test-PingAsync -ThrottleLimit 1 | - Should -HaveCount 4 + $targets | Test-PingAsync -ThrottleLimit 1 | + Should -HaveCount 17 $range | Test-PingAsync -ThrottleLimit 300 -ErrorAction Stop | Should -HaveCount 20 @@ -126,4 +126,16 @@ Describe TestPingAsyncCommand { Should -HaveCount 20 } } + + It 'Formatting' { + BeforeAll { + $ping = Test-PingAsync 127.0.0.1 + $ping | Out-Null + } + + It 'Gets format string for Latency' { + [PSNetScanners.Internal._Format]::GetFormattedLatency($ping) | + Should -Not -BeNullOrEmpty + } + } } From f41e22b65759376601a52555cae645395aa4923c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 18:07:48 -0300 Subject: [PATCH 39/41] typo in test --- src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs | 2 +- src/PSNetScanners/Commands/TestPingAsyncCommand.cs | 8 +++++--- src/PSNetScanners/Commands/TestTcpAsyncCommand.cs | 8 +++++--- src/PSNetScanners/Dbg/Dbg.cs | 6 +++--- tests/PingAsync.tests.ps1 | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs index 8f96ab6..de0d97c 100644 --- a/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs +++ b/src/PSNetScanners/Abstractions/PSNetScannerCommandBase.cs @@ -1,6 +1,6 @@ using System.Management.Automation; -namespace PSNetScanners; +namespace PSNetScanners.Abstractions; public abstract class PSNetScannerCommandBase : PSCmdlet { diff --git a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs index 13c0dbf..916484b 100644 --- a/src/PSNetScanners/Commands/TestPingAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestPingAsyncCommand.cs @@ -2,8 +2,10 @@ using System.Management.Automation; using System.Net.NetworkInformation; using System.Text; +using PSNetScanners.Abstractions; +using PSNetScanners.Dbg; -namespace PSNetScanners; +namespace PSNetScanners.Commands; [Cmdlet(VerbsDiagnostic.Test, "PingAsync")] [OutputType(typeof(PingResult))] @@ -43,7 +45,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - Dbg.Assert(_worker is not null); + Debug.Assert(_worker is not null); try { @@ -66,7 +68,7 @@ protected override void ProcessRecord() protected override void EndProcessing() { - Dbg.Assert(_worker is not null); + Debug.Assert(_worker is not null); try { diff --git a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs index bb0febc..e2511c4 100644 --- a/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs +++ b/src/PSNetScanners/Commands/TestTcpAsyncCommand.cs @@ -1,8 +1,10 @@ using System; using System.Management.Automation; using System.Net; +using PSNetScanners.Abstractions; +using PSNetScanners.Dbg; -namespace PSNetScanners; +namespace PSNetScanners.Commands; [Cmdlet(VerbsDiagnostic.Test, "TcpAsync")] [OutputType(typeof(TcpResult))] @@ -28,7 +30,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - Dbg.Assert(_worker is not null); + Debug.Assert(_worker is not null); try { @@ -57,7 +59,7 @@ protected override void ProcessRecord() protected override void EndProcessing() { - Dbg.Assert(_worker is not null); + Debug.Assert(_worker is not null); try { diff --git a/src/PSNetScanners/Dbg/Dbg.cs b/src/PSNetScanners/Dbg/Dbg.cs index 9bd540d..834d974 100644 --- a/src/PSNetScanners/Dbg/Dbg.cs +++ b/src/PSNetScanners/Dbg/Dbg.cs @@ -1,11 +1,11 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -namespace PSNetScanners; +namespace PSNetScanners.Dbg; -internal static class Dbg +internal static class Debug { [Conditional("DEBUG")] public static void Assert([DoesNotReturnIf(false)] bool condition) => - Debug.Assert(condition); + System.Diagnostics.Debug.Assert(condition); } diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 8fe63d6..7a55b68 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -127,7 +127,7 @@ Describe TestPingAsyncCommand { } } - It 'Formatting' { + Context 'Formatting' { BeforeAll { $ping = Test-PingAsync 127.0.0.1 $ping | Out-Null From e5549c64729054f91050c81beb85d4e550fc7fa3 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 18:42:15 -0300 Subject: [PATCH 40/41] almost done --- .../Abstractions/AbstractWorker.cs | 2 +- .../Abstractions/AbstractWorker_T.cs | 2 +- src/PSNetScanners/PingWorker.cs | 1 + src/PSNetScanners/TcpWorker.cs | 1 + tests/common.psm1 | 20 +++++++++---------- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PSNetScanners/Abstractions/AbstractWorker.cs b/src/PSNetScanners/Abstractions/AbstractWorker.cs index 79fa0a5..310cce1 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -namespace PSNetScanners; +namespace PSNetScanners.Abstractions; internal abstract class WorkerBase(int throttle, Cancellation cancellation) : IDisposable diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index e3960bf..f442803 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace PSNetScanners; +namespace PSNetScanners.Abstractions; internal abstract class WorkerBase(int throttle, Cancellation cancellation) : WorkerBase(throttle, cancellation) diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 67ea687..2059547 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using System.Net.NetworkInformation; using System.Threading.Tasks; +using PSNetScanners.Abstractions; namespace PSNetScanners; diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index c79f36c..5883ff4 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Management.Automation; using System.Threading.Tasks; +using PSNetScanners.Abstractions; namespace PSNetScanners; diff --git a/tests/common.psm1 b/tests/common.psm1 index eccf0f3..8114f6f 100644 --- a/tests/common.psm1 +++ b/tests/common.psm1 @@ -7,21 +7,21 @@ $targets = @' Target,Port google.com,80 -google.com,443 -google.com,8080 -google.com,389 -google.com,636 github.com,80 -github.com,8080 cisco.com,80 -cisco.com,443 -cisco.com,8080 -cisco.com,389 -cisco.com,636 amazon.com,80 +google.com,443 +cisco.com,443 amazon.com,443 -amazon.com,8080 +google.com,389 +cisco.com,389 amazon.com,389 +google.com,8080 +github.com,8080 +amazon.com,8080 +cisco.com,8080 +google.com,636 +cisco.com,636 amazon.com,636 '@ | ConvertFrom-Csv From fcbe2ce1e1739cf3b324b7a9469583677b8fda11 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Jul 2024 19:18:52 -0300 Subject: [PATCH 41/41] few more changes --- src/PSNetScanners/Abstractions/AbstractWorker_T.cs | 8 ++++---- src/PSNetScanners/PingWorker.cs | 2 +- src/PSNetScanners/TcpWorker.cs | 2 +- tests/PingAsync.tests.ps1 | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs index f442803..dd4259d 100644 --- a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs +++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs @@ -4,20 +4,20 @@ namespace PSNetScanners.Abstractions; -internal abstract class WorkerBase(int throttle, Cancellation cancellation) +internal abstract class WorkerBase(int throttle, Cancellation cancellation) : WorkerBase(throttle, cancellation) { protected virtual BlockingCollection InputQueue { get; } = []; - protected virtual BlockingCollection OutputQueue { get; } = []; + protected virtual BlockingCollection OutputQueue { get; } = []; internal void Enqueue(TInput item) => InputQueue.Add(item, Token); internal void CompleteAdding() => InputQueue.CompleteAdding(); - internal virtual IEnumerable GetOutput() => OutputQueue.GetConsumingEnumerable(Token); + internal virtual IEnumerable GetOutput() => OutputQueue.GetConsumingEnumerable(Token); - internal bool TryTake(out TOutput result) => OutputQueue.TryTake(out result, 0, Token); + internal bool TryTake(out Output result) => OutputQueue.TryTake(out result, 0, Token); protected async Task ProcessOneAsync( List> tasks) diff --git a/src/PSNetScanners/PingWorker.cs b/src/PSNetScanners/PingWorker.cs index 2059547..8212989 100644 --- a/src/PSNetScanners/PingWorker.cs +++ b/src/PSNetScanners/PingWorker.cs @@ -7,7 +7,7 @@ namespace PSNetScanners; -internal sealed class PingWorker : WorkerBase +internal sealed class PingWorker : WorkerBase { protected override Task Worker { get; } diff --git a/src/PSNetScanners/TcpWorker.cs b/src/PSNetScanners/TcpWorker.cs index 5883ff4..4d93fed 100644 --- a/src/PSNetScanners/TcpWorker.cs +++ b/src/PSNetScanners/TcpWorker.cs @@ -6,7 +6,7 @@ namespace PSNetScanners; -internal sealed class TcpWorker : WorkerBase +internal sealed class TcpWorker : WorkerBase { protected override Task Worker { get; } diff --git a/tests/PingAsync.tests.ps1 b/tests/PingAsync.tests.ps1 index 7a55b68..5d8dcb2 100644 --- a/tests/PingAsync.tests.ps1 +++ b/tests/PingAsync.tests.ps1 @@ -21,9 +21,11 @@ Describe TestPingAsyncCommand { Context 'DnsResult Type' { It 'DnsSuccess' { - $result = Test-PingAsync google.com -ResolveDns + $result = Test-PingAsync 8.8.8.8 -ResolveDns $result.DnsResult | Should -BeOfType ([PSNetScanners.DnsSuccess]) $result.DnsResult.Status | Should -Be ([PSNetScanners.DnsStatus]::Success) + $result.DnsResult.AddressList | Should -BeOfType ([ipaddress]) + $result.DnsResult.Aliases.Count | Should -BeGreaterOrEqual 0 } It 'DnsFailure' {