PRE-MERGE #18639 Introduce PowerShell installer stub

This commit is contained in:
Carlos Zamora 2025-05-05 18:15:20 -07:00
commit 32252145f2
21 changed files with 534 additions and 317 deletions

View File

@ -77,6 +77,16 @@ namespace winrt::TerminalApp::implementation
}
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) };
if (profile.Source() == L"Windows.Terminal.InstallPowerShell")
{
TraceLoggingWrite(
g_hTerminalAppProvider,
"InstallPowerShellStubInvoked",
TraceLoggingDescription("Event emitted when the 'Install Latest PowerShell' stub was invoked"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
// Try to handle auto-elevation
if (_maybeElevate(newTerminalArgs, settings, profile))
{

View File

@ -170,7 +170,8 @@
x:Uid="Extensions_NavigateToProfileButton"
Click="NavigateToProfile_Click"
Style="{StaticResource SettingContainerResetButtonStyle}"
Tag="{x:Bind Profile.Guid}">
Tag="{x:Bind Profile.Guid}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Profile.Deleted)}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>

View File

@ -38,7 +38,7 @@ std::wstring_view AzureCloudShellGenerator::GetIcon() const noexcept
// - <none>
// Return Value:
// - a vector with the Azure Cloud Shell connection profile, if available.
void AzureCloudShellGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
void AzureCloudShellGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
if (AzureConnection::IsAzureConnectionAvailable())
{

View File

@ -27,6 +27,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
};
};

View File

@ -129,7 +129,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings);
void _addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile);
bool _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
static void _executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList);
static void _executeGenerator(IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList);
void _patchInstallPowerShellProfile();
winrt::com_ptr<implementation::ExtensionPackage> _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope);
Json::StreamWriterBuilder _getJsonStyledWriter();
@ -247,14 +248,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
FragmentProfileEntry(winrt::guid profileGuid, hstring json) :
_profileGuid{ profileGuid },
_json{ json } {}
Json{ json } {}
winrt::guid ProfileGuid() const noexcept { return _profileGuid; }
hstring Json() const noexcept { return _json; }
til::property<hstring> Json;
private:
winrt::guid _profileGuid;
hstring _json;
};
struct FragmentColorSchemeEntry : FragmentColorSchemeEntryT<FragmentColorSchemeEntry>
@ -277,11 +277,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
FragmentSettings(hstring source, hstring json, hstring filename) :
_source{ source },
_json{ json },
_Json{ json },
_filename{ filename } {}
hstring Source() const noexcept { return _source; }
hstring Json() const noexcept { return _json; }
hstring Filename() const noexcept { return _filename; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> ModifiedProfiles() const noexcept { return _modifiedProfiles; }
void ModifiedProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& modifiedProfiles) noexcept { _modifiedProfiles = modifiedProfiles; }
@ -289,7 +288,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void NewProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& newProfiles) noexcept { _newProfiles = newProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> ColorSchemes() const noexcept { return _colorSchemes; }
void ColorSchemes(const Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry>& colorSchemes) noexcept { _colorSchemes = colorSchemes; }
WINRT_PROPERTY(hstring, Json);
public:
// views
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> ModifiedProfilesView() const noexcept { return _modifiedProfiles ? _modifiedProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> NewProfilesView() const noexcept { return _newProfiles ? _newProfiles.GetView() : nullptr; }
@ -297,7 +298,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
hstring _source;
hstring _json;
hstring _filename;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _modifiedProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _newProfiles;

View File

@ -19,6 +19,7 @@
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
#include "SshHostGenerator.h"
#endif
#include "PowershellInstallationProfileGenerator.h"
#include "ApplicationState.h"
#include "DefaultTerminal.h"
@ -209,26 +210,58 @@ Json::StreamWriterBuilder SettingsLoader::_getJsonStyledWriter()
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
auto generateProfiles = [&](IDynamicProfileGenerator& generator) {
if (!_ignoredNamespaces.contains(generator.GetNamespace()))
{
_executeGenerator(generator, inboxSettings.profiles);
}
};
generateProfiles(PowershellCoreProfileGenerator{});
generateProfiles(WslDistroGenerator{});
generateProfiles(AzureCloudShellGenerator{});
generateProfiles(VisualStudioGenerator{});
{
PowershellCoreProfileGenerator powerShellGenerator{};
generateProfiles(powerShellGenerator);
if (Feature_PowerShellInstallerProfileGenerator::IsEnabled())
{
if (!powerShellGenerator.GetPowerShellInstances().empty())
{
// If PowerShell is installed, mark the installer profile for deletion
const winrt::guid profileGuid{ L"{965a10f2-b0f2-55dc-a3c2-2ddbf639bf89}" };
for (const auto& profile : userSettings.profiles)
{
if (profile->Guid() == profileGuid)
{
profile->Deleted(true);
break;
}
}
}
else
{
// Only generate the installer stub profile if PowerShell isn't installed.
PowershellInstallationProfileGenerator pwshInstallationGenerator{};
generateProfiles(pwshInstallationGenerator);
}
}
}
WslDistroGenerator wslGenerator{};
generateProfiles(wslGenerator);
AzureCloudShellGenerator acsGenerator{};
generateProfiles(acsGenerator);
VisualStudioGenerator vsGenerator{};
generateProfiles(vsGenerator);
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateProfiles(SshHostGenerator{});
SshHostGenerator sshGenerator{};
generateProfiles(sshGenerator);
#endif
}
// Generate ExtensionPackage objects from the profile generators.
void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
{
auto generateExtensionPackages = [&](const IDynamicProfileGenerator& generator) {
auto generateExtensionPackages = [&](IDynamicProfileGenerator& generator) {
std::vector<winrt::com_ptr<implementation::Profile>> profilesList;
_executeGenerator(generator, profilesList);
@ -255,17 +288,60 @@ void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
extPkg->Icon(hstring{ generator.GetIcon() });
};
// TODO CARLOS: is there a way to deduplicate this list?
// Is it even worth it if we're adding special logic for the PwshInstallerGenerator PR?
generateExtensionPackages(PowershellCoreProfileGenerator{});
generateExtensionPackages(WslDistroGenerator{});
generateExtensionPackages(AzureCloudShellGenerator{});
generateExtensionPackages(VisualStudioGenerator{});
PowershellCoreProfileGenerator powerShellGenerator{};
generateExtensionPackages(powerShellGenerator);
if (Feature_PowerShellInstallerProfileGenerator::IsEnabled())
{
PowershellInstallationProfileGenerator pwshInstallationGenerator{};
generateExtensionPackages(pwshInstallationGenerator);
_patchInstallPowerShellProfile();
}
WslDistroGenerator wslGenerator{};
generateExtensionPackages(wslGenerator);
AzureCloudShellGenerator acsGenerator{};
generateExtensionPackages(acsGenerator);
VisualStudioGenerator vsGenerator{};
generateExtensionPackages(vsGenerator);
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateExtensionPackages(SshHostGenerator{});
SshHostGenerator sshGenerator{};
generateExtensionPackages(sshGenerator);
#endif
}
// Retrieve the "Install Latest PowerShell" profile and add a comment to the JSON to indicate it's conditionally applied
void SettingsLoader::_patchInstallPowerShellProfile()
{
const hstring pwshInstallerNamespace{ PowershellInstallationProfileGenerator::Namespace };
if (extensionPackageMap.contains(pwshInstallerNamespace))
{
if (const auto& fragExtList = extensionPackageMap[pwshInstallerNamespace]->Fragments(); fragExtList.Size() > 0)
{
auto fragExt = get_self<FragmentSettings>(fragExtList.GetAt(0));
// We want the comment to be the first thing in the object,
// "closeOnExit" is the first property, so target that.
auto fragExtJson = _parseJSON(til::u16u8(fragExt->Json()));
fragExtJson[JsonKey(ProfilesKey)][0]["closeOnExit"].setComment(til::u16u8(fmt::format(FMT_COMPILE(L"// {}"), RS_(L"PowerShellInstallationProfileJsonComment"))), Json::CommentPlacement::commentBefore);
fragExt->Json(hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), fragExtJson)) });
if (const auto& profileEntryList = fragExt->NewProfilesView(); profileEntryList.Size() > 0)
{
auto profileEntry = get_self<FragmentProfileEntry>(profileEntryList.GetAt(0));
// We want the comment to be the first thing in the object,
// "closeOnExit" is the first property, so target that.
auto profileJson = _parseJSON(til::u16u8(profileEntry->Json()));
profileJson["closeOnExit"].setComment(til::u16u8(fmt::format(FMT_COMPILE(L"// {}"), RS_(L"PowerShellInstallationProfileJsonComment"))), Json::CommentPlacement::commentBefore);
profileEntry->Json(hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) });
}
}
}
}
// A new settings.json gets a special treatment:
// 1. The default profile is a PowerShell 7+ one, if one was generated,
// and falls back to the standard PowerShell 5 profile otherwise.
@ -1084,7 +1160,7 @@ bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementat
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
void SettingsLoader::_executeGenerator(IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
{
const auto generatorNamespace = generator.GetNamespace();
const auto previousSize = profilesList.size();
@ -1648,7 +1724,11 @@ void CascadiaSettings::_resolveNewTabMenuProfiles() const
auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
{
remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
const auto& profile = _activeProfiles.GetAt(profileIndex);
if (!profile.Deleted())
{
remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
}
}
// We keep track of the "remaining profiles" - those that have not yet been resolved

View File

@ -32,6 +32,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model
virtual std::wstring_view GetNamespace() const noexcept = 0;
virtual std::wstring_view GetDisplayName() const noexcept = 0;
virtual std::wstring_view GetIcon() const noexcept = 0;
virtual void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const = 0;
virtual void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) = 0;
};
};

View File

@ -90,6 +90,7 @@
<DependentUpon>KeyChordSerialization.idl</DependentUpon>
</ClInclude>
<ClInclude Include="PowershellCoreProfileGenerator.h" />
<ClInclude Include="PowershellInstallationProfileGenerator.h" />
<ClInclude Include="Profile.h">
<DependentUpon>Profile.idl</DependentUpon>
</ClInclude>
@ -167,6 +168,7 @@
<DependentUpon>KeyChordSerialization.idl</DependentUpon>
</ClCompile>
<ClCompile Include="PowershellCoreProfileGenerator.cpp" />
<ClCompile Include="PowershellInstallationProfileGenerator.cpp" />
<ClCompile Include="Profile.cpp">
<DependentUpon>Profile.idl</DependentUpon>
</ClCompile>

View File

@ -15,6 +15,9 @@
<ClCompile Include="PowershellCoreProfileGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="PowershellInstallationProfileGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="WslDistroGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
@ -57,6 +60,9 @@
<ClInclude Include="PowershellCoreProfileGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="PowershellInstallationProfileGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="WslDistroGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>

View File

@ -25,336 +25,301 @@ static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///Profile
static constexpr std::wstring_view GENERATOR_POWERSHELL_ICON{ L"ms-appx:///ProfileGeneratorIcons/PowerShell.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)
namespace winrt::Microsoft::Terminal::Settings::Model
{
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
if (std::filesystem::exists(root))
DEFINE_ENUM_FLAG_OPERATORS(PowershellCoreProfileGenerator::PowerShellFlags);
constexpr bool PowershellCoreProfileGenerator::PowerShellInstance::operator<(const PowerShellInstance& second) const
{
for (const auto& versionedDir : std::filesystem::directory_iterator(root))
if (majorVersion != second.majorVersion)
{
const auto versionedPath = versionedDir.path();
const auto executable = versionedPath / PWSH_EXE;
if (std::filesystem::exists(executable))
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 PowershellCoreProfileGenerator::PowerShellInstance::Name() const
{
std::wstringstream namestream;
namestream << L"PowerShell";
if (WI_IsFlagSet(flags, PowerShellFlags::Store))
{
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
{
const auto preview = versionedPath.filename().native().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 });
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();
}
// 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, PowershellCoreProfileGenerator::PowerShellFlags flags, std::vector<PowershellCoreProfileGenerator::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().native().find(L"-preview") != std::wstring::npos;
const auto previewFlag = preview ? PowershellCoreProfileGenerator::PowerShellFlags::Preview : PowershellCoreProfileGenerator::PowerShellFlags::None;
out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{ std::stoi(versionedPath.filename()),
PowershellCoreProfileGenerator::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())
// 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;
}
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)))
// 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<PowershellCoreProfileGenerator::PowerShellInstance>& out)
{
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))
wil::unique_cotaskmem_string localAppDataFolder;
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder)))
{
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 });
}
return;
}
// App execution aliases for stable powershell
const auto gaPath = appExecAliasPath / POWERSHELL_PFN;
if (std::filesystem::exists(gaPath))
std::filesystem::path appExecAliasPath{ localAppDataFolder.get() };
appExecAliasPath /= L"Microsoft";
appExecAliasPath /= L"WindowsApps";
if (std::filesystem::exists(appExecAliasPath))
{
const auto gaPackage = _getStorePackage(POWERSHELL_PFN);
if (gaPackage)
// App execution aliases for preview powershell
const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN;
if (std::filesystem::exists(previewPath))
{
out.emplace_back(PowerShellInstance{
gaPackage.Id().Version().Major,
PowerShellFlags::Store,
gaPath / PWSH_EXE,
});
const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN);
if (previewPackage)
{
out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{
gsl::narrow_cast<int>(previewPackage.Id().Version().Major),
PowershellCoreProfileGenerator::PowerShellFlags::Store | PowershellCoreProfileGenerator::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(PowershellCoreProfileGenerator::PowerShellInstance{
gaPackage.Id().Version().Major,
PowershellCoreProfileGenerator::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))
// 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 PowershellCoreProfileGenerator::PowerShellFlags flags, std::vector<PowershellCoreProfileGenerator::PowerShellInstance>& out)
{
out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath });
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(PowershellCoreProfileGenerator::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;
// Function Description:
// - Builds a comprehensive priority-ordered list of powershell instances.
// Return value:
// - a comprehensive priority-ordered list of powershell instances.
static std::vector<PowershellCoreProfileGenerator::PowerShellInstance> _collectPowerShellInstances()
{
std::vector<PowershellCoreProfileGenerator::PowerShellInstance> versions;
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowerShellFlags::None, versions);
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowershellCoreProfileGenerator::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);
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(x86)%\\PowerShell", PowershellCoreProfileGenerator::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);
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(Arm)%\\PowerShell", PowershellCoreProfileGenerator::PowerShellFlags::WOWARM, versions);
#endif
_accumulateStorePowerShellInstances(versions);
_accumulateStorePowerShellInstances(versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowerShellFlags::Dotnet, versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowerShellFlags::Scoop, versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowershellCoreProfileGenerator::PowerShellFlags::Dotnet, versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowershellCoreProfileGenerator::PowerShellFlags::Scoop, versions);
std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first)
std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first)
return versions;
}
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 } };
// 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;
}
std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept
{
return RS_(L"PowershellCoreProfileGeneratorDisplayName");
}
std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept
{
return GENERATOR_POWERSHELL_ICON;
}
// 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();
auto first = true;
for (const auto& psI : psInstances)
std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept
{
const auto name = psI.Name();
auto profile{ CreateDynamicProfile(name) };
return PowershellCoreGeneratorNamespace;
}
const auto& unquotedCommandline = psI.executablePath.native();
std::wstring quotedCommandline;
quotedCommandline.reserve(unquotedCommandline.size() + 2);
quotedCommandline.push_back(L'"');
quotedCommandline.append(unquotedCommandline);
quotedCommandline.push_back(L'"');
profile->Commandline(winrt::hstring{ quotedCommandline });
std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept
{
return RS_(L"PowershellCoreProfileGeneratorDisplayName");
}
profile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
profile->DefaultAppearance().DarkColorSchemeName(L"Campbell");
profile->DefaultAppearance().LightColorSchemeName(L"Campbell");
profile->Icon(winrt::hstring{ WI_IsFlagSet(psI.flags, PowerShellFlags::Preview) ? POWERSHELL_PREVIEW_ICON : POWERSHELL_ICON });
std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept
{
return GENERATOR_POWERSHELL_ICON;
}
if (first)
// 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)
{
GetPowerShellInstances();
auto first = true;
for (const auto& psI : _powerShellInstances)
{
// 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 });
const auto name = psI.Name();
auto profile{ CreateDynamicProfile(name) };
first = false;
const auto& unquotedCommandline = psI.executablePath.native();
std::wstring quotedCommandline;
quotedCommandline.reserve(unquotedCommandline.size() + 2);
quotedCommandline.push_back(L'"');
quotedCommandline.append(unquotedCommandline);
quotedCommandline.push_back(L'"');
profile->Commandline(winrt::hstring{ quotedCommandline });
profile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
profile->DefaultAppearance().DarkColorSchemeName(L"Campbell");
profile->DefaultAppearance().LightColorSchemeName(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));
}
}
profiles.emplace_back(std::move(profile));
std::vector<PowershellCoreProfileGenerator::PowerShellInstance> PowershellCoreProfileGenerator::GetPowerShellInstances() noexcept
{
if (_powerShellInstances.empty())
{
_powerShellInstances = _collectPowerShellInstances();
}
return _powerShellInstances;
}
// 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;
}
}
// 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;
}

