Repository: microsoft/terminal (language: cpp)

Issue:
Feature Request: option to relaunch a profile when it exits
If I have a ssh.exe session open to another machine using ssh.exe (Openssh) and I disconnect from the network the session hangs in the Windows Terminal. I must close the tab. Could we make the session resume by pressing enter? Or is there a better way to handle this?


 {
            "guid": "{5f77e6d6-8215-45d0-be70-a277c72accf6}",
            "hidden": false,
            "name": "user@host",
            "commandline": "ssh.exe user@host",
            "fontFace": "Consolas",
            "fontSize": 12,
            "icon": "%USERPROFILE%\\AppData\\Local\\lxss\\bash.ico"
        },


Discussion / hints:
Thanks for the request! I've triaged it and converted it into a request to relaunch a profile when it exits.
For now, you can relaunch a new copy of that profile with <kbd>ctrl+shift+d</kbd> (`duplicateTab`)
Would relaunching the profile preserve the screen buffer, window title, and WT_SESSION environment variable?
From #4772: It might be nice if this was an enum value for `closeOnExit`. Something like `"reconnect"` to automatically reconnect (or reconnect after prompting for <kbd>enter</kbd> or something) to re-spawn the connection. 

There's also #3726 for "manually restart a connection", which is similar, but _different_. 
I wrote a duplicate for this (which I have now closed) that might contain some more context: https://github.com/microsoft/terminal/issues/10075 But in short, this happens all the time when in hibernation or on bad Wifi, and I want to preserve my screen buffer etc. to continue where I left of.

One solution would be to display some text at the bottom of the buffer similar to how like done here in the April 2021 release of VS Code: https://code.visualstudio.com/updates/v1_56#_untitled-editors-hint. So a `Press `<kbd>Enter</kbd>` to relaunch shell` might be an option.

But I also do like the `closeOnExit: "reconnect"` as an added thing for users.
We need to reconnect in the same tab. 
* It will keep the order of the tabs
* No need to close the original tab manually
* It will keep the screen buffer
I have created a very simple little powershell script one can utilize to sort of replicate this behavior.

Save the following script to: `C:\Windows\System32\Keep-Alive.ps1`

```
$command=$args[0]

while ($TRUE) {
    Invoke-Expression $command
    Start-Sleep -Seconds 1
    Write-Output "Scope ended. Press Enter to reload."
    Read-Host
}
```

Then you can put your SSH or whatnot call nested within this function call to loop it on exit.

For example I put the following as my command for my profile's Command Line field:

`PowerShell Keep-Alive 'ssh rancher@Usagi -i \"C:\Users\MyName\Documents\SSH Keys\Usagi\Usagi.pem\"'`

Then you get something like the following:

![image](https://user-images.githubusercontent.com/16726175/130714717-8463f1b5-003e-42fe-9326-a6d376ffbfa2.png)



Highly needed feature for the reasons mentioned above.
Comparison: iterm2 for mac has this feature and would be great we have in windows terminal too.

WT is great but can be even better and the greatest terminal emulator out there.

Fix patch:
diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt
index eaa0a471190..af75e17ab2a 100644
--- a/.github/actions/spelling/allow/allow.txt
+++ b/.github/actions/spelling/allow/allow.txt
@@ -12,6 +12,7 @@ clickable
 clig
 CMMI
 copyable
+CtrlDToClose
 cybersecurity
 dalet
 Dcs
@@ -22,6 +23,7 @@ downside
 downsides
 dze
 dzhe
+DTo
 EDDB
 EDDC
 Enum'd
diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp
index b1060590d4e..ca982bcffc8 100644
--- a/src/cascadia/TerminalApp/Pane.cpp
+++ b/src/cascadia/TerminalApp/Pane.cpp
@@ -44,6 +44,7 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo
 
     _connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
     _warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler });
+    _closeTerminalRequestedToken = _control.CloseTerminalRequested({ this, &Pane::_CloseTerminalRequestedHandler });
 
     // On the first Pane's creation, lookup resources we'll use to theme the
     // Pane, including the brushed to use for the focused/unfocused border
@@ -993,7 +994,7 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio
     // actually no longer _our_ control, and instead could be a descendant.
     //
     // When the control's new Pane takes ownership of the control, the new
-    // parent will register it's own event handler. That event handler will get
+    // parent will register its own event handler. That event handler will get
     // fired after this handler returns, and will properly cleanup state.
     if (!_IsLeaf())
     {
@@ -1036,6 +1037,26 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio
     }
 }
 
