Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix potential lags/deadlocks during tab close #14041

Merged
4 commits merged into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/cascadia/TerminalConnection/ConptyConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,15 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation

_hPC.reset(); // tear down the pseudoconsole (this is like clicking X on a console window)

// CloseHandle() on pipes blocks until any current WriteFile()/ReadFile() has returned.
// CancelSynchronousIo prevents us from deadlocking ourselves.
// At this point in Close(), _inPipe won't be used anymore since the UI parts are torn down.
// _outPipe is probably still stuck in ReadFile() and might currently be written to.
if (_hOutputThread)
{
CancelSynchronousIo(_hOutputThread.get());
}

_inPipe.reset(); // break the pipes
_outPipe.reset();

Expand Down Expand Up @@ -615,10 +624,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
DWORD read{};

const auto readFail{ !ReadFile(_outPipe.get(), _buffer.data(), gsl::narrow_cast<DWORD>(_buffer.size()), &read, nullptr) };

// When we call CancelSynchronousIo() in Close() this is the branch that's taken and gets us out of here.
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
return 0;
}

if (readFail) // reading failed (we must check this first, because read will also be 0.)
{
const auto lastError = GetLastError();
if (lastError != ERROR_BROKEN_PIPE && !_isStateAtOrBeyond(ConnectionState::Closing))
if (lastError != ERROR_BROKEN_PIPE)
{
// EXIT POINT
_indicateExitWithStatus(HRESULT_FROM_WIN32(lastError)); // print a message
Expand All @@ -631,12 +647,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const auto result{ til::u8u16(std::string_view{ _buffer.data(), read }, _u16Str, _u8State) };
if (FAILED(result))
{
if (_isStateAtOrBeyond(ConnectionState::Closing))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we certain that we'll never make it here while a connection is closing? As long as we never transition a connection into the Failed state after we transition it into Closing...

Copy link
Member Author

@lhecker lhecker Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually entirely possible and the source of the race condition that I fixed.
While _isStateAtOrBeyond itself might be atomic, that doesn't mean it protects us from other threads changing the state under us concurrently. Unless I misunderstood your question, I don't believe that this is any more or less correct than before. (But the change is required to make the CancelSynchronousIo handling work correctly.)

{
// This termination was expected.
return 0;
}

// EXIT POINT
_indicateExitWithStatus(result); // print a message
_transitionToState(ConnectionState::Failed);
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalConnection/ConptyConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace wil
{
// These belong in WIL upstream, so when we reingest the change that has them we'll get rid of ours.
using unique_static_pseudoconsole_handle = wil::unique_any<HPCON, decltype(&::ConptyClosePseudoConsole), ::ConptyClosePseudoConsole>;
using unique_static_pseudoconsole_handle = wil::unique_any<HPCON, decltype(&::ConptyClosePseudoConsole), ::ConptyClosePseudoConsoleNoWait>;
}

namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
Expand Down
2 changes: 2 additions & 0 deletions src/inc/conpty-static.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newPare

CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC);

CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleNoWait(HPCON hPC);

CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC);
19 changes: 17 additions & 2 deletions src/interactivity/base/ServiceLocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,29 @@ void ServiceLocator::SetOneCoreTeardownFunction(void (*pfn)()) noexcept
s_oneCoreTeardownFunction = pfn;
}

[[noreturn]] void ServiceLocator::RundownAndExit(const HRESULT hr)
void ServiceLocator::RundownAndExit(const HRESULT hr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes here are scary. Are we sure this won't cause the OneCore console to regress?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I'm not sure, but I don't believe so. The only effective difference is the addition of the thread_local and I could revert the change to s_shutdownLock if you'd like.

{
static thread_local bool preventRecursion = false;
static std::atomic<bool> locked;

// BODGY:
// pRender->TriggerTeardown() might cause another VtEngine pass, which then might fail to write to the IO pipe.
// If that happens it calls VtIo::CloseOutput(), which in turn calls ServiceLocator::RundownAndExit().
// This prevents the unintended recursion and resulting deadlock.
if (std::exchange(preventRecursion, true))
{
return;
}

// MSFT:40146639
// The premise of this function is that 1 thread enters and 0 threads leave alive.
// We need to prevent anyone from calling us until we actually ExitProcess(),
// so that we don't TriggerTeardown() twice. LockConsole() can't be used here,
// because doing so would prevent the render thread from progressing.
AcquireSRWLockExclusive(&s_shutdownLock);
if (locked.exchange(true, std::memory_order_relaxed))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe on all architectures? Is relaxed correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's safe on all architectures. Atomic operations themselves are always exactly as atomic as any other, no matter what memory order is specified. Relaxed only means that no additional memory is synchronized/flushed (which is fine here).

{
Sleep(INFINITE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to confirm.

If we get here, it is the first entry and we take the lock, we proceed .We might cause either...

  1. A recursive call to RundownAndExit
  2. Another thread to RundownAndExit

The first one is fixed by preventRecursion, and the second one is fixed by locked -> Sleep(INFINITE)?

What if we trigger another thread to RundownAndExit while we're waiting on it to complete? Is that possible?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the change to s_shutdownLock is optional. I did it because I initially tried to implement this with a recursive mutex (which SRWLOCK can't do), failed and then wrote the atomic+sleep code, which I kept because I felt like it's easier to see what it does compared to abusing a SRWLOCK for "don't actually lock, only suspend anyone but me".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put a big ol' comment here why it's ok to put Sleep(INFINITE);. I think it'd be nice instead of having to find this PR and read through the discussion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It ended up being a bit of a shorter comment. But I think that one, plus the longer explanation in front of the if condition should sufficiently explain what this does... 🤔

}

// MSFT:15506250
// In VT I/O Mode, a client application might die before we've rendered
Expand Down
5 changes: 1 addition & 4 deletions src/interactivity/inc/ServiceLocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Microsoft::Console::Interactivity
public:
static void SetOneCoreTeardownFunction(void (*pfn)()) noexcept;

[[noreturn]] static void RundownAndExit(const HRESULT hr);
static void RundownAndExit(const HRESULT hr);

// N.B.: Location methods without corresponding creation methods
// automatically create the singleton object on demand.
Expand Down Expand Up @@ -86,7 +86,6 @@ namespace Microsoft::Console::Interactivity

static HWND LocatePseudoWindow(const HWND owner = nullptr /*HWND_DESKTOP = 0*/);

protected:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove protected? Should we keep protected for some of these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class is final so that protected has no effect. It's also not usual / best practice to make classes any more private than necessary. A non-public copy constructor for instance is pretty unusual since you would only be able to use them inside the class. It gets even more unusual when you consider that the functions are explicitly deleted, so not even the class itself can use them. In short, this code was kinda whacky. 😅

ServiceLocator(const ServiceLocator&) = delete;
ServiceLocator& operator=(const ServiceLocator&) = delete;

Expand All @@ -112,7 +111,5 @@ namespace Microsoft::Console::Interactivity
static Globals s_globals;
static bool s_pseudoWindowInitialized;
static wil::unique_hwnd s_pseudoWindow;

static inline SRWLOCK s_shutdownLock = SRWLOCK_INIT;
};
}
1 change: 1 addition & 0 deletions src/winconpty/dll/winconpty.def
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ EXPORTS
ConptyCreatePseudoConsoleAsUser
ConptyResizePseudoConsole
ConptyClosePseudoConsole
ConptyClosePseudoConsoleNoWait
ConptyClearPseudoConsole
ConptyShowHidePseudoConsole
ConptyReparentPseudoConsole
Expand Down
20 changes: 10 additions & 10 deletions src/winconpty/ft_pty/ConPtyTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ void ConPtyTests::CreateConPtyNoPipes()
VERIFY_FAILED(_CreatePseudoConsole(defaultSize, nullptr, nullptr, 0, &pcon));

VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, nullptr, goodOut, 0, &pcon));
_ClosePseudoConsoleMembers(&pcon);
_ClosePseudoConsoleMembers(&pcon, TRUE);

VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, goodIn, nullptr, 0, &pcon));
_ClosePseudoConsoleMembers(&pcon);
_ClosePseudoConsoleMembers(&pcon, TRUE);
}

void ConPtyTests::CreateConPtyBadSize()
Expand Down Expand Up @@ -131,7 +131,7 @@ void ConPtyTests::GoodCreate()
&pcon));

auto closePty = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon);
_ClosePseudoConsoleMembers(&pcon, TRUE);
});
}

Expand Down Expand Up @@ -160,7 +160,7 @@ void ConPtyTests::GoodCreateMultiple()
0,
&pcon1));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon1);
_ClosePseudoConsoleMembers(&pcon1, TRUE);
});

VERIFY_SUCCEEDED(
Expand All @@ -170,7 +170,7 @@ void ConPtyTests::GoodCreateMultiple()
0,
&pcon2));
auto closePty2 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon2);
_ClosePseudoConsoleMembers(&pcon2, TRUE);
});
}

Expand All @@ -197,7 +197,7 @@ void ConPtyTests::SurvivesOnBreakInput()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty);
_ClosePseudoConsoleMembers(&pty, TRUE);
});

DWORD dwExit;
Expand Down Expand Up @@ -242,7 +242,7 @@ void ConPtyTests::SurvivesOnBreakOutput()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty);
_ClosePseudoConsoleMembers(&pty, TRUE);
});

DWORD dwExit;
Expand Down Expand Up @@ -287,7 +287,7 @@ void ConPtyTests::DiesOnBreakBoth()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty);
_ClosePseudoConsoleMembers(&pty, TRUE);
});

DWORD dwExit;
Expand Down Expand Up @@ -358,7 +358,7 @@ void ConPtyTests::DiesOnClose()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty);
_ClosePseudoConsoleMembers(&pty, TRUE);
});

DWORD dwExit;
Expand All @@ -382,7 +382,7 @@ void ConPtyTests::DiesOnClose()
Log::Comment(NoThrowString().Format(L"Sleep a bit to let the process attach"));
Sleep(100);

_ClosePseudoConsoleMembers(&pty);
_ClosePseudoConsoleMembers(&pty, TRUE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y'all don't like default params, huh? :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a C function - that's why it can't have that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also FWIW no, I personally don't like default parameters! I also don't like underdocumented boolean parameters for reasons I've mentioned before, but since this is an internal/in-file interface I don't care as much.


GetExitCodeProcess(hConPtyProcess.get(), &dwExit);
VERIFY_ARE_NOT_EQUAL(dwExit, (DWORD)STILL_ACTIVE);
Expand Down
39 changes: 21 additions & 18 deletions src/winconpty/winconpty.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,10 @@ HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const
// HPCON via the API).
// Arguments:
// - pPty: A pointer to a PseudoConsole struct.
// - wait: If true, waits for conhost/OpenConsole to exit first.
// Return Value:
// - <none>
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty)
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait)
{
if (pPty != nullptr)
{
Expand All @@ -365,19 +366,11 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty)
// has yet to send before we hard kill it.
if (_HandleIsValid(pPty->hConPtyProcess))
{
// If the conhost is already dead, then that's fine. Presumably
// it's finished flushing it's output already.
DWORD dwExit = 0;
// If GetExitCodeProcess failed, it's likely conhost is already dead
// If so, skip waiting regardless of whatever error
// GetExitCodeProcess returned.
// We'll just go straight to killing conhost.
if (GetExitCodeProcess(pPty->hConPtyProcess, &dwExit) && dwExit == STILL_ACTIVE)
if (wait)
{
WaitForSingleObject(pPty->hConPtyProcess, INFINITE);
}

TerminateProcess(pPty->hConPtyProcess, 0);
CloseHandle(pPty->hConPtyProcess);
pPty->hConPtyProcess = nullptr;
}
Expand All @@ -398,13 +391,14 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty)
// PseudoConsoles that were created with CreatePseudoConsole.
// Arguments:
// - pPty: A pointer to a PseudoConsole struct.
// - wait: If true, waits for conhost/OpenConsole to exit first.
// Return Value:
// - <none>
VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty)
static void _ClosePseudoConsole(_In_ PseudoConsole* pPty, BOOL wait) noexcept
{
if (pPty != nullptr)
{
_ClosePseudoConsoleMembers(pPty);
_ClosePseudoConsoleMembers(pPty, wait);
HeapFree(GetProcessHeap(), 0, pPty);
}
}
Expand Down Expand Up @@ -465,7 +459,7 @@ extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
auto pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole));
RETURN_IF_NULL_ALLOC(pPty);
auto cleanupPty = wil::scope_exit([&]() noexcept {
_ClosePseudoConsole(pPty);
_ClosePseudoConsole(pPty, TRUE);
});

wil::unique_handle duplicatedInput;
Expand Down Expand Up @@ -544,13 +538,22 @@ extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newPa
// console window they were running in was closed.
// This can fail if the conhost hosting the pseudoconsole failed to be
// terminated, or if the pseudoconsole was already terminated.
// Waits for conhost/OpenConsole to exit first.
extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC)
{
const auto pPty = (PseudoConsole*)hPC;
if (pPty != nullptr)
{
_ClosePseudoConsole(pPty);
}
_ClosePseudoConsole((PseudoConsole*)hPC, TRUE);
}

// Function Description:
// Closes the conpty and all associated state.
// Client applications attached to the conpty will also behave as though the
// console window they were running in was closed.
// This can fail if the conhost hosting the pseudoconsole failed to be
// terminated, or if the pseudoconsole was already terminated.
// Doesn't wait for conhost/OpenConsole to exit.
extern "C" VOID WINAPI ConptyClosePseudoConsoleNoWait(_In_ HPCON hPC)
{
_ClosePseudoConsole((PseudoConsole*)hPC, FALSE);
}

// NOTE: This one is not defined in the Windows headers but is
Expand Down
3 changes: 1 addition & 2 deletions src/winconpty/winconpty.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty);
HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show);
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty);
VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait);

HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
_In_ COORD size,
Expand Down