-2

I try to make a GUI application that can tolerate piped data as input, if present.

The use case is either it gets started from file explorer, and it may have a command line argument or not. Or, it is started from a batch file (or a console) in this manner:

echo somedata | myprogram.exe or cat file | myprogram.exe or myprogram.exe < file
...whichever method that is supposed to make a stream of data available into stdin.

or maybe

sourceofstream | myprogram.exe -

Using dash as filename as a form of "politeness" to the program to indicate stdin is the source, but not necessary if std::cin.rdbuf()->in_avail() can work.

My experience:

  • std::cin.rdbuf()->in_avail() doesn't work, it's always false.
  • getline(std::cin, l) or std::istream_iterator<char>(std::cin) always skips with no action.

It seems that cin/stdin is always unavailable in /subsystem:windows executables.

The only thing I managed to make work, was to call:

AttachConsole(-1);
freopen("CONIN$", "r", stdin);

After that, the C style functions like fread(...stdin) are able to access the TTY input (also isatty returns non 0). But it's not what I'm looking for, it forgoes of the input pipe, I don't want keyboard input. (Secondary remark: C++ streams don't reconnect. std::cin remains broken)

I want my input pipe (that are supposed to be set ready by the likes of CreateProcess or fork() within cmd.exe I guess?).

Full source:

#include <iostream>
#include <fstream>
#include <iterator>
#include <vector>
#include <filesystem>

int main(int argc, char* argv[])
{
    namespace fs = std::filesystem;
    using std::vector;

    fs::path inpath;
    std::ifstream infile;
    std::istream* in = nullptr;

    // temporary true hack to force cin is picked as source while no solution found
    if (true) // (std::cin.rdbuf()->in_avail() || (argc == 2 && argv[1][0] == '-'))  // input is piped on stdin?
        in = &std::cin;
    else if (argc <= 1)  // no command line -> open file dialog
        ;//filebox picker; // etc...
    else if (argc >= 2)
        inpath = argv[1];

    if (!inpath.empty())
        infile.open(inpath, std::ifstream::in | std::ifstream::binary);

    if (infile.good())
        in = &infile;

    if (!in) return 0;

    // repeat input data to stdout:
    *in >> std::noskipws;
    std::istream_iterator<char> stream_it(*in), end;
    std::vector<char> raw(stream_it, end);

    for (auto i : raw) std::cout << i;

    // gui stuff and treatment

    return 0;
}

#ifdef _WIN32
#include <io.h>
extern "C" { __declspec(dllimport) int __stdcall AttachConsole(unsigned long dwProcessId); }
struct HINSTANCE { int _; };
int WinMain(HINSTANCE, HINSTANCE, char*, int)
{
    //AttachConsole(-1);
    //freopen("CONIN$", "r", stdin);
    //char buffer[16] = {0};
    //fread(buffer, sizeof(char), 16, stdin);
    //std::cout << buffer;  // this makes input from console keyboard work, but not from pipe
    //std::cout << "is a TTY: " << std::boolalpha << !!isatty(_fileno(stdin)) << "\n";   // true if attach+reopen hack actived
    return main(__argc, __argv);
}
#endif

The CMake has:

add_executable(MyApp WIN32 ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)

The WIN32 cmake argument creates a build command line for the linker that looks like this:

C:\PROGRA~2\MICROS~3\2019\PROFES~1\VC\Tools\MSVC\1429~1.301\bin\Hostx64\x64\link.exe /nologo CMakeFiles\MyApp.dir\main.cpp.obj /out:MyApp.exe /implib:MyApp.lib /pdb:MyApp.pdb /version:0.0 /machine:x64 /debug /INCREMENTAL /subsystem:windows kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTFILE:CMakeFiles\MyApp.dir/intermediate.manifest CMakeFiles\MyApp.dir/manifest.res"

