Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
One process to rule them all (microsoft#14843)
## 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 microsoft#5000 * Related to microsoft#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 ```
- Loading branch information