Proses terkadang hang saat menunggu Keluar

13

Apa yang mungkin menjadi alasan proses saya menggantung sambil menunggu keluar?

Kode ini harus memulai skrip PowerShell yang di dalamnya melakukan banyak tindakan misalnya mulai mengkompilasi ulang kode melalui MSBuild, tetapi mungkin masalahnya adalah ia menghasilkan terlalu banyak output dan kode ini macet saat menunggu untuk keluar bahkan setelah skrip shell daya dijalankan dengan benar

itu agak "aneh" karena kadang-kadang kode ini berfungsi dengan baik dan kadang-kadang hanya macet.

Kode hang di:

process.WaitForExit (ProcessTimeOutMiliseconds);

Script Powershell dieksekusi dalam 1-2sec sementara itu timeout adalah 19sec.

public static (bool Success, string Logs) ExecuteScript(string path, int ProcessTimeOutMiliseconds, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                {
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "powershell.exe",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
                    WorkingDirectory = Path.GetDirectoryName(path)
                };

                if (args.Length > 0)
                {
                    var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
                    process.StartInfo.Arguments += $" {arguments}";
                }

                output.AppendLine($"args:'{process.StartInfo.Arguments}'");

                process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output.AppendLine(e.Data);
                    }
                };
                process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error.AppendLine(e.Data);
                    }
                };

                process.Start();

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

                process.WaitForExit(ProcessTimeOutMiliseconds);

                var logs = output + Environment.NewLine + error;

                return process.ExitCode == 0 ? (true, logs) : (false, logs);
            }
        }
        finally
        {
            outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
            errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
        }
    }
}

Naskah:

start-process $args[0] App.csproj -Wait -NoNewWindow

[string]$sourceDirectory  = "\bin\Debug\*"
[int]$count = (dir $sourceDirectory | measure).Count;

If ($count -eq 0)
{
    exit 1;
}
Else
{
    exit 0;
}

dimana

$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"

Edit

Untuk solusi @ ingen saya menambahkan pembungkus kecil yang mencoba untuk mengeksekusi MS Build yang digantung

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    var current = 0;
    int attempts_count = 5;
    bool _local_success = false;
    string _local_logs = "";

    while (attempts_count > 0 && _local_success == false)
    {
        Console.WriteLine($"Attempt: {++current}");
        InternalExecuteScript(path, processTimeOutMilliseconds, out _local_logs, out _local_success, args);
        attempts_count--;
    }

    success = _local_success;
    logs = _local_logs;
}

Di mana InternalExecuteScriptkode asli

Joelty
sumber
pada baris mana sebenarnya proses hang? dan intro kode Anda lebih banyak
Mr.AF
@ Mr.AF Anda benar - selesai.
Joelty
1
Panggilan sebenarnya dari Powershell adalah satu hal, namun apa yang TIDAK Anda berikan adalah sisa sebenarnya dari skrip yang Anda coba proses saat DALAM WAKTU Powershell. Memanggil powershell itu sendiri bukan masalahnya, tetapi dalam apa yang Anda coba lakukan. Edit posting Anda dan berikan panggilan / perintah eksplisit yang Anda coba jalankan.
DRapp
1
Sangat aneh saya mencoba mereplikasi kesalahan. Itu terjadi secara acak dua kali pada 20 upaya atau sesuatu dan saya tidak dapat memicunya lagi.
KiKoS
1
@ Joelty, ohh keren menarik, apakah Anda mengatakan bahwa Rxpendekatan bekerja (seperti di dalamnya tidak ada batas waktu) bahkan dengan proses MSBuild liar tergeletak mengarah ke penantian tanpa batas waktu? tertarik untuk mengetahui bagaimana itu ditangani
Clint

Jawaban:

9

Mari kita mulai dengan rekap jawaban yang diterima di pos terkait.

Masalahnya adalah bahwa jika Anda mengarahkan StandardOutput dan / atau StandardError buffer internal dapat menjadi penuh. Apa pun pesanan yang Anda gunakan, mungkin ada masalah:

  • Jika Anda menunggu proses untuk keluar sebelum membaca StandardOutput proses tersebut dapat memblokir mencoba untuk menulis kepadanya, sehingga proses tidak pernah berakhir.
  • Jika Anda membaca dari StandardOutput menggunakan ReadToEnd maka proses Anda dapat memblokir jika proses tidak pernah menutup StandardOutput (misalnya jika tidak pernah berakhir, atau jika diblokir menulis ke StandardError).

Bahkan jawaban yang diterima, bagaimanapun, bergumul dengan urutan eksekusi dalam kasus-kasus tertentu.

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

Dalam situasi seperti ini, di mana Anda ingin mengatur beberapa acara, Rx benar-benar bersinar.

Perhatikan .NET implementasi Rx tersedia sebagai paket NuGet System.Reactive.

Mari selami untuk melihat bagaimana Rx memfasilitasi bekerja dengan acara.

// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
    .Subscribe(
        eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
        exception => error.AppendLine(exception.Message)
    ).DisposeWith(disposables);

FromEventPatternmemungkinkan kita untuk memetakan kejadian yang berbeda dari suatu peristiwa ke aliran yang disatukan (alias dapat diamati). Hal ini memungkinkan kami untuk menangani acara dalam pipa (dengan semantik seperti LINQ). The Subscribekelebihan yang digunakan di sini disediakan dengan Action<EventPattern<...>>dan Action<Exception>. Setiap kali acara yang diamati dinaikkan, itu senderdan argsakan dibungkus oleh EventPatterndan didorong melalui Action<EventPattern<...>>. Saat eksepsi dimunculkan dalam pipa, Action<Exception>digunakan.

Salah satu kelemahan dari Event pola, dengan jelas diilustrasikan dalam kasus penggunaan ini (dan oleh semua solusi di pos yang dirujuk), adalah bahwa tidak jelas kapan / di mana harus berhenti berlangganan event handler.

Dengan Rx kita mendapatkan kembali IDisposablesaat kita membuat langganan. Ketika kami membuangnya, kami secara efektif mengakhiri langganan. Dengan penambahan DisposeWithmetode ekstensi (dipinjam dari RxUI ), kita dapat menambahkan beberapa IDisposables ke CompositeDisposable(dinamai disposablesdalam contoh kode). Setelah selesai, kami dapat mengakhiri semua langganan dengan satu panggilan disposables.Dispose().

Yang pasti, tidak ada yang bisa kita lakukan dengan Rx, yang tidak bisa kita lakukan dengan vanilla .NET. Kode yang dihasilkan jauh lebih mudah untuk dipikirkan, setelah Anda beradaptasi dengan cara berpikir fungsional.

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var process = new Process())
    using (var disposables = new CompositeDisposable())
    {
        process.StartInfo = new ProcessStartInfo
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            FileName = "powershell.exe",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
            WorkingDirectory = Path.GetDirectoryName(path)
        };

        if (args.Length > 0)
        {
            var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
            process.StartInfo.Arguments += $" {arguments}";
        }

        output.AppendLine($"args:'{process.StartInfo.Arguments}'");

        // Raise the Process.Exited event when the process terminates.
        process.EnableRaisingEvents = true;

        // Subscribe to OutputData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
            .Subscribe(
                eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        // Subscribe to ErrorData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.ErrorDataReceived))
            .Subscribe(
                eventPattern => error.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        var processExited =
            // Observable will tick when the process has gracefully exited.
            Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
                // First two lines to tick true when the process has gracefully exited and false when it has timed out.
                .Select(_ => true)
                .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
                // Force termination when the process timed out
                .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

        // Subscribe to the Process.Exited event.
        processExited
            .Subscribe()
            .DisposeWith(disposables);

        // Start process(ing)
        process.Start();

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

        // Wait for the process to terminate (gracefully or forced)
        processExited.Take(1).Wait();

        logs = output + Environment.NewLine + error;
        success = process.ExitCode == 0;
    }
}

Kita sudah membahas bagian pertama, di mana kita memetakan acara kita ke yang bisa diamati, sehingga kita bisa langsung melompat ke bagian yang gemuk. Di sini kami menetapkan kami diamati untukprocessExited variabel yang , karena kita ingin menggunakannya lebih dari sekali.

Pertama, ketika kita mengaktifkannya, dengan menelepon Subscribe. Dan nanti ketika kita ingin 'menunggu' nilai pertamanya.

var processExited =
    // Observable will tick when the process has gracefully exited.
    Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
        // First two lines to tick true when the process has gracefully exited and false when it has timed out.
        .Select(_ => true)
        .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
        // Force termination when the process timed out
        .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

// Subscribe to the Process.Exited event.
processExited
    .Subscribe()
    .DisposeWith(disposables);

// Start process(ing)
...

// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();

Salah satu masalah dengan OP adalah mengasumsikan process.WaitForExit(processTimeOutMiliseconds)akan menghentikan proses ketika habis. Dari MSDN :

Menginstruksikan komponen Proses untuk menunggu jumlah milidetik yang ditentukan agar proses terkait dapat keluar.

Sebaliknya, ketika waktu habis, ia hanya mengembalikan kontrol ke utas saat ini (yaitu berhenti memblokir). Anda harus memaksakan penghentian secara manual saat proses berakhir. Untuk mengetahui kapan time out telah terjadi, kita dapat memetakan Process.Exitedacara tersebut ke yang processExiteddapat diamati untuk diproses. Dengan cara ini kita dapat menyiapkan input untukDo operator.

Kode ini cukup jelas. Jika exitedSuccessfullyprosesnya akan berakhir dengan anggun. Jika tidak exitedSuccessfully, pemutusan hubungan kerja harus dipaksakan. Catatan yang process.Kill()dijalankan secara tidak serempak, komentar ref . Namun, panggilan process.WaitForExit()tepat setelah akan membuka kemungkinan kebuntuan lagi. Jadi, bahkan dalam kasus pemutusan paksa, lebih baik membiarkan semua sekali pakai dibersihkan ketika usingruang lingkup berakhir, karena output tetap dapat dianggap terganggu / rusak.

The try catchmembangun dicadangkan untuk kasus luar biasa (no pun intended) di mana Anda sudah selaras processTimeOutMillisecondsdengan waktu yang sebenarnya dibutuhkan oleh proses untuk selesai. Dengan kata lain, kondisi lomba terjadi antara Process.Exitedacara dan timer. Kemungkinan ini terjadi sekali lagi diperbesar oleh sifat asinkron process.Kill(). Saya pernah melihatnya sekali selama pengujian.


Untuk kelengkapan, DisposeWithmetode ekstensi.

/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
    /// </summary>
    public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
        where T : IDisposable
    {
        if (compositeDisposable == null)
        {
            throw new ArgumentNullException(nameof(compositeDisposable));
        }

        compositeDisposable.Add(item);
        return item;
    }
}
ingen
sumber
4
IMHO, pasti sepadan dengan karunia. Jawaban yang bagus, dan intro topik yang bagus untuk RX.
quetzalcoatl
Terima kasih!!! ExecuteScriptRxPegangan Anda hangssempurna. Sayangnya hang masih terjadi, tapi saya baru saja menambahkan pembungkus kecil di atas Anda ExecuteScriptRxyang melakukan Retrydan kemudian dijalankan dengan baik. Alasan MSBUILD hang mungkin adalah jawaban @Clint. PS: Kode itu membuat saya merasa bodoh <lol> Itu pertama kali saya lihatSystem.Reactive.Linq;
Joelty
Kode utama Wrapper adalah
Joelty
3

Demi kepentingan pembaca, saya akan membagi ini menjadi 2 Bagian

Bagian A: Masalah & bagaimana menangani skenario serupa

Bagian B: Rekreasi masalah & Solusi

Bagian A: Masalah

Ketika masalah ini terjadi - proses muncul di task manager, kemudian setelah 2-3sec menghilang (baik-baik saja), kemudian menunggu waktu habis dan kemudian pengecualian dilemparkan System.InvalidOperationException: Proses harus keluar sebelum informasi yang diminta dapat ditentukan.

& Lihat Skenario 4 di bawah

