Przechwytywanie wyjścia standardowego i błędów za pomocą funkcji Start-Process

112

Czy jest błąd w Start-Processpoleceniu programu PowerShell podczas uzyskiwania dostępu do właściwości StandardErrori StandardOutput?

Jeśli uruchomię następujące, nie otrzymam żadnych wyników:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.StandardOutput
$process.StandardError

Ale jeśli przekieruję dane wyjściowe do pliku, otrzymam oczekiwany wynik:

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait -RedirectStandardOutput stdout.txt -RedirectStandardError stderr.txt
jzbruno
źródło
5
Czy w tym konkretnym przypadku naprawdę potrzebujesz Start-process? ... $process= ping localhost # zapisałoby dane wyjściowe w zmiennej procesowej.
mjsr,
1
Prawdziwe. Szukałem bardziej przejrzystego sposobu obsługi zwrotów i argumentów. Skończyło się na napisaniu scenariusza, tak jak pokazałeś.
jzbruno

Odpowiedzi:

128

Tak Start-Processzostało zaprojektowane z jakiegoś powodu. Oto sposób na uzyskanie go bez wysyłania do pliku:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Andy Arismendi
źródło
7
Przyjmuję twoją odpowiedź. Szkoda, że ​​nie stworzyli właściwości, które nie są używane, jest to bardzo zagmatwane.
jzbruno
6
Jeśli masz problemy z uruchomieniem procesu w ten sposób, zobacz zaakceptowaną odpowiedź tutaj stackoverflow.com/questions/11531068/… , która zawiera niewielką modyfikację WaitForExit i StandardOutput.ReadToEnd
Ralph Willgoss
3
Kiedy używasz -verb runAs to nie pozwala na h -NoNewWindow lub opcje przekierowania
Maverick
15
Ten kod zostanie zakleszczony w pewnych warunkach, ponieważ zarówno StdErr, jak i StdOut są synchronicznie odczytywane do końca. msdn.microsoft.com/en-us/library/…
codepoke
8
@codepoke - jest trochę gorzej - ponieważ najpierw wykonuje wywołanie WaitForExit, nawet jeśli przekierowuje tylko jeden z nich, może się zakleszczyć, jeśli bufor strumienia zostanie zapełniony (ponieważ nie próbuje odczytać z niego do czasu procesu wyszedł)
James Manning
20

W podanym w pytaniu kodzie myślę, że powinno działać odczytanie właściwości ExitCode zmiennej inicjującej.

$process = Start-Process -FilePath ping -ArgumentList localhost -NoNewWindow -PassThru -Wait
$process.ExitCode

Zauważ, że (tak jak w twoim przykładzie) musisz dodać parametry -PassThrui -Wait(to mnie na chwilę zaskoczyło).

JJones
źródło
A co, jeśli argumentlist zawiera zmienną? Wydaje się, że nie rozszerza się.
OO
1
umieściłbyś listę argumentów w cudzysłowie. Czy to zadziała? ... $ process = Start-Process -FilePath ping -ArgumentList "-t localhost -n 1" -NoNewWindow -PassThru -Wait
JJones
jak wyświetlić dane wyjściowe w oknie programu PowerShell, a także zalogować je do pliku dziennika? Czy to możliwe?
Murali Dhar Darshan
Nie można używać -NoNewWindowz-Verb runAs
Dragas
11

Miałem też ten problem i skończyło się na tym, że użyłem kodu Andy'ego do stworzenia funkcji do czyszczenia rzeczy, gdy trzeba uruchomić wiele poleceń.

Zwróci kody stderr, stdout i zakończenia jako obiekty. Jedna uwaga: funkcja nie przyjmie .\ścieżki; należy używać pełnych ścieżek.

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
}

Oto jak go używać:

$DisableACMonitorTimeOut = Execute-Command -commandTitle "Disable Monitor Timeout" -commandPath "C:\Windows\System32\powercfg.exe" -commandArguments " -x monitor-timeout-ac 0"
LPG
źródło
Dobry pomysł, ale wygląda na to, że składnia nie działa dla mnie. Czy lista parametrów nie powinna używać składni param ([typ] $ ArgumentName)? czy możesz dodać przykładowe wywołanie tej funkcji?
Lockszmith,
Odnośnie „Jedna uwaga: funkcja nie akceptuje. \ W ścieżce; pełne ścieżki muszą być użyte.”: Możesz użyć:> $ pinfo.FileName = Resolve-Path $ commandPath
Lupuz
9

