ReadFile не возвращается при чтении stdout из дочернего процесса после его завершения - PullRequest
0 голосов
/ 29 января 2019

Я работаю над своей библиотекой, которая должна перехватывать и обрабатывать стандартный вывод (и ошибку) дочернего процесса во время его работы.Проблема возникает, когда ReadFile используется для чтения выходных данных, он не возвращается после завершения процесса (уничтожается или завершается).

Похоже, ReadFile не может обнаружитьчто другой конец канала (дескриптор записи) закрыт.Согласно документации он должен вернуть FALSE и установить последнюю ошибку на ERROR_BROKEN_PIPE:

Если используется анонимный канал и дескриптор записи был закрыт, когда ReadFile пытается прочитать с использованиемсоответствующий дескриптор чтения канала, функция возвращает FALSE, а GetLastError возвращает ERROR_BROKEN_PIPE.

Вот мой код, я удалил нерелевантные биты: (ПРИМЕЧАНИЕ. Я обновил allium_start, чтобы следоватьпредлагаемые изменения, я сохраняю оригинал для справки, пожалуйста, используйте новый код функции, чтобы найти недостатки)

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Return on failure
    if (!success) return false;
}

char *allium_read_stdout_line(struct TorInstance *instance) {
    char *buffer = instance->buffer.data;

    // Process the input
    unsigned int read_len = 0;
    while (true) {
        // Read data
        unsigned long bytes_read;
        if (ReadFile(instance->stdout_pipe, buffer, 1, &bytes_read, NULL) == false || bytes_read == 0) return NULL;

        // Check if we have reached end of line
        if (buffer[0] == '\n') break;

        // Proceed to the next character
        ++buffer; ++read_len;
    }

    // Terminate the new line with null character and return
    // Special handling for Windows, terminate at CR if present
    buffer[read_len >= 2 && buffer[-1] == '\r' ? -1 : 0] = '\0';

    return instance->buffer.data;
}

allium_start создает канал для перенаправления вывода (он использует один и тот же канал как для stdout, так и для stdout).stderr, чтобы получить объединенные потоки), а затем создает дочерний процесс.Другая функция allium_read_stdout_line отвечает за чтение вывода из канала и его возврат при обнаружении новой строки.

Проблема возникает при вызове функции ReadFile, она никогда не возвращается, если нет ничегочитать после завершения процесса, из моего понимания все дескрипторы процесса закрываются Windows, когда он завершается, поэтому похоже, что ReadFile не может обнаружить тот факт, что канал (дескриптор записи) на другом конце былзакрыто.

Как это исправить?Я искал решение, но пока не нашел ни одного, один из возможных вариантов - использовать многопоточность и поместить ReadFile в отдельный поток, чтобы он не блокировал всю программу, используя этот метод, который я могупроверять, существует ли процесс периодически, пока я жду окончания чтения ... или убить / остановить поток, если процесс завершен.

Я предпочитаю исправить проблему, а не выбирать обходной путь, ноЯ открыт для любых других решений, чтобы заставить это работать.Заранее спасибо!


Редактировать : После прочтения ответа @ RemyLebeau и комментариев @ RbMm в этом ответе становится совершенно ясно, что мое понимание того, как работает наследование дескрипторов, в корне неверно.Поэтому я включил их предложения (SetHandleInformation, чтобы отключить наследование дескриптора чтения и закрыть его после создания дочернего процесса) в мою функцию allium_start:

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    SetHandleInformation(output_pipes[0], HANDLE_FLAG_INHERIT, 0);
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Close the write end of our stdout handle
    CloseHandle(output_pipes[1]);

    // Return on failure
    if (!success) return false;
}

(приведенный ниже текст изначально был здесь до редактировать 2 )

Редактировать 2 (после принятия ответа) : Это работает!Смотрите мой последний комментарий о принятом ответе.

Ответы [ 2 ]

0 голосов
/ 30 января 2019

Для анонимных конвейеров процесс чтения и процесс записи будут иметь обработчик hRead и hWrite, каждый процесс имеет свой собственный обработчик (копирование после наследования).Так что после того, как ваш дочерний процесс завершит работу и закроет обработчик, пыльник hWrite все еще находится в родительском процессе.Мы должны обратить внимание на закрытие hRead в процессе записи, закрытие hWrite в процессе чтения.

Я могу воспроизвести эту проблему ReadFile, а если закрыть обработчик записи после установки дочерних hStdOutput и hStdError,ReadFile вернет 0 после завершения дочернего процесса.

Вот мой пример кода, Parent.cpp:

#include <windows.h> 
#include <iostream>
#include <stdio.h>

HANDLE childInRead = NULL;
HANDLE W1 = NULL;
HANDLE W2 = NULL;
HANDLE R2 = NULL;

HANDLE R1 = NULL;

#define BUFSIZE 4096

void CreateChildProcess() {
    TCHAR applicationName[] = TEXT("kids.exe");
    PROCESS_INFORMATION pi;
    STARTUPINFO si;
    BOOL success = FALSE;

    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&si, sizeof(STARTUPINFO));

    si.cb = sizeof(STARTUPINFO);
    si.hStdError = W1;
    si.hStdOutput = W1;
    si.hStdInput = R2;
    si.dwFlags |= STARTF_USESTDHANDLES;

    success = CreateProcess(NULL, applicationName, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

    if (!success) {
        printf("Error creating child process \n");
    }
    else {
        printf("Child process successfuly created \n");
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
}

int main()
{
    printf("Parent process running.... \n");

    DWORD dRead, dWritten;
    CHAR chBuf[BUFSIZE] = { 0 };
    BOOL bSuccess = FALSE;

    SECURITY_ATTRIBUTES secAttr;
    secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    secAttr.bInheritHandle = TRUE;
    secAttr.lpSecurityDescriptor = NULL;

    printf("Creating first pipe \n");

    if (!CreatePipe(&R1, &W1, &secAttr, 0)) {
        printf("\n error creating first pipe \n");
    }
    printf("Creating second pipe \n");

    if (!CreatePipe(&R2, &W2, &secAttr, 0)) {
        printf("\n error creating second pipe \n");
    }

    if (!SetHandleInformation(R1, HANDLE_FLAG_INHERIT, 0)) {
        printf("\n R1 SetHandleInformation \n");
    }
    if (!SetHandleInformation(W2, HANDLE_FLAG_INHERIT, 0)) {
        printf("\n W1 SetHandleInformation \n");
    }

    printf("\n Creating child process..... \n");

    HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
    HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
    CreateChildProcess();
    CloseHandle(W1);
    CloseHandle(R2);
    for (;;) {
        printf("Inside for loop \n");

        //1. read from stdin
        printf("read from stdin:\n");
        bSuccess = ReadFile(hStdIn, chBuf, BUFSIZE, &dRead, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }


        //2. write to Pipe2
        printf("write to Pipe2...\n");
        bSuccess = WriteFile(W2, chBuf, 100, &dWritten, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }

        //3. read from Pipe1
        printf("read from Pipe1...\n");
        bSuccess = ReadFile(R1, chBuf, BUFSIZE, &dRead, NULL);
        if (!bSuccess)
        {
            printf("error reading :%d \n", GetLastError());
            break;
        }

        //4. write to stdout
        printf("write to stdout:\n");
        bSuccess = WriteFile(hStdOut, chBuf, 100, &dWritten, NULL);
        if (!bSuccess) {
            printf("error reading \n");
            break;
        }
    }
    getchar();
    return 0;
}

Kids.cpp:

#include <windows.h>
#include <stdio.h>

#define BUFSIZE 4096

int main()
{
    DWORD dRead, dWritten;
    CHAR chBuf[BUFSIZE];
    BOOL success = FALSE;
    HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);

    printf("Child process running....");

    if (stdIn == INVALID_HANDLE_VALUE || stdOut == INVALID_HANDLE_VALUE) {
        ExitProcess(1);
    }

    //for (;;) {
        success = ReadFile(stdIn, chBuf, BUFSIZE, &dRead, NULL);
        //if (!success || dRead == 0) break;

        success = WriteFile(stdOut, chBuf, dRead, &dWritten, NULL);
        //if (!success) break;
    //}
    return 0;
}
0 голосов
/ 29 января 2019

Вы неправильно управляете своими трубами или, точнее, не контролируете наследование дескрипторов своих труб.НЕ ДОПУСКАЙТЕ, чтобы дочерний процесс наследовал дескриптор чтения вашего канала (output_pipes[0]), в противном случае канал не будет корректно разрываться после завершения дочернего процесса.

Прочтите MSDN для получения более подробной информации:

Создание дочернего процесса с перенаправленным вводом и выводом

Случай перенаправленных стандартных дескрипторов, которые не закрываются, даже если дочерний процесс завершился

Используйте SetHandleInformation() или PROC_THREAD_ATTRIBUTE_LIST, чтобы CreateProcess() не передавал дочерний процесс output_pipes[0] в качестве наследуемого дескриптора.Дочерний процесс не нуждается в доступе к этому дескриптору, поэтому в любом случае нет необходимости передавать его через границу процесса.Ему нужен только доступ к ручке записи вашей трубы (output_pipes[1]).

...