ProcessStartInfo “WaitForExit” üzerinde asılı mı kalıyor? Neden?


187

Takip koduna sahibim:

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

Başladığım süreçten elde edilen çıktının 7MB uzunluğunda olduğunu biliyorum. Windows konsolunda çalıştırmak iyi çalışıyor. Maalesef programlı olarak bu WaitForExit'te süresiz olarak duruyor. Ayrıca, kodun daha küçük çıkışlar için kilitlenmediğini unutmayın (3KB gibi).

ProcessStartInfo içindeki dahili StandardOutput'un 7MB'yi arabelleğe alamaması mümkün mü? Öyleyse bunun yerine ne yapmalıyım? Değilse, ne yanlış yapıyorum?


bu konuda tam kaynak kodu ile herhangi bir son çözüm?
Kiquenet

2
Aynı sorunla karşılaştım ve bunu nasıl çözebildim stackoverflow.com/questions/2285288/…
Bedasso

6
Evet, son çözüm: son iki satırı değiştirin. Bu öyle kılavuzda .
Amit Naidu

4
from msdn: Kod örneği p.WaitForExit önce p.StandardOutput.ReadToEnd çağırarak bir kilitlenme koşulunu önler. Üst işlem p.StandardOutput.ReadToEnd öğesinden önce p.WaitForExit öğesini çağırırsa ve alt işlem, yeniden yönlendirilen akışı doldurmak için yeterli metin yazarsa bir kilitlenme koşulu oluşabilir. Üst süreç süresiz olarak alt sürecin çıkmasını beklerdi. Alt süreç, üst öğenin tam StandardOutput akışından okumasını süresiz olarak bekler.
Carlos Liu

bunu düzgün bir şekilde yapmanın ne kadar karmaşık olduğu biraz can sıkıcı. Daha basit komut satırı yönlendirmeleri> outputfile :) ile çalışmaktan memnun
oldum

Yanıtlar:


393

Sorun, yönlendirme yaparsanız StandardOutputve / veya StandardErrordahili arabellek doluysa. Kullandığınız sipariş ne olursa olsun, bir sorun olabilir:

  • İşlemin okumadan önce çıkmasını beklerseniz, işlem StandardOutputyazmaya çalışmayı engelleyebilir, böylece işlem asla bitmez.
  • Eğer gelen okursanız StandardOutputReadToEnd kullanarak daha sonra da işlem süreci hiç kapanmaz ise engelleyebilirsiniz StandardOutput(Bu kadar yazma engellenirse o sonlandırır asla örneğin, ya da StandardError).

Çözüm, arabelleğin dolmamasını sağlamak için eşzamansız okumalar kullanmaktır. Herhangi bir çıkmazdan kaçınmak ve her ikisinden de tüm çıktıları toplamak StandardOutputve StandardErrorbunu yapabilirsiniz:

EDIT: Zaman aşımı oluşursa bir ObjectDisposedException önlemek için aşağıdaki yanıtlara bakın .

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.
        }
    }
}

11
Çıktıyı yönlendirmek hakkında hiçbir fikrim yoktu, soruna neden oluyordu ama yeterince oldu. Bu konuda kafamı döverek 4 saat geçirdim ve yazınızı okuduktan sonra 5 dakika içinde sabitledim. İyi iş!
Ben Gripka

1
@AlexPeck Sorun bunu bir konsol uygulaması olarak çalıştırıyordu. Hans Passant sorunu burada belirledi: stackoverflow.com/a/16218470/279516
Bob Horn

5
komut istemi her kapatıldığında, bu görünür: mscorlib.dll
dosyasında

3
Yukarıda @ user1663380 tarafından açıklananla benzer bir sorun yaşadık. usingOlay işleyicileri için deyimlerin sürecin kendisinin deyiminin üzerinde olması mümkün olduğunu düşünüyor musunuz using?
Dan Forbes