WAŻNY:

Korzystamy z funkcji, jaką zapewnia LPG .

Jednak zawiera błąd, który możesz napotkać podczas uruchamiania procesu, który generuje dużo danych wyjściowych. Z tego powodu podczas korzystania z tej funkcji możesz skończyć z impasem. Zamiast tego użyj dostosowanej wersji poniżej:

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
  Try {
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $commandPath
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $commandArguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    [pscustomobject]@{
        commandTitle = $commandTitle
        stdout = $p.StandardOutput.ReadToEnd()
        stderr = $p.StandardError.ReadToEnd()
        ExitCode = $p.ExitCode
    }
    $p.WaitForExit()
  }
  Catch {
     exit
  }
}

Więcej informacji na ten temat można znaleźć w witrynie MSDN :

Zakleszczenie może wystąpić, jeśli proces nadrzędny wywoła p.WaitForExit przed p.StandardError.ReadToEnd, a proces potomny zapisze wystarczającą ilość tekstu, aby wypełnić przekierowany strumień. Proces nadrzędny czekałby w nieskończoność na zakończenie procesu potomnego. Proces potomny czekałby w nieskończoność na odczytanie przez rodzica z pełnego strumienia StandardError.

pserranne
źródło
3
Ten kod nadal jest zakleszczony z powodu synchronicznego wywołania ReadToEnd (), które opisuje również łącze do MSDN.
bergmeister
1
Wydaje się, że to teraz rozwiązało mój problem. Muszę przyznać, że nie do końca rozumiem, dlaczego się zawiesiło, ale wygląda na to, że pusty stderr zablokował proces do zakończenia. Dziwne, ponieważ działało przez długi czas, ale nagle tuż przed świętami zaczęło zawodzić, powodując zawieszanie się wielu procesów Java.
rhellem
8

Naprawdę miałem kłopoty z tymi przykładami z Andy Arismendi iz LPG . Powinieneś zawsze używać:

$stdout = $p.StandardOutput.ReadToEnd()

zanim zadzwonisz

$p.WaitForExit()

Pełny przykład to:

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$p.WaitForExit()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
Rainer
źródło
Gdzie przeczytałeś, że „Zawsze powinieneś używać: $ p.StandardOutput.ReadToEnd () przed $ p.WaitForExit ()”? Jeśli w buforze znajduje się wyjście, które jest wyczerpane, po większej liczbie danych wyjściowych w późniejszym czasie, zostanie to pominięte, jeśli wiersz wykonania jest na WaitForExit, a proces nie został zakończony (a następnie wyprowadza więcej stderr lub stdout) ....
CJBS
Odnosząc się do mojego komentarza powyżej, później zobaczyłem komentarze do zaakceptowanej odpowiedzi dotyczącej zakleszczenia i przepełnienia bufora w przypadkach dużego wyjścia, ale poza tym spodziewałbym się, że tylko dlatego, że bufor jest odczytywany do końca, nie oznacza to procesu zostało zakończone, a zatem może brakować większej ilości wyników. Czy coś mi brakuje?
CJBS
@CJBS: „tylko dlatego, że bufor jest odczytywany do końca, nie oznacza to, że proces się zakończył” - to znaczy. W rzeczywistości dlatego może się zablokować. Czytanie „do końca” nie oznacza „przeczytaj to, co jest teraz ”. Oznacza to rozpoczęcie czytania i nie zatrzymywanie się, dopóki strumień nie zostanie zamknięty, co jest tym samym, co zakończenie procesu.
Peter Duniho
0

Oto moja wersja funkcji, która zwraca standardowy proces System.Diagnostics.Process z 3 nowymi właściwościami

Function Execute-Command ($commandTitle, $commandPath, $commandArguments)
{
    Try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $commandPath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $True
        $pinfo.Arguments = $commandArguments
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null
        $stdout = $p.StandardOutput.ReadToEnd()
        $stderr = $p.StandardError.ReadToEnd()
        $p.WaitForExit()
        $p | Add-Member "commandTitle" $commandTitle
        $p | Add-Member "stdout" $stdout
        $p | Add-Member "stderr" $stderr
    }
    Catch {
    }
    $p
}
Anabela Mazurek
źródło
0

Oto niezręczny sposób na uzyskanie danych wyjściowych z innego procesu PowerShell:

start-process -wait -nonewwindow powershell 'ps | Export-Clixml out.xml'; import-clixml out.xml
js2010
źródło