-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Need a reasonable way to either cancel ReadConsole
or poll for text input
#12143
Comments
In Windows 8+, It would be a good thing to properly support cancelling I/O requests in the console host, if the design permits it. This may require changes to the console device driver, condrv.sys, if it's currently not canceling the associated IOCTL request in the console. I haven't checked that. Note that Since Windows 8,
The system wait routines support waiting on file objects. It's generally only meaningful in user mode when waiting for I/O completion with an asynchronous-mode file. If I/O requests never overlap, it's safe to wait for I/O completion by waiting on the file object itself. (It's recommended to use events, APCs, or a completion port instead of waiting on the file object, since generally there will be multiple pending I/O requests.) Either end of a named pipe can be opened in asynchronous mode by passing the flag Console input files are an exception to this rule. As background, note that the File type can reuse the
The most noise is from mouse events, which can be disabled. But you'll still have menu, focus, and window size events in the input buffer that have to be ignored. A high-level
This mirrors |
Off-topic comment: The library's The cancel event is initially ignored by the root process of a new group. This state gets inherited by child processes. The cancel event can be enabled manually via For console apps, the nearest equivalent to a POSIX process group is to spawn the root process of a new group with |
Even if entirely discarding these, the primary challenge seemed to be interpreting See vezel-dev/cathode#61 for some background.
Originally, it was just used to synthesize signals for the current process. That should be roughly equivalent on Windows and Unix. I later used it for sending signals to child processes which is indeed where it becomes problematic. I've been meaning to remove that method for this exact reason.
This particular issue was addressed in vezel-dev/cathode@db81ecf. |
That's why I suggested the addition of Note that if line-input mode is enabled and there's no line ending in the input buffer, Note that the classic console is not "CMD". That's a CLI shell that uses standard I/O. Windows Terminal ships with a more up to date version of the console host named "openconsole", in place of the system "conhost". So sometimes behavior differs due to the version, but it's mostly down to classic vs pseudoconsole mode, and whether virtual-terminal mode is enabled.
In POSIX, pid 0 includes all processes in the caller's process group. In Windows, group 0 includes all processes in the console session. Sending a signal to just the current process is possible in POSIX, but nothing like that is possible for Ctrl+C or Ctrl+Break in Windows. Calling
That update mistakenly removed |
As far as I think any |
Well, to be clear, ideally I don't even want to have to deal with Note that when I say "polling" here, I mean The problem with this approach is that, on Windows, It's probably not acceptable to change
Right, that's why
This is why I'll likely end up removing that
Good catch! Will fix that.
In principle I agree completely and this is what I wanted to do originally. Unfortunately, an (IMO) poor decision was made during the design of the .NET 6 |
Their choices aren't bad. Mapping Mapping
|
Ping - any chance this can get triaged? It is still a major roadblock in supporting sane input cancellation on Windows. |
I know that this is a long shot, but I was just reading the initial request (sorry we've left this one quiet for so long) and something caught my eye:
We recently documented an API that should have been public from the get-go, I think you might be able to get what you're looking for by using It is supported all the way down to Windows 7. [1] In fact, Read and Peek just call ReadEx internally; Read passes no flags, and Peek passes |
@DHowett Hmm, at first glance, I don't think this will solve the problem. It seems to me that this isn't meaningfully any different from using It would also still require me to interpret the key events returned and pray that I can figure out how to determine if the key events in the buffer correspond to what |
Just checking in here again. Is there any hope of making progress in this area? This issue (AFAIK) remains a hard blocker for reasonable input cancellation support in Windows terminal apps. |
It's January again (🎆) and I once again find myself thinking about this issue. (Still kinda shocked that I seem to be the only(?) person running into this limitation. 👀) Changing console APIs is apparently a tough sell, judging by the megathread this is included in, so I spent some more time scouring the internet for ideas. I came across this relatively recent answer on SO: https://stackoverflow.com/a/70557694 My gut reaction was "that's kinda whack", but then, thinking about it... my library does input reading under a lock, so the likelihood of a caller discovering that the handle was briefly closed and reopened is very minimal. It can basically only happen if another thread checks the handle's Any other downsides I ought to consider for this approach? |
I can't immediately figure out how Supporting proper cancellation within conhost would definitely be nice and not that difficult to implement. But I'm not sure how to make the necessary changes to condrv and I'm not sure there are any others either who'd readily be able to make such a change. For me personally this definitely falls under the category of "what I'd do if I could clone myself". Alas. 🥲 |
Well, I figure that input cancellation is a rare enough event that this probably doesn't matter in practice. At least the handles go away when the process exits. At any rate, it still seems better than the status quo, which is that my library supports input cancellation on Unix but on Windows it just does nothing, despite the API appearing fully cancellation-aware. Anyhow, I went and tested this approach yesterday. Unfortunately, my first attempt was unsuccessful. I do have some more ideas - namely, dropping down to |
If you need more low-level details on how connections are set up I've recently explored this more and figured out how to spawn conhost manually, create connections and how to switch connections arbitrarily: https://github.com/microsoft/terminal/blob/dev/lhecker/ConsoleBench/src/tools/ConsoleBench/conhost.cpp#L9-L155 |
@alexrp Could you use the method described here? https://www.meziantou.net/cancelling-console-read.htm I just tested it quickly now on Windows 11 on .NET 8, and it seems to work fine in Windows Terminal at least on this setup. |
I recall having tried all the Win32 I/O cancellation functions in the past and all of them having undesirable results. I think that was on Windows 10, though. I'll give it another shot on Windows 11 and report back. |
@alexrp OOC, as things stand right now, what actually happens in cathode on Windows when cancellation is requested on the token provided to the |
@DaRosenberg nothing currently. On Windows, there's only an upfront cancellation check before the read, because none of the options I tried back then worked. So the driver really just ends up calling |
|
Completely ignore the previous comment. I had a bug where I was reusing a It.... actually seems to work, even with repeated cancellations. I'm now super curious if something changed in Windows somewhere along the lines, or if my testing 2 years ago was just flawed. 🤔 Either way, thanks a ton for pointing me in the right direction, @DaRosenberg! I believe we can close this, then. I don't immediately see anything about the |
Glad I could help @alexrp! OT: The reason I started looking into this last night and eventually found my way to this thread is - surprise - I need this functionality in order to implement a continuously updateable prompt in a terminal app we're working on, and all my efforts to solve this using the APIs and streams provided by So now I am pondering whether it would be a good idea for us to move away from
|
I posted on vezel-dev/cathode#63 (comment) looking for people to test it a bit. If no issues present themselves within a few days, I'll probably just go ahead and push a release.
Unfortunately, not yet. vezel-dev/cathode#83 tracks actually writing some proper conceptual docs; I just need to get around to actually doing it. But, for the most part, the API is very obvious if you just IntelliSense your way through. Most of it mirrors Couple of caveats before you commit to using Cathode:
If these caveats aren't blockers for you, then Cathode will probably be a good fit. |
I spoke too soon, unfortunately. 😢 Cooked input does not cancel properly with (Still, as far as Cathode is concerned, shipping support for raw input cancellation is better than nothing.) |
@alexrp I'm not quite sure what you mean by cooked vs. raw line input, but today we are basically doing this when reading line input (in order to support masking of secrets and some form of user cancellation): while (true)
{
var key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter)
{
if (!isRequired || chars.Count > 0)
{
var responseText = new string(chars.Reverse().ToArray());
if (typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(responseText, typeof(T), CultureInfo.InvariantCulture);
}
if (TryParse(responseText, out T response))
{
return response;
}
}
}
else if (key.Key == ConsoleKey.Escape && !isRequired)
{
return default;
}
else if (key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.C)
{
throw new OperationCanceledException();
}
else if (key.Key == ConsoleKey.Backspace && chars.Count > 0)
{
chars.Pop();
PrintToError("\b \b"); // VT100 sequence for an erasing backspace...
}
else if (!Char.IsControl(key.KeyChar))
{
chars.Push(key.KeyChar);
var charToPrint = isSecret ? '*' : key.KeyChar;
PrintToError(charToPrint.ToString(CultureInfo.CurrentCulture), color: SemanticColor.UserAction);
}
} If we can simply migrate this approach to Cathode, I would be happy with that. The additional native DLL I also cannot see any problem with. However, I just noticed this: Can you elaborate a bit on what the issue is there? Does it completely prevent us from using Cathode on any MUSL-based OS, or is the issue limited to some specific functionality? It might be an issue as we do support Alpine and we also provide MUSL-based Docker images. .. |
You would basically have to do similar stuff with Cathode, but the downside (due to the missing parser) is that you don't have the convenience of In terminal lingo, 'cooked mode' basically refers to the mode that programs operate in by default, i.e. On the other hand, 'raw mode' is where you're in charge of everything:
(Technically, you get these sequences in cooked mode too, but in practice, it's very unusual for cooked mode programs to handle them.) There's more nuance to it than that, but that's the gist. You trade complexity for much greater control and flexibility (some things can't really be done sensibly unless you're in raw mode). Most non-trivial terminal apps (shells, editors, etc) use some flavor of raw mode.
When targeting musl-based distros (such as Alpine), Zig isn't properly linking to musl's
I don't actually know for sure, but I would expect so. I don't have an Alpine system on hand to test with. Generally speaking, in Unix land, it's not expected to have multiple copies of libc in the same process. This is in contrast with Windows where it's fairly commonplace to link MSVCRT statically into DLLs. |
And I spoke too soon again. It only sort of works in raw mode, as we discovered in vezel-dev/cathode#165. The next input byte will just be dropped on the floor after a I am officially giving up trying to work around this issue; it's a bug farm. |
In my library I expose a terminal input API that looks like this:
On Unix, implementing cancellation support was quite easy:
CancellationToken
is triggered, write a dummy value to the write end of the pipe.ReadLineAsync
, poll for input on the read end of the pipe andstdin
.OperationCanceledException
.stdin
, actually read the data and return it.(For reference, the implementation of the above can be found here and here.)
I would now like to implement something similar for Windows. Unfortunately, as far as I can tell, this is bordering on impossible at the moment.
I naïvely tried to replicate the approach above on Windows, only to discover that
WaitForMultipleObjects
does not support polling on pipes. I replaced the pipe with an event sinceWaitForMultipleObjects
supports those. I then ran into another problem: Polling on a console input handle will return when anyINPUT_RECORD
arrives, not justKEY_EVENT_RECORD
s. I decided to tryCancelIo
andCancelSynchronousIo
on the off chance that they'd work instead of polling, but they just don't work on console input, apparently. At that point, things got hairy; I went back to the polling approach and tried to inspect the input buffer looking forKEY_EVENT_RECORD
s specifically, discarding other event records, and then resuming the wait if there are noKEY_EVENT_RECORD
s. Besides being very gross and hacky, this fell apart quickly as it turns out there's no clean way to figure out if a givenKEY_EVENT_RECORD
(or a series of them) will actually result in text input being available on the nextReadConsole
call. Worse yet, even when I did hack together some heuristics, the behavior turned out to be different between CMD and Windows Terminal. At that point, I gave up and ripped out Windows cancellation support.So I suppose this issue boils down to me asking: Is there a way to achieve what I'm trying to do that I just haven't realized yet? If not, could one be implemented?
The text was updated successfully, but these errors were encountered: