diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index f9739216ff8..c9e694e0b4d 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -409,20 +409,24 @@ DECAWM DECBKM DECCARA DECCKM +DECCKSR DECCOLM DECCRA DECCTR DECDHL decdld +DECDMAC DECDWL DECEKBD DECERA DECFRA DECID +DECINVM DECKPAM DECKPM DECKPNM DECLRMM +DECMSR DECNKM DECNRCM DECOM @@ -2289,6 +2293,7 @@ YOffset YSubstantial YVIRTUALSCREEN YWalk +zabcd Zabcdefghijklmnopqrstuvwxyz ZCmd ZCtrl diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 7a233535c0b..e621398b859 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -386,6 +386,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes OS_OperatingStatus = ANSIStandardStatus(5), CPR_CursorPositionReport = ANSIStandardStatus(6), ExCPR_ExtendedCursorPositionReport = DECPrivateStatus(6), + MSR_MacroSpaceReport = DECPrivateStatus(62), + MEM_MemoryChecksum = DECPrivateStatus(63), }; using ANSIStandardMode = FlaggedEnumValue<0x00000000>; @@ -508,6 +510,18 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes Size96 = 1 }; + enum class MacroDeleteControl : VTInt + { + DeleteId = 0, + DeleteAll = 1 + }; + + enum class MacroEncoding : VTInt + { + Text = 0, + HexPair = 1 + }; + enum class ReportFormat : VTInt { TerminalStateReport = 1, diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index de0bfe19e37..3129b94ca49 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -93,7 +93,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST virtual bool RequestMode(const DispatchTypes::ModeParams param) = 0; // DECRQM - virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR + virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) = 0; // DSR virtual bool DeviceAttributes() = 0; // DA1 virtual bool SecondaryDeviceAttributes() = 0; // DA2 virtual bool TertiaryDeviceAttributes() = 0; // DA3 @@ -140,6 +140,11 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD + virtual StringHandler DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) = 0; // DECDMAC + virtual bool InvokeMacro(const VTInt macroId) = 0; // DECINVM + virtual StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) = 0; // DECRSTS virtual StringHandler RequestSetting() = 0; // DECRQSS diff --git a/src/terminal/adapter/MacroBuffer.cpp b/src/terminal/adapter/MacroBuffer.cpp new file mode 100644 index 00000000000..7ae1f05e5b7 --- /dev/null +++ b/src/terminal/adapter/MacroBuffer.cpp @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "MacroBuffer.hpp" +#include "../parser/ascii.hpp" +#include "../parser/stateMachine.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +size_t MacroBuffer::GetSpaceAvailable() const noexcept +{ + return MAX_SPACE - _spaceUsed; +} + +uint16_t MacroBuffer::CalculateChecksum() const noexcept +{ + // The algorithm that we're using here is intended to match the checksums + // produced by the original DEC VT420 terminal. Although note that a real + // VT420 would have included the entire macro memory area in the checksum, + // which could still contain remnants of previous macro definitions that + // are no longer active. We don't replicate that behavior, since that's of + // no benefit to applications that might want to use the checksum. + uint16_t checksum = 0; + for (auto& macro : _macros) + { + for (auto ch : macro) + { + checksum -= ch; + } + } + return checksum; +} + +void MacroBuffer::InvokeMacro(const size_t macroId, StateMachine& stateMachine) +{ + if (macroId < _macros.size()) + { + const auto& macroSequence = til::at(_macros, macroId); + // Macros can invoke other macros up to a depth of 16, but we don't allow + // the total sequence length to exceed the maximum buffer size, since that's + // likely to facilitate a denial-of-service attack. + const auto allowedLength = MAX_SPACE - _invokedSequenceLength; + if (_invokedDepth < 16 && macroSequence.length() < allowedLength) + { + _invokedSequenceLength += macroSequence.length(); + _invokedDepth++; + auto resetInvokeDepth = wil::scope_exit([&] { + // Once the invoke depth reaches zero, we know we've reached the end + // of the root invoke, so we can reset the sequence length tracker. + if (--_invokedDepth == 0) + { + _invokedSequenceLength = 0; + } + }); + stateMachine.ProcessString(macroSequence); + } + } +} + +void MacroBuffer::ClearMacrosIfInUse() +{ + // If we receive an RIS from within a macro invocation, we can't release the + // buffer because it's still being used. Instead we'll just replace all the + // macro definitions with NUL characters to prevent any further output. The + // buffer will eventually be released once the invocation finishes. + if (_invokedDepth > 0) + { + for (auto& macro : _macros) + { + std::fill(macro.begin(), macro.end(), AsciiChars::NUL); + } + } +} + +bool MacroBuffer::InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding) +{ + // We're checking the invoked depth here to make sure we aren't defining + // a macro from within a macro invocation. + if (macroId < _macros.size() && _invokedDepth == 0) + { + _activeMacroId = macroId; + _decodedChar = 0; + _repeatPending = false; + + switch (encoding) + { + case DispatchTypes::MacroEncoding::HexPair: + _parseState = State::ExpectingHexDigit; + break; + case DispatchTypes::MacroEncoding::Text: + _parseState = State::ExpectingText; + break; + default: + return false; + } + + switch (deleteControl) + { + case DispatchTypes::MacroDeleteControl::DeleteId: + _deleteMacro(_activeMacro()); + return true; + case DispatchTypes::MacroDeleteControl::DeleteAll: + for (auto& macro : _macros) + { + _deleteMacro(macro); + } + return true; + default: + return false; + } + } + return false; +} + +bool MacroBuffer::ParseDefinition(const wchar_t ch) +{ + // Once we receive an ESC, that marks the end of the definition, but if + // an unterminated repeat is still pending, we should apply that now. + if (ch == AsciiChars::ESC) + { + if (_repeatPending && !_applyPendingRepeat()) + { + _deleteMacro(_activeMacro()); + } + return false; + } + + // Any other control characters are just ignored. + if (ch < L' ') + { + return true; + } + + // For "text encoded" macros, we'll always be in the ExpectingText state. + // For "hex encoded" macros, we'll typically be alternating between the + // ExpectingHexDigit and ExpectingSecondHexDigit states as we parse the two + // digits of each hex pair. But we also need to deal with repeat sequences, + // which start with `!`, followed by a numeric repeat count, and then a + // range of hex pairs between two `;` characters. When parsing the repeat + // count, we use the ExpectingRepeatCount state, but when parsing the hex + // pairs of the repeat, we just use the regular ExpectingHexDigit states. + + auto success = true; + switch (_parseState) + { + case State::ExpectingText: + success = _appendToActiveMacro(ch); + break; + case State::ExpectingHexDigit: + if (_decodeHexDigit(ch)) + { + _parseState = State::ExpectingSecondHexDigit; + } + else if (ch == L'!' && !_repeatPending) + { + _parseState = State::ExpectingRepeatCount; + _repeatCount = 0; + } + else if (ch == L';' && _repeatPending) + { + success = _applyPendingRepeat(); + } + else + { + success = false; + } + break; + case State::ExpectingSecondHexDigit: + success = _decodeHexDigit(ch) && _appendToActiveMacro(_decodedChar); + _decodedChar = 0; + _parseState = State::ExpectingHexDigit; + break; + case State::ExpectingRepeatCount: + if (ch >= L'0' && ch <= L'9') + { + _repeatCount = _repeatCount * 10 + (ch - L'0'); + _repeatCount = std::min(_repeatCount, MAX_PARAMETER_VALUE); + } + else if (ch == L';') + { + _repeatPending = true; + _repeatStart = _activeMacro().length(); + _parseState = State::ExpectingHexDigit; + } + else + { + success = false; + } + break; + default: + success = false; + break; + } + + // If there is an error in the definition, clear everything received so far. + if (!success) + { + _deleteMacro(_activeMacro()); + } + return success; +} + +bool MacroBuffer::_decodeHexDigit(const wchar_t ch) noexcept +{ + _decodedChar <<= 4; + if (ch >= L'0' && ch <= L'9') + { + _decodedChar += (ch - L'0'); + return true; + } + else if (ch >= L'A' && ch <= L'F') + { + _decodedChar += (ch - L'A' + 10); + return true; + } + else if (ch >= L'a' && ch <= L'f') + { + _decodedChar += (ch - L'a' + 10); + return true; + } + return false; +} + +bool MacroBuffer::_appendToActiveMacro(const wchar_t ch) +{ + if (GetSpaceAvailable() > 0) + { + _activeMacro().push_back(ch); + _spaceUsed++; + return true; + } + return false; +} + +std::wstring& MacroBuffer::_activeMacro() +{ + return _macros.at(_activeMacroId); +} + +void MacroBuffer::_deleteMacro(std::wstring& macro) noexcept +{ + _spaceUsed -= macro.length(); + std::wstring{}.swap(macro); +} + +bool MacroBuffer::_applyPendingRepeat() +{ + if (_repeatCount > 1) + { + auto& activeMacro = _activeMacro(); + const auto sequenceLength = activeMacro.length() - _repeatStart; + // Note that the repeat sequence has already been written to the buffer + // once while it was being parsed, so we only need to append additional + // copies for repeat counts that are greater than one. If there is not + // enough space for the additional content, we'll just abort the macro. + const auto spaceRequired = (_repeatCount - 1) * sequenceLength; + if (spaceRequired > GetSpaceAvailable()) + { + return false; + } + for (size_t i = 1; i < _repeatCount; i++) + { + activeMacro.append(activeMacro.substr(_repeatStart, sequenceLength)); + _spaceUsed += sequenceLength; + } + } + _repeatPending = false; + return true; +} diff --git a/src/terminal/adapter/MacroBuffer.hpp b/src/terminal/adapter/MacroBuffer.hpp new file mode 100644 index 00000000000..92c1c07c73f --- /dev/null +++ b/src/terminal/adapter/MacroBuffer.hpp @@ -0,0 +1,79 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- MacrosBuffer.hpp + +Abstract: +- This manages the parsing and storage of macros defined by the DECDMAC control sequence. +--*/ + +#pragma once + +#include "DispatchTypes.hpp" +#include +#include +#include + +// fwdecl unittest classes +#ifdef UNIT_TESTING +class AdapterTest; +#endif + +namespace Microsoft::Console::VirtualTerminal +{ + class StateMachine; + + class MacroBuffer + { + public: + // The original DEC terminals only supported 6K of memory, which is + // probably a bit low for modern usage. But we also don't want to make + // this value too large, otherwise it could be used in a denial-of- + // service attack. So for now this is probably a sufficient limit, but + // we may need to increase it in the future if we intend to support + // macros containing sixel sequences. + static constexpr size_t MAX_SPACE = 0x40000; + + MacroBuffer() = default; + ~MacroBuffer() = default; + + size_t GetSpaceAvailable() const noexcept; + uint16_t CalculateChecksum() const noexcept; + void InvokeMacro(const size_t macroId, StateMachine& stateMachine); + void ClearMacrosIfInUse(); + bool InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding); + bool ParseDefinition(const wchar_t ch); + + private: + bool _decodeHexDigit(const wchar_t ch) noexcept; + bool _appendToActiveMacro(const wchar_t ch); + std::wstring& _activeMacro(); + void _deleteMacro(std::wstring& macro) noexcept; + bool _applyPendingRepeat(); + + enum class State + { + ExpectingText, + ExpectingHexDigit, + ExpectingSecondHexDigit, + ExpectingRepeatCount + }; + + State _parseState{ State::ExpectingText }; + wchar_t _decodedChar{ 0 }; + bool _repeatPending{ false }; + size_t _repeatCount{ 0 }; + size_t _repeatStart{ 0 }; + std::array _macros; + size_t _activeMacroId{ 0 }; + size_t _spaceUsed{ 0 }; + size_t _invokedDepth{ 0 }; + size_t _invokedSequenceLength{ 0 }; + +#ifdef UNIT_TESTING + friend class AdapterTest; +#endif + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 4a784a4fa3e..4c9d60df4aa 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1151,9 +1151,10 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) // - DSR - Reports status of a console property back to the STDIN based on the type of status requested. // Arguments: // - statusType - status type indicating what property we should report back +// - id - a numeric label used to identify the request in DECCKSR reports // Return Value: // - True if handled successfully. False otherwise. -bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType) +bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) { switch (statusType) { @@ -1166,6 +1167,12 @@ bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusTyp case DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport: _CursorPositionReport(true); return true; + case DispatchTypes::StatusType::MSR_MacroSpaceReport: + _MacroSpaceReport(); + return true; + case DispatchTypes::StatusType::MEM_MemoryChecksum: + _MacroChecksumReport(id); + return true; default: return false; } @@ -1321,6 +1328,34 @@ void AdaptDispatch::_CursorPositionReport(const bool extendedReport) } } +// Routine Description: +// - DECMSR - Reports the amount of space available for macro definitions. +// Arguments: +// - +// Return Value: +// - +void AdaptDispatch::_MacroSpaceReport() const +{ + const auto spaceInBytes = _macroBuffer ? _macroBuffer->GetSpaceAvailable() : MacroBuffer::MAX_SPACE; + // The available space is measured in blocks of 16 bytes, so we need to divide by 16. + const auto response = wil::str_printf(L"\x1b[%zu*{", spaceInBytes / 16); + _api.ReturnResponse(response); +} + +// Routine Description: +// - DECCKSR - Reports a checksum of the current macro definitions. +// Arguments: +// - id - a numeric label used to identify the DSR request +// Return Value: +// - +void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const +{ + const auto requestId = id.value_or(0); + const auto checksum = _macroBuffer ? _macroBuffer->CalculateChecksum() : 0; + const auto response = wil::str_printf(L"\033P%d!~%04X\033\\", requestId, checksum); + _api.ReturnResponse(response); +} + // Routine Description: // - Generalizes scrolling movement for up/down // Arguments: @@ -2314,6 +2349,13 @@ bool AdaptDispatch::HardReset() // Reset internal modes to their initial state _modes = {}; + // Clear and release the macro buffer. + if (_macroBuffer) + { + _macroBuffer->ClearMacrosIfInUse(); + _macroBuffer = nullptr; + } + // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this RIS sequence to the connected // terminal application. We've reset our state, but the connected terminal @@ -3054,6 +3096,60 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const return nullptr; } +// Method Description: +// - DECDMAC - Defines a string of characters as a macro that can later be +// invoked with a DECINVM sequence. +// Arguments: +// - macroId - a number to identify the macro when invoked. +// - deleteControl - what gets deleted before loading the new macro data. +// - encoding - whether the data is encoded as plain text or hex digits. +// Return Value: +// - a function to receive the macro data or nullptr if parameters are invalid. +ITermDispatch::StringHandler AdaptDispatch::DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) +{ + if (!_macroBuffer) + { + _macroBuffer = std::make_shared(); + } + + if (_macroBuffer->InitParser(macroId, deleteControl, encoding)) + { + return [&](const auto ch) { + return _macroBuffer->ParseDefinition(ch); + }; + } + + return nullptr; +} + +// Method Description: +// - DECINVM - Invokes a previously defined macro, executing the macro content +// as if it had been received directly from the host. +// Arguments: +// - macroId - the id number of the macro to be invoked. +// Return Value: +// - True +bool AdaptDispatch::InvokeMacro(const VTInt macroId) +{ + if (_macroBuffer) + { + // In order to inject our macro sequence into the state machine + // we need to register a callback that will be executed only + // once it has finished processing the current operation, and + // has returned to the ground state. Note that we're capturing + // a copy of the _macroBuffer pointer here to make sure it won't + // be deleted (e.g. from an invoked RIS) while still in use. + const auto macroBuffer = _macroBuffer; + auto& stateMachine = _api.GetStateMachine(); + stateMachine.OnCsiComplete([=, &stateMachine]() { + macroBuffer->InvokeMacro(macroId, stateMachine); + }); + } + return true; +} + // Method Description: // - DECRSTS - Restores the terminal state from a stream of data previously // saved with a DECRQTSR query. diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index ce19f1143b3..bc5a0f04c25 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -17,10 +17,16 @@ Author(s): #include "termDispatch.hpp" #include "ITerminalApi.hpp" #include "FontBuffer.hpp" +#include "MacroBuffer.hpp" #include "terminalOutput.hpp" #include "../input/terminalInput.hpp" #include "../../types/inc/sgrStack.hpp" +// fwdecl unittest classes +#ifdef UNIT_TESTING +class AdapterTest; +#endif + namespace Microsoft::Console::VirtualTerminal { class AdaptDispatch : public ITermDispatch @@ -66,7 +72,7 @@ namespace Microsoft::Console::VirtualTerminal bool SetCharacterProtectionAttribute(const VTParameters options) override; // DECSCA bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR bool PopGraphicsRendition() override; // XTPOPSGR - bool DeviceStatusReport(const DispatchTypes::StatusType statusType) override; // DSR, DSR-OS, DSR-CPR + bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) override; // DSR bool DeviceAttributes() override; // DA1 bool SecondaryDeviceAttributes() override; // DA2 bool TertiaryDeviceAttributes() override; // DA3 @@ -135,6 +141,11 @@ namespace Microsoft::Console::VirtualTerminal const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + StringHandler DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) override; // DECDMAC + bool InvokeMacro(const VTInt macroId) override; // DECINVM + StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) override; // DECRSTS StringHandler RequestSetting() override; // DECRQSS @@ -202,6 +213,8 @@ namespace Microsoft::Console::VirtualTerminal const VTInt bottomMargin); void _OperatingStatus() const; void _CursorPositionReport(const bool extendedReport); + void _MacroSpaceReport() const; + void _MacroChecksumReport(const VTParameter id) const; void _SetColumnMode(const bool enable); void _SetAlternateScreenBufferMode(const bool enable); @@ -232,6 +245,7 @@ namespace Microsoft::Console::VirtualTerminal TerminalInput& _terminalInput; TerminalOutput _termOutput; std::unique_ptr _fontBuffer; + std::shared_ptr _macroBuffer; std::optional _initialCodePage; // We have two instances of the saved cursor state, because we need @@ -256,5 +270,9 @@ namespace Microsoft::Console::VirtualTerminal TextAttribute& attr) noexcept; void _ApplyGraphicsOptions(const VTParameters options, TextAttribute& attr) noexcept; + +#ifdef UNIT_TESTING + friend class AdapterTest; +#endif }; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index 5bc8488f891..3ecefa5e838 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -14,6 +14,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 10a5aa55ff7..21524f61cd6 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -80,6 +83,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 87c5507685a..d1e63430bb4 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -33,6 +33,7 @@ SOURCES= \ ..\adaptDispatch.cpp \ ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ + ..\MacroBuffer.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ ..\telemetry.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 25ba2b22a7a..56ab5d1578d 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -86,7 +86,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRST bool RequestMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRQM - bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/) override { return false; } // DSR, DSR-OS, DSR-CPR + bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/, const VTParameter /*id*/) override { return false; } // DSR bool DeviceAttributes() override { return false; } // DA1 bool SecondaryDeviceAttributes() override { return false; } // DA2 bool TertiaryDeviceAttributes() override { return false; } // DA3 @@ -133,6 +133,11 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons const VTParameter /*cellHeight*/, const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD + StringHandler DefineMacro(const VTInt /*macroId*/, + const DispatchTypes::MacroDeleteControl /*deleteControl*/, + const DispatchTypes::MacroEncoding /*encoding*/) override { return nullptr; } // DECDMAC + bool InvokeMacro(const VTInt /*macroId*/) override { return false; } // DECINVM + StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat /*format*/) override { return nullptr; }; // DECRSTS StringHandler RequestSetting() override { return nullptr; }; // DECRQSS diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 4c35e64d543..f4e81adf72a 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -60,8 +60,9 @@ using namespace Microsoft::Console::VirtualTerminal; class TestGetSet final : public ITerminalApi { public: - void PrintString(const std::wstring_view /*string*/) override + void PrintString(const std::wstring_view string) override { + _printed += string; } void ReturnResponse(const std::wstring_view response) override @@ -315,6 +316,8 @@ class TestGetSet final : public ITerminalApi _response.clear(); _retainResponse = false; + + _printed.clear(); } void PrepCursor(CursorX xact, CursorY yact) @@ -406,6 +409,8 @@ class TestGetSet final : public ITerminalApi til::inclusive_rect _expectedScrollRegion; til::inclusive_rect _activeScrollRegion; + std::wstring _printed; + til::point _expectedCursorPos; TextAttribute _expectedAttribute = {}; @@ -1389,7 +1394,7 @@ class AdapterTest Log::Comment(L"Test 1: Verify failure when using bad status."); _testGetSet->PrepData(); - VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1)); + VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1, {})); } TEST_METHOD(DeviceStatus_OperatingStatusTests) @@ -1398,7 +1403,7 @@ class AdapterTest Log::Comment(L"Test 1: Verify good operating condition."); _testGetSet->PrepData(); - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus, {})); _testGetSet->ValidateInputEvent(L"\x1b[0n"); } @@ -1421,7 +1426,7 @@ class AdapterTest coordCursorExpected.x++; coordCursorExpected.y++; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); wchar_t pwszBuffer[50]; @@ -1445,7 +1450,7 @@ class AdapterTest // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.) coordCursorExpectedFirst += til::point{ 1, 1 }; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); auto cursorPos = _testGetSet->_textBuffer->GetCursor().GetPosition(); cursorPos.x++; @@ -1455,7 +1460,7 @@ class AdapterTest auto coordCursorExpectedSecond{ coordCursorExpectedFirst }; coordCursorExpectedSecond += til::point{ 1, 1 }; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); wchar_t pwszBuffer[50]; @@ -1484,13 +1489,80 @@ class AdapterTest // Until we support paging (GH#13892) the reported page number should always be 1. const auto pageExpected = 1; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport, {})); wchar_t pwszBuffer[50]; swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); _testGetSet->ValidateInputEvent(pwszBuffer); } + TEST_METHOD(DeviceStatus_MacroSpaceReportTest) + { + Log::Comment(L"Starting test..."); + + // Space is measured in blocks of 16 bytes. + const auto availableSpace = MacroBuffer::MAX_SPACE / 16; + + Log::Comment(L"Test 1: Verify maximum space available"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + wchar_t pwszBuffer[50]; + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 2: Verify space decrease"); + _testGetSet->PrepData(); + // Define four 8-byte macros, i.e. 32 byes (2 macro blocks). + _stateMachine->ProcessString(L"\033P1;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P3;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P4;0;0!z12345678\033\\"); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace - 2); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 3: Verify space reset"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->HardReset()); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace); + _testGetSet->ValidateInputEvent(pwszBuffer); + } + + TEST_METHOD(DeviceStatus_MemoryChecksumReportTest) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify initial checksum is 0"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 12)); + + _testGetSet->ValidateInputEvent(L"\033P12!~0000\033\\"); + + Log::Comment(L"Test 2: Verify checksum after macros defined"); + _testGetSet->PrepData(); + // Define a couple of text macros + _stateMachine->ProcessString(L"\033P1;0;0!zABCD\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zabcd\033\\"); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 34)); + + // Checksum is a 16-bit negated sum of the macro buffer characters. + const auto checksum = gsl::narrow_cast(-('A' + 'B' + 'C' + 'D' + 'a' + 'b' + 'c' + 'd')); + wchar_t pwszBuffer[50]; + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\033P34!~%04X\033\\", checksum); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 3: Verify checksum resets to 0"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->HardReset()); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 56)); + + _testGetSet->ValidateInputEvent(L"\033P56!~0000\033\\"); + } + TEST_METHOD(DeviceAttributesTests) { Log::Comment(L"Starting test..."); @@ -2362,6 +2434,148 @@ class AdapterTest VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1)); } + TEST_METHOD(MacroDefinitions) + { + const auto getMacroText = [&](const auto id) { + return _pDispatch->_macroBuffer->_macros.at(id); + }; + + Log::Comment(L"Text encoding"); + _stateMachine->ProcessString(L"\033P1;0;0!zText Encoding\033\\"); + VERIFY_ARE_EQUAL(L"Text Encoding", getMacroText(1)); + + Log::Comment(L"Hex encoding (uppercase)"); + _stateMachine->ProcessString(L"\033P2;0;1!z486578204A4B4C4D4E4F\033\\"); + VERIFY_ARE_EQUAL(L"Hex JKLMNO", getMacroText(2)); + + Log::Comment(L"Hex encoding (lowercase)"); + _stateMachine->ProcessString(L"\033P3;0;1!z486578206a6b6c6d6e6f\033\\"); + VERIFY_ARE_EQUAL(L"Hex jklmno", getMacroText(3)); + + Log::Comment(L"Default encoding is text"); + _stateMachine->ProcessString(L"\033P4;0;!zDefault Encoding\033\\"); + VERIFY_ARE_EQUAL(L"Default Encoding", getMacroText(4)); + + Log::Comment(L"Default ID is 0"); + _stateMachine->ProcessString(L"\033P;0;0!zDefault ID\033\\"); + VERIFY_ARE_EQUAL(L"Default ID", getMacroText(0)); + + Log::Comment(L"Replacing a single macro"); + _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"Retained", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Replacing all macros"); + _stateMachine->ProcessString(L"\033P1;0;0!zErased\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;1;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Default replacement is a single macro"); + _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"Retained", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Repeating three times"); + _stateMachine->ProcessString(L"\033P5;0;1!z526570656174!3;206563686F;207468726565\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo echo echo three", getMacroText(5)); + + Log::Comment(L"Zero repeats once"); + _stateMachine->ProcessString(L"\033P6;0;1!z526570656174!0;206563686F;207A65726F\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo zero", getMacroText(6)); + + Log::Comment(L"Default repeats once"); + _stateMachine->ProcessString(L"\033P7;0;1!z526570656174!;206563686F;2064656661756C74\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo default", getMacroText(7)); + + Log::Comment(L"Unterminated repeat sequence"); + _stateMachine->ProcessString(L"\033P8;0;1!z556E7465726D696E61746564!3;206563686F\033\\"); + VERIFY_ARE_EQUAL(L"Unterminated echo echo echo", getMacroText(8)); + + Log::Comment(L"Unexpected semicolon cancels definition"); + _stateMachine->ProcessString(L"\033P9;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P9;0;1!z526570656174!3;206563;686F;207468726565\033\\"); + VERIFY_ARE_EQUAL(L"", getMacroText(9)); + + Log::Comment(L"Control characters in a text encoding"); + _stateMachine->ProcessString(L"\033P10;0;0!zA\aB\bC\tD\nE\vF\fG\rH\033\\"); + VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(10)); + + Log::Comment(L"Control characters in a hex encoding"); + _stateMachine->ProcessString(L"\033P11;0;1!z41\a42\b43\t44\n45\v46\f47\r48\033\\"); + VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(11)); + + Log::Comment(L"Control characters in a repeat"); + _stateMachine->ProcessString(L"\033P12;0;1!z!\a3\b;\t4\n1\v4\f2\r4\a3\b;\033\\"); + VERIFY_ARE_EQUAL(L"ABCABCABC", getMacroText(12)); + + Log::Comment(L"Encoded control characters"); + _stateMachine->ProcessString(L"\033P13;0;1!z410742084309440A450B460C470D481B49\033\\"); + VERIFY_ARE_EQUAL(L"A\aB\bC\tD\nE\vF\fG\rH\033I", getMacroText(13)); + + _pDispatch->_macroBuffer = nullptr; + } + + TEST_METHOD(MacroInvokes) + { + _pDispatch->_macroBuffer = std::make_shared(); + + const auto setMacroText = [&](const auto id, const auto value) { + _pDispatch->_macroBuffer->_macros.at(id) = value; + }; + + setMacroText(0, L"Macro 0"); + setMacroText(1, L"Macro 1"); + setMacroText(2, L"Macro 2"); + setMacroText(63, L"Macro 63"); + + Log::Comment(L"Simple macro invoke"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[2*z"); + VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed); + + Log::Comment(L"Default macro invoke"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[*z"); + VERIFY_ARE_EQUAL(L"Macro 0", _testGetSet->_printed); + + Log::Comment(L"Maximum ID number"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[63*z"); + VERIFY_ARE_EQUAL(L"Macro 63", _testGetSet->_printed); + + Log::Comment(L"Out of range ID number"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[64*z"); + VERIFY_ARE_EQUAL(L"", _testGetSet->_printed); + + Log::Comment(L"Only one ID parameter allowed"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[2;0;1*z"); + VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed); + + Log::Comment(L"DECDMAC ignored when inside a macro"); + setMacroText(10, L"[\033P1;0;0!zReplace Macro 1\033\\]"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[10*z"); + _stateMachine->ProcessString(L"\033[1*z"); + VERIFY_ARE_EQUAL(L"[]Macro 1", _testGetSet->_printed); + + Log::Comment(L"Maximum recursive depth is 16"); + setMacroText(0, L"<\033[1*z>"); + setMacroText(1, L"[\033[0*z]"); + _testGetSet->_printed.clear(); + _stateMachine->ProcessString(L"\033[0*z"); + VERIFY_ARE_EQUAL(L"<[<[<[<[<[<[<[<[]>]>]>]>]>]>]>]>", _testGetSet->_printed); + + _pDispatch->_macroBuffer = nullptr; + } + private: TerminalInput _terminalInput{ nullptr }; std::unique_ptr _testGetSet; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 3c2de8aee73..48625cd5af7 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -530,11 +530,11 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete TermTelemetry::Instance().Log(TermTelemetry::Codes::SGR); break; case CsiActionCodes::DSR_DeviceStatusReport: - success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0))); + success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0)), parameters.at(1)); TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); break; case CsiActionCodes::DSR_PrivateDeviceStatusReport: - success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0))); + success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0)), parameters.at(1)); TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); break; case CsiActionCodes::DA_DeviceAttributes: @@ -672,6 +672,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete success = _dispatch->SelectAttributeChangeExtent(parameters.at(0)); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE); break; + case CsiActionCodes::DECINVM_InvokeMacro: + success = _dispatch->InvokeMacro(parameters.at(0).value_or(0)); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECINVM); + break; case CsiActionCodes::DECAC_AssignColor: success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0)); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC); @@ -723,6 +727,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c parameters.at(6), parameters.at(7)); break; + case DcsActionCodes::DECDMAC_DefineMacro: + handler = _dispatch->DefineMacro(parameters.at(0).value_or(0), parameters.at(1), parameters.at(2)); + break; case DcsActionCodes::DECRSTS_RestoreTerminalState: handler = _dispatch->RestoreTerminalState(parameters.at(0)); break; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index cdbed0fed35..af48cf37922 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -157,6 +157,7 @@ namespace Microsoft::Console::VirtualTerminal DECSERA_SelectiveEraseRectangularArea = VTID("${"), DECSCPP_SetColumnsPerPage = VTID("$|"), DECSACE_SelectAttributeChangeExtent = VTID("*x"), + DECINVM_InvokeMacro = VTID("*z"), DECAC_AssignColor = VTID(",|"), DECPS_PlaySound = VTID(",~") }; @@ -164,6 +165,7 @@ namespace Microsoft::Console::VirtualTerminal enum DcsActionCodes : uint64_t { DECDLD_DownloadDRCS = VTID("{"), + DECDMAC_DefineMacro = VTID("!z"), DECRSTS_RestoreTerminalState = VTID("$p"), DECRQSS_RequestSetting = VTID("$q") }; diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp index 36848694f32..d6a9aa31489 100644 --- a/src/terminal/parser/stateMachine.cpp +++ b/src/terminal/parser/stateMachine.cpp @@ -1175,6 +1175,7 @@ void StateMachine::_EventCsiEntry(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1213,6 +1214,7 @@ void StateMachine::_EventCsiIntermediate(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1294,6 +1296,7 @@ void StateMachine::_EventCsiParam(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1996,6 +1999,18 @@ bool StateMachine::IsProcessingLastCharacter() const noexcept return _processingLastCharacter; } +// Routine Description: +// - Registers a function that will be called once the current CSI action is +// complete and the state machine has returned to the ground state. +// Arguments: +// - callback - The function that will be called +// Return Value: +// - +void StateMachine::OnCsiComplete(const std::function callback) +{ + _onCsiCompleteCallback = callback; +} + // Routine Description: // - Wherever the state machine is, whatever it's going, go back to ground. // This is used by conhost to "jiggle the handle" - when VT support is @@ -2058,3 +2073,24 @@ bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) } return success; } + +void StateMachine::_ExecuteCsiCompleteCallback() +{ + if (_onCsiCompleteCallback) + { + // We need to save the state of the string that we're currently + // processing in case the callback injects another string. + const auto savedCurrentString = _currentString; + const auto savedRunOffset = _runOffset; + const auto savedRunSize = _runSize; + // We also need to take ownership of the callback function before + // executing it so there's no risk of it being run more than once. + const auto callback = std::move(_onCsiCompleteCallback); + callback(); + // Once the callback has returned, we can restore the original state + // and continue where we left off. + _currentString = savedCurrentString; + _runOffset = savedRunOffset; + _runSize = savedRunSize; + } +} diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp index 41bce50f97e..a51351e456f 100644 --- a/src/terminal/parser/stateMachine.hpp +++ b/src/terminal/parser/stateMachine.hpp @@ -61,6 +61,8 @@ namespace Microsoft::Console::VirtualTerminal void ProcessString(const std::wstring_view string); bool IsProcessingLastCharacter() const noexcept; + void OnCsiComplete(const std::function callback); + void ResetState() noexcept; bool FlushToTerminal(); @@ -142,6 +144,8 @@ namespace Microsoft::Console::VirtualTerminal template bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda); + void _ExecuteCsiCompleteCallback(); + enum class VTStates { Ground, @@ -202,5 +206,7 @@ namespace Microsoft::Console::VirtualTerminal // can start and finish a sequence. bool _processingIndividually; bool _processingLastCharacter; + + std::function _onCsiCompleteCallback; }; } diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index 05b7ab20dcd..83318b1719d 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -291,6 +291,7 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"), TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"), TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"), + TraceLoggingUInt32(_uiTimesUsed[DECINVM], "DECINVM"), TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"), TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"), TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"), diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index 432fd56d3c1..01eee817e0a 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -118,6 +118,7 @@ namespace Microsoft::Console::VirtualTerminal DECERA, DECSERA, DECSACE, + DECINVM, DECAC, DECPS, // Only use this last enum as a count of the number of codes. diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp index 5cfe3e311b7..38d79f54e6d 100644 --- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -1188,7 +1188,7 @@ class StatefulDispatch final : public TermDispatch } CATCH_LOG_RETURN_FALSE() - bool DeviceStatusReport(const DispatchTypes::StatusType statusType) noexcept override + bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter /*id*/) noexcept override { _deviceStatusReport = true; _statusReportType = statusType;