종료를 기다리는 동안 프로세스가 중단 된 이유는 무엇입니까?
이 코드는 내부에서 많은 작업을 수행하는 powershell 스크립트를 시작해야합니다. 예를 들어 MSBuild를 통해 코드를 다시 컴파일하기 시작하지만 문제는 너무 많은 출력을 생성하고이 코드는 power shell 스크립트가 올바르게 실행 된 후에도 종료를 기다리는 동안 중단되는 것입니다
때로는이 코드가 제대로 작동하고 때로는 막히기 때문에 “이상한”것입니다.
코드가 멈춤 :
process.WaitForExit (ProcessTimeOutMiliseconds);
Powershell 스크립트는 1-2 초 정도 실행되며 시간 제한은 19 초입니다.
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);
}
}
}
스크립트:
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;
}
어디
$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"
편집하다
@ingen의 솔루션에 중단 된 MS 빌드를 실행하기 위해 다시 시도하는 작은 래퍼를 추가했습니다.
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;
}
InternalExecuteScript
Ingen의 코드는 어디에 있습니까
답변
관련 게시물에서 허용되는 답변 을 요약 해 보겠습니다 .
문제는 StandardOutput 및 / 또는 StandardError를 리디렉션하면 내부 버퍼가 가득 찰 수 있다는 것입니다. 어떤 주문을 사용하든 문제가있을 수 있습니다.
- StandardOutput을 읽기 전에 프로세스가 종료 될 때까지 기다리면 프로세스가 쓰기를 차단할 수 있으므로 프로세스가 종료되지 않습니다.
- ReadToEnd를 사용하여 StandardOutput에서 읽는 경우 프로세스가 StandardOutput을 닫지 않는 경우 (예 : 종료되지 않거나 StandardError에 대한 쓰기가 차단 된 경우) 프로세스가 차단 될 수 있습니다.
그러나 수용 된 답변조차도 특정 경우의 집행 순서와 어려움을 겪습니다.
편집 : 시간 초과가 발생 하면 ObjectDisposedException을 피하는 방법은 아래 답변을 참조하십시오 .
Rx가 실제로 빛나는 여러 가지 이벤트를 조정하려는 상황이 있습니다.
Rx의 .NET 구현은 System.Reactive NuGet 패키지로 제공됩니다.
Rx가 이벤트 작업을 용이하게하는 방법을 알아 보겠습니다.
// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
.Subscribe(
eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
exception => error.AppendLine(exception.Message)
).DisposeWith(disposables);
FromEventPattern
이벤트의 개별 발생을 통합 스트림 (일명 관찰 가능)에 매핑 할 수 있습니다. 이를 통해 파이프 라인에서 이벤트를 처리 할 수 있습니다 (LINQ와 같은 의미 체계 사용). Subscribe
여기에 사용 과부하가 제공된다 Action<EventPattern<...>>
하고 Action<Exception>
. 관찰 된 이벤트가 발생할 때마다 그 sender
와는 args
래핑됩니다 EventPattern
과를 통해 밀어 Action<EventPattern<...>>
. 파이프 라인에서 예외가 발생하면 Action<Exception>
이 사용됩니다.
의 단점 중 하나 Event
이 유스 케이스 (및 참조 된 게시물의 모든 해결 방법에서)에 명확하게 설명 된 패턴 이벤트 핸들러를 언제 / 구독 취소해야하는지 명확하지 않다는 것입니다.
Rx를 IDisposable
사용하면 구독을 할 때 다시 돌아옵니다 . 폐기 할 때 구독을 효과적으로 종료합니다. 의 추가로 DisposeWith
(에서 차용 확장 메서드 RxUI ), 우리는 여러 추가 할 수 있습니다 IDisposable
A와 S를 CompositeDisposable
(이름을 disposables
코드 샘플에서). 모두 완료되면 한 번의 호출로 모든 구독을 종료 할 수 있습니다disposables.Dispose()
.
확실히, Rx로 할 수있는 것은 아무것도 없으며, 바닐라 .NET으로는 할 수 없습니다. 기능적인 사고 방식에 적응하면 결과 코드는 추론하기가 훨씬 쉽습니다.
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;
}
}
우리는 이벤트를 옵저버 블에 매핑하는 첫 번째 부분에 대해 이미 논의 했으므로 고기 부분으로 바로 이동할 수 있습니다. 여기에 Observable을processExited
변수를 두 번 이상 사용하려고하기 때문에 변수에 .
먼저을 호출하여 활성화합니다 Subscribe
. 그리고 나중에 우리가 첫 번째 가치를 ‘기다리고 싶어’.
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();
OP의 문제점 중 하나는 process.WaitForExit(processTimeOutMiliseconds)
시간이 초과되면 프로세스가 종료 된다고 가정한다는 것 입니다. 에서 MSDN :
연관된 프로세스가 종료 될 때까지 지정된 밀리 초 동안 프로세스 컴포넌트가 대기하도록 지시합니다 .
대신 시간이 초과되면 제어를 현재 스레드로 되돌립니다 (즉, 블로킹 중지). 프로세스 시간이 초과되면 수동으로 강제 종료해야합니다. 시간 초과가 발생한시기를 알기 위해 Process.Exited
이벤트를processExited
처리 관찰 가능 객체에 . 이런 식으로 Do
연산자에 대한 입력을 준비 할 수 있습니다 .
코드는 매우 자명하다. exitedSuccessfully
프로세스가 정상적으로 종료 된 경우 그렇지 않은 경우 exitedSuccessfully
강제 종료해야합니다. 참고 process.Kill()
로 비동기 적으로 실행되고, 심판 발언 . 그러나 process.WaitForExit()
바로 호출 하면 교착 상태가 다시 발생할 가능성이 열립니다. 따라서 강제 종료의 경우에도 using
출력이 중단 / 손상된 것으로 간주 될 수 있으므로 스코프가 종료 될 때 모든 일회용품을 정리하는 것이 좋습니다 .
try catch
구조는 사용자가 정렬 한 예외적 인 경우 (웃기려는 의도 없음) 예약되어 processTimeOutMilliseconds
프로세스가 완료에 필요한 실제 시간. 즉, Process.Exited
이벤트와 타이머 사이에 경쟁 조건이 발생합니다 . 이런 일이 일어날 가능성은 비동기 특성으로 인해 다시 확대됩니다 process.Kill()
. 테스트하는 동안 한 번 발생했습니다.
완전성을 위해 DisposeWith
확장 방법.
/// <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;
}
}
답변
독자 들의 이익 을 위해 이것을 2 개의 섹션으로 나누겠습니다.
섹션 A : 문제 및 유사한 시나리오를 처리하는 방법
섹션 B : 문제 재현 및 해결
섹션 A : 문제
이 문제가 발생하면 프로세스가 작업 관리자에 나타나고 2-3 초가 사라진 후 (정상) 시간 초과를 기다린 다음 예외가 발생합니다.
& 아래 시나리오 4 참조
귀하의 코드에서 :
Process.WaitForExit(ProcessTimeOutMiliseconds);
이것으로 당신이 기다리고있어Process
에 시간 제한 또는 종료 지금까지 발생, 첫째 .OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
그리고errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
이것으로 당신은 완료를 알리기 위해OutputData
&ErrorData
읽기 동작을 기다리고 있습니다.Process.ExitCode == 0
프로세스가 종료 될 때 상태를 가져옵니다.
다른 설정 및주의 사항 :
- 시나리오 1 (행복한 경로) : 프로세스가 시간 종료 전에 완료되므로 stdoutput 및 stderror도 완료되기 전에 완료됩니다.
- 시나리오 2 : 프로세스, OutputWaitHandle 및 ErrorWaitHandle이 시간 종료되었지만 stdoutput & stderror를 계속 읽고 있으며 시간 종료 대기 핸들러 후에 완료됩니다. 이것은 또 다른 예외로 이어집니다
ObjectDisposedException()
- 시나리오 3 : 프로세스 시간 초과 (19 초)이지만 stdout 및 stderror가 작동 중이면 WaitHandler가 시간 초과 (19 초) 될 때까지 대기하여 추가 지연이 +19 초입니다.
- 시나리오 4 : 프로세스가 시간 초과되고 코드가 조기 쿼리를 시도
Process.ExitCode
하여 오류가 발생했습니다System.InvalidOperationException: Process must exit before requested information can be determined
.
이 시나리오를 12 번 이상 테스트했으며 테스트 중에 다음 설정이 사용되었습니다.
- 약 2-15 개의 프로젝트 빌드를 시작하여 5KB에서 198KB 범위의 출력 스트림 크기
- 시간 종료 창에서 조기 시간 종료 및 프로세스 종료
업데이트 된 코드
.
.
.
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{}
편집하다:
MSBuild로 몇 시간 동안 놀고 난 후에 마침내 시스템에서 문제를 재현 할 수있었습니다.
섹션 B : 문제 재현 및 해결
MSBuild 는
-m[:number]
빌드 할 때 사용할 최대 동시 프로세스 수를 지정하는 데 사용되는 스위치가 있습니다.이 기능을 사용하면 MSBuild가 빌드가 완료된 후에도 존재하는 여러 노드를 생성합니다. 이제는
Process.WaitForExit(milliseconds)
절대 종료하지 않고 결국 시간 초과됩니다.
나는 이것을 몇 가지 방법으로 해결할 수 있었다
-
CMD를 통해 MSBuild 프로세스를 간접적으로 생성
$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
-
MSBuild를 계속 사용하지만 nodeReuse를 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
-
병렬 빌드를 사용하지 않는 경우에도 CMD
WaitForExit
를 통해 빌드를 시작 하여 프로세스가 정지되는 것을 막을 수 있으므로 빌드 프로세스에 직접 의존하지 않습니다.$path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln""" $cmdOutput = cmd.exe /c $path1 '2>&1' $cmdOutput
너무 많은 MSBuild 노드가 놓여 있지 않기 때문에 두 번째 방법이 선호됩니다.
답변
문제는 StandardOutput 및 / 또는 StandardError를 리디렉션하면 내부 버퍼가 가득 찰 수 있다는 것입니다.
위에서 언급 한 문제를 해결하기 위해 별도의 스레드에서 프로세스를 실행할 수 있습니다. WaitForExit를 사용하지 않고 프로세스 종료 이벤트를 사용하여 프로세스의 ExitCode를 비동기식으로 반환하여 프로세스가 완료되었음을 확인합니다.
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;
}
위의 코드는 명령 줄 인수로 FFMPEG.exe를 호출하여 전투 테스트를 거쳤습니다. 나는 mp4 파일을 mp3 파일로 변환하고 실패없이 한 번에 1000 개 이상의 비디오를하고있었습니다. 불행히도 나는 직접적인 파워 쉘 경험이 없지만 이것이 도움이되기를 바랍니다.
답변
이것이 문제인지 확실하지 않지만 MSDN을 보면 출력을 비동기 적으로 리디렉션 할 때 과부하 된 WaitForExit에 이상이있는 것 같습니다. MSDN 기사에서는 오버로드 된 메서드를 호출 한 후 인수가없는 WaitForExit를 호출하는 것이 좋습니다.
표준 출력이 비동기 이벤트 핸들러로 경로 재 지정된 경우이 메소드가 리턴 될 때 출력 처리가 완료되지 않았을 수 있습니다. 비동기 이벤트 처리가 완료되도록하려면이 오버로드에서 true를 수신 한 후 매개 변수가없는 WaitForExit () 오버로드를 호출하십시오. Windows Forms 응용 프로그램에서 Exited 이벤트가 올바르게 처리되도록하려면 SynchronizingObject 속성을 설정하십시오.
코드 수정은 다음과 같습니다.
if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
process.WaitForExit();
}