Looking for a better way to cancel on keypress from the console

I'm using the following code to trigger a cancellation token when the user presses a key. I had to use a thread, because some of these operations are long running, so I can't just check for keypresses between async calls. I feel like there has to be a better way than spawning a thread, but it's just not coming to me.
static void MonitorThreadProc(object state)
{
var cts = (CancellationTokenSource)state;
while(!Console.KeyAvailable)
{
Thread.Sleep(10);
}
cts.Cancel();
}
static async Task<int> Main(string[] args)
{
if (args.Length < 2)
{
return -1;
}
var port = args[0];
var path = args[1];
try
{
using (var link = new EspLink(port))
{
var cts = new CancellationTokenSource();
var mon = new Thread(new ParameterizedThreadStart(MonitorThreadProc));
mon.Start(cts);

var tok = cts.Token;
Console.WriteLine("Press any key to cancel...");
Console.Write("Connecting...");
await Console.Out.FlushAsync();
await link.ConnectAsync(true, 3, true, tok, link.DefaultTimeout, new EspProgress());
Console.WriteLine("\bdone!");
await Console.Out.FlushAsync();
... // complete code is here https://pastebin.com/Cpu18pYm
mon.Abort();
}
return 0;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation canceled by user. Device may be in invalid state.");
return 1;
}
}
static void MonitorThreadProc(object state)
{
var cts = (CancellationTokenSource)state;
while(!Console.KeyAvailable)
{
Thread.Sleep(10);
}
cts.Cancel();
}
static async Task<int> Main(string[] args)
{
if (args.Length < 2)
{
return -1;
}
var port = args[0];
var path = args[1];
try
{
using (var link = new EspLink(port))
{
var cts = new CancellationTokenSource();
var mon = new Thread(new ParameterizedThreadStart(MonitorThreadProc));
mon.Start(cts);

var tok = cts.Token;
Console.WriteLine("Press any key to cancel...");
Console.Write("Connecting...");
await Console.Out.FlushAsync();
await link.ConnectAsync(true, 3, true, tok, link.DefaultTimeout, new EspProgress());
Console.WriteLine("\bdone!");
await Console.Out.FlushAsync();
... // complete code is here https://pastebin.com/Cpu18pYm
mon.Abort();
}
return 0;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation canceled by user. Device may be in invalid state.");
return 1;
}
}
That's an excerpt. The whole code is here: https://pastebin.com/Cpu18pYm
Pastebin
C# monitor keypress console - Pastebin.com
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
24 Replies
honey the codewitch
adding. i tried using the blocking Read() call to do this but i ran into other complications - like not being able to exit the process cleanly i think i just thought of a way by manually polling the tasks for completion and checking for keystrokes during the poll rather than using await, but that seems messy
maxmahem
maxmahem4d ago
give me a sec.
jcotton42
jcotton424d ago
Console.CancelKeyPress Event (System)
Occurs when the Control modifier key (Ctrl) and either the C console key (C) or the Break key are pressed simultaneously (Ctrl+C or Ctrl+Break).
honey the codewitch
i'm not trying to handle the Ctrl+C event I'm trying to break on an arbitrary keypress in this case
jcotton42
jcotton424d ago
Sure. But it's a lot simpler, and it also makes it harder to accidentally cancel.
honey the codewitch
yeah, well consider this a training exercise in using tasks and cancelation tokens - at least that's what I'm treating it as there are a lot of situations where I could end up in a similar predicament, and I'd love to know the best way to handle it, because this isn't it
jcotton42
jcotton424d ago
fair enough
honey the codewitch
the thing is, await is what's really hamstringing me here. If I was just polling the task for completion in a loop, I could check for keypress in the polling loop and set the cancelation token. i want something as efficient, but cleaner than that if such a thing exists
maxmahem
maxmahem4d ago
okay this was way longer then necessary because I did it withhout reactive.linq but... https://paste.mod.gg/dvfzwhdjpopo/0
BlazeBin - dvfzwhdjpopo
A tool for sharing your source code with the world!
maxmahem
maxmahem4d ago
the essence is there is an observable that polls the keyboard for an key, and emits an event if any key is pressed and there is an observer that triggers a cancelation when it sees any event I think reactive has some stuff built in that does just this, but I thought I'd try writing it from scratch.
230V
230V4d ago
Maybe handling an event instead of polling is possible here
A process can specify a console input buffer handle in one of the wait functions to determine when there is unread console input
You could see if the stream returned by Console.OpenStandardInput gets only chars or actual keys, streams let you await when reading Nope, that's not for handling keys
maxmahem
maxmahem4d ago
awaiting the input stream is still a decent idea though.
honey the codewitch
actually awaiting the input stream is more ideal for my scenario. that's a good idea only thing about that @maxmahem is it's doing what i'm doing, but with extra steps i'd almost rather avoid Task.Run as much as new Thread, and maybe ditch await, and poll the tasks for completion to avoid spinning up another thread
maxmahem
maxmahem4d ago
I know my solution looks like a lot, but thats partially because I did it all from scratch. On the consuming end it can just look like:
var cts = new CancellationTokenSource();
var keyPressObservable = new AnyKeyObservable();
var subscription = keyPressObservable.Subscribe(cts.ToCancelObserver());
var cts = new CancellationTokenSource();
var keyPressObservable = new AnyKeyObservable();
var subscription = keyPressObservable.Subscribe(cts.ToCancelObserver());
or really...
var cts = new CancellationTokenSource();
var subscription = new AnyKeyObservable().Subscribe(cts.ToCancelObserver());
var cts = new CancellationTokenSource();
var subscription = new AnyKeyObservable().Subscribe(cts.ToCancelObserver());
honey the codewitch
my concern here is partly efficiency. anyone can turn a poll into an event by spinning a thread, but is that really necessary in this case? I don't think it is
maxmahem
maxmahem4d ago
well the issue is in this case you are somewhat limited by the interface Console exposes.
honey the codewitch
basically as i said to someone else above, it's really await that hamstrings me here if i wasn't using await i could poll using while(!Console.KeyAvailable && !task.IsCompleted) or whatever the only thing I don't like about that is the mess i wonder can i wrap a task using another TaskCompletionSource<> and add a Console.KeyAvailable poll in there somehow? hmmm
maxmahem
maxmahem4d ago
well, my solution at least lets you shove the waiting logic elsewhere. You could also do more sophisticated things with the key stream if you wanted.
honey the codewitch
yeah your solution is flexible, but it's also a bit heavy handed for just monitoring a keypress. like my current solution is it smells bad to me to spin another thread when i'm waiting on the primary thread doing nothing when i could be polling on the primary thread i was just hoping there was an elegant, efficient, Task based solution to situations like this
maxmahem
maxmahem4d ago
doooh. Tried an await stream solution.. big road block, lol. The stream only sends the next line when you hit enter. Doh. I knew that but forgot. yeah... the interface here sucks. Just leaves polling really, as all the other methods are blocking with no way to cancel.
maxmahem
maxmahem4d ago
also, lol:
No description
230V
230V4d ago
Sadly the .net Console doesn't expose any of the general event handling stuff the winapi console has. It looks definitely possible to adapt a "waitable" handle like the one from GetStdHandle(in) to the async model, but the WaitHandle type is a mystery to me (actually not sure if "console input buffer handle" is the standard input one or a different one from CreateConsoleScreenBuffer, but this doesn't have any other implications)
honey the codewitch
The whole serial port business is a mess too. I wish .NET was more consistent about its asynchronous APIs
maxmahem
maxmahem4d ago
Hmm... when canceling/unsubscribing/disposing of this poll, I wonder if I should wait the task? Just to ensure that the task is completed before I return control.
void PollConsole(IObserver<ConsoleKeyInfo> observer, CancellationToken token)
{
try {
while (!token.IsCancellationRequested) {
while (Console.KeyAvailable) {
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
observer.OnNext(keyInfo);
}
Thread.Sleep(this.pollingInterval);
}
}
catch (Exception error) { observer.OnError(error); }
}
void PollConsole(IObserver<ConsoleKeyInfo> observer, CancellationToken token)
{
try {
while (!token.IsCancellationRequested) {
while (Console.KeyAvailable) {
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
observer.OnNext(keyInfo);
}
Thread.Sleep(this.pollingInterval);
}
}
catch (Exception error) { observer.OnError(error); }
}
Want results from more Discord servers?
Add your server