Como adicionar um tempo limite ao Console.ReadLine ()?

122

Eu tenho um aplicativo de console no qual desejo fornecer ao usuário x segundos para responder ao prompt. Se nenhuma entrada for feita após um certo período de tempo, a lógica do programa deve continuar. Assumimos que um tempo limite signifique resposta vazia.

Qual é a maneira mais direta de abordar isso?

Larsenal
fonte

Respostas:

112

Fico surpreso ao saber que, após 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. (Delete / backspace / up-key para entrada anterior).
  • A função se comporta mal quando invocada várias vezes (gerando vários threads, muitos ReadLine pendurados ou comportamento inesperado).
  • A função depende de uma espera ocupada. O que é um desperdício horrível, pois espera-se que a espera seja executada em vários segundos até o tempo limite, o que pode levar vários minutos. Uma espera ocupada, que dura tanto tempo, é uma péssima sucção de recursos, o que é especialmente ruim em um cenário de multithreading. Se a espera ocupada for modificada durante o sono, isso terá um efeito negativo na capacidade de resposta, embora eu admita que esse provavelmente não seja um problema enorme.

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

É claro que ligar é 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.");
}

Como alternativa, você pode usar a 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 maneira:

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

Nos dois casos, você não pode misturar chamadas Readercom Console.ReadLinechamadas normais : se o Readertempo limite expirar, haverá uma ReadLinechamada pendente . Em vez disso, se você deseja ter uma ReadLinechamada normal (não programada) , basta usar Readere omitir o tempo limite, para que o padrão seja um tempo limite infinito.

E os problemas das outras soluções que mencionei?

  • Como você pode ver, o ReadLine é usado, evitando o primeiro problema.
  • A função se comporta corretamente quando invocada várias vezes. Independentemente de um tempo limite ocorrer ou não, apenas um encadeamento em segundo plano estará em execução e apenas no máximo uma chamada para o ReadLine estará ativa. A chamada da 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 uma espera ocupada. Em vez disso, utiliza técnicas adequadas de multithreading para evitar o desperdício de recursos.

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

JSQuareD
fonte
1
Eu recebi uma NullReferenceException seguindo este código. Eu acho que eu poderia consertar o início do thread uma vez que os eventos automáticos são criados.
Augusto Pedraza
1
@JSQuareD Eu não acho que uma espera ocupada com um sono (200ms) seja muito horrible waste, mas é claro que sua sinalização é superior. Além disso, o uso de uma Console.ReadLinechamada de bloqueio em um loop infinito em uma segunda ameaça evita os problemas com muitas dessas chamadas em segundo plano, como nas outras soluções fortemente aprovadas abaixo. Obrigado por compartilhar seu código. 1
Roland
2
Se você não conseguir inserir a tempo, esse método parece ser interrompido na primeira Console.ReadLine()chamada subsequente que você fizer. Você acaba com um "fantasma" ReadLineque precisa ser concluído primeiro.
Derek
1
@Derek, infelizmente, você não pode misturar esse método com as chamadas normais do ReadLine; todas as chamadas devem ser feitas através do Reader. A solução para esse problema seria adicionar um método ao leitor que aguarde gotInput sem tempo limite. Atualmente, estou no celular, então não posso adicioná-lo à resposta com muita facilidade.
JSQuareD
1
Não vejo a necessidade getInput.
23918 silvalli
33
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.
fonte
2
Não sei por que isso não foi votado - funciona de maneira absolutamente perfeita. Muitas outras soluções envolvem "ReadKey ()", que não funciona corretamente: significa que você perde todo o poder do ReadLine (), como pressionar a tecla "para cima" para obter o comando digitado anteriormente, usando backspace e teclas de seta, etc.
Contango 17/11
9
@ Gravitas: Isso não funciona. Bem, funciona uma vez. Mas todas as ReadLinechamadas são feitas esperando por informações. Se você chamá-lo 100 vezes, ele cria 100 threads que nem desaparecem até você pressionar Enter 100 vezes!
Gabe
2
Cuidado. Essa solução parece limpa, mas acabei com milhares de chamadas incompletas penduradas. Portanto, não é adequado se chamado repetidamente.
Tom Makin
@ Gabe, shakinfree: várias chamadas não foram consideradas para a solução, mas apenas uma chamada assíncrona com tempo limite. Eu acho que seria confuso para o usuário ter 10 mensagens impressas no console e, em seguida, insira as entradas uma por uma na ordem respectiva. No entanto, para as chamadas suspensas, você pode tentar comentar a linha TimedoutException e retornar uma string nula / vazia?
gp.
não ... o problema é Console.ReadLine ainda está bloqueando o threadpool que está executando o método Console.ReadLine de ReadLineDelegate.
gp.
27

