private/Invoke-PackageCommand.ps1
function Invoke-PackageCommand { <# .SYNOPSIS Tries to run a command, returns its ExitCode and Output if successful, otherwise returns NULL #> [CmdletBinding()] Param ( [ValidateNotNullOrEmpty()] [string]$Path, [ValidateNotNullOrEmpty()] [Parameter( Mandatory = $true )] [string]$Command, [switch]$FallbackToShellExecute ) # Remove any trailing backslashes from the Path. # This isn't necessary, because Split-ExecutableAndArguments can handle and trims # extra backslashes, but this will make the path look more sane in errors and warnings. $Path = $Path.TrimEnd('\') # Lenovo sometimes forgets to put a directory separator betweeen %PACKAGEPATH% and the executable so make sure it's there # If we end up with two backslashes, Split-ExecutableAndArguments removes the duplicate from the executable path, but # we could still end up with a double-backslash after %PACKAGEPATH% somewhere in the arguments for now. [string]$Command = Resolve-CmdVariable -String $Command -ExtraVariables @{'PACKAGEPATH' = "${Path}\"} [string[]]$StdOutLines = @() [string[]]$StdErrLines = @() $ExeAndArgs = Split-ExecutableAndArguments -Command $Command -WorkingDirectory $Path # Split-ExecutableAndArguments returns NULL if no executable could be found if (-not $ExeAndArgs) { Write-Warning "The command or file '$Command' could not be found from '$Path' and was not run" return $null } $ExeAndArgs.Arguments = Remove-CmdEscapeCharacter -String $ExeAndArgs.Arguments $process = [System.Diagnostics.Process]::new() $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $process.StartInfo.UseShellExecute = $false $process.StartInfo.WorkingDirectory = $Path $process.StartInfo.FileName = $ExeAndArgs.Executable $process.StartInfo.Arguments = $ExeAndArgs.Arguments $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true if ($FallbackToShellExecute) { Write-Warning "Running with ShellExecute - process output cannot be captured!" $process.StartInfo.UseShellExecute = $true $process.StartInfo.RedirectStandardOutput = $false $process.StartInfo.RedirectStandardError = $false } Write-Debug "Starting external process:`r`n File: $($ExeAndArgs.Executable)`r`n Arguments: $($ExeAndArgs.Arguments)`r`n WorkingDirectory: $Path" try { if (-not $process.Start()) { Write-Warning "No new process was created or a handle to it could not be obtained." Write-Warning "Executable was: '$($ExeAndArgs.Executable)' - this should *probably* not have happened" return $null } } catch { # In case we get ERROR_ELEVATION_REQUIRED (740) retry with ShellExecute to elevate with UAC if ($null -ne $_.Exception.InnerException -and $_.Exception.InnerException.NativeErrorCode -eq 740) { if (-not $FallbackToShellExecute) { Write-Warning "This process requires elevated privileges - falling back to ShellExecute to elevate with UAC, consider running PowerShell as Administrator" return (Invoke-PackageCommand -Path:$Path -Command:$Command -FallbackToShellExecute) } # In case we get ERROR_BAD_EXE_FORMAT (193) retry with ShellExecute to open files like MSI } elseif ($null -ne $_.Exception.InnerException -and $_.Exception.InnerException.NativeErrorCode -eq 193) { if (-not $FallbackToShellExecute) { Write-Warning "The file to be run is not an executable - falling back to ShellExecute" return (Invoke-PackageCommand -Path:$Path -Command:$Command -FallbackToShellExecute) } } Write-Warning $_ return $null } if (-not $FallbackToShellExecute) { # When redirecting StandardOutput or StandardError you have to start reading the streams asynchronously, or else it can cause # programs that output a lot (like package u3aud03w_w10 - Conexant USB Audio) to fill a stream and deadlock/hang indefinitely. # See issue #25 and https://stackoverflow.com/questions/11531068/powershell-capturing-standard-out-and-error-with-process-object $StdOutAsync = $process.StandardOutput.ReadToEndAsync() $StdErrAsync = $process.StandardError.ReadToEndAsync() } $process.WaitForExit() if (-not $FallbackToShellExecute) { $StdOutInOneString = $StdOutAsync.GetAwaiter().GetResult() $StdErrInOneString = $StdErrAsync.GetAwaiter().GetResult() [string[]]$StdOutLines = $StdOutInOneString.Split( [string[]]("`r`n", "`r", "`n"), [StringSplitOptions]::None ) [string[]]$StdErrLines = $StdErrInOneString.Split( [string[]]("`r`n", "`r", "`n"), [StringSplitOptions]::None ) } $returnInfo = [ProcessReturnInformation]@{ "FilePath" = $ExeAndArgs.Executable "Arguments" = $ExeAndArgs.Arguments "WorkingDirectory" = $Path "StandardOutput" = $StdOutLines "StandardError" = $StdErrLines "ExitCode" = $process.ExitCode "Runtime" = $process.ExitTime - $process.StartTime } return $returnInfo } |