[c#] Comment ajouter un Timeout à Console.ReadLine ()?


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

J'ai une application de console dans laquelle je veux donner à l'utilisateur x secondes pour répondre à l'invite. Si aucune entrée n'est effectuée après un certain laps de temps, la logique du programme devrait continuer. Nous supposons qu'un délai d'attente signifie une réponse vide.

Quelle est la manière la plus directe d'aborder cela?




Je ne peux malheureusement pas commenter l'article de Gulzar, mais voici un exemple plus complet:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();



Comme s'il n'y avait pas déjà assez de réponses ici: 0), le suivant s'encapsule dans une solution statique de la méthode @ kwl ci-dessus (la première).

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

Usage

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



Beaucoup plus contemporain et le code basé sur les tâches ressemblerait à quelque chose comme ceci:

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



Voici une solution sûre qui simule l'entrée de la console pour débloquer le fil après l'expiration du délai. https://github.com/IgoSol/ConsoleReader projet https://github.com/IgoSol/ConsoleReader fournit un exemple d'implémentation de dialogue utilisateur.

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



Je suis venu à cette réponse et finis par faire:

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



N'est-ce pas gentil et court?

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

    // Handle keyInfo value here...
}



D'une manière ou d'une autre, vous avez besoin d'un deuxième fil. Vous pouvez utiliser des E / S asynchrones pour éviter de déclarer les vôtres:

  • déclarer un ManualResetEvent, l'appeler "evt"
  • appelez System.Console.OpenStandardInput pour obtenir le flux d'entrée. Spécifiez une méthode de rappel qui stockera ses données et définira evt.
  • appelle la méthode BeginRead de ce flux pour démarrer une opération de lecture asynchrone
  • puis entrez une attente temporisée sur un ManualResetEvent
  • si les délais d'attente, puis annuler la lecture

Si la lecture renvoie des données, définissez l'événement et votre thread principal continuera, sinon vous continuerez après le délai d'expiration.




Im mon cas ce travail 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;
}



Je pense que vous devrez créer un thread secondaire et interroger une clé sur la console. Je ne connais aucun moyen construit pour accomplir cela.




.NET 4 rend cela incroyablement simple en utilisant les tâches.

D'abord, construisez votre assistant:

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

Deuxièmement, exécutez avec une tâche et attendez:

      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

Il n'y a aucune tentative de recréer la fonctionnalité ReadLine ou d'effectuer d'autres hacks périlleux pour que cela fonctionne. Les tâches nous permettent de résoudre la question d'une manière très naturelle.




Je lis peut-être trop dans la question, mais je suppose que l'attente serait similaire au menu de démarrage où il attend 15 secondes à moins que vous n'appuyiez sur une touche. Vous pouvez utiliser (1) une fonction de blocage ou (2) vous pouvez utiliser un thread, un événement et un timer. L'événement agirait comme un «continue» et bloquerait jusqu'à ce que le temporisateur expire ou qu'une touche ait été enfoncée.

Pseudo-code pour (1) serait:

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



Voici une solution qui utilise Console.KeyAvailable . Ce sont des appels bloquants, mais il devrait être assez trivial de les appeler de manière asynchrone via le TPL si vous le souhaitez. J'ai utilisé les mécanismes d'annulation standard pour le rendre facile à connecter avec le modèle asynchrone des tâches et toutes ces bonnes choses.

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

Il y a quelques inconvénients avec ceci.

  • Vous n'obtenez pas les fonctions de navigation standard ReadLine par ReadLine (défilement vers le haut / bas, etc.).
  • Ceci injecte des caractères '\ 0' en entrée si une touche spéciale est pressée (F1, PrtScn, etc.). Vous pouvez facilement les filtrer en modifiant le code.



J'ai lutté avec ce problème pendant 5 mois avant de trouver une solution qui fonctionne parfaitement dans un environnement d'entreprise.

Le problème avec la plupart des solutions à ce jour est qu'elles reposent sur autre chose que Console.ReadLine (), et Console.ReadLine () a beaucoup d'avantages:

  • Prise en charge de la suppression, du retour arrière, des touches fléchées, etc.
  • La possibilité d'appuyer sur la touche «haut» et de répéter la dernière commande (cela est très pratique si vous implémentez une console de débogage en arrière-plan qui est très utile).

Ma solution est la suivante:

  1. Générer un thread distinct pour gérer l'entrée de l'utilisateur à l'aide de Console.ReadLine ().
  2. Après le délai d'expiration, débloquez Console.ReadLine () en envoyant une clé [enter] dans la fenêtre de la console en cours, en utilisant http://inputsimulator.codeplex.com/ .

Exemple de code:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Plus d'informations sur cette technique, y compris la technique correcte pour abandonner un thread qui utilise Console.ReadLine:

Appel .NET pour envoyer [entrer] une séquence de touches dans le processus en cours, qui est une application de console?

Comment annuler un autre thread dans .NET, lorsque ce thread exécute Console.ReadLine?




Exemple de mise en œuvre de la publication d'Eric ci-dessus. Cet exemple particulier a été utilisé pour lire des informations qui ont été transmises à une application de console via un tuyau:

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



Links