c# - application - ¿Cómo agregar un tiempo de espera a Console.ReadLine()?
console c# (30)
Tengo una aplicación de consola en la que quiero dar al usuario x segundos para responder al aviso. Si no se realiza ninguna entrada después de un cierto período de tiempo, la lógica del programa debe continuar. Suponemos que un tiempo de espera significa respuesta vacía.
¿Cuál es la forma más directa de abordar esto?
¡Por favor, 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 " nudge " la corriente StdIn para cancelar la lectura.
Sintaxis de llamadas:
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);
}
¿Este enfoque utilizará la ayuda de Console.KeyAvailable ?
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);
}
}
¿No es esto lindo y corto?
if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
ConsoleKeyInfo keyInfo = Console.ReadKey();
// Handle keyInfo value here...
}
.NET 4 lo hace increíblemente simple al usar 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 ningún intento de recrear la funcionalidad ReadLine o realizar otros hacks peligrosos para que funcione. Las tareas nos permiten resolver la pregunta de una manera muy natural.
Aquí hay una solución que usa Console.KeyAvailable
. Estas son llamadas de bloqueo, pero debería ser bastante trivial llamarlas de forma asíncrona a través del TPL, si así lo desea. Usé los mecanismos de cancelación estándar para facilitar el cableado con el Patrón asíncrono 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 funciones de navegación estándar que proporciona
ReadLine
(desplazamiento de flecha hacia arriba / hacia abajo, etc.). - Esto inyecta caracteres ''/ 0'' en la entrada si se presiona una tecla especial (F1, PrtScn, etc.). Sin embargo, puedes fácilmente filtrarlos modificando el código.
Aquí hay una solución segura que simula la entrada de la consola para desbloquear el hilo después del tiempo de espera. https://github.com/IgoSol/ConsoleReader proyecto https://github.com/IgoSol/ConsoleReader proporciona una implementación de ejemplo de diálogo de usuario.
var inputLine = ReadLine(5);
public static string ReadLine(uint timeoutSeconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds)
{
if (timeoutSeconds == 0)
return null;
var timeoutMilliseconds = timeoutSeconds * 1000;
if (samplingFrequencyMilliseconds > timeoutMilliseconds)
throw new ArgumentException("Sampling frequency must not be greater then timeout!", "samplingFrequencyMilliseconds");
CancellationTokenSource cts = new CancellationTokenSource();
Task.Factory
.StartNew(() => SpinUserDialog(timeoutMilliseconds, countDownMessage, samplingFrequencyMilliseconds, cts.Token), cts.Token)
.ContinueWith(t => {
var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
PostMessage(hWnd, 0x100, 0x0D, 9);
}, TaskContinuationOptions.NotOnCanceled);
var inputLine = Console.ReadLine();
cts.Cancel();
return inputLine;
}
private static void SpinUserDialog(uint countDownMilliseconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds,
CancellationToken token)
{
while (countDownMilliseconds > 0)
{
token.ThrowIfCancellationRequested();
Thread.Sleep((int)samplingFrequencyMilliseconds);
countDownMilliseconds -= countDownMilliseconds > samplingFrequencyMilliseconds
? samplingFrequencyMilliseconds
: countDownMilliseconds;
}
}
[DllImport("User32.Dll", EntryPoint = "PostMessageA")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
Como si ya no hubiera suficientes respuestas aquí: 0), lo siguiente se encapsula en una solución de 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");
}
Creo que necesitarás hacer un hilo secundario y sondear una clave en la consola. No sé de ninguna manera construida para lograr esto.
De una forma u otra, necesitas un segundo hilo. Puede usar IO asincrónico para evitar declarar el suyo:
- declare un ManualResetEvent, llámalo "evt"
- llama a System.Console.OpenStandardInput para obtener la corriente de entrada. Especifique un método de devolución de llamada que almacenará sus datos y establecerá evt.
- llamar al método BeginRead de esa secuencia para iniciar una operación de lectura asíncrona
- luego ingrese una espera temporizada en un evento ManualResetEvent
- si la espera se agota, cancele la lectura
Si la lectura devuelve datos, configure el evento y su conversación principal continuará; de lo contrario, continuará después del tiempo de espera.
Ejemplo de implementación de la publicación de Eric anterior. Este ejemplo particular se usó para leer información que se pasó a una aplicación de la 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
}
}
Ejemplo de subprocesamiento 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 cuerda estática arriba para obtener una línea completa.
Este es un ejemplo más completo de la solución de Glen Slayden. Pasé a hacer esto cuando construyo un caso de prueba para otro problema. Utiliza E / S asincrónicas y un evento de restablecimiento 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!");
}
}
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 == true)
{
k = Console.ReadKey();
break;
}
else
{
Console.WriteLine(cnt.ToString());
System.Threading.Thread.Sleep(1000);
}
}
Console.WriteLine("The key pressed was " + k.Key);
Estoy sorprendido de saber que después de 5 años, todas las respuestas aún sufren de uno o más de los siguientes problemas:
- Se usa una función que no sea ReadLine, lo que causa pérdida de funcionalidad. (Borrar / retroceso / tecla arriba para entrada anterior).
- La función se comporta mal cuando se invoca varias veces (generando múltiples hilos, muchos colgando ReadLine, o comportamiento inesperado).
- 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 un número de segundos hasta el tiempo de espera, que puede ser de varios minutos. Una espera ocupada que se ejecuta durante tanto tiempo es una horrible fuente 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 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)
, 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 a Reader
con llamadas normales de Console.ReadLine
: si el Reader
agota el tiempo de espera, habrá una llamada suspendida ReadLine
. En cambio, si desea tener una llamada de ReadLine
normal (no temporizada), simplemente use el Reader
y omita el tiempo de espera, de forma que se establezca un tiempo de espera infinito.
Entonces, ¿qué hay de los 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 hilo de fondo y solo se activará una llamada a ReadLine como máximo. Llamar a la función siempre dará como resultado la última entrada, o en un tiempo de espera, y el usuario no tendrá que presionar ingresar más de una vez para enviar su entrada.
- Y, obviamente, la función no depende de 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, los subprocesos múltiples realmente no pueden solicitar al usuario la entrada al mismo tiempo, por lo que la sincronización debería estar sucediendo antes de realizar una llamada a Reader.ReadLine
todos modos.
Llamar a Console.ReadLine () en el delegado es malo porque si el usuario no presiona ''enter'', esa llamada nunca volverá. El hilo que ejecuta el delegado se bloqueará hasta que el usuario presione ''enter'', sin posibilidad de cancelarlo.
Emitir una secuencia de estas llamadas no se comportará como es de esperar. Considere lo siguiente (usando la clase de consola de ejemplo de arriba):
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 expire 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 pulsa ''enter'', la primera llamada de ReadLine se completará, pero el código ha abandonado esa llamada y esencialmente descartó el resultado. La segunda llamada a ReadLine continuará bloqueándose, el tiempo de espera expirará y el valor devuelto volverá a ser el predeterminado.
Por cierto, hay un error en el código anterior. Al llamar a waitHandle.Close () cierra el evento desde debajo del subproceso trabajador. Si el usuario pulsa ''enter'' después de que expira el tiempo de espera, el subproceso de trabajo intentará señalar el evento que arroja una ObjectDisposedException. La excepción se emite desde el subproceso de trabajo, y si no ha configurado un controlador de excepción no controlada, su proceso finalizará.
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);
}
}
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 confían en algo que no sea Console.ReadLine () y Console.ReadLine () tiene muchas ventajas:
- Soporte para borrado, 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 tiene un gran uso).
Mi solución es la siguiente:
- Genere un hilo separado para manejar la entrada del usuario usando Console.ReadLine ().
- Después del período de tiempo de espera, desbloquee Console.ReadLine () enviando una clave [enter] a la ventana de la consola actual, utilizando 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:
Cómo abortar otro hilo en .NET, cuando dicho hilo está ejecutando Console.ReadLine?
Mucho más contemporáneo y el código 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();
}
No puedo comentar la publicación de Gulzar desafortunadamente, 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();
Otra forma barata de obtener un segundo hilo es envolverlo en un delegado.
Quizás esté leyendo demasiado sobre la cuestión, pero supongo que la espera será similar al menú de inicio, donde esperará 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 ''continuar'' y se bloquearía hasta que el temporizador expire 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);
}
Si está en el método Main()
, no puede usar await
, por lo que deberá 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 introduce la posibilidad de crear un método asincrónico Main()
, por lo que es mejor usar la versión Task.WhenAny()
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;
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;
}
Terminó aquí porque se hizo una pregunta duplicada. Se me ocurrió la siguiente solución que se ve directa. 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;
}
}
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 realizaba el trabajo, el Subproceso UI llama a Console.ReadKey(false)
. Quería cancelar la espera que estaba haciendo el subproceso de la interfaz de usuario desde otro subproceso, así que se me ocurrió una modificación a la solución por parte de @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();
}
}
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");
}
EDITAR : solucionó el problema haciendo que el trabajo real se realice en un proceso separado y eliminando ese proceso si se agota el tiempo. Ver abajo para más detalles. ¡Uf!
Solo di una carrera y pareció funcionar bien. Mi compañero de trabajo tenía una versión que usaba un objeto Thread, pero encuentro que el método BeginInvoke () de tipos de delegados 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());
}
}
}
// 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.
}
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();
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", perderá algunas de las interesantes características de ReadLine, a saber:
- Soporte para borrado, 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 tiene un gran uso).
Para agregar un tiempo de espera, modifique el ciclo while para adaptarlo.