Comment ajouter un Timeout à Console.ReadLine ()?

122

J'ai une application 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 doit continuer. Nous supposons qu'un délai d'attente signifie une réponse vide.

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

Larsenal
la source

Réponses:

112

Je suis surpris d'apprendre qu'après 5 ans, toutes les réponses souffrent encore d'un ou plusieurs des problèmes suivants:

  • Une fonction autre que ReadLine est utilisée, entraînant une perte de fonctionnalité. (Effacer / retour arrière / touche haut pour l'entrée précédente).
  • La fonction se comporte mal lorsqu'elle est appelée plusieurs fois (engendrant plusieurs threads, de nombreuses ReadLine suspendues ou un comportement inattendu).
  • La fonction repose sur une attente occupée. Ce qui est un gaspillage horrible car on s'attend à ce que l'attente dure de quelques secondes jusqu'au délai d'expiration, qui peut durer plusieurs minutes. Une attente chargée qui dure pendant une telle quantité de temps est une horrible perte de ressources, ce qui est particulièrement mauvais dans un scénario multithreading. Si l'attente occupée est modifiée avec un sommeil, cela a un effet négatif sur la réactivité, même si j'admets que ce n'est probablement pas un gros problème.

Je pense que ma solution résoudra le problème d'origine sans souffrir d'aucun des problèmes ci-dessus:

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.");
  }
}

L'appel est bien sûr très simple:

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.");
}

Vous pouvez également utiliser la TryXX(out)convention, comme le suggère 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;
  }

Qui s'appelle comme suit:

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

Dans les deux cas, vous ne pouvez pas mélanger les appels Readeravec les Console.ReadLineappels normaux : si le Readerdélai expire, il y aura un ReadLineappel suspendu . Au lieu de cela, si vous souhaitez avoir un ReadLineappel normal (non chronométré) , utilisez simplement le Readeret omettez le délai d'expiration, de sorte qu'il prenne par défaut un délai d'expiration infini.

Alors, qu'en est-il de ces problèmes des autres solutions que j'ai mentionnées?

  • Comme vous pouvez le voir, ReadLine est utilisé, évitant le premier problème.
  • La fonction se comporte correctement lorsqu'elle est appelée plusieurs fois. Indépendamment du fait qu'un délai d'expiration se produise ou non, un seul thread d'arrière-plan sera toujours en cours d'exécution et un seul appel à ReadLine au maximum sera actif. L'appel de la fonction entraînera toujours la dernière entrée, ou un délai d'expiration, et l'utilisateur n'aura pas à appuyer sur Entrée plus d'une fois pour soumettre son entrée.
  • Et, évidemment, la fonction ne repose pas sur une attente occupée. Au lieu de cela, il utilise des techniques de multithreading appropriées pour éviter de gaspiller des ressources.

Le seul problème que je prévois avec cette solution est qu'elle n'est pas thread-safe. Cependant, plusieurs threads ne peuvent pas vraiment demander à l'utilisateur une entrée en même temps, donc la synchronisation doit avoir lieu avant de passer un appel de Reader.ReadLinetoute façon.

JSQuareD
la source
1
J'ai eu une NullReferenceException à la suite de ce code. Je pense que je pourrais corriger le démarrage du thread une fois que les événements automatiques sont créés.
Augusto Pedraza
1
@JSQuareD Je ne pense pas qu'une attente occupée avec un Sleep (200ms) soit autant de chose horrible waste, mais bien sûr, votre signalisation est supérieure. De plus, l'utilisation d'un Console.ReadLineappel de blocage dans une boucle infinie dans une deuxième menace évite les problèmes liés à de nombreux appels de ce type qui traînent en arrière-plan comme dans les autres solutions fortement votées ci-dessous. Merci d'avoir partagé votre code. +1
Roland
2
Si vous ne parvenez pas à entrer à temps, cette méthode semble interrompre lors du premier Console.ReadLine()appel suivant que vous effectuez. Vous vous retrouvez avec un «fantôme» ReadLinequi doit d'abord être complété.
Derek
1
@Derek, malheureusement, vous ne pouvez pas mélanger cette méthode avec des appels ReadLine normaux, tous les appels doivent être effectués via le Reader. La solution à ce problème serait d'ajouter une méthode au lecteur qui attend gotInput sans délai. Je suis actuellement sur mobile, donc je ne peux pas l'ajouter à la réponse très facilement.
JSQuareD
1
Je n'en vois pas la nécessité getInput.
silvalli
33
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();
gp.
la source
2
Je ne sais pas pourquoi cela n'a pas été voté - cela fonctionne parfaitement. Beaucoup d'autres solutions impliquent "ReadKey ()", qui ne fonctionne pas correctement: cela signifie que vous perdez toute la puissance de ReadLine (), comme appuyer sur la touche "haut" pour obtenir la commande précédemment tapée, en utilisant le retour arrière et touches fléchées, etc.
Contango
9
@Gravitas: Cela ne fonctionne pas. Eh bien, cela fonctionne une fois. Mais chaque que ReadLinevous appelez reste là en attente d'une entrée. Si vous l'appelez 100 fois, cela crée 100 threads qui ne disparaissent pas jusqu'à ce que vous appuyiez 100 fois sur Entrée!
Gabe
2
Il faut se méfier. Cette solution semble soignée mais je me suis retrouvé avec des milliers d'appels non terminés laissés en suspens. Ne convient donc pas s'il est appelé à plusieurs reprises.
Tom Makin
@Gabe, shakinfree: plusieurs appels n'ont pas été pris en compte pour la solution mais juste un appel asynchrone avec timeout. Je suppose que ce serait déroutant pour l'utilisateur d'avoir 10 messages imprimés sur la console, puis de saisir les entrées pour eux un par un dans l'ordre respectif. Néanmoins, pour les appels suspendus, pourriez-vous essayer de commenter la ligne TimedoutException et retourner une chaîne nulle / vide?
gp.
non ... le problème est que Console.ReadLine bloque toujours le thread du pool de threads qui exécute la méthode Console.ReadLine à partir de ReadLineDelegate.
gp.
27

Cette approche utilisant Console.KeyAvailable aidera-t-elle?

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);
    }
}
Gulzar Nazim
la source
C'est vrai, l'OP semble vouloir un appel bloquant, même si je frémis un peu à l'idée ... C'est probablement une meilleure solution.
GEOCHET
Je suis sûr que vous avez vu cela. Je l'ai
Gulzar Nazim
Je ne vois pas comment ce "timeout" si l'utilisateur ne fait rien. Tout ce que cela ferait serait de continuer à exécuter la logique en arrière-plan jusqu'à ce qu'une touche soit enfoncée et que la logique continue.
mphair
Certes, cela doit être corrigé. Mais il est assez facile d'ajouter le timeout à la condition de boucle.
Jonathan Allen
KeyAvailableindique seulement que l'utilisateur a commencé à taper une entrée dans ReadLine, mais nous avons besoin d'un événement en appuyant sur Entrée, qui oblige ReadLine à revenir. Cette solution ne fonctionne que pour ReadKey, c'est-à-dire obtenir un seul caractère. Comme cela ne résout pas la question réelle de ReadLine, je ne peux pas utiliser votre solution. -1 désolé
Roland
13

Cela a fonctionné pour moi.

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);
user980750
la source
4
Je pense que c'est la meilleure et la plus simple solution utilisant des outils déjà intégrés. Bon travail!
Vippy
2
Belle! La simplicité est vraiment la sophistication ultime. Félicitations!
BrunoSalvino
10

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, appelez-le "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éfinissez evt.
  • appelez la méthode BeginRead de ce flux pour démarrer une opération de lecture asynchrone
  • puis entrez une attente chronométrée sur un ManualResetEvent
  • si l'attente expire, annulez la lecture

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

