Skip to content

Commit

Permalink
Add support for DA2 and DA3 device attributes reports (#6850)
Browse files Browse the repository at this point in the history
This PR adds support for the `DA2` (Secondary Device Attributes) and
`DA3` (Tertiary Device Attributes) escape sequences, which are standard
VT queries reporting basic information about the terminal.

The _Secondary Device Attributes_ response is made up of a number of
parameters:
1. An identification code, for which I've used 0 to indicate that we
   have the capabilities of a VT100 (using code 0 for this is an XTerm
   convention, since technically DA2 would not have been supported by a
   VT100).
2. A firmware revision level, which some terminal emulators use to
   report their actual version number, but I thought it best we just
   hardcode a value of 10 (the DEC convention for 1.0).
3. Additional hardware options, which tend to be device specific, but
   I've followed the convention of the later DEC terminals using 1 to
   indicate the presence of a PC keyboard.

The _Tertiary Device Attributes_ response was originally used to provide
a unique terminal identification code, and which some terminal emulators
use as a way to identify themselves. However, I think that's information
we'd probably prefer not to reveal, so I've followed the more common
practice of returning all zeros for the ID.

In terms of implementation, the only complication was the need to add an
additional code path in the `OutputStateMachine` to handle the `>` and
`=` intermediates (technically private parameter prefixes) that these
sequences require. I've done this as a single method - rather than one
for each prefix - since I think that makes the code easier to follow.

VALIDATION
----------

I've added output engine tests to make sure the sequences are dispatched
correctly, and adapter tests to confirm that they are returning the
responses we expect. I've also manually confirmed that they pass the
_Test of terminal reports_ in Vttest.

Closes #5836
  • Loading branch information
j4james authored Jul 10, 2020
1 parent 3388a48 commit 53b224b
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/terminal/adapter/ITermDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch

virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR
virtual bool DeviceAttributes() = 0; // DA1
virtual bool SecondaryDeviceAttributes() = 0; // DA2
virtual bool TertiaryDeviceAttributes() = 0; // DA3
virtual bool Vt52DeviceAttributes() = 0; // VT52 Identify

virtual bool DesignateCodingSystem(const wchar_t codingSystem) = 0; // DOCS
Expand Down
26 changes: 26 additions & 0 deletions src/terminal/adapter/adaptDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,32 @@ bool AdaptDispatch::DeviceAttributes()
return _WriteResponse(L"\x1b[?1;0c");
}

// Routine Description:
// - DA2 - Reports the terminal type, firmware version, and hardware options.
// For now we're following the XTerm practice of using 0 to represent a VT100
// terminal, the version is hardcoded as 10 (1.0), and the hardware option
// is set to 1 (indicating a PC Keyboard).
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SecondaryDeviceAttributes()
{
return _WriteResponse(L"\x1b[>0;10;1c");
}

// Routine Description:
// - DA3 - Reports the terminal unit identification code. Terminal emulators
// typically return a hardcoded value, the most common being all zeros.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::TertiaryDeviceAttributes()
{
return _WriteResponse(L"\x1bP!|00000000\x1b\\");
}

// Routine Description:
// - VT52 Identify - Reports the identity of the terminal in VT52 emulation mode.
// An actual VT52 terminal would typically identify itself with ESC / K.
Expand Down
2 changes: 2 additions & 0 deletions src/terminal/adapter/adaptDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ namespace Microsoft::Console::VirtualTerminal
bool SetGraphicsRendition(const std::basic_string_view<DispatchTypes::GraphicsOptions> options) override; // SGR
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR
bool DeviceAttributes() override; // DA1
bool SecondaryDeviceAttributes() override; // DA2
bool TertiaryDeviceAttributes() override; // DA3
bool Vt52DeviceAttributes() override; // VT52 Identify
bool ScrollUp(const size_t distance) override; // SU
bool ScrollDown(const size_t distance) override; // SD
Expand Down
2 changes: 2 additions & 0 deletions src/terminal/adapter/termDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons

bool DeviceStatusReport(const DispatchTypes::AnsiStatusType /*statusType*/) noexcept override { return false; } // DSR, DSR-OS, DSR-CPR
bool DeviceAttributes() noexcept override { return false; } // DA1
bool SecondaryDeviceAttributes() noexcept override { return false; } // DA2
bool TertiaryDeviceAttributes() noexcept override { return false; } // DA3
bool Vt52DeviceAttributes() noexcept override { return false; } // VT52 Identify