View File

@ -25,9 +25,60 @@ namespace winrt::Microsoft::Terminal::Settings::Model
public:
static const std::wstring_view GetPreferredPowershellProfileName();
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
};
struct PowerShellInstance
{
int majorVersion; // 0 = we don't know, sort last.
PowerShellFlags flags;
std::filesystem::path executablePath;
constexpr bool operator<(const PowerShellInstance& second) const;
std::wstring Name() const;
};
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
std::vector<PowerShellInstance> GetPowerShellInstances() noexcept;
private:
std::vector<PowerShellInstance> _powerShellInstances;
};
};

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "PowershellInstallationProfileGenerator.h"
#include "DynamicProfileUtils.h"
#include <LibraryResources.h>
static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" };
static constexpr std::wstring_view GENERATOR_POWERSHELL_ICON{ L"ms-appx:///ProfileGeneratorIcons/PowerShell.png" };
namespace winrt::Microsoft::Terminal::Settings::Model
{
std::wstring_view PowershellInstallationProfileGenerator::Namespace{ L"Windows.Terminal.InstallPowerShell" };
std::wstring_view PowershellInstallationProfileGenerator::GetNamespace() const noexcept
{
return Namespace;
}
std::wstring_view PowershellInstallationProfileGenerator::GetDisplayName() const noexcept
{
return RS_(L"PowerShellInstallationProfileGeneratorDisplayName");
}
std::wstring_view PowershellInstallationProfileGenerator::GetIcon() const noexcept
{
return GENERATOR_POWERSHELL_ICON;
}
void PowershellInstallationProfileGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
auto profile{ CreateDynamicProfile(RS_(L"PowerShellInstallationProfileName")) };
profile->Commandline(winrt::hstring{ fmt::format(FMT_COMPILE(L"cmd /k winget install --interactive --id Microsoft.PowerShell --source winget & echo. & echo {} & exit"), RS_(L"PowerShellInstallationInstallerGuidance")) });
profile->Icon(winrt::hstring{ POWERSHELL_ICON });
profile->CloseOnExit(CloseOnExitMode::Never);
profiles.emplace_back(std::move(profile));
}
}

