ProcessStartInfo accroché à "WaitForExit"? Pourquoi?
j'ai le code suivant:
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args));
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents
je sais que la sortie du processus que je démarre est d'environ 7 Mo de long. L'exécuter dans la console Windows fonctionne très bien. Malheureusement programmatically cela pend indéfiniment à WaitForExit. Notez aussi que cela ne Code pas hang pour les sorties plus petites (comme 3KB).
est-il possible que la sortie standard interne de ProcessStartInfo ne puisse pas amortir 7MB? Si oui, que devrais-je faire à la place? Si ce n', ce que je fais mal?
17 réponses
le problème est que si vous redirigez StandardOutput
et/ou StandardError
le tampon interne peut devenir plein. Quel que soit l'ordre que vous utilisez, il peut y avoir un problème:
- si vous attendez que le processus se termine avant de lire
StandardOutput
le processus peut bloquer en essayant de lui écrire, de sorte que le processus ne se termine jamais. - si vous lisez à partir de
StandardOutput
en utilisant ReadToEnd alors votre "processus 1519160920" peut bloquer si le processus ne ferme jamaisStandardOutput
(par exemple si elle ne se termine jamais, ou si elle est bloquée en écriture àStandardError
).
la solution consiste à utiliser des lectures asynchrones pour s'assurer que le tampon ne se remplit pas. Pour éviter les blocages et collecter toutes les sorties de StandardOutput
et StandardError
vous pouvez faire ceci:
Modifier: voir les réponses ci-dessous pour savoir comment éviter un ObjectDisposedException si le délai se produit.
using (Process process = new Process())
{
process.StartInfo.FileName = filename;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
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();
if (process.WaitForExit(timeout) &&
outputWaitHandle.WaitOne(timeout) &&
errorWaitHandle.WaitOne(timeout))
{
// Process completed. Check process.ExitCode here.
}
else
{
// Timed out.
}
}
}
le documentation pour Process.StandardOutput
dit de lire avant d'attendre autrement vous pouvez deadlock, snippet copié ci-dessous:
// Start the child process.
Process p = new Process();
// Redirect the output stream of the child process.
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = "Write500Lines.exe";
p.Start();
// Do not wait for the child process to exit before
// reading to the end of its redirected stream.
// p.WaitForExit();
// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Marque Byers réponse est excellente, mais je voudrais juste ajouter les éléments suivants: la OutputDataReceived et ErrorDataReceived délégués doivent être supprimés avant la outputWaitHandle et errorWaitHandle sont disposées. Si le processus continue à produire des données après que le délai d'attente a été dépassé et se termine, les variables outputWaitHandle et errorWaitHandle seront accessibles après élimination.
(pour information j'ai dû ajouter cette mise en garde comme réponse car je ne pouvais pas commenter sur son post.)
le problème avec l'Exception unhandled Objectdisposed se produit lorsque le processus est chronométré. Dans ce cas, les autres parties de la condition:
if (process.WaitForExit(timeout)
&& outputWaitHandle.WaitOne(timeout)
&& errorWaitHandle.WaitOne(timeout))
ne sont pas exécutés. J'ai résolu ce problème de la manière suivante:
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
using (process = new Process())
{
// preparing ProcessStartInfo
try
{
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
outputBuilder.AppendLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
outputBuilder.AppendLine(e.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (process.WaitForExit(timeout))
{
exitCode = process.ExitCode;
}
else
{
// timed out
}
output = outputBuilder.ToString();
}
finally
{
outputWaitHandle.WaitOne(timeout);
errorWaitHandle.WaitOne(timeout);
}
}
}
Rob répondit et me sauva quelques heures de plus d'épreuves. Lire le tampon de sortie / erreur avant d'attendre:
// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
nous avons aussi cette question (ou une variante).
essayez ce qui suit:
1) Ajouter un délai à la P. WaitForExit (nnnn); où nnnn est en millisecondes.
2) Mettez l'appel ReadToEnd avant L'appel WaitForExit. Ce est ce que nous avons vu MS recommande.
il s'agit d'une solution awaitable plus moderne, basée sur une bibliothèque parallèle de tâches (TPL) pour .NET 4.5 et au-dessus.
Exemple D'Utilisation
try
{
var exitCode = await StartProcess(
"dotnet",
"--version",
@"C:\",
10000,
Console.Out,
Console.Out);
Console.WriteLine($"Process Exited with Exit Code {exitCode}!");
}
catch (TaskCanceledException)
{
Console.WriteLine("Process Timed Out!");
}
mise en œuvre
public static async Task<int> StartProcess(
string filename,
string arguments,
string workingDirectory= null,
int? timeout = null,
TextWriter outputTextWriter = null,
TextWriter errorTextWriter = null)
{
using (var process = new Process()
{
StartInfo = new ProcessStartInfo()
{
CreateNoWindow = true,
Arguments = arguments,
FileName = filename,
RedirectStandardOutput = outputTextWriter != null,
RedirectStandardError = errorTextWriter != null,
UseShellExecute = false,
WorkingDirectory = workingDirectory
}
})
{
process.Start();
var cancellationTokenSource = timeout.HasValue ?
new CancellationTokenSource(timeout.Value) :
new CancellationTokenSource();
var tasks = new List<Task>(3) { process.WaitForExitAsync(cancellationTokenSource.Token) };
if (outputTextWriter != null)
{
tasks.Add(ReadAsync(
x =>
{
process.OutputDataReceived += x;
process.BeginOutputReadLine();
},
x => process.OutputDataReceived -= x,
outputTextWriter,
cancellationTokenSource.Token));
}
if (errorTextWriter != null)
{
tasks.Add(ReadAsync(
x =>
{
process.ErrorDataReceived += x;
process.BeginErrorReadLine();
},
x => process.ErrorDataReceived -= x,
errorTextWriter,
cancellationTokenSource.Token));
}
await Task.WhenAll(tasks);
return process.ExitCode;
}
}
/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as cancelled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(
this Process process,
CancellationToken cancellationToken = default(CancellationToken))
{
process.EnableRaisingEvents = true;
var taskCompletionSource = new TaskCompletionSource<object>();
EventHandler handler = null;
handler = (sender, args) =>
{
process.Exited -= handler;
taskCompletionSource.TrySetResult(null);
};
process.Exited += handler;
if (cancellationToken != default(CancellationToken))
{
cancellationToken.Register(
() =>
{
process.Exited -= handler;
taskCompletionSource.TrySetCanceled();
});
}
return taskCompletionSource.Task;
}
/// <summary>
/// Reads the data from the specified data recieved event and writes it to the
/// <paramref name="textWriter"/>.
/// </summary>
/// <param name="addHandler">Adds the event handler.</param>
/// <param name="removeHandler">Removes the event handler.</param>
/// <param name="textWriter">The text writer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ReadAsync(
this Action<DataReceivedEventHandler> addHandler,
Action<DataReceivedEventHandler> removeHandler,
TextWriter textWriter,
CancellationToken cancellationToken = default(CancellationToken))
{
var taskCompletionSource = new TaskCompletionSource<object>();
DataReceivedEventHandler handler = null;
handler = new DataReceivedEventHandler(
(sender, e) =>
{
if (e.Data == null)
{
removeHandler(handler);
taskCompletionSource.TrySetResult(null);
}
else
{
textWriter.WriteLine(e.Data);
}
});
addHandler(handler);
if (cancellationToken != default(CancellationToken))
{
cancellationToken.Register(
() =>
{
removeHandler(handler);
taskCompletionSource.TrySetCanceled();
});
}
return taskCompletionSource.Task;
}
crédit à EM0 pour https://stackoverflow.com/a/17600012/4151626
les autres solutions (y compris EM0's) toujours bloquées pour mon application, en raison des délais internes et de l'utilisation de la production standard et StandardError par l'application générée. Voici ce qui a fonctionné pour moi:
Process p = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = exe,
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
p.Start();
string cv_error = null;
Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); });
et.Start();
string cv_out = null;
Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); });
ot.Start();
p.WaitForExit();
ot.Join();
et.Join();
Edit: ajout de L'initialisation de StartInfo à l'exemple de code
Je l'ai résolu de cette façon:
Process proc = new Process();
proc.StartInfo.FileName = batchFile;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proc.Start();
StreamWriter streamWriter = proc.StandardInput;
StreamReader outputReader = proc.StandardOutput;
StreamReader errorReader = proc.StandardError;
while (!outputReader.EndOfStream)
{
string text = outputReader.ReadLine();
streamWriter.WriteLine(text);
}
while (!errorReader.EndOfStream)
{
string text = errorReader.ReadLine();
streamWriter.WriteLine(text);
}
streamWriter.Close();
proc.WaitForExit();
j'ai redirigé à la fois l'entrée, la sortie et l'erreur et j'ai traité la lecture des flux de sortie et d'erreur. Cette solution fonctionne pour SDK 7-8.1, à la fois pour Windows 7 et Windows 8
j'ai essayé de faire un cours qui résoudrait votre problème en utilisant le flux asynchrone lire, en prenant en compte Mark Byers, Rob, stevejay réponses. Ce faisant, j'ai réalisé qu'il y avait un bug lié à la lecture asynchrone du flux de sortie du processus.
j'ai signalé ce bug chez Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134
résumé:
vous ne pouvez pas faire que:
process.BeginOutputReadLine (); process.Start ();
vous recevrez le système.InvalidOperationException: StandardOut a pas été réexpédiés ou le processus n'a pas encore commencé.
============================================================================================================================
alors vous devez démarrer la sortie asynchrone lire après le processus est commencé:
process.Start (); process.BeginOutputReadLine ();
faire ainsi, faire une condition de course parce que le flux de sortie peut recevoir les données avant de vous le mettre en asynchrone:
process.Start();
// Here the operating system could give the cpu to another thread.
// For example, the newly created thread (Process) and it could start writing to the output
// immediately before next line would execute.
// That create a race condition.
process.BeginOutputReadLine();
============================================================================================================================
alors certains pourraient dire que vous suffit de lire le flux avant de le régler sur asynchrone. Mais le même problème se produit. Y sera une condition de course entre la lecture synchrone et flux en mode asynchrone.
============================================================================================================================
il n'y a aucun moyen d'obtenir une lecture asynchrone sûre d'un flux de sortie d'un processus dans la "Process" et" ProcessStartInfo " ont été conçu.
vous êtes probablement mieux utiliser asynchrone lire comme suggéré par d'autres utilisateurs pour votre cas. Mais vous devez être conscient que vous pourriez manquer certaines informations en raison de l'état de la course.
aucune des réponses ci-dessus ne fait le travail.
Rob solution se bloque et 'A' obtenir la solution disposé d'exception.(J'ai essayé les "solutions" des autres réponses).
J'ai donc décidé de proposer une autre solution:
public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode)
{
string outputLocal = ""; int localExitCode = -1;
var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
{
outputLocal = process.StandardOutput.ReadToEnd();
process.WaitForExit();
localExitCode = process.ExitCode;
}, token);
if (task.Wait(timeoutSec, token))
{
output = outputLocal;
exitCode = localExitCode;
}
else
{
exitCode = -1;
output = "";
}
}
using (var process = new Process())
{
process.StartInfo = ...;
process.Start();
string outputUnicode; int exitCode;
GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode);
}
ce code a été débogué et fonctionne parfaitement.
Introduction
réponse actuellement acceptée ne fonctionne pas (sauf pour les lancers) et il y a trop de solutions de rechange mais pas de code complet. Cela fait évidemment perdre beaucoup de temps aux gens parce que c'est une question populaire.
combinant la réponse de Mark Byers et la réponse de Karol Tyl j'ai écrit le code complet basé sur la façon dont je veux utiliser le processus.Méthode de démarrage.
Utilisation
je l'ai utilisé pour créer progrès dialogue autour des commandes git. Voici comment je l'ai utilisé:
private bool Run(string fullCommand)
{
Error = "";
int timeout = 5000;
var result = ProcessNoBS.Start(
filename: @"C:\Program Files\Git\cmd\git.exe",
arguments: fullCommand,
timeoutInMs: timeout,
workingDir: @"C:\test");
if (result.hasTimedOut)
{
Error = String.Format("Timeout ({0} sec)", timeout/1000);
return false;
}
if (result.ExitCode != 0)
{
Error = (String.IsNullOrWhiteSpace(result.stderr))
? result.stdout : result.stderr;
return false;
}
return true;
}
en théorie, vous pouvez aussi combiner stdout et stderr, mais je ne l'ai pas testé.
Code
public struct ProcessResult
{
public string stdout;
public string stderr;
public bool hasTimedOut;
private int? exitCode;
public ProcessResult(bool hasTimedOut = true)
{
this.hasTimedOut = hasTimedOut;
stdout = null;
stderr = null;
exitCode = null;
}
public int ExitCode
{
get
{
if (hasTimedOut)
throw new InvalidOperationException(
"There was no exit code - process has timed out.");
return (int)exitCode;
}
set
{
exitCode = value;
}
}
}
public class ProcessNoBS
{
public static ProcessResult Start(string filename, string arguments,
string workingDir = null, int timeoutInMs = 5000,
bool combineStdoutAndStderr = false)
{
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
using (var process = new Process())
{
var info = new ProcessStartInfo();
info.CreateNoWindow = true;
info.FileName = filename;
info.Arguments = arguments;
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
if (workingDir != null)
info.WorkingDirectory = workingDir;
process.StartInfo = info;
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = combineStdoutAndStderr
? stdout : new StringBuilder();
var result = new ProcessResult();
try
{
process.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
outputWaitHandle.Set();
else
stdout.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
errorWaitHandle.Set();
else
stderr.AppendLine(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (process.WaitForExit(timeoutInMs))
result.ExitCode = process.ExitCode;
// else process has timed out
// but that's already default ProcessResult
result.stdout = stdout.ToString();
if (combineStdoutAndStderr)
result.stderr = null;
else
result.stderr = stderr.ToString();
return result;
}
finally
{
outputWaitHandle.WaitOne(timeoutInMs);
errorWaitHandle.WaitOne(timeoutInMs);
}
}
}
}
}
je sais que c'est vieux mais, après avoir lu toute cette page aucune des solutions ne fonctionnait pour moi, bien que je n'ai pas essayé Muhammad Rehan car le code était un peu difficile à suivre, bien que je suppose qu'il était sur la bonne voie. Quand je dis que ça n'a pas fonctionné, ce n'est pas tout à fait vrai, parfois ça marchait bien, je suppose que ça a quelque chose à voir avec la longueur de la sortie avant une marque EOF.
quoi qu'il en soit, la solution qui a fonctionné pour moi était d'utiliser différents les threads de lire le StandardOutput et StandardError et écrire les messages.
StreamWriter sw = null;
var queue = new ConcurrentQueue<string>();
var flushTask = new System.Timers.Timer(50);
flushTask.Elapsed += (s, e) =>
{
while (!queue.IsEmpty)
{
string line = null;
if (queue.TryDequeue(out line))
sw.WriteLine(line);
}
sw.FlushAsync();
};
flushTask.Start();
using (var process = new Process())
{
try
{
process.StartInfo.FileName = @"...";
process.StartInfo.Arguments = $"...";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
var outputRead = Task.Run(() =>
{
while (!process.StandardOutput.EndOfStream)
{
queue.Enqueue(process.StandardOutput.ReadLine());
}
});
var errorRead = Task.Run(() =>
{
while (!process.StandardError.EndOfStream)
{
queue.Enqueue(process.StandardError.ReadLine());
}
});
var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0);
if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) &&
process.WaitForExit((int)timeout.TotalMilliseconds))
{
if (process.ExitCode != 0)
{
throw new Exception($"Failed run... blah blah");
}
}
else
{
throw new Exception($"process timed out after waiting {timeout}");
}
}
catch (Exception e)
{
throw new Exception($"Failed to succesfully run the process.....", e);
}
}
}
Espérons que cela aide quelqu'un, qui pensaient que cela pourrait être si dur!
après avoir lu tous les billets ici, je me suis décidé sur la solution consolidée de Marko Avlijaš. cependant , il n'a pas résolu tous mes problèmes.
dans notre environnement, nous avons un service Windows qui est prévu pour fonctionner des centaines de différentes .chauve. cmd .EXE.,.. etc. des dossiers qui se sont accumulés au fil des ans et qui ont été rédigés par de nombreuses personnes et dans des styles différents. Nous n'avons aucun contrôle sur l'écriture des programmes et des scripts, nous sont simplement responsables de l'ordonnancement, de l'exécution et des rapports sur le succès/l'échec.
Alors j'ai essayé à peu près toutes les suggestions ici avec différents niveaux de succès. La réponse de Marko était presque parfaite, mais lorsqu'elle était exécutée en tant que service, elle ne saisissait pas toujours stdout. Je n'ai jamais pu le bas de la pourquoi pas.
la seule solution que nous avons trouvé qui fonctionne dans tous nos cas est celle-ci: http://csharptest.net/319/using-the-processrunner-class/index.html
ce post peut-être périmé, mais j'ai découvert la cause principale pourquoi il est généralement hang est due à débordement de la pile pour le redirectStandardoutput ou si vous avez redirectStandarderror.
comme les données de sortie ou les données d'erreur sont grandes, il causera un temps mort car il est encore en traitement pour une durée indéterminée.
donc, pour résoudre ce problème:
p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
je pense que c'est simple et la meilleure approche (nous n'avons pas besoin AutoResetEvent
):
public static string GGSCIShell(string Path, string Command)
{
using (Process process = new Process())
{
process.StartInfo.WorkingDirectory = Path;
process.StartInfo.FileName = Path + @"\ggsci.exe";
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.UseShellExecute = false;
StringBuilder output = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
output.AppendLine(e.Data);
}
};
process.Start();
process.StandardInput.WriteLine(Command);
process.BeginOutputReadLine();
int timeoutParts = 10;
int timeoutPart = (int)TIMEOUT / timeoutParts;
do
{
Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting)
process.StandardInput.WriteLine("exit");
timeoutParts--;
}
while (!process.WaitForExit(timeoutPart) && timeoutParts > 0);
if (timeoutParts <= 0)
{
output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------");
}
string result = output.ToString();
return result;
}
}
j'avais le même problème, mais la raison était différente. Cela se produirait cependant sous Windows 8, mais pas sous Windows 7. La ligne suivante semble avoir causé le problème.
pProcess.StartInfo.UseShellExecute = False
la solution était de ne pas désactiver Useshellexécute. J'ai maintenant reçu une fenêtre popup Shell, qui est indésirable, mais beaucoup mieux que le programme attendant rien de particulier à se produire. J'ai donc ajouté les travaux suivants pour cela:
pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden
maintenant, la seule chose qui me dérange est de savoir pourquoi cela se passe sous Windows 8.