Как добавить тайм-аут в Console.ReadLine()?

У меня есть консольное приложение, в котором я хочу дать пользователю x секунд ответить на приглашение. Если после определенного периода времени не вводится ввод, логика программы должна продолжаться. Мы предполагаем, что таймаут означает пустой ответ.

Какой самый простой способ приблизиться к этому?

Ответ 1

Я удивлен, узнав, что через 5 лет все ответы по-прежнему страдают одной или несколькими из следующих проблем:

  • Используется функция, отличная от ReadLine, что приводит к потере функциональности. (Delete/backspace/up-key для предыдущего ввода).
  • Функция работает плохо при вызове несколько раз (создавая несколько потоков, много висячих ReadLine или иначе неожиданное поведение).
  • Функция зависит от оживленного ожидания. Это ужасный отход, так как ожидание ожидается в любом месте от нескольких секунд до таймаута, что может быть несколько минут. Ожидаемое ожидание, которое запускается для такого количества времени, является ужасным источником ресурсов, что особенно плохо в сценарии многопоточности. Если ожидание-ожидание изменяется сон, это отрицательно влияет на отзывчивость, хотя я признаю, что это, вероятно, не большая проблема.

Я считаю, что мое решение решит исходную проблему, не испытывая ни одной из вышеуказанных проблем:

class Reader {
  private static Thread inputThread;
  private static AutoResetEvent getInput, gotInput;
  private static string input;

  static Reader() {
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(reader);
    inputThread.IsBackground = true;
    inputThread.Start();
  }

  private static void reader() {
    while (true) {
      getInput.WaitOne();
      input = Console.ReadLine();
      gotInput.Set();
    }
  }

  // omit the parameter to read a line without a timeout
  public static string ReadLine(int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      return input;
    else
      throw new TimeoutException("User did not provide input within the timelimit.");
  }
}

Вызов, конечно, очень прост:

try {
  Console.WriteLine("Please enter your name within the next 5 seconds.");
  string name = Reader.ReadLine(5000);
  Console.WriteLine("Hello, {0}!", name);
} catch (TimeoutException) {
  Console.WriteLine("Sorry, you waited too long.");
}

В качестве альтернативы вы можете использовать соглашение TryXX(out), как предположил shmueli:

  public static bool TryReadLine(out string line, int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      line = input;
    else
      line = null;
    return success;
  }

Это называется следующим:

Console.WriteLine("Please enter your name within the next 5 seconds.");
string name;
bool success = Reader.TryReadLine(out name, 5000);
if (!success)
  Console.WriteLine("Sorry, you waited too long.");
else
  Console.WriteLine("Hello, {0}!", name);

В обоих случаях вы не можете смешивать вызовы с Reader с обычными вызовами Console.ReadLine: если время Reader истечет, произойдет зависание ReadLine. Вместо этого, если вы хотите иметь обычный (не синхронизированный) вызов ReadLine, просто используйте Reader и опустите таймаут, чтобы он по умолчанию использовал бесконечный тайм-аут.

Итак, как насчет тех проблем других решений, о которых я говорил?

  • Как вы можете видеть, используется ReadLine, избегая первой проблемы.
  • Функция работает корректно при вызове несколько раз. Независимо от того, произойдет ли тайм-аут или нет, только один фоновый поток будет работать, и только один вызов ReadLine будет активным. Вызов функции всегда приведет к последнему входу или таймауту, и пользователю не нужно будет вводить несколько раз, чтобы отправить свой ввод.
  • И, очевидно, функция не полагается на ожидание. Вместо этого он использует правильные методы многопоточности, чтобы предотвратить трату ресурсов.

Единственная проблема, которую я предвижу с этим решением, заключается в том, что он не является потокобезопасным. Тем не менее, несколько потоков не могут действительно запрашивать пользователя для ввода в одно и то же время, поэтому синхронизация должна происходить, прежде чем делать вызов Reader.ReadLine в любом случае.

Ответ 2

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();

Ответ 3

Будет ли этот подход использовать 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);
    }
}

Ответ 4

Так или иначе вам понадобится второй поток. Вы можете использовать асинхронный ввод-вывод, чтобы не объявлять свои собственные:

  • объявить ManualResetEvent, называть его "evt"
  • вызовите System.Console.OpenStandardInput, чтобы получить входной поток. Укажите метод обратного вызова, который будет хранить свои данные и установить evt.
  • вызов этого потока BeginRead для запуска асинхронной операции чтения
  • затем введите ожидаемое время ожидания на ManualResetEvent
  • если время ожидания отключено, затем отмените чтение

