mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-12 00:07:24 -06:00
## Summary _In the days of old, the windows were sundered, each with its own process, like the scattered stars in the sky. But a new age hath dawned, for all windows now reside within a single process, like the bright gems of a single crown._ _And lo, there came the `WindowEmperor`, a new lord to rule over the global state, wielding the power of hotkeys and the beacon of the notification icon. The `WindowManager` was cast aside, no longer needed to seek out other processes or determine the `Monarch`._ _Should the `WindowEmperor` determine that a new window shall be raised, it shall set forth a new thread, born from the ether, to govern this new realm. On the main thread shall reside a message loop, charged with the weighty task of preserving the global state, guarded by hotkeys and the beacon of the notification icon._ _Each window doth live on its own thread, each guarded by the new `WindowThread`, a knightly champion to hold the `TerminalWindow`, `AppHost`, and `IslandWindow` in its grasp. And so the windows shall run free, no longer burdened by their former ways._ All windows are now in a single process, rather than in one process per window. We'll add a new `WindowEmperor` class to manage global state such as hotkeys and the notification icon. The `WindowManager` has been streamlined and no longer needs to connect to other processes or determine a new `Monarch`. Each window will run on its own thread, using the new `WindowThread` class to encapsulate the thread and manage the `TerminalWindow`, `AppHost`, and `IslandWindow`. * Related to #5000 * Related to #1256 ## Windows Terminal Process Model 3.0 Everything is now one process. All the windows for a single Terminal instance live in a singular Terminal process. When a new terminal process launches, it will still attempt to communicate with an existing one. If it finds one, it'll pass the commandline to that process and exit. Otherwise, it'll become the "monarch" and create a new window. We'll introduce a new abstraction here, called the `WindowEmperor`. `Monarch` & `Peasant` will still remain, for facilitating cross-window communication. The Emperor will allow us to have a single dedicated class for all global state, and will now always represent the "monarch" (rather than our previously established non-deterministic monarchy to elevate a random peasant to the role of monarch). We still need to do a very minimal amount of x-proc calls. Namely, one right on startup, to see if another `Terminal.exe` was already running. If we find one, then we toss our commandline at it and bail. If we don't, then we need to `CoRegister` the Monarch still, to prepare for subsequent launches to send commands to us. `WindowManager` takes the most changes here. It had a ton of logic to redundantly attempt to connect to other monarchs of other processes, or elect a new one. It doesn't need to do any of that anymore, which is a pretty dramatic change to that class. This creates the opportunity to move some lifetime management around. We've played silly games in the past trying to have individual windows determine if they're the singular monarch for global state. `IslandWindow`s no longer need to track things like global hotkeys or the notification icon. The emperor can do that - there's only ever one emperor. It can also own a singular copy of the settings model, and hand out references to each other thread. Each window lives on separate threads. We'll need to separately initialize XAML islands for each thread. This is totally fine, and actually supported these days. We'll use a new class called `WindowThread` to encapsulate one of these threads. It'll be responsible for owning the `TerminalWindow`, `AppHost` and `IslandWindow` for a single thread. This introduces new classes of bugs we'll need to worry about. It's now easier than ever to have "wrong thread" bugs when interacting with any XAML object from another thread. A good case in point - we used to stash a `static` `Brush` in `Pane`, for the color of the borders. We can't do that anymore! The first window will end up stashing a brush from its thread. So now when a second window starts, the app explodes, because the panes of that window try to draw their borders using a brush from the wrong thread. _Another fun change_: The keybinding labels of the command palette. `TerminalPage` is the thing that ends up expanding iterable `Command`s. It does this largely with copies - it makes a new `map`, a new `vector`, copies the `Command`s over, and does the work there before setting up the cmdpal. Except, it's not making a copy of the `Command`s, it's making a copy of the `vector`, with winrt objects all pointing at the `Command` objects that are ultimately owned by `CascadiaSettings`. This doesn't matter if there's only one `TerminalPage` - we'll only ever do that once. However, now there are many Pages, on different threads. That causes one `TerminalPage` to end up expanding the subcommands of a `Command` while another `TerminalPage` is ALSO iterating on those subcommands. _Emperor message window_: The Emperor will have its own HWND, that's entirely unrelated to any terminal window. This window is a `HWND_MESSAGE` window, which specifically cannot be visible, but is useful for getting messages. We'll use that to handle the notification icon and global hotkeys. This alleviates the need for the IslandWindow to raise events for the tray icon up to the AppHost to handle them. Less plumbing=more good. ### Class ownership diagram _pretend that I know UML for a second_: ```mermaid classDiagram direction LR class Monarch class Peasant class Emperor class WindowThread class AppHost Monarch "1" --o "*" Peasant: Tracks Emperor --* "1" AppLogic: Monarch <..> "1" Emperor Peasant "1" .. "1" WindowThread Emperor "1" --o "*" WindowThread: Tracks WindowThread --* AppHost AppHost --* IslandWindow AppHost --* TerminalWindow TerminalWindow --* TerminalPage ``` * There's still only one `Monarch`. One for the Terminal process. * There's still many `Peasant`s, one per window. * The `Monarch` is no longer associated with a window. It's associated with the `Emperor`, who maintains _all_ the Terminal windows (but is not associated with any particular window) * It may be relevant to note: As far as the `Remoting` dll is concerned, it doesn't care if monarchs and peasants are associated with windows or not. Prior to this PR, _yes_, the Monarch was in fact associated with a specific window (which was also associated with a window). Now, the monarch is associated with the Emperor, who isn't technically any of the windows. * The `Emperor` owns the `App` (and by extension, the single `AppLogic` instance). * Each Terminal window lives on its own thread, owed by a `WindowThread` object. * There's still one `AppHost`, one `IslandWindow`, one `TerminalWindow` & `TerminalPage` per window. * `AppLogic` hands out references to its settings to each `TerminalWindow` as they're created. ### Isolated Mode This was a bit of a tiny brainstorm Dustin and I discussed. This is a new setting introduced as an escape watch from the "one process to rule them all" model. Technically, the Terminal already did something like this if it couldn't find a `Monarch`, though, we were never really sure if that hit. This just adds a setting to manually enable this mode. In isolated mode, we always instantiate a Monarch instance locally, without attempting to use the `CoRegister`-ed one, and we _never_ register one. This prevents the Terminal from talking with other windows. * Global hotkeys won't work right * Trying to run commandlines in other windows (`wt -w foo`) won't work * Every window will be its own process again * Tray icon behavior is left undefined for now. * Tab tearout straight-up won't work. ### A diagram about settings This helps explain how settings changes get propagated ```mermaid sequenceDiagram participant Emperor participant AppLogic participant AppHost participant TerminalWindow participant TerminalPage Note Right of AppLogic: AL::ReloadSettings AppLogic ->> Emperor: raise SettingsChanged Note left of Emperor: E::...GlobalHotkeys Note left of Emperor: E::...NotificationIcon AppLogic ->> TerminalWindow: raise SettingsChanged<br>(to each window) AppLogic ->> TerminalWindow: AppLogic ->> TerminalWindow: Note right of TerminalWindow: TW::UpdateSettingsHandler Note right of TerminalWindow: TW::UpdateSettings TerminalWindow ->> TerminalPage: SetSettings TerminalWindow ->> AppHost: raise SettingsChanged Note right of AppHost: AH::_HandleSettingsChanged ```
133 lines
4.8 KiB
C++
133 lines
4.8 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "WindowThread.h"
|
|
|
|
WindowThread::WindowThread(winrt::TerminalApp::AppLogic logic,
|
|
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
|
|
winrt::Microsoft::Terminal::Remoting::WindowManager manager,
|
|
winrt::Microsoft::Terminal::Remoting::Peasant peasant) :
|
|
_peasant{ std::move(peasant) },
|
|
_appLogic{ std::move(logic) },
|
|
_args{ std::move(args) },
|
|
_manager{ std::move(manager) }
|
|
{
|
|
// DO NOT start the AppHost here in the ctor, as that will start XAML on the wrong thread!
|
|
}
|
|
|
|
void WindowThread::CreateHost()
|
|
{
|
|
// Start the AppHost HERE, on the actual thread we want XAML to run on
|
|
_host = std::make_unique<::AppHost>(_appLogic,
|
|
_args,
|
|
_manager,
|
|
_peasant);
|
|
_host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); });
|
|
|
|
winrt::init_apartment(winrt::apartment_type::single_threaded);
|
|
|
|
// Initialize the xaml content. This must be called AFTER the
|
|
// WindowsXamlManager is initialized.
|
|
_host->Initialize();
|
|
}
|
|
|
|
int WindowThread::RunMessagePump()
|
|
{
|
|
// Enter the main window loop.
|
|
const auto exitCode = _messagePump();
|
|
// Here, the main window loop has exited.
|
|
|
|
_host = nullptr;
|
|
// !! LOAD BEARING !!
|
|
//
|
|
// Make sure to finish pumping all the messages for our thread here. We
|
|
// may think we're all done, but we're not quite. XAML needs more time
|
|
// to pump the remaining events through, even at the point we're
|
|
// exiting. So do that now. If you don't, then the last tab to close
|
|
// will never actually destruct the last tab / TermControl / ControlCore
|
|
// / renderer.
|
|
{
|
|
MSG msg = {};
|
|
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
|
|
{
|
|
::DispatchMessageW(&msg);
|
|
}
|
|
}
|
|
|
|
return exitCode;
|
|
}
|
|
|
|
winrt::TerminalApp::TerminalWindow WindowThread::Logic()
|
|
{
|
|
return _host->Logic();
|
|
}
|
|
|
|
static bool _messageIsF7Keypress(const MSG& message)
|
|
{
|
|
return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7;
|
|
}
|
|
static bool _messageIsAltKeyup(const MSG& message)
|
|
{
|
|
return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU;
|
|
}
|
|
static bool _messageIsAltSpaceKeypress(const MSG& message)
|
|
{
|
|
return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE;
|
|
}
|
|
|
|
int WindowThread::_messagePump()
|
|
{
|
|
MSG message{};
|
|
|
|
while (GetMessageW(&message, nullptr, 0, 0))
|
|
{
|
|
// GH#638 (Pressing F7 brings up both the history AND a caret browsing message)
|
|
// The Xaml input stack doesn't allow an application to suppress the "caret browsing"
|
|
// dialog experience triggered when you press F7. Official recommendation from the Xaml
|
|
// team is to catch F7 before we hand it off.
|
|
// AppLogic contains an ad-hoc implementation of event bubbling for a runtime classes
|
|
// implementing a custom IF7Listener interface.
|
|
// If the recipient of IF7Listener::OnF7Pressed suggests that the F7 press has, in fact,
|
|
// been handled we can discard the message before we even translate it.
|
|
if (_messageIsF7Keypress(message))
|
|
{
|
|
if (_host->OnDirectKeyEvent(VK_F7, LOBYTE(HIWORD(message.lParam)), true))
|
|
{
|
|
// The application consumed the F7. Don't let Xaml get it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// GH#6421 - System XAML will never send an Alt KeyUp event. So, similar
|
|
// to how we'll steal the F7 KeyDown above, we'll steal the Alt KeyUp
|
|
// here, and plumb it through.
|
|
if (_messageIsAltKeyup(message))
|
|
{
|
|
// Let's pass <Alt> to the application
|
|
if (_host->OnDirectKeyEvent(VK_MENU, LOBYTE(HIWORD(message.lParam)), false))
|
|
{
|
|
// The application consumed the Alt. Don't let Xaml get it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// GH#7125 = System XAML will show a system dialog on Alt Space. We want to
|
|
// explicitly prevent that because we handle that ourselves. So similar to
|
|
// above, we steal the event and hand it off to the host.
|
|
if (_messageIsAltSpaceKeypress(message))
|
|
{
|
|
_host->OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true);
|
|
continue;
|
|
}
|
|
|
|
TranslateMessage(&message);
|
|
DispatchMessage(&message);
|
|
}
|
|
return 0;
|
|
}
|
|
winrt::Microsoft::Terminal::Remoting::Peasant WindowThread::Peasant()
|
|
{
|
|
return _peasant;
|
|
}
|