terminal/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp
Leonard Hecker 168d28b036
Reduce usage of Json::Value throughout Terminal.Settings.Model (#11184)
This commit reduces the code surface that interacts with raw JSON data,
reducing code complexity and improving maintainability.
Files that needed to be changed drastically were additionally
cleaned up to remove any code cruft that has accrued over time.

In order to facility this the following changes were made:
* Move JSON handling from `CascadiaSettings` into `SettingsLoader`
  This allows us to use STL containers for data model instances.
  For instance profiles are now added to a hashmap for O(1) lookup.
* JSON parsing within `SettingsLoader` doesn't differentiate between user,
  inbox and fragment JSON data, reducing code complexity and size.
  It also centralizes common concerns, like profile deduplication and
  ensuring that all profiles are assigned a GUID.
* Direct JSON modification, like the insertion of dynamic profiles into
  settings.json were removed. This vastly reduces code complexity,
  but unfortunately removes support for comments in JSON on first start.
* `ColorScheme`s cannot be layered. As such its `LayerJson` API was replaced
  with `FromJson`, allowing us to remove JSON-based color scheme validation.
* `Profile`s used to test their wish to layer using `ShouldBeLayered`, which
  was replaced with a GUID-based hashmap lookup on previously parsed profiles.

Further changes were made as improvements upon the previous changes:
* Compact the JSON files embedded binary, saving 28kB
* Prevent double-initialization of the color table in `ColorScheme`
* Making `til::color` getters `constexpr`, allow better optimizations

The result is a reduction of:
* 48kB binary size for the Settings.Model.dll
* 5-10% startup duration
* 26% code for the `CascadiaSettings` class
* 1% overall code in this project

Furthermore this results in the following breaking changes:
* The long deprecated "globals" settings object will not be detected and no
  warning will be created during load.
* The initial creation of a new settings.json will not produce helpful comments.

Both cases are caused by the removal of manual JSON handling and the
move to representing the settings file with model objects instead

## PR Checklist
* [x] Closes #5276
* [x] Closes #7421
* [x] I work here
* [x] Tests added/passed

## Validation Steps Performed

* Out-of-box-experience is identical to before ✔️
  (Except for the settings.json file lacking comments.)
* Existing user settings load correctly ✔️
* New WSL instances are added to user settings ✔️
* New fragments are added to user settings ✔️
* All profiles are assigned GUIDs ✔️
2021-09-22 16:27:31 +00:00

340 lines
14 KiB
C++

// 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 "DynamicProfileUtils.h"
// These four are headers we do not want proliferating, so they're not in the PCH.
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Management.Deployment.h>
#include <appmodel.h>
#include <shlobj.h>
static constexpr std::wstring_view POWERSHELL_PFN{ L"Microsoft.PowerShell_8wekyb3d8bbwe" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_PFN{ L"Microsoft.PowerShellPreview_8wekyb3d8bbwe" };
static constexpr std::wstring_view PWSH_EXE{ L"pwsh.exe" };
static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///ProfileIcons/pwsh-preview.png" };
static constexpr std::wstring_view POWERSHELL_PREFERRED_PROFILE_NAME{ L"PowerShell" };
namespace
{
enum PowerShellFlags
{
None = 0,
// These flags are used as a sort key, so they encode some native ordering.
// They are ordered such that the "most important" flags have the largest
// impact on the sort space. For example, since we want Preview to be very polar
// we give it the highest flag value.
// The "ideal" powershell instance has 0 flags (stable, native, Program Files location)
//
// With this ordering, the sort space ends up being (for PowerShell 6)
// (numerically greater values are on the left; this is flipped in the final sort)
//
// <-- Less Valued .................................... More Valued -->
// | All instances of PS 6 | All PS7 |
// | Preview | Stable | ~~~ |
// | Non-Native | Native | Non-Native | Native | ~~~ |
// | Trd | Pack | Trd | Pack | Trd | Pack | Trd | Pack | ~~~ |
// (where Pack is a stand-in for store, scoop, dotnet, though they have their own orders,
// and Trd is a stand-in for "Traditional" (Program Files))
//
// In short, flags with larger magnitudes are pushed further down (therefore valued less)
// distribution method (choose one)
Store = 1 << 0, // distributed via the store
Scoop = 1 << 1, // installed via Scoop
Dotnet = 1 << 2, // installed as a dotnet global tool
Traditional = 1 << 3, // installed in traditional Program Files locations
// native architecture (choose one)
WOWARM = 1 << 4, // non-native (Windows-on-Windows, ARM variety)
WOWx86 = 1 << 5, // non-native (Windows-on-Windows, x86 variety)
// build type (choose one)
Preview = 1 << 6, // preview version
};
DEFINE_ENUM_FLAG_OPERATORS(PowerShellFlags);
struct PowerShellInstance
{
int majorVersion; // 0 = we don't know, sort last.
PowerShellFlags flags;
std::filesystem::path executablePath;
constexpr bool operator<(const PowerShellInstance& second) const
{
if (majorVersion != second.majorVersion)
{
return majorVersion < second.majorVersion;
}
if (flags != second.flags)
{
return flags > second.flags; // flags are inverted because "0" is ideal; see above
}
return executablePath < second.executablePath; // fall back to path sorting
}
// Method Description:
// - Generates a name, based on flags, for a powershell instance.
// Return value:
// - the name
std::wstring Name() const
{
std::wstringstream namestream;
namestream << L"PowerShell";
if (WI_IsFlagSet(flags, PowerShellFlags::Store))
{
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
{
namestream << L" Preview";
}
namestream << L" (msix)";
}
else if (WI_IsFlagSet(flags, PowerShellFlags::Dotnet))
{
namestream << L" (dotnet global)";
}
else if (WI_IsFlagSet(flags, PowerShellFlags::Scoop))
{
namestream << L" (scoop)";
}
else
{
if (majorVersion < 7)
{
namestream << L" Core";
}
if (majorVersion != 0)
{
namestream << L" " << majorVersion;
}
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
{
namestream << L" Preview";
}
if (WI_IsFlagSet(flags, PowerShellFlags::WOWx86))
{
namestream << L" (x86)";
}
if (WI_IsFlagSet(flags, PowerShellFlags::WOWARM))
{
namestream << L" (ARM)";
}
}
return namestream.str();
}
};
}
using namespace ::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model;
// Function Description:
// - Finds all powershell instances with the traditional layout under a directory.
// - The "traditional" directory layout requires that pwsh.exe exist in a versioned directory, as in
// ROOT\6\pwsh.exe
// Arguments:
// - directory: the directory under which to search
// - flags: flags to apply to all found instances
// - out: the list into which to accumulate these instances.
static void _accumulateTraditionalLayoutPowerShellInstancesInDirectory(std::wstring_view directory, PowerShellFlags flags, std::vector<PowerShellInstance>& out)
{
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
if (std::filesystem::exists(root))
{
for (const auto& versionedDir : std::filesystem::directory_iterator(root))
{
const auto versionedPath = versionedDir.path();
const auto executable = versionedPath / PWSH_EXE;
if (std::filesystem::exists(executable))
{
const auto preview = versionedPath.filename().wstring().find(L"-preview") != std::wstring::npos;
const auto previewFlag = preview ? PowerShellFlags::Preview : PowerShellFlags::None;
out.emplace_back(PowerShellInstance{ std::stoi(versionedPath.filename()),
PowerShellFlags::Traditional | flags | previewFlag,
executable });
}
}
}
}
// Function Description:
// - Finds the store package, if one exists, for a given package family name
// Arguments:
// - packageFamilyName: the package family name
// Return Value:
// - a package, or nullptr.
static winrt::Windows::ApplicationModel::Package _getStorePackage(const std::wstring_view packageFamilyName) noexcept
try
{
winrt::Windows::Management::Deployment::PackageManager packageManager;
auto foundPackages = packageManager.FindPackagesForUser(L"", packageFamilyName);
auto iterator = foundPackages.First();
if (!iterator.HasCurrent())
{
return nullptr;
}
return iterator.Current();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
// Function Description:
// - Finds all powershell instances that have App Execution Aliases in the standard location
// Arguments:
// - out: the list into which to accumulate these instances.
static void _accumulateStorePowerShellInstances(std::vector<PowerShellInstance>& out)
{
wil::unique_cotaskmem_string localAppDataFolder;
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder)))
{
return;
}
std::filesystem::path appExecAliasPath{ localAppDataFolder.get() };
appExecAliasPath /= L"Microsoft";
appExecAliasPath /= L"WindowsApps";
if (std::filesystem::exists(appExecAliasPath))
{
// App execution aliases for preview powershell
const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN;
if (std::filesystem::exists(previewPath))
{
const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN);
if (previewPackage)
{
out.emplace_back(PowerShellInstance{
gsl::narrow_cast<int>(previewPackage.Id().Version().Major),
PowerShellFlags::Store | PowerShellFlags::Preview,
previewPath / PWSH_EXE });
}
}
// App execution aliases for stable powershell
const auto gaPath = appExecAliasPath / POWERSHELL_PFN;
if (std::filesystem::exists(gaPath))
{
const auto gaPackage = _getStorePackage(POWERSHELL_PFN);
if (gaPackage)
{
out.emplace_back(PowerShellInstance{
gaPackage.Id().Version().Major,
PowerShellFlags::Store,
gaPath / PWSH_EXE,
});
}
}
}
}
// Function Description:
// - Finds a powershell instance that's just a pwsh.exe in a folder.
// - This function cannot determine the version number of such a powershell instance.
// Arguments:
// - directory: the directory under which to search
// - flags: flags to apply to all found instances
// - out: the list into which to accumulate these instances.
static void _accumulatePwshExeInDirectory(const std::wstring_view directory, const PowerShellFlags flags, std::vector<PowerShellInstance>& out)
{
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
const auto pwshPath = root / PWSH_EXE;
if (std::filesystem::exists(pwshPath))
{
out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath });
}
}
// Function Description:
// - Builds a comprehensive priority-ordered list of powershell instances.
// Return value:
// - a comprehensive priority-ordered list of powershell instances.
static std::vector<PowerShellInstance> _collectPowerShellInstances()
{
std::vector<PowerShellInstance> versions;
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowerShellFlags::None, versions);
#if defined(_M_AMD64) || defined(_M_ARM64) // No point in looking for WOW if we're not somewhere it exists
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(x86)%\\PowerShell", PowerShellFlags::WOWx86, versions);
#endif
#if defined(_M_ARM64) // no point in looking for WOA if we're not on ARM64
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(Arm)%\\PowerShell", PowerShellFlags::WOWARM, versions);
#endif
_accumulateStorePowerShellInstances(versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowerShellFlags::Dotnet, versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowerShellFlags::Scoop, versions);
std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first)
return versions;
}
// Legacy GUIDs:
// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336
static constexpr winrt::guid PowershellCoreGuid{ 0x574e775e, 0x4f2a, 0x5b96, { 0xac, 0x1e, 0xa2, 0x96, 0x2a, 0x40, 0x23, 0x36 } };
std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept
{
return PowershellCoreGeneratorNamespace;
}
// Method Description:
// - Checks if pwsh is installed, and if it is, creates a profile to launch it.
// Arguments:
// - <none>
// Return Value:
// - a vector with the PowerShell Core profile, if available.
void PowershellCoreProfileGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
const auto psInstances = _collectPowerShellInstances();
bool first = true;
for (const auto& psI : psInstances)
{
const auto name = psI.Name();
auto profile{ CreateDynamicProfile(name) };
profile->Commandline(winrt::hstring{ psI.executablePath.native() });
profile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
profile->DefaultAppearance().ColorSchemeName(L"Campbell");
profile->Icon(winrt::hstring{ WI_IsFlagSet(psI.flags, PowerShellFlags::Preview) ? POWERSHELL_PREVIEW_ICON : POWERSHELL_ICON });
if (first)
{
// Give the first ("algorithmically best") profile the official, and original, "PowerShell Core" GUID.
// This will turn the anchored default profile into "PowerShell Core Latest for Native Architecture through Store"
// (or the closest approximation thereof). It may choose a preview instance as the "best" if it is a higher version.
profile->Guid(PowershellCoreGuid);
profile->Name(winrt::hstring{ POWERSHELL_PREFERRED_PROFILE_NAME });
first = false;
}
profiles.emplace_back(std::move(profile));
}
}
// Function Description:
// - Returns the thing it's named for.
// Return value:
// - the thing it says in the name
const std::wstring_view PowershellCoreProfileGenerator::GetPreferredPowershellProfileName()
{
return POWERSHELL_PREFERRED_PROFILE_NAME;
}