Если чтение возвращает данные, установите событие, и основной поток будет продолжен, иначе вы продолжите работу после таймаута.

Ответ 5

// 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.
}

Ответ 6

Это сработало для меня.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);

Ответ 7

Думаю, вам нужно будет сделать дополнительный поток и опросить ключ на консоли. Я не знаю, как это сделать.

Ответ 8

Я боролся с этой проблемой в течение 5 месяцев, прежде чем нашел решение, прекрасно работающее в корпоративных настройках.

Проблема с большинством решений пока заключается в том, что они полагаются на нечто иное, чем Console.ReadLine() и Console.ReadLine() имеет множество преимуществ:

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

Мое решение таково:

  • Создайте отдельный поток для обработки ввода пользователя с помощью Console.ReadLine().
  • После периода ожидания разблокируйте Console.ReadLine(), отправив ключ [enter] в текущее окно консоли, используя http://inputsimulator.codeplex.com/.

Пример кода:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Дополнительная информация об этой технике, включая правильную технику, чтобы прервать поток, который использует Console.ReadLine:

.NET-вызов для отправки [enter] нажатия клавиши в текущий процесс, который представляет собой консольное приложение?

Как прервать другой поток в .NET, когда указанный поток выполняет Console.ReadLine?

Ответ 9

Вызов Console.ReadLine() в делегате плохо, потому что если пользователь не ударил 'enter', то этот вызов никогда не вернется. Нить, выполняющая делегат, будет заблокирована до тех пор, пока пользователь не нажмет 'enter', и не сможет отменить его.

Выдача последовательности этих вызовов не будет вести себя так, как вы ожидали. Рассмотрим следующее (используя пример класса Console сверху):

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");

Пользователь позволяет истекать время ожидания для первого запроса, а затем вводит значение для второго приглашения. И firstName, и lastName будут содержать значения по умолчанию. Когда пользователь нажимает 'enter', завершается вызов first ReadLine, но код отказался от этого вызова и по существу отменил результат. Вызов второй ReadLine будет продолжать блокироваться, тайм-аут в конечном итоге истекает, и возвращаемое значение снова будет по умолчанию.

BTW - В приведенном выше коде есть ошибка. Вызывая waitHandle.Close(), вы закрываете событие из-под рабочего потока. Если пользователь нажимает 'enter' после истечения таймаута, рабочий поток пытается сигнализировать о событии, которое генерирует исключение ObjectDisposedException. Исключение выбрано из рабочего потока, и если вы не настроили обработчик необработанных исключений, ваш процесс завершится.

Ответ 10

Возможно, я слишком много читаю в вопросе, но я предполагаю, что ожидание будет похоже на меню загрузки, где он ждет 15 секунд, если вы не нажмете клавишу. Вы можете использовать (1) функцию блокировки или (2) использовать поток, событие и таймер. Событие будет действовать как "продолжить" и будет блокироваться до тех пор, пока не истечет таймер или не будет нажата клавиша.

Псевдокод для (1) будет:

// 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;
// EDIT: adding a missing ! below
while (!keyPressed && (DateTime.Now < expireTime))
{
    if (Console.KeyAvailable)
    {
        cki = Console.ReadKey(true);
        // TODO: Process key
        keyPressed = true;
    }
    Thread.Sleep(10);
}

Ответ 11

Если вы используете метод Main(), вы не можете использовать await, поэтому вам нужно будет использовать Task.WaitAny():

var task = Task.Factory.StartNew(Console.ReadLine);
var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
    ? task.Result : string.Empty;

Однако, С# 7.1 вводит возможность создания метода async Main(), поэтому лучше использовать версию Task.WhenAny() всякий раз, когда у вас есть эта опция:

var task = Task.Factory.StartNew(Console.ReadLine);
var completedTask = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
var result = object.ReferenceEquals(task, completedTask) ? task.Result : string.Empty;

Ответ 12

РЕДАКТИРОВАТЬ: исправлена ​​проблема, если фактическая работа выполняется в отдельном процессе и убивает этот процесс, если он истечет. Подробнее см. Ниже. Уф!

