[c#] Console.ReadLine ()에 타임 아웃을 추가하는 방법?


14 Answers

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

프롬프트에 응답 할 사용자 x 초를 제공하려는 콘솔 앱이 있습니다. 일정 시간 후에 입력이 없으면 프로그램 로직을 계속 진행해야합니다. 시간 초과는 빈 응답을 의미한다고 가정합니다.

이것에 접근하는 가장 직접적인 방법은 무엇입니까?




나는이 대답에 도달하고 결국 :

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



이거 좋고 짧지 않아?

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

    // Handle keyInfo value here...
}



엔터프라이즈 환경에서 완벽하게 작동하는 솔루션을 찾기 전에이 문제로 5 개월 동안 고민했습니다.

지금까지 대부분의 솔루션에 대한 문제점은 Console.ReadLine ()이 아닌 다른 것에 의존하고 있으며 Console.ReadLine ()에 많은 장점이 있다는 것입니다.

  • 삭제, 백 스페이스, 화살표 키 등 지원
  • "up"키를 누르고 마지막 명령을 반복 할 수있는 능력 (많은 용도를 가진 백그라운드 디버깅 콘솔을 구현하면 매우 유용합니다).

내 솔루션은 다음과 같습니다 :

  1. Console.ReadLine ()을 사용하여 사용자 입력을 처리하는 별도의 스레드 를 생성합니다.
  2. 시간 초과 기간이 지나면 http://inputsimulator.codeplex.com/ 사용하여 현재 콘솔 창에 [enter] 키를 보내어 Console.ReadLine ()을 차단 해제합니다.

샘플 코드 :

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Console.ReadLine을 사용하는 스레드를 중단시키는 올바른 기술을 포함하여이 기술에 대한 추가 정보 :

[enter] 키 입력을 현재 프로세스 (콘솔 응용 프로그램)로 보내려면 .NET 호출이 필요합니까?

해당 스레드가 실행 중일 때 .NET에서 다른 스레드를 중단하는 방법 Console.ReadLine?




보조 스레드를 만들고 콘솔에서 키를 폴링해야 할 것입니다. 나는이 일을 성취하기 위해 아무 것도 건설되지 않았 음을 안다.




다음은 콘솔 입력을 가짜로 설정하여 시간 초과 후 스레드를 차단 해제하는 안전한 솔루션입니다. https://github.com/IgoSol/ConsoleReader 프로젝트는 샘플 사용자 대화 상자 구현을 제공합니다.

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



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



한 가지 방법 또는 다른 방법으로 두 번째 스레드가 필요합니다. 비동기 IO를 사용하여 자신의 선언을 피할 수 있습니다.

  • ManualResetEvent를 선언하고 "evt"라고합니다.
  • System.Console.OpenStandardInput을 호출하여 입력 스트림을 가져옵니다. 데이터를 저장하고 evt를 설정할 콜백 메소드를 지정하십시오.
  • 해당 스트림의 BeginRead 메서드를 호출하여 비동기 읽기 작업을 시작합니다.
  • ManualResetEvent에 시간 초과 대기를 입력하십시오.
  • 대기 시간이 초과 된 경우 읽기를 취소합니다.

read가 데이터를 반환하면 이벤트를 설정하면 메인 스레드가 계속 진행되고 그렇지 않으면 시간 초과 후 계속됩니다.




마치 여기에 충분한 답변이없는 것처럼 : 0), 다음은 정적 메소드 @ kwl의 솔루션 (위의 첫 번째 메소드)에 캡슐화됩니다.

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

용법

    static void Main()
    {
        Console.WriteLine("howdy");
        string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
        Console.WriteLine("bye");
    }



임명 내 경우이 잘 작동 :

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



나는이 질문에 너무 많이 읽었을지도 모르지만, 당신이 키를 누르지 않으면 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);
}



훨씬 더 현대적이고 작업 기반 코드는 다음과 같습니다.

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



다음은 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 제공하는 표준 탐색 기능 (위 / 아래 화살표 스크롤 등)을 얻지 못합니다.
  • 특수 키가 (F1, PrtScn 등)이면 '\ 0'문자를 입력에 삽입합니다. 당신은 쉽게 코드를 수정하여 그들을 필터링 할 수 있습니다.



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



Related