views:

3543

answers:

13

I have a console app in which I want to give the user x seconds to respond to the prompt. If no input is made after a certain period of time, program logic should continue. We assume a timeout means empty response.

What is the most straightforward way of approaching this?

+5  A: 

I think you will need to make a secondary thread and poll for a key on the console. I know of no built in way to accomplish this.

Geoffrey Chetwood
This man speaketh truth. You need a second thread.
Jarrett Meyer
+7  A: 

Will this approach using Console.KeyAvailable help?

class Sample 
{
    public static void Main() 
    {
    ConsoleKeyInfo cki = new ConsoleKeyInfo();

    do {
        Console.WriteLine("\nPress a key to display; press the 'x' key to quit.");

// Your code could perform some useful task in the following loop. However, 
// for the sake of this example we'll merely pause for a quarter second.

        while (Console.KeyAvailable == false)
            Thread.Sleep(250); // Loop until input is entered.
        cki = Console.ReadKey(true);
        Console.WriteLine("You pressed the '{0}' key.", cki.Key);
        } while(cki.Key != ConsoleKey.X);
    }
}
Gulzar
This is true, the OP does seem to want a blocking call, although I shudder at the thought a bit... This is probably a better solution.
Geoffrey Chetwood
I am sure you have seen this. Got it from a quick google http://social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/5f954d01-dbaf-410a-9b0d-6bb6f57d0b85/
Gulzar
I don't see how this "timesout" if the user does nothing. All this would do is possibly keep executing logic in the background until a key is pressed and other logic continues.
mphair
+4  A: 

One way or another you do need a second thread. You could use asynchronous IO to avoid declaring your own:

  • declare a ManualResetEvent, call it "evt"
  • call System.Console.OpenStandardInput to get the input stream. Specify a callback method that will store its data and set evt.
  • call that stream's BeginRead method to start an asynchronous read operation
  • then enter a timed wait on a ManualResetEvent
  • if the wait times out, then cancel the read

If the read returns data, set the event and your main thread will continue, otherwise you'll continue after the timeout.

Eric
A: 

Another cheap way to get a 2nd thread is to wrap it in a delegate.

Joel Coehoorn
A: 

EDIT: fixed the problem by having the actual work be done in a separate process and killing that process if it times out. See below for details. Whew!

Just gave this a run and it seemed to work nicely. My coworker had a version which used a Thread object, but I find the BeginInvoke() method of delegate types to be a bit more elegant.

namespace TimedReadLine
{
public static class Console
{
   private delegate string ReadLineInvoker ();

   public static string
   ReadLine (int timeout)
   {
      return ReadLine (timeout, null);
   }

   public static string
   ReadLine (int timeout, string @default)
   {
      using (var  process = new System.Diagnostics.Process
      {
         StartInfo =
         {
            FileName = "ReadLine.exe",
            RedirectStandardOutput = true,
            UseShellExecute = false
         }
      })
      {
         process.Start ();
         var  rli = new ReadLineInvoker (process.StandardOutput.ReadLine);
         var  iar = rli.BeginInvoke (null, null);

         if (!iar.AsyncWaitHandle.WaitOne (new System.TimeSpan (0, 0, timeout)))
         {
            process.Kill ();
            return @default;
         }

         return rli.EndInvoke (iar);
      }
   }
}
}

The ReadLine.exe project is a very simple one which has one class which looks like so:

namespace ReadLine
{
internal class Program
{
   private static void
   Main ()
   {
      System.Console.WriteLine (System.Console.ReadLine ());
   }
}
}
Jesse C. Slicer
Invoking a separate executable in a new process only to do a timed ReadLine() sounds like massive overkill. You are essentially solving the problem of not being able to Abort a ReadLine()-blocking thread by setting up and tearing down a whole process instead.
bzlm
Then tell it to Microsoft, who put us in this position.
Jesse C. Slicer
+3  A: 

Calling Console.ReadLine() in the delegate is bad because if the user doesn't hit 'enter' then that call will never return. The thread executing the delegate will be blocked until the user hits 'enter', with no way to cancel it.

Issuing a sequence of these calls will not behave as you would expect. Consider the following (using the example Console class from above):

System.Console.WriteLine("Enter your first name [John]:");

string firstName = Console.ReadLine(5, "John");

System.Console.WriteLine("Enter your last name [Doe]:");

string lastName = Console.ReadLine(5, "Doe");

The user lets the timeout expire for the first prompt, then enters a value for the second prompt. Both firstName and lastName will contain the default values. When the user hits 'enter', the first ReadLine call will complete, but the code has abandonded that call and essentially discarded the result. The second ReadLine call will continue to block, the timeout will eventually expire and the value returned will again be the default.

BTW- There is a bug in the code above. By calling waitHandle.Close() you close the event out from under the worker thread. If the user hits 'enter' after the timeout expires, the worker thread will attempt to signal the event which throws an ObjectDisposedException. The exception is thrown from the worker thread, and if you haven't setup an unhandled exception handler your process will terminate.

Brannon
The term "above" in your post is ambiguous and confusing. If you are referring to another answer, you should make a proper link to that answer.
bzlm
A: 

I may be reading too much into the question, but I am assuming the wait would be similar to the boot menu where it waits 15 seconds unless you press a key. You could either use (1) a blocking function or (2) you could use a thread, an event, and a timer. The event would act as a 'continue' and would block until either the timer expired or a key was pressed.

Pseudo-code for (1) would be:

// Get configurable wait time
TimeSpan waitTime = TimeSpan.FromSeconds(15.0);
int configWaitTimeSec;
if (int.TryParse(ConfigManager.AppSetting["DefaultWaitTime"], out configWaitTimeSec))
    waitTime = TimeSpan.FromSeconds(configWaitTimeSec);

