¿Cómo agregar un tiempo de espera a Console.ReadLine ()?

122

Tengo una aplicación de consola en la que quiero darle al usuario x segundos para responder a la solicitud. Si no se realiza ninguna entrada después de cierto período de tiempo, la lógica del programa debe continuar. Suponemos que un tiempo de espera significa una respuesta vacía.

¿Cuál es la forma más directa de abordar esto?

Larsenal
fuente

Respuestas:

112

Me sorprende saber que después de 5 años, todas las respuestas aún sufren uno o más de los siguientes problemas:

  • Se utiliza una función distinta de ReadLine, que causa la pérdida de funcionalidad. (Eliminar / retroceso / tecla arriba para la entrada anterior).
  • La función se comporta mal cuando se invoca varias veces (genera múltiples subprocesos, muchos ReadLine colgantes o comportamientos inesperados).
  • La función se basa en una espera ocupada. Lo cual es un desperdicio horrible ya que se espera que la espera se ejecute en cualquier lugar desde una cantidad de segundos hasta el tiempo de espera, que puede ser de varios minutos. Una espera ocupada que dura tanto tiempo es una horrible cantidad de recursos, lo que es especialmente malo en un escenario de subprocesos múltiples. Si la espera ocupada se modifica con un sueño, esto tiene un efecto negativo en la capacidad de respuesta, aunque admito que esto probablemente no sea un gran problema.

Creo que mi solución resolverá el problema original sin sufrir ninguno de los problemas anteriores:

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

Llamar es, por supuesto, muy 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, puede usar la TryXX(out)convención, como sugirió 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 se llama de la siguiente manera:

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

En ambos casos, no puede mezclar llamadas Readercon Console.ReadLinellamadas normales : si se Readeragota el tiempo de espera, habrá una ReadLinellamada pendiente . En cambio, si desea tener una ReadLinellamada normal (no programada) , simplemente use Readery omita el tiempo de espera, de modo que el valor predeterminado sea un tiempo de espera infinito.

Entonces, ¿qué hay de esos problemas de las otras soluciones que mencioné?

  • Como puede ver, se usa ReadLine, evitando el primer problema.
  • La función se comporta correctamente cuando se invoca varias veces. Independientemente de si se produce un tiempo de espera o no, solo se ejecutará un subproceso en segundo plano y solo como máximo una llamada a ReadLine estará activa. Llamar a la función siempre dará como resultado la última entrada o un tiempo de espera, y el usuario no tendrá que presionar enter más de una vez para enviar su entrada.
  • Y, obviamente, la función no se basa en una espera ocupada. En su lugar, utiliza técnicas adecuadas de subprocesamiento múltiple para evitar el desperdicio de recursos.

El único problema que preveo con esta solución es que no es seguro para subprocesos. Sin embargo, varios subprocesos realmente no pueden pedirle al usuario que ingrese al mismo tiempo, por lo que la sincronización debería estar ocurriendo antes de hacer una llamada Reader.ReadLine.

JSQuareD
fuente
1
Obtuve una NullReferenceException siguiendo este código. Creo que podría solucionar el inicio del hilo una vez que se crean los eventos automáticos.
Augusto Pedraza
1
@JSQuareD No creo que una espera ocupada con una suspensión (200 ms) sea tan difícil horrible waste, pero, por supuesto, su señalización es superior. Además, el uso de una Console.ReadLinellamada de bloqueo en un bucle infinito en una segunda amenaza previene los problemas con muchas de esas llamadas colgadas en segundo plano como en las otras soluciones, muy animadas, a continuación. Gracias por compartir tu código. +1
Roland
2
Si no ingresa a tiempo, este método parece interrumpirse en la primera Console.ReadLine()llamada posterior que realice. Terminas con un "fantasma" ReadLineque primero debes completar.
Derek
1
@Derek desafortunadamente, no puede mezclar este método con las llamadas normales de ReadLine, todas las llamadas deben hacerse a través del Reader. La solución a este problema sería agregar un método al lector que espera a GotInput sin tiempo de espera. Actualmente estoy en el móvil, por lo que no puedo agregarlo a la respuesta muy fácilmente.
JSQuareD
1
No veo la necesidad de getInput.
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.
fuente
2
No sé por qué esto no ha sido votado, funciona de manera absolutamente perfecta. Muchas de las otras soluciones implican "ReadKey ()", que no funciona correctamente: significa que pierde todo el poder de ReadLine (), como presionar la tecla "arriba" para obtener el comando previamente escrito, usar la tecla de retroceso y teclas de flecha, etc.
Contango
9
@Gravitas: Esto no funciona. Bueno, funciona una vez. Pero cada vez ReadLineque llamas se sienta allí esperando la entrada. Si lo llamas 100 veces, crea 100 hilos que no desaparecen hasta que presionas Enter 100 veces.
Gabe
2
Tener cuidado. Esta solución parece ordenada, pero terminé con miles de llamadas incompletas pendientes. Por lo tanto, no es adecuado si se llama repetidamente.
Tom Makin
@Gabe, shakinfree: no se consideraron varias llamadas para la solución, sino solo una llamada asincrónica con tiempo de espera. Supongo que sería confuso para el usuario tener 10 mensajes impresos en la consola y luego ingresar entradas para ellos uno por uno en el orden respectivo. Sin embargo, para las llamadas pendientes, ¿podría intentar comentar la línea TimedoutException y devolver una cadena nula / vacía?
gp.
no ... el problema es Console.ReadLine todavía está bloqueando el hilo threadpool que ejecuta el método Console.ReadLine de ReadLineDelegate.
gp.
27

