InvokeExe.ps1
function Register-Exe($path) { $cachePath = "$env:TEMP\Find-App.cache.json" if (-not $global:Exe_Registry) { $global:Exe_Registry = @{} if ( Test-Path $cachePath ) { $json = Get-Content -Path $cachePath -Raw # $global:Exe_Registry = ConvertFrom-Json $json } } $resolvedPath = $Exe_Registry[$path] if ( $resolvedPath ) { return } $resolvedPath = Resolve-Exe $path if ( -not $resolvedPath ) { if ( -not ( Test-Path $path -ErrorAction SilentlyContinue ) ) { $resolvedPath = Find-App $path | Get-Path if (-not $resolvedPath) { throw "Exe not found : '$path' " } $path = $resolvedPath } $resolvedPath = Resolve-Path $path | Get-Path } $exeName = Split-Path $resolvedPath -Leaf $exeName = $exeName.ToLowerInvariant() $Exe_Registry[$exeName] = $resolvedPath $exeName = [System.IO.Path]::GetFileNameWithoutExtension($exeName) $exeName = $exeName.ToLowerInvariant() $Exe_Registry[$exeName] = $resolvedPath $json = $Exe_Registry | ConvertTo-Json try { New-Folder_EnsureExists -folder ( Split-Path $cachePath -Parent ) Set-Content -Path $cachePath -Value $json -ErrorAction SilentlyContinue } catch { Write-Warning "$cachePath not updated for'$path'" # ignore changes on the cache } } function Get-Exe($path) { $path = Get-Path $path if (-not $path) { return } if ( Test-Path $path -ErrorAction SilentlyContinue ) { return $path } $path = $path.ToLowerInvariant() if ($Exe_Registry) { $result = $Exe_Registry[$path] if ($result ) { return $result } } $result = Resolve-Exe $path if ($result ) { return $result } throw "Exe not found : '$path' " } function Update-ExeArguments_Encode { Param ($exeArgs, [switch]$sort, [switch]$ignoreEmpty ) $exeProps = $exeArgs.GetEnumerator() | ForEach-Object { [PSCustomObject]@{ Name = $_.Name.Trim(); Value = $_.Value } } if ($sort){ $exeProps = $exeProps | Sort-Object Name } if ($ignoreEmpty) { $exePropNull = ( $exeProps | Where-Object { [String]::IsNullOrEmpty($_.Value) } ) -join ', ' if ($exePropNull) { Write-Warning "The following properties does not have a value '$exePropNull' " } $exeProps = $exeProps | Where-Object { -not [String]::IsNullOrEmpty($_.Value) } } $exeProps | ForEach-Object { $val = if ($_.Value -is [Uri] ) { $_.Value.OriginalString } else { "$($_.Value)" } $val = $val.Replace('"','\""') $hasChar = $val.IndexOfAny(@(' ','"',"'")) if ( $hasChar -gt -1 ) { $_.Value = $val | Update-String_Enclose '"' } } $result = $exeProps | ForEach-Object { $_.Name + '=' + $_.Value } return $result } function Update-ExeArguments_Encode:::Test { Update-ExeArguments_Encode -exeArgs @{ A = 'b' } Update-ExeArguments_Encode -exeArgs ([Ordered]@{ C = '1'; A = '2' }) Update-ExeArguments_Encode -exeArgs ([Ordered]@{ C = '1'; A = '2' }) -sort Update-ExeArguments_Encode -exeArgs @{ A = 'b'; B = $null } Update-ExeArguments_Encode -exeArgs @{ A = 'b'; B = $null } -ignoreEmpty Update-ExeArguments_Encode -exeArgs @{ A = 'b'; C = 'https://www.abc.com:9090' } Update-ExeArguments_Encode -exeArgs @{ A = 'b"c'; D = 'dd' } } function New-ProcessExeInfo ($exeToRun, $Arguments, $WorkingDirectory, $NoEcho = $false, $logFileName = '', $Timeout = '00:30:00') { if ($logFileName) { $logFolder = Split-Path $logFileName -Parent if ( $logFolder ) { New-Folder_EnsureExists $logFolder } } $User = [Security.Principal.WindowsIdentity]::GetCurrent() [PSCustomObject]@{ Exe = $exeToRun Arguments = $Arguments WorkingDirectory = $WorkingDirectory ExitCode = 0 StartTime = [DateTime]::Now EndTime = $null Error = '' Output = '' LogFileName = $logFileName OutputFileName = $logFileName ErrorFileName = $logFileName NoEcho = $NoEcho IsAdministrator = Test-Administrator TimeOut = $Timeout UserName = $User.Name # "$env:USERDOMAIN\$env:USERNAME" } } function Start-ProcessAsAdmin { param( $ProcessInfo ) $psi = new-object System.Diagnostics.ProcessStartInfo $psi.RedirectStandardError = $true $psi.RedirectStandardOutput = $true $psi.UseShellExecute = $false $psi.FileName = $ProcessInfo.Exe if ($ProcessInfo.Arguments) { $psi.Arguments = "$($ProcessInfo.Arguments)" } if ([Environment]::OSVersion.Version -ge (new-object 'Version' 6,0)){ $psi.Verb = "runas" } if (-not $ProcessInfo.WorkingDirectory) { $ProcessInfo.WorkingDirectory = get-location | Get-Path } $psi.WorkingDirectory = Get-Path $ProcessInfo.WorkingDirectory if ($minimized) { $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden } function AddOutput ([string[]]$lastOutput) { if ($lastOutput) { $ProcessInfo.Output += $lastOutput Write-Host $lastOutput -NoNewline -BackgroundColor DarkGray -ForegroundColor White if ( $ProcessInfo.OutputFileName ) { $lastOutput | Out-File -FilePath $ProcessInfo.OutputFileName -Append } } } function AddError ($lastError) { if ($lastError) { $ProcessInfo.Error += $lastError Write-Host $lastError -NoNewline -BackgroundColor Red -ForegroundColor White if ( $ProcessInfo.ErrorFileName ) { $lastError | Out-File -FilePath $ProcessInfo.ErrorFileName -Append } } } function Read ($outputReader) { if ( -not $outputReader ) { return } $bufferLength = 4096 $buffer = new-object char[] $bufferLength $sb = New-Object 'System.Text.StringBuilder' $StartTime = [DateTime]::Now $refreshInternal = [TimeSpan]::FromMilliseconds(5000) while ( $true ) { $readCount = $outputReader.ReadBlock($buffer, 0, $buffer.Length) if (-not $readCount ) { break } for($i = 0; $i -lt $readCount; $i++) { $sb.Append([string]$buffer[$i]) | Out-Null } $CurrentTime = [DateTime]::Now if ($readCount -lt $bufferLength ) { break } $duration = $CurrentTime - $StartTime if ( $refreshInternal -lt $duration) { break } } $result = $sb.ToString() $result } function GetProcessOutput { $lastOutput = Read $process.StandardOutput AddOutput $lastOutput return $true } try { $process = [System.Diagnostics.Process]::Start($psi) $Timeout = Coalesce $ProcessInfo.TimeOut '00:30:00' if (-not ( Wait-Exe -while { GetProcessOutput } -timeOut $Timeout -process $process ) ) { throw "TIMEOUT: '$($ProcessInfo.Exe)' $($ProcessInfo.Arguments)" } AddOutput $process.StandardOutput.ReadToEnd() AddError $process.StandardError.ReadToEnd() $ProcessInfo.EndTime = [DateTime]::Now $ProcessInfo.ExitCode = $process.ExitCode $global:LASTEXITCODE = $ProcessInfo.ExitCode } finally { Invoke-Dispose ([ref] $process) } # Removes trailing lines and spaces $ProcessInfo.Output = $ProcessInfo.Output.TrimEnd() $ProcessInfo.Error = $ProcessInfo.Error.Trim() } function Join-ExeArguments($argList) { if ( -not $argList ) { return; } $result = [string]::Join(' ', ( $argList | ForEach-Object { $param = [string]$_ if ( $param.StartsWith('-') -and $param.EndsWith(':') ) { $param = $param.Substring(0, $param.Length - 1) } $param } ) ) return $result } function Invoke-Exe_Scope { Param( [Parameter()] [ScriptBlock]$script, [Parameter()] [int]$OkExitCode, [Parameter()] [switch]$ignoredOutput, [Parameter()] [string]$WorkingFolder, [Parameter()] [TimeSpan]$Timeout='00:30:00' ) . $script } function Invoke-Exe () { <# This function cannot declare explicitly parameters in order to be able to send them to the executable. Use Invoke-Exe_Scope #> $OkExitCode = @() + ( Get-Argument OkExitCode -remove -default 0 ) $ignoredOutput = Get-Argument NoOutput -remove -switch $NoEcho = Get-Argument NoEcho -remove -switch $TimeOut = Get-Argument TimeOut -remove $WorkingFolder = Get-Argument WorkingFolder -remove -default ( Get-PS_WorkingFolder ) $unboundArgs = $MyInvocation.UnboundArguments if ( -not $unboundArgs) { throw 'No command line provided' } [String]$exe = Get-Exe $unboundArgs[0] $unboundArgs.RemoveAt(0) $commandArgs = ( Join-ExeArguments $unboundArgs ) $commandLine = if ( $exe.Contains(' ') ) { $exe | Update-String_Enclose '"' } else { $exe } $commandLine = $commandLine + ' ' + $commandArgs if ( $global:SafeExecuteCounter ) { $global:SafeExecuteCounter += 1 } else { $global:SafeExecuteCounter = 1 } $padPid = ([string]$pid).PadLeft(7,'0') $padCounter = ([string]$global:SafeExecuteCounter).PadLeft(5,'0') $counterPrefix = "$padPid-$padCounter-" $logFileName = $counterPrefix + ( Split-Path $exe -Leaf | Update-Suffix '"' | Update-Suffix "'" ) + ".ps1.log" $logFileName = Initialize-ExportFile $logFileName -append -silent "#" + ( Get-Date ) >> $logFileName "cd '$WorkingFolder'" >> $logFileName "$commandLine" >> $logFileName $global:LastCommandLine = $commandLine if (-not $NoEcho) { write-host $global:LastCommandLine } $ProcessInfo = New-ProcessExeInfo -exeToRun $exe -Arguments $commandArgs ` -WorkingDirectory $WorkingFolder -NoEcho $NoEcho -LogFileName $logFileName -Timeout $Timeout try { try { Start-ProcessAsAdmin -ProcessInfo $ProcessInfo } finally { #compress option here fixes a bad format error and removes white spaces $ProcessOutputJson = ConvertTo-Json $ProcessInfo -Compress $ProcessOutputJson = $ProcessOutputJson.Replace('\r\n',"`r`n") $ProcessOutputJson >> $logFileName } } catch { $StackMessage = Get-ErrorStackMessage $StackMessage >> $logFileName $DumpLocation = ' Dump Generated at: ' + ( Get-UNC_FileName $logFileName ) $errorMessage = "Failed to execute : `n$commandLine`n`n$DumpLocation`n`n$StackMessage`n`n$ProcessOutputJson" Write-Warning $errorMessage throw $errorMessage } if ( $ProcessInfo.ExitCode -notin $OkExitCode) { $output = $ProcessInfo | Format-List | Out-String Write-Warning $output throw [System.ApplicationException] "$commandLine`n`Exit code: $($ProcessInfo.ExitCode) but expected [$OkExitCode] " } if (-not $ignoredOutput) { $output = $ProcessInfo.Output.split("`n"); $output } } function Invoke-Exe:::Test { Invoke-Exe ping localhost Invoke-Exe ping -OkExitCode 1 Invoke-Exe ping -OkExitCode 1 -NoOutput $currentFolder = Split-Path ( Get-PSCallStack )[0].ScriptName -Parent Invoke-Exe git remote -v -WorkingFolder $currentFolder # Invoke-Exe ping localhost -t -timeout '00:01:00' } function Wait-Exe ($process, [TimeSpan]$timeOut = '10:00:00', [TimeSpan]$pollingInterval = '00:00:01', [scriptblock]$while) { if ( $while ) { $checkResult = Invoke-Command -ScriptBlock $while [DateTime]$limit = [DateTime]::Now + $timeOut do { $exited = $process.WaitForExit($pollingInterval.TotalMilliseconds) if ($exited) { return $exited } $checkResult = Invoke-Command -ScriptBlock $while } while ( $checkResult -and ( $limit -gt [DateTime]::Now ) ) } else { $exited = $process.WaitForExit($timeOut.TotalMilliseconds) } return $exited } function Get-InvokeArguments { param($BoundParameters, [switch]$ExcludeNames) if (-not $BoundParameters) { return '' } $cmdArgs = $BoundParameters.GetEnumerator() | ForEach-Object{ $result = if ($ExcludeNames) { '' } else { '-' + $_.Key } if ($_.Value) { if ($_.Value -is [Switch]) { if ( $_.Value.IsPresent ) { return $result } return } if ( $result ) { $result += ' ' } $result += $_.Value return $result } } $cmdArgs = $cmdArgs -join ' ' return $cmdArgs } function Get-InvokeArguments:::Example { Get-InvokeArguments | Assert -eq '' Get-InvokeArguments @{} | Assert -eq '' Get-InvokeArguments @{A = 1} | Assert -eq '-A 1' Get-InvokeArguments @{A = 1; B = 'BValue'} | Assert -eq '-A 1 -B Bvalue' Get-InvokeArguments @{A = 1} -ExcludeNames | Assert -eq '1' } function Get-Powershell_Call ($PSConsoleFile, $Version, [Switch]$NoLogo, [Switch]$NoExit, [Switch]$Sta, [Switch]$Mta, [Switch]$NoProfile, [Switch]$NonInteractive, $InputFormat, $OutputFormat, $WindowStyle, $EncodedCommand, $File, $ExecutionPolicy, $Command ) { return Get-InvokeArguments $PSBoundParameters # [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}] # [-WindowStyle <style>] [-EncodedCommand <Base64EncodedCommand>] # [-File <filePath> <args>] [-ExecutionPolicy <ExecutionPolicy>] # [-Command { - | <script-block> [-args <arg-array>] # | <string> [<CommandParameters>] } ] } function Get-Powershell_Call:::Example{ Get-Powershell_Call -Command { $env:COMPUTERNAME } -NoProfile } |