Dalam kode Anda:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); Dengan ini Anda sedang menunggu Processuntuk Timeout atau Exit , yang pernah terjadi pertama .
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)dan errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); Dengan ini Anda menunggu OutputData& ErrorDatastreaming membaca operasi untuk mengisinya lengkap
  3. Process.ExitCode == 0 Mendapat status proses saat keluar

Pengaturan berbeda & peringatan mereka:

  • Skenario 1 (Jalan Bahagia) : Proses selesai sebelum waktu habis, dan dengan demikian stdoutput dan stderror Anda juga selesai sebelum waktu habis dan semuanya baik-baik saja.
  • Skenario 2 : Proses, OutputWaitHandle & ErrorWaitHandle timeout namun stdoutput & stderror masih dibaca dan selesai setelah waktu tunggu WaitHandlers habis. Ini mengarah ke pengecualian lainObjectDisposedException()
  • Skenario 3 : Proses time-out pertama (19 detik) tetapi stdout dan stderror sedang beraksi, Anda menunggu WaitHandler untuk time out (19 detik), menyebabkan penundaan tambahan + 19sec.
  • Skenario 4 : Waktu proses habis dan kode mencoba untuk melakukan query secara prematur yang Process.ExitCodemenghasilkan kesalahan System.InvalidOperationException: Process must exit before requested information can be determined.

Saya telah menguji skenario ini lebih dari selusin kali dan berfungsi dengan baik, pengaturan berikut telah digunakan saat pengujian

  • Ukuran aliran Output mulai dari 5KB hingga 198KB dengan memulai pembangunan sekitar 2-15 proyek
  • Timeout & proses prematur keluar dalam jendela timeout


Kode yang Diperbarui

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

    //First waiting for ReadOperations to Timeout and then check Process to Timeout
    if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
        && !process.WaitForExit(ProcessTimeOutMiliseconds)  )
    {
        //To cancel the Read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
        process.CancelOutputRead();
        process.CancelErrorRead();

        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Timed Out");
        Logs = output + Environment.NewLine + error;
       //To release allocated resource for the Process
        process.Close();
        return  (false, logs);
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Completed On Time");
    Logs = output + Environment.NewLine + error;
    ExitCode = process.ExitCode.ToString();
    // Close frees the memory allocated to the exited process
    process.Close();

    //ExitCode now accessible
    return process.ExitCode == 0 ? (true, logs) : (false, logs);
    }
}
finally{}

EDIT:

Setelah berjam-jam bermain-main dengan MSBuild saya akhirnya bisa mereproduksi masalah di sistem saya


Bagian B: Rekreasi Masalah & Solusi

MSBuild memiliki-m[:number]saklar yang digunakan untuk menentukan jumlah maksimum proses bersamaan untuk digunakan saat membangun.

Ketika ini diaktifkan, MSBuild memunculkan sejumlah node yang hidup bahkan setelah Build selesai. Sekarang, Process.WaitForExit(milliseconds)akan menunggu tidak pernah keluar dan akhirnya habis

Saya bisa menyelesaikan ini dalam beberapa cara

  • Menelurkan proses MSBuild secara tidak langsung melalui CMD

    $path1 = """C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\repos\Test\Test.sln"" -maxcpucount:3"
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
  • Terus menggunakan MSBuild tetapi pastikan untuk mengatur nodeReuse ke False

    $filepath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"
    $arg1 = "C:\Users\John\source\repos\Test\Test.sln"
    $arg2 = "-m:3"
    $arg3 = "-nr:False"
    
    Start-Process -FilePath $filepath -ArgumentList $arg1,$arg2,$arg3 -Wait -NoNewWindow
  • Bahkan jika build paralel tidak diaktifkan, Anda masih bisa mencegah proses Anda menggantung WaitForExitdengan meluncurkan Build via CMD & karena itu Anda tidak membuat ketergantungan langsung pada proses Build

    $path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput

Pendekatan 2 lebih disukai karena Anda tidak ingin terlalu banyak node MSBuild untuk berbaring.

Clint
sumber
Jadi, seperti yang saya katakan di atas, terima kasih, ini "-nr:False","-m:3"tampaknya telah memperbaiki perilaku hang-ish MSBuild, yang dengan Rx solutionmembuat seluruh proses agak dapat diandalkan (waktu akan menunjukkan). Saya berharap saya bisa menerima kedua jawaban atau memberikan dua hadiah
Joelty
@ Joelty, saya hanya ingin tahu apakah Rx pendekatan dalam solusi lain dapat menyelesaikan masalah tanpa menerapkan -nr:False" ,"-m:3". Dalam pemahaman saya ini menangani menunggu tidak pasti dari kebuntuan dan hal-hal lain yang telah saya bahas di bagian 1. Dan rootcause di Bagian 2 adalah apa yang saya yakini adalah akar penyebab masalah yang Anda hadapi;) Saya mungkin salah karena itu Saya bertanya, hanya waktu yang akan memberitahu ... Ceria !!
Clint
3

Masalahnya adalah bahwa jika Anda mengarahkan StandardOutput dan / atau StandardError buffer internal dapat menjadi penuh.

Untuk mengatasi masalah yang disebutkan di atas, Anda dapat menjalankan proses di utas terpisah. Saya tidak menggunakan WaitForExit, saya menggunakan proses keluar dari acara yang akan mengembalikan ExitCode proses asynchronous memastikan telah selesai.

public async Task<int> RunProcessAsync(params string[] args)
    {
        try
        {
            var tcs = new TaskCompletionSource<int>();

            var process = new Process
            {
                StartInfo = {
                    FileName = 'file path',
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    Arguments = "shell command",
                    UseShellExecute = false,
                    CreateNoWindow = true
                },
                EnableRaisingEvents = true
            };


            process.Exited += (sender, args) =>
            {
                tcs.SetResult(process.ExitCode);
                process.Dispose();
            };

            process.Start();
            // Use asynchronous read operations on at least one of the streams.
            // Reading both streams synchronously would generate another deadlock.
            process.BeginOutputReadLine();
            string tmpErrorOut = await process.StandardError.ReadToEndAsync();
            //process.WaitForExit();


            return await tcs.Task;
        }
        catch (Exception ee) {
            Console.WriteLine(ee.Message);
        }
        return -1;
    }

Kode di atas adalah pertempuran yang diuji memanggil FFMPEG.exe dengan argumen baris perintah. Saya mengubah file mp4 menjadi file mp3 dan melakukan lebih dari 1000 video sekaligus tanpa gagal. Sayangnya saya tidak memiliki pengalaman power shell langsung tetapi berharap ini bisa membantu.

Alex
sumber
Aneh kode ini, mirip dengan solusi lain gagal (macet) pada upaya PERTAMA dan kemudian tampaknya berfungsi dengan baik (seperti 5 upaya lainnya, saya akan mengujinya lebih lanjut). Btw kenapa Anda melakukan BegingOutputReadlinedan kemudian melakukan ReadToEndAsyncpada StandardError?
Joelty
OP sudah membaca secara tidak sinkron, jadi tidak mungkin kebuntuan pada buffer konsol adalah masalah di sini.
yaakov
0

Tidak yakin apakah ini masalah Anda, tetapi melihat MSDN tampaknya ada keanehan dengan WaitForExit yang kelebihan beban saat Anda mengarahkan output secara tidak sinkron. Artikel MSDN merekomendasikan untuk memanggil WaitForExit yang tidak memerlukan argumen setelah memanggil metode kelebihan beban.

Halaman dokumen terletak di sini. Teks yang relevan:

Ketika output standar telah dialihkan ke asynchronous event handler, ada kemungkinan bahwa pemrosesan output tidak akan selesai ketika metode ini kembali. Untuk memastikan bahwa penanganan kejadian asinkron telah selesai, panggil kelebihan WaitForExit () yang tidak menggunakan parameter setelah menerima true dari kelebihan ini. Untuk membantu memastikan bahwa acara yang Keluar ditangani dengan benar di aplikasi Windows Forms, setel properti SynchronizingObject.

Modifikasi kode mungkin terlihat seperti ini:

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}
Tyler Hundley
sumber
Ada beberapa seluk-beluk dengan penggunaan process.WaitForExit()seperti yang ditunjukkan oleh komentar untuk jawaban ini .
ingen