Fix two panes being closed when just one is (#17358)

#17333 introduced a regression: While it fixes a recursion *into*
`Pane::Close()` that doesn't fix the recursion outside of it.
In this case, `Close()` raises the `Closed` event which results
in another tab being closed because it's bound to `_RemoveTab`.
The recursion is now obvious, because I made the entire process
synchronous. Previously, it would (hopefully) just be scheduled
after the pane and its content are already gone.

The issue can be fixed by moving the recursion check from
`Pane::Close()` to `TerminalTab::Shutdown()` but I felt like
it would better to fix the issue a bit more thoroughly.

`IPaneContent` can raise a `CloseRequested` event to indicate it wants
to be closed. However, that also contained recursion, because the
content would call its own `Close()` to raise the event, which the
tab catches, calls `Close()` on the `Pane` which calls `Close()` on
the content which raises the event again and so on. That's what was
fixed in #17333 among others. We can do this better by not raising
the event from `IPaneContent::Close()`. Instead, that method will now
be exclusively called by `Pane`. The `CloseRequested` event will now
truly be just a request and nothing more. Furthermore, the ownership
of the event handling was moved from the `TerminalTab` to the `Pane`.

To make all of this a bit simpler and more robust, two new methods
were added to `Pane`: `_takePaneContent` and `_setPaneContent`.
These methods ensure that `Close()` is called on the content,
that the event handlers are always added and revoked
and that the ownership transfers cleanly between panes.

## Validation Steps Performed
* Open 3 tabs, close the middle one 
* Open 3 vertical panes, close the middle one 
* Drag tabs with multiple panes between windows 
This commit is contained in:
Leonard Hecker 2024-06-04 20:58:37 +02:00 committed by GitHub
parent ece0c04c38
commit ce0f8d6db2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 61 additions and 66 deletions

View File

@ -27,10 +27,10 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
static const int AnimationDurationInMilliseconds = 200; static const int AnimationDurationInMilliseconds = 200;
static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds))); static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds)));
Pane::Pane(const IPaneContent& content, const bool lastFocused) : Pane::Pane(IPaneContent content, const bool lastFocused) :
_content{ content },
_lastActive{ lastFocused } _lastActive{ lastFocused }
{ {
_setPaneContent(std::move(content));
_root.Children().Append(_borderFirst); _root.Children().Append(_borderFirst);
const auto& control{ _content.GetRoot() }; const auto& control{ _content.GetRoot() };
@ -985,17 +985,7 @@ void Pane::_ContentLostFocusHandler(const winrt::Windows::Foundation::IInspectab
// - <none> // - <none>
void Pane::Close() void Pane::Close()
{ {
// Pane has two events, CloseRequested and Closed. CloseRequested is raised by the content asking to be closed, _setPaneContent(nullptr);
// but also by the window who owns the tab when it's closing. The event is then caught by the TerminalTab which
// calls Close() which then raises the Closed event. Now, if this is the last pane in the window, this will result
// in the window raising CloseRequested again which leads to infinite recursion, so we need to guard against that.
// Ideally we would have just a single event in the future.
if (_closed)
{
return;
}
_closed = true;
// Fire our Closed event to tell our parent that we should be removed. // Fire our Closed event to tell our parent that we should be removed.
Closed.raise(nullptr, nullptr); Closed.raise(nullptr, nullptr);
} }
@ -1007,7 +997,7 @@ void Pane::Shutdown()
{ {
if (_IsLeaf()) if (_IsLeaf())
{ {
_content.Close(); _setPaneContent(nullptr);
} }
else else
{ {
@ -1411,7 +1401,7 @@ void Pane::_CloseChild(const bool closeFirst)
_borders = _GetCommonBorders(); _borders = _GetCommonBorders();
// take the control, profile, id and isDefTermSession of the pane that _wasn't_ closed. // take the control, profile, id and isDefTermSession of the pane that _wasn't_ closed.
_content = remainingChild->_content; _setPaneContent(remainingChild->_takePaneContent());
_id = remainingChild->Id(); _id = remainingChild->Id();
// Revoke the old event handlers. Remove both the handlers for the panes // Revoke the old event handlers. Remove both the handlers for the panes
@ -1716,6 +1706,34 @@ void Pane::_SetupChildCloseHandlers()
}); });
} }
// With this method you take ownership of the control from this Pane.
// Assign it to another Pane with _setPaneContent() or Close() it.
IPaneContent Pane::_takePaneContent()
{
_closeRequestedRevoker.revoke();
return std::move(_content);
}
// This method safely sets the content of the Pane. It'll ensure to revoke and
// assign event handlers, and to Close() the existing content if there's any.
// The new content can be nullptr to remove any content.
void Pane::_setPaneContent(IPaneContent content)
{
// The IPaneContent::Close() implementation may be buggy and raise the CloseRequested event again.
// _takePaneContent() avoids this as it revokes the event handler.
if (const auto c = _takePaneContent())
{
c.Close();
}
_content = std::move(content);
if (_content)
{
_closeRequestedRevoker = _content.CloseRequested(winrt::auto_revoke, [this](auto&&, auto&&) { Close(); });
}
}
// Method Description: // Method Description:
// - Sets up row/column definitions for this pane. There are three total // - Sets up row/column definitions for this pane. There are three total
// row/cols. The middle one is for the separator. The first and third are for // row/cols. The middle one is for the separator. The first and third are for
@ -2266,8 +2284,7 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
else else
{ {
// Move our control, guid, isDefTermSession into the first one. // Move our control, guid, isDefTermSession into the first one.
_firstChild = std::make_shared<Pane>(_content); _firstChild = std::make_shared<Pane>(_takePaneContent());
_content = nullptr;
_firstChild->_broadcastEnabled = _broadcastEnabled; _firstChild->_broadcastEnabled = _broadcastEnabled;
} }
@ -2462,6 +2479,11 @@ bool Pane::_HasChild(const std::shared_ptr<Pane> child)
}); });
} }
winrt::TerminalApp::TerminalPaneContent Pane::_getTerminalContent() const
{
return _IsLeaf() ? _content.try_as<winrt::TerminalApp::TerminalPaneContent>() : nullptr;
}
// Method Description: // Method Description:
// - Recursive function that finds a pane with the given ID // - Recursive function that finds a pane with the given ID
// Arguments: // Arguments:

