ProcessStartInfo tergantung pada "WaitForExit"? Mengapa?

187

Saya memiliki kode berikut:

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

Saya tahu bahwa output dari proses yang saya mulai sekitar 7MB. Menjalankannya di konsol Windows berfungsi dengan baik. Sayangnya secara terprogram ini hang tanpa batas di WaitForExit. Perhatikan juga kode ini TIDAK menggantung untuk output yang lebih kecil (seperti 3KB).

Apakah mungkin StandardOutput internal di ProcessStartInfo tidak dapat buffer 7MB? Jika demikian, apa yang harus saya lakukan? Jika tidak, apa yang saya lakukan salah?

Epaga
sumber
ada solusi akhir dengan kode sumber lengkap tentang hal itu?
Kiquenet
2
Saya mengalami masalah yang sama dan ini bagaimana saya bisa menyelesaikannya stackoverflow.com/questions/2285288/…
Bedasso
6
Ya, solusi terakhir: tukar dua baris terakhir. Itu ada di manual .
Amit Naidu
4
from msdn: Contoh kode menghindari kondisi jalan buntu dengan memanggil p.StandardOutput.ReadToEnd sebelum p.WaitForExit. Kondisi jalan buntu dapat terjadi jika proses induk memanggil p.WaitForExit sebelum p.StandardOutput.ReadToEnd dan proses anak menulis teks yang cukup untuk mengisi aliran yang dialihkan. Proses induk akan menunggu tanpa batas waktu untuk proses anak keluar. Proses anak akan menunggu tanpa batas waktu untuk orang tua membaca dari aliran StandardOutput penuh.
Carlos Liu
Agak menyebalkan betapa rumitnya melakukan ini dengan benar. Senang bekerja di sekitarnya dengan pengalihan baris perintah lebih sederhana> outputfile :)
eglasius

Jawaban:

393

Masalahnya adalah bahwa jika Anda mengarahkan ulang StandardOutputdan / atau StandardErrorbuffer internal dapat menjadi penuh. Apa pun pesanan yang Anda gunakan, mungkin ada masalah:

  • Jika Anda menunggu proses untuk keluar sebelum membaca StandardOutputproses dapat memblokir mencoba menulis untuk itu, sehingga proses tidak pernah berakhir.
  • Jika Anda membaca dari StandardOutputmenggunakan ReadToEnd maka proses Anda dapat memblokir jika proses tidak pernah ditutup StandardOutput(misalnya jika tidak pernah berakhir, atau jika diblokir untuk menulis StandardError).

Solusinya adalah menggunakan pembacaan asinkron untuk memastikan bahwa buffer tidak mendapatkan penuh. Untuk menghindari kebuntuan dan mengumpulkan semua output dari keduanya StandardOutputdan StandardErrorAnda dapat melakukan ini:

EDIT: Lihat jawaban di bawah ini untuk cara menghindari ObjectDisposedException jika batas waktu terjadi.

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.
        }
    }
}
Mark Byers
sumber
11
Tidak punya ide mengarahkan output yang menyebabkan masalah tetapi cukup yakin itu. Menghabiskan 4 jam memukul-mukul kepala saya ini dan memperbaikinya dalam 5 menit setelah membaca posting Anda. Kerja bagus!
Ben Gripka
1
@AlexPeck Masalah ini menjalankan ini sebagai aplikasi konsol. Hans Passant mengidentifikasi masalah di sini: stackoverflow.com/a/16218470/279516
Bob Horn
5
setiap kali prompt perintah ditutup, ini muncul: Pengecualian tidak tertangani dari tipe "System.ObjectDisposed" terjadi di mscorlib.dll Info tambahan: Pegangan aman telah ditutup
user1663380
3
Kami memiliki masalah yang sama seperti yang dijelaskan oleh @ user1663380 di atas. Apakah Anda pikir adalah mungkin bahwa usingpernyataan untuk event handler harus di atas yang usingpernyataan untuk proses itu sendiri?
Dan Forbes
2
Saya tidak berpikir menunggu menangani diperlukan. Per msdn, cukup selesaikan dengan versi non-timeout WaitForExit: Ketika output standar telah dialihkan ke pengendali event asinkron, ada kemungkinan bahwa pemrosesan output tidak akan selesai ketika metode ini kembali. Untuk memastikan bahwa penanganan kejadian asinkron telah selesai, panggil overload WaitForExit () yang tidak menggunakan parameter setelah menerima true dari overload ini.
Patrick
98

The dokumentasi untuk Process.StandardOutputmengatakan untuk membaca sebelum Anda menunggu jika tidak, anda bisa jalan buntu, potongan disalin di bawah ini:

 // 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();
rampok
sumber
14
Saya tidak 100% yakin apakah ini hanya hasil dari lingkungan saya, tetapi saya menemukan jika Anda telah mengatur RedirectStandardOutput = true;dan tidak menggunakan p.StandardOutput.ReadToEnd();Anda mendapatkan jalan buntu / hang.
Chris S
3
Benar. Saya berada dalam situasi yang sama. Saya mengarahkan StandardError tanpa alasan ketika mengkonversi dengan ffmpeg dalam suatu proses, itu cukup memadai dalam aliran StandardError untuk membuat jalan buntu.
Léon Pelletier
Ini masih menggantung bagi saya bahkan dengan mengarahkan dan membaca output standar.
user3791372
@ user3791372 Saya kira ini hanya berlaku jika buffer di belakang StandardOutput tidak sepenuhnya terisi. Di sini MSDN tidak melakukan keadilan. Artikel hebat yang saya sarankan Anda baca adalah di: dzone.com/articles/async-io-and-threadpool
Cary
19

Jawaban Mark Byers sangat bagus, tetapi saya hanya ingin menambahkan yang berikut:

Para delegasi OutputDataReceiveddan ErrorDataReceivedperlu dihapus sebelum outputWaitHandledan errorWaitHandledibuang. Jika proses terus menghasilkan data setelah batas waktu terlampaui dan kemudian berakhir, outputWaitHandledanerrorWaitHandle variabel akan diakses setelah dibuang.

(FYI saya harus menambahkan peringatan ini sebagai jawaban karena saya tidak bisa mengomentari posnya.)

stevejay
sumber
2
Mungkin akan lebih baik untuk memanggil CancelOutputRead ?
Mark Byers
Menambahkan kode yang diedit Markus ke jawaban ini akan sangat luar biasa! Saya memiliki masalah yang sama persis saat ini.
ianbailey
8
@ianbailey Cara termudah untuk menyelesaikan ini adalah dengan menempatkan penggunaan (Proses p ...) di dalam penggunaan (AutoResetEvent errorWaitHandle ...)
Didier A.
18

Ini adalah solusi berbasis TPL, Task Parallel Library (TPL) yang lebih modern untuk .NET 4.5 dan di atasnya.

Contoh Penggunaan

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

Penerapan

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;
}
Muhammad Rehan Saeed
sumber
2
jawaban terbaik dan terlengkap saat ini
TermoTux
1
Untuk beberapa alasan, ini adalah satu-satunya solusi yang bekerja untuk saya, aplikasi berhenti hangging.
Jack
1
Tampaknya, Anda tidak menangani kondisi tersebut, tempat proses berakhir setelah dimulai, tetapi sebelum acara yang Keluar dilampirkan. Saran saya - memulai proses setelah semua pendaftaran.
Stas Boyarincev
@StasBoyarincev Terima kasih, diperbarui. Saya lupa memperbarui jawaban StackOverflow dengan perubahan ini.
Muhammad Rehan Saeed
1
@MuhammadRehanSaeed Namun satu hal lagi - sepertinya tidak diizinkan proses panggilan.BeginOutputReadLine () atau proses.BeginErrorReadLine () sebelum proses.Mulai. Dalam hal ini saya mendapatkan kesalahan: StandardOut belum dialihkan atau proses belum dimulai.
Stas Boyarincev
17

