diff --git a/patch.diff b/patch.diff new file mode 100644 index 000000000..6f59399e3 --- /dev/null +++ b/patch.diff @@ -0,0 +1,896 @@ +diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h +index 53f6145d2..01b4cdfe8 100644 +--- a/src/cascadia/LocalTests_SettingsModel/pch.h ++++ b/src/cascadia/LocalTests_SettingsModel/pch.h +@@ -61,6 +61,9 @@ Author(s): + // Manually include til after we include Windows.Foundation to give it winrt superpowers + #include "til.h" + ++#include ++#include ++ + // Common includes for most tests: + #include "../../inc/argb.h" + #include "../../inc/conattrs.hpp" +diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp +index 6d53a6ba7..38edbc2c9 100644 +--- a/src/cascadia/TerminalApp/AppLogic.cpp ++++ b/src/cascadia/TerminalApp/AppLogic.cpp +@@ -189,7 +210,7 @@ namespace winrt::TerminalApp::implementation + } + + AppLogic::AppLogic() : +- _reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } } ++ _reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); ElevatedState::SharedInstance().Reload(); } } + { + // 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, +@@ -906,8 +927,6 @@ namespace winrt::TerminalApp::implementation + void AppLogic::_RegisterSettingsChange() + { + const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } }; +- const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } }; +- + _reader.create( + settingsPath.parent_path().c_str(), + false, +@@ -916,14 +935,17 @@ namespace winrt::TerminalApp::implementation + // 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(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) { ++ [this, settingsPath](wil::FolderChangeEvent, PCWSTR fileModified) { ++ static const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } }; ++ static const std::filesystem::path elevatedStatePath{ std::wstring_view{ ElevatedState::SharedInstance().FilePath() } }; ++ + const auto modifiedBasename = std::filesystem::path{ fileModified }.filename(); + +- if (modifiedBasename == settingsBasename) ++ if (modifiedBasename == settingsPath.filename()) + { + _reloadSettings->Run(); + } +- else if (modifiedBasename == stateBasename) ++ else if (modifiedBasename == statePath.filename() || modifiedBasename == elevatedStatePath.filename()) + { + _reloadState(); + } +diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +index 5a00ba2b4..ec7f5cd7a 100644 +--- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp ++++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +@@ -60,38 +60,40 @@ using namespace ::Microsoft::Terminal::Settings::Model; + + namespace winrt::Microsoft::Terminal::Settings::Model::implementation + { ++ ApplicationState::ApplicationState(std::filesystem::path path) noexcept : ++ BaseApplicationState{ path } {} ++ + // Returns the application-global ApplicationState object. + Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() + { + static auto state = winrt::make_self(GetBaseSettingsPath() / stateFileName); ++ state->Reload(); + return *state; + } + +- ApplicationState::ApplicationState(std::filesystem::path path) noexcept : +- _path{ std::move(path) }, +- _throttler{ std::chrono::seconds(1), [this]() { _write(); } } ++ void ApplicationState::FromJson(const Json::Value& root) const noexcept + { +- _read(); +- } +- +- // The destructor ensures that the last write is flushed to disk before returning. +- ApplicationState::~ApplicationState() +- { +- // This will ensure that we not just cancel the last outstanding timer, +- // but instead force it to run as soon as possible and wait for it to complete. +- _throttler.flush(); ++ auto state = _state.lock(); ++ // GetValueForKey() comes in two variants: ++ // * take a std::optional reference ++ // * return std::optional by value ++ // At the time of writing the former version skips missing fields in the json, ++ // but we want to explicitly clear state fields that were removed from state.json. ++#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); ++ MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) ++#undef MTSM_APPLICATION_STATE_GEN + } +- +- // Re-read the state.json from disk. +- void ApplicationState::Reload() const noexcept ++ Json::Value ApplicationState::ToJson() const noexcept + { +- _read(); +- } ++ Json::Value root{ Json::objectValue }; + +- // Returns the state.json path on the disk. +- winrt::hstring ApplicationState::FilePath() const noexcept +- { +- return winrt::hstring{ _path.wstring() }; ++ { ++ auto state = _state.lock_shared(); ++#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); ++ MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) ++#undef MTSM_APPLICATION_STATE_GEN ++ } ++ return root; + } + + // Generate all getter/setters +@@ -115,58 +117,4 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) + #undef MTSM_APPLICATION_STATE_GEN + +- // Deserializes the state.json at _path into this ApplicationState. +- // * ANY errors during app state will result in the creation of a new empty state. +- // * ANY errors during runtime will result in changes being partially ignored. +- void ApplicationState::_read() const noexcept +- try +- { +- const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); +- if (data.empty()) +- { +- return; +- } +- +- std::string errs; +- std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; +- +- Json::Value root; +- if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) +- { +- throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); +- } +- +- auto state = _state.lock(); +- // GetValueForKey() comes in two variants: +- // * take a std::optional reference +- // * return std::optional by value +- // At the time of writing the former version skips missing fields in the json, +- // but we want to explicitly clear state fields that were removed from state.json. +-#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); +- MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) +-#undef MTSM_APPLICATION_STATE_GEN +- } +- CATCH_LOG() +- +- // Serialized this ApplicationState (in `context`) into the state.json at _path. +- // * Errors are only logged. +- // * _state->_writeScheduled is set to false, signaling our +- // setters that _synchronize() needs to be called again. +- void ApplicationState::_write() const noexcept +- try +- { +- Json::Value root{ Json::objectValue }; +- +- { +- auto state = _state.lock_shared(); +-#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); +- MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) +-#undef MTSM_APPLICATION_STATE_GEN +- } +- +- Json::StreamWriterBuilder wbuilder; +- const auto content = Json::writeString(wbuilder, root); +- WriteUTF8FileAtomic(_path, content); +- } +- CATCH_LOG() + } +diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h +index 71c6a576e..f7011289e 100644 +--- a/src/cascadia/TerminalSettingsModel/ApplicationState.h ++++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h +@@ -12,12 +12,11 @@ Abstract: + --*/ + #pragma once + ++#include "BaseApplicationState.h" + #include "ApplicationState.g.h" + #include "WindowLayout.g.h" + + #include +-#include +-#include + #include + + // This macro generates all getters and setters for ApplicationState. +@@ -40,18 +39,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation + friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait; + }; + +- struct ApplicationState : ApplicationStateT ++ struct ApplicationState : public BaseApplicationState, ApplicationStateT + { + static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance(); + + ApplicationState(std::filesystem::path path) noexcept; +- ~ApplicationState(); + +- // Methods +- void Reload() const noexcept; +- +- // General getters/setters +- winrt::hstring FilePath() const noexcept; ++ virtual void FromJson(const Json::Value& root) const noexcept override; ++ virtual Json::Value ToJson() const noexcept override; + + // State getters/setters + #define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ +@@ -67,13 +62,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) + #undef MTSM_APPLICATION_STATE_GEN + }; +- +- void _write() const noexcept; +- void _read() const noexcept; +- +- std::filesystem::path _path; + til::shared_mutex _state; +- til::throttled_func_trailing<> _throttler; + }; + } + +diff --git a/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp b/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp +new file mode 100644 +index 000000000..3b5c02446 +--- /dev/null ++++ b/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp +@@ -0,0 +1,92 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT license. ++ ++#include "pch.h" ++#include "BaseApplicationState.h" ++#include "CascadiaSettings.h" ++ ++#include "JsonUtils.h" ++#include "FileUtils.h" ++ ++constexpr std::wstring_view stateFileName{ L"state.json" }; ++using namespace ::Microsoft::Terminal::Settings::Model; ++ ++BaseApplicationState::BaseApplicationState(std::filesystem::path path) noexcept : ++ _path{ std::move(path) }, ++ _throttler{ std::chrono::seconds(1), [this]() { _write(); } } ++{ ++ // DON'T _read() here! _read() will call FromJson, which is virtual, and ++ // needs to be implemented in a derived class. Classes that derive from ++ // BaseApplicationState should make sure to call Reload() after construction ++ // to ensure the data is loaded. ++} ++ ++// The destructor ensures that the last write is flushed to disk before returning. ++BaseApplicationState::~BaseApplicationState() ++{ ++ // This will ensure that we not just cancel the last outstanding timer, ++ // but instead force it to run as soon as possible and wait for it to complete. ++ _throttler.flush(); ++} ++ ++// Re-read the state.json from disk. ++void BaseApplicationState::Reload() const noexcept ++{ ++ _read(); ++} ++ ++// Returns the state.json path on the disk. ++winrt::hstring BaseApplicationState::FilePath() const noexcept ++{ ++ return winrt::hstring{ _path.wstring() }; ++} ++ ++// Deserializes the state.json at _path into this ApplicationState. ++// * ANY errors during app state will result in the creation of a new empty state. ++// * ANY errors during runtime will result in changes being partially ignored. ++void BaseApplicationState::_read() const noexcept ++try ++{ ++ const auto data = _readFileContents().value_or(std::string{}); ++ if (data.empty()) ++ { ++ return; ++ } ++ ++ std::string errs; ++ std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; ++ ++ Json::Value root; ++ if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) ++ { ++ throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); ++ } ++ ++ FromJson(root); ++} ++CATCH_LOG() ++ ++// Serialized this ApplicationState (in `context`) into the state.json at _path. ++// * Errors are only logged. ++// * _state->_writeScheduled is set to false, signaling our ++// setters that _synchronize() needs to be called again. ++void BaseApplicationState::_write() const noexcept ++try ++{ ++ Json::Value root{ this->ToJson() }; ++ ++ Json::StreamWriterBuilder wbuilder; ++ const auto content = Json::writeString(wbuilder, root); ++ _writeFileContents(content); ++} ++CATCH_LOG() ++ ++std::optional BaseApplicationState::_readFileContents() const ++{ ++ return ReadUTF8FileIfExists(_path); ++} ++ ++void BaseApplicationState::_writeFileContents(const std::string_view content) const ++{ ++ WriteUTF8FileAtomic(_path, content); ++} +diff --git a/src/cascadia/TerminalSettingsModel/BaseApplicationState.h b/src/cascadia/TerminalSettingsModel/BaseApplicationState.h +new file mode 100644 +index 000000000..e684d3192 +--- /dev/null ++++ b/src/cascadia/TerminalSettingsModel/BaseApplicationState.h +@@ -0,0 +1,36 @@ ++/*++ ++Copyright (c) Microsoft Corporation ++Licensed under the MIT license. ++ ++Module Name: ++- ApplicationState.h ++ ++Abstract: ++- TODO! ++--*/ ++#pragma once ++ ++struct BaseApplicationState ++{ ++ BaseApplicationState(std::filesystem::path path) noexcept; ++ ~BaseApplicationState(); ++ ++ // Methods ++ void Reload() const noexcept; ++ ++ // General getters/setters ++ winrt::hstring FilePath() const noexcept; ++ ++ virtual void FromJson(const Json::Value& root) const noexcept = 0; ++ virtual Json::Value ToJson() const noexcept = 0; ++ ++protected: ++ virtual std::optional _readFileContents() const; ++ virtual void _writeFileContents(const std::string_view content) const; ++ ++ void _write() const noexcept; ++ void _read() const noexcept; ++ ++ std::filesystem::path _path; ++ til::throttled_func_trailing<> _throttler; ++}; +diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.cpp b/src/cascadia/TerminalSettingsModel/ElevatedState.cpp +new file mode 100644 +index 000000000..f7d72207e +--- /dev/null ++++ b/src/cascadia/TerminalSettingsModel/ElevatedState.cpp +@@ -0,0 +1,134 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT license. ++ ++#include "pch.h" ++#include "ElevatedState.h" ++#include "CascadiaSettings.h" ++#include "ElevatedState.g.cpp" ++ ++#include "JsonUtils.h" ++#include "FileUtils.h" ++ ++#include ++ ++constexpr std::wstring_view stateFileName{ L"elevated-state.json" }; ++ ++using namespace ::Microsoft::Terminal::Settings::Model; ++ ++namespace winrt::Microsoft::Terminal::Settings::Model::implementation ++{ ++ ElevatedState::ElevatedState(std::filesystem::path path) noexcept : ++ BaseApplicationState{ path } {} ++ ++ // Returns the application-global ElevatedState object. ++ Microsoft::Terminal::Settings::Model::ElevatedState ElevatedState::SharedInstance() ++ { ++ // TODO! place in a totally different file! and path! ++ static auto state = winrt::make_self(GetBaseSettingsPath() / stateFileName); ++ state->Reload(); ++ ++ // const auto testPath{ GetBaseSettingsPath() / L"test.json" }; ++ ++ // PSID pEveryoneSID = NULL; ++ // SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_NT_AUTHORITY; ++ // BOOL success = AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryoneSID); ++ ++ // EXPLICIT_ACCESS ea[1]; ++ // ZeroMemory(&ea, 1 * sizeof(EXPLICIT_ACCESS)); ++ // ea[0].grfAccessPermissions = KEY_READ; ++ // ea[0].grfAccessMode = SET_ACCESS; ++ // ea[0].grfInheritance = NO_INHERITANCE; ++ // ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ++ // ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ++ // ea[0].Trustee.ptstrName = (LPTSTR)pEveryoneSID; ++ ++ // ACL acl; ++ // PACL pAcl = &acl; ++ // DWORD dwRes = SetEntriesInAcl(1, ea, NULL, &pAcl); ++ // dwRes; ++ ++ // SECURITY_DESCRIPTOR sd; ++ // success = InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); ++ // success = SetSecurityDescriptorDacl(&sd, ++ // TRUE, // bDaclPresent flag ++ // pAcl, ++ // FALSE); ++ ++ // SECURITY_ATTRIBUTES sa; ++ // // Initialize a security attributes structure. ++ // sa.nLength = sizeof(SECURITY_ATTRIBUTES); ++ // sa.lpSecurityDescriptor = &sd; ++ // sa.bInheritHandle = FALSE; ++ // success; ++ ++ // wil::unique_hfile file{ CreateFileW(testPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; ++ // THROW_LAST_ERROR_IF(!file); ++ ++ return *state; ++ } ++ ++ void ElevatedState::FromJson(const Json::Value& root) const noexcept ++ { ++ auto state = _state.lock(); ++ // GetValueForKey() comes in two variants: ++ // * take a std::optional reference ++ // * return std::optional by value ++ // At the time of writing the former version skips missing fields in the json, ++ // but we want to explicitly clear state fields that were removed from state.json. ++#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); ++ MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) ++#undef MTSM_ELEVATED_STATE_GEN ++ } ++ Json::Value ElevatedState::ToJson() const noexcept ++ { ++ Json::Value root{ Json::objectValue }; ++ ++ { ++ auto state = _state.lock_shared(); ++#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); ++ MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) ++#undef MTSM_ELEVATED_STATE_GEN ++ } ++ return root; ++ } ++ ++ // Generate all getter/setters ++#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) \ ++ type ElevatedState::name() const noexcept \ ++ { \ ++ const auto state = _state.lock_shared(); \ ++ const auto& value = state->name; \ ++ return value ? *value : type{ __VA_ARGS__ }; \ ++ } \ ++ \ ++ void ElevatedState::name(const type& value) noexcept \ ++ { \ ++ { \ ++ auto state = _state.lock(); \ ++ state->name.emplace(value); \ ++ } \ ++ \ ++ _throttler(); \ ++ } ++ MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) ++#undef MTSM_ELEVATED_STATE_GEN ++ ++ void ElevatedState::_writeFileContents(const std::string_view content) const ++ { ++ // DON'T use WriteUTF8FileAtomic, which will write to a temporary file ++ // then rename that file to the final filename. That actually lets us ++ // overwrite the elevate file's contents even when unelevated, because ++ // we're effectively deleting the original file, then renaming a ++ // different file in it's place. ++ // ++ // We're not worried about someone else doing that though, if they do ++ // that with the wrong permissions, then we'll just ignore the file and ++ // start over. ++ WriteUTF8File(_path, content, true); ++ } ++ ++ std::optional ElevatedState::_readFileContents() const ++ { ++ return ReadUTF8FileIfExists(_path, true); ++ } ++} +diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.h b/src/cascadia/TerminalSettingsModel/ElevatedState.h +new file mode 100644 +index 000000000..d0eb8381f +--- /dev/null ++++ b/src/cascadia/TerminalSettingsModel/ElevatedState.h +@@ -0,0 +1,60 @@ ++/*++ ++Copyright (c) Microsoft Corporation ++Licensed under the MIT license. ++ ++Module Name: ++- ElevatedState.h ++ ++Abstract: ++- If the CascadiaSettings class were AppData, then this class would be LocalAppData. ++ Put anything in here that you wouldn't want to be stored next to user-editable settings. ++- Modify ElevatedState.idl and MTSM_ELEVATED_STATE_FIELDS to add new fields. ++--*/ ++#pragma once ++ ++#include "BaseApplicationState.h" ++#include "ElevatedState.g.h" ++#include ++ ++// This macro generates all getters and setters for ElevatedState. ++// It provides X with the following arguments: ++// (type, function name, JSON key, ...variadic construction arguments) ++#define MTSM_ELEVATED_STATE_FIELDS(X) \ ++ X(Windows::Foundation::Collections::IVector, AllowedCommandlines, "allowedCommandlines") ++ ++namespace winrt::Microsoft::Terminal::Settings::Model::implementation ++{ ++ struct ElevatedState : ElevatedStateT, public BaseApplicationState ++ { ++ static Microsoft::Terminal::Settings::Model::ElevatedState SharedInstance(); ++ ++ ElevatedState(std::filesystem::path path) noexcept; ++ ++ void FromJson(const Json::Value& root) const noexcept override; ++ Json::Value ToJson() const noexcept override; ++ ++ // State getters/setters ++#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) \ ++ type name() const noexcept; \ ++ void name(const type& value) noexcept; ++ MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) ++#undef MTSM_ELEVATED_STATE_GEN ++ ++ private: ++ struct state_t ++ { ++#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) std::optional name{ __VA_ARGS__ }; ++ MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) ++#undef MTSM_ELEVATED_STATE_GEN ++ }; ++ til::shared_mutex _state; ++ ++ virtual std::optional _readFileContents() const override; ++ virtual void _writeFileContents(const std::string_view content) const override; ++ }; ++} ++ ++namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation ++{ ++ BASIC_FACTORY(ElevatedState); ++} +diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.idl b/src/cascadia/TerminalSettingsModel/ElevatedState.idl +new file mode 100644 +index 000000000..09e2d05b0 +--- /dev/null ++++ b/src/cascadia/TerminalSettingsModel/ElevatedState.idl +@@ -0,0 +1,15 @@ ++// Copyright (c) Microsoft Corporation. ++// Licensed under the MIT license. ++ ++namespace Microsoft.Terminal.Settings.Model ++{ ++ [default_interface] runtimeclass ElevatedState { ++ static ElevatedState SharedInstance(); ++ ++ void Reload(); ++ ++ String FilePath { get; }; ++ ++ Windows.Foundation.Collections.IVector AllowedCommandlines { get; set; }; ++ } ++} +diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp +index 81fbb6cee..dff35897a 100644 +--- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp ++++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp +@@ -8,6 +8,8 @@ + #include + #include + ++#include ++ + static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; + static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" }; + +@@ -39,10 +41,89 @@ namespace Microsoft::Terminal::Settings::Model + return baseSettingsPath; + } + ++ static bool _hasExpectedPermissions(const std::filesystem::path& path) ++ { ++ // If we want to only open the file if it's elevated, check the ++ // permissions on this file. We want to make sure that: ++ // * Everyone has permission to read ++ // * admins can do anything ++ // * no one else can do anything. ++ PACL pAcl{ nullptr }; // This doesn't need to be cleanup up apparently ++ ++ auto status = GetNamedSecurityInfo(path.c_str(), ++ SE_FILE_OBJECT, ++ DACL_SECURITY_INFORMATION, ++ nullptr, ++ nullptr, ++ &pAcl, ++ nullptr, ++ nullptr); ++ THROW_IF_WIN32_ERROR(status); ++ ++ PEXPLICIT_ACCESS pEA{ nullptr }; ++ DWORD count = 0; ++ status = GetExplicitEntriesFromAcl(pAcl, &count, &pEA); ++ THROW_IF_WIN32_ERROR(status); ++ ++ auto explicitAccessCleanup = wil::scope_exit([&]() { ::LocalFree(pEA); }); ++ ++ if (count != 2) ++ { ++ return false; ++ } ++ ++ // Now, get the Everyone and Admins SIDS so we can make sure they're ++ // the ones in this file. ++ ++ wil::unique_sid everyoneSid{}; ++ wil::unique_sid adminGroupSid{}; ++ SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; ++ SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; ++ ++ // Create a SID for the BUILTIN\Administrators group. ++ THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid)); ++ ++ // Create a well-known SID for the Everyone group. ++ THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid)); ++ ++ bool hadExpectedPermissions = true; ++ ++ // Check that the permissions are what we'd expect them to be if only ++ // admins can write to the file. This is basically a mirror of what we ++ // set up in `WriteUTF8File`. ++ ++ // For grfAccessPermissions, GENERIC_ALL turns into STANDARD_RIGHTS_ALL, ++ // and GENERIC_READ -> READ_CONTROL ++ hadExpectedPermissions = hadExpectedPermissions && WI_AreAllFlagsSet(pEA[0].grfAccessPermissions, STANDARD_RIGHTS_ALL); ++ hadExpectedPermissions = hadExpectedPermissions && pEA[0].grfInheritance == NO_INHERITANCE; ++ hadExpectedPermissions = hadExpectedPermissions && pEA[0].Trustee.TrusteeForm == TRUSTEE_IS_SID; ++ // SIDs are void*'s that happen to convert to a wchar_t ++ hadExpectedPermissions = hadExpectedPermissions && *(pEA[0].Trustee.ptstrName) == *(LPWSTR)(adminGroupSid.get()); ++ ++ // Now check the other EXPLICIT_ACCESS ++ hadExpectedPermissions = hadExpectedPermissions && WI_IsFlagSet(pEA[1].grfAccessPermissions, READ_CONTROL); ++ hadExpectedPermissions = hadExpectedPermissions && pEA[1].grfInheritance == NO_INHERITANCE; ++ hadExpectedPermissions = hadExpectedPermissions && pEA[1].Trustee.TrusteeForm == TRUSTEE_IS_SID; ++ hadExpectedPermissions = hadExpectedPermissions && *(pEA[1].Trustee.ptstrName) == *(LPWSTR)(everyoneSid.get()); ++ ++ return hadExpectedPermissions; ++ } + // Tries to read a file somewhat atomically without locking it. + // Strips the UTF8 BOM if it exists. +- std::string ReadUTF8File(const std::filesystem::path& path) ++ std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly) + { ++ if (elevatedOnly) ++ { ++ const bool hadExpectedPermissions{ _hasExpectedPermissions(path) }; ++ if (!hadExpectedPermissions) ++ { ++ // delete the file. It's been compromised. ++ LOG_LAST_ERROR_IF(!DeleteFile(path.c_str())); ++ // Exit early, because obviously there's nothing to read from the deleted file. ++ return ""; ++ } ++ } ++ + // From some casual observations we can determine that: + // * ReadFile() always returns the requested amount of data (unless the file is smaller) + // * It's unlikely that the file was changed between GetFileSize() and ReadFile() +@@ -89,11 +170,11 @@ namespace Microsoft::Terminal::Settings::Model + } + + // Same as ReadUTF8File, but returns an empty optional, if the file couldn't be opened. +- std::optional ReadUTF8FileIfExists(const std::filesystem::path& path) ++ std::optional ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly) + { + try + { +- return { ReadUTF8File(path) }; ++ return { ReadUTF8File(path, elevatedOnly) }; + } + catch (const wil::ResultException& exception) + { +@@ -106,9 +187,75 @@ namespace Microsoft::Terminal::Settings::Model + } + } + +- void WriteUTF8File(const std::filesystem::path& path, const std::string_view content) ++ void WriteUTF8File(const std::filesystem::path& path, ++ const std::string_view content, ++ const bool elevatedOnly) + { +- wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; ++ SECURITY_ATTRIBUTES sa; ++ if (elevatedOnly) ++ { ++ // This is very vaguely taken from ++ // https://docs.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c-- ++ // With using https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids ++ // to find out that ++ // * SECURITY_NT_AUTHORITY+SECURITY_LOCAL_SYSTEM_RID == NT AUTHORITY\SYSTEM ++ // * SECURITY_NT_AUTHORITY+SECURITY_BUILTIN_DOMAIN_RID+DOMAIN_ALIAS_RID_ADMINS == BUILTIN\Administrators ++ // * SECURITY_WORLD_SID_AUTHORITY+SECURITY_WORLD_RID == Everyone ++ // ++ // Raymond Chen recommended that I make this file only writable by ++ // SYSTEM, but if I did that, then even we can't write the file ++ // while elevated, which isn't what we want. ++ ++ wil::unique_sid everyoneSid{}; ++ wil::unique_sid adminGroupSid{}; ++ SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; ++ SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; ++ ++ // Create a SID for the BUILTIN\Administrators group. ++ THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid)); ++ ++ // Create a well-known SID for the Everyone group. ++ THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid)); ++ ++ EXPLICIT_ACCESS ea[2]; ++ ZeroMemory(&ea, 2 * sizeof(EXPLICIT_ACCESS)); ++ // Grant Admins all permissions on this file ++ ea[0].grfAccessPermissions = GENERIC_ALL; ++ ea[0].grfAccessMode = SET_ACCESS; ++ ea[0].grfInheritance = NO_INHERITANCE; ++ ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ++ ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ++ ea[0].Trustee.ptstrName = (LPWSTR)(adminGroupSid.get()); ++ ++ // Grant Everyone the permission or read this file ++ ea[1].grfAccessPermissions = GENERIC_READ; ++ ea[1].grfAccessMode = SET_ACCESS; ++ ea[1].grfInheritance = NO_INHERITANCE; ++ ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; ++ ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ++ ea[1].Trustee.ptstrName = (LPWSTR)(everyoneSid.get()); ++ ++ ACL acl; ++ PACL pAcl = &acl; ++ THROW_IF_WIN32_ERROR(SetEntriesInAcl(2, ea, nullptr, &pAcl)); ++ ++ SECURITY_DESCRIPTOR sd; ++ THROW_IF_WIN32_BOOL_FALSE(InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)); ++ THROW_IF_WIN32_BOOL_FALSE(SetSecurityDescriptorDacl(&sd, true, pAcl, false)); ++ ++ // Initialize a security attributes structure. ++ sa.nLength = sizeof(SECURITY_ATTRIBUTES); ++ sa.lpSecurityDescriptor = &sd; ++ sa.bInheritHandle = false; ++ } ++ ++ wil::unique_hfile file{ CreateFileW(path.c_str(), ++ GENERIC_WRITE, ++ FILE_SHARE_READ | FILE_SHARE_WRITE, ++ elevatedOnly ? &sa : nullptr, ++ CREATE_ALWAYS, ++ FILE_ATTRIBUTE_NORMAL, ++ nullptr) }; + THROW_LAST_ERROR_IF(!file); + + const auto fileSize = gsl::narrow(content.size()); +@@ -121,7 +268,8 @@ namespace Microsoft::Terminal::Settings::Model + } + } + +- void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content) ++ void WriteUTF8FileAtomic(const std::filesystem::path& path, ++ const std::string_view content) + { + // GH#10787: rename() will replace symbolic links themselves and not the path they point at. + // It's thus important that we first resolve them before generating temporary path. +diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h +index d2e2eb53c..0426b4dd5 100644 +--- a/src/cascadia/TerminalSettingsModel/FileUtils.h ++++ b/src/cascadia/TerminalSettingsModel/FileUtils.h +@@ -4,8 +4,8 @@ + namespace Microsoft::Terminal::Settings::Model + { + std::filesystem::path GetBaseSettingsPath(); +- std::string ReadUTF8File(const std::filesystem::path& path); +- std::optional ReadUTF8FileIfExists(const std::filesystem::path& path); +- void WriteUTF8File(const std::filesystem::path& path, const std::string_view content); ++ std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly = false); ++ std::optional ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly = false); ++ void WriteUTF8File(const std::filesystem::path& path, const std::string_view content, const bool elevatedOnly = false); + void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content); + } +diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h +index 58124d84b..1b26f7bae 100644 +--- a/src/cascadia/TerminalSettingsModel/JsonUtils.h ++++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h +@@ -382,7 +382,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils + }; + + template +- + struct ConversionTrait> + { + std::unordered_map FromJson(const Json::Value& json) const +diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +index 202f07808..ce12741d1 100644 +--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj ++++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +@@ -21,6 +21,7 @@ + + IconPathConverter.idl + ++ + + + ActionArgs.idl +@@ -35,6 +36,9 @@ + + ApplicationState.idl + ++ ++ ElevatedState.idl ++ + + CascadiaSettings.idl + +@@ -88,6 +92,7 @@ + IconPathConverter.idl + + ++ + + Create + +@@ -107,6 +112,9 @@ + + ApplicationState.idl + ++ ++ ElevatedState.idl ++ + + CascadiaSettings.idl + +@@ -156,6 +164,7 @@ + + + ++ + + + +diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h +index d5473939c..25773a45d 100644 +--- a/src/cascadia/TerminalSettingsModel/pch.h ++++ b/src/cascadia/TerminalSettingsModel/pch.h +@@ -53,3 +53,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider); + + // Manually include til after we include Windows.Foundation to give it winrt superpowers + #include "til.h" ++ ++#include ++#include diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h index 53f6145d2..01b4cdfe8 100644 --- a/src/cascadia/LocalTests_SettingsModel/pch.h +++ b/src/cascadia/LocalTests_SettingsModel/pch.h @@ -61,6 +61,9 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include +#include + // Common includes for most tests: #include "../../inc/argb.h" #include "../../inc/conattrs.hpp" diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index c5a81c100..38edbc2c9 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -210,7 +210,7 @@ namespace winrt::TerminalApp::implementation } AppLogic::AppLogic() : - _reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } } + _reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); ElevatedState::SharedInstance().Reload(); } } { // 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, @@ -927,8 +927,6 @@ namespace winrt::TerminalApp::implementation void AppLogic::_RegisterSettingsChange() { const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } }; - const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } }; - _reader.create( settingsPath.parent_path().c_str(), false, @@ -937,14 +935,17 @@ namespace winrt::TerminalApp::implementation // 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(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) { + [this, settingsPath](wil::FolderChangeEvent, PCWSTR fileModified) { + static const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } }; + static const std::filesystem::path elevatedStatePath{ std::wstring_view{ ElevatedState::SharedInstance().FilePath() } }; + const auto modifiedBasename = std::filesystem::path{ fileModified }.filename(); - if (modifiedBasename == settingsBasename) + if (modifiedBasename == settingsPath.filename()) { _reloadSettings->Run(); } - else if (modifiedBasename == stateBasename) + else if (modifiedBasename == statePath.filename() || modifiedBasename == elevatedStatePath.filename()) { _reloadState(); } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 5a00ba2b4..ec7f5cd7a 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -60,38 +60,40 @@ using namespace ::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + ApplicationState::ApplicationState(std::filesystem::path path) noexcept : + BaseApplicationState{ path } {} + // Returns the application-global ApplicationState object. Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() { static auto state = winrt::make_self(GetBaseSettingsPath() / stateFileName); + state->Reload(); return *state; } - ApplicationState::ApplicationState(std::filesystem::path path) noexcept : - _path{ std::move(path) }, - _throttler{ std::chrono::seconds(1), [this]() { _write(); } } + void ApplicationState::FromJson(const Json::Value& root) const noexcept { - _read(); + auto state = _state.lock(); + // GetValueForKey() comes in two variants: + // * take a std::optional reference + // * return std::optional by value + // At the time of writing the former version skips missing fields in the json, + // but we want to explicitly clear state fields that were removed from state.json. +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) +#undef MTSM_APPLICATION_STATE_GEN } - - // The destructor ensures that the last write is flushed to disk before returning. - ApplicationState::~ApplicationState() + Json::Value ApplicationState::ToJson() const noexcept { - // This will ensure that we not just cancel the last outstanding timer, - // but instead force it to run as soon as possible and wait for it to complete. - _throttler.flush(); - } + Json::Value root{ Json::objectValue }; - // Re-read the state.json from disk. - void ApplicationState::Reload() const noexcept - { - _read(); - } - - // Returns the state.json path on the disk. - winrt::hstring ApplicationState::FilePath() const noexcept - { - return winrt::hstring{ _path.wstring() }; + { + auto state = _state.lock_shared(); +#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); + MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) +#undef MTSM_APPLICATION_STATE_GEN + } + return root; } // Generate all getter/setters @@ -115,58 +117,4 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN - // Deserializes the state.json at _path into this ApplicationState. - // * ANY errors during app state will result in the creation of a new empty state. - // * ANY errors during runtime will result in changes being partially ignored. - void ApplicationState::_read() const noexcept - try - { - const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); - if (data.empty()) - { - return; - } - - std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - - Json::Value root; - if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) - { - throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); - } - - auto state = _state.lock(); - // GetValueForKey() comes in two variants: - // * take a std::optional reference - // * return std::optional by value - // At the time of writing the former version skips missing fields in the json, - // but we want to explicitly clear state fields that were removed from state.json. -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); - MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) -#undef MTSM_APPLICATION_STATE_GEN - } - CATCH_LOG() - - // Serialized this ApplicationState (in `context`) into the state.json at _path. - // * Errors are only logged. - // * _state->_writeScheduled is set to false, signaling our - // setters that _synchronize() needs to be called again. - void ApplicationState::_write() const noexcept - try - { - Json::Value root{ Json::objectValue }; - - { - auto state = _state.lock_shared(); -#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); - MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) -#undef MTSM_APPLICATION_STATE_GEN - } - - Json::StreamWriterBuilder wbuilder; - const auto content = Json::writeString(wbuilder, root); - WriteUTF8FileAtomic(_path, content); - } - CATCH_LOG() } diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 71c6a576e..f7011289e 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -12,12 +12,11 @@ Abstract: --*/ #pragma once +#include "BaseApplicationState.h" #include "ApplicationState.g.h" #include "WindowLayout.g.h" #include -#include -#include #include // This macro generates all getters and setters for ApplicationState. @@ -40,18 +39,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation friend ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait; }; - struct ApplicationState : ApplicationStateT + struct ApplicationState : public BaseApplicationState, ApplicationStateT { static Microsoft::Terminal::Settings::Model::ApplicationState SharedInstance(); ApplicationState(std::filesystem::path path) noexcept; - ~ApplicationState(); - // Methods - void Reload() const noexcept; - - // General getters/setters - winrt::hstring FilePath() const noexcept; + virtual void FromJson(const Json::Value& root) const noexcept override; + virtual Json::Value ToJson() const noexcept override; // State getters/setters #define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ @@ -67,13 +62,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) #undef MTSM_APPLICATION_STATE_GEN }; - - void _write() const noexcept; - void _read() const noexcept; - - std::filesystem::path _path; til::shared_mutex _state; - til::throttled_func_trailing<> _throttler; }; } diff --git a/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp b/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp new file mode 100644 index 000000000..3b5c02446 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/BaseApplicationState.cpp @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "BaseApplicationState.h" +#include "CascadiaSettings.h" + +#include "JsonUtils.h" +#include "FileUtils.h" + +constexpr std::wstring_view stateFileName{ L"state.json" }; +using namespace ::Microsoft::Terminal::Settings::Model; + +BaseApplicationState::BaseApplicationState(std::filesystem::path path) noexcept : + _path{ std::move(path) }, + _throttler{ std::chrono::seconds(1), [this]() { _write(); } } +{ + // DON'T _read() here! _read() will call FromJson, which is virtual, and + // needs to be implemented in a derived class. Classes that derive from + // BaseApplicationState should make sure to call Reload() after construction + // to ensure the data is loaded. +} + +// The destructor ensures that the last write is flushed to disk before returning. +BaseApplicationState::~BaseApplicationState() +{ + // This will ensure that we not just cancel the last outstanding timer, + // but instead force it to run as soon as possible and wait for it to complete. + _throttler.flush(); +} + +// Re-read the state.json from disk. +void BaseApplicationState::Reload() const noexcept +{ + _read(); +} + +// Returns the state.json path on the disk. +winrt::hstring BaseApplicationState::FilePath() const noexcept +{ + return winrt::hstring{ _path.wstring() }; +} + +// Deserializes the state.json at _path into this ApplicationState. +// * ANY errors during app state will result in the creation of a new empty state. +// * ANY errors during runtime will result in changes being partially ignored. +void BaseApplicationState::_read() const noexcept +try +{ + const auto data = _readFileContents().value_or(std::string{}); + if (data.empty()) + { + return; + } + + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + + FromJson(root); +} +CATCH_LOG() + +// Serialized this ApplicationState (in `context`) into the state.json at _path. +// * Errors are only logged. +// * _state->_writeScheduled is set to false, signaling our +// setters that _synchronize() needs to be called again. +void BaseApplicationState::_write() const noexcept +try +{ + Json::Value root{ this->ToJson() }; + + Json::StreamWriterBuilder wbuilder; + const auto content = Json::writeString(wbuilder, root); + _writeFileContents(content); +} +CATCH_LOG() + +std::optional BaseApplicationState::_readFileContents() const +{ + return ReadUTF8FileIfExists(_path); +} + +void BaseApplicationState::_writeFileContents(const std::string_view content) const +{ + WriteUTF8FileAtomic(_path, content); +} diff --git a/src/cascadia/TerminalSettingsModel/BaseApplicationState.h b/src/cascadia/TerminalSettingsModel/BaseApplicationState.h new file mode 100644 index 000000000..e684d3192 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/BaseApplicationState.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApplicationState.h + +Abstract: +- TODO! +--*/ +#pragma once + +struct BaseApplicationState +{ + BaseApplicationState(std::filesystem::path path) noexcept; + ~BaseApplicationState(); + + // Methods + void Reload() const noexcept; + + // General getters/setters + winrt::hstring FilePath() const noexcept; + + virtual void FromJson(const Json::Value& root) const noexcept = 0; + virtual Json::Value ToJson() const noexcept = 0; + +protected: + virtual std::optional _readFileContents() const; + virtual void _writeFileContents(const std::string_view content) const; + + void _write() const noexcept; + void _read() const noexcept; + + std::filesystem::path _path; + til::throttled_func_trailing<> _throttler; +}; diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.cpp b/src/cascadia/TerminalSettingsModel/ElevatedState.cpp new file mode 100644 index 000000000..f7d72207e --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ElevatedState.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ElevatedState.h" +#include "CascadiaSettings.h" +#include "ElevatedState.g.cpp" + +#include "JsonUtils.h" +#include "FileUtils.h" + +#include + +constexpr std::wstring_view stateFileName{ L"elevated-state.json" }; + +using namespace ::Microsoft::Terminal::Settings::Model; + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + ElevatedState::ElevatedState(std::filesystem::path path) noexcept : + BaseApplicationState{ path } {} + + // Returns the application-global ElevatedState object. + Microsoft::Terminal::Settings::Model::ElevatedState ElevatedState::SharedInstance() + { + // TODO! place in a totally different file! and path! + static auto state = winrt::make_self(GetBaseSettingsPath() / stateFileName); + state->Reload(); + + // const auto testPath{ GetBaseSettingsPath() / L"test.json" }; + + // PSID pEveryoneSID = NULL; + // SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_NT_AUTHORITY; + // BOOL success = AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryoneSID); + + // EXPLICIT_ACCESS ea[1]; + // ZeroMemory(&ea, 1 * sizeof(EXPLICIT_ACCESS)); + // ea[0].grfAccessPermissions = KEY_READ; + // ea[0].grfAccessMode = SET_ACCESS; + // ea[0].grfInheritance = NO_INHERITANCE; + // ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; + // ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + // ea[0].Trustee.ptstrName = (LPTSTR)pEveryoneSID; + + // ACL acl; + // PACL pAcl = &acl; + // DWORD dwRes = SetEntriesInAcl(1, ea, NULL, &pAcl); + // dwRes; + + // SECURITY_DESCRIPTOR sd; + // success = InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); + // success = SetSecurityDescriptorDacl(&sd, + // TRUE, // bDaclPresent flag + // pAcl, + // FALSE); + + // SECURITY_ATTRIBUTES sa; + // // Initialize a security attributes structure. + // sa.nLength = sizeof(SECURITY_ATTRIBUTES); + // sa.lpSecurityDescriptor = &sd; + // sa.bInheritHandle = FALSE; + // success; + + // wil::unique_hfile file{ CreateFileW(testPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + // THROW_LAST_ERROR_IF(!file); + + return *state; + } + + void ElevatedState::FromJson(const Json::Value& root) const noexcept + { + auto state = _state.lock(); + // GetValueForKey() comes in two variants: + // * take a std::optional reference + // * return std::optional by value + // At the time of writing the former version skips missing fields in the json, + // but we want to explicitly clear state fields that were removed from state.json. +#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey>(root, key); + MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) +#undef MTSM_ELEVATED_STATE_GEN + } + Json::Value ElevatedState::ToJson() const noexcept + { + Json::Value root{ Json::objectValue }; + + { + auto state = _state.lock_shared(); +#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); + MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) +#undef MTSM_ELEVATED_STATE_GEN + } + return root; + } + + // Generate all getter/setters +#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) \ + type ElevatedState::name() const noexcept \ + { \ + const auto state = _state.lock_shared(); \ + const auto& value = state->name; \ + return value ? *value : type{ __VA_ARGS__ }; \ + } \ + \ + void ElevatedState::name(const type& value) noexcept \ + { \ + { \ + auto state = _state.lock(); \ + state->name.emplace(value); \ + } \ + \ + _throttler(); \ + } + MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) +#undef MTSM_ELEVATED_STATE_GEN + + void ElevatedState::_writeFileContents(const std::string_view content) const + { + // DON'T use WriteUTF8FileAtomic, which will write to a temporary file + // then rename that file to the final filename. That actually lets us + // overwrite the elevate file's contents even when unelevated, because + // we're effectively deleting the original file, then renaming a + // different file in it's place. + // + // We're not worried about someone else doing that though, if they do + // that with the wrong permissions, then we'll just ignore the file and + // start over. + WriteUTF8File(_path, content, true); + } + + std::optional ElevatedState::_readFileContents() const + { + return ReadUTF8FileIfExists(_path, true); + } +} diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.h b/src/cascadia/TerminalSettingsModel/ElevatedState.h new file mode 100644 index 000000000..d0eb8381f --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ElevatedState.h @@ -0,0 +1,60 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ElevatedState.h + +Abstract: +- If the CascadiaSettings class were AppData, then this class would be LocalAppData. + Put anything in here that you wouldn't want to be stored next to user-editable settings. +- Modify ElevatedState.idl and MTSM_ELEVATED_STATE_FIELDS to add new fields. +--*/ +#pragma once + +#include "BaseApplicationState.h" +#include "ElevatedState.g.h" +#include + +// This macro generates all getters and setters for ElevatedState. +// It provides X with the following arguments: +// (type, function name, JSON key, ...variadic construction arguments) +#define MTSM_ELEVATED_STATE_FIELDS(X) \ + X(Windows::Foundation::Collections::IVector, AllowedCommandlines, "allowedCommandlines") + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct ElevatedState : ElevatedStateT, public BaseApplicationState + { + static Microsoft::Terminal::Settings::Model::ElevatedState SharedInstance(); + + ElevatedState(std::filesystem::path path) noexcept; + + void FromJson(const Json::Value& root) const noexcept override; + Json::Value ToJson() const noexcept override; + + // State getters/setters +#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) \ + type name() const noexcept; \ + void name(const type& value) noexcept; + MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) +#undef MTSM_ELEVATED_STATE_GEN + + private: + struct state_t + { +#define MTSM_ELEVATED_STATE_GEN(type, name, key, ...) std::optional name{ __VA_ARGS__ }; + MTSM_ELEVATED_STATE_FIELDS(MTSM_ELEVATED_STATE_GEN) +#undef MTSM_ELEVATED_STATE_GEN + }; + til::shared_mutex _state; + + virtual std::optional _readFileContents() const override; + virtual void _writeFileContents(const std::string_view content) const override; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(ElevatedState); +} diff --git a/src/cascadia/TerminalSettingsModel/ElevatedState.idl b/src/cascadia/TerminalSettingsModel/ElevatedState.idl new file mode 100644 index 000000000..09e2d05b0 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ElevatedState.idl @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Model +{ + [default_interface] runtimeclass ElevatedState { + static ElevatedState SharedInstance(); + + void Reload(); + + String FilePath { get; }; + + Windows.Foundation.Collections.IVector AllowedCommandlines { get; set; }; + } +} diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index 81fbb6cee..dff35897a 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -8,6 +8,8 @@ #include #include +#include + static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" }; @@ -39,10 +41,89 @@ namespace Microsoft::Terminal::Settings::Model return baseSettingsPath; } + static bool _hasExpectedPermissions(const std::filesystem::path& path) + { + // If we want to only open the file if it's elevated, check the + // permissions on this file. We want to make sure that: + // * Everyone has permission to read + // * admins can do anything + // * no one else can do anything. + PACL pAcl{ nullptr }; // This doesn't need to be cleanup up apparently + + auto status = GetNamedSecurityInfo(path.c_str(), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + &pAcl, + nullptr, + nullptr); + THROW_IF_WIN32_ERROR(status); + + PEXPLICIT_ACCESS pEA{ nullptr }; + DWORD count = 0; + status = GetExplicitEntriesFromAcl(pAcl, &count, &pEA); + THROW_IF_WIN32_ERROR(status); + + auto explicitAccessCleanup = wil::scope_exit([&]() { ::LocalFree(pEA); }); + + if (count != 2) + { + return false; + } + + // Now, get the Everyone and Admins SIDS so we can make sure they're + // the ones in this file. + + wil::unique_sid everyoneSid{}; + wil::unique_sid adminGroupSid{}; + SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; + SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; + + // Create a SID for the BUILTIN\Administrators group. + THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid)); + + // Create a well-known SID for the Everyone group. + THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid)); + + bool hadExpectedPermissions = true; + + // Check that the permissions are what we'd expect them to be if only + // admins can write to the file. This is basically a mirror of what we + // set up in `WriteUTF8File`. + + // For grfAccessPermissions, GENERIC_ALL turns into STANDARD_RIGHTS_ALL, + // and GENERIC_READ -> READ_CONTROL + hadExpectedPermissions = hadExpectedPermissions && WI_AreAllFlagsSet(pEA[0].grfAccessPermissions, STANDARD_RIGHTS_ALL); + hadExpectedPermissions = hadExpectedPermissions && pEA[0].grfInheritance == NO_INHERITANCE; + hadExpectedPermissions = hadExpectedPermissions && pEA[0].Trustee.TrusteeForm == TRUSTEE_IS_SID; + // SIDs are void*'s that happen to convert to a wchar_t + hadExpectedPermissions = hadExpectedPermissions && *(pEA[0].Trustee.ptstrName) == *(LPWSTR)(adminGroupSid.get()); + + // Now check the other EXPLICIT_ACCESS + hadExpectedPermissions = hadExpectedPermissions && WI_IsFlagSet(pEA[1].grfAccessPermissions, READ_CONTROL); + hadExpectedPermissions = hadExpectedPermissions && pEA[1].grfInheritance == NO_INHERITANCE; + hadExpectedPermissions = hadExpectedPermissions && pEA[1].Trustee.TrusteeForm == TRUSTEE_IS_SID; + hadExpectedPermissions = hadExpectedPermissions && *(pEA[1].Trustee.ptstrName) == *(LPWSTR)(everyoneSid.get()); + + return hadExpectedPermissions; + } // Tries to read a file somewhat atomically without locking it. // Strips the UTF8 BOM if it exists. - std::string ReadUTF8File(const std::filesystem::path& path) + std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly) { + if (elevatedOnly) + { + const bool hadExpectedPermissions{ _hasExpectedPermissions(path) }; + if (!hadExpectedPermissions) + { + // delete the file. It's been compromised. + LOG_LAST_ERROR_IF(!DeleteFile(path.c_str())); + // Exit early, because obviously there's nothing to read from the deleted file. + return ""; + } + } + // From some casual observations we can determine that: // * ReadFile() always returns the requested amount of data (unless the file is smaller) // * It's unlikely that the file was changed between GetFileSize() and ReadFile() @@ -89,11 +170,11 @@ namespace Microsoft::Terminal::Settings::Model } // Same as ReadUTF8File, but returns an empty optional, if the file couldn't be opened. - std::optional ReadUTF8FileIfExists(const std::filesystem::path& path) + std::optional ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly) { try { - return { ReadUTF8File(path) }; + return { ReadUTF8File(path, elevatedOnly) }; } catch (const wil::ResultException& exception) { @@ -106,9 +187,75 @@ namespace Microsoft::Terminal::Settings::Model } } - void WriteUTF8File(const std::filesystem::path& path, const std::string_view content) + void WriteUTF8File(const std::filesystem::path& path, + const std::string_view content, + const bool elevatedOnly) { - wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) }; + SECURITY_ATTRIBUTES sa; + if (elevatedOnly) + { + // This is very vaguely taken from + // https://docs.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c-- + // With using https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids + // to find out that + // * SECURITY_NT_AUTHORITY+SECURITY_LOCAL_SYSTEM_RID == NT AUTHORITY\SYSTEM + // * SECURITY_NT_AUTHORITY+SECURITY_BUILTIN_DOMAIN_RID+DOMAIN_ALIAS_RID_ADMINS == BUILTIN\Administrators + // * SECURITY_WORLD_SID_AUTHORITY+SECURITY_WORLD_RID == Everyone + // + // Raymond Chen recommended that I make this file only writable by + // SYSTEM, but if I did that, then even we can't write the file + // while elevated, which isn't what we want. + + wil::unique_sid everyoneSid{}; + wil::unique_sid adminGroupSid{}; + SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; + SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; + + // Create a SID for the BUILTIN\Administrators group. + THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid)); + + // Create a well-known SID for the Everyone group. + THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyoneSid)); + + EXPLICIT_ACCESS ea[2]; + ZeroMemory(&ea, 2 * sizeof(EXPLICIT_ACCESS)); + // Grant Admins all permissions on this file + ea[0].grfAccessPermissions = GENERIC_ALL; + ea[0].grfAccessMode = SET_ACCESS; + ea[0].grfInheritance = NO_INHERITANCE; + ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + ea[0].Trustee.ptstrName = (LPWSTR)(adminGroupSid.get()); + + // Grant Everyone the permission or read this file + ea[1].grfAccessPermissions = GENERIC_READ; + ea[1].grfAccessMode = SET_ACCESS; + ea[1].grfInheritance = NO_INHERITANCE; + ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + ea[1].Trustee.ptstrName = (LPWSTR)(everyoneSid.get()); + + ACL acl; + PACL pAcl = &acl; + THROW_IF_WIN32_ERROR(SetEntriesInAcl(2, ea, nullptr, &pAcl)); + + SECURITY_DESCRIPTOR sd; + THROW_IF_WIN32_BOOL_FALSE(InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)); + THROW_IF_WIN32_BOOL_FALSE(SetSecurityDescriptorDacl(&sd, true, pAcl, false)); + + // Initialize a security attributes structure. + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = &sd; + sa.bInheritHandle = false; + } + + wil::unique_hfile file{ CreateFileW(path.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + elevatedOnly ? &sa : nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr) }; THROW_LAST_ERROR_IF(!file); const auto fileSize = gsl::narrow(content.size()); @@ -121,7 +268,8 @@ namespace Microsoft::Terminal::Settings::Model } } - void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content) + void WriteUTF8FileAtomic(const std::filesystem::path& path, + const std::string_view content) { // GH#10787: rename() will replace symbolic links themselves and not the path they point at. // It's thus important that we first resolve them before generating temporary path. diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h index d2e2eb53c..0426b4dd5 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.h +++ b/src/cascadia/TerminalSettingsModel/FileUtils.h @@ -4,8 +4,8 @@ namespace Microsoft::Terminal::Settings::Model { std::filesystem::path GetBaseSettingsPath(); - std::string ReadUTF8File(const std::filesystem::path& path); - std::optional ReadUTF8FileIfExists(const std::filesystem::path& path); - void WriteUTF8File(const std::filesystem::path& path, const std::string_view content); + std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly = false); + std::optional ReadUTF8FileIfExists(const std::filesystem::path& path, const bool elevatedOnly = false); + void WriteUTF8File(const std::filesystem::path& path, const std::string_view content, const bool elevatedOnly = false); void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content); } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index 58124d84b..1b26f7bae 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -382,7 +382,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils }; template - struct ConversionTrait> { std::unordered_map FromJson(const Json::Value& json) const diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index 202f07808..ce12741d1 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -21,6 +21,7 @@ IconPathConverter.idl + ActionArgs.idl @@ -35,6 +36,9 @@ ApplicationState.idl + + ElevatedState.idl + CascadiaSettings.idl @@ -88,6 +92,7 @@ IconPathConverter.idl + Create @@ -107,6 +112,9 @@ ApplicationState.idl + + ElevatedState.idl + CascadiaSettings.idl @@ -156,6 +164,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h index d5473939c..25773a45d 100644 --- a/src/cascadia/TerminalSettingsModel/pch.h +++ b/src/cascadia/TerminalSettingsModel/pch.h @@ -53,3 +53,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider); // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" + +#include +#include