2
Bekleme kollarının gerekli olduğunu düşünmüyorum. Msdn'ye göre, WaitForExit'in zaman aşımı olmayan sürümü ile sonlandırın: Standart çıktı zaman uyumsuz olay işleyicilere yeniden yönlendirildiğinde, bu yöntem döndüğünde çıktı işlemenin tamamlanmamış olması mümkündür. Eşzamansız olay işlemenin tamamlandığından emin olmak için, bu aşırı yükten bir doğru aldıktan sonra parametre almayan WaitForExit () aşırı yükünü çağırın.
Patrick

98

Dokümantasyon için Process.StandardOutputaksi takdirde kilitlenmeye beklemek önce aşağıya kopyalanmıştır pasajı, okumak için diyor ki:

 // 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();

14
Bu sadece ortamımın bir sonucu olup olmadığından% 100 emin değilim, ama belirlediyseniz RedirectStandardOutput = true;ve kullanmazsanız p.StandardOutput.ReadToEnd();bir kilitlenme / asma elde edersiniz.
Chris S

3
Doğru. Ben de benzer bir durumdaydım. Bir işlemde ffmpeg ile dönüştürürken hiçbir sebepten dolayı StandardError'u yeniden yönlendiriyordum, bir kilitlenme oluşturmak için StandardError akışında yeterince yazıyordu.
Léon Pelletier

Bu, standart çıktıyı yeniden yönlendirip okurken bile benim için hala asılı.
user3791372

@ user3791372 Sanırım bu yalnızca StandardOutput'un arkasındaki arabellek tam olarak doldurulmamışsa uygulanabilir. Burada MSDN adaletini yapmıyor. Okumanızı
Cary

19

Mark Byers'ın cevabı mükemmel, ama ben sadece aşağıdakileri eklerdim:

OutputDataReceivedVe ErrorDataReceiveddelegeler önce kaldırılması gerekiroutputWaitHandle ve errorWaitHandlebertaraf olsun. Süre aşımı zaman aşımından sonra veri çıkışı devam ederse ve sonlandırılırsa, outputWaitHandleve errorWaitHandledeğişkenlere atıldıktan sonra erişilir.

(FYI görevine yorum yapamadığım için bu uyarıyı cevap olarak eklemek zorunda kaldım.)


2
Belki CancelOutputRead'i aramak daha iyi olur ?
Mark Byers

Bu cevaba Mark'ın düzenlenmiş kodunu eklemek harika olurdu! Şu anda aynı sorunu yaşıyorum.
ianbailey

8
@ianbailey bu çözmek için en kolay yolu kullanarak (AutoResetEvent errorWaitHandle ...) içindeki (... Süreç p) kullanarak koymaktır
Didier A.

18

Bu, .NET 4.5 ve üstü için daha modern, beklenebilir, Görev Paralel Kütüphanesi (TPL) tabanlı bir çözümdür.

Kullanım Örneği

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!");
}

uygulama

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
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

        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;
}

2
bugüne kadarki en iyi ve en eksiksiz cevap
TermoTux

1
Bazı nedenlerden dolayı, bu benim için çalışan tek çözümdü, uygulama durdu.
Jack

1
Görünüşe göre, işlem başladıktan sonra değil, Exited olayı eklenmeden önce sona eren durumu ele almıyorsunuz. Benim önerim - tüm kayıtlardan sonra işleme başlamak.
Stas Boyarincev

@StasBoyarincev Teşekkürler, güncellendi. StackOverflow yanıtını bu değişiklikle güncellemeyi unuttum.
Muhammad Rehan Saeed

1
@MuhammadRehanSaeed Yine başka bir şey - process.Start işleminden önce process.BeginOutputReadLine () veya process.BeginErrorReadLine () çağrısına izin verilmiyor gibi görünüyor. Bu durumda hatayı alıyorum: StandardOut yeniden yönlendirilmedi veya işlem henüz başlamadı.
Stas Boyarincev

17

İşlenmemiş ObjectDisposedException ile ilgili sorun, işlem zaman aşımına uğradığında oluşur. Bu durumda, durumun diğer kısımları:

