Fix a crash when closing panes (#17333)

Calling Close() from within WalkPanes is not safe. Simply using
_FindPane is enough to fix this.

This PR also fixes another potential source of infinite recursion, and
fixes panes being passed by-value into the callbacks.

Closes #17305

## Validation Steps Performed
* Split panes vertically 3 times
* `exit` the middle, the bottom and final one, in that order
* Doesn't crash 
This commit is contained in:
Leonard Hecker 2024-05-30 17:00:56 +02:00 committed by GitHub
parent bdc7c4fdbc
commit baba406a07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 37 additions and 32 deletions

View File

@ -467,7 +467,7 @@ std::shared_ptr<Pane> Pane::NextPane(const std::shared_ptr<Pane> targetPane)
std::shared_ptr<Pane> nextPane = nullptr; std::shared_ptr<Pane> nextPane = nullptr;
auto foundTarget = false; auto foundTarget = false;
auto foundNext = WalkTree([&](auto pane) { auto foundNext = WalkTree([&](const auto& pane) {
// If we are a parent pane we don't want to move to one of our children // If we are a parent pane we don't want to move to one of our children
if (foundTarget && targetPane->_HasChild(pane)) if (foundTarget && targetPane->_HasChild(pane))
{ {
@ -985,6 +985,17 @@ 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,
// 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);
} }
@ -1341,7 +1352,7 @@ std::shared_ptr<Pane> Pane::DetachPane(std::shared_ptr<Pane> pane)
detached->_ApplySplitDefinitions(); detached->_ApplySplitDefinitions();
// Trigger the detached event on each child // Trigger the detached event on each child
detached->WalkTree([](auto pane) { detached->WalkTree([](const auto& pane) {
pane->Detached.raise(pane); pane->Detached.raise(pane);
}); });
@ -1542,7 +1553,7 @@ void Pane::_CloseChild(const bool closeFirst)
{ {
// update our path to our first remaining leaf // update our path to our first remaining leaf
_parentChildPath = _firstChild; _parentChildPath = _firstChild;
_firstChild->WalkTree([](auto p) { _firstChild->WalkTree([](const auto& p) {
if (p->_IsLeaf()) if (p->_IsLeaf())
{ {
return true; return true;
@ -2398,7 +2409,7 @@ void Pane::Id(uint32_t id) noexcept
bool Pane::FocusPane(const uint32_t id) bool Pane::FocusPane(const uint32_t id)
{ {
// Always clear the parent child path if we are focusing a leaf // Always clear the parent child path if we are focusing a leaf
return WalkTree([=](auto p) { return WalkTree([=](const auto& p) {
p->_parentChildPath.reset(); p->_parentChildPath.reset();
if (p->_id == id) if (p->_id == id)
{ {
@ -2421,7 +2432,7 @@ bool Pane::FocusPane(const uint32_t id)
// - true if focus was set // - true if focus was set
bool Pane::FocusPane(const std::shared_ptr<Pane> pane) bool Pane::FocusPane(const std::shared_ptr<Pane> pane)
{ {
return WalkTree([&](auto p) { return WalkTree([&](const auto& p) {
if (p == pane) if (p == pane)
{ {
p->_Focus(); p->_Focus();

View File

@ -248,7 +248,7 @@ 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 };

View File

@ -739,7 +739,7 @@ namespace winrt::TerminalApp::implementation
} }
// Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab
pane->WalkTree([](auto p) { pane->WalkTree([](const auto& p) {
if (const auto control{ p->GetTerminalControl() }) if (const auto control{ p->GetTerminalControl() })
{ {
if (control.ReadOnly()) if (control.ReadOnly())

View File

@ -3812,7 +3812,7 @@ namespace winrt::TerminalApp::implementation
// recipe for disaster. We won't ever open up a tab in this window. // recipe for disaster. We won't ever open up a tab in this window.
newTerminalArgs.Elevate(false); newTerminalArgs.Elevate(false);
const auto newPane = _MakePane(newTerminalArgs, nullptr, connection); const auto newPane = _MakePane(newTerminalArgs, nullptr, connection);
newPane->WalkTree([](auto pane) { newPane->WalkTree([](const auto& pane) {
pane->FinalizeConfigurationGivenDefault(); pane->FinalizeConfigurationGivenDefault();
}); });
_CreateNewTabFromPane(newPane); _CreateNewTabFromPane(newPane);

View File

@ -41,7 +41,7 @@ namespace winrt::TerminalApp::implementation
auto firstId = _nextPaneId; auto firstId = _nextPaneId;
_rootPane->WalkTree([&](std::shared_ptr<Pane> pane) { _rootPane->WalkTree([&](const auto& pane) {
// update the IDs on each pane // update the IDs on each pane
if (pane->_IsLeaf()) if (pane->_IsLeaf())
{ {
@ -203,7 +203,7 @@ namespace winrt::TerminalApp::implementation
{ {
ASSERT_UI_THREAD(); ASSERT_UI_THREAD();
_rootPane->WalkTree([&](std::shared_ptr<Pane> pane) { _rootPane->WalkTree([&](const auto& pane) {
// Attach event handlers to each new pane // Attach event handlers to each new pane
_AttachEventHandlersToPane(pane); _AttachEventHandlersToPane(pane);
if (auto content = pane->GetContent()) if (auto content = pane->GetContent())
@ -275,7 +275,7 @@ namespace winrt::TerminalApp::implementation
_UpdateHeaderControlMaxWidth(); _UpdateHeaderControlMaxWidth();
// Update the settings on all our panes. // Update the settings on all our panes.
_rootPane->WalkTree([&](auto pane) { _rootPane->WalkTree([&](const auto& pane) {
pane->UpdateSettings(settings); pane->UpdateSettings(settings);
return false; return false;
}); });
@ -534,7 +534,7 @@ namespace winrt::TerminalApp::implementation
// Add the new event handlers to the new pane(s) // Add the new event handlers to the new pane(s)
// and update their ids. // and update their ids.
pane->WalkTree([&](auto p) { pane->WalkTree([&](const auto& p) {
_AttachEventHandlersToPane(p); _AttachEventHandlersToPane(p);
if (p->_IsLeaf()) if (p->_IsLeaf())
{ {
@ -624,7 +624,7 @@ namespace winrt::TerminalApp::implementation
// manually. // manually.
_rootPane->Closed(_rootClosedToken); _rootPane->Closed(_rootClosedToken);
auto p = _rootPane; auto p = _rootPane;
p->WalkTree([](auto pane) { p->WalkTree([](const auto& pane) {
pane->Detached.raise(pane); pane->Detached.raise(pane);
}); });
@ -650,7 +650,7 @@ namespace winrt::TerminalApp::implementation
// Add the new event handlers to the new pane(s) // Add the new event handlers to the new pane(s)
// and update their ids. // and update their ids.
pane->WalkTree([&](auto p) { pane->WalkTree([&](const auto& p) {
_AttachEventHandlersToPane(p); _AttachEventHandlersToPane(p);
if (p->_IsLeaf()) if (p->_IsLeaf())
{ {
@ -949,26 +949,20 @@ namespace winrt::TerminalApp::implementation
events.CloseRequested = content.CloseRequested( events.CloseRequested = content.CloseRequested(
winrt::auto_revoke, winrt::auto_revoke,
[dispatcher, weakThis](auto sender, auto&&) -> winrt::fire_and_forget { [this](auto&& sender, auto&&) {
// Don't forget! this ^^^^^^^^ sender can't be a reference, this is a async callback. if (const auto content{ sender.try_as<TerminalApp::IPaneContent>() })
// The lambda lives in the `std::function`-style container owned by `control`. That is, when the
// `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to
// copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines.
// See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
const auto weakThisCopy = weakThis;
co_await wil::resume_foreground(dispatcher);
// Check if Tab's lifetime has expired
if (auto tab{ weakThisCopy.get() })
{ {
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)
{ {
tab->_rootPane->WalkTree([content](std::shared_ptr<Pane> pane) { pane->Close();
if (pane->GetContent() == content)
{
pane->Close();
}
});
} }
} }
}); });