プロンプトに応答するためにユーザーにx秒を与えたいコンソール アプリがあります。一定時間入力がない場合、プログラム ロジックは続行する必要があります。タイムアウトは空の応答を意味すると想定しています。
これにアプローチする最も簡単な方法は何ですか?
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();
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);
}
}
これは私にとってはうまくいきました。
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);
いずれにせよ、2 番目のスレッドが必要です。非同期 IO を使用して、独自の宣言を回避できます。
読み取りがデータを返す場合は、イベントを設定するとメイン スレッドが続行されます。それ以外の場合は、タイムアウト後に続行されます。
// 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.
}
セカンダリ スレッドを作成し、コンソールでキーをポーリングする必要があると思います。これを達成するための組み込みの方法はありません。
この問題に 5 か月間悩まされた後、エンタープライズ環境で完全に機能するソリューションを見つけました。
これまでのほとんどのソリューションの問題は、Console.ReadLine() 以外のものに依存していることであり、Console.ReadLine() には多くの利点があります。
私の解決策は次のとおりです。
サンプルコード:
InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);
Console.ReadLine を使用するスレッドを中止する正しい手法を含む、この手法の詳細:
デリゲートで Console.ReadLine() を呼び出すのは良くありません。ユーザーが「Enter」キーを押さないと、その呼び出しが返されないためです。デリゲートを実行するスレッドは、ユーザーが「Enter」を押すまでブロックされ、キャンセルする方法はありません。
これらの呼び出しのシーケンスを発行すると、期待どおりに動作しません。以下を検討してください (上記の Console クラスの例を使用)。
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");
ユーザーは、最初のプロンプトでタイムアウトが切れるのを待ち、2 番目のプロンプトで値を入力します。firstName と lastName の両方にデフォルト値が含まれます。ユーザーが「Enter」を押すと、最初のReadLine 呼び出しが完了しますが、コードはその呼び出しを放棄し、基本的に結果を破棄しています。2 番目のReadLine呼び出しは引き続きブロックされ、タイムアウトは最終的に期限切れになり、返される値は再びデフォルトになります。
ところで-上記のコードにはバグがあります。waitHandle.Close() を呼び出すことで、ワーカー スレッドの下からイベントを閉じます。タイムアウトの期限が切れた後にユーザーが「Enter」を押すと、ワーカー スレッドは ObjectDisposedException をスローするイベントを通知しようとします。例外はワーカー スレッドからスローされ、未処理の例外ハンドラーを設定していない場合、プロセスは終了します。
質問を読みすぎているかもしれませんが、キーを押さない限り、15 秒間待機するブート メニューに似た待機時間になると思います。(1) ブロッキング関数を使用するか、(2) スレッド、イベント、およびタイマーを使用することができます。イベントは「継続」として機能し、タイマーが切れるか、キーが押されるまでブロックされます。
(1) の疑似コードは次のようになります。
// 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);
}
.NET 4 では、タスクを使用してこれを非常に簡単にします。
まず、ヘルパーをビルドします。
Private Function AskUser() As String
Console.Write("Answer my question: ")
Return Console.ReadLine()
End Function
次に、タスクを実行して待機します。
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
これを機能させるために、ReadLine 機能を再作成しようとしたり、他の危険なハックを実行したりする必要はありません。タスクを使用すると、非常に自然な方法で問題を解決できます。
残念ながら、Gulzar の投稿にコメントすることはできませんが、より完全な例を次に示します。
while (Console.KeyAvailable == false)
{
Thread.Sleep(250);
i++;
if (i > 3)
throw new Exception("Timedout waiting for input.");
}
input = Console.ReadLine();
編集:実際の作業を別のプロセスで実行し、タイムアウトした場合はそのプロセスを強制終了することで問題を修正しました。詳細については、以下を参照してください。うわー!
これを実行しただけで、うまく機能しているように見えました。私の同僚は Thread オブジェクトを使用するバージョンを持っていましたが、デリゲート型の BeginInvoke() メソッドの方がもう少し洗練されていることがわかりました。
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);
}
}
}
}
ReadLine.exe プロジェクトは、次のような 1 つのクラスを持つ非常に単純なものです。
namespace ReadLine
{
internal static class Program
{
private static void Main()
{
System.Console.WriteLine(System.Console.ReadLine());
}
}
}
私の場合、これはうまくいきます:
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;
}
これは、Glen Slayden のソリューションのより完全な例です。別の問題のテストケースを作成するときに、たまたまこれを作成しました。非同期 I/O と手動リセット イベントを使用します。
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!");
}
}
私は、Windows アプリケーション (Windows サービス) を持っているという独特の状況にありました。プログラムをインタラクティブにEnvironment.IsInteractive
(VS Debugger または 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();
}
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
delegate
{
readline = Console.ReadLine();
}
);
do
{
Thread.Sleep(100);
} while (readline == "?");
「Console.ReadKey」ルートをたどると、ReadLine の優れた機能の一部が失われることに注意してください。
タイムアウトを追加するには、while ループを適切に変更します。
既存の回答の過多に別の解決策を追加することで私を嫌わないでください! これは Console.ReadKey() で機能しますが、ReadLine() などで機能するように簡単に変更できます。
「Console.Read」メソッドがブロックされているため、StdIn ストリームを「微調整」して読み取りをキャンセルする必要があります。
呼び出し構文:
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);
}
を使った解決法をご紹介しますConsole.KeyAvailable
。これらは呼び出しをブロックしていますが、必要に応じて TPL を介して非同期に呼び出すのはかなり簡単です。標準のキャンセル メカニズムを使用して、Task Asynchronous Pattern などの優れた機能を簡単に接続できるようにしました。
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
提供される標準のナビゲーション機能 (上下矢印のスクロールなど)は利用できません。上記のエリックの投稿の実装例。この特定の例は、パイプ経由でコンソール アプリに渡された情報を読み取るために使用されました。
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
}
}
2 番目のスレッドを取得するもう 1 つの安価な方法は、デリゲートでラップすることです。
重複した質問があったため、ここにたどり着きました。簡単に見える次のソリューションを思いつきました。私が見逃したいくつかの欠点があると確信しています。
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;
}
}