if (process.WaitForExit(timeout) 
    && outputWaitHandle.WaitOne(timeout) 
    && errorWaitHandle.WaitOne(timeout))

yürütülmez. Bu sorunu şu şekilde çözdüm:

using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
    using (Process 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
                    {
                        errorBuilder.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);
        }
    }
}

1
tamlık uğruna,
doğruya

ve işlem kullanıcı girişi (örneğin bir şey yazın) isteyebilir, çünkü kullanıcının hızlı olmasını istemiyorum çünkü sonunda zaman aşımlarını kaldırdım
knocte

Neden değişti outputve errorhiç outputBuilder? Birisi lütfen işe yarayan eksiksiz bir cevap verebilir mi?
Marko Avlijaš

System.ObjectDisposedException: Benim için de bu sürümde güvenli tanıtıcı kapatıldı
Matt

8

Rob cevap verdi ve bana birkaç saat daha deneme verdi. Beklemeden önce çıktı / hata arabelleğini okuyun:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();

1
ama aradıktan sonra daha fazla veri gelirse WaitForExit()?
knocte

@knocte testlerime dayalı ReadToEndveya benzeri yöntemler (gibi StandardOutput.BaseStream.CopyTo) TÜM veriler okunduktan sonra dönecektir . peşinden hiçbir şey gelmeyecek
S.Serpooshan

ReadToEnd () 'nin de çıkış için beklediğini mi söylüyorsunuz?
knocte

2
@knocte microsoft tarafından oluşturulan bir API'yı anlamaya mı çalışıyorsunuz?
aaaaaa

Karşılık gelen MSDN sayfasının sorunu, StandardOutput'un arkasındaki arabelleğin doldurulabileceğini ve bu durumda çocuğun yazmayı durdurması ve arabellek boşalıncaya kadar beklemesi gerektiğinin açıklanmamasıydı (üst veri, arabellekteki verileri okur) . ReadToEnd () yalnızca arabellek kapatılıncaya veya arabellek dolana veya arabellek dolu değilken çocuk çıkıncaya kadar okunabilir. Benim anlayışım bu.
Cary

7

Bu sorun da var (veya bir varyant).

Takip etmeyi dene:

1) p.WaitForExit'e (nnnn) bir zaman aşımı ekleyin; burada nnnn milisaniye cinsindendir.

2) ReadToEnd çağrısını WaitForExit çağrısından önce koyun. Bu ise biz MS tavsiye gördüklerimi.


5

Https://stackoverflow.com/a/17600012/4151626 için EM0'e teşekkür ederiz

