.NET 11 adiciona captura de saída de processos livre de deadlock
.NET 11 Preview 4 traz novas APIs em System.Diagnostics.Process que drenam stdout e stderr em paralelo, helpers de uma linha para executar e capturar, e KillOnParentExit.
Adam Sitnik anunciou uma rodada de melhorias em System.Diagnostics.Process que entraram no .NET 11 Preview 4. O ponto principal é que a armadilha de deadlock que todo desenvolvedor .NET pisa pelo menos uma vez finalmente foi resolvida no nível da BCL, e o código repetitivo de “iniciar um processo, capturar a saída, aguardar” se reduz a uma única chamada.
Por que o padrão antigo causa deadlock
A forma clássica parece segura e não é:
var psi = new ProcessStartInfo("git", "log")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using var p = Process.Start(psi)!;
string stdout = p.StandardOutput.ReadToEnd();
string stderr = p.StandardError.ReadToEnd();
p.WaitForExit();
Se o processo filho escrever em stderr mais do que cabe no buffer do pipe antes do processo pai chegar em stderr.ReadToEnd(), o filho trava esperando o pipe esvaziar e o pai trava esperando o stdout fechar. Os dois ficam esperando para sempre. A solução documentada era a API baseada em eventos com OutputDataReceived, StringBuilder manuais e um ManualResetEvent por stream. Funciona, mas ninguém escreve isso corretamente na primeira tentativa.
Os novos one-liners
O .NET 11 adiciona Process.RunAndCaptureText e Process.RunAndCaptureTextAsync, que multiplexam os dois pipes internamente:
ProcessTextOutput result = await Process.RunAndCaptureTextAsync(
"git",
["log", "--oneline", "-n", "20"]);
if (result.ExitStatus.ExitCode != 0)
throw new InvalidOperationException(result.StandardError);
Console.WriteLine(result.StandardOutput);
ProcessTextOutput carrega o stdout capturado, o stderr, o ID do processo e um ProcessExitStatus que reporta tanto o código de saída quanto, no Unix, o sinal de término. Existe um irmão Process.ReadAllText para quem já tem um Process rodando, além de ReadAllLines/Async para transmitir IEnumerable<ProcessOutputLine> com uma flag StandardError em cada linha, e ReadAllBytes/Async para ferramentas binárias. Segundo o blog, o RunAndCaptureText síncrono aloca cerca de 4.5x menos memória que o loop equivalente baseado em eventos no Windows.
Tempo de vida e desanexação
Dois problemas antigos também ganham suporte de primeira classe. ProcessStartInfo.KillOnParentExit amarra o filho ao ciclo de vida do pai no Windows, Linux e Android, de forma que uma ferramenta de CLI que quebra não deixa mais workers órfãos. ProcessStartInfo.StartDetached é o oposto: o filho sobrevive ao pai e ao terminal que o iniciou. E Process.StartAndForget devolve apenas o PID e libera imediatamente o handle do pai, que é o que você quer para lançamentos “fire and forget”:
int pid = Process.StartAndForget("notepad.exe");
Encanamento de handles
Para cenários de mais baixo nível, ProcessStartInfo.StandardInputHandle, StandardOutputHandle e StandardErrorHandle aceitam qualquer SafeFileHandle, incluindo pipes do novo SafeFileHandle.CreateAnonymousPipe e o sumidouro de descarte do File.OpenNullHandle. ProcessStartInfo.InheritedHandles permite que você liste explicitamente quais handles cruzam a fronteira do fork, o que fecha uma fonte real de vazamentos de lock de arquivo no Windows.
Teste no .NET 11 Preview 4 e comece a apagar handlers OutputDataReceived.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.