我有一个控制台应用程序,我想在其中给用户x秒来响应提示。如果一定时间后没有输入,则应继续执行程序逻辑。我们假设超时意味着空响应。
最直接的方法是什么?
两个主要缺点:
基本思路是Windows API有一个函数来取消未完成的I/O请求:CancelIoEx。 当您将其用于取消STDIN上的操作时,Console.ReadLine会抛出OperationCanceledException。
所以这就是你该怎么做:
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleHelper
{
public static class ConsoleHelper
{
public static string ReadLine(TimeSpan timeout)
{
return ReadLine(Task.Delay(timeout));
}
public static string ReadLine(Task cancel_trigger)
{
var status = new Status();
var cancel_task = Task.Run(async () =>
{
await cancel_trigger;
status.Mutex.WaitOne();
bool io_done = status.IODone;
if (!io_done)
status.CancellationStarted = true;
status.Mutex.ReleaseMutex();
while (!status.IODone)
{
var success = CancelStdIn(out int error_code);
if (!success && error_code != 0x490) // 0x490 is what happens when you call cancel and there is not a pending I/O request
throw new Exception($"Canceling IO operation on StdIn failed with error {error_code} ({error_code:x})");
}
});
ReadLineWithStatus(out string input, out bool read_canceled);
if (!read_canceled)
{
status.Mutex.WaitOne();
bool must_wait = status.CancellationStarted;
status.IODone = true;
status.Mutex.ReleaseMutex();
if (must_wait)
cancel_task.Wait();
return input;
}
else // read_canceled == true
{
status.Mutex.WaitOne();
bool cancel_started = status.CancellationStarted;
status.IODone = true;
status.Mutex.ReleaseMutex();
if (!cancel_started)
throw new Exception("Received cancelation not triggered by this method.");
else
cancel_task.Wait();
return null;
}
}
private const int STD_INPUT_HANDLE = -10;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);
private static bool CancelStdIn(out int error_code)
{
var handle = GetStdHandle(STD_INPUT_HANDLE);
bool success = CancelIoEx(handle, IntPtr.Zero);
if (success)
{
error_code = 0;
return true;
}
else
{
var rc = Marshal.GetLastWin32Error();
error_code = rc;
return false;
}
}
private class Status
{
public Mutex Mutex = new Mutex(false);
public volatile bool IODone;
public volatile bool CancellationStarted;
}
private static void ReadLineWithStatus(out string result, out bool operation_canceled)
{
try
{
result = Console.ReadLine();
operation_canceled = false;
}
catch (OperationCanceledException)
{
result = null;
operation_canceled = true;
}
}
}
}
Console.ReadLine
开始之前调用CancelStdIn
(这就是为什么需要在cancel_trigger
中使用循环的原因)。Console.ReadLine
在触发取消之前返回(可能早得多)。Console.ReadLine
在触发取消之后但在调用CancelStdIn
之前返回。Console.ReadLine
由于响应取消触发器而由于调用CancelStdIn
而引发异常。鸣谢: 从SO答案中获得了CancelIoEx的想法,该答案来自Gérald Barré's博客。但是这些解决方案存在微妙的并发错误。
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);
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");
}
因为有一个重复的问题被问到了,所以我来到这里。我想出了以下看起来很简单的解决方案。我相信我可能错过了一些缺点。
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;
}
}
我有一个独特的情况,即拥有一个Windows应用程序(Windows服务)。当以交互方式运行程序Environment.IsInteractive
(从VS调试器或cmd.exe中),我使用AttachConsole/AllocConsole来获取我的stdin/stdout。
为了在工作进行时保持进程不结束,UI线程调用Console.ReadKey(false)
。我想从另一个线程取消UI线程正在等待的操作,因此我对@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();
}
}
以下是对Eric文章的示例实现。此特定示例用于通过管道传递信息并读取控制台应用程序中的信息:
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
}
}
ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout
代码:
public class AsyncConsole // not thread safe
{
private static readonly Lazy<AsyncConsole> Instance =
new Lazy<AsyncConsole>();
private bool _keyPressed;
private ConsoleKeyInfo _keyInfo;
private bool DoReadKey(
int millisecondsTimeout,
out ConsoleKeyInfo keyInfo)
{
_keyPressed = false;
_keyInfo = new ConsoleKeyInfo();
Thread readKeyThread = new Thread(ReadKeyThread);
readKeyThread.IsBackground = false;
readKeyThread.Start();
Thread.Sleep(millisecondsTimeout);
if (readKeyThread.IsAlive)
{
try
{
IntPtr stdin = GetStdHandle(StdHandle.StdIn);
CloseHandle(stdin);
readKeyThread.Join();
}
catch { }
}
readKeyThread = null;
keyInfo = _keyInfo;
return _keyPressed;
}
private void ReadKeyThread()
{
try
{
_keyInfo = Console.ReadKey();
_keyPressed = true;
}
catch (InvalidOperationException) { }
}
public static bool ReadKey(
int millisecondsTimeout,
out ConsoleKeyInfo keyInfo)
{
return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
}
private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };
[DllImport("kernel32.dll")]
private static extern IntPtr GetStdHandle(StdHandle std);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hdl);
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
delegate
{
readline = Console.ReadLine();
}
);
do
{
Thread.Sleep(100);
} while (readline == "?");
Console.KeyAvailable
的解决方案。虽然这些是阻塞调用,但如果需要,通过TPL异步调用它们应该相当简单。我使用了标准的取消机制,以便轻松地与任务异步模式和所有好东西连接起来。public static class ConsoleEx
{
public static string ReadLine(TimeSpan timeout)
{
var cts = new CancellationTokenSource();
return ReadLine(timeout, cts.Token);
}
public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
{
string line = "";
DateTime latest = DateTime.UtcNow.Add(timeout);
do
{
cancellation.ThrowIfCancellationRequested();
if (Console.KeyAvailable)
{
ConsoleKeyInfo cki = Console.ReadKey();
if (cki.Key == ConsoleKey.Enter)
{
return line;
}
else
{
line += cki.KeyChar;
}
}
Thread.Sleep(1);
}
while (DateTime.UtcNow < latest);
return null;
}
}
这有一些缺点。
ReadLine
提供的标准导航功能(上/下箭头滚动等)。更现代化和基于任务的代码可能看起来像这样:
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();
}