PSNative.psm1
function Resolve-PathEx { <# .SYNOPSIS Resolve a path. .DESCRIPTION Resolve a path. Allows specifying success criteria, as well as selecting files or folders only. .PARAMETER Path The input path to resolve. .PARAMETER Type What kind of item to resolve to. Supported types: - Any: Can be whatever, so long as it exists. - File: Must be a file/leaf object and exist - Directory: Must be a container/directory object and exist - NewFile: The parent path must exist and be a container/directory. The item itself needs not exist, but if it exists, it must be a leaf/file Defaults to Any. .PARAMETER SingleItem Whether resolving to more than one item should cause an error. .PARAMETER Mode How results should be handled: - Any: At least one single successful path must be resolved, any errors are ignored as long as at least one is valid. - All: All items resolved must be valid. - AnyWarning: At least one single successful path must be resolved, any errors are filed as warning. .PARAMETER Provider What provider the item must be from. Defaults to FileSystem. .EXAMPLE PS C:\> Resolve-PathEx -Path . Resolves the current path. .EXAMPLE PS C:\> Resolve-PathEx -Path .\test\report.csv -Type NewFile -SingleItem Must resolve the full path of the file "report.csv" in the folder "test" under the current path. The file need not exist, but the folder must be present. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path, [ValidateSet('File', 'Directory', 'Any', 'NewFile')] [string] $Type = 'Any', [switch] $SingleItem, [ValidateSet('Any', 'All', 'AnyWarning')] [string] $Mode = 'Any', [string] $Provider = 'FileSystem' ) process { foreach ($pathEntry in $Path) { $data = [PSCustomObject]@{ Input = $pathEntry Path = $null Success = $false Message = '' Error = $null } $basePath = $pathEntry if ('NewFile' -eq $Type) { $basePath = Split-Path -Path $pathEntry -Parent $leaf = Split-Path -Path $pathEntry -Leaf } try { $resolved = (Resolve-Path -Path $basePath -ErrorAction Stop).ProviderPath } catch { $data.Error = $_ $data.Message = "Path cannot be resolved: $pathEntry" return $data } if (@($resolved).Count -gt 1 -and $SingleItem) { $data.Message = "More than one item found: $pathEntry" return $data } $paths = [System.Collections.ArrayList]@() $messages = [System.Collections.ArrayList]@() $success = $false $failed = $false foreach ($resolvedPath in $resolved) { $item = Get-Item -LiteralPath $resolvedPath if ($Provider -ne $item.PSProvider.Name) { $failed = $true $null = $messages.Add("Not a $Provider path: $($resolvedPath)") continue } if ('File' -eq $Type -and $item.PSIsContainer) { $failed = $true $null = $messages.Add("Not a file: $($resolvedPath)") continue } if ('Directory' -eq $Type -and -not $item.PSIsContainer) { $failed = $true $null = $messages.Add("Not a directory: $($resolvedPath)") continue } if ('NewFile' -eq $Type) { if (-not $item.PSIsContainer) { $failed = $true $null = $messages.Add("Parent of $($pathEntry) is not a container: $($resolvedPath)") continue } $newFilePath = Join-Path -Path $resolvedPath -ChildPath $leaf if (Test-Path -LiteralPath $newFilePath -PathType Container) { $failed = $true $null = $messages.Add("Target path $($newFilePath) must not be a directory!") continue } $null = $paths.Add($newFilePath) $success = $true continue } $null = $paths.Add($resolvedPath) $success = $true } $data.Path = $($paths) $data.Message = $($messages) foreach ($pathItem in $data.Path) { Write-Verbose "Resolved $pathEntry to $pathItem" } switch ($Mode) { 'Any' { $data.Success = $success foreach ($message in $data.Message) { Write-Verbose $message } } 'All' { $data.Success = -not $failed foreach ($message in $data.Message) { Write-Verbose $message } } 'AnyWarning' { $data.Success = $success foreach ($message in $data.Message) { Write-Warning $message } } } $data } } } function Invoke-NativeCommand { <# .SYNOPSIS Execute an external application and wait for it to conclude. .DESCRIPTION Execute an external application and wait for it to conclude. Provides convenient parameterization options and output processing. .PARAMETER Name Name of or path to the process to run. .PARAMETER ArgumentList Parameters to provide to the process launched. .PARAMETER WorkingDirectory Directory from which to launch the process. Defaults to the current filesystem path. .PARAMETER Timeout How long to wait for the process to complete. Defaults to 15 minutes. .PARAMETER Credential Credentials to use when launching the process. .PARAMETER Environment Environment variables to add to the launched process .EXAMPLE PS C:\> Invoke-NativeCommand -Name nslookup 'wikipedia.org' '1.1.1.1' Launches nslookup, resolving wikipedia.org against 1.1.1.1. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding(PositionalBinding = $false)] param ( [Parameter(Mandatory = $true, Position = 0)] [Alias('FilePath')] [string] $Name, [Parameter(ValueFromRemainingArguments = $true)] [string[]] $ArgumentList, [string] $WorkingDirectory = (Get-Location -PSProvider FileSystem).ProviderPath, [Timespan] $Timeout = '00:15:00', [PSCredential] $Credential, [hashtable] $Environment = @{ } ) begin { $info = [System.Diagnostics.ProcessStartInfo]::new() $info.FileName = $Name if ($WorkingDirectory) { $resolved = Resolve-PathEx -Path $WorkingDirectory -SingleItem -Type Directory if (-not $resolved.Success) { throw ($resolved.Message -join "`n") } $info.WorkingDirectory = $resolved.Path } foreach ($entry in $ArgumentList) { $info.ArgumentList.Add($entry) } $info.RedirectStandardError = $true $info.RedirectStandardOutput = $true if ($Credential) { $info.Password = $Credential.Password $networkCred = $Credential.GetNetworkCredential() $domain = $networkCred.Domain $user = $networkCred.UserName if (-not $domain -and $networkCred.UserName -like '*@*') { $domain = ($networkCred.UserName -split '@')[-1] $user = ($networkCred.UserName -split '@')[0] } $info.UserName = $user $info.Domain = $domain } foreach ($pair in $Environment.GetEnumerator()) { $info.EnvironmentVariables[$pair.Key] = $pair.Value } $proc = [System.Diagnostics.Process]::new() $proc.StartInfo = $info $started = $false } process { if (-not $started) { $start = Get-Date $null = $proc.Start() } } end { $failed = $false $limit = $start.Add($Timeout) while (-not $proc.HasExited) { Start-Sleep -Milliseconds 250 if ($limit -lt (Get-Date)) { $failed = $true $proc.Close() break } } if ($proc.ExitCode -ne 0) { $failed = $true } [PSCustomObject]@{ File = $Name Success = -not $failed Output = $(try { $proc.StandardOutput.ReadToEnd() } catch { }) Error = $(try { $proc.StandardError.ReadToEnd() } catch { }) ExitCode = $proc.ExitCode } } } function Start-NativeProcess { <# .SYNOPSIS Create a native process that allows for ongoing interaction. .DESCRIPTION Create a native process that allows for ongoing interaction. This can be use to wrap interactive console applications, then repeatedly send commands and receive output. The returned object has a few relevant methods: + Stop(): Kill the process. + Start(): Start the process again. + Send([string]): Send a line of command to the process + ReadOutput(): Read the lines of output from the proceess + ReadError(): Read the lines of errors from the process The two read commands will retrieve all the data written so far (and then discard them from the object, so calling them a second time without new output will not return anything). There is no indication whether the wrapped process is done doing what it is intending to do, making it necessary to understand the expected output. Sending multiple lines of input will queue up commands, which will be processed sequentially. .PARAMETER Name Name of the application to start. Can be the full path or the simple name, if the application is in the PATH environment variable. .PARAMETER ArgumentList Arguments to send to the application. These basically are the parameters needed BEFORE running the command. Use the .Send([string]) method to send commands to the process after starting it. .PARAMETER WorkingDirectory Path to run the program in. Defaults to the current file system path. .PARAMETER Credential Credentials under which to run the process. .PARAMETER Environment Any environment variables to inject into the process being started. .PARAMETER NoStart Do not automatically start th process after preparing the starting information. By default, the wrapped process is launched immediately instead. .EXAMPLE PS C:\ $cmd = Start-NativeProcess -Name cmd PS C:\> $cmd.Send('dir C:\') PS C:\> $cmd.ReadOutput() Starts a persistent cmd process, then sends the dir command to it and reads the response. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [string[]] $ArgumentList = @(), [string] $WorkingDirectory = (Get-Location -PSProvider FileSystem).ProviderPath, [PSCredential] $Credential, [hashtable] $Environment = @{ }, [switch] $NoStart ) $process = [NativeProcess]::New($Name, $ArgumentList) if ($WorkingDirectory) { $resolved = Resolve-PathEx -Path $WorkingDirectory -SingleItem -Type Directory if (-not $resolved.Success) { throw ($resolved.Message -join "`n") } $process.WorkingDirectory = $resolved.Path } if ($Credential) { $process.Credential = $Credential } $process.EnvironmentVariables = $Environment if (-not $NoStart) { $process.Start() } $process } class NativeProcess { [string]$Name [object[]]$ArgumentList [string]$WorkingDirectory [hashtable]$EnvironmentVariables = @{ } [datetime]$Started [datetime]$Stopped [PSCredential]$Credential [System.Diagnostics.Process]$Process hidden [object] $OutputTask hidden [object] $ErrorTask NativeProcess([string]$Name) { $this.Name = $Name } NativeProcess([string]$Name, [object[]]$ArgumentList) { $this.Name = $Name $this.ArgumentList = $ArgumentList } [void] Start() { if ($this.Process -and -not $this.Process.HasExited) { $this.Stop() } $this.OutputTask = $null $this.ErrorTask = $null #region Process $info = [System.Diagnostics.ProcessStartInfo]::new() $info.FileName = $this.Name if ($this.WorkingDirectory) { $info.WorkingDirectory = $this.WorkingDirectory } foreach ($entry in $this.ArgumentList) { $info.ArgumentList.Add($entry) } if ($global:PSVersionTable.PSVersion.Major -lt 6) { $info.UseShellExecute = $false } $info.RedirectStandardInput = $true $info.RedirectStandardError = $true $info.RedirectStandardOutput = $true if ($this.Credential) { $info.Password = $this.Credential.Password $networkCred = $this.Credential.GetNetworkCredential() $domain = $networkCred.Domain $user = $networkCred.UserName if (-not $domain -and $networkCred.UserName -like '*@*') { $domain = ($networkCred.UserName -split '@')[-1] $user = ($networkCred.UserName -split '@')[0] } $info.UserName = $user $info.Domain = $domain } foreach ($pair in $this.EnvironmentVariables.GetEnumerator()) { $info.EnvironmentVariables[$pair.Key] = $pair.Value } $proc = [System.Diagnostics.Process]::new() $proc.StartInfo = $info # Start $null = $proc.Start() $this.Process = $proc #endregion Process $this.Started = Get-Date } [void] Stop() { $this.Process.Kill() $this.Process = $null $this.OutputTask = $null $this.ErrorTask = $null $this.Stopped = Get-Date } [string[]] ReadOutput() { if (-not $this.Process) {return @() } $lastStart = [DateTime]::Now $lines = while ($true) { if ($this.OutputTask -and $this.OutputTask.Status -eq 'WaitingForActivation') { if ($lastStart -gt [DateTime]::Now.AddMilliseconds(-100)) { continue } break } if ($this.OutputTask) { $this.OutputTask.Result } $this.OutputTask = $this.Process.StandardOutput.ReadLineAsync() $lastStart = [DateTime]::Now if ($this.Process.HasExited) { if ($this.OutputTask) { $this.OutputTask.Result } $this.Process.StandardOutput.ReadToEnd() -split "`n" } } if (-not $lines) { return @() } return $lines } [string[]] ReadError() { if (-not $this.Process) {return @() } $lastStart = [DateTime]::Now $lines = while ($true) { if ($this.ErrorTask -and $this.ErrorTask.Status -eq 'WaitingForActivation') { if ($lastStart -gt [DateTime]::Now.AddMilliseconds(-100)) { continue } break } if ($this.ErrorTask) { $this.ErrorTask.Result } $this.ErrorTask = $this.Process.StandardError.ReadLineAsync() $lastStart = [DateTime]::Now if ($this.Process.HasExited) { if ($this.ErrorTask) { $this.ErrorTask.Result } $this.Process.StandardOutput.ReadToEnd() -split "`n" } } if (-not $lines) { return @() } return $lines } [void] Send([string[]]$Lines) { if ($null -eq $this.Process) { throw "Process not running! Use Start() first." } if ($this.Process.HasExited) { throw "Process has stopped already! Use Start() to restart it first." } foreach ($line in $Lines) { $this.Process.StandardInput.WriteLine($line) } } } |