+void Pane::_CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                          const winrt::Windows::Foundation::IInspectable& /*args*/)
+{
+    std::unique_lock lock{ _createCloseLock };
+
+    // It's possible that this event handler started being executed, then before
+    // we got the lock, another thread created another child. So our control is
+    // actually no longer _our_ control, and instead could be a descendant.
+    //
+    // When the control's new Pane takes ownership of the control, the new
+    // parent will register its own event handler. That event handler will get
+    // fired after this handler returns, and will properly cleanup state.
+    if (!_IsLeaf())
+    {
+        return;
+    }
+
+    Close();
+}
+
 winrt::fire_and_forget Pane::_playBellSound(winrt::Windows::Foundation::Uri uri)
 {
     auto weakThis{ weak_from_this() };
@@ -1586,6 +1607,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
         // Add our new event handler before revoking the old one.
         _connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
         _warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler });
+        _closeTerminalRequestedToken = _control.CloseTerminalRequested({ this, &Pane::_CloseTerminalRequestedHandler });
 
         // Revoke the old event handlers. Remove both the handlers for the panes
         // themselves closing, and remove their handlers for their controls
@@ -1601,6 +1623,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
                 {
                     p->_control.ConnectionStateChanged(p->_connectionStateChangedToken);
                     p->_control.WarningBell(p->_warningBellToken);
+                    p->_control.CloseTerminalRequested(p->_closeTerminalRequestedToken);
                 }
             });
         }
@@ -1609,6 +1632,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
         remainingChild->Closed(remainingChildClosedToken);
         remainingChild->_control.ConnectionStateChanged(remainingChild->_connectionStateChangedToken);
         remainingChild->_control.WarningBell(remainingChild->_warningBellToken);
+        remainingChild->_control.CloseTerminalRequested(remainingChild->_closeTerminalRequestedToken);
 
         // If we or either of our children was focused, we want to take that
         // focus from them.
@@ -1690,6 +1714,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
                 {
                     p->_control.ConnectionStateChanged(p->_connectionStateChangedToken);
                     p->_control.WarningBell(p->_warningBellToken);
+                    p->_control.CloseTerminalRequested(p->_closeTerminalRequestedToken);
                 }
             });
         }
@@ -2448,6 +2473,8 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
         _connectionStateChangedToken.value = 0;
         _control.WarningBell(_warningBellToken);
         _warningBellToken.value = 0;
+        _control.CloseTerminalRequested(_closeTerminalRequestedToken);
+        _closeTerminalRequestedToken.value = 0;
 
         // Remove our old GotFocus handler from the control. We don't want the
         // control telling us that it's now focused, we want it telling its new
diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h
index c81bd3fd096..70f7fbfd7a2 100644
--- a/src/cascadia/TerminalApp/Pane.h
+++ b/src/cascadia/TerminalApp/Pane.h
@@ -237,6 +237,7 @@ class Pane : public std::enable_shared_from_this<Pane>
     winrt::event_token _firstClosedToken{ 0 };
     winrt::event_token _secondClosedToken{ 0 };
     winrt::event_token _warningBellToken{ 0 };
+    winrt::event_token _closeTerminalRequestedToken{ 0 };
 
     winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker;
     winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker;
@@ -290,6 +291,7 @@ class Pane : public std::enable_shared_from_this<Pane>
                                  const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
     void _ControlLostFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
                                   const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
+    void _CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
 
     std::pair<float, float> _CalcChildrenSizes(const float fullSize) const;
     SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const;
diff --git a/src/cascadia/TerminalConnection/ConnectionStateHolder.h b/src/cascadia/TerminalConnection/ConnectionStateHolder.h
index 947e16788f3..f037b6e9c9c 100644
--- a/src/cascadia/TerminalConnection/ConnectionStateHolder.h
+++ b/src/cascadia/TerminalConnection/ConnectionStateHolder.h
@@ -86,6 +86,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
             return _isStateOneOf(ConnectionState::Connected);
         }
 
+        void _resetConnectionState()
+        {
+            {
+                std::lock_guard<std::mutex> stateLock{ _stateMutex };
+                _connectionState = ConnectionState::NotConnected;
+            }
+        }
+
     private:
         std::atomic<ConnectionState> _connectionState{ ConnectionState::NotConnected };
         mutable std::mutex _stateMutex;
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp
index 76922788845..659cfc4bd47 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.cpp
+++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp
@@ -205,8 +205,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
                                        const HANDLE hServerProcess,
                                        const HANDLE hClientProcess,
                                        TERMINAL_STARTUP_INFO startupInfo) :