View File

@ -0,0 +1,33 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- PowershellInstallationProfileGenerator
Abstract:
- This is the dynamic profile generator for a PowerShell stub. Checks if pwsh is
installed, and if it is NOT installed, creates a profile that installs the
latest PowerShell.
Author(s):
- Carlos Zamora - March 2025
--*/
#pragma once
#include "IDynamicProfileGenerator.h"
namespace winrt::Microsoft::Terminal::Settings::Model
{
class PowershellInstallationProfileGenerator final : public IDynamicProfileGenerator
{
public:
static std::wstring_view Namespace;
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
};
};

View File

@ -1018,6 +1018,14 @@
<value>PowerShell Profile Generator</value>
<comment>The display name of a dynamic profile generator for PowerShell</comment>
</data>
<data name="PowershellInstallationProfileGeneratorDisplayName" xml:space="preserve">
<value>PowerShell Installation Generator</value>
<comment>The display name of a dynamic profile generator that installs the latest PowerShell</comment>
</data>
<data name="PowershellInstallationProfileName" xml:space="preserve">
<value>Install Latest PowerShell</value>
<comment>The display name of a profile generated by the PowerShellInstallationProfileGenerator. This profile installs the latest PowerShell.</comment>
</data>
<data name="AzureCloudShellGeneratorDisplayName" xml:space="preserve">
<value>Azure Cloud Shell Profile Generator</value>
<comment>The display name of a dynamic profile generator for Azure Cloud Shell</comment>
@ -1030,4 +1038,11 @@
<value>SSH Host Profile Generator</value>
<comment>The display name of a dynamic profile generator for SSH hosts</comment>
</data>
<data name="PowerShellInstallationInstallerGuidance" xml:space="preserve">
<value>Restart Windows Terminal to apply the new profile.</value>
<comment>Guidance displayed by the installer directing the user to restart the app.</comment>
</data>
<data name="PowerShellInstallationProfileJsonComment" xml:space="preserve">
<value>This profile only appears if PowerShell is not installed</value>
</data>
</root>

