Introduce a basic ApplicationState class (#10513)
This commit introduces a basic ApplicationState class, without being used for anything yet to aid reviewers. At a later point actual usages of this new class may be added separately. ## References This commit is an initial step towards implementing #8324. ## PR Checklist * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Creating a `state.json` with `{"generatedProfiles":["{53e75ed9-2b63-4118-856d-0510c4f6b97e}"]}` updates the ApplicationState, as observed through a debugger ✔️ * Deleting the "generatedProfiles" field sets the corresponding field back to nullopt ✔️
This commit is contained in:
parent
6a37818c07
commit
ab5a8d701d
|
@ -1443,6 +1443,7 @@ MSVCRTD
|
|||
MSVS
|
||||
msys
|
||||
msysgit
|
||||
MTSM
|
||||
mui
|
||||
Mul
|
||||
multiline
|
||||
|
@ -2416,7 +2417,6 @@ uapadmin
|
|||
UAX
|
||||
ubuntu
|
||||
ucd
|
||||
ucd
|
||||
ucdxml
|
||||
uch
|
||||
UCHAR
|
||||
|
@ -2780,7 +2780,6 @@ xml
|
|||
xmlns
|
||||
xor
|
||||
xorg
|
||||
xorg
|
||||
Xpath
|
||||
XPosition
|
||||
XResource
|
||||
|
|
|
@ -6,9 +6,9 @@ Adding a setting to Windows Terminal is fairly straightforward. This guide serve
|
|||
|
||||
The Terminal Settings Model (`Microsoft.Terminal.Settings.Model`) is responsible for (de)serializing and exposing settings.
|
||||
|
||||
### `GETSET_SETTING` macro
|
||||
### `INHERITABLE_SETTING` macro
|
||||
|
||||
The `GETSET_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
|
||||
The `INHERITABLE_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
|
||||
- `type`: the type that the setting will be stored as
|
||||
- `name`: the name of the variable for storage
|
||||
- `defaultValue`: the value to use if the user does not define the setting anywhere
|
||||
|
@ -20,7 +20,7 @@ This tutorial will add `CloseOnExitMode CloseOnExit` as a profile setting.
|
|||
1. In `Profile.h`, declare/define the setting:
|
||||
|
||||
```c++
|
||||
GETSET_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
|
||||
INHERITABLE_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
|
||||
```
|
||||
|
||||
2. In `Profile.idl`, expose the setting via WinRT:
|
||||
|
@ -141,7 +141,7 @@ struct OpenSettingsArgs : public OpenSettingsArgsT<OpenSettingsArgs>
|
|||
OpenSettingsArgs() = default;
|
||||
|
||||
// adds a getter/setter for your argument, and defines the json key
|
||||
GETSET_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
|
||||
WINRT_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
|
||||
static constexpr std::string_view TargetKey{ "target" };
|
||||
|
||||
public:
|
||||
|
@ -213,9 +213,9 @@ Terminal-level settings are settings that affect a shell session. Generally, the
|
|||
- Declare the setting in `IControlSettings.idl` or `ICoreSettings.idl` (whichever is relevant to your setting). If your setting is an enum setting, declare the enum here instead of in the `TerminalSettingsModel` project.
|
||||
- In `TerminalSettings.h`, declare/define the setting...
|
||||
```c++
|
||||
// The GETSET_PROPERTY macro declares/defines a getter setter for the setting.
|
||||
// Like GETSET_SETTING, it takes in a type, name, and defaultValue.
|
||||
GETSET_PROPERTY(bool, UseAcrylic, false);
|
||||
// The WINRT_PROPERTY macro declares/defines a getter setter for the setting.
|
||||
// Like INHERITABLE_SETTING, it takes in a type, name, and defaultValue.
|
||||
WINRT_PROPERTY(bool, UseAcrylic, false);
|
||||
```
|
||||
- In `TerminalSettings.cpp`...
|
||||
- update `_ApplyProfileSettings` for profile settings
|
||||
|
|
|
@ -189,9 +189,7 @@ namespace winrt::TerminalApp::implementation
|
|||
}
|
||||
|
||||
AppLogic::AppLogic() :
|
||||
_dialogLock{},
|
||||
_loadedInitialSettings{ false },
|
||||
_settingsLoadedResult{ S_OK }
|
||||
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::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,
|
||||
|
@ -204,6 +202,13 @@ namespace winrt::TerminalApp::implementation
|
|||
// SetTitleBarContent
|
||||
_isElevated = _isUserAdmin();
|
||||
_root = winrt::make_self<TerminalPage>();
|
||||
|
||||
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() {
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
self->_ReloadSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -859,59 +864,29 @@ namespace winrt::TerminalApp::implementation
|
|||
// - <none>
|
||||
void AppLogic::_RegisterSettingsChange()
|
||||
{
|
||||
// Get the containing folder.
|
||||
const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } };
|
||||
const auto folder = settingsPath.parent_path();
|
||||
const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } };
|
||||
|
||||
_reader.create(folder.c_str(),
|
||||
false,
|
||||
wil::FolderChangeEvents::All,
|
||||
[this, settingsPath](wil::FolderChangeEvent event, PCWSTR fileModified) {
|
||||
// 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.
|
||||
if (!(event == wil::FolderChangeEvent::Modified ||
|
||||
event == wil::FolderChangeEvent::RenameNewName ||
|
||||
event == wil::FolderChangeEvent::Removed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_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(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
|
||||
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename();
|
||||
|
||||
std::filesystem::path modifiedFilePath = fileModified;
|
||||
|
||||
// Getting basename (filename.ext)
|
||||
const auto settingsBasename = settingsPath.filename();
|
||||
const auto modifiedBasename = modifiedFilePath.filename();
|
||||
|
||||
if (settingsBasename == modifiedBasename)
|
||||
{
|
||||
this->_DispatchReloadSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Dispatches a settings reload with debounce.
|
||||
// Text editors implement Save in a bunch of different ways, so
|
||||
// this stops us from reloading too many times or too quickly.
|
||||
fire_and_forget AppLogic::_DispatchReloadSettings()
|
||||
{
|
||||
if (_settingsReloadQueued.exchange(true))
|
||||
{
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto weakSelf = get_weak();
|
||||
|
||||
co_await winrt::resume_after(std::chrono::milliseconds(100));
|
||||
co_await winrt::resume_foreground(_root->Dispatcher());
|
||||
|
||||
if (auto self{ weakSelf.get() })
|
||||
{
|
||||
_ReloadSettings();
|
||||
_settingsReloadQueued.store(false);
|
||||
}
|
||||
if (modifiedBasename == settingsBasename)
|
||||
{
|
||||
_reloadSettings->Run();
|
||||
}
|
||||
else if (modifiedBasename == stateBasename)
|
||||
{
|
||||
_reloadState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AppLogic::_ApplyLanguageSettingChange() noexcept
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
#include "FindTargetWindowResult.g.h"
|
||||
#include "TerminalPage.h"
|
||||
#include "Jumplist.h"
|
||||
#include "../../cascadia/inc/cppwinrt_utils.h"
|
||||
|
||||
#include <inc/cppwinrt_utils.h>
|
||||
#include <ThrottledFunc.h>
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
// fwdecl unittest classes
|
||||
|
@ -111,17 +113,15 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
|
||||
|
||||
HRESULT _settingsLoadedResult;
|
||||
winrt::hstring _settingsLoadExceptionText{};
|
||||
|
||||
bool _loadedInitialSettings;
|
||||
|
||||
wil::unique_folder_change_reader_nothrow _reader;
|
||||
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
|
||||
til::throttled_func_trailing<> _reloadState;
|
||||
winrt::hstring _settingsLoadExceptionText;
|
||||
HRESULT _settingsLoadedResult = S_OK;
|
||||
bool _loadedInitialSettings = false;
|
||||
|
||||
std::shared_mutex _dialogLock;
|
||||
|
||||
std::atomic<bool> _settingsReloadQueued{ false };
|
||||
|
||||
::TerminalApp::AppCommandlineArgs _appArgs;
|
||||
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
|
||||
static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view<const hstring> args,
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
#include "ApplicationState.h"
|
||||
#include "CascadiaSettings.h"
|
||||
#include "ApplicationState.g.cpp"
|
||||
|
||||
#include "JsonUtils.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
constexpr std::wstring_view stateFileName{ L"state.json" };
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model::JsonUtils
|
||||
{
|
||||
// This trait exists in order to serialize the std::unordered_set for GeneratedProfiles.
|
||||
template<typename T>
|
||||
struct ConversionTrait<std::unordered_set<T>>
|
||||
{
|
||||
std::unordered_set<T> FromJson(const Json::Value& json) const
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
std::unordered_set<T> val;
|
||||
val.reserve(json.size());
|
||||
|
||||
for (const auto& element : json)
|
||||
{
|
||||
val.emplace(trait.FromJson(element));
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json) const
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) -> bool { return trait.CanConvert(json); });
|
||||
}
|
||||
|
||||
Json::Value ToJson(const std::unordered_set<T>& val)
|
||||
{
|
||||
ConversionTrait<T> trait;
|
||||
Json::Value json{ Json::arrayValue };
|
||||
|
||||
for (const auto& key : val)
|
||||
{
|
||||
json.append(trait.ToJson(key));
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
std::string TypeDescription() const
|
||||
{
|
||||
return fmt::format("{}[]", ConversionTrait<GUID>{}.TypeDescription());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
using namespace ::Microsoft::Terminal::Settings::Model;
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{
|
||||
// Returns the application-global ApplicationState object.
|
||||
Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance()
|
||||
{
|
||||
static auto state = winrt::make_self<ApplicationState>(GetBaseSettingsPath() / stateFileName);
|
||||
return *state;
|
||||
}
|
||||
|
||||
ApplicationState::ApplicationState(std::filesystem::path path) noexcept :
|
||||
_path{ std::move(path) },
|
||||
_throttler{ std::chrono::seconds(1), [this]() { _write(); } }
|
||||
{
|
||||
_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();
|
||||
}
|
||||
|
||||
// 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() };
|
||||
}
|
||||
|
||||
// Generate all getter/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
|
||||
type ApplicationState::name() const noexcept \
|
||||
{ \
|
||||
const auto state = _state.lock_shared(); \
|
||||
const auto& value = state->name; \
|
||||
return value ? *value : type{ __VA_ARGS__ }; \
|
||||
} \
|
||||
\
|
||||
void ApplicationState::name(const type& value) noexcept \
|
||||
{ \
|
||||
{ \
|
||||
auto state = _state.lock(); \
|
||||
state->name.emplace(value); \
|
||||
} \
|
||||
\
|
||||
_throttler(); \
|
||||
}
|
||||
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<Json::CharReader> 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<T> reference
|
||||
// * return std::optional<T> 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<std::optional<type>>(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()
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- ApplicationState.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 ApplicationState.idl and MTSM_APPLICATION_STATE_FIELDS to add new fields.
|
||||
--*/
|
||||
#pragma once
|
||||
|
||||
#include "ApplicationState.g.h"
|
||||
|
||||
#include <inc/cppwinrt_utils.h>
|
||||
#include <til/mutex.h>
|
||||
#include <til/throttled_func.h>
|
||||
|
||||
// This macro generates all getters and setters for ApplicationState.
|
||||
// It provides X with the following arguments:
|
||||
// (type, function name, JSON key, ...variadic construction arguments)
|
||||
#define MTSM_APPLICATION_STATE_FIELDS(X) \
|
||||
X(std::unordered_set<winrt::guid>, GeneratedProfiles, "generatedProfiles")
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{
|
||||
struct ApplicationState : ApplicationStateT<ApplicationState>
|
||||
{
|
||||
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;
|
||||
|
||||
// State getters/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \
|
||||
type name() const noexcept; \
|
||||
void name(const type& value) noexcept;
|
||||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN)
|
||||
#undef MTSM_APPLICATION_STATE_GEN
|
||||
|
||||
private:
|
||||
struct state_t
|
||||
{
|
||||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) std::optional<type> name{ __VA_ARGS__ };
|
||||
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_t> _state;
|
||||
til::throttled_func_trailing<> _throttler;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(ApplicationState);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Model
|
||||
{
|
||||
[default_interface] runtimeclass ApplicationState {
|
||||
static ApplicationState SharedInstance();
|
||||
|
||||
void Reload();
|
||||
|
||||
String FilePath { get; };
|
||||
}
|
||||
}
|
|
@ -147,9 +147,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
std::unordered_set<std::string> _AccumulateJsonFilesInDirectory(const std::wstring_view directory);
|
||||
void _ParseAndLayerFragmentFiles(const std::unordered_set<std::string> files, const winrt::hstring source);
|
||||
|
||||
static void _WriteSettings(std::string_view content, const hstring filepath);
|
||||
static const std::filesystem::path& _SettingsPath();
|
||||
static std::optional<std::string> _ReadUserSettings();
|
||||
static std::optional<std::string> _ReadFile(HANDLE hFile);
|
||||
|
||||
std::optional<guid> _GetProfileGuidByName(const hstring) const;
|
||||
std::optional<guid> _GetProfileGuidByIndex(std::optional<int> index) const;
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
#include <fmt/chrono.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
#include <WtExeUtils.h>
|
||||
|
||||
// defaults.h is a file containing the default json settings in a std::string_view
|
||||
#include "defaults.h"
|
||||
#include "defaults-universal.h"
|
||||
|
@ -17,12 +15,15 @@
|
|||
// Both defaults.h and userDefaults.h are generated at build time into the
|
||||
// "Generated Files" directory.
|
||||
|
||||
#include "ApplicationState.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
|
||||
using namespace ::Microsoft::Console;
|
||||
using namespace ::Microsoft::Terminal::Settings::Model;
|
||||
|
||||
static constexpr std::wstring_view SettingsFilename{ L"settings.json" };
|
||||
static constexpr std::wstring_view LegacySettingsFilename{ L"profiles.json" };
|
||||
static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" };
|
||||
|
||||
static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" };
|
||||
|
||||
|
@ -40,7 +41,6 @@ static constexpr std::string_view GuidKey{ "guid" };
|
|||
|
||||
static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" };
|
||||
|
||||
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
|
||||
static constexpr std::string_view SettingsSchemaFragment{ "\n"
|
||||
R"( "$schema": "https://aka.ms/terminal-profiles-schema")" };
|
||||
|
||||
|
@ -234,7 +234,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings::
|
|||
|
||||
try
|
||||
{
|
||||
_WriteSettings(resultPtr->_userSettingsString, CascadiaSettings::SettingsPath());
|
||||
WriteUTF8FileAtomic(_SettingsPath(), resultPtr->_userSettingsString);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
@ -491,23 +491,11 @@ std::unordered_set<std::string> CascadiaSettings::_AccumulateJsonFilesInDirector
|
|||
{
|
||||
if (fragmentExt.path().extension() == jsonExtension)
|
||||
{
|
||||
wil::unique_hfile hFile{ CreateFileW(fragmentExt.path().c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
|
||||
if (!hFile)
|
||||
try
|
||||
{
|
||||
LOG_LAST_ERROR();
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto fileData = _ReadFile(hFile.get()).value();
|
||||
jsonFiles.emplace(fileData);
|
||||
jsonFiles.emplace(ReadUTF8File(fragmentExt.path()));
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
return jsonFiles;
|
||||
|
@ -637,13 +625,8 @@ void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool is
|
|||
Json::Value CascadiaSettings::_ParseUtf8JsonString(std::string_view fileData)
|
||||
{
|
||||
Json::Value result;
|
||||
// Ignore UTF-8 BOM
|
||||
auto actualDataStart = fileData.data();
|
||||
const auto actualDataStart = fileData.data();
|
||||
const auto actualDataEnd = fileData.data() + fileData.size();
|
||||
if (fileData.compare(0, Utf8Bom.size(), Utf8Bom) == 0)
|
||||
{
|
||||
actualDataStart += Utf8Bom.size();
|
||||
}
|
||||
|
||||
std::string errs; // This string will receive any error text from failing to parse.
|
||||
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
|
||||
|
@ -693,8 +676,7 @@ bool CascadiaSettings::_PrependSchemaDirective()
|
|||
// them into the user's settings at the end of the list of profiles.
|
||||
// - Does not reformat the user's settings file.
|
||||
// - Does not write the file! Only modifies in-place the _userSettingsString
|
||||
// member. Callers should make sure to call
|
||||
// _WriteSettings(_userSettingsString) to make sure to persist these changes!
|
||||
// member. Callers should make sure to persist these changes (see WriteSettingsToDisk).
|
||||
// - Assumes that the `profiles` object is at an indentation of 4 spaces, and
|
||||
// therefore each profile should be indented 8 spaces. If the user's settings
|
||||
// have a different indentation, we'll still insert valid json, it'll just be
|
||||
|
@ -1056,28 +1038,15 @@ winrt::com_ptr<ColorScheme> CascadiaSettings::_FindMatchingColorScheme(const Jso
|
|||
}
|
||||
|
||||
// Method Description:
|
||||
// - Writes the given content in UTF-8 to a settings file using the Win32 APIS's.
|
||||
// Will overwrite any existing content in the file.
|
||||
// - Returns the path of the settings.json file.
|
||||
// Arguments:
|
||||
// - content: the given string of content to write to the file.
|
||||
// Return Value:
|
||||
// - <none>
|
||||
// This can throw an exception if we fail to open the file for writing, or we
|
||||
// fail to write the file
|
||||
void CascadiaSettings::_WriteSettings(const std::string_view content, const hstring filepath)
|
||||
// Return Value:
|
||||
// - Returns a path in 80% of cases. I measured!
|
||||
const std::filesystem::path& CascadiaSettings::_SettingsPath()
|
||||
{
|
||||
wil::unique_hfile hOut{ CreateFileW(filepath.c_str(),
|
||||
GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
if (!hOut)
|
||||
{
|
||||
THROW_LAST_ERROR();
|
||||
}
|
||||
THROW_LAST_ERROR_IF(!WriteFile(hOut.get(), content.data(), gsl::narrow<DWORD>(content.size()), nullptr, nullptr));
|
||||
static const auto path = GetBaseSettingsPath() / SettingsFilename;
|
||||
return path;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -1091,92 +1060,7 @@ void CascadiaSettings::_WriteSettings(const std::string_view content, const hstr
|
|||
// from reading the file
|
||||
std::optional<std::string> CascadiaSettings::_ReadUserSettings()
|
||||
{
|
||||
const auto pathToSettingsFile{ CascadiaSettings::SettingsPath() };
|
||||
wil::unique_hfile hFile{ CreateFileW(pathToSettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
|
||||
if (!hFile)
|
||||
{
|
||||
// GH#5186 - We moved from profiles.json to settings.json; we want to
|
||||
// migrate any file we find. We're using MoveFile in case their settings.json
|
||||
// is a symbolic link.
|
||||
std::filesystem::path pathToLegacySettingsFile{ std::wstring_view{ pathToSettingsFile } };
|
||||
pathToLegacySettingsFile.replace_filename(LegacySettingsFilename);
|
||||
|
||||
wil::unique_hfile hLegacyFile{ CreateFileW(pathToLegacySettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
|
||||
if (hLegacyFile)
|
||||
{
|
||||
// Close the file handle, move it, and re-open the file in its new location.
|
||||
hLegacyFile.reset();
|
||||
|
||||
// Note: We're unsure if this is unsafe. Theoretically it's possible
|
||||
// that two instances of the app will try and move the settings file
|
||||
// simultaneously. We don't know what might happen in that scenario,
|
||||
// but we're also not sure how to safely lock the file to prevent
|
||||
// that from occurring.
|
||||
THROW_LAST_ERROR_IF(!MoveFile(pathToLegacySettingsFile.c_str(),
|
||||
pathToSettingsFile.c_str()));
|
||||
|
||||
hFile.reset(CreateFileW(pathToSettingsFile.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr));
|
||||
|
||||
// hFile shouldn't be INVALID. That's unexpected - We just moved the
|
||||
// file, we should be able to open it. Throw the error so we can get
|
||||
// some information here.
|
||||
THROW_LAST_ERROR_IF(!hFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the roaming file didn't exist, and the local file doesn't exist,
|
||||
// that's fine. Just log the error and return nullopt - we'll
|
||||
// create the defaults.
|
||||
LOG_LAST_ERROR();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return _ReadFile(hFile.get());
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Reads the content in UTF-8 encoding of the given file using the Win32 APIs
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - an optional with the content of the file if we were able to read it. If we
|
||||
// fail to read it, this can throw an exception from reading the file
|
||||
std::optional<std::string> CascadiaSettings::_ReadFile(HANDLE hFile)
|
||||
{
|
||||
// fileSize is in bytes
|
||||
const auto fileSize = GetFileSize(hFile, nullptr);
|
||||
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
|
||||
|
||||
auto utf8buffer = std::make_unique<char[]>(fileSize);
|
||||
|
||||
DWORD bytesRead = 0;
|
||||
THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr));
|
||||
|
||||
// convert buffer to UTF-8 string
|
||||
std::string utf8string(utf8buffer.get(), fileSize);
|
||||
|
||||
return { utf8string };
|
||||
return ReadUTF8FileIfExists(_SettingsPath());
|
||||
}
|
||||
|
||||
// function Description:
|
||||
|
@ -1191,23 +1075,7 @@ std::optional<std::string> CascadiaSettings::_ReadFile(HANDLE hFile)
|
|||
// - the full path to the settings file
|
||||
winrt::hstring CascadiaSettings::SettingsPath()
|
||||
{
|
||||
wil::unique_cotaskmem_string localAppDataFolder;
|
||||
// KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return
|
||||
// the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests.
|
||||
// Using this flag allows us to avoid Windows.Storage.ApplicationData completely.
|
||||
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_FORCE_APP_DATA_REDIRECTION, nullptr, &localAppDataFolder));
|
||||
|
||||
std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() };
|
||||
|
||||
if (!IsPackaged())
|
||||
{
|
||||
parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName;
|
||||
}
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
std::filesystem::create_directories(parentDirectoryForSettingsFile);
|
||||
|
||||
return winrt::hstring{ (parentDirectoryForSettingsFile / SettingsFilename).wstring() };
|
||||
return winrt::hstring{ _SettingsPath().wstring() };
|
||||
}
|
||||
|
||||
winrt::hstring CascadiaSettings::DefaultSettingsPath()
|
||||
|
@ -1222,15 +1090,12 @@ winrt::hstring CascadiaSettings::DefaultSettingsPath()
|
|||
// directory as the exe, that will work for unpackaged scenarios as well. So
|
||||
// let's try that.
|
||||
|
||||
HMODULE hModule = GetModuleHandle(nullptr);
|
||||
THROW_LAST_ERROR_IF(hModule == nullptr);
|
||||
|
||||
std::wstring exePathString;
|
||||
THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, exePathString));
|
||||
THROW_IF_FAILED(wil::GetModuleFileNameW(nullptr, exePathString));
|
||||
|
||||
const std::filesystem::path exePath{ exePathString };
|
||||
const std::filesystem::path rootDir = exePath.parent_path();
|
||||
return winrt::hstring{ (rootDir / DefaultsFilename).wstring() };
|
||||
std::filesystem::path path{ exePathString };
|
||||
path.replace_filename(DefaultsFilename);
|
||||
return winrt::hstring{ path.wstring() };
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
|
@ -1275,15 +1140,13 @@ const Json::Value& CascadiaSettings::_GetDisabledProfileSourcesJsonObject(const
|
|||
// - <none>
|
||||
void CascadiaSettings::WriteSettingsToDisk() const
|
||||
{
|
||||
const auto settingsPath{ CascadiaSettings::SettingsPath() };
|
||||
const auto settingsPath = _SettingsPath();
|
||||
|
||||
try
|
||||
{
|
||||
// create a timestamped backup file
|
||||
const auto clock{ std::chrono::system_clock() };
|
||||
const auto timeStamp{ clock.to_time_t(clock.now()) };
|
||||
const winrt::hstring backupSettingsPath{ fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath, fmt::localtime(timeStamp)) };
|
||||
_WriteSettings(_userSettingsString, backupSettingsPath);
|
||||
const auto backupSettingsPath = fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath.wstring(), fmt::localtime(std::time(nullptr)));
|
||||
WriteUTF8File(backupSettingsPath, _userSettingsString);
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
|
@ -1293,7 +1156,7 @@ void CascadiaSettings::WriteSettingsToDisk() const
|
|||
wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
|
||||
|
||||
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
|
||||
_WriteSettings(styledString, settingsPath);
|
||||
WriteUTF8FileAtomic(settingsPath, styledString);
|
||||
|
||||
// Persists the default terminal choice
|
||||
//
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
#include <appmodel.h>
|
||||
#include <shlobj.h>
|
||||
#include <WtExeUtils.h>
|
||||
|
||||
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
|
||||
static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" };
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
// Returns a path like C:\Users\<username>\AppData\Local\Packages\<packagename>\LocalState
|
||||
// You can put your settings.json or state.json in this directory.
|
||||
std::filesystem::path GetBaseSettingsPath()
|
||||
{
|
||||
static std::filesystem::path baseSettingsPath = []() {
|
||||
wil::unique_cotaskmem_string localAppDataFolder;
|
||||
// KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return
|
||||
// the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests.
|
||||
// Using this flag allows us to avoid Windows.Storage.ApplicationData completely.
|
||||
THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_FORCE_APP_DATA_REDIRECTION, nullptr, &localAppDataFolder));
|
||||
|
||||
std::filesystem::path parentDirectoryForSettingsFile{ localAppDataFolder.get() };
|
||||
|
||||
if (!IsPackaged())
|
||||
{
|
||||
parentDirectoryForSettingsFile /= UnpackagedSettingsFolderName;
|
||||
}
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
std::filesystem::create_directories(parentDirectoryForSettingsFile);
|
||||
|
||||
return parentDirectoryForSettingsFile;
|
||||
}();
|
||||
return baseSettingsPath;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 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()
|
||||
// -> Lets add a retry-loop just in case, to not fail if the file size changed while reading.
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!file);
|
||||
|
||||
const auto fileSize = GetFileSize(file.get(), nullptr);
|
||||
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
|
||||
|
||||
// By making our buffer just slightly larger we can detect if
|
||||
// the file size changed and we've failed to read the full file.
|
||||
std::string buffer(static_cast<size_t>(fileSize) + 1, '\0');
|
||||
DWORD bytesRead = 0;
|
||||
THROW_IF_WIN32_BOOL_FALSE(ReadFile(file.get(), buffer.data(), gsl::narrow<DWORD>(buffer.size()), &bytesRead, nullptr));
|
||||
|
||||
// This implementation isn't atomic as we'd need to use an exclusive file lock.
|
||||
// But this would be annoying for users as it forces them to close the file in their editor.
|
||||
// The next best alternative is to at least try to detect file changes and retry the read.
|
||||
if (bytesRead != fileSize)
|
||||
{
|
||||
// This continue is unlikely to be hit (see the prior for loop comment).
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
// As mentioned before our buffer was allocated oversized.
|
||||
buffer.resize(bytesRead);
|
||||
|
||||
if (til::starts_with(buffer, Utf8Bom))
|
||||
{
|
||||
// Yeah this memmove()s the entire content.
|
||||
// But I don't really want to deal with UTF8 BOMs any more than necessary,
|
||||
// as basically not a single editor writes a BOM for UTF8.
|
||||
buffer.erase(0, Utf8Bom.size());
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
THROW_WIN32_MSG(ERROR_READ_FAULT, "file size changed while reading");
|
||||
}
|
||||
|
||||
// Same as ReadUTF8File, but returns an empty optional, if the file couldn't be opened.
|
||||
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return { ReadUTF8File(path) };
|
||||
}
|
||||
catch (const wil::ResultException& exception)
|
||||
{
|
||||
if (exception.GetErrorCode() == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteUTF8File(const std::filesystem::path& path, const std::string_view content)
|
||||
{
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
THROW_LAST_ERROR_IF(!file);
|
||||
|
||||
const auto fileSize = gsl::narrow<DWORD>(content.size());
|
||||
DWORD bytesWritten = 0;
|
||||
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr));
|
||||
|
||||
if (bytesWritten != fileSize)
|
||||
{
|
||||
THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file");
|
||||
}
|
||||
}
|
||||
|
||||
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content)
|
||||
{
|
||||
auto tmpPath = path;
|
||||
tmpPath += L".tmp";
|
||||
|
||||
// Writing to a file isn't atomic, but...
|
||||
WriteUTF8File(tmpPath, content);
|
||||
|
||||
// renaming one is (supposed to be) atomic.
|
||||
// Wait... "supposed to be"!? Well it's technically not always atomic,
|
||||
// but it's pretty darn close to it, so... better than nothing.
|
||||
std::filesystem::rename(tmpPath, path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
std::filesystem::path GetBaseSettingsPath();
|
||||
std::string ReadUTF8File(const std::filesystem::path& path);
|
||||
std::optional<std::string> ReadUTF8FileIfExists(const std::filesystem::path& path);
|
||||
void WriteUTF8File(const std::filesystem::path& path, const std::string_view content);
|
||||
void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content);
|
||||
}
|
|
@ -32,6 +32,9 @@
|
|||
<DependentUpon>ActionMap.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="AzureCloudShellGenerator.h" />
|
||||
<ClInclude Include="ApplicationState.h">
|
||||
<DependentUpon>ApplicationState.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="CascadiaSettings.h">
|
||||
<DependentUpon>CascadiaSettings.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -42,6 +45,7 @@
|
|||
<DependentUpon>Command.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="DefaultProfileUtils.h" />
|
||||
<ClInclude Include="FileUtils.h" />
|
||||
<ClInclude Include="GlobalAppSettings.h">
|
||||
<DependentUpon>GlobalAppSettings.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -97,6 +101,9 @@
|
|||
<DependentUpon>ActionMap.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="AzureCloudShellGenerator.cpp" />
|
||||
<ClCompile Include="ApplicationState.cpp">
|
||||
<DependentUpon>ApplicationState.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="CascadiaSettings.cpp">
|
||||
<DependentUpon>CascadiaSettings.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -110,6 +117,7 @@
|
|||
<DependentUpon>Command.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DefaultProfileUtils.cpp" />
|
||||
<ClCompile Include="FileUtils.cpp" />
|
||||
<ClCompile Include="GlobalAppSettings.cpp">
|
||||
<DependentUpon>GlobalAppSettings.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -141,6 +149,7 @@
|
|||
<ItemGroup>
|
||||
<Midl Include="ActionArgs.idl" />
|
||||
<Midl Include="ActionMap.idl" />
|
||||
<Midl Include="ApplicationState.idl" />
|
||||
<Midl Include="CascadiaSettings.idl" />
|
||||
<Midl Include="ColorScheme.idl" />
|
||||
<Midl Include="Command.idl" />
|
||||
|
|
Loading…
Reference in New Issue