Essa abordagem usando o Console.KeyAvailable 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);
    }
}
Gulzar Nazim
fonte
Isso é verdade, o OP parece querer uma chamada de bloqueio, embora eu estremeça um pouco com o pensamento ... Essa é provavelmente uma solução melhor.
GEOCHET
Tenho certeza que você já viu isso. Entendi de um rápido google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/...
Gulzar Nazim
Não vejo como esse "tempo limite" se o usuário não fizer nada. Tudo o que isso faria é continuar executando a lógica em segundo plano até que uma tecla seja pressionada e outra lógica continue.
mphair
É verdade que isso precisa ser corrigido. Mas é fácil adicionar o tempo limite à condição do loop.
Jonathan Allen
KeyAvailableindica apenas que o usuário começou a digitar a entrada no ReadLine, mas precisamos de um evento ao pressionar Enter, que faz com que o ReadLine retorne. Esta solução funciona apenas para o ReadKey, ou seja, obtendo apenas um caractere. Como isso não resolve a questão real do ReadLine, não posso usar sua solução. -1 desculpe
Roland
13

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)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);
user980750
fonte
4
Eu acho que essa é a melhor e mais simples solução usando ferramentas já incorporadas. Bom trabalho!
Vippy
2
Lindo! Simplicidade é realmente a sofisticação máxima. Parabéns!
BrunoSalvino 25/02
10

De uma forma ou de outra, você precisa de um segundo segmento. Você pode usar E / S assíncronas para evitar declarar o seu:

  • declarar 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 armazene seus dados e defina 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 encadeamento principal continuará; caso contrário, você continuará após o tempo limite.

Eric
fonte
Isso é mais ou menos o que a Solução Aceita faz.
Roland
9
// 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
fonte
8

Eu acho que você precisará fazer um thread secundário e pesquisar por uma chave no console. Não conheço nenhuma maneira construída de conseguir isso.

GEOCHET
fonte
Sim, se você tem um segundo segmento de pesquisa de chaves e seu aplicativo é fechado enquanto ele fica parado esperando, esse segmento de pesquisa de chave fica parado, esperando, para sempre.
22711 Kelly Elton
Na verdade: um segundo tópico ou um delegado com "BeginInvoke" (que usa um tópico, nos bastidores - veja a resposta em @gp).
Contango 17/11
@ kelton52, o thread secundário será encerrado se você finalizar o processo no Gerenciador de Tarefas?
Arlen Beiler
6

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

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 utilizado).

Minha solução é a seguinte:

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

Código de amostra:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

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

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

Contango
fonte
5

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

A emissão de uma sequência dessas chamadas não se comportará como você esperaria. Considere o seguinte (usando a classe Console de exemplo 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 insere um valor para o segundo prompt. FirstName e lastName conterão os valores padrão. Quando o usuário pressiona 'enter', a primeira chamada ReadLine será concluída, mas o código abandonou essa chamada e essencialmente descartou o resultado. A segunda chamada ReadLine continuará sendo bloqueada, o tempo limite expirará e o valor retornado será o padrão novamente.

Entre - Existe um erro no código acima. Ao chamar waitHandle.Close (), você fecha o evento sob o segmento de trabalho. Se o usuário pressionar 'enter' após o tempo limite expirar, o encadeamento do trabalhador tentará sinalizar o evento que gera uma ObjectDisposedException. A exceção é lançada do encadeamento de trabalho e, se você não tiver configurado um manipulador de exceções sem tratamento, seu processo será encerrado.

Brannon
fonte
1
O termo "acima" em sua postagem é ambíguo e confuso. Se você estiver se referindo a outra resposta, faça um link adequado para essa resposta.
bzlm
5

Se você estiver no Main()método, não poderá usá- awaitlo, portanto precisará 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 apresenta a possibilidade de criar um Main()método assíncrono ; portanto, é melhor usar a 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;
kwl
fonte
4

Talvez eu esteja lendo muito sobre a questão, mas presumo que a espera seja semelhante ao menu de inicialização, onde aguarda 15 segundos, a menos que você pressione uma tecla. Você pode usar (1) uma função de bloqueio ou (2) você pode usar um encadeamento, um evento e um cronômetro. O evento atuaria como um 'continuar' e seria bloqueado até o tempo expirar ou uma tecla ser 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);
}
Ryan
fonte
2

Infelizmente, não posso comentar no post 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();
Jamie Kitson
fonte
Observe que você também pode usar Console.In.Peek () se o console não estiver visível (?) Ou se a entrada for direcionada de um arquivo.
precisa
2

EDIT : corrigido o problema executando o trabalho real em um processo separado e eliminando esse processo se o tempo limite expirar. Veja abaixo para detalhes. Uau!

Apenas deu uma corrida e parecia funcionar bem. Meu colega de trabalho tinha uma versão que usava um objeto Thread, mas acho que o método BeginInvoke () dos tipos de delegado é 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 com a seguinte aparência:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}
Jesse C. Slicer
fonte
2
Invocar um executável separado em um novo processo apenas para executar um ReadLine () cronometrado parece um grande exagero. Você está basicamente resolvendo o problema de não conseguir abortar um thread de bloqueio ReadLine () configurando e desmontando um processo inteiro.
bzlm
Depois conte para a Microsoft, que nos colocou nessa posição.
Jesse C. Slicer
A Microsoft não colocou você nessa posição. Veja algumas das outras respostas que fazem o mesmo trabalho em algumas linhas. Eu acho que o código acima deve obter algum tipo de prêmio - mas não do tipo que você quer :)
Contango
1
Não, nenhuma das outras respostas fez exatamente o que o OP queria. Todos eles perdem recursos das rotinas de entrada padrão ou ficam paralisados ​​com o fato de todas as solicitações Console.ReadLine() estarem bloqueando e reterão a entrada na próxima solicitação. A resposta aceita é bastante próxima, mas ainda tem limitações.
Jesse C. Slicer
1
Hum, não, não é. O buffer de entrada ainda bloqueia (mesmo que o programa não o faça). Experimente você mesmo: insira alguns caracteres, mas não pressione enter. Deixe o tempo limite. Capture a exceção no chamador. Depois, tenha outro ReadLine()no seu programa depois de chamar este. Veja o que acontece. Você tem que pressionar return TWICE para fazê-lo funcionar devido à natureza de thread único do Console. Isto. Não. Trabalhos.
Jesse C. Slicer
2

O .NET 4 torna isso incrivelmente simples usando o Tasks.

Primeiro, construa seu ajudante:

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

Segundo, execute com uma tarefa e aguarde:

      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 recriar a funcionalidade ReadLine ou executar outros hacks perigosos para que isso funcione. Tarefas, vamos resolver a questão de uma maneira muito natural.

StevoInco
fonte
2

Como se já não houvesse respostas suficientes aqui: 0), o seguinte é encapsulado na solução de método estático @ 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");
    }
Nicholas Petersen
fonte
1

Exemplo simples de encadeamento 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.

mphair
fonte
1

No meu caso, este trabalho é bom:

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
fonte
1

Isso não é legal e curto?

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

    // Handle keyInfo value here...
}
John Atac
fonte
1
O que diabos é SpinWait?
John ktejik
1

Este é um exemplo mais completo da solução de Glen Slayden. Eu fiz isso ao criar um caso de teste para outro problema. Ele usa E / S assíncrona e um evento de redefiniçã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!");
        }
    }
mikemay
fonte
1

Meu código é inteiramente baseado na resposta do amigo @JSQuareD

Mas eu precisava usar o Stopwatchtimer porque, quando terminei o programa, Console.ReadKey()ele ainda estava esperando Console.ReadLine()e gerou um comportamento inesperado.

Funcionou perfeitamente para mim. Mantém o Console.ReadLine () original

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.");
            }
        }
    }
}
Sergio Cabral
fonte
0

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

Joel Coehoorn
fonte
0

Exemplo de implementação da postagem de Eric acima. Este exemplo específico foi usado para ler as informações que foram passadas para um aplicativo de console via canal:

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

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

Observe que, se você seguir a rota "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 utilizado).

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

Contango
fonte
0

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 " empurrar " 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);
}
David Kirkland
fonte
0

Aqui está uma solução que usa Console.KeyAvailable. Essas são chamadas de bloqueio, mas deve ser bastante trivial chamá-las de forma assíncrona via TPL, se desejado. Usei os mecanismos de cancelamento padrão para facilitar a conexão com o padrão de tarefas assíncronas 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 ReadLinefornecidos (rolagem de seta para cima / baixo, etc.).
  • Isso injeta caracteres '\ 0' na entrada se uma tecla especial for pressionada (F1, PrtScn, etc.). Você pode filtrá-los facilmente, modificando o código.
Brian Gideon
fonte
0

Acabamos aqui porque foi feita uma pergunta duplicada. Eu vim com a seguinte solução, que parece simples. Tenho certeza de que há 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;
    }
}
Frank Rem
fonte
0

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);                
        }            
    }
Tono Nam
fonte
0

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");
}
cprcrack
fonte
E se o usuário pressionar a tecla e soltar dentro de 2000ms?
Izzy
0

Um código muito mais contemporâneo e baseado em tarefas se pareceria com isso:

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();
}
Shonn Lyga
fonte
0

Eu tive uma situação única de ter um aplicativo do Windows (Windows Service). Ao executar o programa interativamente Environment.IsInteractive(VS Debugger ou no cmd.exe), usei o AttachConsole / AllocConsole para obter meu stdin / stdout. Para impedir que o processo termine enquanto o trabalho estava sendo concluído, o thread da interface do usuário chama Console.ReadKey(false). Queria cancelar a espera que o thread da interface do usuário estava aguardando de outro thread, então eu vim com 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();
  }    
}
JJS
fonte