Eric
la source
C'est plus ou moins ce que fait la solution acceptée.
Roland
9
// 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.
}
Glenn Slayden
la source
8

Je pense que vous devrez créer un fil secondaire et rechercher une clé sur la console. Je ne connais aucun moyen intégré pour y parvenir.

GEOCHET
la source
Oui, si vous avez un deuxième thread d'interrogation pour les clés et que votre application se ferme pendant qu'elle attend, ce thread d'interrogation de clé restera là, attendant, pour toujours.
Kelly Elton
En fait: soit un deuxième thread, soit un délégué avec "BeginInvoke" (qui utilise un thread, dans les coulisses - voir la réponse de @gp).
Contango du
@ kelton52, Le thread secondaire se fermera-t-il si vous terminez le processus dans le Gestionnaire des tâches?
Arlen Beiler
6

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 jusqu'à présent est qu'elles reposent sur autre chose que Console.ReadLine () et Console.ReadLine () présente de nombreux 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 utilisée).

Ma solution est la suivante:

  1. Créez un thread séparé pour gérer l'entrée 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 console actuelle, 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 une touche [entrée] dans le processus en cours, qui est une application console?

Comment abandonner un autre thread dans .NET, lorsque ledit thread exécute Console.ReadLine?

Contango
la source
5

L'appel de Console.ReadLine () dans le délégué est incorrect car si l'utilisateur n'appuie pas sur «entrée», cet appel ne reviendra jamais. Le thread exécutant le délégué sera bloqué jusqu'à ce que l'utilisateur clique sur «entrée», sans moyen de l'annuler.

L'émission d'une séquence de ces appels ne se comportera pas comme prévu. Considérez ce qui suit (en utilisant l'exemple de classe Console ci-dessus):

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

L'utilisateur laisse le délai d'expiration pour la première invite, puis entre une valeur pour la deuxième invite. FirstName et lastName contiendront les valeurs par défaut. Lorsque l'utilisateur clique sur «enter», le premier appel ReadLine se termine, mais le code a abandonné cet appel et a essentiellement rejeté le résultat. Le deuxième appel ReadLine continuera à se bloquer, le délai d'expiration finira par expirer et la valeur renvoyée sera à nouveau la valeur par défaut.

BTW- Il y a un bogue dans le code ci-dessus. En appelant waitHandle.Close (), vous fermez l'événement sous le thread de travail. Si l'utilisateur frappe «entrée» après l'expiration du délai, le thread de travail tentera de signaler l'événement qui lève une ObjectDisposedException. L'exception est levée à partir du thread de travail, et si vous n'avez pas configuré de gestionnaire d'exceptions non géré, votre processus se terminera.

Brannon
la source
1
Le terme «ci-dessus» dans votre message est ambigu et déroutant. Si vous faites référence à une autre réponse, vous devez créer un lien approprié vers cette réponse.
bzlm
5

Si vous êtes dans la Main()méthode, vous ne pouvez pas utiliser await, vous devrez donc utiliser Task.WaitAny():

var task = Task.Factory.StartNew(Console.ReadLine);
var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
    ? task.Result : string.Empty;

Cependant, C # 7.1 introduit la possibilité de créer une Main()méthode asynchrone , il est donc préférable d'utiliser la Task.WhenAny()version chaque fois que vous avez cette option:

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;
kwl
la source
4

Je lis peut-être trop dans la question, mais je suppose que l'attente serait similaire au menu de démarrage où elle 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 une minuterie. L'événement agirait comme un «continuer» et se bloquerait jusqu'à ce que le minuteur expire ou qu'une touche soit enfoncée.

Le 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);
}
Ryan
la source
2

Je ne peux malheureusement pas commenter le post 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();
Jamie Kitson
la source
Notez que vous pouvez également utiliser Console.In.Peek () si la console n'est pas visible (?) Ou si l'entrée est dirigée à partir d'un fichier.
Jamie Kitson
2

