Pergunta

Tenho um aplicativo de console no qual quero fornecer ao usuário x segundos para responder ao prompt.Se nenhuma entrada for feita após um determinado período de tempo, a lógica do programa deverá continuar.Presumimos que um tempo limite significa uma resposta vazia.

Qual é a maneira mais direta de abordar isso?

Foi útil?

Solução

Estou surpreso ao saber que depois de 5 anos, todas as respostas ainda sofrem de um ou mais dos seguintes problemas:

  • Uma função diferente de ReadLine é usada, causando perda de funcionalidade.(Excluir/backspace/tecla para cima para entrada anterior).
  • A função se comporta mal quando invocada várias vezes (gerando vários threads, muitos ReadLines suspensos ou comportamento inesperado).
  • A função depende de uma espera ocupada.O que é um desperdício horrível, já que espera-se que a espera decorra de alguns segundos até o tempo limite, que pode levar vários minutos.Uma espera ocupada que dura tanto tempo é uma terrível perda de recursos, o que é especialmente ruim em um cenário multithreading.Se a espera ocupada for modificada com um sono, isso terá um efeito negativo na capacidade de resposta, embora eu admita que isso provavelmente não seja um grande problema.

Acredito que minha solução resolverá o problema original sem sofrer nenhum dos problemas acima:

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

Ligar é, obviamente, muito fácil:

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

Alternativamente, você pode usar o TryXX(out) convenção, como sugeriu 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;
  }

Que é chamado da seguinte forma:

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

Em ambos os casos, você não pode misturar chamadas para Reader com normal Console.ReadLine chamadas:se o Reader expirar, haverá um enforcamento ReadLine chamar.Em vez disso, se você quiser ter um normal (não cronometrado) ReadLine ligue, basta usar o Reader e omita o tempo limite, para que o padrão seja um tempo limite infinito.

Então, e os problemas das outras soluções que mencionei?

  • Como você pode ver, ReadLine é utilizado, evitando o primeiro problema.
  • A função se comporta corretamente quando invocada várias vezes.Independentemente de ocorrer ou não um tempo limite, apenas um thread em segundo plano estará em execução e apenas no máximo uma chamada para ReadLine estará ativa.Chamar a função sempre resultará na entrada mais recente ou em um tempo limite, e o usuário não precisará pressionar Enter mais de uma vez para enviar sua entrada.
  • E, obviamente, a função não depende de espera ocupada.Em vez disso, utiliza técnicas multithreading adequadas para evitar o desperdício de recursos.

O único problema que prevejo com esta solução é que ela não é segura para threads.No entanto, vários threads não podem realmente solicitar informações ao usuário ao mesmo tempo, portanto, a sincronização deve acontecer antes de fazer uma chamada para Reader.ReadLine de qualquer forma.

Outras dicas

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

Será que esta abordagem usando Console.KeyDisponível ajuda?

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

De uma forma ou de outra, você precisa de um segundo tópico.Você poderia usar IO assíncrono para evitar declarar o seu próprio:

  • declare um ManualResetEvent, chame-o de "evt"
  • chame System.Console.OpenStandardInput para obter o fluxo de entrada.Especifique um método de retorno de chamada que armazenará seus dados e definirá evt.
  • chame o método BeginRead desse fluxo para iniciar uma operação de leitura assíncrona
  • em seguida, insira uma espera cronometrada em um ManualResetEvent
  • se a espera expirar, cancele a leitura

Se a leitura retornar dados, defina o evento e seu thread principal continuará, caso contrário, você continuará após o tempo limite.

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

Isso funcionou para mim.

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

Acho que você precisará criar um thread secundário e pesquisar uma chave no console.Não conheço nenhuma maneira integrada de conseguir isso.

Lutei com esse problema por 5 meses antes de encontrar uma solução que funcionasse perfeitamente em um ambiente empresarial.

O problema com a maioria das soluções até agora é que elas dependem de algo diferente de Console.ReadLine(), e Console.ReadLine() tem muitas vantagens:

  • Suporte para exclusão, backspace, teclas de seta, etc.
  • A capacidade de pressionar a tecla "para cima" e repetir o último comando (isso é muito útil se você implementar um console de depuração em segundo plano que seja muito útil).

Minha solução é a seguinte:

  1. Gerar um tópico separado para lidar com a entrada do usuário usando Console.ReadLine().
  2. Após o período de tempo limite, desbloqueie Console.ReadLine() enviando uma tecla [enter] para a janela do console atual, usando http://inputsimulator.codeplex.com/.

Código de amostra:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Mais informações sobre esta técnica, incluindo a técnica correta para abortar um thread que usa Console.ReadLine:

Chamada .NET para enviar o pressionamento de tecla [enter] no processo atual, que é um aplicativo de console?

Como abortar outro thread no .NET, quando esse thread está executando Console.ReadLine?

Chamar Console.ReadLine() no delegado é ruim porque se o usuário não pressionar 'enter', a chamada nunca retornará.O thread que executa o delegado será bloqueado até que o usuário pressione 'enter', sem possibilidade de cancelá-lo.

A emissão de uma sequência dessas chamadas não se comportará como esperado.Considere o seguinte (usando o exemplo da classe Console acima):

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

O usuário deixa o tempo limite expirar para o primeiro prompt e, em seguida, insere um valor para o segundo prompt.Tanto firstName quanto lastName conterão os valores padrão.Quando o usuário pressiona 'enter', o primeiro A chamada ReadLine será concluída, mas o código abandonou essa chamada e essencialmente descartou o resultado.O segundo A chamada ReadLine continuará bloqueada, o tempo limite eventualmente expirará e o valor retornado será novamente o padrão.