¿Este enfoque utilizando Console.KeyAvailable ayuda?

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
fuente
Esto es cierto, el OP parece querer una llamada de bloqueo, aunque me estremezco un poco al pensar ... Esta es probablemente una mejor solución.
GEOCHET
Estoy seguro de que has visto esto. Lo obtuve de un rápido google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim
No veo cómo este "tiempo de espera" si el usuario no hace nada. Todo lo que esto haría es posiblemente seguir ejecutando la lógica en segundo plano hasta que se presione una tecla y continúe otra lógica.
mphair
Es cierto que esto necesita ser arreglado. Pero es bastante fácil agregar el tiempo de espera a la condición del bucle.
Jonathan Allen
KeyAvailablesolo indica que el usuario ha comenzado a escribir entradas en ReadLine, pero necesitamos un evento al presionar Enter, que hace que ReadLine regrese. Esta solución solo funciona para ReadKey, es decir, obtener solo un carácter. Como esto no resuelve la pregunta real para ReadLine, no puedo usar su solución. -1 lo siento
Roland
13

Esto funcionó para mí.

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);
usuario980750
fuente
44
Creo que esta es la mejor y más simple solución usando herramientas ya integradas. ¡Gran trabajo!
Vippy
2
¡Hermoso! La simplicidad es realmente la máxima sofisticación. Felicidades!
BrunoSalvino
10

De una forma u otra necesitas un segundo hilo. Puede usar IO asincrónico para evitar declarar su propio:

  • declare un ManualResetEvent, llámelo "evt"
  • llame a System.Console.OpenStandardInput para obtener la secuencia de entrada. Especifique un método de devolución de llamada que almacenará sus datos y establecerá evt.
  • llame al método BeginRead de esa secuencia para iniciar una operación de lectura asincrónica
  • luego ingrese una espera programada en un ManualResetEvent
  • si se agota el tiempo de espera, cancele la lectura

Si la lectura devuelve datos, configure el evento y su hilo principal continuará, de lo contrario continuará después del tiempo de espera.

Eric
fuente
Esto es más o menos lo que hace la solución aceptada.
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
fuente
8

Creo que necesitará hacer un hilo secundario y sondear una clave en la consola. No conozco ninguna forma integrada de lograr esto.

GEOCHET
fuente
Sí, si tiene un segundo sondeo de hilo para las claves, y su aplicación se cierra mientras está allí esperando, ese hilo de sondeo de teclas simplemente se quedará allí, esperando, para siempre.
Kelly Elton
En realidad: ya sea un segundo hilo o un delegado con "BeginInvoke" (que usa un hilo, detrás de escena, vea la respuesta de @gp).
Contango
@ kelton52, ¿Se cerrará el subproceso secundario si finaliza el proceso en el Administrador de tareas?
Arlen Beiler
6

Luché con este problema durante 5 meses antes de encontrar una solución que funcionara perfectamente en un entorno empresarial.

El problema con la mayoría de las soluciones hasta ahora es que dependen de algo distinto de Console.ReadLine () y Console.ReadLine () tiene muchas ventajas:

  • Soporte para eliminar, retroceso, teclas de flecha, etc.
  • La capacidad de presionar la tecla "arriba" y repetir el último comando (esto es muy útil si implementa una consola de depuración en segundo plano que se usa mucho).

Mi solución es la siguiente:

  1. Genera un hilo separado para manejar la entrada del usuario usando Console.ReadLine ().
  2. Después del tiempo de espera, desbloquee Console.ReadLine () enviando una tecla [enter] a la ventana actual de la consola, usando http://inputsimulator.codeplex.com/ .

Código de muestra:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Más información sobre esta técnica, incluida la técnica correcta para abortar un hilo que usa Console.ReadLine:

Llamada .NET para enviar pulsación de tecla [enter] en el proceso actual, ¿cuál es una aplicación de consola?

¿Cómo abortar otro hilo en .NET, cuando dicho hilo está ejecutando Console.ReadLine?

Aplazamiento de pago
fuente
5

Llamar a Console.ReadLine () en el delegado es malo porque si el usuario no presiona 'enter', esa llamada nunca volverá. El hilo que ejecuta al delegado se bloqueará hasta que el usuario presione 'enter', sin forma de cancelarlo.

Emitir una secuencia de estas llamadas no se comportará como cabría esperar. Considere lo siguiente (usando la clase de consola de ejemplo anterior):

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

El usuario deja que caduque el tiempo de espera para el primer aviso, luego ingresa un valor para el segundo aviso. Tanto firstName como lastName contendrán los valores predeterminados. Cuando el usuario presiona 'enter', la primera llamada de ReadLine se completará, pero el código ha abandonado esa llamada y esencialmente ha descartado el resultado. La segunda llamada ReadLine continuará bloqueándose, el tiempo de espera expirará y el valor devuelto será nuevamente el predeterminado.

Por cierto, hay un error en el código anterior. Al llamar a waitHandle.Close (), cierra el evento por debajo del subproceso de trabajo. Si el usuario presiona 'enter' después de que expire el tiempo de espera, el hilo de trabajo intentará señalar el evento que arroja una ObjectDisposedException. La excepción se genera desde el subproceso de trabajo, y si no ha configurado un controlador de excepciones no controlado, su proceso finalizará.

Brannon
fuente
1
El término "arriba" en su publicación es ambiguo y confuso. Si se refiere a otra respuesta, debe hacer un enlace apropiado a esa respuesta.
bzlm
5

Si está en el Main()método, no puede usar await, por lo que tendrá 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;

Sin embargo, C # 7.1 presenta la posibilidad de crear un Main()método asíncrono , por lo que es mejor usar la Task.WhenAny()versión siempre que tenga esa opción:

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
fuente
4

Puede que esté leyendo demasiado la pregunta, pero supongo que la espera sería similar al menú de inicio donde espera 15 segundos a menos que presione una tecla. Puede usar (1) una función de bloqueo o (2) puede usar un hilo, un evento y un temporizador. El evento actuaría como un 'continuar' y se bloquearía hasta que expire el temporizador o se presione una tecla.

El seudocódigo para (1) sería:

// 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
fuente
2

Lamentablemente, no puedo comentar sobre la publicación de Gulzar, pero aquí hay un ejemplo más completo:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();
Jamie Kitson
fuente
Tenga en cuenta que también puede usar Console.In.Peek () si la consola no está visible (?) O si la entrada se dirige desde un archivo.
Jamie Kitson
2

EDITAR : solucionó el problema haciendo que el trabajo real se realizara en un proceso separado y eliminando ese proceso si se agota el tiempo de espera. Ver abajo para más detalles. ¡Uf!

Solo le di una oportunidad y pareció funcionar bien. Mi compañero de trabajo tenía una versión que usaba un objeto Thread, pero creo que el método BeginInvoke () para delegar tipos es un poco más 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);
         }
      }
   }
}

El proyecto ReadLine.exe es muy simple y tiene una clase que se ve así:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}
Jesse C. Slicer
fuente
2
Invocar un ejecutable separado en un nuevo proceso solo para hacer una ReadLine () cronometrada suena como una exageración masiva. Básicamente, está resolviendo el problema de no poder Abortar un ReadLine (), bloqueando el hilo configurando y desmantelando todo un proceso.
bzlm
Luego díselo a Microsoft, que nos puso en esta posición.
Jesse C. Slicer
Microsoft no te puso en esa posición. Mire algunas de las otras respuestas que hacen el mismo trabajo en pocas líneas. Creo que el código anterior debería recibir algún tipo de premio, pero no el que desea :)
Contango
1
No, ninguna de las otras respuestas hizo exactamente lo que el OP quería. Todos ellos pierden características de las rutinas de entrada estándar o quedan atrapados por el hecho de que todas las solicitudes Console.ReadLine() están bloqueando y retrasarán la entrada en la próxima solicitud. La respuesta aceptada es bastante cercana, pero aún tiene limitaciones.
Jesse C. Slicer
1
Um, no, no lo es. El búfer de entrada todavía se bloquea (incluso si el programa no lo hace). Pruébelo usted mismo: ingrese algunos caracteres pero no presione enter. Deja que se agote el tiempo. Capture la excepción en la persona que llama. Luego tenga otro ReadLine()en su programa después de llamar a este. Mira qué pasa. Tienes que presionar return TWICE para que funcione debido a la naturaleza de un solo subproceso Console. Eso. No lo hace Trabajo.
Jesse C. Slicer
2