EDIT : résolu le problème en effectuant le travail réel dans un processus séparé et en tuant ce processus s'il expire. Voir ci-dessous pour plus de détails. Ouf!

Je viens de faire un tour et cela a semblé bien fonctionner. Mon collègue avait une version qui utilisait un objet Thread, mais je trouve que la méthode BeginInvoke () des types de délégués est un peu plus élégante.

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

Le projet ReadLine.exe est très simple qui a une classe qui ressemble à ceci:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}
Jesse C. Slicer
la source
2
Invoquer un exécutable séparé dans un nouveau processus uniquement pour faire une ReadLine () chronométrée semble être une surpuissance massive. Vous résolvez essentiellement le problème de ne pas pouvoir abandonner un thread bloquant ReadLine () en configurant et en supprimant un processus entier à la place.
bzlm
Alors dites-le à Microsoft, qui nous a mis dans cette position.
Jesse C. Slicer
Microsoft ne vous a pas mis dans cette position. Regardez quelques-unes des autres réponses qui font le même travail en quelques lignes. Je pense que le code ci-dessus devrait obtenir une sorte de récompense - mais pas le genre que vous voulez :)
Contango
1
Non, aucune des autres réponses n'a fait exactement ce que le PO voulait. Tous perdent les fonctionnalités des routines d'entrée standard ou se raccrochent au fait que toutes les demandes Console.ReadLine() sont bloquantes et bloquent l'entrée à la demande suivante. La réponse acceptée est assez proche, mais a encore des limites.
Jesse C. Slicer
1
Non, ce n'est pas le cas. Le tampon d'entrée se bloque toujours (même si le programme ne le fait pas). Essayez-le par vous-même: entrez certains caractères mais n'appuyez pas sur Entrée. Laissez-le expirer. Capturez l'exception dans l'appelant. Puis en avoir un autre ReadLine()dans votre programme après avoir appelé celui-ci. Voyez ce qui se passe. Vous devez appuyer deux fois sur return pour le faire fonctionner en raison de la nature à thread unique du Console. Il. Non. Travail.
Jesse C. Slicer
2

.NET 4 rend cela incroyablement simple à l'aide de tâches.

Tout d'abord, construisez votre assistant:

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

Deuxièmement, exécutez 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 est impossible d'essayer 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 de manière très naturelle.

StevoInco
la source
2

Comme s'il n'y avait pas déjà assez de réponses ici: 0), ce qui suit s'encapsule dans une méthode statique @ la solution de 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");
    }
Nicholas Petersen
la source
1

Exemple de filetage simple pour résoudre ce problème

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

ou une chaîne statique en haut pour obtenir une ligne entière.

mphair
la source
1

Im mon cas, cela fonctionne 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;
}
Sasha
la source
1

N'est-ce pas joli et court?

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

    // Handle keyInfo value here...
}
John Atac
la source
1
Qu'est-ce que SpinWait?
john ktejik
1

Ceci est un exemple plus complet de la solution de Glen Slayden. J'ai eu l'occasion de le faire lors de la création d'un scénario de test pour un autre problème. Il utilise des E / S asynchrones et un événement de réinitialisation manuelle.

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!");
        }
    }
Mikemay
la source
1

Mon code est entièrement basé sur la réponse de l'ami @JSQuareD

Mais j'avais besoin d'utiliser la Stopwatchminuterie parce que lorsque j'ai terminé le programme avec, Console.ReadKey()il attendait toujours Console.ReadLine()et cela a généré un comportement inattendu.

Cela a fonctionné parfaitement pour moi. Conserve la console d'origine.ReadLine ()

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("What is the answer? (5 secs.)");
        try
        {
            var answer = ConsoleReadLine.ReadLine(5000);
            Console.WriteLine("Answer is: {0}", answer);
        }
        catch
        {
            Console.WriteLine("No answer");
        }
        Console.ReadKey();
    }
}