BTW- Há um bug no código acima.Ao chamar waitHandle.Close() você fecha o evento no thread de trabalho.Se o usuário pressionar 'enter' após o tempo limite expirar, o thread de trabalho tentará sinalizar o evento que lança uma ObjectDisposedException.A exceção é lançada a partir do thread de trabalho e, se você não configurou um manipulador de exceções não tratadas, seu processo será encerrado.

Posso estar lendo muito sobre a questão, mas presumo que a espera seria semelhante ao menu de inicialização, onde espera 15 segundos, a menos que você pressione uma tecla.Você pode usar (1) uma função de bloqueio ou (2) um thread, um evento e um cronômetro.O evento funcionaria como um 'continuar' e seria bloqueado até que o cronômetro expirasse ou uma tecla fosse pressionada.

O pseudocódigo para (1) seria:

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

Se você estiver no Main() método, você não pode usar await, então você terá que usar Task.WaitAny():

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

No entanto, o C# 7.1 introduz a possibilidade de criar um assíncrono Main() método, então é melhor usar o Task.WhenAny() versão sempre que você tiver essa opção:

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;

Infelizmente, não posso comentar a postagem de Gulzar, mas aqui está um exemplo mais completo:

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

EDITAR:resolveu o problema fazendo com que o trabalho real fosse feito em um processo separado e eliminando esse processo se ele expirasse.Veja abaixo para obter detalhes.Uau!

Apenas experimentei e pareceu funcionar bem.Meu colega de trabalho tinha uma versão que usava um objeto Thread, mas acho o método BeginInvoke() de tipos delegados um pouco mais elegante.

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

O projeto ReadLine.exe é muito simples e possui uma classe semelhante a esta:

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

O .NET 4 torna isso incrivelmente simples usando Tarefas.

Primeiro, construa seu ajudante:

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

Segundo, execute uma tarefa e espere:

      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

Não há como tentar recriar a funcionalidade ReadLine ou realizar outros hacks perigosos para que isso funcione.As tarefas permitem-nos resolver a questão de uma forma muito natural.

Como se já não houvesse respostas suficientes aqui: 0), o seguinte encapsula em um método estático a solução do @kwl acima (a primeira).

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

Uso

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

Exemplo simples de threading para resolver isso

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

ou uma string estática no topo para obter uma linha inteira.

No meu caso, isso funciona bem:

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

Isso não é bom e curto?

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

    // Handle keyInfo value here...
}

Este é um exemplo mais completo da solução de Glen Slayden.Acontece que fiz isso ao construir um caso de teste para outro problema.Ele usa E/S assíncrona e um evento de reinicialização manual.

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

Outra maneira barata de obter um segundo thread é envolvê-lo em um delegado.

Exemplo de implementação da postagem de Eric acima.Este exemplo específico foi usado para ler informações que foram passadas para um aplicativo de console 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
    }
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Observe que se você seguir o caminho "Console.ReadKey", perderá alguns dos recursos interessantes do ReadLine, a saber:

  • Suporte para exclusão, backspace, teclas de seta, etc.
  • A capacidade de pressionar a tecla "para cima" e repetir o último comando (isso é muito útil se você implementar um console de depuração em segundo plano que seja muito útil).

Para adicionar um tempo limite, altere o loop while para se adequar.

Por favor, não me odeie por adicionar outra solução à infinidade de respostas existentes!Isso funciona para Console.ReadKey(), mas pode ser facilmente modificado para funcionar com ReadLine(), etc.

Como os métodos "Console.Read" estão bloqueando, é necessário "cutucar"o fluxo StdIn para cancelar a leitura.

Sintaxe de chamada:

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

Código:

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

Aqui está uma solução que usa Console.KeyAvailable.Estas são chamadas de bloqueio, mas deve ser bastante trivial chamá-las de forma assíncrona por meio do TPL, se desejado.Usei os mecanismos de cancelamento padrão para facilitar a conexão com o Padrão Assíncrono de Tarefas e todas essas coisas boas.

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

Existem algumas desvantagens nisso.

  • Você não obtém os recursos de navegação padrão que ReadLine fornece (rolagem com setas para cima/para baixo, etc.).
  • Isso injeta caracteres '\0' na entrada se uma tecla especial for pressionada (F1, PrtScn, etc.).Você pode facilmente filtrá-los modificando o código.

Acabei aqui porque uma pergunta duplicada foi feita.Eu criei a seguinte solução que parece simples.Tenho certeza de que tem algumas desvantagens que perdi.

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

Cheguei a esta resposta e acabei fazendo:

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

Um exemplo simples usando 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");
}

Um código muito mais contemporâneo e baseado em tarefas seria mais ou menos assim:

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

Tive uma situação única de ter um aplicativo Windows (serviço Windows).Ao executar o programa interativamente Environment.IsInteractive (VS Debugger ou de cmd.exe), usei AttachConsole/AllocConsole para obter meu stdin/stdout.Para evitar que o processo termine enquanto o trabalho estava sendo realizado, o UI Thread chama Console.ReadKey(false).Eu queria cancelar a espera que o thread da UI estava fazendo em outro thread, então criei uma modificação na solução por @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();
  }    
}

Esta parece ser a solução mais simples e funcional, que não usa APIs nativas:

    static Task<string> ReadLineAsync(CancellationToken cancellation)
    {
        return Task.Run(() =>
        {
            while (!Console.KeyAvailable)
            {
                if (cancellation.IsCancellationRequested)
                    return null;

                Thread.Sleep(100);
            }
            return Console.ReadLine();
        });
    }

Exemplo de uso:

    static void Main(string[] args)
    {
        AsyncContext.Run(async () =>
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            cancelSource.CancelAfter(1000);
            Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null");
        });
    }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top