View File

@ -62,7 +62,7 @@ struct PaneResources
class Pane : public std::enable_shared_from_this<Pane> class Pane : public std::enable_shared_from_this<Pane>
{ {
public: public:
Pane(const winrt::TerminalApp::IPaneContent& content, Pane(winrt::TerminalApp::IPaneContent content,
const bool lastFocused = false); const bool lastFocused = false);
Pane(std::shared_ptr<Pane> first, Pane(std::shared_ptr<Pane> first,
@ -248,13 +248,13 @@ private:
std::optional<uint32_t> _id; std::optional<uint32_t> _id;
std::weak_ptr<Pane> _parentChildPath{}; std::weak_ptr<Pane> _parentChildPath{};
bool _closed{ false };
bool _lastActive{ false }; bool _lastActive{ false };
winrt::event_token _firstClosedToken{ 0 }; winrt::event_token _firstClosedToken{ 0 };
winrt::event_token _secondClosedToken{ 0 }; winrt::event_token _secondClosedToken{ 0 };
winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker;
winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker; winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker;
winrt::TerminalApp::IPaneContent::CloseRequested_revoker _closeRequestedRevoker;
Borders _borders{ Borders::None }; Borders _borders{ Borders::None };
@ -264,11 +264,10 @@ private:
bool _IsLeaf() const noexcept; bool _IsLeaf() const noexcept;
bool _HasFocusedChild() const noexcept; bool _HasFocusedChild() const noexcept;
void _SetupChildCloseHandlers(); void _SetupChildCloseHandlers();
winrt::TerminalApp::IPaneContent _takePaneContent();
void _setPaneContent(winrt::TerminalApp::IPaneContent content);
bool _HasChild(const std::shared_ptr<Pane> child); bool _HasChild(const std::shared_ptr<Pane> child);
winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const;
{
return _IsLeaf() ? _content.try_as<winrt::TerminalApp::TerminalPaneContent>() : nullptr;
}
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
const float splitSize, const float splitSize,

View File

@ -45,7 +45,6 @@ namespace winrt::TerminalApp::implementation
} }
void ScratchpadContent::Close() void ScratchpadContent::Close()
{ {
CloseRequested.raise(*this, nullptr);
} }
INewContentArgs ScratchpadContent::GetNewTerminalArgs(const BuildStartupKind /* kind */) const INewContentArgs ScratchpadContent::GetNewTerminalArgs(const BuildStartupKind /* kind */) const

View File

@ -47,7 +47,6 @@ namespace winrt::TerminalApp::implementation
} }
void SettingsPaneContent::Close() void SettingsPaneContent::Close()
{ {
CloseRequested.raise(*this, nullptr);
} }
INewContentArgs SettingsPaneContent::GetNewTerminalArgs(const BuildStartupKind /*kind*/) const INewContentArgs SettingsPaneContent::GetNewTerminalArgs(const BuildStartupKind /*kind*/) const

View File

@ -78,8 +78,6 @@ namespace winrt::TerminalApp::implementation
_bellPlayer = nullptr; _bellPlayer = nullptr;
_bellPlayerCreated = false; _bellPlayerCreated = false;
} }
CloseRequested.raise(*this, nullptr);
} }
winrt::hstring TerminalPaneContent::Icon() const winrt::hstring TerminalPaneContent::Icon() const
@ -239,19 +237,20 @@ namespace winrt::TerminalApp::implementation
if (_profile) if (_profile)
{ {
if (_isDefTermSession && _profile.CloseOnExit() == CloseOnExitMode::Automatic)
{
// For 'automatic', we only care about the connection state if we were launched by Terminal
// Since we were launched via defterm, ignore the connection state (i.e. we treat the
// close on exit mode as 'always', see GH #13325 for discussion)
Close();
}
const auto mode = _profile.CloseOnExit(); const auto mode = _profile.CloseOnExit();
if ((mode == CloseOnExitMode::Always) ||
((mode == CloseOnExitMode::Graceful || mode == CloseOnExitMode::Automatic) && newConnectionState == ConnectionState::Closed)) if (
// This one is obvious: If the user asked for "always" we do just that.
(mode == CloseOnExitMode::Always) ||
// Otherwise, and unless the user asked for the opposite of "always",
// close the pane when the connection closed gracefully (not failed).
(mode != CloseOnExitMode::Never && newConnectionState == ConnectionState::Closed) ||
// However, defterm handoff can result in Windows Terminal randomly opening which may be annoying,
// so by default we should at least always close the pane, even if the command failed.
// See GH #13325 for discussion.
(mode == CloseOnExitMode::Automatic && _isDefTermSession))
{ {
Close(); CloseRequested.raise(nullptr, nullptr);
} }
} }
} }
@ -331,7 +330,7 @@ namespace winrt::TerminalApp::implementation
void TerminalPaneContent::_closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, void TerminalPaneContent::_closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Windows::Foundation::IInspectable& /*args*/) const winrt::Windows::Foundation::IInspectable& /*args*/)
{ {
Close(); CloseRequested.raise(nullptr, nullptr);
} }
void TerminalPaneContent::_restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, void TerminalPaneContent::_restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,

View File

@ -947,26 +947,6 @@ namespace winrt::TerminalApp::implementation
auto dispatcher = TabViewItem().Dispatcher(); auto dispatcher = TabViewItem().Dispatcher();
ContentEventTokens events{}; ContentEventTokens events{};
events.CloseRequested = content.CloseRequested(
winrt::auto_revoke,
[this](auto&& sender, auto&&) {
if (const auto content{ sender.try_as<TerminalApp::IPaneContent>() })
{
// Calling Close() while walking the tree is not safe, because Close() mutates the tree.
const auto pane = _rootPane->_FindPane([&](const auto& p) -> std::shared_ptr<Pane> {
if (p->GetContent() == content)
{
return p;
}
return {};
});
if (pane)
{
pane->Close();
}
}
});
events.TitleChanged = content.TitleChanged( events.TitleChanged = content.TitleChanged(
winrt::auto_revoke, winrt::auto_revoke,
[dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {

View File

@ -134,7 +134,6 @@ namespace winrt::TerminalApp::implementation
winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged; winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged;
winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged; winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged;
winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested; winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested;
winrt::TerminalApp::IPaneContent::CloseRequested_revoker CloseRequested;
// These events literally only apply if the content is a TermControl. // These events literally only apply if the content is a TermControl.
winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent; winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent;

View File

@ -431,12 +431,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
try try
{ {
// GH#11556 - make sure to format the error code to this string as an UNSIGNED int // GH#11556 - make sure to format the error code to this string as an UNSIGNED int
winrt::hstring exitText{ fmt::format(std::wstring_view{ RS_(L"ProcessExited") }, fmt::format(_errorFormat, status)) }; const auto msg1 = fmt::format(std::wstring_view{ RS_(L"ProcessExited") }, fmt::format(_errorFormat, status));
TerminalOutput.raise(L"\r\n"); const auto msg2 = RS_(L"CtrlDToClose");
TerminalOutput.raise(exitText); const auto msg = fmt::format(FMT_COMPILE(L"\r\n{}\r\n{}\r\n"), msg1, msg2);
TerminalOutput.raise(L"\r\n"); TerminalOutput.raise(msg);
TerminalOutput.raise(RS_(L"CtrlDToClose"));
TerminalOutput.raise(L"\r\n");
} }
CATCH_LOG(); CATCH_LOG();
} }