Andy
Andy

Reputation: 11472

Win32 - read from stdin with timeout

I'm trying to do something which I think should be simple: do a blocking read from standard input, but timing out after a specified interval if no data is available.

In the Unix world this would be simple with select() but that doesn't work in Windows because stdin isn't a socket. What's the next simplest option without creating extra threads etc?

I'm using visual C++ targeting a Win32 environment.

so far I have tried:

  1. using select (doesn't work if the input is not a socket)

  2. using WaitForSingleObject(GetStdHandle(STD_INPUT_HANDLE)). - Remy's first suggestion. This always seems to return immediately when you call it if the standard input is a console (others have reported the same problem)

  3. using overlapped IO and doing a WaitForSingleObject (Remy's third suggestion). In this case the read always seems to block when the input is coming from a console - it seems that stdin does not support asynchronous I/O.

At the moment I'm thinking my only remaining option is to create a thread which will do a blocking read and then signal an event, and then have another thread which waits for the event with a timeout.

Upvotes: 19

Views: 17814

Answers (8)

Eric
Eric

Reputation: 409

When you are sure stdin is a console, and you want Windows to interpret the raw console events for you (i.e. so you can read it as text line-by-line), you can open it as a file in overlapped (async) mode using the special filename CONIN$, then use ReadFile + WaitForSingleObject + GetOverlappedResult:

BYTE inputBuffer[1024];
const DWORD timeoutMs = 1000;

HANDLE stdinHandle = CreateFile(
    _T("CONIN$"),
    GENERIC_READ,
    FILE_SHARE_WRITE,
    NULL,
    OPEN_EXISTING,
    FILE_FLAG_OVERLAPPED,
    NULL
);
if (stdinHandle == INVALID_HANDLE_VALUE) // TODO handle

HANDLE dataReadyEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (dataReadyEvent == INVALID_HANDLE_VALUE) // TODO handle

OVERLAPPED asyncIoObj;
ZeroMemory(&asyncIoObj, sizeof(asyncIoObj));
asyncIoObj.Event = dataReadyEvent;

if (!ReadFile(
    stdinHandle,
    inputBuffer,
    sizeof(inputBuffer),
    NULL,
    &asyncIoObj
)) {
    DWORD lastError = GetLastError();
    if (lastError != ERROR_IO_PENDING) // TODO handle

    DWORD waitResult = WaitForSingleObject(
        dataReadyEvent,
        timeoutMs
    );

    if (waitResult == WAIT_FAILED) // TODO handle
} // (otherwise, data was immediately available)

if (!CancelIo(stdinHandle)) // TODO handle

DWORD numBytesTransferred;

if (!GetOverlappedResult(
    stdinHandle,
    &asyncIoObj,
    &numBytesTransferred
)) {
    DWORD lastError = GetLastError();
    if (lastError != ERROR_OPERATION_ABORTED) // TODO handle
    numBytesTransferred = 0;
}

CloseHandle(dataReadyEvent);
CloseHandle(stdinHandle);

// numBytesTransferred holds number of bytes from stdin in inputBuffer

This may not behave as expected if stdin has been redirected. From Microsoft's "Console Handles" doc:

The CreateFile function enables a process to get a handle to its console's input buffer and active screen buffer, even if STDIN and STDOUT have been redirected. To open a handle to a console's input buffer, specify the CONIN$ value in a call to CreateFile.

Upvotes: 0

Remy Lebeau
Remy Lebeau

Reputation: 596196

Use GetStdHandle() to get the stdin handle. You can then either:

  1. use WaitForSingleObject() on the stdin handle itself to detect when there is console input available for reading, then read from it as needed.

  2. use GetNumberOfConsoleInputEvents() or PeekConsoleInput() on the stdin handle in a loop to determine when there is data available for reading, then read from it as needed.

In any case, be careful if stdin has been redirected. If it is redirected to something that is not console I/O, you can't use a GetStdHandle() handle with console functions. If it is redirected to a file, you have to use ReadFile() instead.

Upvotes: 1

bruceceng
bruceceng

Reputation: 2182

The existing answers don't address the case where the standard input is an anonymous pipe rather than a console. In this case functions like GetNumberOfConsoleInputEvents() will return 0x6 (bad handle) and the methods described above will not work.

In my case I am trying to use stdin and stdout to facilitate asynchronous interprocess communication, so the parent process (nodejs) opens stdin and stdout as anonymous pipes with a child process (c++). For this case, the type of stdin can be detected as follows:

 HANDLE stdinput = GetStdHandle(STD_INPUT_HANDLE);
 if (stdinput == INVALID_HANDLE_VALUE) {
    DWORD problem1 = GetLastError();
    cout << "Failed to get input handle. " << (void*) problem1 << endl;
    return(1);
 }
 DWORD fileType = GetFileType(stdinput);
 if (fileType != FILE_TYPE_PIPE) {
    cout << "Input type is not pipe. Instead: " << (void*) fileType << endl;
    return(2);
 }

Then, as to enable asynchronous reads from this input pipe, I came up with two ideas:

Method 1: Continuously poll in a loop for available input

do {
  DWORD bytesAvailable = 0;
  BOOL success = PeekNamedPipe(stdinput, NULL, NULL, NULL, &bytesAvailable, NULL );

  if (!success) {
    cout << "Couldn't run PeekNamedPipe." << endl;
    DWORD problem = GetLastError();
    cout << "Error code: " << (void*)problem << endl;
  }

  char buf[bytesAvailable+1]; //mingw allows dynamic stack allocation. In Visual studio might need to allocate on heap.

  if (bytesAvailable > 0) {
    ReadFile(stdinput, buf, additionalBytesAvailable, NULL, NULL);
    cout << "Received: " << buf << endl;
  }

  Sleep(10); //Small delay between checking for new input
} while(1);

Method 1 suffers from the issue that input can't be processed any faster than the small delay. Of course the delay could be shortened, but then the thread will consume more CPU resources. For this reason I came up with an alternate method.

Method 2: Block using ReadFile and send to different thread for processing. In the input processing thread, which will block when waiting for input:

do {
  char firstChar[2]; //we will read the first byte not sure if it is null terminated...

  //block until at least one byte is available
  ReadFile(stdinput, firstChar, 1, NULL, NULL);
  DWORD additionalBytesAvailable = 0;
  BOOL success = PeekNamedPipe(stdinput, NULL, NULL, NULL, &additionalBytesAvailable, NULL );
  if (!success) {
    cout << "Couldn't run PeekNamedPipe." << endl;
    DWORD problem = GetLastError();
    cout << "Error code: " << (void*)problem << endl;
  }
  char buf[additionalBytesAvailable+2]; //mingw allows stack allocation.

  buf[0] = firstChar[0];
  buf[1] = '\0';

  if (additionalBytesAvailable > 0) {    
    ReadFile(stdinput, buf+1, additionalBytesAvailable, NULL, NULL);
  }

  std::cout << count << " Read: " << buf << endl;

  //write some data to a different thread that is still responsive
  pthread_mutex_lock(&responsiveThreadLock);
  mutexProtectedString = std::string(buf);
  pthread_mutex_unlock(&responsiveThreadLock);
  PostThreadMessage(handleOfResponsiveThread, WM_NEWINPUT, NULL, NULL);
} while(1);

And in the thread that stays responsive:

MSG msg;
do {
  GetMessageWithTimeout(&msg, 1000);
  if (msg.message == WM_NEWINPUT) {
     std::string receivedStringCopy = "";
     pthread_mutex_lock(&responsiveThreadLock);
     receivedStringCopy  = mutexProtectedString;
     pthread_mutex_unlock(&responsiveThreadLock);
     std::cout << "Received: " << receivedStringCopy << endl;
  }
  TranslateMessage(&msg);
  DispatchMessage(&msg);
  std::cout << "Still responsive. " << endl;
} while(1);

GetMessageWithTimeout is a function designed to stay responsive (after a timeout) while waiting for a message:

//Wait upto timeoutMs milliseconds for a message.
//Return TRUE if a message is received or FALSE if the timeout occurs or there is an error.
BOOL GetMessageWithTimeout(MSG *msg, UINT timeoutMs)
{
    //Check the message queue and return immediately if there is a message available
    BOOL hasMessage = PeekMessage(msg, NULL, 0, 0, PM_REMOVE);
    if (hasMessage) {
        return(TRUE);
    }
    else {
        //Any new messages that have arrived since we last checked the message queue will
        //cause MsgWaitForMultipleObjects to return immediately.
        //otherwise this will block the thread until a message arrives or timeout occurs
        DWORD res1 = MsgWaitForMultipleObjects(0, NULL, FALSE, timeoutMs, QS_ALLINPUT);
        if (res1 == WAIT_TIMEOUT) {
            printf("!");
            return(FALSE);
        }
        if (res1 == WAIT_OBJECT_0) {
            //If we are here, there *should* be a message available. We can just get it with PeekMessage
            hasMessage = PeekMessage(msg, NULL, 0, 0, PM_REMOVE);
            return(hasMessage);
        }
        //If we get here, its because we have a WAIT_FAILED. Don't know why this would occur, but if it
        //does, lets pause for a bit, so we don't end up in a tight loop
        Sleep(100);
        return(FALSE);
    }
}

This second method will respond immediately to new inputs. For additional robustness, it may be necessary to check and make sure that the contents of the pipe ends with a \n (so incomplete messages aren't sent) and to push messages to a vector so that multiple messages don't override one another if the receiving thread can't process them fast enough.

Upvotes: 2

Pat
Pat

Reputation: 1766

You'll need the GetStdHandle function to obtain a handle to the console, then you can use WaitForSingleObject to wait for an event to happen on that handle, with a timeout.

Upvotes: 0

J&#246;rgen
J&#246;rgen

Reputation: 316

Using GetStdHandle + WaitForSingleObject works fine. But be sure to set the approriate flags and flush the console buffer as well before entering the loop.

In short (without error checks)

std::string inStr;
DWORD fdwMode, fdwOldMode;
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdIn, &fdwOldMode);
// disable mouse and window input
fdwMode = fdwOldMode ^ ENABLE_MOUSE_INPUT ^ ENABLE_WINDOW_INPUT;
SetConsoleMode(hStdIn, fdwMode);
// flush to remove existing events
FlushConsoleInputBuffer(hStdIn);
while (!abort)
{
    if (WaitForSingleObject(hStdIn, 100) == WAIT_OBJECT_0)
    {
         std::getline(std::cin, inStr);
    }
}
// restore console mode when exit
SetConsoleMode(hStdIn, fdwOldMode);

Upvotes: 5

dejv25
dejv25

Reputation: 21

In case anyone is writing chrome native messaging host and is looking for solution to check if there is any input on stdin without blocking then this works perfect:

HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
int timer = GetTickCount();
while(timer+10000 > GetTickCount())
{
    unsigned int length = 0;
    DWORD bytesAvailable = 0; 
    PeekNamedPipe(hStdin,NULL,0,NULL,&bytesAvailable,NULL);
    if(bytesAvailable > 0)
    {
        for (int i = 0; i < 4; i++)
        {
            unsigned int read_char = getchar();
            length = length | (read_char << i*8);
        }


        for (int i = 0; i < length; i++)
        {
            msg += getchar();
        }
        timer = GetTickCount();
    }
    else
    {
        // nothing to read, stdin empty
        Sleep(10);
    }
}

Upvotes: 2

Clay
Clay

Reputation: 1157

I had to solve a similar problem. On Windows it is not as easy or obvious as Linux. It is, however, possible. The trick is that Windows places console events in the console input event queue. You've got to filter out the events you don't care about and only process those events you do care about (like key presses).

For further reading: see the Win32 console documentation

Here is some mostly-debugged sample code based on a socket and stdin multiplexer I was working on:

void ProcessStdin(void)
{
    INPUT_RECORD record;
    DWORD numRead;
    if(!ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &record, 1, &numRead)) {
        // hmm handle this error somehow...
        return;
    }

    if(record.EventType != KEY_EVENT) {
        // don't care about other console events
        return;
    }

    if(!record.Event.KeyEvent.bKeyDown) {
        // really only care about keydown
        return;
    }

    // if you're setup for ASCII, process this:
    //record.Event.KeyEvent.uChar.AsciiChar

} // end ProcessStdin

int main(char argc, char* argv[])
{
    HANDLE eventHandles[] = {
        GetStdHandle(STD_INPUT_HANDLE)
        // ... add more handles and/or sockets here
        };

    DWORD result = WSAWaitForMultipleEvents(sizeof(eventHandles)/sizeof(eventHandle[0]), 
        &eventHandles[0], 
        FALSE, 
        1000, 
        TRUE
        );

    switch(result) {
        case WSA_WAIT_TIMEOUT: // no I/O going on right now
            break;

        case WSA_WAIT_EVENT_0 + 0: // stdin at array index 0
            ProcessStdin();
            break;

        case WSA_WAIT_EVENT_0 + 1: // handle/socket at array index 1
            break;

        case WSA_WAIT_EVENT_0 + 2: // ... and so on
            break;

        default: // handle the other possible conditions
            break;
    } // end switch result
}

Upvotes: 6

flux
flux

Reputation: 199

This should do it:

int main()
{
    static HANDLE stdinHandle;
    // Get the IO handles
    // getc(stdin);
    stdinHandle = GetStdHandle(STD_INPUT_HANDLE);

    while( 1 )
    {
        switch( WaitForSingleObject( stdinHandle, 1000 ) )
        {
        case( WAIT_TIMEOUT ):
            cerr << "timeout" << endl;
            break; // return from this function to allow thread to terminate
        case( WAIT_OBJECT_0 ):
            if( _kbhit() ) // _kbhit() always returns immediately
            {
                int i = _getch();
                cerr << "key: " << i << endl;
            }
            else // some sort of other events , we need to clear it from the queue
            {
                // clear events
                INPUT_RECORD r[512];
                DWORD read;
                ReadConsoleInput( stdinHandle, r, 512, &read );
                cerr << "mouse event" << endl;
            }
            break;
        case( WAIT_FAILED ):
            cerr << "WAIT_FAILED" << endl;
            break;
        case( WAIT_ABANDONED ): 
            cerr << "WAIT_ABANDONED" << endl;
            break;
        default:
            cerr << "Someting's unexpected was returned.";
        }
    }

    return 0;
}

Upvotes: 3

Related Questions