“WaitForExit”에 ProcessStartInfo가 걸려 있습니까? 왜?


187

다음 코드가 있습니다.

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

시작하는 프로세스의 출력 길이가 약 7MB라는 것을 알고 있습니다. Windows 콘솔에서 실행하면 정상적으로 작동합니다. 불행히도 프로그래밍 방식으로 이것은 WaitForExit에서 무기한 정지됩니다. 더 작은 출력 (예 : 3KB)의 경우 코드가 멈추지 않습니다.

ProcessStartInfo의 내부 StandardOutput이 7MB를 버퍼링 할 수 없습니까? 그렇다면 대신 어떻게해야합니까? 그렇지 않은 경우, 내가 뭘 잘못하고 있습니까?


그것에 대한 전체 소스 코드가있는 최종 솔루션은 무엇입니까?
Kiquenet

2
나는 같은 문제에
부딪쳤다.

6
예, 최종 해결책 : 마지막 두 줄을 바꾸십시오. 그것은에서의 매뉴얼 .
Amit Naidu

4
msdn에서 : 코드 예제는 p.WaitForExit 전에 p.StandardOutput.ReadToEnd를 호출하여 교착 상태를 피합니다. 부모 프로세스가 p.StandardOutput.ReadToEnd 전에 p.WaitForExit를 호출하고 자식 프로세스가 리디렉션 된 스트림을 채우기에 충분한 텍스트를 쓰면 교착 상태가 발생할 수 있습니다. 부모 프로세스는 자식 프로세스가 종료 될 때까지 무기한 대기합니다. 자식 프로세스는 부모가 전체 StandardOutput 스트림에서 읽을 때까지 무기한 대기합니다.
Carlos Liu

이 작업을 제대로 수행하는 것이 얼마나 복잡합니까? 더 간단한 명령 행 리디렉션> outputfile :)을 사용하여 문제를 해결하게되어 기뻤습니다.
eglasius

답변:


393

문제는 리디렉션 StandardOutput및 / 또는 StandardError내부 버퍼가 가득 찰 수 있다는 것입니다. 어떤 주문을 사용하든 문제가있을 수 있습니다.

  • 프로세스를 읽기 전에 프로세스가 종료 될 때까지 기다리면 프로세스 StandardOutput쓰기를 차단할 수 있으므로 프로세스가 종료되지 않습니다.
  • 당신이 읽을 경우 StandardOutputReadToEnd를 사용하여 다음 당신의 프로세스는 프로세스가 종료되지 않을 경우 차단할 수 있습니다 StandardOutput(이에 쓰기를 차단하는 경우가 종료되지 않을 경우 예를 들어, 또는 StandardError).

해결책은 비동기 읽기를 사용하여 버퍼가 가득 차지 않도록하는 것입니다. 어떤 교착 상태를 방지하고 모두에서 모든 출력을 수집하는 방법 StandardOutputStandardError이 작업을 수행 할 수 있습니다 :

편집 : 시간 초과가 발생 하면 ObjectDisposedException을 피하는 방법은 아래 답변을 참조하십시오 .

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
출력을 리디렉션하여 문제를 일으키는 지 전혀 몰랐지만 확실했습니다. 4 시간 동안 머리를 두드리고 게시물을 읽은 후 5 분 안에 고쳤습니다. 잘 하셨어요!
벤 그립 카

1
@AlexPeck 문제가 콘솔 앱으로 실행되었습니다. 한스 옆모습은 여기에서 문제를 확인 : stackoverflow.com/a/16218470/279516
밥 혼에게

5
명령 프롬프트가 닫힐 때마다 다음과 같이 나타납니다. mscorlib.dll에서 "System.ObjectDisposed"유형의 처리되지 않은 예외가 발생했습니다. 추가 정보 : 안전 핸들이 닫혔습니다
user1663380

3
위의 @ user1663380에서 설명한 것과 비슷한 문제가있었습니다. 당신은 가능성이있다 생각하십니까 using이벤트 핸들러의 문이해야 위의using 프로세스 자체에 대한 문?
Dan Forbes

2
대기 핸들이 필요하다고 생각하지 않습니다. msdn에 따라 타임 아웃이 아닌 버전의 WaitForExit로 마무리합니다. 표준 출력이 비동기 이벤트 처리기로 리디렉션되면이 메서드가 반환 될 때 출력 처리가 완료되지 않았을 수 있습니다. 비동기 이벤트 처리가 완료되도록하려면이 오버로드에서 true를받은 후 매개 변수가없는 WaitForExit () 오버로드를 호출하십시오.
패트릭

98

문서 에 대한은 Process.StandardOutput당신이 그렇지 않으면 당신은 교착 수 있습니다 기다려야하기 전에 아래 복사 니펫을 읽고 말한다 :

 // 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
이것이 내 환경의 결과인지 100 % 확신 할 수는 없지만 설정 RedirectStandardOutput = true;하고 사용하지 않으면 p.StandardOutput.ReadToEnd();교착 상태 / 중단이 발생합니다.
Chris S

3
진실. 나는 비슷한 상황에 있었다. 프로세스에서 ffmpeg로 변환 할 때 아무런 이유없이 StandardError를 리디렉션하고 있었으며 StandardError 스트림에서 교착 상태를 생성하기에 충분합니다.
레온 펠티에

표준 출력을 리디렉션하고 읽는 경우에도 여전히 중단됩니다.
user3791372

@ user3791372 StandardOutput 뒤의 버퍼가 완전히 채워지지 않은 경우에만 적용 가능하다고 생각합니다. 여기서 MSDN은 그 정의를하지 않습니다. 내가 읽을 것을 권장하는 훌륭한 기사는 다음과 같습니다. dzone.com/articles/async-io-and-threadpool
Cary

19

Mark Byers의 답변은 훌륭하지만 다음을 추가합니다.

OutputDataReceivedErrorDataReceived대표는 전에 제거해야 outputWaitHandle하고 errorWaitHandle배치받을. 제한 시간이 초과 된 후 프로세스가 계속 데이터를 출력 한 후 종료되면 outputWaitHandleerrorWaitHandle변수는 폐기 된 후 액세스됩니다.

(참고로 나는 그의 게시물에 대해 언급 할 수 없었기 때문에이 경고를 대답으로 추가해야했습니다.)


2
아마도 CancelOutputRead ? 를 호출하는 것이 좋습니다 .
Mark Byers 2016 년

이 답변에 Mark의 편집 코드를 추가하는 것은 다소 훌륭합니다! 분에 정확히 같은 문제가 있습니다.
ianbailey

8
@ianbailey 이것을 해결하는 가장 쉬운 방법은 using (Process p ...)을 using (AutoResetEvent errorWaitHandle ...) 안에 넣는 것입니다.
Didier A.

18

이것은 .NET 4.5 이상을위한보다 현대적이고 대기 가능한 TPL (Task Parallel Library) 기반 솔루션입니다.

사용 예

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

이행

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
현재까지 가장
훌륭하고

1
어떤 이유로, 이것은 나를 위해 일한 유일한 솔루션이었습니다. 응용 프로그램이 중단되었습니다.
Jack

1
프로세스가 시작된 후 종료되고 종료 이벤트가 첨부되기 전의 조건을 처리하지 않는 것 같습니다. 내 제안-모든 등록 후 프로세스 시작
Stas Boyarincev 2016 년

@StasBoyarincev 감사합니다. 이 변경 사항으로 StackOverflow 답변을 업데이트하는 것을 잊었습니다.
무하마드 Rehan Saeed

1
@MuhammadRehanSaeed 또 다른 점은 process.Start 전에 process.BeginOutputReadLine () 또는 process.BeginErrorReadLine ()을 호출 할 수없는 것 같습니다. 이 경우 오류가 발생합니다. StandardOut이 리디렉션되지 않았거나 프로세스가 아직 시작되지 않았습니다.
Stas Boyarincev