.NET 4 hace esto increíblemente simple usando Tareas.

Primero, construye tu ayudante:

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

Segundo, ejecuta con una tarea y espera:

      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

No hay que intentar recrear la funcionalidad de ReadLine ni realizar otros hacks peligrosos para que esto funcione. Las tareas nos permiten resolver la cuestión de una manera muy natural.

StevoInco
fuente
2

Como si no hubiera suficientes respuestas aquí: 0), lo siguiente encapsula en una solución del método estático @ kwl anterior (la primera).

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

Ejemplo de enhebrado simple para resolver esto

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

o una cadena estática en la parte superior para obtener una línea completa.

mphair
fuente
1

Soy mi caso, esto funciona bien:

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

¿No es esto agradable y corto?

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

    // Handle keyInfo value here...
}
John Atac
fuente
1
¿Qué diablos es SpinWait?
John Ktejik
1

Este es un ejemplo más completo de la solución de Glen Slayden. Se me ocurrió hacer esto al construir un caso de prueba para otro problema. Utiliza E / S asíncrona y un evento de reinicio 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
fuente
1

Mi código se basa completamente en la respuesta del amigo @JSQuareD

Pero necesitaba usar el Stopwatchtemporizador porque cuando terminé el programa Console.ReadKey()todavía estaba esperando Console.ReadLine()y generó un comportamiento inesperado.

Funcionó perfectamente para mí. Mantiene la consola original. 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.");
            }
        }
    }
}
Sergio Cabral
fuente
0

Otra forma económica de obtener un segundo hilo es envolverlo en un delegado.

Joel Coehoorn
fuente
0

Ejemplo de implementación de la publicación de Eric anterior. Este ejemplo particular se usó para leer la información que se pasó a una aplicación de consola a través de una tubería:

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

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

Tenga en cuenta que si baja por la ruta "Console.ReadKey", pierde algunas de las características interesantes de ReadLine, a saber:

  • Soporte para eliminar, retroceso, teclas de flecha, etc.
  • La capacidad de presionar la tecla "arriba" y repetir el último comando (esto es muy útil si implementa una consola de depuración en segundo plano que se usa mucho).

Para agregar un tiempo de espera, modifique el ciclo while para adaptarlo.

Aplazamiento de pago
fuente
0

¡No me odies por agregar otra solución a la gran cantidad de respuestas existentes! Esto funciona para Console.ReadKey (), pero podría modificarse fácilmente para que funcione con ReadLine (), etc.

Como los métodos "Console.Read" están bloqueando, es necesario " empujar " la secuencia StdIn para cancelar la lectura.

Sintaxis de llamada:

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
fuente
0

Aquí hay una solución que utiliza Console.KeyAvailable. Estas son llamadas de bloqueo, pero debería ser bastante trivial llamarlas de forma asincrónica a través del TPL si lo desea. Utilicé los mecanismos de cancelación estándar para facilitar la conexión con el patrón asincrónico de tareas y todas esas cosas buenas.

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

Hay algunas desventajas con esto.

  • No obtiene las características de navegación estándar que ReadLineproporciona (desplazamiento de flecha arriba / abajo, etc.).
  • Esto inyecta caracteres '\ 0' en la entrada si se presiona una tecla especial (F1, PrtScn, etc.). Sin embargo, podría filtrarlos fácilmente modificando el código.
Brian Gideon
fuente
0

Terminé aquí porque se hizo una pregunta duplicada. Se me ocurrió la siguiente solución que parece sencilla. Estoy seguro de que tiene algunos inconvenientes que me perdí.

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
fuente
0

Llegué a esta respuesta y terminé haciendo:

    /// <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
fuente
0

Un ejemplo simple 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
fuente
¿Qué sucede si el usuario presiona la tecla y se suelta dentro de 2000 ms?
Izzy
0

El código mucho más contemporáneo y basado en tareas se vería así:

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
fuente
0

Tuve una situación única de tener una aplicación de Windows (servicio de Windows). Al ejecutar el programa de forma interactiva Environment.IsInteractive(VS Debugger o desde cmd.exe), utilicé AttachConsole / AllocConsole para obtener mi stdin / stdout. Para evitar que el proceso finalice mientras se realiza el trabajo, el subproceso de IU llama Console.ReadKey(false). Quería cancelar la espera que el hilo de la interfaz de usuario estaba haciendo desde otro hilo, así que se me ocurrió una modificación de la solución 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
fuente