-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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
Skip DECPS/MIDI output on Ctrl+C/Break #14214
Conversation
6841ae1
to
fc3d134
Compare
if (_skip.is_signaled()) | ||
{ | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the new trick.
if (_hwnd != windowHandle) | ||
{ | ||
throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{}; | ||
_initialize(windowHandle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll explain reason for this change below in ControlCore.
if (ch == L'\x3') // Ctrl+C or Ctrl+Break | ||
{ | ||
_handleControlC(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure where to put this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... You want this behavior to be a part of the terminal handling it right? Like, if a user binds an action to ctrl+c, the action should be executed and this new behavior shouldn't happen right?
If that's the case, I suggest taking a look at ControlCore::TrySendKeyEvent()
. By that point, we know that the input wasn't handled by an action so we can intercept it right before it goes to the shell (NOTE: this is where we have logic to update the selection from the keyboard when not in mark mode).
{ | ||
if (!_midiAudioSkipTimer) | ||
{ | ||
_midiAudioSkipTimer = _dispatcher.CreateTimer(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the best way to create timers in Windows Terminal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, yeah; we can audit dispatcher use later. Is it acceptable to Start()
a timer multiple times? If the user really wails on ^C
for example... which they assuredly will.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation isn't particularly great
DispatcherQueueTimer.Start Method
Definition
Starts the timer.
However wading through the underlying code for this, the timer is canceled before it gets restarted.
// Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song. | ||
_midiAudio.BeginSkip(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what required larger changes to MidiAudio
:
On shutdown we have to tell the VT output thread to skip all DECPS/MIDI sequences. _getMidiAudio
uses no mutex to allow thread-safe access of _midiAudio
from multiple threads. But luckily we can either rely on how it's only called when the console lock is held, or we simply add another mutex there.
But both options are bad IMO: We had a long history of deadlock in Windows Terminal due to the main thread acquiring the console lock. And technically we don't need one, since all we want to do is call BeginSkip
which is already inherently thread-safe. I simply dropped the lazy creation of MidiAudio
which now allows me to blindly call BeginSkip
witout if conditions and checks. I think this makes the code simpler and more robust. But it requires us to pass the HWND
on all calls now for the internal lazy creation of DirectSound types.
Another downside is that I had to sprinkle the DirectSound headers into a bunch of .cpp files so that the destructor compiles without "undefined type" errors. I tried declaring a custom MidiAudio::~MidiAudio
destructor, but that didn't make the compiler treat the destructor as an "extern" symbol either. I don't get why std::unique_ptr
allows such type erasure where the compiler doesn't need to know the destructor definition, but Microsoft::Wrl
and wil::com_ptr
can't do that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is OK for now. We can reevalute the header situation in the future. :)
@j4james FYI This might seem like a weird thing to do, but we got a report of the potential for DECPS blocking to be used as part of deceiving users into thinking that their session has hung (and they can't get out of it.) |
OK I'll try and take a look at this over the weekend - it's going to take me a while to figure out how it works. It doesn't seem obvious to me at first glance how you can get away without locking the But I do like the idea of being able to stop the sound with Ctrl+C. |
BTW the original issue mentions:
Unfortunately I don't think it was ever specifically mentioned how the VT520 behaved. Did it block IO until a control sequence finished playing or did it allow up to 16 notes to be queued up in a small circular buffer? |
As far we could establish on the VT525 that jerch tested, that 16 note buffer was not a thing. The These details were uncovered in the comment thread here: jerch/xterm.js#1 (comment). It's really just the three messages at that point in the thread that are relevant to |
I've had a chance to play around with this PR, and try and figure out how it works without locking, i.e. what is keeping the When I open a tab, I see the constructors being called, but when I close a tab, nothing. Keep opening and closing tabs - lots of construction, but no destruction. So I close the whole terminal and suddenly there's a long string of destructor calls to free up all those tabs that had previously been used. So it seems the trick is that we're just not releasing any memory until the whole app is closed? That's surely not intentional is it? If anyone is using the terminal for an extended period of time it's just going to keep using more and more memory. Or am I misunderstanding something here? And in the case of conhost, nothing ever gets destroyed as far I can tell. I suppose that is OK when the whole app is exiting anyway, but I'd like to know if that was an intentional optimization rather than a bug that we're just ignoring. Anyway I'm assuming this change in behavior was introduced at some point in the past, so it's not technically a problem with this PR, but I'm not certain this PR would work without that change being in place. |
It was "intentional". We use terminal/src/interactivity/win32/window.cpp Lines 68 to 81 in 1f19ed0
|
OK, with PR #14228 applied, I've taken another look at this, and I think I understand how it works now. In case anyone else shared my confusion, the reason we don't need a lock is because the With that understood, and the understanding that conhost is just going to exit immediately, I'm happy with this approach. I suspect there might be weird edge cases where we could get a bit of a delay in closing, but I think the simpler implementation more than makes up for that. Also I love the Ctrl+C functionality. I have a few minor nits on the PR which I'll comment on later, but I don't think there's anything blocking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. Just some comment updates, and clean up of obsolete code. I don't like the MidiDuration
thing, but feel free to ignore that feedback if you disagree.
src/audio/midi/MidiAudio.cpp
Outdated
// By waiting on the shutdown future with the duration of the note, we'll | ||
// either be paused for the appropriate amount of time, or we'll break out | ||
// of the wait early if we've been shutdown. | ||
_shutdownFuture.wait_for(duration); | ||
_skip.wait(::base::saturated_cast<DWORD>(duration.count())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment could do with an update. Maybe something like "By waiting on the skip event...", and possibly also mention that it applies to both shutdown and skipping notes.
// We create the audio instance on demand, and lock it for the duration | ||
// of the note output so it can't be destroyed while in use. | ||
auto& midiAudio = _getMidiAudio(); | ||
midiAudio.Lock(); | ||
|
||
// We then unlock the terminal, so the UI doesn't hang while we're busy. | ||
auto& terminalLock = _terminal->GetReadWriteLock(); | ||
terminalLock.unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the code above removed, this comment doesn't make sense. Maybe just need to drop the "then".
// Once complete, we reacquire the terminal lock and unlock the audio. | ||
// If the terminal has shutdown in the meantime, the Unlock call | ||
// will throw an exception, forcing the thread to exit ASAP. | ||
terminalLock.lock(); | ||
midiAudio.Unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again this comment doesn't make sense with the Unlock code removed. Maybe just drop everything after "reacquire the terminal lock".
src/host/outputStream.cpp
Outdated
// We create the audio instance on demand, and lock it for the duration | ||
// of the note output so it can't be destroyed while in use. | ||
auto& midiAudio = ServiceLocator::LocateGlobals().getConsoleInformation().GetMidiAudio(); | ||
midiAudio.Lock(); | ||
|
||
// We then unlock the console, so the UI doesn't hang while we're busy. | ||
UnlockConsole(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same deal as the comments in ControlCore::_terminalPlayMidiNote
. Just drop the "then".
src/host/outputStream.cpp
Outdated
// Once complete, we reacquire the console lock and unlock the audio. | ||
// If the console has shutdown in the meantime, the Unlock call | ||
// will throw an exception, forcing the thread to exit ASAP. | ||
LockConsole(); | ||
midiAudio.Unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as ControlCore::_terminalPlayMidiNote
again. Just drop everything after "reacquire the console lock".
{ | ||
throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we aren't using this exception anymore, we should get rid of the definition in statemachine.hpp
, and the other bits of code associated with it that were added in PR #13208. There was a catch
that was added to ControlCore::_connectionOutputHandler
and another in StateMachine::_SafeExecute
. You may also need to add back a bunch of noexcepts in the StateMachine
class if the audit complains.
std::unique_ptr<MidiAudio> _midiAudio; | ||
MidiAudio _midiAudio; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious: why? Don't we want this to be a unique_ptr to simplify destruction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I removed the unique_ptr
to simplify destruction. The reasoning behind this is annoying and difficult and explained in detail in this comment: #14214 (comment)
But the gist of it is: In order to correctly shut down without race conditions we either need to protect access to _midiAudio
with a mutex or remove the unique_ptr
. The latter is the safer alternative.
if (ch == L'\x3') // Ctrl+C or Ctrl+Break | ||
{ | ||
_handleControlC(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... You want this behavior to be a part of the terminal handling it right? Like, if a user binds an action to ctrl+c, the action should be executed and this new behavior shouldn't happen right?
If that's the case, I suggest taking a look at ControlCore::TrySendKeyEvent()
. By that point, we know that the input wasn't handled by an action so we can intercept it right before it goes to the shell (NOTE: this is where we have logic to update the selection from the keyboard when not in mark mode).
Yeah, unfortunately this is correct. It should be overridable by an action just like actual |
@DHowett @carlos-zamora Since a handled action returns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm cool with this. Thank you!
auto& terminalLock = _terminal->GetReadWriteLock(); | ||
terminalLock.unlock(); | ||
|
||
// This call will block for the duration, unless shutdown early. | ||
midiAudio.PlayNote(noteNumber, velocity, duration); | ||
_midiAudio.PlayNote(reinterpret_cast<HWND>(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _owningHwnd
will change at runtime in the future; is MidiAudio prepared to handle that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The device will be reinitialized if the HWND changes. But we don't expect this to happen too often right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, we don't!
// Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song. | ||
_midiAudio.BeginSkip(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is OK for now. We can reevalute the header situation in the future. :)
@msftbot merge this in 15 minutes |
Hello @DHowett! Because you've given me some instructions on how to help merge this pull request, I'll be modifying my merge approach. Here's how I understand your requirements for merging this pull request:
If this doesn't seem right to you, you can tell me to cancel these instructions and use the auto-merge policy that has been configured for this repository. Try telling me "forget everything I just told you". |
I'm admin-merging with one Team sign-off since @j4james is the local area expert & I trust his review here 😄 Thanks! |
@@ -512,8 +512,6 @@ void CloseConsoleProcessState() | |||
|
|||
HandleCtrlEvent(CTRL_CLOSE_EVENT); | |||
|
|||
gci.ShutdownMidiAudio(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, reading another PR, I just realized that this change isn't quite correct. This should still call midiAudio.BeginSkip()
, just like ControlCore
does on shutdown.
Silent MIDI notes can be used to seemingly deny a user's input for long durations (multiple minutes). This commit improves the situation by ignoring all DECPS sequences for a second when Ctrl+C/Ctrl+Break is pressed. Additionally it fixes a regression introduced in 666c446: When we close a tab we need to unblock/shutdown `MidiAudio` early, so that `ConptyConnection::Close()` can run down as fast as possible. * In pwsh in Windows Terminal 1.16 run ``while ($True) { echo "`e[3;8;3,~" }`` * Ctrl+C doesn't do anything ❎ * Closing the tab doesn't do anything ❎ * With these modifications in Windows Terminal: * Ctrl+C stops the output ✅ * Closing the tab completes instantly ✅ * With these modifications in OpenConsole: * Ctrl+C stops the output ✅ * Closing the window completes instantly ✅ (cherry picked from commit b4fce27) Service-Card-Id: 86518584 Service-Version: 1.15
Silent MIDI notes can be used to seemingly deny a user's input for long durations (multiple minutes). This commit improves the situation by ignoring all DECPS sequences for a second when Ctrl+C/Ctrl+Break is pressed. Additionally it fixes a regression introduced in 666c446: When we close a tab we need to unblock/shutdown `MidiAudio` early, so that `ConptyConnection::Close()` can run down as fast as possible. * In pwsh in Windows Terminal 1.16 run ``while ($True) { echo "`e[3;8;3,~" }`` * Ctrl+C doesn't do anything ❎ * Closing the tab doesn't do anything ❎ * With these modifications in Windows Terminal: * Ctrl+C stops the output ✅ * Closing the tab completes instantly ✅ * With these modifications in OpenConsole: * Ctrl+C stops the output ✅ * Closing the window completes instantly ✅ (cherry picked from commit b4fce27) Service-Card-Id: 86518585 Service-Version: 1.16
🎉 Handy links: |
🎉 Handy links: |
Silent MIDI notes can be used to seemingly deny a user's input for long
durations (multiple minutes). This commit improves the situation by ignoring
all DECPS sequences for a second when Ctrl+C/Ctrl+Break is pressed.
Additionally it fixes a regression introduced in 666c446:
When we close a tab we need to unblock/shutdown
MidiAudio
early,so that
ConptyConnection::Close()
can run down as fast as possible.Validation Steps Performed
while ($True) { echo "`e[3;8;3,~" }