terminal/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp

406 lines
14 KiB
C++
Raw Normal View History

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include <argb.h>
#include "CascadiaSettings.h"
#include "../../types/inc/utils.hpp"
#include <appmodel.h>
#include <wil/com.h>
#include <wil/filesystem.h>
#include <shlobj.h>
using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Data::Json;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Streams;
using namespace ::Microsoft::Console;
static constexpr std::wstring_view FILENAME { L"profiles.json" };
static constexpr std::wstring_view SETTINGS_FOLDER_NAME{ L"\\Microsoft\\Windows Terminal\\" };
static constexpr std::wstring_view PROFILES_KEY{ L"profiles" };
static constexpr std::wstring_view KEYBINDINGS_KEY{ L"keybindings" };
static constexpr std::wstring_view SCHEMES_KEY{ L"schemes" };
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
// a new one with the default values. If we're running as a packaged app,
// it will load the settings from our packaged localappdata. If we're
// running as an unpackaged application, it will read it from the path
// we've set under localappdata.
// Arguments:
// - saveOnLoad: If true, we'll write the settings back out after we load them,
// to make sure the schema is updated.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadAll(const bool saveOnLoad)
{
std::unique_ptr<CascadiaSettings> resultPtr;
std::optional<winrt::hstring> fileData = _IsPackaged() ?
_LoadAsPackagedApp() : _LoadAsUnpackagedApp();
const bool foundFile = fileData.has_value();
if (foundFile)
{
const auto actualData = fileData.value();
JsonValue root{ nullptr };
bool parsedSuccessfully = JsonValue::TryParse(actualData, root);
// TODO:MSFT:20737698 - Display an error if we failed to parse settings
if (parsedSuccessfully)
{
JsonObject obj = root.GetObjectW();
resultPtr = FromJson(obj);
// Update profile only if it has changed.
if (saveOnLoad)
{
const JsonObject json = resultPtr->ToJson();
auto serializedSettings = json.Stringify();
if (actualData != serializedSettings)
{
resultPtr->SaveAll();
}
}
}
else
{
// Until 20737698 is done, throw an error, so debugging can trace
// the exception here, instead of later on in unrelated code
THROW_HR(E_INVALIDARG);
}
}
else
{
resultPtr = std::make_unique<CascadiaSettings>();
resultPtr->_CreateDefaults();
// The settings file does not exist. Let's commit one.
resultPtr->SaveAll();
}
return resultPtr;
}
// Method Description:
// - Serialize this settings structure, and save it to a file. The location of
// the file changes depending whether we're running as a packaged
// application or not.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::SaveAll() const
{
const JsonObject json = ToJson();
auto serializedSettings = json.Stringify();
if (_IsPackaged())
{
_SaveAsPackagedApp(serializedSettings);
}
else
{
_SaveAsUnpackagedApp(serializedSettings);
}
}
// Method Description:
// - Serialize this object to a JsonObject.
// Arguments:
// - <none>
// Return Value:
// - a JsonObject which is an equivalent serialization of this object.
JsonObject CascadiaSettings::ToJson() const
{
// _globals.ToJson will initialize the settings object will all the global
// settings in the root of the object.
winrt::Windows::Data::Json::JsonObject jsonObject = _globals.ToJson();
JsonArray schemesArray{};
const auto& colorSchemes = _globals.GetColorSchemes();
for (auto& scheme : colorSchemes)
{
schemesArray.Append(scheme.ToJson());
}
JsonArray profilesArray{};
for (auto& profile : _profiles)
{
profilesArray.Append(profile.ToJson());
}
jsonObject.Insert(PROFILES_KEY, profilesArray);
jsonObject.Insert(SCHEMES_KEY, schemesArray);
jsonObject.Insert(KEYBINDINGS_KEY,
_globals.GetKeybindings().ToJson());
return jsonObject;
}
// Method Description:
// - Create a new instance of this class from a serialized JsonObject.
// Arguments:
// - json: an object which should be a serialization of a CascadiaSettings object.
// Return Value:
// - a new CascadiaSettings instance created from the values in `json`
std::unique_ptr<CascadiaSettings> CascadiaSettings::FromJson(JsonObject json)
{
std::unique_ptr<CascadiaSettings> resultPtr = std::make_unique<CascadiaSettings>();
resultPtr->_globals = GlobalAppSettings::FromJson(json);
// TODO:MSFT:20737698 - Display an error if we failed to parse settings
// What should we do here if these keys aren't found?For default profile,
// we could always pick the first profile and just set that as the default.
// Finding no schemes is probably fine, unless of course one profile
// references a scheme. We could fail with come error saying the
// profiles file is corrupted.
// Not having any profiles is also bad - should we say the file is corrupted?
// Or should we just recreate the default profiles?
auto& resultSchemes = resultPtr->_globals.GetColorSchemes();
if (json.HasKey(SCHEMES_KEY))
{
auto schemes = json.GetNamedArray(SCHEMES_KEY);
for (auto schemeJson : schemes)
{
if (schemeJson.ValueType() == JsonValueType::Object)
{
auto schemeObj = schemeJson.GetObjectW();
auto scheme = ColorScheme::FromJson(schemeObj);
resultSchemes.emplace_back(std::move(scheme));
}
}
}
if (json.HasKey(PROFILES_KEY))
{
auto profiles = json.GetNamedArray(PROFILES_KEY);
for (auto profileJson : profiles)
{
if (profileJson.ValueType() == JsonValueType::Object)
{
auto profileObj = profileJson.GetObjectW();
auto profile = Profile::FromJson(profileObj);
resultPtr->_profiles.emplace_back(std::move(profile));
}
}
}
// Load the keybindings from the file as well
if (json.HasKey(KEYBINDINGS_KEY))
{
const auto keybindingsObj = json.GetNamedArray(KEYBINDINGS_KEY);
auto loadedBindings = AppKeyBindings::FromJson(keybindingsObj);
resultPtr->_globals.SetKeybindings(loadedBindings);
}
else
{
// Create the default keybindings if we couldn't find any keybindings.
resultPtr->_CreateDefaultKeybindings();
}
return resultPtr;
}
// Function Description:
// - Returns true if we're running in a packaged context. If we are, then we
// have to use the Windows.Storage API's to save/load our files. If we're
// not, then we won't be able to use those API's.
// Arguments:
// - <none>
// Return Value:
// - true iff we're running in a packaged context.
bool CascadiaSettings::_IsPackaged()
{
UINT32 length = 0;
LONG rc = GetCurrentPackageFullName(&length, NULL);
return rc != APPMODEL_ERROR_NO_PACKAGE;
}
// Method Description:
// - Writes the given content to our settings file as UTF-8 encoded using the Windows.Storage
// APIS's. This will only work within the context of an application with
// package identity, so make sure to call _IsPackaged before calling this method.
// Will overwrite any existing content in the file.
// Arguments:
// - content: the given string of content to write to the file.
// Return Value:
// - <none>
void CascadiaSettings::_SaveAsPackagedApp(const winrt::hstring content)
{
auto curr = ApplicationData::Current();
auto folder = curr.RoamingFolder();
auto file_async = folder.CreateFileAsync(FILENAME,
CreationCollisionOption::ReplaceExisting);
auto file = file_async.get();
DataWriter dw = DataWriter();
// DataWriter will convert UTF-16 string to UTF-8 (expected settings file encoding)
dw.UnicodeEncoding(UnicodeEncoding::Utf8);
dw.WriteString(content);
FileIO::WriteBufferAsync(file, dw.DetachBuffer()).get();
}
// Method Description:
// - Writes the given content in UTF-8 to our settings file using the Win32 APIS's.
// Will overwrite any existing content in the 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::_SaveAsUnpackagedApp(const winrt::hstring content)
{
// convert UTF-16 to UTF-8
auto contentString = winrt::to_string(content);
// Get path to output file
// In this scenario, the settings file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json
std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile();
auto hOut = CreateFileW(pathToSettingsFile.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOut == INVALID_HANDLE_VALUE)
{
THROW_LAST_ERROR();
}
THROW_LAST_ERROR_IF(!WriteFile(hOut, contentString.c_str(), gsl::narrow<DWORD>(contentString.length()), 0, 0));
CloseHandle(hOut);
}
// Method Description:
// - Computes the path to the settings file if the app is run unpackaged.
// Will create any intermediate directories if they don't exist.
// The file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json
// Arguments:
// - <none>
// Return Value:
// - A string containing the path to the unpackaged settings file
// This can throw an exception if it fails to get the roaming app data folder.
std::wstring CascadiaSettings::_GetFullPathToUnpackagedSettingsFile()
{
wil::unique_cotaskmem_string roamingAppDataFolder;
if (FAILED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, 0, &roamingAppDataFolder)))
{
THROW_LAST_ERROR();
}
std::wstring parentDirectoryForSettingsFile(roamingAppDataFolder.get());
parentDirectoryForSettingsFile.append(SETTINGS_FOLDER_NAME);
// Create the directory if it doesn't exist
wil::CreateDirectoryDeep(parentDirectoryForSettingsFile.c_str());
std::wstring pathToSettingsFile(parentDirectoryForSettingsFile);
pathToSettingsFile.append(FILENAME);
return pathToSettingsFile;
}
// Method Description:
// - Reads the content of our settings file using the Windows.Storage
// APIS's. This will only work within the context of an application with
// package identity, so make sure to call _IsPackaged before calling this method.
// Arguments:
// - <none>
// Return Value:
// - an optional with the content of the file if we were able to open it,
// otherwise the optional will be empty
std::optional<winrt::hstring> CascadiaSettings::_LoadAsPackagedApp()
{
auto curr = ApplicationData::Current();
auto folder = curr.RoamingFolder();
auto file_async = folder.TryGetItemAsync(FILENAME);
auto file = file_async.get();
if (file == nullptr)
{
return std::nullopt;
}
const auto storageFile = file.as<StorageFile>();
// settings file is UTF-8 without BOM
return { FileIO::ReadTextAsync(storageFile, UnicodeEncoding::Utf8).get() };
}
// Method Description:
// - Reads the content in UTF-8 enconding of our settings file using the Win32 APIs
// Arguments:
// - <none>
// Return Value:
// - an optional with the content of the file if we were able to open it,
// otherwise the optional will be empty.
// If the file exists, but we fail to read it, this can throw an exception
// from reading the file
std::optional<winrt::hstring> CascadiaSettings::_LoadAsUnpackagedApp()
{
std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile();
const auto hFile = CreateFileW(pathToSettingsFile.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// If the 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;
}
// 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));
CloseHandle(hFile);
// convert buffer to UTF-8 string
std::string utf8string(utf8buffer.get(), fileSize);
// UTF-8 to UTF-16
const winrt::hstring fileData = winrt::to_hstring(utf8string);
return { fileData };
}
// function Description:
// - Returns the full path to the settings file, either within the application
2019-05-21 08:15:44 +02:00
// package, or in its unpackaged location.
// Arguments:
// - <none>
// Return Value:
// - the full path to the settings file
winrt::hstring CascadiaSettings::GetSettingsPath()
{
return _IsPackaged() ? CascadiaSettings::_GetPackagedSettingsPath() :
winrt::hstring{ CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() };
}
// Function Description:
2019-05-21 08:15:44 +02:00
// - Get the full path to settings file in its packaged location.
// Arguments:
// - <none>
// Return Value:
// - the full path to the packaged settings file.
winrt::hstring CascadiaSettings::_GetPackagedSettingsPath()
{
const auto curr = ApplicationData::Current();
const auto folder = curr.RoamingFolder();
const auto file_async = folder.TryGetItemAsync(FILENAME);
const auto file = file_async.get();
return file.Path();
}