-        _initialRows{ 25 },
-        _initialCols{ 80 },
+        _rows{ 25 },
+        _cols{ 80 },
         _guid{ Utils::CreateGuid() },
         _inPipe{ hIn },
         _outPipe{ hOut }
@@ -268,8 +268,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
             _commandline = winrt::unbox_value_or<winrt::hstring>(settings.TryLookup(L"commandline").try_as<Windows::Foundation::IPropertyValue>(), _commandline);
             _startingDirectory = winrt::unbox_value_or<winrt::hstring>(settings.TryLookup(L"startingDirectory").try_as<Windows::Foundation::IPropertyValue>(), _startingDirectory);
             _startingTitle = winrt::unbox_value_or<winrt::hstring>(settings.TryLookup(L"startingTitle").try_as<Windows::Foundation::IPropertyValue>(), _startingTitle);
-            _initialRows = winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialRows").try_as<Windows::Foundation::IPropertyValue>(), _initialRows);
-            _initialCols = winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialCols").try_as<Windows::Foundation::IPropertyValue>(), _initialCols);
+            _rows = winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialRows").try_as<Windows::Foundation::IPropertyValue>(), _rows);
+            _cols = winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialCols").try_as<Windows::Foundation::IPropertyValue>(), _cols);
             _guid = winrt::unbox_value_or<winrt::guid>(settings.TryLookup(L"guid").try_as<Windows::Foundation::IPropertyValue>(), _guid);
             _environment = settings.TryLookup(L"environment").try_as<Windows::Foundation::Collections::ValueSet>();
             if constexpr (Feature_VtPassthroughMode::IsEnabled())
@@ -307,9 +307,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
     void ConptyConnection::Start()
     try
     {
+        bool usingExistingBuffer = false;
+        if (_isStateAtOrBeyond(ConnectionState::Closed))
+        {
+            _resetConnectionState();
+            usingExistingBuffer = true;
+        }
+
         _transitionToState(ConnectionState::Connecting);
 
-        const til::size dimensions{ gsl::narrow<til::CoordType>(_initialCols), gsl::narrow<til::CoordType>(_initialRows) };
+        const til::size dimensions{ gsl::narrow<til::CoordType>(_cols), gsl::narrow<til::CoordType>(_rows) };
 
         // If we do not have pipes already, then this is a fresh connection... not an inbound one that is a received
         // handoff from an already-started PTY process.
@@ -317,6 +324,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
         {
             DWORD flags = PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE;
 
+            // If we're using an existing buffer, we want the new connection
+            // to reuse the existing cursor. When not setting this flag, the
+            // PseudoConsole sends a clear screen VT code which our renderer
+            // interprets into making all the previous lines be outside the
+            // current viewport.
+            if (usingExistingBuffer)
+            {
+                flags |= PSEUDOCONSOLE_INHERIT_CURSOR;
+            }
+
             if constexpr (Feature_VtPassthroughMode::IsEnabled())
             {
                 if (_passthroughMode)
@@ -440,6 +457,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
             winrt::hstring exitText{ fmt::format(std::wstring_view{ RS_(L"ProcessExited") }, fmt::format(_errorFormat, status)) };
             _TerminalOutputHandlers(L"\r\n");
             _TerminalOutputHandlers(exitText);
+            _TerminalOutputHandlers(L"\r\n");
+            _TerminalOutputHandlers(RS_(L"CtrlDToClose"));
+            _TerminalOutputHandlers(L"\r\n");
         }
         CATCH_LOG();
     }
@@ -492,15 +512,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
 
     void ConptyConnection::Resize(uint32_t rows, uint32_t columns)
     {
-        // If we haven't started connecting at all, it's still fair to update
-        // the initial rows and columns before we set things up.
-        if (!_isStateAtOrBeyond(ConnectionState::Connecting))
-        {
-            _initialRows = rows;
-            _initialCols = columns;
-        }
-        // Otherwise, we can really only dispatch a resize if we're already connected.
-        else if (_isConnected())
+        // Always keep these in case we ever want to disconnect/restart
+        _rows = rows;
+        _cols = columns;
+
+        if (_isConnected())
         {
             THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), { Utils::ClampToShortMax(columns, 1), Utils::ClampToShortMax(rows, 1) }));
         }
