Beginnen wir mit einer Zusammenfassung der akzeptierten Antwort in einem verwandten Beitrag.
Das Problem ist, dass der interne Puffer voll werden kann, wenn Sie StandardOutput und / oder StandardError umleiten. Unabhängig von der Reihenfolge, die Sie verwenden, kann ein Problem auftreten:
- Wenn Sie warten, bis der Prozess beendet ist, bevor Sie StandardOutput lesen, kann der Prozess den Versuch blockieren, darauf zu schreiben, sodass der Prozess niemals endet.
- Wenn Sie mit ReadToEnd aus StandardOutput lesen, kann Ihr Prozess blockieren, wenn der Prozess StandardOutput nie schließt (z. B. wenn er nie beendet wird oder wenn das Schreiben in StandardError blockiert ist).
Selbst die akzeptierte Antwort hat jedoch in bestimmten Fällen Probleme mit der Reihenfolge der Ausführung.
BEARBEITEN: In den folgenden Antworten erfahren Sie, wie Sie eine ObjectDisposedException vermeiden, wenn das Zeitlimit auftritt.
In solchen Situationen, in denen Sie mehrere Ereignisse orchestrieren möchten, strahlt Rx wirklich.
Beachten Sie, dass die .NET-Implementierung von Rx als System.Reactive NuGet-Paket verfügbar ist.
Lassen Sie uns einen Blick darauf werfen, wie Rx die Arbeit mit Ereignissen erleichtert.
// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
.Subscribe(
eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
exception => error.AppendLine(exception.Message)
).DisposeWith(disposables);
FromEventPatternErmöglicht es uns, verschiedene Ereignisse eines Ereignisses einem einheitlichen Stream (auch als beobachtbar bezeichnet) zuzuordnen. Dies ermöglicht es uns, die Ereignisse in einer Pipeline zu behandeln (mit LINQ-ähnlicher Semantik). Die Subscribehier verwendete Überlast ist mit einem Action<EventPattern<...>>und einem versehen Action<Exception>. Jedes Mal , wenn das beobachtete Ereignis ausgelöst wird, sein senderund argswird durch eingewickelt werden EventPatternund durch die geschoben Action<EventPattern<...>>. Wenn eine Ausnahme in der Pipeline ausgelöst wird, Action<Exception>wird verwendet.
Einer der Nachteile des EventMusters, der in diesem Anwendungsfall (und durch alle Problemumgehungen im referenzierten Beitrag) deutlich dargestellt wird, besteht darin, dass nicht ersichtlich ist, wann / wo die Ereignishandler abgemeldet werden müssen.
Mit Rx erhalten wir eine zurück, IDisposablewenn wir ein Abonnement abschließen. Wenn wir darüber verfügen, beenden wir das Abonnement effektiv. Mit der Hinzufügung der DisposeWithErweiterungsmethode (von RxUI ausgeliehen ) können wir IDisposableeinem CompositeDisposable( disposablesin den Codebeispielen genannten) mehrere s hinzufügen . Wenn wir alle fertig sind, können wir alle Abonnements mit einem Anruf beenden disposables.Dispose().
Natürlich können wir mit Rx nichts tun, was wir mit Vanilla .NET nicht tun könnten. Der resultierende Code ist viel einfacher zu überlegen, sobald Sie sich an die funktionale Denkweise angepasst haben.
public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
using (var process = new Process())
using (var disposables = new CompositeDisposable())
{
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}'");
// Raise the Process.Exited event when the process terminates.
process.EnableRaisingEvents = true;
// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
.Subscribe(
eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
exception => error.AppendLine(exception.Message)
).DisposeWith(disposables);
// Subscribe to ErrorData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.ErrorDataReceived))
.Subscribe(
eventPattern => error.AppendLine(eventPattern.EventArgs.Data),
exception => error.AppendLine(exception.Message)
).DisposeWith(disposables);
var processExited =
// Observable will tick when the process has gracefully exited.
Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
// First two lines to tick true when the process has gracefully exited and false when it has timed out.
.Select(_ => true)
.Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
// Force termination when the process timed out
.Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );
// Subscribe to the Process.Exited event.
processExited
.Subscribe()
.DisposeWith(disposables);
// Start process(ing)
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();
logs = output + Environment.NewLine + error;
success = process.ExitCode == 0;
}
}
Wir haben bereits den ersten Teil besprochen, in dem wir unsere Ereignisse Observablen zuordnen, damit wir direkt zum fleischigen Teil springen können. Hier weisen wir der processExitedVariablen unser Observable zu , weil wir es mehr als einmal verwenden möchten.
Erstens, wenn wir es aktivieren, indem wir anrufen Subscribe. Und später, wenn wir auf seinen ersten Wert warten wollen.
var processExited =
// Observable will tick when the process has gracefully exited.
Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
// First two lines to tick true when the process has gracefully exited and false when it has timed out.
.Select(_ => true)
.Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
// Force termination when the process timed out
.Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );
// Subscribe to the Process.Exited event.
processExited
.Subscribe()
.DisposeWith(disposables);
// Start process(ing)
...
// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();
Eines der Probleme mit OP besteht darin, dass davon process.WaitForExit(processTimeOutMiliseconds)ausgegangen wird, dass der Prozess beendet wird, wenn das Zeitlimit überschritten wird. Von MSDN :
Weist die Prozesskomponente an, die angegebene Anzahl von Millisekunden auf das Beenden des zugeordneten Prozesses zu warten.
Wenn das Zeitlimit überschritten wird, wird lediglich die Kontrolle an den aktuellen Thread zurückgegeben (dh es wird nicht mehr blockiert). Sie müssen die Beendigung manuell erzwingen, wenn der Prozess abläuft. Um zu wissen, wann Process.Exitedeine processExitedZeitüberschreitung aufgetreten ist, können wir das Ereignis einem Observable zur Verarbeitung zuordnen. Auf diese Weise können wir die Eingabe für den DoBediener vorbereiten .
Der Code ist ziemlich selbsterklärend. Wenn exitedSuccessfullyder Prozess ordnungsgemäß beendet wurde. Wenn nicht exitedSuccessfully, muss die Kündigung erzwungen werden. Beachten Sie, dass process.Kill()asynchron ausgeführt wird, siehe Anmerkungen . Wenn Sie jedoch process.WaitForExit()direkt danach anrufen, besteht erneut die Möglichkeit von Deadlocks. Selbst im Fall einer erzwungenen Kündigung ist es besser, alle Einwegartikel nach Ablauf des usingGültigkeitsbereichs bereinigen zu lassen , da die Ausgabe ohnehin als unterbrochen / beschädigt angesehen werden kann.
Das try catchKonstrukt ist für den Ausnahmefall reserviert (kein Wortspiel beabsichtigt), in dem Sie sich an processTimeOutMillisecondsder tatsächlichen Zeit ausgerichtet haben, die der Prozess zum Abschließen benötigt. Mit anderen Worten, zwischen dem Process.ExitedEreignis und dem Timer tritt eine Rennbedingung auf . Die Möglichkeit, dass dies geschieht, wird erneut durch die asynchrone Natur von vergrößert process.Kill(). Ich habe es einmal beim Testen angetroffen.
Der Vollständigkeit halber die DisposeWithErweiterungsmethode.
/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableExtensions
{
/// <summary>
/// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
/// </summary>
public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
where T : IDisposable
{
if (compositeDisposable == null)
{
throw new ArgumentNullException(nameof(compositeDisposable));
}
compositeDisposable.Add(item);
return item;
}
}