class ConsoleReadLine
{
    private static string inputLast;
    private static Thread inputThread = new Thread(inputThreadAction) { IsBackground = true };
    private static AutoResetEvent inputGet = new AutoResetEvent(false);
    private static AutoResetEvent inputGot = new AutoResetEvent(false);

    static ConsoleReadLine()
    {
        inputThread.Start();
    }

    private static void inputThreadAction()
    {
        while (true)
        {
            inputGet.WaitOne();
            inputLast = Console.ReadLine();
            inputGot.Set();
        }
    }

    // omit the parameter to read a line without a timeout
    public static string ReadLine(int timeout = Timeout.Infinite)
    {
        if (timeout == Timeout.Infinite)
        {
            return Console.ReadLine();
        }
        else
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            while (stopwatch.ElapsedMilliseconds < timeout && !Console.KeyAvailable) ;

            if (Console.KeyAvailable)
            {
                inputGet.Set();
                inputGot.WaitOne();
                return inputLast;
            }
            else
            {
                throw new TimeoutException("User did not provide input within the timelimit.");
            }
        }
    }
}
Sergio Cabral
la source
0

Un autre moyen peu coûteux d'obtenir un deuxième fil est de l'envelopper dans un délégué.

Joël Coehoorn
la source
0

Exemple d'implémentation du post d'Eric ci-dessus. Cet exemple particulier a été utilisé pour lire les informations transmises à une application console via un tube:

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

la source
0
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Notez que si vous suivez la route «Console.ReadKey», vous perdez certaines des fonctionnalités intéressantes de ReadLine, à savoir:

  • 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 utilisée).

Pour ajouter un délai, modifiez la boucle while en conséquence.

Contango
la source
0

Ne me détestez pas pour avoir ajouté une autre solution à la pléthore de réponses existantes! Cela fonctionne pour Console.ReadKey (), mais pourrait facilement être modifié pour fonctionner avec ReadLine (), etc.

Comme les méthodes «Console.Read» bloquent, il est nécessaire de « pousser » le flux StdIn pour annuler la lecture.

Syntaxe d'appel:

ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

Code:

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);
}
David Kirkland
la source
0

Voici une solution qui utilise Console.KeyAvailable. Ce sont des appels bloquants, mais il devrait être assez simple de les appeler de manière asynchrone via le TPL si vous le souhaitez. J'ai utilisé les mécanismes d'annulation standard pour faciliter le câblage avec le modèle asynchrone de 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 à cela.

  • Vous n'obtenez pas les fonctionnalités de navigation standard ReadLinefournies (défilement des flèches haut / bas, etc.).
  • Cela injecte des caractères '\ 0' dans l'entrée si une touche spéciale est pressée (F1, PrtScn, etc.). Vous pouvez facilement les filtrer en modifiant le code.
Brian Gideon
la source
0

Terminé ici parce qu'une question en double a été posée. J'ai proposé la solution suivante qui semble simple. Je suis sûr qu'il a quelques inconvénients que j'ai manqués.

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;
    }
}
Frank Rem
la source
0

Je suis venu à cette réponse et j'ai fini 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);                
        }            
    }
Tono Nam
la source
0

Un exemple simple utilisant 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");
}
cprcrack
la source
Que faire si l'utilisateur appuie sur la touche et la relâche dans les 2000 ms?
Izzy
0

Un code beaucoup plus contemporain et basé sur les tâches ressemblerait à 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();
}
Shonn Lyga
la source
0

J'ai eu une situation unique d'avoir une application Windows (service Windows). Lors de l'exécution du programme de manière interactive Environment.IsInteractive(VS Debugger ou à partir de cmd.exe), j'ai utilisé AttachConsole / AllocConsole pour obtenir mon stdin / stdout. Pour empêcher le processus de se terminer pendant le travail, le thread d'interface utilisateur appelle Console.ReadKey(false). Je voulais annuler l'attente que le thread de l'interface utilisateur faisait à partir d'un autre thread, j'ai donc proposé une modification de la solution par @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();
  }    
}
JJS
la source