View File

@ -150,7 +150,7 @@ std::wstring_view SshHostGenerator::GetIcon() const noexcept
// - <none>
// Return Value:
// - <A list of SSH host profiles.>
void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
std::wstring sshExePath;
if (_tryFindSshExePath(sshExePath))

View File

@ -26,7 +26,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
private:
static const std::wregex _configKeyValueRegex;

View File

@ -28,7 +28,7 @@ std::wstring_view VisualStudioGenerator::GetIcon() const noexcept
return IconPath;
}
void VisualStudioGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
void VisualStudioGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
const auto instances = VsSetupConfiguration::QueryInstances();

View File

@ -30,7 +30,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
class IVisualStudioProfileGenerator
{

View File

@ -239,7 +239,7 @@ static bool getWslNames(const wil::unique_hkey& wslRootKey,
// - <none>
// Return Value:
// - A list of WSL profiles.
void WslDistroGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
void WslDistroGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
auto wslRootKey{ openWslRegKey() };
if (wslRootKey)

View File

@ -26,6 +26,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
};
};

View File

@ -202,4 +202,16 @@
<alwaysDisabledReleaseTokens/>
</feature>
<feature>
<name>Feature_PowerShellInstallerProfileGenerator</name>
<description>Enables the PowerShell Installer Dynamic Profile Generator</description>
<id>18639</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
<brandingToken>Canary</brandingToken>
<brandingToken>Preview</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
</featureStaging>