Просто дал ему пробег, и он, похоже, работал красиво. У моего коллеги была версия, которая использовала объект Thread, но я нахожу метод BeginInvoke() типов делегатов более элегантным.

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);
         }
      }
   }
}

Проект ReadLine.exe очень простой, у которого есть один класс, который выглядит так:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}

Ответ 13

Я не могу комментировать сообщение Gulzar, к сожалению, но здесь более полный пример:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();

Ответ 14

.NET 4 делает это невероятно простым, используя Tasks.

Сначала создайте своего помощника:

   Private Function AskUser() As String
      Console.Write("Answer my question: ")
      Return Console.ReadLine()
   End Function

Во-вторых, выполните задачу и подождите:

      Dim askTask As Task(Of String) = New TaskFactory().StartNew(Function() AskUser())
      askTask.Wait(TimeSpan.FromSeconds(30))
      If Not askTask.IsCompleted Then
         Console.WriteLine("User failed to respond.")
      Else
         Console.WriteLine(String.Format("You responded, '{0}'.", askTask.Result))
      End If

Нет попытки воссоздать функциональность ReadLine или выполнять другие опасные взломы, чтобы заставить это работать. Задачи позволяют нам решить вопрос очень естественным образом.

Ответ 15

Как будто здесь не было достаточно ответов: 0), следующее инкапсулируется в статическое решение @kwl выше (первое).

    public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
    {
        Task<string> task = Task.Factory.StartNew(Console.ReadLine);

        string result = Task.WaitAny(new Task[] { task }, timeout) == 0
            ? task.Result 
            : string.Empty;
        return result;
    }

Использование

    static void Main()
    {
        Console.WriteLine("howdy");
        string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
        Console.WriteLine("bye");
    }

Ответ 16

Простой пример резьбы для решения этой проблемы

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();
}

или статическая строка вверху для получения всей строки.

Ответ 17

В моем случае эта работа прекрасна:

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;
}

Ответ 18

Разве это не красиво и коротко?

if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
    ConsoleKeyInfo keyInfo = Console.ReadKey();

    // Handle keyInfo value here...
}

Ответ 19

Это более полный пример решения Glen Slayden. Я решил сделать это, создав тестовый пример для другой проблемы. Он использует асинхронный ввод-вывод и ручное событие reset.

public static void Main() {
    bool readInProgress = false;
    System.IAsyncResult result = null;
    var stop_waiting = new System.Threading.ManualResetEvent(false);
    byte[] buffer = new byte[256];
    var s = System.Console.OpenStandardInput();
    while (true) {
        if (!readInProgress) {
            readInProgress = true;
            result = s.BeginRead(buffer, 0, buffer.Length
              , ar => stop_waiting.Set(), null);

        }
        bool signaled = true;
        if (!result.IsCompleted) {
            stop_waiting.Reset();
            signaled = stop_waiting.WaitOne(5000);
        }
        else {
            signaled = true;
        }
        if (signaled) {
            readInProgress = false;
            int numBytes = s.EndRead(result);
            string text = System.Text.Encoding.UTF8.GetString(buffer
              , 0, numBytes);
            System.Console.Out.Write(string.Format(
              "Thank you for typing: {0}", text));
        }
        else {
            System.Console.Out.WriteLine("oy, type something!");
        }
    }

Ответ 20

Мой код полностью основан на ответе друга @JSQuareD

Но мне нужно было использовать Stopwatch для таймера, потому что, когда я закончил программу с Console.ReadKey(), он все еще ждал Console.ReadLine(), и это вызвало неожиданное поведение.

Это работало идеально для меня. Поддерживает оригинальную Console.ReadLine()

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("What is the answer? (5 secs.)");
        try
        {
            var answer = ConsoleReadLine.ReadLine(5000);
            Console.WriteLine("Answer is: {0}", answer);
        }
        catch
        {
            Console.WriteLine("No answer");
        }
        Console.ReadKey();
    }
}

class ConsoleReadLine
{
    private static string inputLast;
    private static Thread inputThread = new Thread(inputThreadAction) { IsBackground = true };
    private static AutoResetEvent inputGet = new AutoResetEvent(false);
    private static AutoResetEvent inputGot = new AutoResetEvent(false);

    static ConsoleReadLine()
    {
        inputThread.Start();
    }

    private static void inputThreadAction()
    {
        while (true)
        {
            inputGet.WaitOne();
            inputLast = Console.ReadLine();
            inputGot.Set();
        }
    }

    // omit the parameter to read a line without a timeout
    public static string ReadLine(int timeout = Timeout.Infinite)
    {
        if (timeout == Timeout.Infinite)
        {
            return Console.ReadLine();
        }
        else
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            while (stopwatch.ElapsedMilliseconds < timeout && !Console.KeyAvailable) ;

            if (Console.KeyAvailable)
            {
                inputGet.Set();
                inputGot.WaitOne();
                return inputLast;
            }
            else
            {
                throw new TimeoutException("User did not provide input within the timelimit.");
            }
        }
    }
}

Ответ 21

Еще один дешевый способ получить второй поток - обернуть его в делегат.

Ответ 22

Пример реализации сообщения Эрика выше. Этот конкретный пример использовался для чтения информации, переданной в консольное приложение через канал:

 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 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
    }
}

Ответ 23

string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Обратите внимание, что если вы спуститесь по маршруту "Console.ReadKey", вы потеряете некоторые из классных функций ReadLine, а именно:

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

Чтобы добавить тайм-аут, измените цикл while, чтобы он соответствовал.

Ответ 24

Пожалуйста, не ненавидите меня за то, что добавили еще одно решение для множества существующих ответов! Это работает для Console.ReadKey(), но может быть легко изменено для работы с ReadLine() и т.д.

Поскольку методы "Console.Read" блокируются, необходимо "nudge" поток StdIn, чтобы отменить чтение.

Синтаксис вызова:

ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

код:

public class AsyncConsole // not thread safe
{
    private static readonly Lazy<AsyncConsole> Instance =
        new Lazy<AsyncConsole>();

    private bool _keyPressed;
    private ConsoleKeyInfo _keyInfo;

    private bool DoReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        _keyPressed = false;
        _keyInfo = new ConsoleKeyInfo();

        Thread readKeyThread = new Thread(ReadKeyThread);
        readKeyThread.IsBackground = false;
        readKeyThread.Start();

        Thread.Sleep(millisecondsTimeout);

        if (readKeyThread.IsAlive)
        {
            try
            {
                IntPtr stdin = GetStdHandle(StdHandle.StdIn);
                CloseHandle(stdin);
                readKeyThread.Join();
            }
            catch { }
        }

        readKeyThread = null;

        keyInfo = _keyInfo;
        return _keyPressed;
    }

    private void ReadKeyThread()
    {
        try
        {
            _keyInfo = Console.ReadKey();
            _keyPressed = true;
        }
        catch (InvalidOperationException) { }
    }

    public static bool ReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
    }

    private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(StdHandle std);

    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hdl);
}

Ответ 25

Вот решение, которое использует Console.KeyAvailable. Это блокировка вызовов, но должно быть довольно тривиально вызывать их асинхронно через TPL, если это необходимо. Я использовал стандартные механизмы отмены, чтобы упростить проводку с помощью асинхронного шаблона задачи и всего этого хорошего материала.

public static class ConsoleEx
{
  public static string ReadLine(TimeSpan timeout)
  {
    var cts = new CancellationTokenSource();
    return ReadLine(timeout, cts.Token);
  }

  public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
  {
    string line = "";
    DateTime latest = DateTime.UtcNow.Add(timeout);
    do
    {
        cancellation.ThrowIfCancellationRequested();
        if (Console.KeyAvailable)
        {
            ConsoleKeyInfo cki = Console.ReadKey();
            if (cki.Key == ConsoleKey.Enter)
            {
                return line;
            }
            else
            {
                line += cki.KeyChar;
            }
        }
        Thread.Sleep(1);
    }
    while (DateTime.UtcNow < latest);
    return null;
  }
}

Есть некоторые недостатки.

  • Вы не получаете стандартные функции навигации, которые ReadLine предоставляет (прокрутка стрелок вверх/вниз и т.д.).
  • Это вводит символы "\ 0" на вход, если специальная клавиша нажата (F1, PrtScn и т.д.). Вы можете легко отфильтровать их, изменив код.

Ответ 26

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

static void Main(string[] args)
{
    Console.WriteLine("Hit q to continue or wait 10 seconds.");

    Task task = Task.Factory.StartNew(() => loop());

    Console.WriteLine("Started waiting");
    task.Wait(10000);
    Console.WriteLine("Stopped waiting");
}

