Win32 GUI C (++) приложение перенаправляет как stdout, так и stderr в один файл на диске - PullRequest
2 голосов
/ 08 июля 2019

Я создаю службу Windows, с которой не может быть связана консоль.Поэтому я хочу перенаправить stdout и stderr в один и тот же файл.Вот что я обнаружил до сих пор:

  1. Перенаправление cout и cerr в C ++ может быть сделано путем изменения буферов , но это не влияет на CI / O, напримерputs или дескрипторы ввода / вывода Windows.
  2. Следовательно, мы можем использовать freopen для повторного открытия stdout или stderr в виде файла, подобного здесь , но мы не можем указатьодин и тот же файл дважды.
  3. Чтобы по-прежнему использовать один и тот же файл для обоих, мы можем перенаправить stderr в стандартный вывод, используя dup2 как здесь .

Пока все хорошо, и когда мы запускаем этот код с /SUBSYSTEM:CONSOLE (свойства проекта → Линкер → Система), все работает отлично:

#include <Windows.h>
#include <io.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>

void doit()
{
    FILE *stream;
    if (_wfreopen_s(&stream, L"log.log", L"w", stdout)) __debugbreak();
    // Also works as service when uncommenting this line: if (_wfreopen_s(&stream, L"log2.log", L"w", stderr)) __debugbreak();
    if (_dup2(_fileno(stdout), _fileno(stderr)))
    {
        const auto err /*EBADF if service; hover over in debugger*/ = errno;
        __debugbreak();
    }

    // Seemingly can be left out for console applications
    if (!SetStdHandle(STD_OUTPUT_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stdout))))) __debugbreak();
    if (!SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(stderr))))) __debugbreak();

    if (_setmode(_fileno(stdout), _O_WTEXT) == -1) __debugbreak();
    if (_setmode(_fileno(stderr), _O_WTEXT) == -1) __debugbreak();

    std::wcout << L"1☺a" << std::endl;
    std::wcerr << L"1☺b" << std::endl;

    _putws(L"2☺a");
    fflush(stdout);
    fputws(L"2☺b\n", stderr);
    fflush(stderr);

    const std::wstring a3(L"3☺a\n"), b3(L"3☺b\n");
    if (!WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), a3.c_str(), a3.size() * sizeof(wchar_t), nullptr, nullptr))
        __debugbreak();
    if (!WriteFile(GetStdHandle(STD_ERROR_HANDLE), b3.c_str(), b3.size() * sizeof(wchar_t), nullptr, nullptr))
        __debugbreak();
}

int        main() { doit(); }
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { return doit(), 0; }

Это приятно записывает следующий текст в log.log:

1☺a
1☺b
2☺a
2☺b
3☺a
3☺b

( Конечно мы хотим смайликов, поэтому нам нужен какой-то юникод. В этом случае мы используем широкие символы, что означает, что нам нужно использовать setmode или все остальноеиспортит.)

Но теперь вернемся к первоначальной проблеме: сделать это как сервис без консоли или, что то же самое, но простоотладка, приложение с графическим интерфейсом (/SUBSYSTEM:WINDOWS).Проблема в том, что в этом случае dup2 завершается с ошибкой , потому что fileno(stderr) не является допустимым дескриптором файла, поскольку приложение изначально не имеет связанных потоков.Как уже упоминалось здесь , fileno(stderr) == -2 в данном случае.

Обратите внимание, что когда мы впервые открываем stderr как другой файл, используя freopen, все работает нормально, но мы создали пустой файл.

Итак, теперь мой вопрос: Каков наилучший способ перенаправить как stdout, так и stderr в один и тот же файл в приложении, в котором изначально нет потоков?

ПростоНапомним: проблема в том, что , когда stdout или stderr не связаны с выходным потоком, fileno возвращает -2 , поэтому мы не можем передать его в dup2.

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

1 Ответ

1 голос
/ 08 июля 2019

Вот пример программы, которая создает файл для записи, а затем использует CreateProcess и устанавливает stdout и stderr для процесса в HANDLE созданного файла. Этот пример начинается с фиктивного аргумента, заставляющего его записывать множество вещей в stdout и stderr, которые будут записаны в output.txt.

// RedirectStd.cpp

#include <iostream>
#include <string_view>
#include <vector>

#include <Windows.h>

struct SecAttrs_t : public SECURITY_ATTRIBUTES {
    SecAttrs_t() : SECURITY_ATTRIBUTES{ 0 } {
        nLength = sizeof(SECURITY_ATTRIBUTES);
        bInheritHandle = TRUE;
    }
    operator SECURITY_ATTRIBUTES* () { return this;  }
};

struct StartupInfo_t : public STARTUPINFO {
    StartupInfo_t(HANDLE output) : STARTUPINFO{ 0 } {
        cb = sizeof(STARTUPINFO);
        dwFlags = STARTF_USESTDHANDLES;
        hStdOutput = output;
        hStdError = output;
    }
    operator STARTUPINFO* () { return this; }
};

int cppmain(const std::string_view program, std::vector<std::string_view> args) {
    if (args.size() == 0) {
        // no arguments, create a file and start a new process
        SecAttrs_t sa;

        HANDLE hFile = CreateFile(L"output.txt",
            GENERIC_WRITE,
            FILE_SHARE_READ,
            sa, // lpSecurityAttributes
            CREATE_ALWAYS, // dwCreationDisposition
            FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
            NULL // dwFlagsAndAttributesparameter
        );
        if (hFile == INVALID_HANDLE_VALUE) return 1;

        StartupInfo_t su(hFile); // set output handles to hFile
        PROCESS_INFORMATION pi;

        std::wstring commandline = L"RedirectStd.exe dummy";
        BOOL bCreated = CreateProcess(
            NULL,
            commandline.data(),
            NULL, // lpProcessAttributes
            NULL, // lpThreadAttributes
            TRUE, // bInheritHandles
            0, // dwCreationFlags
            NULL, // lpEnvironment
            NULL, // lpCurrentDirectory
            su, // lpStartupInfo
            &pi
        );
        if (bCreated == 0) return 2;
        CloseHandle(pi.hThread); // no need for this

        WaitForSingleObject(pi.hProcess, INFINITE); // wait for the process to finish         
        CloseHandle(pi.hProcess);
        CloseHandle(hFile);
    }
    else {
        // called with an argument, output stuff to stdout and stderr 
        for (int i = 0; i < 1024; ++i) {
            std::cout << "stdout\n";
            std::cerr << "stderr\n";
        }
    }

    return 0;
}

int main(int argc, char* argv[]) {
    return cppmain(argv[0], { argv + 1, argv + argc });
}
...