Public/Common/Start-SilentProcess.ps1
function Start-SilentProcess { [CmdletBinding()] param ( # The path to the command to be run [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [Alias("PSPath")] [ValidateNotNullOrEmpty()] [string] $FilePath, # An optional list of arguments to be passed to it [Parameter( Mandatory = $false, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [Alias("Arguments")] [array] $ArgumentList, # If set will set the working directory for the called command [Parameter( Mandatory = $false, Position = 2, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [string] $WorkingDirectory, # The exit codes expected from this process when it has been successful [Parameter( Mandatory = $false )] [array] $ExitCodes = @(0), # The path to where the redirected output should be stored # Defaults to the contents of the environment variable 'RepoLogDirectory' if available # If that isn't set then defaults to a temp directory [Parameter( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $RedirectOutputPath, # The prefix to use on the redirected streams, defaults to the command run time [Parameter( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $RedirectOutputPrefix, # The suffix for the redirected streams (defaults to log) [Parameter( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $RedirectOutputSuffix = "log", # By default this won't return any output from the called command # however if this param is set then the result of stdout is returned as an object at the end along with the locations of the stdout and stderr files # This can be useful if you need the output from the command or when debugging [Parameter( Mandatory = $false )] [switch] $PassThru ) # Start off by ensuring we can find the command and then get it's full path. # This is useful when using things like Set-Alias as the Start-Process command won't have access to these # So instead we can pass in the full path to the command Write-Verbose "Finding absolute path to command $FilePath" try { $AbsoluteCommandPath = (Get-Command $FilePath -ErrorAction Stop).Definition } catch { throw "Could not find command $FilePath.`n$($_.Exception.Message)" } # Set redirected output to the repos log directory if it exists, otherwise to temp if (!$RedirectOutputPath) { if ($global:RepoLogDirectory) { $RedirectOutputPath = $global:RepoLogDirectory } else { # Determine our temp directory depending on flavour of PowerShell if ($PSVersionTable.PSEdition -eq 'Desktop') { $RedirectOutputPath = $env:TEMP } else { $RedirectOutputPath = (Get-PSDrive Temp).Root } } } # Check the redirect stream path is valid try { $RedirectOutputPathCheck = Get-Item $RedirectOutputPath -Force } catch { throw "$RedirectOutputPath does not appear to be a valid directory." } if (!$RedirectOutputPathCheck.PSIsContainer) { throw "$RedirectOutputPath must be a directory" } Write-Verbose "Redirecting output to: $RedirectOutputPath" # If we don't have a redirect output prefix then create one if (-not $RedirectOutputPrefix) { # See if the value in $FilePath is a path or just a command name. # If it's a path we don't want to use that as a prefix for our redirected output files as it could be stupidly long # If it's a command name then we can just straight up use that as our redirect name try { $isPath = Resolve-Path $FilePath -ErrorAction Stop } catch { $RedirectOutputPrefix = $FilePath } # We've got a path, do some work to extract just the name of the program from the file path if ($isPath) { try { $RedirectOutputPrefix = $isPath | Get-Item | Select-Object -ExpandProperty Name -ErrorAction Stop } catch { # Don't throw, we'll still get a valid filename below anyways it'll just be missing a prefix Write-Warning "Failed to auto-generate RedirectOutputPrefix" } } } # Define our redirected stream names $StdOutFileName = "$($RedirectOutputPrefix)_$(Get-Date -Format yyMMddhhmm)_stdout.$($RedirectOutputSuffix)" $StdErrFileName = "$($RedirectOutputPrefix)_$(Get-Date -Format yyMMddhhmm)_stderr.$($RedirectOutputSuffix)" # Set the paths $StdOutFilePath = Join-Path $RedirectOutputPath -ChildPath $StdOutFileName $StdErrFilePath = Join-Path $RedirectOutputPath -ChildPath $StdErrFileName # Set the default calling params $ProcessParams = @{ FilePath = $AbsoluteCommandPath RedirectStandardError = $StdErrFilePath RedirectStandardOutput = $StdOutFilePath PassThru = $true NoNewWindow = $true Wait = $true } # Add optional params if we have them if ($ArgumentList) { $ProcessParams.Add('ArgumentList', $ArgumentList) } if ($WorkingDirectory) { $ProcessParams.Add('WorkingDirectory', $WorkingDirectory) } # Run the process # We've changed these writes to use the debug stream instead, # this way we can still capture this information when we want to debug a command but we avoid polluting the # verbose stream which is used in a lot of our builds. # This should fix #7 and stop us leaking passwords and such in our builds 😬 Write-Debug "Calling '$AbsoluteCommandPath' with arguments: '$($ArgumentList -join ' ')'" Write-Debug "Valid exit codes: $($ExitCodes -join ', ')" try { $Process = Start-Process @ProcessParams } catch { # If we get a failure at this stage we won't have any stderr to grab so just return our exception throw $_.Exception.Message } # Check the exit code is expected, if not grab the contents of stderr (if we can) and return it if ($Process.ExitCode -notin $ExitCodes) { $ErrorContent = Get-Content $StdErrFilePath -Raw -ErrorAction SilentlyContinue # Write-Error is preferable to 'throw' as it gives much cleaner output, it also allows more control over how errors are handled Write-Error "$FilePath has returned a non-zero exit code: $($Process.ExitCode).`n$ErrorContent" } # If we've requested the output from this command then return it along with the paths to our StdOut and StdErr files should we need them if ($PassThru) { try { $OutputContent = Get-Content $StdOutFilePath Return [pscustomobject]@{ StdOutFilePath = $StdOutFilePath StdErrFilePath = $StdErrFilePath OutputContent = $OutputContent } } catch { Write-Error "Unable to get contents of $StdOutFilePath.`n$($_.Exception.Message)" } } } |