static void loop()
{
    while (true)
    {
        if ('q' == Console.ReadKey().KeyChar) break;
    }
}

Ответ 27

Я пришел к этому ответу и в итоге:

    /// <summary>
    /// Reads Line from console with timeout. 
    /// </summary>
    /// <exception cref="System.TimeoutException">If user does not enter line in the specified time.</exception>
    /// <param name="timeout">Time to wait in milliseconds. Negative value will wait forever.</param>        
    /// <returns></returns>        
    public static string ReadLine(int timeout = -1)
    {
        ConsoleKeyInfo cki = new ConsoleKeyInfo();
        StringBuilder sb = new StringBuilder();

        // if user does not want to spesify a timeout
        if (timeout < 0)
            return Console.ReadLine();

        int counter = 0;

        while (true)
        {
            while (Console.KeyAvailable == false)
            {
                counter++;
                Thread.Sleep(1);
                if (counter > timeout)
                    throw new System.TimeoutException("Line was not entered in timeout specified");
            }

            cki = Console.ReadKey(false);

            if (cki.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return sb.ToString();
            }
            else
                sb.Append(cki.KeyChar);                
        }            
    }

Ответ 28

Простой пример с использованием Console.KeyAvailable:

Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}

Ответ 29

Более современный и основанный на задачах код будет выглядеть примерно так:

public string ReadLine(int timeOutMillisecs)
{
    var inputBuilder = new StringBuilder();

    var task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            var consoleKey = Console.ReadKey(true);
            if (consoleKey.Key == ConsoleKey.Enter)
            {
                return inputBuilder.ToString();
            }

            inputBuilder.Append(consoleKey.KeyChar);
        }
    });


    var success = task.Wait(timeOutMillisecs);
    if (!success)
    {
        throw new TimeoutException("User did not provide input within the timelimit.");
    }

    return inputBuilder.ToString();
}

Ответ 30

У меня была уникальная ситуация с Windows Application (Windows Service). При интерактивной работе программы Environment.IsInteractive (VS Debugger или из cmd.exe) я использовал AttachConsole/AllocConsole для получения stdin/stdout. Чтобы процесс не заканчивался во время выполнения работы, поток пользовательского интерфейса вызывает Console.ReadKey(false). Я хотел отменить ожидание потока пользовательского интерфейса из другого потока, поэтому я придумал модификацию решения @JSquaredD.

using System;
using System.Diagnostics;

internal class PressAnyKey
{
  private static Thread inputThread;
  private static AutoResetEvent getInput;
  private static AutoResetEvent gotInput;
  private static CancellationTokenSource cancellationtoken;

  static PressAnyKey()
  {
    // Static Constructor called when WaitOne is called (technically Cancel too, but who cares)
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(ReaderThread);
    inputThread.IsBackground = true;
    inputThread.Name = "PressAnyKey";
    inputThread.Start();
  }

  private static void ReaderThread()
  {
    while (true)
    {
      // ReaderThread waits until PressAnyKey is called
      getInput.WaitOne();
      // Get here 
      // Inner loop used when a caller uses PressAnyKey
      while (!Console.KeyAvailable && !cancellationtoken.IsCancellationRequested)
      {
        Thread.Sleep(50);
      }
      // Release the thread that called PressAnyKey
      gotInput.Set();
    }
  }

  /// <summary>
  /// Signals the thread that called WaitOne should be allowed to continue
  /// </summary>
  public static void Cancel()
  {
    // Trigger the alternate ending condition to the inner loop in ReaderThread
    if(cancellationtoken== null) throw new InvalidOperationException("Must call WaitOne before Cancelling");
    cancellationtoken.Cancel();
  }

  /// <summary>
  /// Wait until a key is pressed or <see cref="Cancel"/> is called by another thread
  /// </summary>
  public static void WaitOne()
  {
    if(cancellationtoken==null || cancellationtoken.IsCancellationRequested) throw new InvalidOperationException("Must cancel a pending wait");
    cancellationtoken = new CancellationTokenSource();
    // Release the reader thread
    getInput.Set();
    // Calling thread will wait here indefiniately 
    // until a key is pressed, or Cancel is called
    gotInput.WaitOne();
  }    
}