Skip to content
1 change: 1 addition & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,7 @@ IME
CLDR
doy
Hinnant
IDT
LONGDATE
MRT
Uninstalls
35 changes: 34 additions & 1 deletion src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing;

namespace Protocol = winrt::Microsoft::Terminal::Protocol;

// Static state — set once before registration, never mutated.
// Static state
// s_emperor: set once before registration, never mutated.
// s_emperorHwnd, s_liveObjectCount: mutable at runtime (see s_setEmperorHwnd, constructor/destructor).
WindowEmperor* TerminalProtocolComServer::s_emperor = nullptr;
std::atomic<HWND> TerminalProtocolComServer::s_emperorHwnd{ nullptr };
std::atomic<int32_t> TerminalProtocolComServer::s_liveObjectCount{ 0 };

static DWORD g_comRegistration = 0;
static std::shared_mutex g_mtx;
Expand All @@ -36,6 +40,27 @@ void TerminalProtocolComServer::s_setEmperor(WindowEmperor* emperor) noexcept
s_emperor = emperor;
}

void TerminalProtocolComServer::s_setEmperorHwnd(HWND hwnd) noexcept
{
s_emperorHwnd.store(hwnd, std::memory_order_release);
}
Comment thread
yeelam-gordon marked this conversation as resolved.

int32_t TerminalProtocolComServer::s_GetLiveObjectCount() noexcept
{
return s_liveObjectCount.load(std::memory_order_relaxed);
}

// Post a message to the emperor's UI thread to re-evaluate idle state.
// Called from the COM MTA thread — PostMessage is thread-safe.
void TerminalProtocolComServer::s_notifyEmperorIdleCheck()
{
const auto hwnd = TerminalProtocolComServer::s_emperorHwnd.load(std::memory_order_acquire);
if (hwnd)
{
PostMessage(hwnd, WindowEmperor::WM_COM_IDLE_CHECK, 0, 0);
}
}

HRESULT TerminalProtocolComServer::s_StartListening()
try
{
Expand Down Expand Up @@ -96,9 +121,17 @@ HRESULT TerminalProtocolComServer::s_StopListening()
return S_OK;
}

TerminalProtocolComServer::TerminalProtocolComServer()
{
s_liveObjectCount.fetch_add(1, std::memory_order_relaxed);
s_notifyEmperorIdleCheck();
}
Comment thread
yeelam-gordon marked this conversation as resolved.

TerminalProtocolComServer::~TerminalProtocolComServer()
{
_removeInstance();
s_liveObjectCount.fetch_sub(1, std::memory_order_relaxed);
s_notifyEmperorIdleCheck();
}

void TerminalProtocolComServer::_addInstance()
Expand Down
11 changes: 11 additions & 0 deletions src/cascadia/WindowsTerminal/TerminalProtocolComServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma once

#include <atomic>
#include <mutex>
#include <vector>

Expand Down Expand Up @@ -51,6 +52,7 @@ struct Factory : winrt::implements<Factory<T>, IClassFactory, winrt::no_module_l
struct __declspec(uuid(__CLSID_TerminalProtocolServer))
TerminalProtocolComServer : winrt::implements<TerminalProtocolComServer, Protocol::IProtocolServer>
{
TerminalProtocolComServer();
~TerminalProtocolComServer();

// ── IProtocolServer ──
Expand Down Expand Up @@ -98,10 +100,16 @@ TerminalProtocolComServer : winrt::implements<TerminalProtocolComServer, Protoco

// Static setup — must be called before s_StartListening().
static void s_setEmperor(WindowEmperor* emperor) noexcept;
// Store the emperor's hidden HWND so MTA threads can PostMessage to the UI thread.
static void s_setEmperorHwnd(HWND hwnd) noexcept;

static HRESULT s_StartListening();
static HRESULT s_StopListening();

// Live COM object count (all objects, regardless of auth state).
// Used by WindowEmperor to decide when the process is truly idle.
static int32_t s_GetLiveObjectCount() noexcept;

// Called from WindowEmperor after a new AppHost is appended to its
// _windows vector. Re-runs the per-window page event registration so
// that the new window's TerminalPage::ProtocolVtSequenceReceived is
Expand Down Expand Up @@ -129,6 +137,7 @@ TerminalProtocolComServer : winrt::implements<TerminalProtocolComServer, Protoco
void _addInstance();
void _removeInstance();
static void _ensurePageEventsRegistered();
static void s_notifyEmperorIdleCheck();

// Dispatch an {method:"autofix_state"} payload to every window's
// TerminalPage on its UI thread.
Expand All @@ -154,4 +163,6 @@ TerminalProtocolComServer : winrt::implements<TerminalProtocolComServer, Protoco
static void _dispatchResumeInNewAgentTabToPage(const winrt::hstring& eventJson);

static WindowEmperor* s_emperor;
static std::atomic<HWND> s_emperorHwnd;
static std::atomic<int32_t> s_liveObjectCount;
};
66 changes: 66 additions & 0 deletions src/cascadia/WindowsTerminal/WindowEmperor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
WindowEmperor::WindowEmperor() = default;
WindowEmperor::~WindowEmperor()
{
// Clear the HWND before revoking COM to prevent stale PostMessage calls.
TerminalProtocolComServer::s_setEmperorHwnd(nullptr);
// Revoke COM class factory before destroying resources.
LOG_IF_FAILED(TerminalProtocolComServer::s_StopListening());
}
Expand Down Expand Up @@ -284,6 +286,9 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args
_windowCount += 1;
_windows.emplace_back(std::move(host));

// A new window means we're no longer idle — cancel any pending COM idle timer.
_updateComIdleTimer();

// Wire the new window's TerminalPage::ProtocolVtSequenceReceived
// into the COM fan-out so events emitted by panes in this window
// (agent-pane attach_pane / detach_pane, autofix OSC 133;D, etc.)
Expand Down Expand Up @@ -1035,12 +1040,44 @@ void WindowEmperor::_postQuitMessageIfNeeded() const
if (
_messageBoxCount <= 0 &&
_windowCount <= 0 &&
TerminalProtocolComServer::s_GetLiveObjectCount() <= 0 &&
!_app.Logic().Settings().GlobalSettings().AllowHeadless())
Comment thread
yeelam-gordon marked this conversation as resolved.
{
PostQuitMessage(0);
}
}

// Re-evaluates whether the process should schedule an exit.
// When headless with no COM clients, uses a short grace period.
// When headless but COM clients remain, uses a longer timeout to
// cover crashed clients whose stub refs are stuck in COM GC.
// Avoids resetting the timer if the desired timeout hasn't changed,
// so partial COM GC releases don't extend the stale window.
void WindowEmperor::_updateComIdleTimer()
{
const auto headless =
_windowCount <= 0 &&
_messageBoxCount <= 0 &&
!_app.Logic().Settings().GlobalSettings().AllowHeadless();

if (headless)
{
const auto timeout = TerminalProtocolComServer::s_GetLiveObjectCount() > 0
? COM_STALE_TIMEOUT_MS
: COM_IDLE_TIMEOUT_MS;
if (_activeComIdleTimeoutMs != timeout)
Comment on lines +1058 to +1068
Comment on lines +1058 to +1068
{
_activeComIdleTimeoutMs = timeout;
SetTimer(_window.get(), IDT_COM_IDLE, timeout, nullptr);
}
}
else
{
KillTimer(_window.get(), IDT_COM_IDLE);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
_activeComIdleTimeoutMs = 0;
}
}

safe_void_coroutine WindowEmperor::_showMessageBox(winrt::hstring message, bool error)
{
// Prevent the main loop from exiting until the message box is closed.
Expand Down Expand Up @@ -1124,13 +1161,41 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
// Counterpart specific to CreateNewWindow().
_windowCount -= 1;
_postQuitMessageIfNeeded();
_updateComIdleTimer();
return 0;
}
case WM_MESSAGE_BOX_CLOSED:
// Counterpart specific to _showMessageBox().
_messageBoxCount -= 1;
_postQuitMessageIfNeeded();
_updateComIdleTimer();
return 0;
case WM_COM_IDLE_CHECK:
// Posted by the COM MTA thread when a COM object is created or
// destroyed. Re-evaluate: if the process is now truly idle
// (headless AND no COM objects), quit immediately; otherwise
// update the grace-period timer.
_postQuitMessageIfNeeded();
_updateComIdleTimer();
return 0;
case WM_TIMER:
if (wParam == IDT_COM_IDLE)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
KillTimer(_window.get(), IDT_COM_IDLE);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
_activeComIdleTimeoutMs = 0;
// If we're still headless after the grace period, exit.
// Any remaining COM objects belong to crashed clients whose
// stub references haven't been reclaimed by the COM GC yet.
// TerminateProcess (after the message loop) cleans everything.
if (_windowCount <= 0 &&
_messageBoxCount <= 0 &&
!_app.Logic().Settings().GlobalSettings().AllowHeadless())
{
PostQuitMessage(0);
}
return 0;
}
break;
Comment thread
yeelam-gordon marked this conversation as resolved.
case WM_IDENTIFY_ALL_WINDOWS:
for (const auto& host : _windows)
{
Expand Down Expand Up @@ -1658,6 +1723,7 @@ void WindowEmperor::_initializeProtocolServer()
{
// Register COM class factory for cross-process access (runs on MTA thread).
TerminalProtocolComServer::s_setEmperor(this);
TerminalProtocolComServer::s_setEmperorHwnd(_window.get());
if (SUCCEEDED_LOG(TerminalProtocolComServer::s_StartListening()))
{
// Stringify the CLSID so child processes can discover us via CoCreateInstance.
Expand Down
12 changes: 12 additions & 0 deletions src/cascadia/WindowsTerminal/WindowEmperor.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ class WindowEmperor
WM_MESSAGE_BOX_CLOSED,
WM_IDENTIFY_ALL_WINDOWS,
WM_NOTIFY_FROM_NOTIFICATION_AREA,
WM_COM_IDLE_CHECK, // Posted by COM MTA thread when live object count changes
};

// Grace period before exiting when no windows and no COM clients remain.
static constexpr UINT_PTR IDT_COM_IDLE = 42;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
// Grace period after the process becomes fully idle (no windows, no COM).
static constexpr DWORD COM_IDLE_TIMEOUT_MS = 5000;
// Maximum time a headless process waits for stale COM objects to
// disconnect before force-exiting. Covers crashed clients whose
// stub references haven't been reclaimed by the COM garbage collector.
static constexpr DWORD COM_STALE_TIMEOUT_MS = 30000;

WindowEmperor();
~WindowEmperor();

Expand Down Expand Up @@ -68,6 +78,8 @@ class WindowEmperor
LRESULT _messageHandler(HWND window, UINT message, WPARAM wParam, LPARAM lParam) noexcept;
void _createMessageWindow(const wchar_t* className);
void _postQuitMessageIfNeeded() const;
void _updateComIdleTimer();
DWORD _activeComIdleTimeoutMs{ 0 }; // tracks the currently running timer value to avoid resets
safe_void_coroutine _showMessageBox(winrt::hstring message, bool error);
void _notificationAreaMenuRequested(WPARAM wParam);
void _notificationAreaMenuClicked(WPARAM wParam, LPARAM lParam) const;
Expand Down
Loading