17

처리 시간이 초과되면 처리되지 않은 ObjectDisposedException 관련 문제가 발생합니다. 그러한 경우 조건의 다른 부분 :

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

실행되지 않습니다. 이 문제는 다음과 같은 방식으로 해결되었습니다.

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
완전성을 위해, 리디렉션을 true로 설정하는 것이 누락되었습니다.
knocte

프로세스가 사용자 입력을 요구할 수 있으므로 (예 : 무언가 입력) 시간이 초과 된 것을 제거했습니다. 따라서 사용자가 빠를 필요는 없습니다.
knocte

왜 변경 outputerroroutputBuilder? 누군가가 작동하는 완전한 답변을 제공 할 수 있습니까?
Marko Avlijaš

System.ObjectDisposedException :이 버전에서도 안전한 핸들이 닫혔습니다
Matt

8

Rob은 이에 응답하여 몇 시간의 시련을 더 절약했습니다. 대기하기 전에 출력 / 오류 버퍼를 읽으십시오.

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

1
하지만 전화 한 후에 더 많은 데이터가 나오면 어떻게해야 WaitForExit()합니까?
knocte

내 테스트를 기반으로하는 @knocte ReadToEnd또는 모든 데이터를 읽은 StandardOutput.BaseStream.CopyTo후 유사한 방법 (예 :) 이 반환됩니다 . 아무것도 뒤에 올 것이다
S.Serpooshan

당신은 ReadToEnd ()도 종료를 기다리고 있다고 말하고 있습니까?
knocte

2
@knocte 당신은 Microsoft가 만든 API를 이해하려고합니까?
aaaaaa

해당 MSDN 페이지의 문제는 StandardOutput 뒤의 버퍼가 가득 찰 수 있다고 설명하지 않았으며 그 상황에서 자식은 쓰기를 중지하고 버퍼가 비워 질 때까지 기다려야합니다 (부모가 버퍼의 데이터를 읽습니다) . ReadToEnd ()는 버퍼가 닫히거나 버퍼가 가득 찼거나 자식이 버퍼가 가득 찼을 때까지만 읽을 수 있습니다. 이것이 나의 이해입니다.
Cary

7

우리는이 문제 (또는 변형)도 가지고 있습니다.

다음을 시도하십시오 :

1) p.WaitForExit (nnnn)에 타임 아웃을 추가하십시오. 여기서 nnnn은 밀리 초입니다.

2) ReadForEnd 호출을 WaitForExit 호출 앞에 두십시오. 이것은 이다 우리는 MS가 추천 무엇을 본 적이.


5

https://stackoverflow.com/a/17600012/4151626에 대한 EM0의 크레딧

내부 시간 초과 및 생성 된 응용 프로그램의 StandardOutput 및 StandardError 사용으로 인해 다른 응용 프로그램 (EM0 포함)이 여전히 응용 프로그램에 교착 상태가되었습니다. 다음은 나를 위해 일한 것입니다.

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

편집 : 코드 샘플에 StartInfo 초기화 추가


이것은 내가 사용하는 것이며 더 이상 교착 상태에 문제가 없었습니다.
Roemer

3

나는 이것을 이렇게 해결했다.

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

입력, 출력 및 오류를 모두 리디렉션하고 출력 및 오류 스트림에서의 읽기를 처리했습니다. 이 솔루션은 Windows 7 및 Windows 8 모두 SDK 7-8.1에서 작동합니다.


2
엘리나 : 답변 주셔서 감사합니다. 이 MSDN doc ( msdn.microsoft.com/en-us/library/… ) 의 맨 아래에는 리디렉션 된 stdout 및 stderr 스트림의 끝을 동 기적으로 읽는 경우 잠재적 교착 상태에 대해 경고 하는 몇 가지 참고 사항이 있습니다. 귀하의 솔루션이이 문제에 취약한 지 말하기는 어렵습니다. 또한 프로세스의 stdout / stderr 출력을 입력으로 다시 보내는 것 같습니다. 왜? :)
Matthew Piatt