bool DesignateCodingSystem(const wchar_t /*codingSystem*/) noexcept override { return false; } // DOCS
Expand Down
36 changes: 36 additions & 0 deletions src/terminal/adapter/ut_adapter/adapterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,42 @@ class AdapterTest
VERIFY_IS_FALSE(_pDispatch.get()->DeviceAttributes());
}

TEST_METHOD(SecondaryDeviceAttributesTests)
{
Log::Comment(L"Starting test...");

Log::Comment(L"Test 1: Verify normal response.");
_testGetSet->PrepData();
VERIFY_IS_TRUE(_pDispatch.get()->SecondaryDeviceAttributes());

PCWSTR pwszExpectedResponse = L"\x1b[>0;10;1c";
_testGetSet->ValidateInputEvent(pwszExpectedResponse);

Log::Comment(L"Test 2: Verify failure when WriteConsoleInput doesn't work.");
_testGetSet->PrepData();
_testGetSet->_privatePrependConsoleInputResult = FALSE;

VERIFY_IS_FALSE(_pDispatch.get()->SecondaryDeviceAttributes());
}

TEST_METHOD(TertiaryDeviceAttributesTests)
{
Log::Comment(L"Starting test...");

Log::Comment(L"Test 1: Verify normal response.");
_testGetSet->PrepData();
VERIFY_IS_TRUE(_pDispatch.get()->TertiaryDeviceAttributes());

PCWSTR pwszExpectedResponse = L"\x1bP!|00000000\x1b\\";
_testGetSet->ValidateInputEvent(pwszExpectedResponse);

Log::Comment(L"Test 2: Verify failure when WriteConsoleInput doesn't work.");
_testGetSet->PrepData();
_testGetSet->_privatePrependConsoleInputResult = FALSE;

VERIFY_IS_FALSE(_pDispatch.get()->TertiaryDeviceAttributes());
}

TEST_METHOD(CursorKeysModeTest)
{
Log::Comment(L"Starting test...");
Expand Down
41 changes: 41 additions & 0 deletions src/terminal/parser/OutputStateMachineEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const wchar_t wch,
case L'?':
success = _IntermediateQuestionMarkDispatch(wch, parameters);
break;
case L'>':
case L'=':
success = _IntermediateGreaterThanOrEqualDispatch(wch, value, parameters);
break;
case L'!':
success = _IntermediateExclamationDispatch(wch);
break;
Expand Down Expand Up @@ -773,6 +777,43 @@ bool OutputStateMachineEngine::_IntermediateQuestionMarkDispatch(const wchar_t w
return success;
}

// Routine Description:
// - Handles actions that have postfix params on an intermediate '>' or '='.
// Arguments:
// - wch - Character to dispatch.
// - intermediate - The intermediate character.
// - parameters - Set of numeric parameters collected while parsing the sequence.
// Return Value:
// - True if handled successfully. False otherwise.
bool OutputStateMachineEngine::_IntermediateGreaterThanOrEqualDispatch(const wchar_t wch,
const wchar_t intermediate,
const std::basic_string_view<size_t> parameters)
{
bool success = false;

switch (wch)
{
case VTActionCodes::DA_DeviceAttributes:
if (_VerifyDeviceAttributesParams(parameters))
{
switch (intermediate)
{
case L'>':
success = _dispatch->SecondaryDeviceAttributes();
TermTelemetry::Instance().Log(TermTelemetry::Codes::DA2);
break;
case L'=':
success = _dispatch->TertiaryDeviceAttributes();
TermTelemetry::Instance().Log(TermTelemetry::Codes::DA3);
break;
}
}
break;
}

return success;
}

// Routine Description:
// - Handles actions that have an intermediate '!', such as DECSTR
// Arguments:
Expand Down
3 changes: 3 additions & 0 deletions src/terminal/parser/OutputStateMachineEngine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ namespace Microsoft::Console::VirtualTerminal
const std::basic_string_view<wchar_t> intermediates);
bool _IntermediateQuestionMarkDispatch(const wchar_t wchAction,
const std::basic_string_view<size_t> parameters);
bool _IntermediateGreaterThanOrEqualDispatch(const wchar_t wch,
const wchar_t intermediate,
const std::basic_string_view<size_t> parameters);
bool _IntermediateExclamationDispatch(const wchar_t wch);
bool _IntermediateSpaceDispatch(const wchar_t wchAction,
const std::basic_string_view<size_t> parameters);
Expand Down
2 changes: 2 additions & 0 deletions src/terminal/parser/telemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ void TermTelemetry::WriteFinalTraceLog() const
TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"),
TraceLoggingUInt32(_uiTimesUsed[DSR], "DSR"),
TraceLoggingUInt32(_uiTimesUsed[DA], "DA"),
TraceLoggingUInt32(_uiTimesUsed[DA2], "DA2"),
TraceLoggingUInt32(_uiTimesUsed[DA3], "DA3"),
TraceLoggingUInt32(_uiTimesUsed[VPA], "VPA"),
TraceLoggingUInt32(_uiTimesUsed[HPR], "HPR"),
TraceLoggingUInt32(_uiTimesUsed[VPR], "VPR"),
Expand Down
2 changes: 2 additions & 0 deletions src/terminal/parser/telemetry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ namespace Microsoft::Console::VirtualTerminal
DECKPNM,
DSR,
DA,
DA2,
DA3,
VPA,
HPR,
VPR,
Expand Down
98 changes: 98 additions & 0 deletions src/terminal/parser/ut_parser/OutputEngineTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ class StatefulDispatch final : public TermDispatch
_statusReportType{ (DispatchTypes::AnsiStatusType)-1 },
_deviceStatusReport{ false },
_deviceAttributes{ false },
_secondaryDeviceAttributes{ false },
_tertiaryDeviceAttributes{ false },
_vt52DeviceAttributes{ false },
_isAltBuffer{ false },
_cursorKeysMode{ false },
Expand Down Expand Up @@ -770,6 +772,20 @@ class StatefulDispatch final : public TermDispatch
return true;
}

bool SecondaryDeviceAttributes() noexcept override
{
_secondaryDeviceAttributes = true;

return true;
}

bool TertiaryDeviceAttributes() noexcept override
{
_tertiaryDeviceAttributes = true;

return true;
}

bool Vt52DeviceAttributes() noexcept override
{
_vt52DeviceAttributes = true;
Expand Down Expand Up @@ -976,6 +992,8 @@ class StatefulDispatch final : public TermDispatch
DispatchTypes::AnsiStatusType _statusReportType;
bool _deviceStatusReport;
bool _deviceAttributes;
bool _secondaryDeviceAttributes;
bool _tertiaryDeviceAttributes;
bool _vt52DeviceAttributes;
bool _isAltBuffer;
bool _cursorKeysMode;
Expand Down Expand Up @@ -1804,6 +1822,86 @@ class StateMachineExternalTest final
pDispatch->ClearState();
}

TEST_METHOD(TestSecondaryDeviceAttributes)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));

Log::Comment(L"Test 1: Check default case, no params.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'>');
mach.ProcessCharacter(L'c');

VERIFY_IS_TRUE(pDispatch->_secondaryDeviceAttributes);

pDispatch->ClearState();

Log::Comment(L"Test 2: Check default case, 0 param.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'>');
mach.ProcessCharacter(L'0');
mach.ProcessCharacter(L'c');

VERIFY_IS_TRUE(pDispatch->_secondaryDeviceAttributes);

pDispatch->ClearState();

Log::Comment(L"Test 3: Check fail case, 1 (or any other) param.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'>');
mach.ProcessCharacter(L'1');
mach.ProcessCharacter(L'c');

VERIFY_IS_FALSE(pDispatch->_secondaryDeviceAttributes);

pDispatch->ClearState();
}

TEST_METHOD(TestTertiaryDeviceAttributes)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));

Log::Comment(L"Test 1: Check default case, no params.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'=');
mach.ProcessCharacter(L'c');

VERIFY_IS_TRUE(pDispatch->_tertiaryDeviceAttributes);

pDispatch->ClearState();

Log::Comment(L"Test 2: Check default case, 0 param.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'=');
mach.ProcessCharacter(L'0');
mach.ProcessCharacter(L'c');

VERIFY_IS_TRUE(pDispatch->_tertiaryDeviceAttributes);

pDispatch->ClearState();

Log::Comment(L"Test 3: Check fail case, 1 (or any other) param.");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'[');
mach.ProcessCharacter(L'=');
mach.ProcessCharacter(L'1');
mach.ProcessCharacter(L'c');

VERIFY_IS_FALSE(pDispatch->_tertiaryDeviceAttributes);

pDispatch->ClearState();
}

TEST_METHOD(TestStrings)
{
auto dispatch = std::make_unique<StatefulDispatch>();
Expand Down

0 comments on commit 53b224b

Please sign in to comment.