Diğer çözümler (EM0'ler dahil), dahili zaman aşımları ve ortaya çıkan uygulama tarafından hem StandardOutput hem de StandardError kullanımı nedeniyle, başvurum için hala kilitlendi. İşte benim için işe yarayan:

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();

Düzenleme: kod örneği için StartInfo başlatma eklendi


Kullandığım bu ve artık bir kilitlenme ile ilgili hiçbir zaman sorun yaşamadım.
Roemer

3

Bu şekilde çözdüm:

            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();

Hem girdi, çıktı ve hata yönlendirme yaptım, hem de çıktı ve hata akışlarından okuma işledim. Bu çözüm, Windows 7 ve Windows 8 için SDK 7- 8.1 için çalışır


2
Elina: Cevabınız için teşekkürler. Bu MSDN belgesinin ( msdn.microsoft.com/en-us/library/… ) altında, hem yeniden yönlendirilen stdout'un hem de stderr akışlarının eşzamanlı olarak sonuna okuduğunuzda olası çıkmaza karşı uyarı veren bazı notlar vardır . Çözümünüzün bu soruna duyarlı olup olmadığını söylemek zor. Ayrıca, işlemin stdout / stderr çıktısını doğrudan girdi olarak gönderdiğiniz görünüyor. Neden? :)
Matthew Piatt

3

Mark Byers, Rob, stevejay cevaplarını göz önünde bulundurarak asenkron akış okundu kullanarak sorununuzu çözecek bir sınıf yapmaya çalıştım. Bunu yaparak, eşzamansız işlem çıktı akışının okunmasıyla ilgili bir hata olduğunu fark ettim.

Microsoft'ta bu hatayı bildirdim: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Özet:

Bunu yapamazsın:

process.BeginOutputReadLine (); Process.Start ();

System.InvalidOperationException alacaksınız: StandardOut yeniden yönlendirilmedi veya işlem henüz başlamadı.

================================================== ================================================== ========================

Ardından, işlem başladıktan sonra eşzamansız çıktı okumasını başlatmanız gerekir:

Process.Start (); process.BeginOutputReadLine ();

Bunu yaptığınızda, bir yarış durumu yapın, çünkü çıkış akışı senkronize olmayana ayarlamadan önce veri alabilir:

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();

================================================== ================================================== ========================

Sonra bazı insanlar, senkronize olmayana ayarlamadan önce akışı okumak zorunda olduğunuzu söyleyebilir. Ancak aynı sorun ortaya çıkar. Senkronize okuma ve akışı senkronize olmayan moda ayarlama arasında bir yarış durumu olacaktır.

================================================== ================================================== ========================

Bir işlemin çıkış akışının "Süreci" ve "ProcessStartInfo" tasarlandığı şekilde güvenli eşzamansız olarak okunmasını sağlamanın bir yolu yoktur.

Muhtemelen vakanız için diğer kullanıcılar tarafından önerilen gibi eşzamansız okuma kullanmanız daha iyi olacaktır. Ancak, yarış durumu nedeniyle bazı bilgileri kaçırabileceğinizi bilmelisiniz.


1

Bunun basit ve daha iyi bir yaklaşım olduğunu düşünüyorum (ihtiyacımız yok 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;
    }
}

Doğru, ama .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"kodunuzu basitleştirmek için de yapmıyor musunuz? Ya da "echo command | " + Path + @"\ggsci.exe"gerçekten ayrı bir obeycommand.txt dosyası kullanmak istemiyorsanız, eşdeğer bir şey olabilir.
Amit Naidu

3
Çözümünüzün AutoResetEvent'e ihtiyacı yoktur ancak siz anket yaparsınız. Olay kullanmak yerine yoklama yaptığınızda (kullanılabilir olduğunda) CPU'yu hiçbir nedenle kullanmazsınız ve bu da kötü bir programcı olduğunuzu gösterir. AutoResetEvent kullanan çözümle karşılaştırıldığında çözümünüz gerçekten kötü. (Ama sana -1 vermedim çünkü yardım etmeye çalıştın!).
Eric Ouellet

1

Yukarıdaki cevapların hiçbiri işi yapmıyor.

Rob çözümü askıda kalıyor ve 'Mark Byers' çözümü atılan istisnayı alıyor. (Diğer cevapların "çözümlerini" denedim).

Bu yüzden başka bir çözüm önermeye karar verdim:

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);
}

Bu kod hata ayıkladı ve mükemmel çalışıyor.


1
İyi! GetProcessOutputWithTimeoutyöntem çağrılırken jeton parametresinin sağlanmadığını unutmayın .
S.Serpooshan

1

Giriş

Şu anda kabul edilen cevap çalışmıyor (istisna atıyor) ve çok fazla geçici çözüm var, ancak tam kod yok. Bu çok fazla insanın zamanını boşa harcıyor çünkü bu popüler bir soru.

Mark Byers ve Karol Tyl'ın cevabını birleştirerek Process.Start yöntemini nasıl kullanmak istediğime dayanarak tam kod yazdım.

kullanım

Git komutları etrafında ilerleme iletişim kutusu oluşturmak için kullandım. Ben böyle kullandım:

    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;
    }

Teoride stdout ve stderr'i de birleştirebilirsiniz, ancak bunu test etmedim.

kod

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);
                }
            }
        }
    }
}

Hala System.ObjectDisposedException olsun: Bu sürümde güvenli tanıtıcı da kapatıldı.
Matt

1

Bunun eski olduğunu biliyorum ama bu sayfayı okuduktan sonra çözümlerin hiçbiri işe yaramadı, ancak kodun takip edilmesi biraz zor olduğu için Muhammed Rehan'ı denemedim, ancak doğru yolda olduğunu tahmin ettim . Tamamen doğru olmayan bir işe yaramadığını söylediğimde, bazen işe yarayabilir, sanırım bir EOF işaretinden önce çıktının uzunluğu ile ilgili bir şey.

Her neyse, benim için çalışan çözüm, StandardOutput ve StandardError okumak ve iletileri yazmak için farklı iş parçacıkları kullanmaktı.

        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);
            }
        }
    }

Umarım bu çok zor olabileceğini düşünen birine yardım eder!


İstisna: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. Nasıl / nerede swtanımlanmalıdır?
wallyk

1

Buradaki tüm mesajları okuduktan sonra, Marko Avlijaš'ın konsolide çözümüne karar verdim. Ancak , tüm sorunlarımı çözmedi.

Çevremizde, yıllar boyunca biriken ve birçok farklı kişi tarafından ve farklı tarzlarda yazılmış yüzlerce farklı .bat .cmd .exe, ... vb dosyalarını çalıştırması planlanan bir Windows Hizmetimiz var. Programların ve komut dosyalarının yazımı üzerinde hiçbir kontrole sahip değiliz, sadece başarı / başarısızlık hakkında planlama, çalıştırma ve raporlamadan sorumluyuz.

Bu yüzden tüm önerileri hemen hemen farklı seviyelerde denedim. Marko'nun yanıtı neredeyse mükemmeldi, ancak bir hizmet olarak çalıştırıldığında, her zaman stdout'u yakalamadı. Neden olmasın asla dibe vuramadım.

TÜM vakalarımızda işe yaradığını tespit ettiğimiz tek çözüm şudur: http://csharptest.net/319/using-the-processrunner-class/index.html


Bu kütüphaneyi deneyeceğim. Kodun kapsamını belirledim ve delegeleri mantıklı kullanıyor gibi görünüyor. Nuget'te güzel bir şekilde paketlenmiştir. Temelde profesyonellik kokuyor, asla suçlanamayacağım bir şey. Isırırsa söyler.
Steve Hibbert

Kaynak kodun bağlantısı öldü. Lütfen bir dahaki sefere kodu cevaba kopyalayın.
Vitaly Zdanevich

1

Geçici çözüm tüm karmaşıklığı önlemek için kullanarak sona erdi:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

Bu yüzden bir geçici dosya oluşturuyorum, hem çıktıyı hem de hatayı kullanarak yönlendiriyorum > outputfile > 2>&1ve sonra işlem bittikten sonra dosyayı okuyorum.

Diğer çözümler, çıktıyla başka şeyler yapmak istediğiniz senaryolar için iyidir, ancak basit şeyler için bu çok karmaşıklığı önler.


1

Birçok cevabı okudum ve kendim cevapladım. Bunun her halükarda düzelteceğinden emin değilim, ancak ortamımda düzeltiliyor. Ben sadece WaitForExit kullanmıyorum ve her iki çıkış ve hata sonu sinyalleri WaitHandle.WaitAll kullanın. Birisi bununla ilgili olası sorunları görürse sevinirim. Ya da birisine yardım ederse. Benim için daha iyi çünkü zaman aşımı kullanmıyor.

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}

Ben bunu kullandım ve zaman aşımı işlemek için Görev.Run ile sarılmış, ben de zaman aşımı öldürmek için processid iade
plus5volt

0

Async ile düşünüyorum, daha şık bir çözüm olması ve hem standardOutput hem de standardError kullanırken bile kilitlenmeleri olmaması mümkündür:

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;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

Mark Byers cevabına dayanır. Zaman uyumsuz bir yöntemde değilseniz, string output = tStandardOutput.result;bunun yerineawait



-1

Bu yazı belki eski ama neden asmak neden redirectStandardoutput için yığın taşması veya redirectStandarderror varsa ana nedenini öğrendim.

Çıktı verisi veya hata verisi büyük olduğundan, belirsiz süre boyunca işlendiği için askıda kalma süresine neden olur.

bu sorunu çözmek için:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False

11
Sorun şu ki, insanlar bunları açıkça doğruya ayarlıyorlar çünkü bu akışlara erişmek istiyorlar! Başka gerçekten onları yanlış bırakabiliriz.
user276648 7:13

-1

Burada yeniden yönlendiriciye gönderilen örnek kodu ve yönlendirilen diğer programı çağıralım. Eğer o olsaydım, muhtemelen sorunu çoğaltmak için kullanılabilecek bir test yönlendirme programı yazardım.

Ben de yaptım. Test verileri için ECMA-334 C # Dil Spesifikasyonu v PDF'yi kullandım; yaklaşık 5MB. Aşağıdakiler bunun önemli bir parçası.

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

Veri boyutu değeri gerçek dosya boyutuyla eşleşmiyor, ancak önemli değil. Bir PDF dosyasının satırların sonunda her zaman hem CR hem de LF kullanıp kullanmadığı açık değildir, ancak bunun önemi yoktur. Test etmek için başka herhangi bir büyük metin dosyası kullanabilirsiniz.

Büyük miktarda veri yazarken örnek yeniden yönlendirme kodunun askıda kaldığını, ancak küçük bir miktar yazdığımda kilitlenmediğini.

Bir şekilde bu kodun yürütülmesini izlemek için çok çalıştı ve yapamadım. Yeniden yönlendirilen program için ayrı bir konsol penceresi almaya çalışmak üzere bir konsol oluşturmayı devre dışı bırakan yeniden yönlendirilen programın satırlarını yorumladım ama yapamadım.

Sonra yeni bir pencerede, üst pencerenin penceresinde veya penceresiz bir konsol uygulaması nasıl başlatılır . Görünüşe göre, bir konsol programı ShellExecute olmadan başka bir konsol programı başlattığında (kolayca) ayrı bir konsola sahip olamayız ve ShellExecute yeniden yönlendirmeyi desteklemediğinden, diğer işlem için pencere belirtmesek bile bir konsolu paylaşmalıyız.

Yeniden yönlendirilen program bir yerde bir arabellek doldurursa, verilerin okunmasını beklemeli ve bu noktada yeniden yönlendirici tarafından hiçbir veri okunmuyorsa bir kilitlenme olduğunu varsayıyorum.

Çözüm, ReadToEnd kullanmamak ve veri yazılırken verileri okumaktır, ancak eşzamansız okumalar kullanmak gerekli değildir. Çözüm oldukça basit olabilir. Aşağıdakiler benim için 5 MB PDF ile çalışıyor.

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

Başka bir olasılık, yeniden yönlendirme yapmak için bir GUI programı kullanmaktır. Yukarıdaki kod, belirgin değişiklikler haricinde bir WPF uygulamasında çalışır.


-3

Aynı sorunu yaşıyordum ama sebebi farklıydı. Ancak Windows 8 altında olur, ancak Windows 7 altında olmaz. Aşağıdaki satır soruna neden olmuş gibi görünüyor.

pProcess.StartInfo.UseShellExecute = False

Çözüm, UseShellExecute'u devre dışı ETMEMEKTEDİR. Şimdi istenmeyen bir Kabuk açılır penceresi aldım, ancak programdan daha iyi bir şey olmasını bekliyorum. Bunun için aşağıdaki çözümü ekledim:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Şimdi beni rahatsız eden tek şey, bunun neden ilk başta Windows 8 altında gerçekleştiğidir.


1
UseShellExecuteÇıktıyı yeniden yönlendirmek istiyorsanız false olarak ayarlamanız gerekir .
Brad Moore
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.