Repository: microsoft/terminal (language: cpp)

Issue:
Add support for VT100 Answerback Feature
# Add support for VT100 Answerback Feature

I am attempting to use Windows Terminal to execute legacy Linux applications using WSL2, and it's great that Terminal now seems to support most VT100 features, but most of the applications I am attempting to host require the terminal to provide an "answerback message" (string) in response to an ASCII 5 character (Ctrl+E) being received by the terminal. The answerback message must be able to be defined by the user, ideally in settings UI, or at least in the settings file. I believe there is also an "auto answerback" capability, but I am unclear on exactly how that works, but do not need it for my use case.



Discussion / hints:
Might as well, though, out of curiosity, any specific examples when you say

> most of the applications I am attempting to host

I'm under the impression that the console/Terminal does a pretty good job of hosting most linux applications these days, and there aren't that many apps that actually rely on [`ENQ`](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Single-character-functions)
@zadjii-msft, I work for a software tools company supporting a software development environment that has been used for 45 years to develop a significant number of "legacy" applications across all industries. While it is not the case that all such applications rely on terminal answerback, a significant portion do. Currently I am migrating a specific application for a customer, and without terminal answerback it literally won't run. We could re-engineer the code to remove this requirement, but I would consider it a win for Windows Terminal if it could support the feature. Currently we are forced to use a terminal emulator that implements full DEC VT compatibility, such as Reflections, PowerTerm or Putty. It would be a win for us if we could run our apps using native Windows tools, and we are SO CLOSE!
Note that if it needs to be user configurable, then we can't really implement this on the conhost side. It would have to be passed through to WT and have the answerback message configured somewhere in the settings (probably at the profile level).

One other possibility would be to support the `DECLANS` escape sequence for defining the answerback, and that way we could avoid having to add a setting for it. However, that could be a bit of a security risk, unless maybe limited to printable characters, and even then I'm not sure that's wise.
Another thought: I know we don't want to add more stuff to the legacy conhost properties dialog, but can we add new properties to the console registry area? It's obviously not very user-friendly, but it would be a quick way to get the functionality implemented on the conhost side, and then we could leave the WT settings UI until later.
@j4james For my use case, I'm totally fine if there is no UI, as long as I can specify the answerback value in a configuration file.

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index fd80a86e42d..720fc383b01 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -38,6 +38,7 @@ ANSISYS
 ANSISYSRC
 ANSISYSSC
 answerback
+ANSWERBACKMESSAGE
 antialiasing
 ANull
 anycpu
diff --git a/dep/Console/winconp.h b/dep/Console/winconp.h
index 23fba76c144..8c247eaf3f8 100644
--- a/dep/Console/winconp.h
+++ b/dep/Console/winconp.h
@@ -104,6 +104,7 @@ GetConsoleKeyboardLayoutNameW(
 #define CONSOLE_REGISTRY_DEFAULTFOREGROUND             L"DefaultForeground"
 #define CONSOLE_REGISTRY_DEFAULTBACKGROUND             L"DefaultBackground"
 #define CONSOLE_REGISTRY_TERMINALSCROLLING             L"TerminalScrolling"
+#define CONSOLE_REGISTRY_ANSWERBACKMESSAGE             L"AnswerbackMessage"
 // end V2 console settings
 
     /*
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 931661698c4..e851d3c284b 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -3032,6 +3032,10 @@
           "description": "By default Windows treats Ctrl+Alt as an alias for AltGr. When altGrAliasing is set to false, this behavior will be disabled.",
           "type": "boolean"
         },
+        "answerbackMessage": {
+          "description": "The response that is sent when an ENQ control character is received.",
+          "type": "string"
+        },
         "source": {
           "description": "Stores the name of the profile generator that originated this profile.",
           "type": [
diff --git a/src/cascadia/TerminalCore/ICoreSettings.idl b/src/cascadia/TerminalCore/ICoreSettings.idl
index 115fdce5a62..112a6f6b1f2 100644
--- a/src/cascadia/TerminalCore/ICoreSettings.idl
+++ b/src/cascadia/TerminalCore/ICoreSettings.idl
@@ -14,6 +14,7 @@ namespace Microsoft.Terminal.Core
 
         Boolean SnapOnInput;
         Boolean AltGrAliasing;
+        String AnswerbackMessage;
 
         String StartingTitle;
         Boolean SuppressApplicationTitle;
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 2aec6b1445a..e6af6c22ee8 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -82,6 +82,7 @@ void Terminal::UpdateSettings(ICoreSettings settings)
 
     _snapOnInput = settings.SnapOnInput();
     _altGrAliasing = settings.AltGrAliasing();
+    _answerbackMessage = settings.AnswerbackMessage();
     _wordDelimiters = settings.WordDelimiters();
     _suppressApplicationTitle = settings.SuppressApplicationTitle();
     _startingTitle = settings.StartingTitle();
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 2c4b72fe9d3..3b6a8b1612e 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -135,6 +135,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetTextAttributes(const TextAttribute& attrs) noexcept override;
     void SetSystemMode(const Mode mode, const bool enabled) noexcept override;
     bool GetSystemMode(const Mode mode) const noexcept override;
+    void ReturnAnswerback() override;
     void WarningBell() override;
     void SetWindowTitle(const std::wstring_view title) override;
     CursorType GetUserDefaultCursorStyle() const noexcept override;
@@ -371,6 +372,7 @@ class Microsoft::Terminal::Core::Terminal final :
 
     size_t _hyperlinkPatternId = 0;
 
+    std::wstring _answerbackMessage;
     std::wstring _workingDirectory;
 
     // This default fake font value is only used to check if the font is a raster font.
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 5982d184c52..5e94972d58b 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -71,6 +71,11 @@ bool Terminal::GetSystemMode(const Mode mode) const noexcept
     return _systemMode.test(mode);
 }
 
+void Terminal::ReturnAnswerback()
+{
+    ReturnResponse(_answerbackMessage);
+}
+
 void Terminal::WarningBell()
 {
     _pfnWarningBell();
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index ad0eaad4c0a..397a852c581 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -81,6 +81,7 @@ Author(s):
     X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE)                                                                                               \
     X(bool, SnapOnInput, "snapOnInput", true)                                                                                                                  \
     X(bool, AltGrAliasing, "altGrAliasing", true)                                                                                                              \
+    X(hstring, AnswerbackMessage, "answerbackMessage")                                                                                                         \
     X(hstring, Commandline, "commandline", L"%SystemRoot%\\System32\\cmd.exe")                                                                                 \
     X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible)                      \
     X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl
index e99ab3c0b86..1ba21c4086c 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.idl
+++ b/src/cascadia/TerminalSettingsModel/Profile.idl
@@ -73,6 +73,7 @@ namespace Microsoft.Terminal.Settings.Model
         INHERITABLE_PROFILE_SETTING(Int32, HistorySize);
         INHERITABLE_PROFILE_SETTING(Boolean, SnapOnInput);
         INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing);
+        INHERITABLE_PROFILE_SETTING(String, AnswerbackMessage);
         INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle);
 
         INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap<String COMMA String>, EnvironmentVariables);
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
index 52b3d030f93..9e4c0ba9864 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
@@ -284,6 +284,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         _HistorySize = profile.HistorySize();
         _SnapOnInput = profile.SnapOnInput();
         _AltGrAliasing = profile.AltGrAliasing();
+        _AnswerbackMessage = profile.AnswerbackMessage();
 
         // Fill in the remaining properties from the profile
         _ProfileName = profile.Name();
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
index ee4e0ca939d..dc478ddb838 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
@@ -86,6 +86,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
         INHERITABLE_SETTING(Model::TerminalSettings, bool, SnapOnInput, true);
         INHERITABLE_SETTING(Model::TerminalSettings, bool, AltGrAliasing, true);
+        INHERITABLE_SETTING(Model::TerminalSettings, hstring, AnswerbackMessage);
         INHERITABLE_SETTING(Model::TerminalSettings, til::color, CursorColor, DEFAULT_CURSOR_COLOR);
         INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Core::CursorStyle, CursorShape, Core::CursorStyle::Vintage);
         INHERITABLE_SETTING(Model::TerminalSettings, uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT);
diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h
index 6f70a868bf6..4698045f9ea 100644
--- a/src/cascadia/inc/ControlProperties.h
+++ b/src/cascadia/inc/ControlProperties.h
@@ -38,6 +38,7 @@
     X(int32_t, InitialCols, 80)                                                                                   \
     X(bool, SnapOnInput, true)                                                                                    \
     X(bool, AltGrAliasing, true)                                                                                  \
+    X(winrt::hstring, AnswerbackMessage)                                                                          \
     X(winrt::hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS)                                                    \
     X(bool, CopyOnSelect, false)                                                                                  \
     X(bool, FocusFollowMouse, false)                                                                              \
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index a719f7a63c6..5edd6927c43 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -146,6 +146,16 @@ bool ConhostInternalGetSet::GetSystemMode(const Mode mode) const
     }
 }
 
+// Routine Description:
+// - Sends the configured answerback message in response to an ENQ query.
+// Return Value:
+// - <none>
+void ConhostInternalGetSet::ReturnAnswerback()
+{
+    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    ReturnResponse(gci.GetAnswerbackMessage());
+}
+
 // Routine Description:
 // - Sends a notify message to play the "SystemHand" sound event.
 // Return Value:
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 866a6f643dc..27ad3c025d8 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -40,6 +40,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
     void SetSystemMode(const Mode mode, const bool enabled) override;
     bool GetSystemMode(const Mode mode) const override;
 
+    void ReturnAnswerback() override;
     void WarningBell() override;
 
     void SetWindowTitle(const std::wstring_view title) override;
diff --git a/src/host/settings.cpp b/src/host/settings.cpp
index e0b07e38f75..e2dd84b78e7 100644
--- a/src/host/settings.cpp
+++ b/src/host/settings.cpp
@@ -765,6 +765,11 @@ void Settings::SetTerminalScrolling(const bool terminalScrollingEnabled) noexcep
     _TerminalScrolling = terminalScrollingEnabled;
 }
 
+std::wstring_view Settings::GetAnswerbackMessage() const noexcept
+{
+    return _answerbackMessage;
+}
+
 // Determines whether our primary renderer should be DirectX or GDI.
 // This is based on user preference and velocity hold back state.
 bool Settings::GetUseDx() const noexcept
diff --git a/src/host/settings.hpp b/src/host/settings.hpp
index 94c38afbf43..04b169a7b76 100644
--- a/src/host/settings.hpp
+++ b/src/host/settings.hpp
@@ -176,6 +176,8 @@ class Settings
     bool IsTerminalScrolling() const noexcept;
     void SetTerminalScrolling(const bool terminalScrollingEnabled) noexcept;
 
+    std::wstring_view GetAnswerbackMessage() const noexcept;
+
     bool GetUseDx() const noexcept;
     bool GetCopyColor() const noexcept;
     SettingsTextMeasurementMode GetTextMeasurementMode() const noexcept;
@@ -236,5 +238,6 @@ class Settings
     bool _fInterceptCopyPaste;
 
     bool _TerminalScrolling;
+    WCHAR _answerbackMessage[32] = {};
     friend class RegistrySerialization;
 };
diff --git a/src/propslib/RegistrySerialization.cpp b/src/propslib/RegistrySerialization.cpp
index 37ff1c34711..4830a549114 100644
--- a/src/propslib/RegistrySerialization.cpp
+++ b/src/propslib/RegistrySerialization.cpp
@@ -60,6 +60,7 @@ const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_PropertyMa
     { _RegPropertyType::Dword,          CONSOLE_REGISTRY_CURSORTYPE,                    SET_FIELD_AND_SIZE(_CursorType)                  },
     { _RegPropertyType::Boolean,        CONSOLE_REGISTRY_INTERCEPTCOPYPASTE,            SET_FIELD_AND_SIZE(_fInterceptCopyPaste)         },
     { _RegPropertyType::Boolean,        CONSOLE_REGISTRY_TERMINALSCROLLING,             SET_FIELD_AND_SIZE(_TerminalScrolling)           },
+    { _RegPropertyType::String,         CONSOLE_REGISTRY_ANSWERBACKMESSAGE,             SET_FIELD_AND_SIZE(_answerbackMessage)           },
     { _RegPropertyType::Boolean,        CONSOLE_REGISTRY_USEDX,                         SET_FIELD_AND_SIZE(_fUseDx)                      },
     { _RegPropertyType::Boolean,        CONSOLE_REGISTRY_COPYCOLOR,                     SET_FIELD_AND_SIZE(_fCopyColor)                  },
     { _RegPropertyType::Dword,          L"TextMeasurement",                             SET_FIELD_AND_SIZE(_textMeasurement)             },
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index a7af79a0958..01313efb8b4 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -63,6 +63,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
     virtual bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM
     virtual bool SetLeftRightScrollingMargins(const VTInt leftMargin, const VTInt rightMargin) = 0; // DECSLRM
+    virtual bool EnquireAnswerback() = 0; // ENQ
     virtual bool WarningBell() = 0; // BEL
     virtual bool CarriageReturn() = 0; // CR
     virtual bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) = 0; // IND, NEL, LF, FF, VT
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index 0cb900626fc..ec6e1859011 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -64,6 +64,7 @@ namespace Microsoft::Console::VirtualTerminal
         virtual void SetSystemMode(const Mode mode, const bool enabled) = 0;
         virtual bool GetSystemMode(const Mode mode) const = 0;
 
+        virtual void ReturnAnswerback() = 0;
         virtual void WarningBell() = 0;
         virtual void SetWindowTitle(const std::wstring_view title) = 0;
         virtual void UseAlternateScreenBuffer(const TextAttribute& attrs) = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 68cd77bfc47..c4ff50c6a8a 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2475,6 +2475,18 @@ bool AdaptDispatch::SetLeftRightScrollingMargins(const VTInt leftMargin,
     return true;
 }
 
+// Routine Description:
+// - ENQ - Directs the terminal to send the answerback message.
+// Arguments:
+// - None
+// Return Value:
+// - True.
+bool AdaptDispatch::EnquireAnswerback()
+{
+    _api.ReturnAnswerback();
+    return true;
+}
+
 // Routine Description:
 // - BEL - Rings the warning bell.
 //    Causes the terminal to emit an audible tone of brief duration.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index c7a913e2bf7..69c7c6ffe7b 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -101,6 +101,7 @@ namespace Microsoft::Console::VirtualTerminal
                                           const VTInt bottomMargin) override; // DECSTBM
         bool SetLeftRightScrollingMargins(const VTInt leftMargin,
                                           const VTInt rightMargin) override; // DECSLRM
+        bool EnquireAnswerback() override; // ENQ
         bool WarningBell() override; // BEL
         bool CarriageReturn() override; // CR
         bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) override; // IND, NEL, LF, FF, VT
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 1b8ac3986fb..cce60582fd5 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -56,6 +56,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool SetAnsiMode(const bool /*ansiMode*/) override { return false; } // DECANM
     bool SetTopBottomScrollingMargins(const VTInt /*topMargin*/, const VTInt /*bottomMargin*/) override { return false; } // DECSTBM
     bool SetLeftRightScrollingMargins(const VTInt /*leftMargin*/, const VTInt /*rightMargin*/) override { return false; } // DECSLRM
+    bool EnquireAnswerback() override { return false; } // ENQ
     bool WarningBell() override { return false; } // BEL
     bool CarriageReturn() override { return false; } // CR
     bool LineFeed(const DispatchTypes::LineFeedType /*lineFeedType*/) override { return false; } // IND, NEL, LF, FF, VT
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index ca44278b96e..7d6001831ef 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -118,6 +118,11 @@ class TestGetSet final : public ITerminalApi
         return _systemMode.test(mode);
     }
 
+    void ReturnAnswerback()
+    {
+        Log::Comment(L"ReturnAnswerback MOCK called...");
+    }
+
     void WarningBell() override
     {
         Log::Comment(L"WarningBell MOCK called...");
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 82576afd542..38036ce6da8 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -49,9 +49,7 @@ bool OutputStateMachineEngine::ActionExecute(const wchar_t wch)
     switch (wch)
     {
     case AsciiChars::ENQ:
-        // GH#11946: At some point we may want to add support for the VT
-        // answerback feature, which requires responding to an ENQ control
-        // with a user-defined reply, but until then we just ignore it.
+        _dispatch->EnquireAnswerback();
         break;
     case AsciiChars::BEL:
         _dispatch->WarningBell();


Test patch: