From 4d27a05318f4e86b0918497ba6f980968725a1d2 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 9 Dec 2022 21:10:41 +0000 Subject: [PATCH] Add support for DEC macro operations (#14402) ## Summary of the Pull Request This PR adds support for the DEC macro operations `DECDMAC` (Define Macro), and `DECINVM` (Invoke Macro), which allow an application to define a sequence of characters as a macro, and then later invoke that macro to execute the content as if it had just been received from the host. This PR also adds two new `DSR` queries: one for reporting the available space remaining in the macro buffer (`DECMSR`), and another reporting a checksum of the macros that are currently defined (`DECCKSR`). ## PR Checklist * [x] Closes #14205 * [x] CLA signed. * [x] Tests added/passed * [ ] Documentation updated. * [ ] Schema updated. * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx ## Detailed Description of the Pull Request / Additional comments I've created a separate `MacroBuffer` class to handle the parsing and storage of macros, so the `AdaptDispatch` class doesn't have to do much more than delegate the macro operations to that. The one complication is the macro invocation, which requires injecting characters back into the state machine's input stream. Ideally we'd just pass the content to the `ProcessString` method, but we can't do that when it's already in the middle of a `CSI` dispatch. My solution for this was to add an `OnCsiComplete` method via which we could register a callback function that injects the macro sequence only once the state machine has returned to the ground state. This feels a bit hacky, but that was the best approach I could come up with. ## Validation Steps Performed Thanks to @KalleOlaviNiemitalo, we've been able to do some testing on a real VT420 to determine how the macro operations are intended to work, and I've tried to get our implementation to match that behavior as much as possible (we differ in some aspects of the checksum reporting, where the VT420 behavior seemed undesirable, or potentially buggy). I've also added unit tests covering some of the same scenarios that we tested on the VT420. --- .github/actions/spelling/expect/expect.txt | 5 + src/terminal/adapter/DispatchTypes.hpp | 14 + src/terminal/adapter/ITermDispatch.hpp | 7 +- src/terminal/adapter/MacroBuffer.cpp | 270 ++++++++++++++++++ src/terminal/adapter/MacroBuffer.hpp | 79 +++++ src/terminal/adapter/adaptDispatch.cpp | 98 ++++++- src/terminal/adapter/adaptDispatch.hpp | 20 +- src/terminal/adapter/lib/adapter.vcxproj | 2 + .../adapter/lib/adapter.vcxproj.filters | 6 + src/terminal/adapter/sources.inc | 1 + src/terminal/adapter/termDispatch.hpp | 7 +- .../adapter/ut_adapter/adapterTest.cpp | 228 ++++++++++++++- .../parser/OutputStateMachineEngine.cpp | 11 +- .../parser/OutputStateMachineEngine.hpp | 2 + src/terminal/parser/stateMachine.cpp | 36 +++ src/terminal/parser/stateMachine.hpp | 6 + src/terminal/parser/telemetry.cpp | 1 + src/terminal/parser/telemetry.hpp | 1 + .../parser/ut_parser/OutputEngineTest.cpp | 2 +- 19 files changed, 782 insertions(+), 14 deletions(-) create mode 100644 src/terminal/adapter/MacroBuffer.cpp create mode 100644 src/terminal/adapter/MacroBuffer.hpp 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;