3

Mark Byers, Rob, stevejay 답변을 고려하여 비동기 스트림 읽기를 사용하여 문제를 해결할 클래스를 만들려고했습니다. 그렇게하면 비동기 프로세스 출력 스트림 읽기와 관련된 버그가 있음을 깨달았습니다.

Microsoft에서 해당 버그를보고했습니다. https://connect.microsoft.com/VisualStudio/feedback/details/3119134

요약:

당신은 할 수 없습니다 :

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

System.InvalidOperationException이 나타납니다. StandardOut이 리디렉션되지 않았거나 프로세스가 아직 시작되지 않았습니다.

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

그런 다음 프로세스가 시작된 후 비동기 출력 읽기를 시작해야합니다.

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

이렇게하면 출력 스트림이 비동기로 설정하기 전에 데이터를 수신 할 수 있으므로 경쟁 조건을 설정하십시오.

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

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

그런 다음 일부 사람들은 스트림을 비동기로 설정하기 전에 스트림을 읽어야한다고 말할 수 있습니다. 그러나 같은 문제가 발생합니다. 동기 읽기 사이에 경쟁 조건이 있으며 스트림을 비동기 모드로 설정합니다.

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

"Process"및 "ProcessStartInfo"가 실제로 설계된 방식으로 프로세스의 출력 스트림에 대한 안전한 비동기식 읽기를 달성 할 수있는 방법은 없습니다.

다른 사용자가 제안한 것처럼 비동기 읽기를 사용하는 것이 좋습니다. 그러나 경쟁 조건으로 인해 일부 정보를 놓칠 수 있습니다.


1

나는 이것이 간단하고 더 나은 접근법이라고 생각합니다 (필요하지 않습니다 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;
    }
}

사실이지만 .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"코드를 단순화하기 위해 해서는 안 됩니까? 또는 "echo command | " + Path + @"\ggsci.exe"별도의 obeycommand.txt 파일을 사용하지 않으려는 경우 와 동일한 기능을 수행 할 수 있습니다.
Amit Naidu

3
솔루션에는 AutoResetEvent가 필요하지 않지만 폴링합니다. 이벤트를 사용하는 대신 폴링을 수행 할 때 (사용 가능한 경우) 아무 이유없이 CPU를 사용하고 있으며 이는 프로그래머가 잘못되었음을 나타냅니다. AutoResetEvent를 사용하는 다른 솔루션과 비교할 때 솔루션이 실제로 나쁩니다. (그러나 나는 당신이 도와 주려고했기 때문에 -1을주지 않았습니다!).
Eric Ouellet

1

위의 답변 중 어느 것도 일을하고 있지 않습니다.

Rob 솔루션이 멈추고 'Mark Byers'솔루션이 예외를 얻습니다 (다른 답변의 "솔루션"을 시도했습니다).

그래서 다른 해결책을 제안하기로 결정했습니다.

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

이 코드는 디버깅되어 완벽하게 작동합니다.


1
좋은! GetProcessOutputWithTimeout메소드를 호출 할 때 토큰 매개 변수가 제공되지 않습니다 .
S.Serpooshan

1

소개

현재 허용되는 답변이 작동하지 않으며 (예외 발생) 해결 방법이 너무 많지만 완전한 코드는 없습니다. 이것은 대중적인 질문이기 때문에 많은 사람들의 시간을 낭비하고 있습니다.

Mark Byers의 답변과 Karol Tyl의 답변을 결합하여 Process.Start 메서드를 사용하려는 방법에 따라 전체 코드를 작성했습니다.

용법

git 명령 주위에 진행률 대화 상자를 만드는 데 사용했습니다. 이것이 내가 사용한 방법입니다.

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

이론적으로 stdout과 stderr을 결합 할 수도 있지만 테스트하지는 않았습니다.

암호

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

여전히 System.ObjectDisposedException을 가져옵니다.이 버전에서도 안전한 핸들이 닫혔습니다.
Matt

