Skip DECPS/MIDI output on Ctrl+C/Break (#14214)

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
* 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 
This commit is contained in:
Leonard Hecker 2022-10-31 23:18:16 +01:00 committed by GitHub
parent 8ea3cb9972
commit b4fce27203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 109 additions and 162 deletions

View File

@ -5,10 +5,6 @@
#include "MidiAudio.hpp"
#include "../terminal/parser/stateMachine.hpp"
#include <dsound.h>
#pragma comment(lib, "dxguid.lib")
using Microsoft::WRL::ComPtr;
using namespace std::chrono_literals;
@ -17,12 +13,13 @@ using namespace std::chrono_literals;
constexpr auto WAVE_SIZE = 16u;
constexpr auto WAVE_DATA = std::array<byte, WAVE_SIZE>{ 128, 159, 191, 223, 255, 223, 191, 159, 128, 96, 64, 32, 0, 32, 64, 96 };
MidiAudio::MidiAudio(HWND windowHandle)
void MidiAudio::_initialize(HWND windowHandle) noexcept
{
_hwnd = windowHandle;
_directSoundModule.reset(LoadLibraryExW(L"dsound.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
if (_directSoundModule)
{
if (auto createFunction = GetProcAddressByFunctionDeclaration(_directSoundModule.get(), DirectSoundCreate8))
if (const auto createFunction = GetProcAddressByFunctionDeclaration(_directSoundModule.get(), DirectSoundCreate8))
{
if (SUCCEEDED(createFunction(nullptr, &_directSound, nullptr)))
{
@ -35,55 +32,29 @@ MidiAudio::MidiAudio(HWND windowHandle)
}
}
MidiAudio::~MidiAudio() noexcept
void MidiAudio::BeginSkip() noexcept
{
try
{
#pragma warning(suppress : 26447)
// We acquire the lock here so the class isn't destroyed while in use.
// If this throws, we'll catch it, so the C26447 warning is bogus.
const auto lock = std::unique_lock{ _inUseMutex };
}
catch (...)
{
// If the lock fails, we'll just have to live with the consequences.
}
_skip.SetEvent();
}
void MidiAudio::Initialize()
void MidiAudio::EndSkip() noexcept
{
_shutdownFuture = _shutdownPromise.get_future();
_skip.ResetEvent();
}
void MidiAudio::Shutdown()
{
// Once the shutdown promise is set, any note that is playing will stop
// immediately, and the Unlock call will exit the thread ASAP.
_shutdownPromise.set_value();
}
void MidiAudio::Lock()
{
_inUseMutex.lock();
}
void MidiAudio::Unlock()
{
// We need to check the shutdown status before releasing the mutex,
// because after that the class could be destroyed.
const auto shutdownStatus = _shutdownFuture.wait_for(0s);
_inUseMutex.unlock();
// If the wait didn't timeout, that means the shutdown promise was set,
// so we need to exit the thread ASAP by throwing an exception.
if (shutdownStatus != std::future_status::timeout)
{
throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{};
}
}
void MidiAudio::PlayNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) noexcept
void MidiAudio::PlayNote(HWND windowHandle, const int noteNumber, const int velocity, const std::chrono::milliseconds duration) noexcept
try
{
if (_skip.is_signaled())
{
return;
}
if (_hwnd != windowHandle)
{
_initialize(windowHandle);
}
const auto& buffer = _buffers.at(_activeBufferIndex);
if (velocity && buffer)
{
@ -106,10 +77,10 @@ try
buffer->SetCurrentPosition((_lastBufferPosition + 12) % WAVE_SIZE);
}
// 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);
// By waiting on the skip event with a maximum duration of the note, we'll
// either be paused for the appropriate amount of time, or we'll break out early
// because BeginSkip() was called. This happens for Ctrl+C or during shutdown.
_skip.wait(::base::saturated_cast<DWORD>(duration.count()));
if (velocity && buffer)
{

View File

@ -12,8 +12,6 @@ Abstract:
#pragma once
#include <array>
#include <future>
#include <mutex>
struct IDirectSound8;
struct IDirectSoundBuffer;
@ -21,27 +19,20 @@ struct IDirectSoundBuffer;
class MidiAudio
{
public:
MidiAudio(HWND windowHandle);
MidiAudio(const MidiAudio&) = delete;
MidiAudio(MidiAudio&&) = delete;
MidiAudio& operator=(const MidiAudio&) = delete;
MidiAudio& operator=(MidiAudio&&) = delete;
~MidiAudio() noexcept;
void Initialize();
void Shutdown();
void Lock();
void Unlock();
void PlayNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) noexcept;
void BeginSkip() noexcept;
void EndSkip() noexcept;
void PlayNote(HWND windowHandle, const int noteNumber, const int velocity, const std::chrono::milliseconds duration) noexcept;
private:
void _initialize(HWND windowHandle) noexcept;
void _createBuffers() noexcept;
wil::slim_event_manual_reset _skip;
HWND _hwnd = nullptr;
wil::unique_hmodule _directSoundModule;
Microsoft::WRL::ComPtr<IDirectSound8> _directSound;
std::array<Microsoft::WRL::ComPtr<IDirectSoundBuffer>, 2> _buffers;
wil::com_ptr<IDirectSound8> _directSound;
std::array<wil::com_ptr<IDirectSoundBuffer>, 2> _buffers;
size_t _activeBufferIndex = 0;
DWORD _lastBufferPosition = 0;
std::promise<void> _shutdownPromise;
std::future<void> _shutdownFuture;
std::mutex _inUseMutex;
};

View File

@ -25,7 +25,9 @@ Abstract:
#endif
// Windows Header Files:
#include <windows.h>
#include <Windows.h>
#include <mmeapi.h>
#include <dsound.h>
// clang-format on

View File

@ -4,6 +4,10 @@
#include "pch.h"
#include "ControlCore.h"
// MidiAudio
#include <mmeapi.h>
#include <dsound.h>
#include <DefaultSettings.h>
#include <unicode.hpp>
#include <Utf16Parser.hpp>
@ -241,8 +245,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
_renderer->TriggerTeardown();
}
_shutdownMidiAudio();
}
bool ControlCore::Initialize(const double actualWidth,
@ -401,9 +403,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const WORD scanCode,
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
{
if (ch == L'\x3') // Ctrl+C or Ctrl+Break
{
_handleControlC();
}
return _terminal->SendCharEvent(ch, scanCode, modifiers);
}
void ControlCore::_handleControlC()
{
if (!_midiAudioSkipTimer)
{
_midiAudioSkipTimer = _dispatcher.CreateTimer();
_midiAudioSkipTimer.Interval(std::chrono::seconds(1));
_midiAudioSkipTimer.IsRepeating(false);
_midiAudioSkipTimer.Tick([weakSelf = get_weak()](auto&&, auto&&) {
if (const auto self = weakSelf.get())
{
self->_midiAudio.EndSkip();
}
});
}
_midiAudio.BeginSkip();
_midiAudioSkipTimer.Start();
}
bool ControlCore::_shouldTryUpdateSelection(const WORD vkey)
{
// GH#6423 - don't update selection if the key that was pressed was a
@ -1386,57 +1412,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - duration - How long the note should be sustained (in microseconds).
void ControlCore::_terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
{
// 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.
// Unlock the terminal, so the UI doesn't hang while we're busy.
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));
// 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();
}
// Method Description:
// - Returns the MIDI audio instance, created on demand.
// Arguments:
// - <none>
// Return Value:
// - a reference to the MidiAudio instance.
MidiAudio& ControlCore::_getMidiAudio()
{
if (!_midiAudio)
{
const auto windowHandle = reinterpret_cast<HWND>(_owningHwnd);
_midiAudio = std::make_unique<MidiAudio>(windowHandle);
_midiAudio->Initialize();
}
return *_midiAudio;
}
// Method Description:
// - Shuts down the MIDI audio system if previously instantiated.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ControlCore::_shutdownMidiAudio()
{
if (_midiAudio)
{
// We lock the terminal here to make sure the shutdown promise is
// set before the audio is unlocked in the thread that is playing.
auto lock = _terminal->LockForWriting();
_midiAudio->Shutdown();
}
}
bool ControlCore::HasSelection() const
@ -1521,6 +1504,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
_closing = true;
// Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song.
_midiAudio.BeginSkip();
// Stop accepting new output and state changes before we disconnect everything.
_connection.TerminalOutput(_connectionOutputEventToken);
_connectionStateChangedRevoker.revoke();

View File

@ -288,6 +288,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _updateSelectionUI();
bool _shouldTryUpdateSelection(const WORD vkey);
void _handleControlC();
void _sendInputToConnection(std::wstring_view wstr);
#pragma region TerminalCoreCallbacks
@ -305,10 +306,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const std::chrono::microseconds duration);
#pragma endregion
std::unique_ptr<MidiAudio> _midiAudio;
MidiAudio& _getMidiAudio();
void _shutdownMidiAudio();
MidiAudio _midiAudio;
winrt::Windows::System::DispatcherQueueTimer _midiAudioSkipTimer{ nullptr };
#pragma region RendererCallbacks
void _rendererWarning(const HRESULT hr);

View File

@ -4,6 +4,10 @@
#include "precomp.h"
#include <intsafe.h>
// MidiAudio
#include <mmeapi.h>
#include <dsound.h>
#include "misc.h"
#include "output.h"
#include "srvinit.h"
@ -373,38 +377,14 @@ Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexc
}
// Method Description:
// - Returns the MIDI audio instance, created on demand.
// - Returns the MIDI audio instance.
// Arguments:
// - <none>
// Return Value:
// - a reference to the MidiAudio instance.
MidiAudio& CONSOLE_INFORMATION::GetMidiAudio()
{
if (!_midiAudio)
{
const auto windowHandle = ServiceLocator::LocateConsoleWindow()->GetWindowHandle();
_midiAudio = std::make_unique<MidiAudio>(windowHandle);
_midiAudio->Initialize();
}
return *_midiAudio;
}
// Method Description:
// - Shuts down the MIDI audio system if previously instantiated.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CONSOLE_INFORMATION::ShutdownMidiAudio()
{
if (_midiAudio)
{
// We lock the console here to make sure the shutdown promise is
// set before the audio is unlocked in the thread that is playing.
LockConsole();
_midiAudio->Shutdown();
UnlockConsole();
}
return _midiAudio;
}
// Method Description:

View File

@ -5,6 +5,10 @@
#include "globals.h"
// MidiAudio
#include <mmeapi.h>
#include <dsound.h>
#pragma hdrstop
Globals::Globals()

View File

@ -242,6 +242,24 @@ void HandleCtrlEvent(const DWORD EventType)
}
}
static void CALLBACK midiSkipTimerCallback(HWND, UINT, UINT_PTR idEvent, DWORD) noexcept
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& midiAudio = gci.GetMidiAudio();
KillTimer(nullptr, idEvent);
midiAudio.EndSkip();
}
static void beginMidiSkip() noexcept
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& midiAudio = gci.GetMidiAudio();
midiAudio.BeginSkip();
SetTimer(nullptr, 0, 1000, midiSkipTimerCallback);
}
void ProcessCtrlEvents()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
@ -251,6 +269,8 @@ void ProcessCtrlEvents()
return;
}
beginMidiSkip();
// Make our own copy of the console process handle list.
const auto LimitingProcessId = gci.LimitingProcessId;
gci.LimitingProcessId = 0;

View File

@ -512,8 +512,6 @@ void CloseConsoleProcessState()
HandleCtrlEvent(CTRL_CLOSE_EVENT);
gci.ShutdownMidiAudio();
// Jiggle the handle: (see MSFT:19419231)
// When we call this function, we'll only actually close the console once
// we're totally unlocked. If our caller has the console locked, great,

View File

@ -373,22 +373,15 @@ void ConhostInternalGetSet::SetWorkingDirectory(const std::wstring_view /*uri*/)
// - true if successful. false otherwise.
void ConhostInternalGetSet::PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
{
// 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.
// Unlock the console, so the UI doesn't hang while we're busy.
UnlockConsole();
// This call will block for the duration, unless shutdown early.
midiAudio.PlayNote(noteNumber, velocity, duration);
const auto windowHandle = ServiceLocator::LocateConsoleWindow()->GetWindowHandle();
auto& midiAudio = ServiceLocator::LocateGlobals().getConsoleInformation().GetMidiAudio();
midiAudio.PlayNote(windowHandle, noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
// 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();
}
// Routine Description:

View File

@ -140,7 +140,6 @@ public:
Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept;
MidiAudio& GetMidiAudio();
void ShutdownMidiAudio();
CHAR_INFO AsCharInfo(const OutputCellView& cell) const noexcept;
@ -157,7 +156,7 @@ private:
Microsoft::Console::VirtualTerminal::VtIo _vtIo;
Microsoft::Console::CursorBlinker _blinker;
std::unique_ptr<MidiAudio> _midiAudio;
MidiAudio _midiAudio;
};
#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread)

View File

@ -3,6 +3,10 @@
#include "precomp.h"
// MidiAudio
#include <mmeapi.h>
#include <dsound.h>
#include "../inc/ServiceLocator.hpp"
#include "InteractivityFactory.hpp"