Em muitas aplicações desktop, é necessário invocar utilitários de linha de comando (como ferramentas de compressão ou clientes de controle de versão) para realizar tarefas específicas. No entanto, a execução desses utilitários não deve exibir a janela do console, e a saída padrão (stdout) e de erro (stderr) deve ser capturada e exibida na interface gráfica da aplicação principal.
Para alcançar esse comportamento no Windows utilizando C++, é preciso resolver dois desafios: invocar o processo filho de forma oculta e redirecionar seus fluxos de entrada e saída.
Seleção da API do Windows
O Windows oferece três APIs principais para execução de processos externos:
- WinExec: A mais simples, mas limitada e considerada obsoleta.
- ShellExecute: Mais flexível, permite abrir arquivos com suas associações padrão e URLs, mas não oferece controle granular sobre o processo.
- CreateProcess: A API mais robusta e complexa. Permite configurar atributos de segurança, herança de identificadores (handles) e redirecionamento de fluxos padrão.
Para capturar a saída do console, o CreateProcess é a escolha obrigatória, pois permite a manipulação direta dos identificadores de entrada e saída padrão do novo processo.
Mecanismo de Redirecionamento: Pipes Aônimos
Para interceptar os dados que seriam exibidos no console, utilizamos pipes anônimos. Um pipe anônimo é um canal de comunicação unidirecional que conecta dois processos. No contexto de redirecionamento, o processo pai cria o pipe e passa o identificador de gravação para o processo filho. Tudo o que o filho escrever na saída padrão será direcionado para o pipe, permitindo que o pai leia esses dados posteriormente.
Para que isso funcione, o identificador do pipe deve ser configurado como herdável, e o processo filho deve ser instruído a usar esse identificador como sua saída padrão.
Implementação Prática
Abaixo está a implementação detalhada em C++ utilizando a API Win32. O código foi estruturado para criar um pipe de saída (para capturar stdout/stderr) e um pipe de entrada (para enviar comandos ao processo filho).
1. Configuração de Atributos de Segurança e Criação dos Pipes
Primeiro, definimos a estrutura SECURITY_ATTRIBUTES para permitir que os handles sejam herdados pelo processo filho. Em seguida, criamos dois pipes anônimos: um para a saída do filho (lido pelo pai) e outro para a entrada do filho (escrito pelo pai).
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>
// Estrutura para gerenciar os handles dos pipes
struct PipeHandles {
HANDLE hRead;
HANDLE hWrite;
};
bool InitializePipes(PipeHandles& outPipe, PipeHandles& inPipe) {
SECURITY_ATTRIBUTES secAttr;
secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
secAttr.bInheritHandle = TRUE; // Permite herança pelo processo filho
secAttr.lpSecurityDescriptor = nullptr;
// Cria pipe para captura de saída (Filho escreve, Pai lê)
if (!CreatePipe(&outPipe.hRead, &outPipe.hWrite, &secAttr, 0)) {
return false;
}
// Garante que o handle de leitura não seja herdado pelo filho
SetHandleInformation(outPipe.hRead, HANDLE_FLAG_INHERIT, 0);
// Cria pipe para entrada de dados (Pai escreve, Filho lê)
if (!CreatePipe(&inPipe.hRead, &inPipe.hWrite, &secAttr, 0)) {
return false;
}
// Garante que o handle de escrita não seja herdado pelo filho
SetHandleInformation(inPipe.hWrite, HANDLE_FLAG_INHERIT, 0);
return true;
}
2. Configuração e Lançamento do Processo Filho
Com os pipes criados, configuramos a estrutura STARTUPINFO para mapear os handles de entrada, saída e erro padrão do processo filho para os handles de escrita e leitura dos pipes.
bool LaunchHiddenProcess(const std::wstring& commandLine,
const PipeHandles& outPipe,
const PipeHandles& inPipe,
PROCESS_INFORMATION& procInfo) {
STARTUPINFO startInfo;
ZeroMemory(&startInfo, sizeof(STARTUPINFO));
startInfo.cb = sizeof(STARTUPINFO);
// Mapeamento dos fluxos padrão para os pipes
startInfo.hStdError = outPipe.hWrite;
startInfo.hStdOutput = outPipe.hWrite;
startInfo.hStdInput = inPipe.hRead;
startInfo.dwFlags |= STARTF_USESTDHANDLES;
// Oculta a janela do console
startInfo.dwFlags |= STARTF_USESHOWWINDOW;
startInfo.wShowWindow = SW_HIDE;
ZeroMemory(&procInfo, sizeof(PROCESS_INFORMATION));
// Cria um buffer gravável para a linha de comando, exigência da API
std::vector<wchar_t> cmdBuffer(commandLine.begin(), commandLine.end());
cmdBuffer.push_back(L'\0');
// Cria o processo
BOOL success = CreateProcess(
nullptr, // Nome do aplicativo
cmdBuffer.data(), // Linha de comando
nullptr, // Atributos de segurança do processo
nullptr, // Atributos de segurança da thread
TRUE, // Herda handles (CRUCIAL)
CREATE_NO_WINDOW, // Flags de criação (oculta o console)
nullptr, // Variáveis de ambiente
nullptr, // Diretório de trabalho
&startInfo, // Informações de inicialização
&procInfo // Informações do processo
);
if (success) {
// O pai não precisa dos handles que foram passados para o filho
CloseHandle(outPipe.hWrite);
CloseHandle(inPipe.hRead);
}
return success != FALSE;
}
3. Leitura da Saída do Processo
Após o lançamento, o processo pai pode ler continuamente o pipe de saída até que o processo filho seja encerrado e o pipe seja fechado.
std::string ReadProcessOutput(HANDLE hReadPipe) {
std::string result;
DWORD bytesRead;
char buffer[4096];
// Loop de leitura até o fim do fluxo
while (ReadFile(hReadPipe, buffer, sizeof(buffer) - 1, &bytesRead, nullptr) && bytesRead > 0) {
buffer[bytesRead] = '\0';
result += buffer;
}
return result;
}
int main() {
PipeHandles outPipe = {nullptr, nullptr};
PipeHandles inPipe = {nullptr, nullptr};
PROCESS_INFORMATION procInfo;
if (!InitializePipes(outPipe, inPipe)) {
std::cerr << "Falha ao criar os pipes." << std::endl;
return 1;
}
std::wstring cmd = L"cmd.exe /c dir C:\\";
if (!LaunchHiddenProcess(cmd, outPipe, inPipe, procInfo)) {
std::cerr << "Falha ao iniciar o processo." << std::endl;
return 1;
}
// Aguarda o processo terminar para garantir que toda a saída foi escrita
WaitForSingleObject(procInfo.hProcess, INFINITE);
// Lê a saída capturada
std::string output = ReadProcessOutput(outPipe.hRead);
std::cout << "Saída capturada:\n" << output << std::endl;
// Limpeza de recursos
CloseHandle(outPipe.hRead);
CloseHandle(inPipe.hWrite);
CloseHandle(procInfo.hProcess);
CloseHandle(procInfo.hThread);
return 0;
}
Fluxo de Execução e Considerações
O ciclo de vida dessa integração segue uma ordem estrita para evitar deadlocks e vazamento de recursos:
- Criação dos Pipes: Estabelece os canais de comunicação com atributos de herança ativados.
- Configuração do STARTUPINFO: Vincula os fluxos padrão do futuro processo aos pipes.
- Invocação do CreateProcess: Inicia o utilitário externo. Imediatamente após, o processo pai deve fechar as cópias dos handles que pertencem exclusivamente ao filho (ex: o handle de escrita do pipe de saída).
- Leitura e Escrita: O pai lê o pipe de saída. Se o processo filho exigir entrada interativa, o pai escreve no pipe de entrada.
- Encerramento: O pai aguarda o fim do processo (
WaitForSingleObject) e libera todos os handles restantes.
É fundamental fechar os handles não utilizados em ambos os processos. Se o pai mantiver aberto o handle de escrita do pipe de saída, a função ReadFile nunca retornará FALSE indicando o fim do fluxo, resultando em um bloqueio infinito (deadlock) na thread de leitura.