Assinar
Voltar
Backend

Fim do deadlock: as novas APIs de Process do .NET 11 que a comunidade pedia havia anos

Capturar a saida de um processo externo em C# sempre foi um convite a travamentos silenciosos. O .NET 11 preview traz metodos de alto nivel como Process.RunAndCaptureText que resolvem o problema em uma linha, sem risco de deadlock.

{}

Se você já rodou um processo externo em C# e o programa simplesmente congelou sem nenhum erro, você conheceu de perto um dos bugs mais antigos e traiçoeiros do .NET: o deadlock ao redirecionar a saída de um Process. O .NET 11 (preview) finalmente entrega APIs de alto nível para resolver isso de vez, um pedido que a comunidade repetia havia mais de uma década em threads intermináveis do StackOverflow.

O problema que atormentava a comunidade

Executar um processo filho e capturar o que ele imprime parece trivial. Na prática, o padrão “ingênuo” era este:

var process = new Process();
process.StartInfo.FileName = "dotnet";
process.StartInfo.Arguments = "--help";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();

process.WaitForExit();
string saida = process.StandardOutput.ReadToEnd();

O código acima funciona nos testes e trava em produção. O motivo é sutil: os pipes que ligam o processo pai ao filho têm buffer limitado (por volta de 4 KB no Windows e 64 KB no Unix). Se o processo filho gera muita saída, ele enche o buffer e fica bloqueado esperando alguém ler. Só que o pai está parado em WaitForExit(), esperando o filho terminar. Nenhum dos dois avança. Deadlock.

Os dois processos ficam se olhando: o filho não termina porque ninguém esvazia o buffer, e o pai não lê porque está esperando o filho terminar.

Havia ainda uma segunda armadilha. Ler StandardOutput até o fim antes de ler StandardError significa que, se o filho encher o buffer de erro, ele trava do mesmo jeito. A solução recomendada pela própria documentação era usar leitura assíncrona com BeginOutputReadLine, OutputDataReceived e um WaitForExit cuidadosamente ordenado, uma cerimônia verbosa que quase ninguém acertava de primeira.

Como o .NET 11 resolve

Segundo o blog oficial da Microsoft, a classe System.Diagnostics.Process recebeu no .NET 11 Preview 4 o maior conjunto de melhorias em anos. A ideia central é oferecer métodos de alto nível que fazem a coisa certa por padrão: iniciar o processo, drenar stdout e stderr ao mesmo tempo (usando multiplexação interna) e aguardar a saída, tudo numa única chamada segura contra deadlock.

Uma linha em vez de vinte

O caso mais comum, capturar a saída de texto de um comando, vira literalmente uma chamada:

using System.Diagnostics;

// Inicia, captura stdout + stderr e espera terminar, sem risco de deadlock
ProcessTextOutput resultado = Process.RunAndCaptureText("dotnet", ["--help"]);

Console.WriteLine($"Exit code: {resultado.ExitCode}");
Console.WriteLine(resultado.StandardOutput);

if (!string.IsNullOrEmpty(resultado.StandardError))
    Console.WriteLine($"Erros: {resultado.StandardError}");

Repare que os argumentos agora são passados como uma coleção (["--help"]), o que elimina outra fonte clássica de bugs: a montagem manual da string de argumentos com aspas e escapes. E existe a versão assíncrona correspondente, ideal para não bloquear a thread:

ProcessTextOutput resultado =
    await Process.RunAndCaptureTextAsync("git", ["status", "--porcelain"]);

Uma família de métodos para cada necessidade

O recurso não é um método isolado, e sim um conjunto coeso. As novas APIs (todas com variante ...Async) cobrem os cenários mais frequentes:

  • Process.RunAndCaptureText – inicia, captura saída/erro e espera, retornando texto.
  • Process.ReadAllText – drena stdout e stderr simultaneamente.
  • Process.ReadAllBytes – retorna bytes em vez de strings, útil para saída binária.
  • Process.ReadAllLines – entrega uma sequência de objetos ProcessOutputLine, distinguindo o que veio de cada canal.
  • Process.Run – inicia e espera sem capturar a saída.
  • Process.StartAndForget – dispara o processo, devolve o PID e libera os recursos imediatamente.

Além disso, o ProcessStartInfo ganhou controles finos que antes exigiam interop manual: KillOnParentExit (encerra o filho se o pai morrer, ótimo para evitar processos órfãos), InheritedHandles (controla quais handles o filho herda) e novos StandardInputHandle/StandardOutputHandle/StandardErrorHandle para redirecionar direto a um SafeFileHandle.

Percorrendo a saída linha a linha

Para logs em tempo real, ReadAllLines permite iterar sem se preocupar com a ordem de leitura dos canais:

await foreach (ProcessOutputLine linha in
    Process.ReadAllLinesAsync("npm", ["run", "build"]))
{
    // Cada linha sabe se veio do stdout ou do stderr
    var prefixo = linha.IsError ? "[ERRO] " : "[INFO] ";
    Console.WriteLine(prefixo + linha.Text);
}

Por que isso importa

Ferramentas de linha de comando, wrappers de git, chamadas a ffmpeg, build scripts, CLIs de infraestrutura: uma fatia enorme do software backend em .NET orquestra outros processos. Até agora, fazer isso de forma correta e resiliente exigia conhecer uma armadilha histórica e escrever código defensivo repetitivo. O .NET 11 transforma o caminho seguro no caminho mais fácil, que é exatamente como boas APIs deveriam funcionar.

Vale lembrar: o .NET 11 ainda está em preview, com lançamento final previsto para novembro de 2026. As assinaturas podem sofrer ajustes até a versão estável, então trate estas APIs como algo para experimentar, não para levar a produção crítica agora.

Se você mantém qualquer serviço que invoca processos externos, vale baixar o SDK de preview e testar. É uma daquelas melhorias que não aparecem nas manchetes, mas que apagam uma classe inteira de bugs difíceis de reproduzir, e a comunidade esperou por isso por muito tempo.

Fontes

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *