diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 6c1e5950db8..83d061dbaba 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -491,12 +491,14 @@ namespace TerminalAppLocalTests L"default profiles in the opposite order of the default ordering.")); CascadiaSettings settings; - settings._LayerJsonString(defaultProfilesString, true); + settings._ParseJsonString(defaultProfilesString, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - settings._LayerJsonString(userProfiles0String, false); + settings._ParseJsonString(userProfiles0String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); @@ -512,12 +514,14 @@ namespace TerminalAppLocalTests L"Case 2: Make sure all the user's profiles appear before the defaults.")); CascadiaSettings settings; - settings._LayerJsonString(defaultProfilesString, true); + settings._ParseJsonString(defaultProfilesString, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - settings._LayerJsonString(userProfiles1String, false); + settings._ParseJsonString(userProfiles1String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(3u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); @@ -588,14 +592,16 @@ namespace TerminalAppLocalTests { CascadiaSettings settings; - settings._LayerJsonString(defaultProfilesString, true); + settings._ParseJsonString(defaultProfilesString, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); - settings._LayerJsonString(userProfiles0String, false); + settings._ParseJsonString(userProfiles0String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); @@ -611,14 +617,16 @@ namespace TerminalAppLocalTests { CascadiaSettings settings; - settings._LayerJsonString(defaultProfilesString, true); + settings._ParseJsonString(defaultProfilesString, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); - settings._LayerJsonString(userProfiles1String, false); + settings._ParseJsonString(userProfiles1String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(4u, settings._profiles.size()); VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); @@ -788,7 +796,8 @@ namespace TerminalAppLocalTests const auto settings0Json = VerifyParseSucceeded(settings0String); CascadiaSettings settings; - settings._LayerJsonString(settings0String, false); + settings._ParseJsonString(settings0String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); @@ -824,14 +833,16 @@ namespace TerminalAppLocalTests const auto settings0Json = VerifyParseSucceeded(settings0String); CascadiaSettings settings; - settings._LayerJsonString(DefaultJson, true); + settings._ParseJsonString(DefaultJson, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(1)._name); - settings._LayerJsonString(settings0String, false); + settings._ParseJsonString(settings0String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(4u, settings._profiles.size()); VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); @@ -923,14 +934,16 @@ namespace TerminalAppLocalTests const auto settings0Json = VerifyParseSucceeded(settings0String); CascadiaSettings settings; - settings._LayerJsonString(DefaultJson, true); + settings._ParseJsonString(DefaultJson, true); + settings.LayerJson(settings._defaultSettings); VERIFY_ARE_EQUAL(2u, settings._profiles.size()); VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(1)._name); - settings._LayerJsonString(settings0String, false); + settings._ParseJsonString(settings0String, false); + settings.LayerJson(settings._userSettings); VERIFY_ARE_EQUAL(4u, settings._profiles.size()); VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); diff --git a/src/cascadia/TerminalApp/AzureCloudShellGenerator.cpp b/src/cascadia/TerminalApp/AzureCloudShellGenerator.cpp new file mode 100644 index 00000000000..0414322ba06 --- /dev/null +++ b/src/cascadia/TerminalApp/AzureCloudShellGenerator.cpp @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include + +#include "AzureCloudShellGenerator.h" +#include "LegacyProfileGeneratorNamespaces.h" + +#include "../../types/inc/utils.hpp" +#include "../../inc/DefaultSettings.h" +#include "Utils.h" +#include "DefaultProfileUtils.h" + +using namespace ::TerminalApp; + +std::wstring_view AzureCloudShellGenerator::GetNamespace() +{ + return AzureGeneratorNamespace; +} + +// Method Description: +// - Checks if the Azure Cloud shell is available on this platform, and if it +// is, creates a profile to be able to launch it. +// Arguments: +// - +// Return Value: +// - a vector with the Azure Cloud Shell connection profile, if available. +std::vector AzureCloudShellGenerator::GenerateProfiles() +{ + std::vector profiles; + + if (winrt::Microsoft::Terminal::TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) + { + auto azureCloudShellProfile{ CreateDefaultProfile(L"Azure Cloud Shell") }; + azureCloudShellProfile.SetCommandline(L"Azure"); + azureCloudShellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); + azureCloudShellProfile.SetColorScheme({ L"Vintage" }); + azureCloudShellProfile.SetAcrylicOpacity(0.6); + azureCloudShellProfile.SetUseAcrylic(true); + azureCloudShellProfile.SetCloseOnExit(false); + azureCloudShellProfile.SetConnectionType(AzureConnectionType); + profiles.emplace_back(azureCloudShellProfile); + } + + return profiles; +} diff --git a/src/cascadia/TerminalApp/AzureCloudShellGenerator.h b/src/cascadia/TerminalApp/AzureCloudShellGenerator.h new file mode 100644 index 00000000000..3e752276d40 --- /dev/null +++ b/src/cascadia/TerminalApp/AzureCloudShellGenerator.h @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- AzureCloudShellGenerator + +Abstract: +- This is the dynamic profile generator for the azure cloud shell connector. + Checks if the Azure Cloud shell is available on this platform, and if it is, + creates a profile to be able to launch it. + +Author(s): +- Mike Griese - August 2019 + +--*/ + +#pragma once +#include "IDynamicProfileGenerator.h" + +static constexpr GUID AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } }; + +namespace TerminalApp +{ + class AzureCloudShellGenerator : public TerminalApp::IDynamicProfileGenerator + { + public: + AzureCloudShellGenerator() = default; + ~AzureCloudShellGenerator() = default; + std::wstring_view GetNamespace() override; + + std::vector GenerateProfiles() override; + }; +}; diff --git a/src/cascadia/TerminalApp/CascadiaSettings.cpp b/src/cascadia/TerminalApp/CascadiaSettings.cpp index 3d8a786f2f1..a9652f0e85b 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettings.cpp @@ -11,107 +11,40 @@ #include "../../inc/DefaultSettings.h" #include "Utils.h" +#include "PowershellCoreProfileGenerator.h" +#include "WslDistroGenerator.h" +#include "AzureCloudShellGenerator.h" + using namespace winrt::Microsoft::Terminal::Settings; using namespace ::TerminalApp; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::TerminalApp; using namespace Microsoft::Console; -// {2bde4a90-d05f-401c-9492-e40884ead1d8} -// uuidv5 properties: name format is UTF-16LE bytes -static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; - static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" }; static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" }; static constexpr std::wstring_view DEFAULT_LINUX_ICON_GUID{ L"{9acb9455-ca41-5af7-950f-6bca1bc9722f}" }; CascadiaSettings::CascadiaSettings() : - _globals{}, - _profiles{} -{ -} - -CascadiaSettings::~CascadiaSettings() + CascadiaSettings(true) { } -// Method Description: -// - Create a set of profiles to use as the "default" profiles when initializing -// the terminal. Currently, we create two or three profiles: -// * one for cmd.exe -// * one for powershell.exe (inbox Windows Powershell) -// * if Powershell Core (pwsh.exe) is installed, we'll create another for -// Powershell Core. -void CascadiaSettings::_CreateDefaultProfiles() +// Constructor Description: +// - Creates a new settings object. If addDynamicProfiles is true, we'll +// automatically add the built-in profile generators to our list of profile +// generators. Set this to `false` for unit testing. +// Arguments: +// - addDynamicProfiles: if true, we'll add the built-in DPGs. +CascadiaSettings::CascadiaSettings(const bool addDynamicProfiles) { - auto cmdProfile{ _CreateDefaultProfile(L"cmd") }; - cmdProfile.SetFontFace(L"Consolas"); - cmdProfile.SetCommandline(L"cmd.exe"); - cmdProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - cmdProfile.SetColorScheme({ L"Campbell" }); - cmdProfile.SetAcrylicOpacity(0.75); - cmdProfile.SetUseAcrylic(true); - - auto powershellProfile{ _CreateDefaultProfile(L"Windows PowerShell") }; - powershellProfile.SetCommandline(L"powershell.exe"); - powershellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - powershellProfile.SetColorScheme({ L"Campbell" }); - powershellProfile.SetDefaultBackground(POWERSHELL_BLUE); - powershellProfile.SetUseAcrylic(false); - - // If the user has installed PowerShell Core, we add PowerShell Core as a default. - // PowerShell Core default folder is "%PROGRAMFILES%\PowerShell\[Version]\". - std::filesystem::path psCoreCmdline{}; - if (_isPowerShellCoreInstalled(psCoreCmdline)) - { - auto pwshProfile{ _CreateDefaultProfile(L"PowerShell Core") }; - pwshProfile.SetCommandline(std::move(psCoreCmdline)); - pwshProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - pwshProfile.SetColorScheme({ L"Campbell" }); - - // If powershell core is installed, we'll use that as the default. - // Otherwise, we'll use normal Windows Powershell as the default. - _profiles.emplace_back(pwshProfile); - _globals.SetDefaultProfile(pwshProfile.GetGuid()); - } - else + if (addDynamicProfiles) { - _globals.SetDefaultProfile(powershellProfile.GetGuid()); + _profileGenerators.emplace_back(std::make_unique()); + _profileGenerators.emplace_back(std::make_unique()); + _profileGenerators.emplace_back(std::make_unique()); } - - _profiles.emplace_back(powershellProfile); - _profiles.emplace_back(cmdProfile); - - if (winrt::Microsoft::Terminal::TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) - { - auto azureCloudShellProfile{ _CreateDefaultProfile(L"Azure Cloud Shell") }; - azureCloudShellProfile.SetCommandline(L"Azure"); - azureCloudShellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - azureCloudShellProfile.SetColorScheme({ L"Vintage" }); - azureCloudShellProfile.SetAcrylicOpacity(0.6); - azureCloudShellProfile.SetUseAcrylic(true); - azureCloudShellProfile.SetCloseOnExit(false); - azureCloudShellProfile.SetConnectionType(AzureConnectionType); - _profiles.emplace_back(azureCloudShellProfile); - } - - try - { - _AppendWslProfiles(_profiles); - } - CATCH_LOG() -} - -// Method Description: -// - Initialize this object with default color schemes, profiles, and keybindings. -// Arguments: -// - -// Return Value: -// - -void CascadiaSettings::CreateDefaults() -{ - _CreateDefaultProfiles(); } // Method Description: @@ -196,160 +129,6 @@ GlobalAppSettings& CascadiaSettings::GlobalSettings() return _globals; } -// Function Description: -// - Returns true if the user has installed PowerShell Core. This will check -// both %ProgramFiles% and %ProgramFiles(x86)%, and will return true if -// powershell core was installed in either location. -// Arguments: -// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. -// Return Value: -// - true iff powershell core (pwsh.exe) is present. -bool CascadiaSettings::_isPowerShellCoreInstalled(std::filesystem::path& cmdline) -{ - return _isPowerShellCoreInstalledInPath(L"%ProgramFiles%", cmdline) || - _isPowerShellCoreInstalledInPath(L"%ProgramFiles(x86)%", cmdline); -} - -// Function Description: -// - Returns true if the user has installed PowerShell Core. -// Arguments: -// - A string that contains an environment-variable string in the form: %variableName%. -// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. -// Return Value: -// - true iff powershell core (pwsh.exe) is present in the given path -bool CascadiaSettings::_isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline) -{ - std::wstring programFileEnvNulTerm{ programFileEnv }; - std::filesystem::path psCorePath{ wil::ExpandEnvironmentStringsW(programFileEnvNulTerm.data()) }; - psCorePath /= L"PowerShell"; - if (std::filesystem::exists(psCorePath)) - { - for (auto& p : std::filesystem::directory_iterator(psCorePath)) - { - psCorePath = p.path(); - psCorePath /= L"pwsh.exe"; - if (std::filesystem::exists(psCorePath)) - { - cmdline = psCorePath; - return true; - } - } - } - return false; -} - -// Function Description: -// - Adds all of the WSL profiles to the provided container. -// Arguments: -// - A ref to the profiles container where the WSL profiles are to be added -// Return Value: -// - -void CascadiaSettings::_AppendWslProfiles(std::vector& profileStorage) -{ - wil::unique_handle readPipe; - wil::unique_handle writePipe; - SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, true }; - THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, &sa, 0)); - STARTUPINFO si{}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; - si.hStdOutput = writePipe.get(); - si.hStdError = writePipe.get(); - wil::unique_process_information pi{}; - wil::unique_cotaskmem_string systemPath; - THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath)); - std::wstring command(systemPath.get()); - command += L"\\wsl.exe --list"; - - THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, - const_cast(command.c_str()), - nullptr, - nullptr, - TRUE, - CREATE_NO_WINDOW, - nullptr, - nullptr, - &si, - &pi)); - switch (WaitForSingleObject(pi.hProcess, INFINITE)) - { - case WAIT_OBJECT_0: - break; - case WAIT_ABANDONED: - case WAIT_TIMEOUT: - THROW_HR(ERROR_CHILD_NOT_COMPLETE); - case WAIT_FAILED: - THROW_LAST_ERROR(); - default: - THROW_HR(ERROR_UNHANDLED_EXCEPTION); - } - DWORD exitCode; - if (GetExitCodeProcess(pi.hProcess, &exitCode) == false) - { - THROW_HR(E_INVALIDARG); - } - else if (exitCode != 0) - { - return; - } - DWORD bytesAvailable; - THROW_IF_WIN32_BOOL_FALSE(PeekNamedPipe(readPipe.get(), nullptr, NULL, nullptr, &bytesAvailable, nullptr)); - std::wfstream pipe{ _wfdopen(_open_osfhandle((intptr_t)readPipe.get(), _O_WTEXT | _O_RDONLY), L"r") }; - // don't worry about the handle returned from wfdOpen, readPipe handle is already managed by wil - // and closing the file handle will cause an error. - std::wstring wline; - std::getline(pipe, wline); // remove the header from the output. - while (pipe.tellp() < bytesAvailable) - { - std::getline(pipe, wline); - std::wstringstream wlinestream(wline); - if (wlinestream) - { - std::wstring distName; - std::getline(wlinestream, distName, L'\r'); - size_t firstChar = distName.find_first_of(L"( "); - // Some localizations don't have a space between the name and "(Default)" - // https://github.com/microsoft/terminal/issues/1168#issuecomment-500187109 - if (firstChar < distName.size()) - { - distName.resize(firstChar); - } - auto WSLDistro{ _CreateDefaultProfile(distName) }; - WSLDistro.SetCommandline(L"wsl.exe -d " + distName); - WSLDistro.SetColorScheme({ L"Campbell" }); - WSLDistro.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH }; - iconPath.append(DEFAULT_LINUX_ICON_GUID); - iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION); - WSLDistro.SetIconPath(iconPath); - profileStorage.emplace_back(WSLDistro); - } - } -} - -// Method Description: -// - Helper function for creating a skeleton default profile with a pre-populated -// guid and name. -// Arguments: -// - name: the name of the new profile. -// Return Value: -// - A Profile, ready to be filled in -Profile CascadiaSettings::_CreateDefaultProfile(const std::wstring_view name) -{ - auto profileGuid{ Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))) }; - Profile newProfile{ profileGuid }; - - newProfile.SetName(static_cast(name)); - - std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH }; - iconPath.append(Utils::GuidToString(profileGuid)); - iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION); - - newProfile.SetIconPath(iconPath); - - return newProfile; -} - // Method Description: // - Gets our list of warnings we found during loading. These are things that we // knew were bad when we called `_ValidateSettings` last. diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index 746e9563388..dd8f3581ad9 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -20,8 +20,7 @@ Author(s): #include "GlobalAppSettings.h" #include "TerminalWarnings.h" #include "Profile.h" - -static constexpr GUID AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } }; +#include "IDynamicProfileGenerator.h" // fwdecl unittest classes namespace TerminalAppLocalTests @@ -30,7 +29,11 @@ namespace TerminalAppLocalTests class ProfileTests; class ColorSchemeTests; class KeyBindingsTests; -} +}; +namespace TerminalAppUnitTests +{ + class DynamicProfileTests; +}; namespace TerminalApp { @@ -41,7 +44,7 @@ class TerminalApp::CascadiaSettings final { public: CascadiaSettings(); - ~CascadiaSettings(); + CascadiaSettings(const bool addDynamicProfiles); static std::unique_ptr LoadDefaults(); static std::unique_ptr LoadAll(); @@ -64,8 +67,6 @@ class TerminalApp::CascadiaSettings final const Profile* FindProfile(GUID profileGuid) const noexcept; - void CreateDefaults(); - std::vector& GetWarnings(); private: @@ -73,17 +74,22 @@ class TerminalApp::CascadiaSettings final std::vector _profiles; std::vector _warnings; + std::vector> _profileGenerators; + + std::string _userSettingsString; Json::Value _userSettings; Json::Value _defaultSettings; - void _CreateDefaultProfiles(); - void _LayerOrCreateProfile(const Json::Value& profileJson); Profile* _FindMatchingProfile(const Json::Value& profileJson); void _LayerOrCreateColorScheme(const Json::Value& schemeJson); ColorScheme* _FindMatchingColorScheme(const Json::Value& schemeJson); - void _LayerJsonString(std::string_view fileData, const bool isDefaultSettings); + void _ParseJsonString(std::string_view fileData, const bool isDefaultSettings); static const Json::Value& _GetProfilesJsonObject(const Json::Value& json); + static const Json::Value& _GetDisabledProfileSourcesJsonObject(const Json::Value& json); + bool _AppendDynamicProfilesToUserSettings(); + + void _LoadDynamicProfiles(); static bool _IsPackaged(); static void _WriteSettings(const std::string_view content); @@ -98,13 +104,9 @@ class TerminalApp::CascadiaSettings final void _ReorderProfilesToMatchUserSettingsOrder(); void _RemoveHiddenProfiles(); - static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline); - static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline); - static void _AppendWslProfiles(std::vector& profileStorage); - static Profile _CreateDefaultProfile(const std::wstring_view name); - friend class TerminalAppLocalTests::SettingsTests; friend class TerminalAppLocalTests::ProfileTests; friend class TerminalAppLocalTests::ColorSchemeTests; friend class TerminalAppLocalTests::KeyBindingsTests; + friend class TerminalAppUnitTests::DynamicProfileTests; }; diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp index 3bf4c31dcb9..7c593ee3aa9 100644 --- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -32,7 +32,10 @@ static constexpr std::string_view KeybindingsKey{ "keybindings" }; static constexpr std::string_view GlobalsKey{ "globals" }; static constexpr std::string_view SchemesKey{ "schemes" }; +static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" }; + static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; +static constexpr std::string_view DefaultProfilesIndentation{ " " }; // Method Description: // - Creates a CascadiaSettings from whatever's saved on disk, or instantiates @@ -41,6 +44,9 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; // running as an unpackaged application, it will read it from the path // we've set under localappdata. // - Loads both the settings from the defaults.json and the user's profiles.json +// - Also runs and dynamic profile generators. If any of those generators create +// new profiles, we'll write the user settings back to the file, with the new +// profiles inserted into their list of profiles. // Return Value: // - a unique_ptr containing a new CascadiaSettings object. std::unique_ptr CascadiaSettings::LoadAll() @@ -49,18 +55,51 @@ std::unique_ptr CascadiaSettings::LoadAll() std::optional fileData = _ReadUserSettings(); const bool foundFile = fileData.has_value(); + // Make sure the file isn't totally empty. If it is, we'll treat the file // like it doesn't exist at all. const bool fileHasData = foundFile && !fileData.value().empty(); + bool needToWriteFile = false; if (fileHasData) { - resultPtr->_LayerJsonString(fileData.value(), false); + resultPtr->_ParseJsonString(fileData.value(), false); } else { // We didn't find the user settings. We'll need to create a file // to use as the user defaults. - _WriteSettings(UserSettingsJson); + // For now, just parse our user settings template as their user settings. + resultPtr->_ParseJsonString(UserSettingsJson, false); + needToWriteFile = true; + } + + // Load profiles from dynamic profile generators. _userSettings should be + // created by now, because we're going to check in there for any generators + // that should be disabled. + resultPtr->_LoadDynamicProfiles(); + + // Apply the user's settings + resultPtr->LayerJson(resultPtr->_userSettings); + + // After layering the user settings, check if there are any new profiles + // that need to be inserted into their user settings file. + needToWriteFile = resultPtr->_AppendDynamicProfilesToUserSettings() || needToWriteFile; + + // TODO:GH#2721 If powershell core is installed, we need to set that to the + // default profile, but only when the settings file was newly created. We'll + // re-write the segment of the user settings for "default profile" to have + // the powershell core GUID instead. + + // If we created the file, or found new dynamic profiles, write the user + // settings string back to the file. + if (needToWriteFile) + { + // If AppendDynamicProfilesToUserSettings (or the pwsh check above) + // changed the file, then our local settings JSON is no longer accurate. + // We should re-parse, but not re-layer + resultPtr->_ParseJsonString(resultPtr->_userSettingsString, false); + + _WriteSettings(resultPtr->_userSettingsString); } // If this throws, the app will catch it and use the default settings @@ -83,18 +122,66 @@ std::unique_ptr CascadiaSettings::LoadDefaults() // We already have the defaults in memory, because we stamp them into a // header as part of the build process. We don't need to bother with reading // them from a file (and the potential that could fail) - resultPtr->_LayerJsonString(DefaultJson, true); + resultPtr->_ParseJsonString(DefaultJson, true); + resultPtr->LayerJson(resultPtr->_defaultSettings); return resultPtr; } // Method Description: -// - Attempts to read the given data as a string of JSON, parse that JSON, and -// then layer the settings from that JSON object on our current settings. See -// CascadiaSettings::LayerJson for detauls on how the layering works. +// - Runs each of the configured dynamic profile generators (DPGs). Adds +// profiles from any DPGs that ran to the end of our list of profiles. +// - Uses the Json::Value _userSettings to check which DPGs should not be run. +// If the user settings has any namespaces in the "disabledProfileSources" +// property, we'll ensure that any DPGs with a matching namespace _don't_ run. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_LoadDynamicProfiles() +{ + std::unordered_set ignoredNamespaces; + const auto disabledProfileSources = CascadiaSettings::_GetDisabledProfileSourcesJsonObject(_userSettings); + if (disabledProfileSources.isArray()) + { + for (const auto& ns : disabledProfileSources) + { + ignoredNamespaces.emplace(GetWstringFromJson(ns)); + } + } + + const GUID nullGuid{ 0 }; + for (auto& generator : _profileGenerators) + { + const std::wstring generatorNamespace{ generator->GetNamespace() }; + + if (ignoredNamespaces.find(generatorNamespace) != ignoredNamespaces.end()) + { + // namespace should be ignored + } + else + { + auto profiles = generator->GenerateProfiles(); + for (auto& profile : profiles) + { + // If the profile did not have a GUID when it was generated, + // we'll synthesize a GUID for it in _ValidateProfilesHaveGuid + profile.SetSource(generatorNamespace); + + _profiles.emplace_back(profile); + } + } + } +} + +// Method Description: +// - Attempts to read the given data as a string of JSON and parse that JSON +// into a Json::Value. // - Will ignore leading UTF-8 BOMs. // - Additionally, will store the parsed JSON in this object, as either our // _defaultSettings or our _userSettings, depending on isDefaultSettings. +// - Does _not_ apply the json onto our current settings. Callers should make +// sure to call LayerJson to ensure the settings are applied. // Arguments: // - fileData: the string to parse as JSON data // - isDefaultSettings: if true, we should store the parsed JSON as our @@ -102,10 +189,11 @@ std::unique_ptr CascadiaSettings::LoadDefaults() // settings. // Return Value: // - -void CascadiaSettings::_LayerJsonString(std::string_view fileData, const bool isDefaultSettings) +void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool isDefaultSettings) { // Ignore UTF-8 BOM auto actualDataStart = fileData.data(); + const auto actualDataEnd = fileData.data() + fileData.size(); if (fileData.compare(0, Utf8Bom.size(), Utf8Bom) == 0) { actualDataStart += Utf8Bom.size(); @@ -119,14 +207,20 @@ void CascadiaSettings::_LayerJsonString(std::string_view fileData, const bool is // their raw contents again. Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings; // `parse` will return false if it fails. - if (!reader->parse(actualDataStart, fileData.data() + fileData.size(), &root, &errs)) + if (!reader->parse(actualDataStart, actualDataEnd, &root, &errs)) { // This will be caught by App::_TryLoadSettings, who will display // the text to the user. throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); } - LayerJson(root); + // If this is the user settings, also store away the original settings + // string. We'll need to keep it around so we can modify it without + // re-serializing their settings. + if (!isDefaultSettings) + { + _userSettingsString = fileData; + } } // Method Description: @@ -148,6 +242,101 @@ void CascadiaSettings::SaveAll() const _WriteSettings(serializedString); } +// Method Description: +// - Finds all the dynamic profiles we've generated that _don't_ exist in the +// user's settings. Generates a minimal blob of json for them, and inserts +// 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! +// - 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 +// indented incorrectly. +// Arguments: +// - +// Return Value: +// - true iff we've made changes to the _userSettingsString that should be persisted. +bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() +{ + // - Find the set of profiles that weren't either in the default profiles or + // in the user profiles. TODO:GH#2723 Do this in not O(N^2) + // - For each of those profiles, + // * Diff them from the default profile + // * Serialize that diff + // * Insert that diff to the end of the list of profiles. + + const Profile defaultProfile; + + Json::StreamWriterBuilder wbuilder; + // Use 4 spaces to indent instead of \t + wbuilder.settings_["indentation"] = " "; + + auto isInJsonObj = [](const auto& profile, const auto& json) { + for (auto profileJson : _GetProfilesJsonObject(json)) + { + if (profileJson.isObject()) + { + if (profile.ShouldBeLayered(profileJson)) + { + return true; + } + } + } + return false; + }; + + // Get the index in the user settings string of the _last_ profile. + // We want to start inserting profiles immediately following the last profile. + const auto userProfilesObj = _GetProfilesJsonObject(_userSettings); + const auto numProfiles = userProfilesObj.size(); + const auto lastProfile = userProfilesObj[numProfiles - 1]; + size_t currentInsertIndex = lastProfile.getOffsetLimit(); + + bool changedFile = false; + + for (const auto& profile : _profiles) + { + // Skip profiles that are in the user settings or the default settings. + if (isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings)) + { + continue; + } + + // Generate a diff for the profile, that contains the minimal set of + // changes to re-create this profile. + const auto diff = profile.GenerateStub(); + auto profileSerialization = Json::writeString(wbuilder, diff); + + // Add 8 spaces to the start of each line + profileSerialization.insert(0, DefaultProfilesIndentation); + // Get the first newline + size_t pos = profileSerialization.find("\n"); + // for each newline... + while (pos != std::string::npos) + { + // Insert 8 spaces immediately following the current newline + profileSerialization.insert(pos + 1, DefaultProfilesIndentation); + // Get the next newline + pos = profileSerialization.find("\n", pos + 9); + } + + // Write a comma, newline to the file + changedFile = true; + _userSettingsString.insert(currentInsertIndex, ","); + currentInsertIndex++; + _userSettingsString.insert(currentInsertIndex, "\n"); + currentInsertIndex++; + + // Write the profile's serialization to the file + _userSettingsString.insert(currentInsertIndex, profileSerialization); + currentInsertIndex += profileSerialization.size(); + } + + return changedFile; +} + // Method Description: // - Serialize this object to a JsonObject. // Arguments: @@ -242,6 +431,10 @@ void CascadiaSettings::LayerJson(const Json::Value& json) // - Given a partial json serialization of a Profile object, either layers that // json on a matching Profile we already have, or creates a new Profile // object from those settings. +// - For profiles that were created from a dynamic profile source, they'll have +// both a guid and source guid that must both match. If a user profile with a +// source set does not find a matching profile at load time, the profile +// should be ignored. // Arguments: // - json: an object which may be a partial serialization of a Profile object. // Return Value: @@ -256,8 +449,14 @@ void CascadiaSettings::_LayerOrCreateProfile(const Json::Value& profileJson) } else { - auto profile = Profile::FromJson(profileJson); - _profiles.emplace_back(profile); + // If this JSON represents a dynamic profile, we _shouldn't_ create the + // profile here. We only want to create profiles for profiles without a + // `source`. Dynamic profiles _must_ be layered on an existing profile. + if (!Profile::IsDynamicProfileObject(profileJson)) + { + auto profile = Profile::FromJson(profileJson); + _profiles.emplace_back(profile); + } } } @@ -549,3 +748,21 @@ const Json::Value& CascadiaSettings::_GetProfilesJsonObject(const Json::Value& j { return json[JsonKey(ProfilesKey)]; } + +// Function Description: +// - Gets the object in the given JSON object under the "disabledProfileSources" +// key. Returns null if there's no "disabledProfileSources" key. +// Arguments: +// - json: the json object to get the disabled profile sources from. +// Return Value: +// - the Json::Value representing the `disabledProfileSources` property from the +// given object +const Json::Value& CascadiaSettings::_GetDisabledProfileSourcesJsonObject(const Json::Value& json) +{ + // Check the globals first, then look in the root. + if (json.isMember(JsonKey(GlobalsKey))) + { + return json[JsonKey(GlobalsKey)][JsonKey(DisabledProfileSourcesKey)]; + } + return json[JsonKey(DisabledProfileSourcesKey)]; +} diff --git a/src/cascadia/TerminalApp/DefaultProfileUtils.cpp b/src/cascadia/TerminalApp/DefaultProfileUtils.cpp new file mode 100644 index 00000000000..c6eca459cb1 --- /dev/null +++ b/src/cascadia/TerminalApp/DefaultProfileUtils.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "DefaultProfileUtils.h" +#include "../../types/inc/utils.hpp" + +static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" }; +static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" }; + +// Method Description: +// - Helper function for creating a skeleton default profile with a pre-populated +// guid and name. +// Arguments: +// - name: the name of the new profile. +// Return Value: +// - A Profile, ready to be filled in +TerminalApp::Profile CreateDefaultProfile(const std::wstring_view name) +{ + const auto profileGuid{ Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, + gsl::as_bytes(gsl::make_span(name))) }; + TerminalApp::Profile newProfile{ profileGuid }; + + newProfile.SetName(static_cast(name)); + + std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH }; + iconPath.append(Microsoft::Console::Utils::GuidToString(profileGuid)); + iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION); + + newProfile.SetIconPath(iconPath); + + return newProfile; +} diff --git a/src/cascadia/TerminalApp/DefaultProfileUtils.h b/src/cascadia/TerminalApp/DefaultProfileUtils.h new file mode 100644 index 00000000000..16d0f99deb1 --- /dev/null +++ b/src/cascadia/TerminalApp/DefaultProfileUtils.h @@ -0,0 +1,23 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Abstract: +- This header stores our default namespace guid. This is used in the creation of + default and in-box dynamic profiles. It also provides a helper function for + creating a "default" profile. Prior to GH#754, this was used to create the + cmd, powershell, wsl, pwsh, and azure profiles. Now, this helper is used for + any of the in-box dynamic profile generators. + +Author(s): +- Mike Griese - August 2019 +-- */ +#pragma once + +#include "Profile.h" + +// {2bde4a90-d05f-401c-9492-e40884ead1d8} +// uuidv5 properties: name format is UTF-16LE bytes +static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; + +TerminalApp::Profile CreateDefaultProfile(const std::wstring_view name); diff --git a/src/cascadia/TerminalApp/IDynamicProfileGenerator.h b/src/cascadia/TerminalApp/IDynamicProfileGenerator.h new file mode 100644 index 00000000000..740ed483c1b --- /dev/null +++ b/src/cascadia/TerminalApp/IDynamicProfileGenerator.h @@ -0,0 +1,37 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IDynamicProfileGenerator + +Abstract: +- The DynamicProfileGenerator interface. A dynamic profile generator is a object + that can synthesize a list of profiles based on some arbitrary, typically + external criteria. Profiles from dynamic sources are only available in the + user's profiles if the generator actually ran and created the profile. +- Each DPG must have a unique namespace to associate with itself. If the + namespace is not unique, the generator risks affecting profiles from + conflicting generators. + +Author(s): +- Mike Griese - August 2019 + +--*/ + +#pragma once +#include "Profile.h" + +namespace TerminalApp +{ + class IDynamicProfileGenerator; +}; + +class TerminalApp::IDynamicProfileGenerator +{ +public: + virtual ~IDynamicProfileGenerator() = 0; + virtual std::wstring_view GetNamespace() = 0; + virtual std::vector GenerateProfiles() = 0; +}; +inline TerminalApp::IDynamicProfileGenerator::~IDynamicProfileGenerator() {} diff --git a/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h b/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h new file mode 100644 index 00000000000..30d0f636aed --- /dev/null +++ b/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h @@ -0,0 +1,21 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Abstract: +- This header simply contains all the namespaces of the "legacy" dynamic profile + generators. These generators were built-in to the code before we had a proper + concept of a dynamic profile source. As such, these profiles will exist in + user's settings without a `source` attribute, and we'll need to make sure to + handle layering them specially. + +Author(s): +- Mike Griese - August 2019 + +--*/ + +#pragma once + +static constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; +static constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; +static constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; diff --git a/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.cpp b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.cpp new file mode 100644 index 00000000000..f93f15100b6 --- /dev/null +++ b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.cpp @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "PowershellCoreProfileGenerator.h" +#include "LegacyProfileGeneratorNamespaces.h" +#include "../../types/inc/utils.hpp" +#include "../../inc/DefaultSettings.h" +#include "Utils.h" +#include "DefaultProfileUtils.h" + +using namespace ::TerminalApp; + +// Legacy GUIDs: +// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336 + +std::wstring_view PowershellCoreProfileGenerator::GetNamespace() +{ + return PowershellCoreGeneratorNamespace; +} + +// Method Description: +// - Checks if pwsh is installed, and if it is, creates a profile to launch it. +// Arguments: +// - +// Return Value: +// - a vector with the PowerShell Core profile, if available. +std::vector PowershellCoreProfileGenerator::GenerateProfiles() +{ + std::vector profiles; + + std::filesystem::path psCoreCmdline; + if (_isPowerShellCoreInstalled(psCoreCmdline)) + { + auto pwshProfile{ CreateDefaultProfile(L"PowerShell Core") }; + + pwshProfile.SetCommandline(std::move(psCoreCmdline)); + pwshProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); + pwshProfile.SetColorScheme({ L"Campbell" }); + + // If powershell core is installed, we'll use that as the default. + // Otherwise, we'll use normal Windows Powershell as the default. + profiles.emplace_back(pwshProfile); + } + + return profiles; +} + +// Function Description: +// - Returns true if the user has installed PowerShell Core. This will check +// both %ProgramFiles% and %ProgramFiles(x86)%, and will return true if +// powershell core was installed in either location. +// Arguments: +// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. +// Return Value: +// - true iff powershell core (pwsh.exe) is present. +bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalled(std::filesystem::path& cmdline) +{ + return _isPowerShellCoreInstalledInPath(L"%ProgramFiles%", cmdline) || + _isPowerShellCoreInstalledInPath(L"%ProgramFiles(x86)%", cmdline); +} + +// Function Description: +// - Returns true if the user has installed PowerShell Core. +// Arguments: +// - A string that contains an environment-variable string in the form: %variableName%. +// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. +// Return Value: +// - true iff powershell core (pwsh.exe) is present in the given path +bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline) +{ + std::wstring programFileEnvNulTerm{ programFileEnv }; + std::filesystem::path psCorePath{ wil::ExpandEnvironmentStringsW(programFileEnvNulTerm.data()) }; + psCorePath /= L"PowerShell"; + if (std::filesystem::exists(psCorePath)) + { + for (auto& p : std::filesystem::directory_iterator(psCorePath)) + { + psCorePath = p.path(); + psCorePath /= L"pwsh.exe"; + if (std::filesystem::exists(psCorePath)) + { + cmdline = psCorePath; + return true; + } + } + } + return false; +} diff --git a/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h new file mode 100644 index 00000000000..2acae60f8f8 --- /dev/null +++ b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PowershellCoreProfileGenerator + +Abstract: +- This is the dynamic profile generator for PowerShell Core. Checks if pwsh is + installed, and if it is, creates a profile to launch it. + +Author(s): +- Mike Griese - August 2019 + +--*/ + +#pragma once + +#include "IDynamicProfileGenerator.h" + +namespace TerminalApp +{ + class PowershellCoreProfileGenerator : public TerminalApp::IDynamicProfileGenerator + { + public: + PowershellCoreProfileGenerator() = default; + ~PowershellCoreProfileGenerator() = default; + std::wstring_view GetNamespace() override; + + std::vector GenerateProfiles() override; + + private: + static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline); + static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline); + }; +}; diff --git a/src/cascadia/TerminalApp/Profile.cpp b/src/cascadia/TerminalApp/Profile.cpp index 2112612ae65..aa1b0156177 100644 --- a/src/cascadia/TerminalApp/Profile.cpp +++ b/src/cascadia/TerminalApp/Profile.cpp @@ -8,12 +8,16 @@ #include "../../types/inc/Utils.hpp" #include +#include "LegacyProfileGeneratorNamespaces.h" + using namespace TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Windows::UI::Xaml; using namespace ::Microsoft::Console; static constexpr std::string_view NameKey{ "name" }; static constexpr std::string_view GuidKey{ "guid" }; +static constexpr std::string_view SourceKey{ "source" }; static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; static constexpr std::string_view ColorSchemeKeyOld{ "colorscheme" }; static constexpr std::string_view HiddenKey{ "hidden" }; @@ -122,6 +126,11 @@ GUID Profile::GetGuid() const noexcept return _guid.value(); } +void Profile::SetSource(std::wstring_view sourceNamespace) noexcept +{ + _source = sourceNamespace; +} + // Function Description: // - Searches a list of color schemes to find one matching the given name. Will //return the first match in the list, if the list has multiple schemes with the same name. @@ -227,8 +236,8 @@ TerminalSettings Profile::CreateTerminalSettings(const std::vector& if (_backgroundImageAlignment) { - const auto imageHorizontalAlignment = std::get(_backgroundImageAlignment.value()); - const auto imageVerticalAlignment = std::get(_backgroundImageAlignment.value()); + const auto imageHorizontalAlignment = std::get(_backgroundImageAlignment.value()); + const auto imageVerticalAlignment = std::get(_backgroundImageAlignment.value()); terminalSettings.BackgroundImageHorizontalAlignment(imageHorizontalAlignment); terminalSettings.BackgroundImageVerticalAlignment(imageVerticalAlignment); } @@ -244,14 +253,9 @@ TerminalSettings Profile::CreateTerminalSettings(const std::vector& // - a JsonObject which is an equivalent serialization of this object. Json::Value Profile::ToJson() const { - Json::Value root; + Json::Value root = GenerateStub(); ///// Profile-specific settings ///// - if (_guid.has_value()) - { - root[JsonKey(GuidKey)] = winrt::to_string(Utils::GuidToString(_guid.value())); - } - root[JsonKey(NameKey)] = winrt::to_string(_name); root[JsonKey(HiddenKey)] = _hidden; ///// Core Settings ///// @@ -345,6 +349,98 @@ Json::Value Profile::ToJson() const return root; } +// Method Description: +// - This generates a json object `diff` s.t. +// this = other.LayerJson(diff) +// So if: +// - this has a nullopt for an optional, diff will have null for that member +// - this has a value for an optional, diff will have our value. If the other +// did _not_ have a value, and we did, diff will have our value. +// Arguments: +// - other: the other profile object to use as the "base" for this diff. The +// result could be layered upon that json object to re-create this object's +// serialization. +// Return Value: +// - a diff between this and the other object, such that this could be recreated +// from the diff and the other object. +Json::Value Profile::DiffToJson(const Profile& other) const +{ + auto otherJson = other.ToJson(); + auto myJson = ToJson(); + Json::Value diff; + + // Iterate in two steps: + // - first over all the keys in the 'other' object's serialization. + // - then over all the keys in our serialization. + // In this way, we ensure all keys from both objects are present in the + // final object. + for (const auto& key : otherJson.getMemberNames()) + { + if (myJson.isMember(key)) + { + // Both objects have the key + auto otherVal = otherJson[key]; + auto myVal = myJson[key]; + if (otherVal != myVal) + { + diff[key] = myVal; + } + } + else + { + // key is not in this json object. Set to null, so that when the + // diff is layered upon the original object, we'll properly set + // nullopt for any optionals that weren't present in this object. + diff[key] = Json::Value::null; + } + } + for (const auto& key : myJson.getMemberNames()) + { + if (otherJson.isMember(key)) + { + // both objects have this key. Do nothing, this is handled above + } + else + { + // We have a key the other object did not. Add our value. + diff[key] = myJson[key]; + } + } + + return diff; +} + +// Method Description: +// - Generates a Json::Value which is a "stub" of this profile. This stub will +// have enough information that it could be layered with this profile. +// - This method is used during dynamic profile generation - if a profile is +// ever generated that didn't already exist in the user's settings, we'll add +// this stub to the user's settings file, so the user has an easy point to +// modify the generated profile. +// Arguments: +// - +// Return Value: +// - A json::Value with a guid, name and source (if applicable). +Json::Value Profile::GenerateStub() const +{ + Json::Value stub; + + ///// Profile-specific settings ///// + if (_guid.has_value()) + { + stub[JsonKey(GuidKey)] = winrt::to_string(Utils::GuidToString(_guid.value())); + } + + stub[JsonKey(NameKey)] = winrt::to_string(_name); + + if (_source.has_value()) + { + stub[JsonKey(SourceKey)] = winrt::to_string(_source.value()); + } + + return stub; +} + // Method Description: // - Create a new instance of this class from a serialized JsonObject. // Arguments: @@ -375,16 +471,61 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const return false; } + // First, check that GUIDs match. This is easy. If they don't match, they + // should _definitely_ not layer. if (json.isMember(JsonKey(GuidKey))) { const auto guid{ json[JsonKey(GuidKey)] }; const auto otherGuid = Utils::GuidFromString(GetWstringFromJson(guid)); - return _guid.value() == otherGuid; + if (_guid.value() != otherGuid) + { + return false; + } } + else + { + // If the other json object didn't have a GUID, we definitely don't want + // to layer. We technically might have the same name, and would + // auto-generate the same guid, but they should be treated as different + // profiles. + return false; + } + + const auto& otherSource = json.isMember(JsonKey(SourceKey)) ? json[JsonKey(SourceKey)] : Json::Value::null; - // TODO: GH#754 - for profiles with a `source`, also check the `source` property. + // For profiles with a `source`, also check the `source` property. + bool sourceMatches = false; + if (_source.has_value()) + { + if (json.isMember(JsonKey(SourceKey))) + { + const auto otherSourceString = GetWstringFromJson(otherSource); + sourceMatches = otherSourceString == _source.value(); + } + else + { + // Special case the legacy dynamic profiles here. In this case, + // `this` is a dynamic profile with a source, and our _source is one + // of the legacy DPG namespaces. We're looking to see if the other + // json object has the same guid, but _no_ "source" + if (_source.value() == WslGeneratorNamespace || + _source.value() == AzureGeneratorNamespace || + _source.value() == PowershellCoreGeneratorNamespace) + { + sourceMatches = true; + } + } + } + else + { + // We do not have a source. The only way we match is if source is set to null or "". + if (otherSource.isNull() || (otherSource.isString() && otherSource == "")) + { + sourceMatches = true; + } + } - return false; + return sourceMatches; } // Method Description: @@ -394,7 +535,7 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const // - json: the Json::Value object to parse. // Return Value: // - An appropriate value from Windows.UI.Xaml.Media.Stretch -winrt::Windows::UI::Xaml::Media::Stretch Profile::_ConvertJsonToStretchMode(const Json::Value& json) +Media::Stretch Profile::_ConvertJsonToStretchMode(const Json::Value& json) { return Profile::ParseImageStretchMode(json.asString()); } @@ -406,7 +547,7 @@ winrt::Windows::UI::Xaml::Media::Stretch Profile::_ConvertJsonToStretchMode(cons // - json: the Json::Value object to parse. // Return Value: // - A pair of HorizontalAlignment and VerticalAlignment -std::tuple Profile::_ConvertJsonToAlignment(const Json::Value& json) +std::tuple Profile::_ConvertJsonToAlignment(const Json::Value& json) { return Profile::ParseImageAlignment(json.asString()); } @@ -745,23 +886,23 @@ ScrollbarState Profile::ParseScrollbarState(const std::wstring& scrollbarState) // - The value from the profiles.json file // Return Value: // - The corresponding enum value which maps to the string provided by the user -winrt::Windows::UI::Xaml::Media::Stretch Profile::ParseImageStretchMode(const std::string_view imageStretchMode) +Media::Stretch Profile::ParseImageStretchMode(const std::string_view imageStretchMode) { if (imageStretchMode == ImageStretchModeNone) { - return winrt::Windows::UI::Xaml::Media::Stretch::None; + return Media::Stretch::None; } else if (imageStretchMode == ImageStretchModeFill) { - return winrt::Windows::UI::Xaml::Media::Stretch::Fill; + return Media::Stretch::Fill; } else if (imageStretchMode == ImageStretchModeUniform) { - return winrt::Windows::UI::Xaml::Media::Stretch::Uniform; + return Media::Stretch::Uniform; } else // Fall through to default behavior { - return winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill; + return Media::Stretch::UniformToFill; } } @@ -772,18 +913,18 @@ winrt::Windows::UI::Xaml::Media::Stretch Profile::ParseImageStretchMode(const st // - imageStretchMode: The enum value to convert to a string. // Return Value: // - The string value for the given ImageStretchMode -std::string_view Profile::SerializeImageStretchMode(const winrt::Windows::UI::Xaml::Media::Stretch imageStretchMode) +std::string_view Profile::SerializeImageStretchMode(const Media::Stretch imageStretchMode) { switch (imageStretchMode) { - case winrt::Windows::UI::Xaml::Media::Stretch::None: + case Media::Stretch::None: return ImageStretchModeNone; - case winrt::Windows::UI::Xaml::Media::Stretch::Fill: + case Media::Stretch::Fill: return ImageStretchModeFill; - case winrt::Windows::UI::Xaml::Media::Stretch::Uniform: + case Media::Stretch::Uniform: return ImageStretchModeUniform; default: - case winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill: + case Media::Stretch::UniformToFill: return ImageStretchModeUniformTofill; } } @@ -795,52 +936,52 @@ std::string_view Profile::SerializeImageStretchMode(const winrt::Windows::UI::Xa // - The value from the profiles.json file // Return Value: // - The corresponding enum values tuple which maps to the string provided by the user -std::tuple Profile::ParseImageAlignment(const std::string_view imageAlignment) +std::tuple Profile::ParseImageAlignment(const std::string_view imageAlignment) { if (imageAlignment == ImageAlignmentTopLeft) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left, - winrt::Windows::UI::Xaml::VerticalAlignment::Top); + return std::make_tuple(HorizontalAlignment::Left, + VerticalAlignment::Top); } else if (imageAlignment == ImageAlignmentBottomLeft) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left, - winrt::Windows::UI::Xaml::VerticalAlignment::Bottom); + return std::make_tuple(HorizontalAlignment::Left, + VerticalAlignment::Bottom); } else if (imageAlignment == ImageAlignmentLeft) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left, - winrt::Windows::UI::Xaml::VerticalAlignment::Center); + return std::make_tuple(HorizontalAlignment::Left, + VerticalAlignment::Center); } else if (imageAlignment == ImageAlignmentTopRight) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right, - winrt::Windows::UI::Xaml::VerticalAlignment::Top); + return std::make_tuple(HorizontalAlignment::Right, + VerticalAlignment::Top); } else if (imageAlignment == ImageAlignmentBottomRight) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right, - winrt::Windows::UI::Xaml::VerticalAlignment::Bottom); + return std::make_tuple(HorizontalAlignment::Right, + VerticalAlignment::Bottom); } else if (imageAlignment == ImageAlignmentRight) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right, - winrt::Windows::UI::Xaml::VerticalAlignment::Center); + return std::make_tuple(HorizontalAlignment::Right, + VerticalAlignment::Center); } else if (imageAlignment == ImageAlignmentTop) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center, - winrt::Windows::UI::Xaml::VerticalAlignment::Top); + return std::make_tuple(HorizontalAlignment::Center, + VerticalAlignment::Top); } else if (imageAlignment == ImageAlignmentBottom) { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center, - winrt::Windows::UI::Xaml::VerticalAlignment::Bottom); + return std::make_tuple(HorizontalAlignment::Center, + VerticalAlignment::Bottom); } else // Fall through to default alignment { - return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center, - winrt::Windows::UI::Xaml::VerticalAlignment::Center); + return std::make_tuple(HorizontalAlignment::Center, + VerticalAlignment::Center); } } @@ -851,46 +992,46 @@ std::tuple imageAlignment) +std::string_view Profile::SerializeImageAlignment(const std::tuple imageAlignment) { - const auto imageHorizontalAlignment = std::get(imageAlignment); - const auto imageVerticalAlignment = std::get(imageAlignment); + const auto imageHorizontalAlignment = std::get(imageAlignment); + const auto imageVerticalAlignment = std::get(imageAlignment); switch (imageHorizontalAlignment) { - case winrt::Windows::UI::Xaml::HorizontalAlignment::Left: + case HorizontalAlignment::Left: switch (imageVerticalAlignment) { - case winrt::Windows::UI::Xaml::VerticalAlignment::Top: + case VerticalAlignment::Top: return ImageAlignmentTopLeft; - case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom: + case VerticalAlignment::Bottom: return ImageAlignmentBottomLeft; default: - case winrt::Windows::UI::Xaml::VerticalAlignment::Center: + case VerticalAlignment::Center: return ImageAlignmentLeft; } - case winrt::Windows::UI::Xaml::HorizontalAlignment::Right: + case HorizontalAlignment::Right: switch (imageVerticalAlignment) { - case winrt::Windows::UI::Xaml::VerticalAlignment::Top: + case VerticalAlignment::Top: return ImageAlignmentTopRight; - case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom: + case VerticalAlignment::Bottom: return ImageAlignmentBottomRight; default: - case winrt::Windows::UI::Xaml::VerticalAlignment::Center: + case VerticalAlignment::Center: return ImageAlignmentRight; } default: - case winrt::Windows::UI::Xaml::HorizontalAlignment::Center: + case HorizontalAlignment::Center: switch (imageVerticalAlignment) { - case winrt::Windows::UI::Xaml::VerticalAlignment::Top: + case VerticalAlignment::Top: return ImageAlignmentTop; - case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom: + case VerticalAlignment::Bottom: return ImageAlignmentBottom; default: - case winrt::Windows::UI::Xaml::VerticalAlignment::Center: + case VerticalAlignment::Center: return ImageAlignmentCenter; } } @@ -964,7 +1105,7 @@ void Profile::GenerateGuidIfNecessary() noexcept { // Always use the name to generate the temporary GUID. That way, across // reloads, we'll generate the same static GUID. - _guid = Profile::_GenerateGuidForProfile(_name); + _guid = Profile::_GenerateGuidForProfile(_name, _source); TraceLoggingWrite( g_hTerminalAppProvider, @@ -975,15 +1116,38 @@ void Profile::GenerateGuidIfNecessary() noexcept } } +// Function Description: +// - Returns true if the given JSON object represents a dynamic profile object. +// If it is a dynamic profile object, we should make sure to only layer the +// object on a matching profile from a dynamic source. +// Arguments: +// - json: the partial serialization of a profile object to check +// Return Value: +// - true iff the object has a non-null `source` property +bool Profile::IsDynamicProfileObject(const Json::Value& json) +{ + const auto& source = json.isMember(JsonKey(SourceKey)) ? json[JsonKey(SourceKey)] : Json::Value::null; + return !source.isNull(); +} + // Function Description: // - Generates a unique guid for a profile, given the name. For an given name, will always return the same GUID. // Arguments: // - name: The name to generate a unique GUID from // Return Value: // - a uuidv5 GUID generated from the given name. -GUID Profile::_GenerateGuidForProfile(const std::wstring& name) noexcept +GUID Profile::_GenerateGuidForProfile(const std::wstring& name, const std::optional& source) noexcept { - return Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); + // If we have a _source, then we can from a dynamic profile generator. Use + // our source to build the naespace guid, instead of using the default GUID. + + const GUID namespaceGuid = source.has_value() ? + Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(source.value()))) : + RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID; + + // Always use the name to generate the temporary GUID. That way, across + // reloads, we'll generate the same static GUID. + return Utils::CreateV5Uuid(namespaceGuid, gsl::as_bytes(gsl::make_span(name))); } // Function Description: @@ -1004,6 +1168,9 @@ GUID Profile::GetGuidOrGenerateForJson(const Json::Value& json) noexcept return guid.value(); } - auto name = GetWstringFromJson(json[JsonKey(NameKey)]); - return Profile::_GenerateGuidForProfile(name); + const auto name = GetWstringFromJson(json[JsonKey(NameKey)]); + std::optional source{ std::nullopt }; + JsonUtils::GetOptionalString(json, SourceKey, source); + + return Profile::_GenerateGuidForProfile(name, source); } diff --git a/src/cascadia/TerminalApp/Profile.h b/src/cascadia/TerminalApp/Profile.h index 2bb7dd68f57..5e16f642799 100644 --- a/src/cascadia/TerminalApp/Profile.h +++ b/src/cascadia/TerminalApp/Profile.h @@ -25,6 +25,7 @@ namespace TerminalAppLocalTests namespace TerminalAppUnitTests { class JsonTests; + class DynamicProfileTests; }; // GUID used for generating GUIDs at runtime, for profiles that did not have a @@ -47,11 +48,15 @@ class TerminalApp::Profile final winrt::Microsoft::Terminal::Settings::TerminalSettings CreateTerminalSettings(const std::vector<::TerminalApp::ColorScheme>& schemes) const; Json::Value ToJson() const; + Json::Value DiffToJson(const Profile& other) const; + Json::Value GenerateStub() const; static Profile FromJson(const Json::Value& json); bool ShouldBeLayered(const Json::Value& json) const; void LayerJson(const Json::Value& json); + static bool IsDynamicProfileObject(const Json::Value& json); GUID GetGuid() const noexcept; + void SetSource(std::wstring_view sourceNamespace) noexcept; std::wstring_view GetName() const noexcept; bool HasConnectionType() const noexcept; GUID GetConnectionType() const noexcept; @@ -77,6 +82,7 @@ class TerminalApp::Profile final bool IsHidden() const noexcept; void GenerateGuidIfNecessary() noexcept; + static GUID GetGuidOrGenerateForJson(const Json::Value& json) noexcept; private: @@ -93,9 +99,10 @@ class TerminalApp::Profile final static winrt::Microsoft::Terminal::Settings::CursorStyle _ParseCursorShape(const std::wstring& cursorShapeString); static std::wstring_view _SerializeCursorStyle(const winrt::Microsoft::Terminal::Settings::CursorStyle cursorShape); - static GUID _GenerateGuidForProfile(const std::wstring& name) noexcept; + static GUID _GenerateGuidForProfile(const std::wstring& name, const std::optional& source) noexcept; std::optional _guid{ std::nullopt }; + std::optional _source{ std::nullopt }; std::wstring _name; std::optional _connectionType; bool _hidden; @@ -134,4 +141,5 @@ class TerminalApp::Profile final friend class TerminalAppLocalTests::SettingsTests; friend class TerminalAppLocalTests::ProfileTests; friend class TerminalAppUnitTests::JsonTests; + friend class TerminalAppUnitTests::DynamicProfileTests; }; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 00b226ce0f3..b8e986f9dc1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -8,6 +8,8 @@ #include "TerminalPage.g.cpp" #include +#include "AzureCloudShellGenerator.h" // For AzureConnectionType + using namespace winrt; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; diff --git a/src/cascadia/TerminalApp/WslDistroGenerator.cpp b/src/cascadia/TerminalApp/WslDistroGenerator.cpp new file mode 100644 index 00000000000..40921cdcc92 --- /dev/null +++ b/src/cascadia/TerminalApp/WslDistroGenerator.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "WslDistroGenerator.h" +#include "LegacyProfileGeneratorNamespaces.h" + +#include "../../types/inc/utils.hpp" +#include "../../inc/DefaultSettings.h" +#include "Utils.h" +#include +#include +#include "DefaultProfileUtils.h" + +using namespace ::TerminalApp; + +// Legacy GUIDs: +// - Debian 58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530 +// - Ubuntu 2c4de342-38b7-51cf-b940-2309a097f518 +// - Alpine 1777cdf0-b2c4-5a63-a204-eb60f349ea7c +// - Ubuntu-18.04 c6eaf9f4-32a7-5fdc-b5cf-066e8a4b1e40 + +std::wstring_view WslDistroGenerator::GetNamespace() +{ + return WslGeneratorNamespace; +} + +// Method Description: +// - Enumerates all the installed WSL distros to create profiles for them. +// Arguments: +// - +// Return Value: +// - a vector with all distros for all the installed WSL distros +std::vector WslDistroGenerator::GenerateProfiles() +{ + std::vector profiles; + + wil::unique_handle readPipe; + wil::unique_handle writePipe; + SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, true }; + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, &sa, 0)); + STARTUPINFO si{ 0 }; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = writePipe.get(); + si.hStdError = writePipe.get(); + wil::unique_process_information pi; + wil::unique_cotaskmem_string systemPath; + THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath)); + std::wstring command(systemPath.get()); + command += L"\\wsl.exe --list"; + + THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + const_cast(command.c_str()), + nullptr, + nullptr, + TRUE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi)); + switch (WaitForSingleObject(pi.hProcess, INFINITE)) + { + case WAIT_OBJECT_0: + break; + case WAIT_ABANDONED: + case WAIT_TIMEOUT: + THROW_HR(ERROR_CHILD_NOT_COMPLETE); + case WAIT_FAILED: + THROW_LAST_ERROR(); + default: + THROW_HR(ERROR_UNHANDLED_EXCEPTION); + } + DWORD exitCode; + if (GetExitCodeProcess(pi.hProcess, &exitCode) == false) + { + THROW_HR(E_INVALIDARG); + } + else if (exitCode != 0) + { + return profiles; + } + DWORD bytesAvailable; + THROW_IF_WIN32_BOOL_FALSE(PeekNamedPipe(readPipe.get(), nullptr, NULL, nullptr, &bytesAvailable, nullptr)); + std::wfstream pipe{ _wfdopen(_open_osfhandle((intptr_t)readPipe.get(), _O_WTEXT | _O_RDONLY), L"r") }; + // don't worry about the handle returned from wfdOpen, readPipe handle is already managed by wil + // and closing the file handle will cause an error. + std::wstring wline; + std::getline(pipe, wline); // remove the header from the output. + while (pipe.tellp() < bytesAvailable) + { + std::getline(pipe, wline); + std::wstringstream wlinestream(wline); + if (wlinestream) + { + std::wstring distName; + std::getline(wlinestream, distName, L'\r'); + size_t firstChar = distName.find_first_of(L"( "); + // Some localizations don't have a space between the name and "(Default)" + // https://github.com/microsoft/terminal/issues/1168#issuecomment-500187109 + if (firstChar < distName.size()) + { + distName.resize(firstChar); + } + auto WSLDistro{ CreateDefaultProfile(distName) }; + + WSLDistro.SetCommandline(L"wsl.exe -d " + distName); + WSLDistro.SetColorScheme({ L"Campbell" }); + WSLDistro.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); + WSLDistro.SetIconPath(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png"); + profiles.emplace_back(WSLDistro); + } + } + + return profiles; +} diff --git a/src/cascadia/TerminalApp/WslDistroGenerator.h b/src/cascadia/TerminalApp/WslDistroGenerator.h new file mode 100644 index 00000000000..8f4aedea57b --- /dev/null +++ b/src/cascadia/TerminalApp/WslDistroGenerator.h @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WslDistroGenerator + +Abstract: +- This is the dynamic profile generator for WSL distros. Enumerates all the + installed WSL distros to create profiles for them. + +Author(s): +- Mike Griese - August 2019 + +--*/ + +#pragma once +#include "IDynamicProfileGenerator.h" + +namespace TerminalApp +{ + class WslDistroGenerator : public TerminalApp::IDynamicProfileGenerator + { + public: + WslDistroGenerator() = default; + ~WslDistroGenerator() = default; + std::wstring_view GetNamespace() override; + std::vector GenerateProfiles() override; + }; +}; diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index daa23082af3..1fa8b3efa49 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -68,7 +68,12 @@ + + + + + ../ActionArgs.idl @@ -108,7 +113,11 @@ + + + + Create diff --git a/src/cascadia/ut_app/DynamicProfileTests.cpp b/src/cascadia/ut_app/DynamicProfileTests.cpp new file mode 100644 index 00000000000..1fe9793147b --- /dev/null +++ b/src/cascadia/ut_app/DynamicProfileTests.cpp @@ -0,0 +1,677 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "../TerminalApp/ColorScheme.h" +#include "../TerminalApp/Profile.h" +#include "../TerminalApp/CascadiaSettings.h" +#include "../TerminalApp/LegacyProfileGeneratorNamespaces.h" + +#include "../LocalTests_TerminalApp/JsonTestClass.h" + +#include "TestDynamicProfileGenerator.h" + +using namespace Microsoft::Console; +using namespace TerminalApp; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppUnitTests +{ + class DynamicProfileTests : public JsonTestClass + { + BEGIN_TEST_CLASS(DynamicProfileTests) + TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.Unit.Tests.manifest") + END_TEST_CLASS() + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + + TEST_METHOD(TestSimpleGenerate); + + // Simple test of CascadiaSettings generating profiles with _LoadDynamicProfiles + TEST_METHOD(TestSimpleGenerateMultipleGenerators); + + // Make sure we gen GUIDs for profiles without guids + TEST_METHOD(TestGenGuidsForProfiles); + + // Profiles without a source should not be layered on those with one + TEST_METHOD(DontLayerUserProfilesOnDynamicProfiles); + TEST_METHOD(DoLayerUserProfilesOnDynamicsWhenSourceMatches); + + // Make sure profiles that are disabled in _userSettings don't get generated + TEST_METHOD(TestDontRunDisabledGenerators); + + // Make sure profiles that are disabled in _userSettings don't get generated + TEST_METHOD(TestLegacyProfilesMigrate); + + // Both these do similar things: + // This makes sure that a profile with a `source` _only_ layers, it won't create a new profile + TEST_METHOD(UserProfilesWithInvalidSourcesAreIgnored); + // This does the same, but by disabling a profile source + TEST_METHOD(UserProfilesFromDisabledSourcesDontAppear); + }; + + void DynamicProfileTests::TestSimpleGenerate() + { + TestDynamicProfileGenerator gen{ L"Terminal.App.UnitTest" }; + gen.pfnGenerate = []() { + std::vector profiles; + Profile p0; + p0.SetName(L"profile0"); + profiles.push_back(p0); + return profiles; + }; + + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest", gen.GetNamespace()); + std::vector profiles = gen.GenerateProfiles(); + VERIFY_ARE_EQUAL(1u, profiles.size()); + VERIFY_ARE_EQUAL(L"profile0", profiles.at(0).GetName()); + VERIFY_IS_FALSE(profiles.at(0)._guid.has_value()); + } + + void DynamicProfileTests::TestSimpleGenerateMultipleGenerators() + { + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = []() { + std::vector profiles; + Profile p0; + p0.SetName(L"profile0"); + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = []() { + std::vector profiles; + Profile p0; + p0.SetName(L"profile1"); + profiles.push_back(p0); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(0)._guid.has_value()); + + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value()); + } + + void DynamicProfileTests::TestGenGuidsForProfiles() + { + // We'll generate GUIDs during + // CascadiaSettings::_ValidateProfilesHaveGuid. We should make sure that + // the GUID generated for a dynamic profile (with a source) is different + // than that of a profile without a source. + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = []() { + std::vector profiles; + Profile p0; + p0.SetName(L"profile0"); // this is _profiles.at(2) + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = []() { + std::vector profiles; + Profile p0, p1; + p0.SetName(L"profile0"); // this is _profiles.at(3) + p1.SetName(L"profile1"); // this is _profiles.at(4) + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + Profile p0, p1; + p0.SetName(L"profile0"); // this is _profiles.at(0) + p1.SetName(L"profile1"); // this is _profiles.at(1) + settings._profiles.push_back(p0); + settings._profiles.push_back(p1); + + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(5u, settings._profiles.size()); + + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(0)._source.has_value()); + + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(1)._source.has_value()); + + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(2).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(3).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value()); + + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(4).GetName()); + VERIFY_IS_FALSE(settings._profiles.at(4)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(4)._source.has_value()); + + settings._ValidateProfilesHaveGuid(); + + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value()); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(), + settings._profiles.at(1)._guid.value()); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(), + settings._profiles.at(2)._guid.value()); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(), + settings._profiles.at(3)._guid.value()); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1)._guid.value(), + settings._profiles.at(4)._guid.value()); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3)._guid.value(), + settings._profiles.at(4)._guid.value()); + } + + void DynamicProfileTests::DontLayerUserProfilesOnDynamicProfiles() + { + GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + const std::string userProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }; + p0.SetName(L"profile0"); // this is _profiles.at(0) + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }, p1{ guid1 }; + p0.SetName(L"profile0"); // this is _profiles.at(1) + p1.SetName(L"profile1"); // this is _profiles.at(2) + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + Log::Comment(NoThrowString().Format( + L"All profiles with the same name have the same GUID. However, they" + L" will not be layered, because they have different sources")); + + // parse userProfiles as the user settings + settings._ParseJsonString(userProfiles, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them"); + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(5u, settings._profiles.size()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(3)._source.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(4)._source.has_value()); + + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value()); + + VERIFY_ARE_EQUAL(guid0, settings._profiles.at(0)._guid.value()); + VERIFY_ARE_EQUAL(guid0, settings._profiles.at(1)._guid.value()); + VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid.value()); + VERIFY_ARE_EQUAL(guid0, settings._profiles.at(3)._guid.value()); + VERIFY_ARE_EQUAL(guid1, settings._profiles.at(4)._guid.value()); + } + + void DynamicProfileTests::DoLayerUserProfilesOnDynamicsWhenSourceMatches() + { + GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + const std::string userProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0FromUserSettings", // this is _profiles.at(0) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.0" + }, + { + "name" : "profile1FromUserSettings", // this is _profiles.at(2) + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.1" + } + ] + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }; + p0.SetName(L"profile0"); // this is _profiles.at(0) + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }, p1{ guid1 }; + p0.SetName(L"profile0"); // this is _profiles.at(1) + p1.SetName(L"profile1"); // this is _profiles.at(2) + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + Log::Comment(NoThrowString().Format( + L"All profiles with the same name have the same GUID. However, they" + L" will not be layered, because they have different source's")); + + // parse userProfiles as the user settings + settings._ParseJsonString(userProfiles, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them"); + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + + VERIFY_ARE_EQUAL(guid0, settings._profiles.at(0)._guid.value()); + VERIFY_ARE_EQUAL(guid0, settings._profiles.at(1)._guid.value()); + VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid.value()); + + VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(2)._name); + } + + void DynamicProfileTests::TestDontRunDisabledGenerators() + { + const std::string settings0String{ R"( + { + "disabledProfileSources": ["Terminal.App.UnitTest.0"] + })" }; + const std::string settings1String{ R"( + { + "disabledProfileSources": ["Terminal.App.UnitTest.0", "Terminal.App.UnitTest.1"] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto gen0GenerateFn = []() { + std::vector profiles; + Profile p0; + p0.SetName(L"profile0"); + profiles.push_back(p0); + return profiles; + }; + + auto gen1GenerateFn = []() { + std::vector profiles; + Profile p0, p1; + p0.SetName(L"profile1"); + p1.SetName(L"profile2"); + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + auto gen2GenerateFn = []() { + std::vector profiles; + Profile p0, p1; + p0.SetName(L"profile3"); + p1.SetName(L"profile4"); + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + { + Log::Comment(NoThrowString().Format( + L"Case 1: Disable a single profile generator")); + CascadiaSettings settings{ false }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + auto gen2 = std::make_unique(L"Terminal.App.UnitTest.2"); + gen0->pfnGenerate = gen0GenerateFn; + gen1->pfnGenerate = gen1GenerateFn; + gen2->pfnGenerate = gen2GenerateFn; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + settings._profileGenerators.emplace_back(std::move(gen2)); + + // Parse as the user settings: + settings._ParseJsonString(settings0String, false); + settings._LoadDynamicProfiles(); + + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(2)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(3)._source.value()); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(3)._name); + } + + { + Log::Comment(NoThrowString().Format( + L"Case 2: Disable multiple profile generators")); + CascadiaSettings settings{ false }; + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + auto gen2 = std::make_unique(L"Terminal.App.UnitTest.2"); + gen0->pfnGenerate = gen0GenerateFn; + gen1->pfnGenerate = gen1GenerateFn; + gen2->pfnGenerate = gen2GenerateFn; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + settings._profileGenerators.emplace_back(std::move(gen2)); + + // Parse as the user settings: + settings._ParseJsonString(settings1String, false); + settings._LoadDynamicProfiles(); + + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); + } + } + + void DynamicProfileTests::TestLegacyProfilesMigrate() + { + GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + GUID guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + GUID guid4 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}"); + + const std::string settings0String{ R"( + { + "profiles": [ + { + // This pwsh profile does not have a source, but should still be layered + "name" : "profile0FromUserSettings", // this is _profiles.at(0) + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + }, + { + // This Azure profile does not have a source, but should still be layered + "name" : "profile3FromUserSettings", // this is _profiles.at(3) + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + }, + { + // This profile did not come from a dynamic source + "name" : "profile4FromUserSettings", // this is _profiles.at(4) + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + }, + { + // This WSL profile does not have a source, but should still be layered + "name" : "profile1FromUserSettings", // this is _profiles.at(1) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + // This WSL profile does have a source, and should be layered + "name" : "profile2FromUserSettings", // this is _profiles.at(2) + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "source": "Windows.Terminal.Wsl" + } + ] + })" }; + + auto gen0 = std::make_unique(L"Windows.Terminal.PowershellCore"); + gen0->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }; + p0.SetName(L"profile0"); + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Windows.Terminal.Wsl"); + gen1->pfnGenerate = [guid2, guid1]() { + std::vector profiles; + Profile p0{ guid1 }, p1{ guid2 }; + p0.SetName(L"profile1"); + p1.SetName(L"profile2"); + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + auto gen2 = std::make_unique(L"Windows.Terminal.Azure"); + gen2->pfnGenerate = [guid3]() { + std::vector profiles; + Profile p0{ guid3 }; + p0.SetName(L"profile3"); + profiles.push_back(p0); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + settings._profileGenerators.emplace_back(std::move(gen2)); + + settings._ParseJsonString(settings0String, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size()); + + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.PowershellCore", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(2)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Azure", settings._profiles.at(3)._source.value()); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(3)._name); + + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(5u, settings._profiles.size()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(4)._source.has_value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.PowershellCore", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(2)._source.value()); + VERIFY_ARE_EQUAL(L"Windows.Terminal.Azure", settings._profiles.at(3)._source.value()); + // settings._profiles.at(4) does not have a soruce + VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile2FromUserSettings", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile3FromUserSettings", settings._profiles.at(3)._name); + VERIFY_ARE_EQUAL(L"profile4FromUserSettings", settings._profiles.at(4)._name); + } + + void DynamicProfileTests::UserProfilesWithInvalidSourcesAreIgnored() + { + GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + const std::string settings0String{ R"( + { + "profiles": [ + { + "name" : "profile0FromUserSettings", // this is _profiles.at(0) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.0" + }, + { + "name" : "profile2", // this shouldn't be in the profiles at all + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.1" + }, + { + "name" : "profile3", // this is _profiles.at(3) + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }; + p0.SetName(L"profile0"); // this is _profiles.at(0) + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }, p1{ guid1 }; + p0.SetName(L"profile0"); // this is _profiles.at(1) + p1.SetName(L"profile1"); // this is _profiles.at(2) + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + settings._ParseJsonString(settings0String, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size()); + + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + } + + void DynamicProfileTests::UserProfilesFromDisabledSourcesDontAppear() + { + GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + const std::string settings0String{ R"( + { + "disabledProfileSources": ["Terminal.App.UnitTest.1"], + "profiles": [ + { + "name" : "profile0FromUserSettings", // this is _profiles.at(0) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.0" + }, + { + "name" : "profile1FromUserSettings", // this shouldn't be in the profiles at all + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.1" + }, + { + "name" : "profile3", // this is _profiles.at(1) + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }; + p0.SetName(L"profile0"); // this is _profiles.at(0) + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid0, guid1]() { + std::vector profiles; + Profile p0{ guid0 }, p1{ guid1 }; + p0.SetName(L"profile0"); // this shouldn't be in the profiles at all + p1.SetName(L"profile1"); // this shouldn't be in the profiles at all + profiles.push_back(p0); + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + settings._ParseJsonString(settings0String, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size()); + + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(1u, settings._profiles.size()); + + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + } + +}; diff --git a/src/cascadia/ut_app/JsonTests.cpp b/src/cascadia/ut_app/JsonTests.cpp index e7c7ac8eaad..36c4f3504cc 100644 --- a/src/cascadia/ut_app/JsonTests.cpp +++ b/src/cascadia/ut_app/JsonTests.cpp @@ -10,6 +10,7 @@ using namespace Microsoft::Console; using namespace TerminalApp; using namespace WEX::Logging; using namespace WEX::TestExecution; +using namespace WEX::Common; namespace TerminalAppUnitTests { @@ -22,10 +23,15 @@ namespace TerminalAppUnitTests TEST_METHOD(ParseInvalidJson); TEST_METHOD(ParseSimpleColorScheme); TEST_METHOD(ProfileGeneratesGuid); + TEST_METHOD(DiffProfile); + TEST_METHOD(DiffProfileWithNull); TEST_CLASS_SETUP(ClassSetup) { reader = std::unique_ptr(Json::CharReaderBuilder::CharReaderBuilder().newCharReader()); + + // Use 4 spaces to indent instead of \t + _builder.settings_["indentation"] = " "; return true; } @@ -34,6 +40,7 @@ namespace TerminalAppUnitTests private: std::unique_ptr reader; + Json::StreamWriterBuilder _builder; }; Json::Value JsonTests::VerifyParseSucceeded(std::string content) @@ -156,4 +163,57 @@ namespace TerminalAppUnitTests VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); } + void JsonTests::DiffProfile() + { + Profile profile0; + Profile profile1; + + Log::Comment(NoThrowString().Format( + L"Both these profiles are the same, their diff should have _no_ values")); + + auto diff = profile1.DiffToJson(profile0); + + Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str())); + + VERIFY_ARE_EQUAL(0u, diff.getMemberNames().size()); + + profile1._name = L"profile1"; + diff = profile1.DiffToJson(profile0); + Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str())); + VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size()); + } + + void JsonTests::DiffProfileWithNull() + { + Profile profile0; + Profile profile1; + + profile0._icon = L"foo"; + + Log::Comment(NoThrowString().Format( + L"Case 1: Base object has an optional that the derived does not - diff will have null for that value")); + auto diff = profile1.DiffToJson(profile0); + + Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str())); + + VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size()); + VERIFY_IS_TRUE(diff.isMember("icon")); + VERIFY_IS_TRUE(diff["icon"].isNull()); + + Log::Comment(NoThrowString().Format( + L"Case 2: Add an optional to the derived object that's not present in the root.")); + + profile0._icon = std::nullopt; + profile1._icon = L"bar"; + + diff = profile1.DiffToJson(profile0); + + Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str())); + + VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size()); + VERIFY_IS_TRUE(diff.isMember("icon")); + VERIFY_IS_TRUE(diff["icon"].isString()); + VERIFY_IS_TRUE("bar" == diff["icon"].asString()); + } + } diff --git a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj index 94784d216ee..0cde6a7a2b5 100644 --- a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj +++ b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj @@ -11,6 +11,7 @@ + Create diff --git a/src/cascadia/ut_app/TestDynamicProfileGenerator.h b/src/cascadia/ut_app/TestDynamicProfileGenerator.h new file mode 100644 index 00000000000..bda1608e5cb --- /dev/null +++ b/src/cascadia/ut_app/TestDynamicProfileGenerator.h @@ -0,0 +1,44 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TestDynamicProfileGenerator.hpp + +Abstract: +- This is a helper class for writing tests using dynamic profiles. Lets you + easily set a arbitrary namespace and generation function for the profiles. + +Author(s): +- Mike Griese - August 2019 +--*/ + +#include "../TerminalApp/IDynamicProfileGenerator.h" + +namespace TerminalAppUnitTests +{ + class TestDynamicProfileGenerator; +}; + +class TerminalAppUnitTests::TestDynamicProfileGenerator final : + public TerminalApp::IDynamicProfileGenerator +{ +public: + TestDynamicProfileGenerator(std::wstring_view ns) : + _namespace{ ns } {}; + + std::wstring_view GetNamespace() override { return _namespace; }; + + std::vector GenerateProfiles() override + { + if (pfnGenerate) + { + return pfnGenerate(); + } + return std::vector{}; + } + + std::wstring _namespace; + + std::function()> pfnGenerate{ nullptr }; +}; diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index d50e2e16394..91308664a04 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -44,6 +44,7 @@ #include #include #include +#include // WIL #include