1

나는 이것이 오래되었다는 것을 알고 있지만,이 전체 페이지를 읽은 후에는 해결책이 없었습니다.하지만 비록 코드가 따르기가 조금 어려워서 Muhammad Rehan을 시도하지는 않았지만, 그가 올바른 길을 가고 있다고 생각합니다. . 완전히 작동하지 않는 작동하지 않는다고 말하면 때로는 제대로 작동하지만 EOF 마크 이전의 출력 길이와 관련이 있다고 생각합니다.

어쨌든, 나를 위해 일한 해결책은 다른 스레드를 사용하여 StandardOutput 및 StandardError를 읽고 메시지를 작성하는 것이 었습니다.

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

이것이 어려운 일이라고 생각한 누군가를 돕기를 바랍니다!


예외 : sw.FlushAsync(): Object is not set to an instance of an object. sw is null. 어떻게 / 어떻게 sw정의 해야 합니까?
wallyk

1

여기에서 모든 게시물을 읽은 후 Marko Avlijaš의 통합 솔루션에 정착했습니다. 그러나 모든 문제를 해결하지 못했습니다.

우리 환경에는 수백 년 동안 축적되어 많은 사람들이 다른 스타일로 작성한 수백 개의 다른 .bat .cmd .exe 등의 파일을 실행하도록 예약 된 Windows 서비스가 있습니다. 우리는 프로그램 및 스크립트 작성을 제어 할 수 없으며, 성공 / 실패에 대한 예약, 실행 및보고에 대한 책임이 있습니다.

그래서 나는 다른 수준의 성공으로 여기에서 거의 모든 제안을 시도했습니다. Marko의 답변은 거의 완벽했지만 서비스로 실행할 때 항상 표준 출력을 캡처하지는 않았습니다. 나는 왜 바닥에 도달하지 못했습니다.

우리의 모든 경우에 작동하는 유일한 해결책은 다음과 같습니다. http://csharptest.net/319/using-the-processrunner-class/index.html


이 라이브러리를 사용하려고합니다. 코드의 범위를 지정했으며 대의원을 현명하게 사용하는 것으로 보입니다. Nuget에 잘 패키지되어 있습니다. 그것은 기본적으로 내가 결코 비난받을 수없는 전문성을 악취한다. 물면 말해 줄 것이다.
Steve Hibbert

소스 코드에 대한 링크가 작동하지 않습니다. 다음에 코드를 답변에 복사하십시오.
Vitaly Zdanevich가

1

해결 방법 모든 복잡성을 피하기 위해 사용했습니다.

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

그래서 나는 임시 파일을 만들고, 출력과 오류를 모두 사용하여 리디렉션 > outputfile > 2>&1하고 프로세스가 끝나면 파일을 읽습니다.

다른 솔루션은 출력으로 다른 작업을 수행하려는 시나리오에는 적합하지만 간단한 작업의 경우 많은 복잡성을 피할 수 있습니다.


1

나는 많은 답변을 읽었으며 스스로 만들었습니다. 어떤 경우 에도이 문제가 해결되는지 확실하지 않지만 내 환경에서는 수정됩니다. 나는 WaitForExit를 사용하지 않고 출력 및 오류 종료 신호 모두에서 WaitHandle.WaitAll을 사용합니다. 누군가가 그것에 대해 가능한 문제를 보게되면 기쁠 것입니다. 아니면 누군가를 도울 것입니다. 시간 초과를 사용하지 않기 때문에 나에게 더 좋습니다.

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

내가 이것을 사용하고 핸들 시간 초과 Task.Run과 포장, 나 또한 시간 제한에 죽일 프로세스 id를 반환
plus5volt

0

비동기 적으로 생각하면 standardOutput과 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.
    }
}

Mark Byers의 답변을 기반으로합니다. 비동기 방식이 아닌 경우 string output = tStandardOutput.result;대신 사용할 수 있습니다.await



-1

이 게시물은 오래되었지만 어쩌면 일반적으로 중단되는 주된 원인은 redirectStandardoutput의 스택 오버플로 때문이거나 redirectStandarderror가있는 경우입니다.

출력 데이터 또는 오류 데이터가 크기 때문에 여전히 무한정 처리하는 동안 정지 시간이 발생합니다.

이 문제를 해결하려면

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

11
문제는 사람들이 스트림에 액세스하기를 원하기 때문에 명시 적으로 true로 설정한다는 것입니다! 그렇지 않으면 우리는 그것들을 거짓으로 남겨 둘 수 있습니다.
user276648

-1

여기에 게시 된 샘플 코드를 리디렉터 및 다른 프로그램을 리디렉트 한 코드라고합니다. 그것이 나라면 아마도 문제를 복제하는 데 사용할 수있는 테스트 리디렉션 프로그램을 작성했을 것입니다.

그래서 나는했다. 테스트 데이터를 위해 ECMA-334 C # 언어 사양 v PDF를 사용했습니다. 약 5MB입니다. 다음은 그 중 중요한 부분입니다.

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

datasize 값은 실제 파일 크기와 일치하지 않지만 중요하지 않습니다. PDF 파일이 항상 줄 끝에서 CR과 LF를 모두 사용하는지 여부는 확실하지 않지만 중요하지 않습니다. 다른 큰 텍스트 파일을 사용하여 테스트 할 수 있습니다.

이를 사용하면 대량의 데이터를 작성할 때 샘플 리디렉터 코드가 정지하지만 소량을 쓸 때는 정지합니다.

나는 어떻게 든 그 코드의 실행을 추적하려고 많이 노력했지만 그렇게 할 수 없었습니다. 리디렉션 된 프로그램이 별도의 콘솔 창을 얻으려고 콘솔을 만들 수없는 리디렉션 된 프로그램 줄을 주석 처리했지만 할 수 없었습니다.

그런 다음 새 창, 부모 창 또는 창 없음에서 콘솔 앱을 시작하는 방법을 찾았습니다 . 따라서 한 콘솔 프로그램이 ShellExecute없이 다른 콘솔 프로그램을 시작할 때 별도의 콘솔을 (쉽게) 가질 수 없으며 ShellExecute가 리디렉션을 지원하지 않기 때문에 다른 프로세스에 대한 창을 지정하지 않아도 콘솔을 공유해야합니다.

리디렉션 된 프로그램이 버퍼를 어딘가에 채우면 데이터를 읽을 때까지 기다려야하며 그 시점에서 리디렉터가 데이터를 읽지 않으면 교착 상태라고 가정합니다.

해결책은 ReadToEnd를 사용하지 않고 데이터를 쓰는 동안 데이터를 읽는 것이지만 비동기 읽기를 사용할 필요는 없습니다. 해결책은 매우 간단 할 수 있습니다. 다음은 5MB PDF로 작동합니다.

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

다른 가능성은 GUI 프로그램을 사용하여 경로 재 지정을 수행하는 것입니다. 위의 코드는 명백한 수정을 제외하고 WPF 응용 프로그램에서 작동합니다.


-3

나는 같은 문제가 있었지만 그 이유는 달랐습니다. 그러나 Windows 8에서는 발생하지만 Windows 7에서는 발생하지 않습니다. 다음 줄에서 문제가 발생한 것 같습니다.

pProcess.StartInfo.UseShellExecute = False

솔루션은 UseShellExecute를 비활성화하지 않았습니다. 나는 이제 원하지 않는 쉘 팝업 창을 받았지만 특별한 일이 일어나기를 기다리는 프로그램보다 훨씬 낫습니다. 그래서 그에 대한 다음 해결 방법을 추가했습니다.

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

이제 나를 귀찮게하는 유일한 이유는 이것이 왜 Windows 8에서 처음 발생하는지에 대한 것입니다.


1
UseShellExecute출력을 재 지정하려면 false로 설정 해야 합니다.
Brad Moore
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.