Skip to content

Add COM server idle timeout for cleaner MSIX upgrades#22

Open
yeelam-gordon wants to merge 8 commits into
mainfrom
dev/yeelam/com-idle-timeout
Open

Add COM server idle timeout for cleaner MSIX upgrades#22
yeelam-gordon wants to merge 8 commits into
mainfrom
dev/yeelam/com-idle-timeout

Conversation

@yeelam-gordon

Copy link
Copy Markdown
Collaborator

Problem

When all Terminal windows are closed, WindowsTerminal.exe can linger indefinitely because the COM server registration (TerminalProtocolComServer) keeps the process alive. This blocks MSIX package upgrades with error 0x80073D02.

Solution

Add a 5-second idle timeout: when the process has no windows, no message boxes, and no live COM client objects, start a grace-period timer. If the timer fires and the process is still idle, post \WM_QUIT\ to exit cleanly.

Design

  • Live COM object tracking — atomic counter incremented in constructor, decremented in destructor (separate from the authenticated-instance list used for event delivery)
  • Cross-thread notification — COM MTA thread posts \WM_COM_IDLE_CHECK\ to the emperor's UI thread
  • Stateless recomputation — _updateComIdleTimer()\ always recomputes from scratch; never relies on message ordering
  • Double-check on fire — \WM_TIMER\ handler re-verifies all conditions before calling \PostQuitMessage\
  • Cancellation — new window creation or COM client connection cancels the timer
  • 5-second timeout — matches the ATL COM default (\CAtlModule::m_dwTimeOut = 5000)

What's unchanged

The existing non-headless quit path (_postQuitMessageIfNeeded) is completely unchanged. The idle timer is an additional mechanism that only engages when the process would otherwise linger.

Files changed

File Change
\TerminalProtocolComServer.h\ Added constructor, \s_liveObjectCount, \s_emperorHwnd, static helpers
\TerminalProtocolComServer.cpp\ Constructor/destructor update live count + notify emperor
\WindowEmperor.h\ Added \WM_COM_IDLE_CHECK, timer constants, _updateComIdleTimer()\
\WindowEmperor.cpp\ Timer management, message handlers, HWND setup/cleanup

Testing

  • Manual: close all windows → verify process exits within ~5s
  • Manual: close windows with active \wtcli\ client → verify process stays alive until client disconnects
  • Manual: close windows, reopen within 5s → verify timer cancels and app continues normally

When all Terminal windows are closed AND all COM client objects are
released, start a 5-second grace-period timer. If the timer fires
and the process is still idle, post WM_QUIT to exit cleanly.

This prevents WindowsTerminal.exe from lingering indefinitely after
the last window closes (due to COM server registration keeping the
process alive), which blocks MSIX package upgrades with error
0x80073D02.

Design:
- Track live COM objects via atomic counter (constructor/destructor)
- COM MTA thread posts WM_COM_IDLE_CHECK to emperor's UI thread
- _updateComIdleTimer() recomputes state statelessly each time
- WM_TIMER fires -> double-checks conditions -> PostQuitMessage
- New window or COM client cancels the timer
- Non-headless quit path is completely unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 20, 2026 01:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an “idle shutdown” mechanism to Windows Terminal’s out-of-proc COM server path so the process exits shortly after becoming truly idle (no windows, no message boxes, and no connected COM clients), preventing MSIX upgrade failures caused by a lingering WindowsTerminal.exe.

Changes:

  • Track live COM server object instances via an atomic counter and notify the UI thread when the count changes.
  • Add a COM-idle grace-period timer in WindowEmperor that posts WM_QUIT after 5 seconds of sustained idleness.
  • Wire up the emperor message-window HWND so COM MTA threads can PostMessage an idle-check request to the UI thread.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/cascadia/WindowsTerminal/WindowEmperor.h Adds WM_COM_IDLE_CHECK message and COM-idle timer constants + helper declaration.
src/cascadia/WindowsTerminal/WindowEmperor.cpp Implements COM idle timer recomputation and message handling; sets/clears emperor HWND for COM thread notifications.
src/cascadia/WindowsTerminal/TerminalProtocolComServer.h Adds constructor and static state for emperor HWND + live object counter access.
src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp Implements live object counting and cross-thread idle-check posting.

Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated
Comment thread src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Fixed
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Fixed
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Fixed
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Fixed
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.h Fixed
@github-actions

This comment has been minimized.

- Make s_emperorHwnd std::atomic<HWND> to fix data race between
  UI thread (writer) and COM MTA thread (reader)
- Add AllowHeadless() check to idle timer — headless mode stays
  alive indefinitely, matching _postQuitMessageIfNeeded behavior
- Add IDT to spelling expect list

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .github/actions/spelling/expect/expect.txt Fixed
@yeelam-gordon yeelam-gordon requested a review from Copilot May 20, 2026 04:01
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp:131

  • Same as in the constructor: the decrement side of the liveness counter doesn’t need release semantics (and the reader doesn’t need acquire) since no other state is being published/consumed via this atomic. Consider using memory_order_relaxed to keep teardown fast and consistent with the intended use as a plain counter.
TerminalProtocolComServer::~TerminalProtocolComServer()
{
    _removeInstance();
    s_liveObjectCount.fetch_sub(1, std::memory_order_release);
    s_notifyEmperorIdleCheck();
}

Comment thread src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp Outdated
Comment thread src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
- Update static state comment to distinguish immutable (s_emperor)
  from mutable (s_emperorHwnd, s_liveObjectCount) members
- Switch s_liveObjectCount to memory_order_relaxed since it's a
  pure numeric liveness counter with no data dependencies

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@yeelam-gordon

Copy link
Copy Markdown
Collaborator Author

@PankajBhojwani , this is the COMServer will not shutdown itself when no usage one.
Now, set it as 5 seconds.

Resolve merge conflicts:
- TerminalProtocolComServer.h: keep both s_GetLiveObjectCount and s_OnWindowAdded
- WindowEmperor.cpp: keep both idle timer update and s_OnWindowAdded call
- expect.txt: merge both sets of spelling entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The function accessed private static member s_emperorHwnd but was
defined as a file-level static instead of a class static method.
Move it to TerminalProtocolComServer:: scope and declare in header.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 27, 2026 07:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated
@github-actions

This comment has been minimized.

_postQuitMessageIfNeeded now checks s_liveObjectCount so the process
does not exit immediately when windows close while COM clients are
still connected.

_updateComIdleTimer starts a grace-period timer whenever the process
becomes headless (no windows, no message boxes):
  - If no COM objects remain: 5 s (COM_IDLE_TIMEOUT_MS).
  - If COM objects still exist: 30 s (COM_STALE_TIMEOUT_MS) to cover
    crashed clients whose stub references are stuck in COM GC.

When the timer fires the process exits regardless of outstanding COM
objects -- any survivors at that point are stale stubs that will never
be reclaimed.  TerminateProcess (after the message loop) cleans up.

WM_COM_IDLE_CHECK now also calls _postQuitMessageIfNeeded so that a
clean COM disconnect triggers an immediate exit instead of waiting for
the timer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

When multiple COM clients are killed, COM GC releases stubs one at a time.
Each release triggered _updateComIdleTimer() which called SetTimer() again,
restarting the 30s countdown. With 3 staggered releases this could extend
the total wait to 90s+.

Track the currently active timer value in _activeComIdleTimeoutMs and skip
SetTimer() if the desired timeout hasn't changed. Reset to 0 when the timer
fires or is killed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 28, 2026 10:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp
Comment thread src/cascadia/WindowsTerminal/WindowEmperor.cpp
@github-actions

This comment has been minimized.

- Fix _postQuitMessageIfNeeded comment to mention COM object count check
- Document why WM_TIMER intentionally skips s_GetLiveObjectCount:
  stale timer overrides crashed-client stubs; legitimate new connections
  create windows which are checked via _windowCount

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

@check-spelling-bot Report

⚠️ Dictionary not found

Problems were encountered retrieving check dictionaries (cspell:software-terms/dict/webServices.txt cspell:cpp/src/stdlib-cpp.txt cspell:powershell/dict/powershell.txt cspell:gaming-terms/dict/gaming-terms.txt cspell:scala/dict/scala.txt cspell:cpp/src/stdlib-cerrno.txt cspell:docker/src/docker-words.txt cspell:typescript/dict/typescript.txt cspell:npm/dict/npm.txt cspell:elixir/dict/elixir.txt cspell:redis/dict/redis.txt cspell:dart/src/dart.txt cspell:sql/src/sql.txt cspell:cpp/src/lang-jargon.txt cspell:cpp/src/people.txt cspell:java/src/java.txt cspell:php/dict/php.txt cspell:cpp/src/template-strings.txt cspell:java/src/java-terms.txt cspell:python/src/common/extra.txt cspell:cpp/src/stdlib-c.txt cspell:clojure/src/clojure.txt cspell:public-licenses/src/generated/public-licenses.txt cspell:ada/dict/ada.txt cspell:cpp/src/compiler-gcc.txt cspell:shell/dict/shell-all-words.txt cspell:k8s/dict/k8s.txt cspell:cpp/src/stdlib-cmath.txt cspell:html/dict/html.txt cspell:monkeyc/src/monkeyc_keywords.txt cspell:golang/dict/go.txt cspell:lua/dict/lua.txt cspell:cpp/src/lang-keywords.txt cspell:python/src/additional_words.txt cspell:haskell/dict/haskell.txt cspell:cpp/src/compiler-clang-attributes.txt cspell:python/src/python/python.txt cspell:rust/dict/rust.txt cspell:sql/src/tsql.txt cspell:cpp/src/ecosystem.txt cspell:dotnet/dict/dotnet.txt cspell:swift/src/swift.txt cspell:ruby/dict/ruby.txt cspell:public-licenses/src/additional-licenses.txt cspell:fullstack/dict/fullstack.txt cspell:node/dict/node.txt cspell:django/dict/django.txt cspell:python/src/python/python-lib.txt cspell:software-terms/dict/softwareTerms.txt cspell:cpp/src/compiler-msvc.txt cspell:css/dict/css.txt cspell:svelte/dict/svelte.txt cspell:r/src/r.txt cspell:latex/dict/latex.txt).

⚠️ For more information, see check-dictionary-not-found.

🔴 Please review

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

Unrecognized words (127)
adbea
agentic
aiagents
alacritty
arget
asid
askuser
azmcp
bestpractices
caac
CACHEDIR
capturep
cbe
cdfabe
checkmarks
chpwd
Cim
CLAUDECODE
clis
cmdkey
CWDs
Dedented
demotable
DFX
dotent
drx
dtx
eef
eku
ession
extened
ffi
focusp
foob
fooba
footgun
formedness
Ghostty
githubnext
gpt
greenfield
greppable
haikus
inputbox
installable
IOCP
ipfs
keyspace
killp
llm
lrx
LSBs
lsp
lsw
ltx
MBM
mcp
meproj
MMdd
mojibake
mpsc
mtimes
myproj
nafter
ncwd
neww
NOAGGREGATION
noname
nopath
normaliser
normalises
noshortcuts
nrx
nsummary
ntwo
ntx
Nushell
obra
Oids
oobe
ools
openai
parallelizable
peekable
Prereq
PRIs
proactively
prx
psobject
ptx
pytest
qdk
qqqqq
Rasterize
recognises
regen
reparses
replacen
respawning
rmcp
rrx
rtx
rustc
serde
sideload
SIGKILLs
signtool
splitn
splitw
SSZ
submittable
synthesises
THH
toolpath
trn
undercounted
undercounting
unrecognised
usize
vcxprojs
vendored
Wez
wezterm
yeelam
ymdhms
ZDOTDIR
zzzzz
These words are not needed and should be removed Ccc cplusplus ctl Debian dotnet drv endptr EOFs evt Fullwidth gitlab hdr idl IME inbox intelligentterminal Ioctl KVM lbl lld lsb NONINFRINGEMENT notif oss outdir pri prioritization PSobject rcv segfault Signtool sourced SWP Tbl testname transitioning unk unparseable unregisters Virt VMs VTE webpage websites WTCLI xsi

Some files were automatically ignored 🙈

These sample patterns would exclude them:

^\.dotnet\/\.dotnet\/TelemetryStorageService/
^\Q.dotnet/.dotnet/.workloadAdvertisingManifestSentinel10.0.200\E$
^\Q.dotnet/.dotnet/10.0.201.aspNetCertificateSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.dotnetFirstUseSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.toolpath.sentinel\E$
^\Qinstaller/bootstrap/target/.rustc_info.json\E$
^copilot-version\.err$
^copilot-version\.out$

You should consider excluding directory paths (e.g. (?:^|/)vendor/), filenames (e.g. (?:^|/)yarn\.lock$), or file extensions (e.g. \.gz$)

You should consider adding them to:

.github/actions/spelling/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To accept these unrecognized words as correct, update file exclusions, and remove the previously acknowledged and now absent words, you could run the following commands

... in a clone of the git@github.com:microsoft/intelligent-terminal.git repository
on the dev/yeelam/com-idle-timeout branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c/apply.pl' |
perl - 'https://github.com/microsoft/intelligent-terminal/actions/runs/26570460234/attempts/1' &&
git commit -m 'Update check-spelling metadata'
Available 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary

This includes both expected items (2063) from .github/actions/spelling/expect/alphabet.txt .github/actions/spelling/expect/expect.txt .github/actions/spelling/expect/web.txt and unrecognized words (127)

Dictionary Entries Covers Uniquely
cspell:csharp/csharp.txt 32 2 2
cspell:aws/aws.txt 232 2 2
cspell:fonts/fonts.txt 536 1 1

Consider adding to the extra_dictionaries array (in the .github/actions/spelling/config.json file):

    "cspell:csharp/csharp.txt",
    "cspell:aws/aws.txt",
    "cspell:fonts/fonts.txt",

To stop checking additional dictionaries, put (in the .github/actions/spelling/config.json file):

"check_extra_dictionaries": []
Forbidden patterns 🙅 (9)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

Should be nonexistent
\b[Nn]o[nt][- ]existent\b
Should be preexisting
[Pp]re[- ]existing
Should be ; otherwise or . Otherwise

https://study.com/learn/lesson/otherwise-in-a-sentence.html

, [Oo]therwise\b
Should probably be Otherwise,
(?<=\. )Otherwise\s
Complete sentences in parentheticals should not have a space before the period.
\s\.\)(?!.*\}\})
Should be set up (setup is a noun / set up is a verb)
\b[Ss]etup(?= (?:an?|the|to)\b)
Should be reentrant
[Rr]e[- ]entrant
Should be whether or not ...
(?i)\b(?:whe|ra)ther(?:\s\w+)+ or not\.
Should be WinGet
\bWinget\b

Pattern suggestions ✂️ (1)

You could add these patterns to .github/actions/spelling/patterns/75ba38412d70c1019a205fe7ef6e2ef4ec9ce694.txt:

# Automatically suggested patterns

# hit-count: 1 file-count: 1
# python
\b(?i)py(?!gment|gmy|lon|ramid|ro|th)(?=[a-z]{2,})

Alternatively, if a pattern suggestion doesn't make sense for this project, add a # to the beginning of the line in the candidates file with the pattern to stop suggesting it.

Errors, Warnings, and Notices ❌ (9)

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

❌ Errors, Warnings, and Notices Count
⚠️ binary-file 6
ℹ️ candidate-pattern 1
⚠️ check-dictionary-not-found 54
❌ check-file-path 20
❌ forbidden-pattern 16
⚠️ ignored-expect-variant 1
⚠️ noisy-file 7
⚠️ single-line-file 1
⚠️ token-is-substring 5

See ❌ Event descriptions for more information.

✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Comment on lines +1058 to +1068
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 +1187 to +1193
// Any remaining COM objects belong to crashed clients whose
// stub references haven't been reclaimed by the COM GC yet
// (COM GC can take up to 6+ minutes for killed processes).
// We intentionally do NOT check s_GetLiveObjectCount here —
// the stale timer exists precisely to override those stubs.
// A legitimate new connection during this window would have
// created a window (_windowCount > 0), which is checked below.
CLDR
doy
Hinnant
IDT
Comment on lines +1058 to +1068
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 +1187 to +1193
// Any remaining COM objects belong to crashed clients whose
// stub references haven't been reclaimed by the COM GC yet
// (COM GC can take up to 6+ minutes for killed processes).
// We intentionally do NOT check s_GetLiveObjectCount here —
// the stale timer exists precisely to override those stubs.
// A legitimate new connection during this window would have
// created a window (_windowCount > 0), which is checked below.
CLDR
doy
Hinnant
IDT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants