Co może być przyczyną zawieszania się procesu podczas oczekiwania na wyjście?
Ten kod musi uruchomić skrypt PowerShell, który wykonuje wiele akcji, np. Rozpocząć rekompilację kodu za pomocą MSBuild, ale prawdopodobnie problem polega na tym, że generuje on zbyt wiele danych wyjściowych i kod ten blokuje się podczas oczekiwania na zakończenie, nawet po prawidłowym wykonaniu skryptu powłoki
to trochę „dziwne”, ponieważ czasami ten kod działa dobrze, a czasem po prostu się zacina.
Kod zawiesza się w:
process.WaitForExit (ProcessTimeOutMiliseconds);
Skrypt PowerShell wykonuje się w ciągu 1–2 sekund, a limit czasu wynosi 19 sekund.
public static (bool Success, string Logs) ExecuteScript(string path, int ProcessTimeOutMiliseconds, params string[] args)
{
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
using (var outputWaitHandle = new AutoResetEvent(false))
using (var errorWaitHandle = new AutoResetEvent(false))
{
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "powershell.exe",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
WorkingDirectory = Path.GetDirectoryName(path)
};
if (args.Length > 0)
{
var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
process.StartInfo.Arguments += $" {arguments}";
}
output.AppendLine($"args:'{process.StartInfo.Arguments}'");
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
output.AppendLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
error.AppendLine(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit(ProcessTimeOutMiliseconds);
var logs = output + Environment.NewLine + error;
return process.ExitCode == 0 ? (true, logs) : (false, logs);
}
}
finally
{
outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
}
}
}
Scenariusz:
start-process $args[0] App.csproj -Wait -NoNewWindow
[string]$sourceDirectory = "\bin\Debug\*"
[int]$count = (dir $sourceDirectory | measure).Count;
If ($count -eq 0)
{
exit 1;
}
Else
{
exit 0;
}
gdzie
$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"
Edytować
Do rozwiązania @ ingen dodałem małe opakowanie, które próbuje wykonać powieszony MS Build
public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
var current = 0;
int attempts_count = 5;
bool _local_success = false;
string _local_logs = "";
while (attempts_count > 0 && _local_success == false)
{
Console.WriteLine($"Attempt: {++current}");
InternalExecuteScript(path, processTimeOutMilliseconds, out _local_logs, out _local_success, args);
attempts_count--;
}
success = _local_success;
logs = _local_logs;
}
Gdzie InternalExecuteScript
jest kod ingen
Rx
podejście zadziałało (bo w nim nie przekroczyło limitu czasu) nawet w przypadku nieudanego procesu MSBuild, który prowadzi do nieokreślonego oczekiwania? zainteresowany wiedzieć, jak to było obsługiwaneOdpowiedzi:
Zacznijmy od podsumowania zaakceptowanej odpowiedzi w powiązanym poście.
Jednak nawet zaakceptowana odpowiedź w niektórych przypadkach zmaga się z kolejnością wykonania.
To w takich sytuacjach, w których chcesz zorganizować kilka wydarzeń, Rx naprawdę błyszczy.
Uwaga: implementacja Rx .NET jest dostępna jako pakiet System.Reactive NuGet.
Zanurzmy się, aby zobaczyć, jak Rx ułatwia pracę ze zdarzeniami.
FromEventPattern
pozwala nam mapować różne wystąpienia zdarzenia na zunifikowany strumień (znany również jako obserwowalny). To pozwala nam obsługiwać zdarzenia w potoku (z semantyką podobną do LINQ).Subscribe
Przeciążenie stosowane tutaj jest zaopatrzona wAction<EventPattern<...>>
iAction<Exception>
. Za każdym razem, gdy obserwowane zdarzenie zostanie podniesione, jegosender
iargs
zostanie owinięteEventPattern
i przepchnięte przezAction<EventPattern<...>>
. Kiedy wyjątek jest zgłaszany w potoku,Action<Exception>
jest używany.Jedną z wad
Event
wzorca, wyraźnie zilustrowaną w tym przypadku użycia (i wszystkimi obejściami w odnośnym poście), jest to, że nie jest oczywiste, kiedy / gdzie wypisać procedury obsługi zdarzeń.Z Rx wrócimy
IDisposable
po dokonaniu subskrypcji. Gdy go pozbędziemy, skutecznie zakończymy subskrypcję. Dzięki dodaniuDisposeWith
metody rozszerzenia (zapożyczonej z RxUI ) możemy dodać wieleIDisposable
s doCompositeDisposable
(nazwanychdisposables
w przykładach kodu). Kiedy skończymy, możemy zakończyć wszystkie subskrypcje za pomocą jednego połączenia zdisposables.Dispose()
.Oczywiście, nic nie możemy zrobić z Rx, czego nie moglibyśmy zrobić z waniliowym .NET. Wynikowy kod jest o wiele łatwiejszy do zrozumienia, gdy dostosujesz się do funkcjonalnego sposobu myślenia.
Omówiliśmy już pierwszą część, w której mapujemy nasze wydarzenia do obserwowalnych, abyśmy mogli przejść bezpośrednio do mięsnej części. Tutaj przypisujemy naszą obserwowalność do
processExited
zmiennej, ponieważ chcemy jej użyć więcej niż raz.Po pierwsze, kiedy go aktywujemy, dzwoniąc
Subscribe
. A później, kiedy chcemy „poczekać” na jego pierwszą wartość.Jednym z problemów związanych z OP jest to, że zakłada on,
process.WaitForExit(processTimeOutMiliseconds)
że zakończy proces po przekroczeniu limitu czasu. Z MSDN :Zamiast tego, gdy upłynie limit czasu, po prostu przywraca kontrolę nad bieżącym wątkiem (tzn. Przestaje blokować). Musisz ręcznie wymusić zakończenie, gdy proces przekroczy limit czasu. Aby wiedzieć, kiedy upłynął limit czasu, możemy zmapować
Process.Exited
zdarzenie naprocessExited
obserwowalne w celu przetworzenia. W ten sposób możemy przygotować dane wejściowe dlaDo
operatora.Kod jest dość zrozumiały. Jeśli
exitedSuccessfully
proces zakończy się z gracją. Jeśli nieexitedSuccessfully
, wypowiedzenie będzie musiało zostać wymuszone. Uwaga:process.Kill()
jest wykonywany asynchronicznie, patrz uwagi . Jednakprocess.WaitForExit()
natychmiastowe wywołanie otworzy możliwość impasu. Dlatego nawet w przypadku wymuszonego zakończenia lepiej jest pozwolić, aby wszystkie artykuły jednorazowego użytku zostały wyczyszczone po zakończeniuusing
zakresu, ponieważ dane wyjściowe można uznać za przerwane / uszkodzone.try catch
Konstrukt jest zarezerwowane dla wyjątkowych przypadkach (gra słów nie przeznaczonych), gdzie już wyrównanyprocessTimeOutMilliseconds
z rzeczywistego czasu potrzebnego do zakończenia procesu. Innymi słowy, między wyścigiemProcess.Exited
a licznikiem występuje warunek wyścigu . Możliwość takiego zdarzenia ponownie zwiększa asynchroniczna naturaprocess.Kill()
. Spotkałem go raz podczas testów.Dla kompletności
DisposeWith
metoda rozszerzenia.źródło
ExecuteScriptRx
uchwytyhangs
doskonale. Niestety nadal się zawiesza, ale właśnie dodałem małe opakowanie,ExecuteScriptRx
które działa,Retry
a następnie działa dobrze. Przyczyną zawieszania się MSBUILD może być odpowiedź @Clint. PS: Ten kod sprawił, że poczułam się głupio <lol> Po raz pierwszy widzęSystem.Reactive.Linq;
Z korzyścią dla czytelników podzielę to na 2 sekcje
Sekcja A: Problem i sposób radzenia sobie z podobnymi scenariuszami
Część B: Rekreacja i rozwiązanie problemu
Część A: Problem
W twoim kodzie:
Process.WaitForExit(ProcessTimeOutMiliseconds);
Z tego czekaszProcess
na Timeout lub Wyjdź , które kiedykolwiek ma miejsce pierwszy .OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
ierrorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
dzięki temu czekasz na operację odczytuOutputData
iErrorData
przesyłania strumieniowego, aby zasygnalizować jej zakończenieProcess.ExitCode == 0
Pobiera status procesu po jego wyjściuRóżne ustawienia i ich zastrzeżenia:
ObjectDisposedException()
Process.ExitCode
powodowały błądSystem.InvalidOperationException: Process must exit before requested information can be determined
.Testowałem ten scenariusz kilkanaście razy i działa dobrze, podczas testowania użyto następujących ustawień
Zaktualizowany kod
EDYTOWAĆ:
Po wielu godzinach zabawy z MSBuild mogłem w końcu odtworzyć problem w moim systemie
Część B: Rekreacja i rozwiązanie problemu
Byłem w stanie rozwiązać to na kilka sposobów
Odradzaj proces MSBuild pośrednio poprzez CMD
Kontynuuj korzystanie z MSBuild, ale pamiętaj, aby ustawić wartość węzła Reuse na False
Nawet jeśli kompilacja równoległa nie jest włączona, nadal możesz uniemożliwić zawieszenie się procesu,
WaitForExit
uruchamiając kompilację za pośrednictwem CMD, a zatem nie tworzysz bezpośredniej zależności od procesu kompilacjiPreferowane jest drugie podejście, ponieważ nie chcesz, aby leżało zbyt wiele węzłów MSBuild.
źródło
"-nr:False","-m:3"
wydaje się, że to poprawiło zachowanie zawieszania MSBuild, któreRx solution
sprawiło, że cały proces stał się w pewnym stopniu niezawodny (czas pokaże). Chciałbym zaakceptować obie odpowiedzi lub dać dwie nagrodyRx
podejście w innym rozwiązaniu jest w stanie rozwiązać problem bez zastosowania-nr:False" ,"-m:3"
. W moim rozumieniu radzi sobie z nieokreślonym oczekiwaniem od impasu i innymi rzeczami, które omówiłem w części 1. A przyczyną problemu w części 2 jest, jak sądzę, podstawowa przyczyna problemu, z którym się spotkałeś;) Mogę się mylić i dlatego Zapytałem, tylko czas pokaże ... Pozdrawiam !!Problem polega na tym, że jeśli przekierujesz StandardOutput i / lub StandardError, bufor wewnętrzny może się zapełnić.
Aby rozwiązać wyżej wymienione problemy, możesz uruchomić proces w osobnych wątkach. Nie używam WaitForExit, wykorzystuję zdarzenie zakończenia procesu, które zwróci kod wyjścia procesu asynchronicznie, zapewniając jego zakończenie.
Powyższy kod jest testowany w bitwie pod nazwą FFMPEG.exe z argumentami wiersza poleceń. Konwertowałem pliki mp4 do plików mp3 i robiłem ponad 1000 filmów naraz. Niestety nie mam bezpośredniego doświadczenia z powłoką mocy, ale mam nadzieję, że to pomoże.
źródło
BegingOutputReadline
, a następnie wykonaćReadToEndAsync
naStandardError
?Nie jestem pewien, czy to jest twój problem, ale patrząc na MSDN wydaje się, że jest trochę dziwności z przeciążonym WaitForExit, gdy przekierowujesz wyjście asynchronicznie. Artykuł MSDN zaleca wywołanie WaitForExit, który nie przyjmuje żadnych argumentów po wywołaniu przeciążonej metody.
Strona Dokumentów znajduje się tutaj. Odpowiedni tekst:
Modyfikacja kodu może wyglądać mniej więcej tak:
źródło
process.WaitForExit()
jak wskazano w komentarzach do tej odpowiedzi .