@@ -548,7 +564,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
     void ConptyConnection::Close() noexcept
     try
     {
-        if (_transitionToState(ConnectionState::Closing))
+        const bool isClosing = _transitionToState(ConnectionState::Closing);
+        if (isClosing || _isStateAtOrBeyond(ConnectionState::Closed))
         {
             // EXIT POINT
 
@@ -579,7 +596,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
                 _hOutputThread.reset();
             }
 
-            _transitionToState(ConnectionState::Closed);
+            if (isClosing)
+            {
+                _transitionToState(ConnectionState::Closed);
+            }
         }
     }
     CATCH_LOG()
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h
index 601141d9852..904eb50ff73 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.h
+++ b/src/cascadia/TerminalConnection/ConptyConnection.h
@@ -72,8 +72,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
         void _indicateExitWithStatus(unsigned int status) noexcept;
         void _ClientTerminated() noexcept;
 
-        til::CoordType _initialRows{};
-        til::CoordType _initialCols{};
+        til::CoordType _rows{};
+        til::CoordType _cols{};
         uint64_t _initialParentHwnd{ 0 };
         hstring _commandline{};
         hstring _startingDirectory{};
diff --git a/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw b/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw
index 3395cb7dd55..3532e310491 100644
--- a/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw
@@ -207,6 +207,10 @@
     <value>[process exited with code {0}]</value>
     <comment>The first argument {0} is the error code of the process. When there is no error, the number ZERO will be displayed. </comment>
   </data>
+  <data name="CtrlDToClose" xml:space="preserve">
+    <value>You can now close this terminal with Ctrl+D, or press Enter to restart.</value>
+	  <comment>"Ctrl+D" and "Enter" represent keys the user will press (control+D and Enter).</comment>
+  </data>
   <data name="ProcessFailedToLaunch" xml:space="preserve">
     <value>[error {0} when launching `{1}']</value>
     <comment>The first argument {0} is the error code. The second argument {1} is the user-specified path to a program.
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 1fe34b73cb5..d1388214a5f 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -381,6 +381,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                     const WORD scanCode,
                                     const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
     {
+        const wchar_t CtrlD = 0x4;
+        const wchar_t Enter = '\r';
+
+        if (_connection.State() >= winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Closed)
+        {
+            if (ch == CtrlD)
+            {
+                _CloseTerminalRequestedHandlers(*this, nullptr);
+                return true;
+            }
+
+            if (ch == Enter)
+            {
+                _connection.Close();
+                _connection.Start();
+                return true;
+            }
+        }
+
         if (ch == L'\x3') // Ctrl+C or Ctrl+Break
         {
             _handleControlC();
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 7d5fa75dd23..890e9ec3d51 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -232,6 +232,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         TYPED_EVENT(ShowWindowChanged,         IInspectable, Control::ShowWindowArgs);
         TYPED_EVENT(UpdateSelectionMarkers,    IInspectable, Control::UpdateSelectionMarkersEventArgs);
         TYPED_EVENT(OpenHyperlink,             IInspectable, Control::OpenHyperlinkEventArgs);
+        TYPED_EVENT(CloseTerminalRequested,    IInspectable, IInspectable);
         // clang-format on
 
     private:
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index a65384e2bc2..a8df501469b 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -161,5 +161,6 @@ namespace Microsoft.Terminal.Control
         event Windows.Foundation.TypedEventHandler<Object, ShowWindowArgs> ShowWindowChanged;
         event Windows.Foundation.TypedEventHandler<Object, UpdateSelectionMarkersEventArgs> UpdateSelectionMarkers;
         event Windows.Foundation.TypedEventHandler<Object, OpenHyperlinkEventArgs> OpenHyperlink;
+        event Windows.Foundation.TypedEventHandler<Object, Object> CloseTerminalRequested;
     };
 }
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index bed685ede24..590780e7524 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -148,6 +148,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         PROJECTED_FORWARDED_TYPED_EVENT(SetTaskbarProgress,     IInspectable, IInspectable, _core, TaskbarProgressChanged);
         PROJECTED_FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable, _core, ConnectionStateChanged);
         PROJECTED_FORWARDED_TYPED_EVENT(ShowWindowChanged,      IInspectable, Control::ShowWindowArgs, _core, ShowWindowChanged);
+        PROJECTED_FORWARDED_TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable, _core, CloseTerminalRequested);
 
         PROJECTED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs, _interactivity, PasteFromClipboard);
 
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index a287989d19f..044bfb0a6b1 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -52,6 +52,8 @@ namespace Microsoft.Terminal.Control
 
         event Windows.Foundation.TypedEventHandler<Object, ShowWindowArgs> ShowWindowChanged;
 
+        event Windows.Foundation.TypedEventHandler<Object, Object> CloseTerminalRequested;
+
         Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference<CopyFormat> formats);
         void PasteTextFromClipboard();
         void SelectAll();


Test patch: