340 lines
14 KiB
C++
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;
|
|
}
|