Last active
November 14, 2022 09:19
-
-
Save LindnerBrewery/def296407e4cc5720b22ff4a614f6d42 to your computer and use it in GitHub Desktop.
Start-ProcessPlus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Start-ProcessPlus { | |
<# | |
.SYNOPSIS | |
Starts an external process | |
.DESCRIPTION | |
Starts an external process | |
.EXAMPLE | |
PS C:\> Start-ProcessPlus -FilePath ping.exe -ArgumentList "127.0.0.1 -n 60" | |
Process will be executed without any feedback until it is finished | |
.EXAMPLE | |
PS C:\> Start-ProcessPlus -FilePath ping.exe -ArgumentList "127.0.0.1 -n 60" -ConsoleOut | |
Process will be executed and print out the stdout and error out directly to the console and the output object | |
.EXAMPLE | |
PS C:\> Start-ProcessPlus -FilePath ping.exe -ArgumentList "127.0.0.1 -n 60" -ConsoleOut -Timeout 60 | |
The process will exit with an error if it doesn't finish within 60 sec | |
.EXAMPLE | |
PS C:\> Start-ProcessPlus -FilePath ping.exe -ArgumentList "127.0.0.1 -n 60" -FileOut | |
The sta and error output of the process will be saved to files. If files are not specified with the parameters -OutputFile and -ErrorFile tmp files will automatically be created | |
.INPUTS | |
Inputs (if any) | |
.OUTPUTS | |
Output (if any) | |
.NOTES | |
General notes | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'default', | |
SupportsShouldProcess = $true, | |
PositionalBinding = $false, | |
HelpUri = 'http://www.microsoft.com/', | |
ConfirmImpact = 'Medium')] | |
[Alias()] | |
[OutputType([PsCustomObject])] | |
Param ( | |
# Specifies the optional path and filename of the program that runs in the process | |
[Parameter(Mandatory = $true, | |
Position = 0, | |
ValueFromPipeline = $true, | |
ValueFromPipelineByPropertyName = $true, | |
ValueFromRemainingArguments = $false)] | |
[ValidateNotNullOrEmpty()] | |
[String]$FilePath, | |
# Specifies parameters or parameter values to use when this cmdlet starts the process. Arguments can be accepted as a single string with the arguments separated by spaces, or as an array of strings separated by commas. | |
[Parameter(Mandatory = $false)] | |
[ValidateNotNullOrEmpty()] | |
[String[]]$ArgumentList, | |
# StdOut and ErrorOut will be printed to the console | |
[Parameter(Mandatory = $false)] | |
[Switch]$ConsoleOut, | |
# Output will be saved as a file. If you don't provide an optional file(s) a temp will be created | |
[Parameter(Mandatory = $true, | |
ParameterSetName = 'Fileout')] | |
[Switch]$FileOut, | |
# Optional output file. If not defined a tmp file will be created to save the stdOut | |
[Parameter(Mandatory = $false, | |
ParameterSetName = 'Fileout')] | |
[String]$OutputFile, | |
# Optional output file. If not defined a tmp file will be created to save the errOut | |
[Parameter(Mandatory = $false, | |
ParameterSetName = 'Fileout')] | |
[String]$ErrorFile, | |
# Timeout in seconds before process execution will be aborted | |
[Parameter(Mandatory = $false)] | |
[int]$Timeout, | |
# Working dir for the process | |
[Parameter(Mandatory = $false)] | |
[String]$workingDirectory, | |
[Parameter(Mandatory = $false)] | |
[pscredential]$Credential, | |
[Parameter(Mandatory = $false)] | |
[Switch]$WithProfile | |
) | |
begin {} | |
Process { | |
$stdOutput = $null | |
$errorOut = $null | |
#region setup process | |
$Arguments = $ArgumentList -join " " | |
$pinfo = New-Object System.Diagnostics.ProcessStartInfo | |
$pinfo.FileName = $FilePath | |
$pinfo.RedirectStandardError = $true | |
$pinfo.RedirectStandardOutput = $true | |
$pinfo.UseShellExecute = $false | |
$pinfo.Arguments = $Arguments | |
if ($Credential) { | |
$uname = $Credential.UserName -split '\\' | |
if ($uname[1]) { | |
$pinfo.Domain = $uname[0] # AD domain | |
$pinfo.UserName = $uname[1] | |
}else{ | |
$pinfo.UserName = $uname[0] | |
} | |
$pinfo.Password = $Credential.Password | |
if ($Withprofile.IsPresent) { | |
$pinfo.LoadUserProfile = $true | |
} | |
} | |
# I was to lazy to set parametersetname. | |
if ($Withprofile.IsPresent -and -not $credential) { | |
Write-Host "-WhithProfile only can be used in combination with -Credential" -ForegroundColor Yellow | |
} | |
if(-Not [string]::IsNullOrEmpty($workingDirectory)) { | |
$pinfo.WorkingDirectory = $workingDirectory | |
} | |
$p = New-Object System.Diagnostics.Process | |
$p.StartInfo = $pinfo | |
#endregion setup process | |
#region Events | |
# all registered events will be stored in this variable and unregistered at the end | |
$events = [System.Collections.ArrayList]::new() | |
# setup optional console output | |
if ($ConsoleOut.IsPresent) { | |
$stdOutConsole = { Write-Host $Event.SourceEventArgs.Data } | |
$errOutConsole = { Write-Host $Event.SourceEventArgs.Data -ForegroundColor Red } | |
$events += Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $stdOutConsole | |
$events += Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $errOutConsole | |
} | |
if ($pscmdlet.ParameterSetName -eq 'Fileout') { | |
# was out and err files defined? | |
if ($OutputFile) { | |
$OutputFile = New-Item $OutputFile -ItemType File -ErrorAction Stop | |
} | |
else { | |
$OutputFile = New-TemporaryFile | |
} | |
if ($ErrorFile) { | |
$ErrorFile = New-Item $ErrorFile -ItemType File -ErrorAction Stop | |
} | |
else { | |
$ErrorFile = New-TemporaryFile | |
} | |
# create Streambuilder and actions for the Events | |
$outFileSW = [System.IO.StreamWriter]::new($OutputFile) | |
$outFileSW.AutoFlush = $true | |
$ErrorFileSW = [System.IO.StreamWriter]::new($ErrorFile) | |
$ErrorFileSW.AutoFlush = $true | |
$FileAction = { if (-not [String]::IsNullOrEmpty($EventArgs.Data)) { | |
$Event.MessageData.WriteLine($Event.SourceEventArgs.Data) | |
} } | |
$events += Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $FileAction -MessageData $outFileSW | |
$events += Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $FileAction -MessageData $ErrorFileSW | |
$stdOutput = $OutputFile | |
$errorOut = $ErrorFile | |
} | |
else { | |
$stdSb = [System.Text.StringBuilder]::new() | |
$errorSb = [System.Text.StringBuilder]::new() | |
$OutAction = { if (-not [String]::IsNullOrEmpty($EventArgs.Data)) { | |
$Event.MessageData.AppendLine($Event.SourceEventArgs.Data) | |
} } | |
$events += Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $OutAction -MessageData $stdSb | |
$events += Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $OutAction -MessageData $errorSb | |
$stdOutput = $stdSb | |
$errorOut = $errorSb | |
} | |
#$exitAction = | |
#{ | |
# $myEventVar = $true | |
#} | |
#$events += Register-ObjectEvent -InputObject $p -EventName Exited -Action $exitAction -SourceIdentifier exitedEvent | |
$exitAction = {$myEventVar = $true} | |
$exitedEvent = Register-ObjectEvent -InputObject $p -EventName Exited -Action $exitAction | |
$events += $exitedEvent | |
#endregion Events | |
# start a stopwatch, even id not needed | |
$sw = [system.Diagnostics.stopwatch]::StartNew() | |
try { | |
# start process | |
$p.Start() | Out-Null | |
$p.BeginOutputReadLine() | |
$p.BeginErrorReadLine() | |
# This will simulate $p.waitforexit([int]timeout) but will not block the console output | |
while ((& $exitedEvent.Module { $myEventVar }) -ne $true) { | |
if ($Timeout) { | |
if ($sw.Elapsed.TotalSeconds -ge $Timeout) { | |
Write-Verbose "Timeout reached. Will Kill the process" | |
$p.Kill() | |
$p.Dispose() | |
Write-Verbose "Process killed" | |
Throw "Process did not end within $timeout seconds" | |
} | |
} | |
Start-Sleep 1 | |
} | |
# output is ugly. consider implementing Format.ps1xml | |
[PSCustomObject]@{ | |
ExitCode = $p.ExitCode | |
Output = $stdOutput.ToString() | |
Error = $errorOut.ToString() | |
} | |
} | |
catch { | |
Throw $_ | |
} | |
finally { | |
$events | ForEach-Object { Unregister-Event -SourceIdentifier $_.name } | |
if ($ErrorFileSW) { | |
$ErrorFileSW.close() | |
} | |
if ($outFileSW) { | |
$outFileSW.close() | |
} | |
} | |
} | |
end {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Scope='Function')] | |
Param() | |
BeforeAll { | |
. $PSCommandPath.Replace('.Tests.ps1','.ps1') | |
} | |
Describe Start-ProcessPlus { | |
Context 'Basic tests' { | |
It "Invoking ping on localhost. This should always work" { | |
$result = Start-ProcessPlus -FilePath ping.exe -ArgumentList 'localhost' | |
$result.exitcode | Should -Be 0 | |
} | |
It "Invoking non existing file. This should always Fail" { | |
{ Start-ProcessPlus -FilePath asdfasfdf.exe } | Should -Throw | |
} | |
It "Invoking non ping on non existent target. This should always return exitcode 1" { | |
$result = Start-ProcessPlus -FilePath ping.exe -ArgumentList "Host_that_doesn't exist" | |
$result.exitcode | Should -Be 1 | |
} | |
It "Invoking non ping on non existent target. This should always return exitcode 1" { | |
{Start-ProcessPlus -FilePath ping -ArgumentList '-t localhost' -Timeout 3} | Should -Throw 'Process did not end within 3 seconds' | |
} | |
} | |
Context 'Complex tests' { | |
BeforeAll{ | |
$Name = "PU{0}" -f (get-date).ToFileTime() | |
$passWord = ($Name.ToCharArray() | Sort-Object {Get-Random}) -join '!' | |
$securePassWord = $passWord | ConvertTo-SecureString -AsPlainText -Force | |
$creds = [pscredential]::new($name,$securePassWord) | |
New-LocalUser -Name $Name -Password $securePassWord -ErrorAction SilentlyContinue | |
} | |
AfterAll{ | |
Remove-LocalUser $Name -ErrorAction SilentlyContinue | |
} | |
It "Testing with other user"{ | |
$output = Start-ProcessPlus -FilePath powershell.exe -ArgumentList '-NoProfile', '-command', '$env:username' -Credential $creds | |
$output.Output.Trim() | Should -Be $Name | |
} | |
It "Testing working directory" { | |
$output = Start-ProcessPlus -FilePath powershell.exe -ArgumentList '-NoProfile', '-command', '$pwd.path' -workingDirectory $env:TMP | |
$output.Output.Trim() | Should -Be ([System.IO.DirectoryInfo]::new($env:TMP).fullname) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment