Files
terminal/src/cascadia/TerminalApp/AppLogic.cpp
Leonard Hecker 8bbf00e054 Remove Monarch/Peasant & Make UI single-threaded (#18215)
As before, a minor refactor:
* I started off by removing the Monarch/Peasant with the goal of moving
  it into and deduplicating its functionality with `WindowEmperor`.
* Since I needed a replacement for the Monarch (= ensures that there's
  a single instance), I wrote single-instance code with a NT mutex
  and by yeeting data across processes with `WM_COPYDATA`.
* This resulted in severe threading issues, because it now started up
  way faster. The more I tried to solve them the deeper I had to dig,
  because you can't just put a mutex around `CascadiaSettings`.
  I then tried to seeif WinUI can run multiple windows on a single
  thread and, as it turns out, it can.
  So, I removed the multi- from the window threading.
* At this point I had dig about 1 mile deep and brought no ladder.
  So, to finish it up, I had to clean up the entire eventing system
  around `WindowEmperor`, cleaned up all the coroutines,
  and cleaned up all the callbacks.

Closes #16183
Closes #16221
Closes #16487
Closes #16532
Closes #16733
Closes #16755
Closes #17015
Closes #17360
Closes #17420
Closes #17457
Closes #17799
Closes #17976
Closes #18057
Closes #18084
Closes #18169
Closes #18176
Closes #18191

## Validation Steps Performed
* It does not crash 
* New/close tab 
* New/close window 
* Move tabs between windows 
* Split tab into new window 
* Persist windows on exit / restore startup 
2024-12-12 22:21:24 +01:00

556 lines
21 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "AppLogic.h"
#include "AppLogic.g.cpp"
#include "SettingsLoadEventArgs.h"
#include <LibraryResources.h>
#include <WtExeUtils.h>
#include <wil/token_helpers.h>
#include "../../types/inc/utils.hpp"
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace ::TerminalApp;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
using IInspectable = Windows::Foundation::IInspectable;
}
////////////////////////////////////////////////////////////////////////////////
// Error message handling. This is in this file rather than with the warnings in
// TerminalWindow, because the error text might be:
// * A error we defined here
// * An error from deserializing the json
// * Any other fatal error loading the settings
// So all we pass on is the actual text of the error, rather than the
// combination of things that might have caused an error.
// !!! IMPORTANT !!!
// Make sure that these keys are in the same order as the
// SettingsLoadWarnings/Errors enum is!
static const std::array settingsLoadErrorsLabels{
USES_RESOURCE(L"NoProfilesText"),
USES_RESOURCE(L"AllProfilesHiddenText")
};
static_assert(settingsLoadErrorsLabels.size() == static_cast<size_t>(SettingsLoadErrors::ERRORS_SIZE));
// Function Description:
// - General-purpose helper for looking up a localized string for a
// warning/error. First will look for the given key in the provided map of
// keys->strings, where the values in the map are ResourceKeys. If it finds
// one, it will lookup the localized string from that ResourceKey.
// - If it does not find a key, it'll return an empty string
// Arguments:
// - key: the value to use to look for a resource key in the given map
// - map: A map of keys->Resource keys.
// Return Value:
// - the localized string for the given type, if it exists.
template<typename T>
winrt::hstring _GetMessageText(uint32_t index, const T& keys)
{
if (index < keys.size())
{
return GetLibraryResourceString(til::at(keys, index));
}
return {};
}
// Function Description:
// - Gets the text from our ResourceDictionary for the given
// SettingsLoadError. If there is no such text, we'll return nullptr.
// - The warning should have an entry in settingsLoadErrorsLabels.
// Arguments:
// - error: the SettingsLoadErrors value to get the localized text for.
// Return Value:
// - localized text for the given error
static winrt::hstring _GetErrorText(SettingsLoadErrors error)
{
return _GetMessageText(static_cast<uint32_t>(error), settingsLoadErrorsLabels);
}
static constexpr std::wstring_view StartupTaskName = L"StartTerminalOnLoginTask";
namespace winrt::TerminalApp::implementation
{
// Function Description:
// - Get the AppLogic for the current active Xaml application, or null if there isn't one.
// Return value:
// - A pointer (bare) to the AppLogic, or nullptr. The app logic outlives all other objects,
// unless the application is in a terrible way, so this is "safe."
AppLogic* AppLogic::Current() noexcept
try
{
if (auto currentXamlApp{ winrt::Windows::UI::Xaml::Application::Current().try_as<winrt::TerminalApp::App>() })
{
if (auto appLogicPointer{ winrt::get_self<AppLogic>(currentXamlApp.Logic()) })
{
return appLogicPointer;
}
}
return nullptr;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
// Method Description:
// - Returns the settings currently in use by the entire Terminal application.
// - IMPORTANT! This can throw! Make sure to try/catch this, so that the
// LocalTests don't crash (because their Application::Current() won't be a
// AppLogic)
// Throws:
// - HR E_INVALIDARG if the app isn't up and running.
const CascadiaSettings AppLogic::CurrentAppSettings()
{
auto appLogic{ ::winrt::TerminalApp::implementation::AppLogic::Current() };
THROW_HR_IF_NULL(E_INVALIDARG, appLogic);
return appLogic->Settings();
}
AppLogic::AppLogic()
{
// For your own sanity, it's better to do setup outside the ctor.
// If you do any setup in the ctor that ends up throwing an exception,
// then it might look like App just failed to activate, which will
// cause you to chase down the rabbit hole of "why is App not
// registered?" when it definitely is.
// The TerminalPage has to be constructed during our construction, to
// make sure that there's a terminal page for callers of
// SetTitleBarContent
_isElevated = ::Microsoft::Console::Utils::IsRunningElevated();
_canDragDrop = ::Microsoft::Console::Utils::CanUwpDragDrop();
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
if (auto self{ weakSelf.get() })
{
self->ReloadSettings();
}
});
_languageProfileNotifier = winrt::make_self<LanguageProfileNotifier>([this]() {
_reloadSettings->Run();
});
// Do this here, rather than at the top of main. This will prevent us from
// including this variable in the vars we serialize in the
// Remoting::CommandlineArgs up in HandleCommandlineArgs.
_setupFolderPathEnvVar();
}
// Method Description:
// - Called around the codebase to discover if Terminal is running elevated
// Arguments:
// - <none> - reports internal state
// Return Value:
// - True if elevated, false otherwise.
bool AppLogic::IsRunningElevated() const noexcept
{
return _isElevated;
}
bool AppLogic::CanDragDrop() const noexcept
{
return _canDragDrop;
}
// Method Description:
// - Build the UI for the terminal app. Before this method is called, it
// should not be assumed that the TerminalApp is usable. The Settings
// should be loaded before this is called, either with LoadSettings or
// GetLaunchDimensions (which will call LoadSettings)
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppLogic::Create()
{
// Assert that we've already loaded our settings. We have to do
// this as a MTA, before the app is Create()'d
WINRT_ASSERT(_loadedInitialSettings);
_ApplyLanguageSettingChange();
TraceLoggingWrite(
g_hTerminalAppProvider,
"AppCreated",
TraceLoggingDescription("Event emitted when the application is started"),
TraceLoggingBool(_settings.GlobalSettings().ShowTabsInTitlebar(), "TabsInTitlebar"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
// Method Description:
// - Attempt to load the settings. If we fail for any reason, returns an error.
// Return Value:
// - S_OK if we successfully parsed the settings, otherwise an appropriate HRESULT.
[[nodiscard]] HRESULT AppLogic::_TryLoadSettings() noexcept
{
auto hr = E_FAIL;
try
{
auto newSettings = CascadiaSettings::LoadAll();
if (newSettings.GetLoadingError())
{
_settingsLoadExceptionText = _GetErrorText(newSettings.GetLoadingError().Value());
return E_INVALIDARG;
}
else if (!newSettings.GetSerializationErrorMessage().empty())
{
_settingsLoadExceptionText = newSettings.GetSerializationErrorMessage();
return E_INVALIDARG;
}
_warnings.clear();
for (uint32_t i = 0; i < newSettings.Warnings().Size(); i++)
{
_warnings.push_back(newSettings.Warnings().GetAt(i));
}
_hasSettingsStartupActions = false;
const auto startupActions = newSettings.GlobalSettings().StartupActions();
if (!startupActions.empty())
{
_settingsAppArgs.FullResetState();
ExecuteCommandlineArgs args{ newSettings.GlobalSettings().StartupActions() };
auto result = _settingsAppArgs.ParseArgs(args);
if (result == 0)
{
_hasSettingsStartupActions = true;
// Validation also injects new-tab command if implicit new-tab was provided.
_settingsAppArgs.ValidateStartupCommands();
}
else
{
_warnings.push_back(SettingsLoadWarnings::FailedToParseStartupActions);
}
}
_settings = std::move(newSettings);
hr = _warnings.empty() ? S_OK : S_FALSE;
}
catch (const winrt::hresult_error& e)
{
hr = e.code();
_settingsLoadExceptionText = e.message();
LOG_HR(hr);
}
catch (...)
{
hr = wil::ResultFromCaughtException();
LOG_HR(hr);
}
return hr;
}
bool AppLogic::HasSettingsStartupActions() const noexcept
{
return _hasSettingsStartupActions;
}
// Call this function after loading your _settings.
// It handles any CPU intensive settings updates (like updating the Jumplist)
// which should thus only occur if the settings file actually changed.
void AppLogic::_ProcessLazySettingsChanges()
{
const auto hash = _settings.Hash();
const auto applicationState = ApplicationState::SharedInstance();
const auto cachedHash = applicationState.SettingsHash();
// The hash might be empty if LoadAll() failed and we're dealing with the defaults settings object.
// In that case we can just wait until the user fixed their settings or CascadiaSettings fixed
// itself and either will soon trigger a settings reload.
if (hash.empty() || hash == cachedHash)
{
return;
}
Jumplist::UpdateJumplist(_settings);
applicationState.SettingsHash(hash);
}
// Method Description:
// - Registers for changes to the settings folder and upon a updated settings
// profile calls ReloadSettings().
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppLogic::_RegisterSettingsChange()
{
const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } };
_reader.create(
settingsPath.parent_path().c_str(),
false,
// We want file modifications, AND when files are renamed to be
// settings.json. This second case will oftentimes happen with text
// editors, who will write a temp file, then rename it to be the
// actual file you wrote. So listen for that too.
wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime,
[this, settingsBasename = settingsPath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
// DO NOT create a static reference to ApplicationState::SharedInstance here.
//
// ApplicationState::SharedInstance already caches its own
// static ref. If _we_ keep a static ref to the member in
// AppState, then our reference will keep ApplicationState alive
// after the `ActionToStringMap` gets cleaned up. Then, when we
// try to persist the actions in the window state, we won't be
// able to. We'll try to look up the action and the map just
// won't exist. We'll explode, even though the Terminal is
// tearing down anyways. So we'll just die, but still invoke
// WinDBG's post-mortem debugger, who won't be able to attach to
// the process that's already exiting.
//
// So DON'T ~give a mouse a cookie~ take a static ref here.
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename();
if (modifiedBasename == settingsBasename)
{
_reloadSettings->Run();
}
});
}
void AppLogic::_ApplyLanguageSettingChange() noexcept
try
{
if (!IsPackaged())
{
return;
}
using ApplicationLanguages = winrt::Windows::Globalization::ApplicationLanguages;
// NOTE: PrimaryLanguageOverride throws if this instance is unpackaged.
const auto primaryLanguageOverride = ApplicationLanguages::PrimaryLanguageOverride();
const auto language = _settings.GlobalSettings().Language();
if (primaryLanguageOverride != language)
{
ApplicationLanguages::PrimaryLanguageOverride(language);
}
}
CATCH_LOG()
safe_void_coroutine AppLogic::_ApplyStartupTaskStateChange()
try
{
// First, make sure we're running in a packaged context. This method
// won't work, and will crash mysteriously if we're running unpackaged.
if (!IsPackaged())
{
co_return;
}
const auto tryEnableStartupTask = _settings.GlobalSettings().StartOnUserLogin();
const auto task = co_await StartupTask::GetAsync(StartupTaskName);
switch (task.State())
{
case StartupTaskState::Disabled:
if (tryEnableStartupTask)
{
co_await task.RequestEnableAsync();
}
break;
case StartupTaskState::DisabledByUser:
// TODO: GH#6254: define UX for other StartupTaskStates
break;
case StartupTaskState::Enabled:
if (!tryEnableStartupTask)
{
task.Disable();
}
break;
}
}
CATCH_LOG();
// Method Description:
// - Reloads the settings from the settings.json file.
// - When this is called the first time, this initializes our settings. See
// CascadiaSettings for more details. Additionally hooks up our callbacks
// for keybinding events to the keybindings object.
// - NOTE: when called initially, this must be called from a MTA if we're
// running as a packaged application. The Windows.Storage APIs require a
// MTA. If this isn't happening during startup, it'll need to happen on
// a background thread.
void AppLogic::ReloadSettings()
{
// Attempt to load our settings.
// If it fails,
// - don't change the settings (and don't actually apply the new settings)
// - don't persist them.
// - display a loading error
_settingsLoadedResult = _TryLoadSettings();
const auto initialLoad = !_loadedInitialSettings;
_loadedInitialSettings = true;
if (FAILED(_settingsLoadedResult))
{
if (initialLoad)
{
_settings = CascadiaSettings::LoadDefaults();
}
else
{
auto warnings{ winrt::multi_threaded_vector<SettingsLoadWarnings>() };
for (auto&& warn : _warnings)
{
warnings.Append(warn);
}
auto ev = winrt::make_self<SettingsLoadEventArgs>(true,
static_cast<uint64_t>(_settingsLoadedResult),
_settingsLoadExceptionText,
warnings,
_settings);
SettingsChanged.raise(*this, *ev);
return;
}
}
else
{
_settings.LogSettingChanges(true);
}
if (initialLoad)
{
// Register for directory change notification.
_RegisterSettingsChange();
return;
}
// Here, we successfully reloaded the settings, and created a new
// TerminalSettings object.
_ApplyLanguageSettingChange();
_ApplyStartupTaskStateChange();
_ProcessLazySettingsChanges();
auto warnings{ winrt::multi_threaded_vector<SettingsLoadWarnings>() };
for (auto&& warn : _warnings)
{
warnings.Append(warn);
}
auto ev = winrt::make_self<SettingsLoadEventArgs>(!initialLoad,
_settingsLoadedResult,
_settingsLoadExceptionText,
warnings,
_settings);
SettingsChanged.raise(*this, *ev);
}
// This is a continuation of AppLogic::Create() and includes the more expensive parts.
void AppLogic::NotifyRootInitialized()
{
if (_notifyRootInitializedCalled.exchange(true, std::memory_order_relaxed))
{
return;
}
// Both LoadSettings and ReloadSettings are supposed to call this function,
// but LoadSettings skips it, so that the UI starts up faster.
// Now that the UI is present we can do them with a less significant UX impact.
_ApplyStartupTaskStateChange();
_ProcessLazySettingsChanges();
FILETIME creationTime, exitTime, kernelTime, userTime, now;
if (GetThreadTimes(GetCurrentThread(), &creationTime, &exitTime, &kernelTime, &userTime))
{
static constexpr auto asInteger = [](const FILETIME& f) {
ULARGE_INTEGER i;
i.LowPart = f.dwLowDateTime;
i.HighPart = f.dwHighDateTime;
return i.QuadPart;
};
static constexpr auto asSeconds = [](uint64_t v) {
return v * 1e-7f;
};
GetSystemTimeAsFileTime(&now);
const auto latency = asSeconds(asInteger(now) - asInteger(creationTime));
TraceLoggingWrite(
g_hTerminalAppProvider,
"AppInitialized",
TraceLoggingDescription("Event emitted once the app is initialized"),
TraceLoggingFloat32(latency, "latency"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
}
// Method Description:
// - Returns a pointer to the global shared settings.
[[nodiscard]] CascadiaSettings AppLogic::Settings() const noexcept
{
return _settings;
}
Windows::Foundation::Collections::IMapView<Microsoft::Terminal::Control::KeyChord, Microsoft::Terminal::Settings::Model::Command> AppLogic::GlobalHotkeys()
{
return _settings.GlobalSettings().ActionMap().GlobalHotkeys();
}
TerminalApp::TerminalWindow AppLogic::CreateNewWindow()
{
auto warnings{ winrt::multi_threaded_vector<SettingsLoadWarnings>() };
for (auto&& warn : _warnings)
{
warnings.Append(warn);
}
auto ev = winrt::make_self<SettingsLoadEventArgs>(false,
_settingsLoadedResult,
_settingsLoadExceptionText,
warnings,
_settings);
auto window = winrt::make_self<implementation::TerminalWindow>(*ev, _contentManager);
if (_hasSettingsStartupActions)
{
window->SetSettingsStartupArgs(_settingsAppArgs.GetStartupActions());
}
return *window;
}
winrt::TerminalApp::ContentManager AppLogic::ContentManager()
{
return _contentManager;
}
// Function Description
// * Adds a `WT_SETTINGS_DIR` env var to our own environment block, that
// points at our settings directory. This allows portable installs to
// refer to files in the portable install using %WT_SETTINGS_DIR%
void AppLogic::_setupFolderPathEnvVar()
{
std::wstring path{ CascadiaSettings::SettingsPath() };
auto folderPath = path.substr(0, path.find_last_of(L"\\"));
SetEnvironmentVariableW(L"WT_SETTINGS_DIR", folderPath.c_str());
}
}