v.oddou
  • 6,476
  • 3
  • 32
  • 63
  • 1
    `stdin` is part of a console, which doesn't exist if you set the subsystem to `Windows` instead of `Console`. You can use `AttachConsole` to attach to an existing console, or you can use `AllocConsole` to create a new one (but this still won't create `std::cin`, `std::cout` and `std::cerr`, or attach them to the console's streams). Or you can use `/subsystem:console` to have that all happen automatically. Even a console subsystem program can create and use windows, just like a windows subsystem program can. – Jerry Coffin May 11 '23 at 14:43
  • @JerryCoffin it's not a matter of console but a matter of pipes. The parent process (which can happen to be a console, even for a GUI app, or it can be a batch shell, or a python script...) prepares a pipe which gets inherited by the child process and accessible through `stdin` handle (normally). It's just that I suspect windows CRT pre-main breaks normal posix behavior in incredible fashion in subsystem:windows case. I just found that `ReadFile(GetStdHandle(STD_INPUT_HANDLE),...)` actually works for my use case. `AttachConsole` is detrimental because it breaks the pipe. – v.oddou May 11 '23 at 15:19

1 Answers1

0

Self answering to publicize the result of my investigation.

This works:

FILE* fixed_stdin_file = stdin;

int main(int argc, char* argv[])
{
    char data[128];
    fread(data, 1, 128, fixed_stdin_file);  // give up stdin
    // and give up about C++ streams
}

// /subsystem:windows adaptation:
#ifdef _WIN32
#include <windows.h>
#include <fcntl.h>
#include <io.h>
int WinMain(HINSTANCE, HINSTANCE, char*, int)
{
    // DO NOT call AttachConsole here, stdin_hdl will become
    // attached to TTY input and lose connection to the input pipe.

    HANDLE stdin_hdl = GetStdHandle(STD_INPUT_HANDLE);
    if (stdin_hdl != INVALID_HANDLE_VALUE)
    {
        int inpipe_handleconv_fd = _open_osfhandle((intptr_t)stdin_hdl , _O_RDONLY | _O_BINARY);
        fixed_stdin_file = _fdopen(inpipe_handleconv_fd, "rb");
    }
    return main(__argc, __argv);
}
#endif

I will however not adopt this method, because of the impossibility to reconnect std::cin to the new valid FILE*.

Posterior to _fdopen call, to reset the stdin pointer so it can be used transparently after;
what I tried that doesn't work:

freopen(("/dev/fd/" + tostring(inpipe_handleconv_fd).c_str(), "r", stdin);

Because it's windows and /dev/fd doesn't exist. I haven't found a \\.\fd equivalence.

_dup2(inpipe_handleconv_fd, fileno(stdin));

the dup2 call fails with -1 and errno to 9 (EBADF invalid file descriptor). It's inexplicable why. Both descriptors are valid, I verified with _get_osfhandl that they are.
github has many records of people using the GetStdHandle/open_osfhandle/_dup2 method but it's after creating a new console for themselves, maybe that's the difference. I don't know.

What cannot work, but can be found in some MSDN answers:

stdin = fixed_stdin_file;

That's just illegal, and fails to build since stdin is not an lvalue.

What works, but is illegal, and can be found on github and MSDN answers:

*stdin = *fixed_stdin_file

That indeed executes and allows fread(..., stdin); as normal, but by chance. FILE is opaque and shouldn't be assigned directly.

And finally, even if stdin could be successfully repurposed thanks to a miracle in _dup2, even then, std::cin becomes moot because it cannot be rebound to its underlying FILE*. And no, ios_base::sync_with_stdio will not help you.

As this answers states https://stackoverflow.com/a/14990896/893406 the only way to use C++ streams with a redirected FILE* would be to have the facility to make a new streambuf from FILE* which doesn't exist. (appart from the implementation provided by GroovX library http://ilab.usc.edu/rjpeters/groovx/stdiobuf_8cc-source.html)

All in all, I give up. Microsoft has made it impossible to work with clean elegant standard pipe concepts when making an ELF with subsystem:windows. I will just tolerate the useless console window poping up in parallel to my GUI, just like blender does. The final boss fix would be to use a dual executable, one .com and one .exe like visual studio does, and mentioned in this MSDN blog.

v.oddou
  • 6,476
  • 3
  • 32
  • 63