Add support for DA2 and DA3 device attributes reports (#6850)

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
This commit is contained in:
James Holderness 2020-07-10 23:27:47 +01:00 committed by GitHub
parent 3388a486dc
commit 53b224b1c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 0 deletions

View file

@ -94,6 +94,8 @@ public:
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

View file

@ -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.

View file

@ -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

View file

@ -88,6 +88,8 @@ public:
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

View file

@ -1688,6 +1688,42 @@ public:
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...");

View file

@ -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;
@ -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:

View file

@ -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);

View file

@ -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"),

View file

@ -52,6 +52,8 @@ namespace Microsoft::Console::VirtualTerminal
DECKPNM,
DSR,
DA,
DA2,
DA3,
VPA,
HPR,
VPR,

View file

@ -594,6 +594,8 @@ public:
_statusReportType{ (DispatchTypes::AnsiStatusType)-1 },
_deviceStatusReport{ false },
_deviceAttributes{ false },
_secondaryDeviceAttributes{ false },
_tertiaryDeviceAttributes{ false },
_vt52DeviceAttributes{ false },
_isAltBuffer{ false },
_cursorKeysMode{ false },
@ -770,6 +772,20 @@ public:
return true;
}
bool SecondaryDeviceAttributes() noexcept override
{
_secondaryDeviceAttributes = true;
return true;
}
bool TertiaryDeviceAttributes() noexcept override
{
_tertiaryDeviceAttributes = true;
return true;
}
bool Vt52DeviceAttributes() noexcept override
{
_vt52DeviceAttributes = true;
@ -976,6 +992,8 @@ public:
DispatchTypes::AnsiStatusType _statusReportType;
bool _deviceStatusReport;
bool _deviceAttributes;
bool _secondaryDeviceAttributes;
bool _tertiaryDeviceAttributes;
bool _vt52DeviceAttributes;
bool _isAltBuffer;
bool _cursorKeysMode;
@ -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>();