Masalah dengan ObjectDisposedException yang tidak ditangani terjadi ketika prosesnya habis. Dalam kasus seperti itu bagian lain dari kondisi:

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

tidak dieksekusi. Saya mengatasi masalah ini dengan cara berikut:

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);
        }
    }
}
Karol Tyl
sumber
1
demi kelengkapan, ini tidak ada yang mengatur pengalihan ke true
knocte
dan saya telah menghapus timeout pada akhirnya karena proses mungkin meminta input pengguna (misalnya ketik sesuatu) jadi saya tidak ingin mengharuskan pengguna untuk menjadi cepat
knocte
Mengapa Anda berubah outputdan errorke outputBuilder? Bisakah seseorang tolong berikan jawaban lengkap yang berfungsi?
Marko Avlijaš
System.ObjectDisposedException: Pegangan yang aman telah ditutup terjadi pada versi ini juga untuk saya
Matt
8

Rob menjawabnya dan menyelamatkan saya beberapa jam dari cobaan. Baca buffer output / kesalahan sebelum menunggu:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Jon
sumber
1
tetapi bagaimana jika lebih banyak data datang setelah Anda menelepon WaitForExit()?
knocte
@knocte berdasarkan pengujian saya, ReadToEndatau metode serupa (seperti StandardOutput.BaseStream.CopyTo) akan kembali setelah SEMUA data dibaca. tidak ada yang akan datang setelah itu
S.Serpooshan
Anda mengatakan bahwa ReadToEnd () juga menunggu pintu keluar?
knocte
2
@knocte Anda mencoba memahami API yang dibuat oleh microsoft?
aaaaaa
Masalah dari halaman MSDN yang sesuai adalah, itu tidak menjelaskan bahwa buffer di belakang StandardOutput dapat menjadi penuh dan dalam situasi itu anak harus berhenti menulis dan menunggu sampai buffer terkuras (orang tua membaca data dalam buffer) . ReadToEnd () hanya dapat secara sinkronisasi membaca hingga buffer ditutup atau buffer penuh, atau anak keluar dengan buffer tidak penuh. Itulah pengertian saya.
Cary
7

Kami memiliki masalah ini juga (atau varian).

Coba yang berikut ini:

1) Tambahkan batas waktu ke p.WaitForExit (nnnn); di mana nnnn dalam milidetik.

2) Letakkan panggilan ReadToEnd sebelum panggilan WaitForExit. Ini adalah apa yang kita lihat MS merekomendasikan.

torial
sumber
5

Kredit ke EM0 untuk https://stackoverflow.com/a/17600012/4151626

Solusi lain (termasuk EM0) masih menemui jalan buntu untuk aplikasi saya, karena batas waktu internal dan penggunaan baik StandardOutput dan StandardError oleh aplikasi spawned. Inilah yang bekerja untuk saya:

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

Sunting: menambahkan inisialisasi StartInfo ke sampel kode

ergohack
sumber
Inilah yang saya gunakan dan tidak pernah memiliki masalah lagi dengan jalan buntu.
Roemer
3

Saya memecahkannya dengan cara ini:

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

Saya mengarahkan ulang input, output dan error dan menangani pembacaan dari stream output dan error. Solusi ini berfungsi untuk SDK 7- 8.1, baik untuk Windows 7 dan Windows 8

Elina Maliarsky
sumber
2
Elina: terima kasih atas jawaban Anda. Ada beberapa catatan di bagian bawah dokumen MSDN ini ( msdn.microsoft.com/en-us/library/… ) yang memperingatkan tentang kemungkinan deadlock jika Anda membaca hingga akhir baik stdout yang dialihkan dan stream stderr secara serempak. Sulit untuk mengatakan apakah solusi Anda rentan terhadap masalah ini. Selain itu, tampaknya Anda mengirimkan output stdout / stderr proses segera kembali sebagai input. Mengapa? :)
Matthew Piatt
3

Saya mencoba membuat kelas yang akan menyelesaikan masalah Anda menggunakan asynchronous stream read, dengan memperhitungkan Mark Byers, Rob, stevejay jawaban. Melakukan hal itu saya menyadari bahwa ada bug yang berhubungan dengan aliran keluaran proses asinkron yang dibaca.

Saya melaporkan bug itu di Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Ringkasan:

Anda tidak bisa melakukan itu:

process.BeginOutputReadLine (); proses.Mulai ();

Anda akan menerima System.InvalidOperationException: StandardOut belum dialihkan atau proses belum dimulai.

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

Maka Anda harus mulai membaca asinkron setelah proses dimulai:

proses.Mulai (); process.BeginOutputReadLine ();

Melakukannya, buat kondisi balapan karena aliran output dapat menerima data sebelum Anda menyetelnya menjadi tidak sinkron:

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

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

Kemudian beberapa orang dapat mengatakan bahwa Anda hanya perlu membaca aliran sebelum Anda menyetelnya menjadi asinkron. Tetapi masalah yang sama terjadi. Akan ada kondisi perlombaan antara pembacaan sinkron dan mengatur aliran ke mode asinkron.

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

Tidak ada cara untuk mencapai pembacaan asinkron yang aman dari aliran output dari suatu proses dengan cara yang sebenarnya "Proses" dan "ProcessStartInfo" telah dirancang.

Anda mungkin lebih baik menggunakan asynchronous read seperti yang disarankan oleh pengguna lain untuk kasus Anda. Tetapi Anda harus sadar bahwa Anda dapat kehilangan beberapa informasi karena kondisi balapan.

Eric Ouellet
sumber
1

Saya kira ini adalah pendekatan yang sederhana dan lebih baik (kita tidak perlu 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;
    }
}
Kuzman Marinov
sumber
Benar, tetapi tidakkah seharusnya Anda lakukan .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"untuk menyederhanakan kode Anda juga? Atau mungkin sesuatu yang setara "echo command | " + Path + @"\ggsci.exe"jika Anda benar-benar tidak ingin menggunakan file obeycommand.txt yang terpisah.
Amit Naidu
3
Solusi Anda tidak perlu AutoResetEvent tetapi Anda polling. Ketika Anda melakukan polling alih-alih menggunakan acara (ketika mereka tersedia) maka Anda menggunakan CPU tanpa alasan dan itu menunjukkan bahwa Anda adalah programmer yang buruk. Solusi Anda sangat buruk jika dibandingkan dengan yang lain menggunakan AutoResetEvent. (Tapi saya tidak memberi Anda -1 karena Anda mencoba membantu!).
Eric Ouellet
1

Tidak ada jawaban di atas yang melakukan pekerjaan.

Solusi Rob hang dan solusi 'Mark Byers' mendapatkan pengecualian dibuang (saya mencoba "solusi" dari jawaban lain).

Jadi saya memutuskan untuk menyarankan solusi lain:

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

Kode ini di-debug dan berfungsi dengan baik.

omriman12
sumber
1
Baik! hanya perhatikan bahwa parameter token tidak disediakan saat memanggil GetProcessOutputWithTimeoutmetode.
S.Serpooshan
1

pengantar

Jawaban yang diterima saat ini tidak bekerja (melempar pengecualian) dan ada terlalu banyak solusi tetapi tidak ada kode lengkap. Ini jelas menghabiskan banyak waktu orang karena ini adalah pertanyaan yang populer.

Menggabungkan jawaban Mark Byers dan jawaban Karol Tyl, saya menulis kode lengkap berdasarkan bagaimana saya ingin menggunakan metode Process.Start.

Pemakaian

Saya telah menggunakannya untuk membuat dialog progres di sekitar perintah git. Beginilah cara saya menggunakannya:

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

Secara teori Anda juga dapat menggabungkan stdout dan stderr, tetapi saya belum mengujinya.

Kode

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);
                }
            }
        }
    }
}
Marko Avlijaš
sumber
Masih mendapatkan System.ObjectDisposedException: Pegangan yang aman telah ditutup pada versi ini juga.
Matt
1

Saya tahu ini sudah lama makan malam tapi, setelah membaca seluruh halaman ini tidak ada solusi yang bekerja untuk saya, walaupun saya tidak mencoba Muhammad Rehan karena kodenya agak sulit untuk diikuti, walaupun saya kira dia berada di jalur yang benar . Ketika saya mengatakan itu tidak bekerja itu tidak sepenuhnya benar, kadang-kadang itu akan berfungsi dengan baik, saya kira itu ada hubungannya dengan panjang output sebelum tanda EOF.

Bagaimanapun, solusi yang berhasil bagi saya adalah menggunakan utas yang berbeda untuk membaca StandardOutput dan StandardError dan menulis pesan.

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

Semoga ini bisa membantu seseorang, yang berpikir ini bisa sangat sulit!

Alexis Coles
sumber
Pengecualian: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. Bagaimana / di mana harus swdidefinisikan?
wallyk
1

Setelah membaca semua posting di sini, saya memutuskan pada solusi gabungan Marko Avlijaš. Namun , itu tidak menyelesaikan semua masalah saya.

Di lingkungan kami, kami memiliki Layanan Windows yang dijadwalkan untuk menjalankan ratusan file .bat .cmd .exe, ... dll. Yang berbeda yang telah terakumulasi selama bertahun-tahun dan ditulis oleh banyak orang dan gaya yang berbeda. Kami tidak memiliki kendali atas penulisan program & skrip, kami hanya bertanggung jawab untuk menjadwalkan, menjalankan, dan melaporkan keberhasilan / kegagalan.

Jadi saya mencoba hampir semua saran di sini dengan berbagai tingkat keberhasilan. Jawaban Marko hampir sempurna, tetapi ketika dijalankan sebagai layanan, itu tidak selalu menangkap stdout. Saya tidak pernah sampai ke dasar mengapa tidak.

Satu-satunya solusi yang kami temukan yang berfungsi dalam SEMUA kasus kami adalah ini: http://csharptest.net/319/using-the-processrunner-class/index.html

flapster
sumber
Saya akan mencoba perpustakaan ini. Saya telah mengambil kode, dan tampaknya menggunakan delegasi dengan bijaksana. Ini dikemas dengan baik di Nuget. Ini pada dasarnya berbau profesionalisme, sesuatu yang tidak pernah bisa saya tuduhkan. Jika menggigit, akan memberi tahu.
Steve Hibbert
Tautan ke kode sumber sudah mati. Harap lain kali salin kode ke jawabannya.
Vitaly Zdanevich
1

Solusi yang akhirnya saya gunakan untuk menghindari semua kompleksitas:

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

Jadi saya membuat file temp, redirect baik output dan kesalahan ke sana dengan menggunakan > outputfile > 2>&1dan kemudian hanya membaca file setelah proses selesai.

Solusi lain baik untuk skenario di mana Anda ingin melakukan hal-hal lain dengan output, tetapi untuk hal-hal sederhana ini menghindari banyak kerumitan.

eglasius
sumber
1

Saya sudah membaca banyak jawaban dan membuat jawaban saya sendiri. Tidak yakin yang satu ini akan memperbaiki dalam hal apapun, tetapi itu memperbaiki di lingkungan saya. Saya hanya tidak menggunakan WaitForExit dan menggunakan WaitHandle.WaitAll pada kedua output & sinyal kesalahan akhir. Saya akan senang, jika seseorang akan melihat kemungkinan masalah dengan itu. Atau jika itu akan membantu seseorang. Bagi saya itu lebih baik karena tidak menggunakan timeout.

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;
}
Deepscorn
sumber
Saya menggunakan ini dan dibungkus dengan Task.Run untuk menangani timeout, saya juga mengembalikan processid untuk membunuh pada timeout
plus5volt
0

Saya pikir dengan async, dimungkinkan untuk memiliki solusi yang lebih elegan dan tidak memiliki deadlock bahkan ketika menggunakan standardOutput dan standardError:

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

