Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a basic ApplicationState class #10513

Merged
merged 4 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,7 @@ MSVCRTD
MSVS
msys
msysgit
MTSM
mui
Mul
multiline
Expand Down Expand Up @@ -2416,7 +2417,6 @@ uapadmin
UAX
ubuntu
ucd
ucd
ucdxml
uch
UCHAR
Expand Down Expand Up @@ -2780,7 +2780,6 @@ xml
xmlns
xor
xorg
xorg
Xpath
XPosition
XResource
Expand Down
14 changes: 7 additions & 7 deletions doc/cascadia/AddASetting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
85 changes: 30 additions & 55 deletions src/cascadia/TerminalApp/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,7 @@ namespace winrt::TerminalApp::implementation
}

AppLogic::AppLogic() :
_dialogLock{},
_loadedInitialSettings{ false },
_settingsLoadedResult{ S_OK }
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the existing settings-reload throttler, this does the same but for state.json.
Since til::throttled_func cannot be copied or moved, this type must be initialized in the member list initializer (instead of more ergonomically in the constructor function body).

{
// 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,
Expand All @@ -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();
}
});
Comment on lines +206 to +211
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the previous, manual settings.json load delay & throttling we had before.

}

// Method Description:
Expand Down Expand Up @@ -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();

_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;
}

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);
}
const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } };

_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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We previously listened for wil::FolderChangeEvents::All and then filtered the events, but this isn't necessary...

  • FileName maps to FILE_NOTIFY_CHANGE_FILE_NAME and includes renaming, creating, or deleting a file name.
  • LastWriteTime maps to FILE_NOTIFY_CHANGE_LAST_WRITE and includes any writes that are flushed to disk.

[this, settingsBasename = settingsPath.filename(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) {
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename();

if (modifiedBasename == settingsBasename)
{
_reloadSettings->Run();
}
else if (modifiedBasename == stateBasename)
{
_reloadState();
}
});
}

void AppLogic::_ApplyLanguageSettingChange() noexcept
Expand Down
16 changes: 8 additions & 8 deletions src/cascadia/TerminalApp/AppLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
173 changes: 173 additions & 0 deletions src/cascadia/TerminalSettingsModel/ApplicationState.cpp
Original file line number Diff line number Diff line change
@@ -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()
}
Loading