bool keyPressed = false;
DateTime expireTime = DateTime.Now + waitTime;

// Timer and key processor
ConsoleKeyInfo cki;
while (keyPressed || (DateTime.Now < expireTime))
{
    if (Console.KeyAvailable)
    {
        cki = Console.ReadKey(true);
        // TODO: Process key
        keyPressed = true;
    }
    Thread.Sleep(10);
}
Ryan
A: 

Example implementation of Eric's post above. This particular example was used to read information that was passed to a console app via pipe:

 using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace PipedInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader buffer = ReadPipedInfo();

            Console.WriteLine(buffer.ReadToEnd());
        }

        #region ReadPipedInfo
        public static StreamReader ReadPipedInfo()
        {
            //call with a default value of 5 milliseconds
            return ReadPipedInfo(5);
        }

        public static StreamReader ReadPipedInfo(int waitTimeInMilliseconds)
        {
            //allocate the class we're going to callback to
            ReadPipedInfoCallback callbackClass = new ReadPipedInfoCallback();

            //to indicate read complete or timeout
            AutoResetEvent readCompleteEvent = new AutoResetEvent(false);

            //open the StdIn so that we can read against it asynchronously
            Stream stdIn = Console.OpenStandardInput();

            //allocate a one-byte buffer, we're going to read off the stream one byte at a time
            byte[] singleByteBuffer = new byte[1];

            //allocate a list of an arbitary size to store the read bytes
            List<byte> byteStorage = new List<byte>(4096);

            IAsyncResult asyncRead = null;
            int readLength = 0; //the bytes we have successfully read

            do
            {
                //perform the read and wait until it finishes, unless it's already finished
                asyncRead = stdIn.BeginRead(singleByteBuffer, 0, singleByteBuffer.Length, new AsyncCallback(callbackClass.ReadCallback), readCompleteEvent);
                if (!asyncRead.CompletedSynchronously)
                    readCompleteEvent.WaitOne(waitTimeInMilliseconds);

                //end the async call, one way or another

                //if our read succeeded we store the byte we read
                if (asyncRead.IsCompleted)
                {
                    readLength = stdIn.EndRead(asyncRead);
                    if (readLength > 0)
                        byteStorage.Add(singleByteBuffer[0]);
                }

            } while (asyncRead.IsCompleted && readLength > 0);
            //we keep reading until we fail or read nothing

            //return results, if we read zero bytes the buffer will return empty
            return new StreamReader(new MemoryStream(byteStorage.ToArray(), 0, byteStorage.Count));
        }

        private class ReadPipedInfoCallback
        {
            public void ReadCallback(IAsyncResult asyncResult)
            {
                //pull the user-defined variable and strobe the event, the read finished successfully
                AutoResetEvent readCompleteEvent = asyncResult.AsyncState as AutoResetEvent;
                readCompleteEvent.Set();
            }
        }
        #endregion ReadPipedInfo
    }
}
A: 
        string ReadLine(int timeoutms)
        {
            ReadLineDelegate d = Console.ReadLine;
            IAsyncResult result = d.BeginInvoke(null, null);
            result.AsyncWaitHandle.WaitOne(timeoutms);//timeout e.g. 15000 for 15 secs
            if (result.IsCompleted)
            {
                string resultstr = d.EndInvoke(result);
                Console.WriteLine("Read: " + resultstr);
                return resultstr;
            }
            else
            {
                Console.WriteLine("Timed out!");
                throw new TimedoutException("Timed Out!");
            }
        }

        delegate string ReadLineDelegate();
gp
+1  A: 

Simple threading example to solve this

Thread readKeyThread = new Thread(ReadKeyMethod);
static ConsoleKeyInfo cki = null;

void Main()
{
    readKeyThread.Start();
    bool keyEntered = false;
    for(int ii = 0; ii < 10; ii++)
    {
        Thread.Sleep(1000);
        if(readKeyThread.ThreadState == ThreadState.Stopped)
            keyEntered = true;
    }
    if(keyEntered)
    { //do your stuff for a key entered
    }
}

void ReadKeyMethod()
{
    cki = Console.ReadKey();
}

or a static string up top for getting an entire line.

mphair
A: 

I can't comment on Gulzar's post unfortunately, but here's a fuller example:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();
Jamie Kitson
Note you can also use Console.In.Peek() if the console is not visible(?) or the input is directed from a file.
Jamie Kitson
A: 

Im my case this work fine:

public static ManualResetEvent evtToWait = new ManualResetEvent(false);

private static void ReadDataFromConsole( object state )
{
    Console.WriteLine("Enter \"x\" to exit or wait for 5 seconds.");

    while (Console.ReadKey().KeyChar != 'x')
    {
        Console.Out.WriteLine("");
        Console.Out.WriteLine("Enter again!");
    }

    evtToWait.Set();
}

static void Main(string[] args)
{
        Thread status = new Thread(ReadDataFromConsole);
        status.Start();

        evtToWait = new ManualResetEvent(false);

        evtToWait.WaitOne(5000); // wait for evtToWait.Set() or timeOut

        status.Abort(); // exit anyway
        return;
}
Sasha
A: 
// Wait for 'Enter' to be pressed or 5 seconds to elapse
using (Stream s = Console.OpenStandardInput())
{
    ManualResetEvent stop_waiting = new ManualResetEvent(false);
    s.BeginRead(new Byte[1], 0, 1, ar => stop_waiting.Set(), null);

    // ...do anything else, or simply...

    stop_waiting.WaitOne(5000);
    // If desired, other threads could also set 'stop_waiting' 
    // Disposing the stream cancels the async read operation. It can be
    // re-opened if needed.
}
Glenn Slayden