Ini didasarkan pada jawaban Mark Byers. Jika Anda tidak menggunakan metode async, Anda dapat menggunakannya string output = tStandardOutput.result;sebagai gantinyaawait

Yepeekai
sumber
-1

Posting ini mungkin sudah usang tetapi saya menemukan penyebab utama mengapa biasanya hang adalah karena stack overflow untuk redirectStandardoutput atau jika Anda memiliki redirectStandarderror.

Karena data output atau data kesalahan besar, itu akan menyebabkan waktu hang karena masih memproses untuk durasi yang tidak terbatas.

jadi untuk mengatasi masalah ini:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
lagu
sumber
11
Masalahnya adalah orang-orang secara eksplisit mengaturnya menjadi benar karena mereka ingin dapat mengakses aliran itu! Kalau tidak, kita bisa membiarkannya salah.
user276648
-1

Mari kita sebut kode sampel yang diposting di sini pengalih dan program lain diarahkan. Jika itu saya maka saya mungkin akan menulis sebuah program tes diarahkan yang dapat digunakan untuk menduplikasi masalah.

Jadi saya lakukan. Untuk data uji saya menggunakan ECMA-334 C # Language Specificationv PDF; sekitar 5MB. Berikut ini adalah bagian penting dari itu.

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

Nilai data tidak cocok dengan ukuran file aktual tetapi itu tidak masalah. Tidak jelas apakah file PDF selalu menggunakan CR dan LF di akhir baris tapi itu tidak masalah untuk ini. Anda dapat menggunakan file teks besar lainnya untuk mengujinya.

Menggunakan kode redirector sampel hang ketika saya menulis sejumlah besar data tetapi tidak ketika saya menulis sejumlah kecil.

Saya mencoba sangat banyak untuk entah bagaimana melacak eksekusi kode itu dan saya tidak bisa. Saya mengomentari garis-garis program yang diarahkan yang menonaktifkan pembuatan konsol untuk program yang diarahkan untuk mencoba mendapatkan jendela konsol yang terpisah tetapi saya tidak bisa.

Kemudian saya menemukan Cara memulai aplikasi konsol di jendela baru, jendela induk, atau tanpa jendela . Jadi ternyata kita tidak dapat (dengan mudah) memiliki konsol terpisah ketika satu program konsol memulai program konsol lain tanpa ShellExecute dan karena ShellExecute tidak mendukung pengalihan, kita harus berbagi konsol, bahkan jika kita tidak menentukan jendela untuk proses lainnya.

Saya berasumsi bahwa jika program diarahkan mengisi buffer di suatu tempat maka ia harus menunggu data untuk dibaca dan jika pada saat itu tidak ada data yang dibaca oleh redirector maka itu adalah jalan buntu.

Solusinya adalah tidak menggunakan ReadToEnd dan membaca data saat data sedang ditulis tetapi tidak perlu menggunakan bacaan asinkron. Solusinya bisa sangat sederhana. Berikut ini berfungsi untuk saya dengan PDF 5 MB.

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

Kemungkinan lain adalah menggunakan program GUI untuk melakukan pengalihan. Kode sebelumnya berfungsi dalam aplikasi WPF kecuali dengan modifikasi yang jelas.

pengguna34660
sumber
-3

Saya memiliki masalah yang sama, tetapi alasannya berbeda. Namun itu akan terjadi di bawah Windows 8, tetapi tidak di bawah Windows 7. Baris berikut ini tampaknya telah menyebabkan masalah.

pProcess.StartInfo.UseShellExecute = False

Solusinya adalah TIDAK menonaktifkan UseShellExecute. Saya sekarang menerima jendela popup Shell, yang tidak diinginkan, tetapi jauh lebih baik daripada program menunggu apa-apa terjadi. Jadi saya menambahkan solusi berikut untuk itu:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Sekarang satu-satunya hal yang mengganggu saya adalah mengapa ini terjadi di bawah Windows 8 di tempat pertama.

ohgodnotanotherone
sumber
1
Anda harus UseShellExecutedisetel ke false jika Anda ingin mengarahkan ulang output.
Brad Moore