Workspaces: PWA apps should not necessarily configure profile to launch. (#39971)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Workspace support for pwa is now limited, it is tight to specific
Profile launch. If you create a pwa app with a profile other than
"Default", launch will fail. Then you have to manually configure that
profile to launch.
This pr fix it by launching with shell:appsfolder\appusermodelId

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [X] **Closes:** #36384
- [ ] **Communication:** I've discussed this with core contributors
already. If work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end user facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
- [X] Create a new workspace with a pwa app(Other than default profile)
should be no problem.
- [X] Existing workspace with a pwa(default profile and other profile)
should launch successfully without problem

1. with pt version 91.1, create a loop pwa with "Profile 1" instead of
"Default" in edge.
2. capture and launch actually launch the edge instead of loop
3. Create profile with this impl and launch
4. Launch pwa successfully

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kai Tao
2025-06-17 15:39:44 +08:00
committed by GitHub
parent 252dbb5853
commit c487638758
18 changed files with 223 additions and 123 deletions

View File

@@ -49,6 +49,8 @@ namespace WorkspacesEditor.Data
public WindowPositionWrapper Position { get; set; }
public int Monitor { get; set; }
public string Version { get; set; }
}
public struct MonitorConfigurationWrapper

View File

@@ -41,6 +41,7 @@ namespace WorkspacesEditor.Models
Maximized = other.Maximized;
Position = other.Position;
MonitorNumber = other.MonitorNumber;
Version = other.Version;
Parent = other.Parent;
IsNotFound = other.IsNotFound;
@@ -274,5 +275,7 @@ namespace WorkspacesEditor.Models
CommandLineArguments = newCommandLineValue;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
}
public string Version { get; set; }
}
}

View File

@@ -246,6 +246,7 @@ namespace WorkspacesEditor.Models
AppPath = app.ApplicationPath,
AppTitle = app.Title,
PwaAppId = string.IsNullOrEmpty(app.PwaAppId) ? string.Empty : app.PwaAppId,
Version = string.IsNullOrEmpty(app.Version) ? string.Empty : app.Version,
PackageFullName = app.PackageFullName,
AppUserModelId = app.AppUserModelId,
Parent = this,

View File

@@ -107,6 +107,7 @@ namespace WorkspacesEditor.Utils
CommandLineArguments = app.CommandLineArguments,
IsElevated = app.IsElevated,
CanLaunchElevated = app.CanLaunchElevated,
Version = app.Version,
Maximized = app.Maximized,
Minimized = app.Minimized,
Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper

View File

@@ -165,16 +165,52 @@ namespace AppLauncher
if (!launched && !app.pwaAppId.empty())
{
std::filesystem::path appPath(app.path);
if (appPath.filename() == NonLocalizable::EdgeFilename)
int version = 0;
if (app.version != L"")
{
appPathFinal = appPath.parent_path() / NonLocalizable::EdgePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
try
{
version = std::stoi(app.version);
}
catch (const std::invalid_argument&)
{
Logger::error(L"Invalid version format: {}", app.version);
version = 0;
}
catch (const std::out_of_range&)
{
Logger::error(L"Version out of range: {}", app.version);
version = 0;
}
}
if (appPath.filename() == NonLocalizable::ChromeFilename)
if (version >= 1)
{
appPathFinal = appPath.parent_path() / NonLocalizable::ChromePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ app.appUserModelId, res.error() });
}
}
if (!launched)
{
std::filesystem::path appPath(app.path);
if (appPath.filename() == NonLocalizable::EdgeFilename)
{
appPathFinal = appPath.parent_path() / NonLocalizable::EdgePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
if (appPath.filename() == NonLocalizable::ChromeFilename)
{
appPathFinal = appPath.parent_path() / NonLocalizable::ChromePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
}
}

View File

@@ -76,19 +76,6 @@ namespace WorkspacesLibUnitTests
Assert::IsTrue(result == nonExistentWindowAumid);
}
TEST_METHOD (PwaHelper_GetAUMIDFromWindow_InvalidWindow_ReturnsEmpty)
{
// Arrange
Utils::PwaHelper helper;
HWND invalidWindow = nullptr;
// Act
std::wstring result = helper.GetAUMIDFromWindow(invalidWindow);
// Assert
Assert::IsTrue(result.empty());
}
TEST_METHOD (PwaHelper_GetEdgeAppId_ValidConstruction_DoesNotCrash)
{
// Arrange

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "PwaHelper.h"
#include "WindowUtils.h"
#include <filesystem>
@@ -51,7 +52,7 @@ namespace Utils
localFolder = L""; // Ensure it is explicitly set to empty on failure
}
}
return localFolder;
}
@@ -193,7 +194,7 @@ namespace Utils
return std::nullopt;
}
std::optional<std::wstring> PwaHelper::GetChromeAppId(const std::wstring& windowAumid) const
{
const auto appIdIndexStart = windowAumid.find(NonLocalizable::ChromeAppIdIdentifier);
@@ -256,85 +257,4 @@ namespace Utils
return result;
}
std::wstring PwaHelper::GetAUMIDFromWindow(HWND hwnd) const
{
std::wstring result{};
if (hwnd == NULL)
{
return result;
}
Microsoft::WRL::ComPtr<IPropertyStore> propertyStore;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&propertyStore));
if (FAILED(hr))
{
return result;
}
PROPVARIANT propVariant;
PropVariantInit(&propVariant);
hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &propVariant);
if (SUCCEEDED(hr) && propVariant.vt == VT_LPWSTR && propVariant.pwszVal != nullptr)
{
result = propVariant.pwszVal;
}
PropVariantClear(&propVariant);
Logger::info(L"Found a window with aumid {}", result);
return result;
}
std::wstring PwaHelper::GetAUMIDFromProcessId(DWORD processId) const
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
if (hProcess == NULL)
{
Logger::error(L"Failed to open process handle. Error: {}", get_last_error_or_default(GetLastError()));
return {};
}
// Get the package full name for the process
UINT32 packageFullNameLength = 0;
LONG rc = GetPackageFullName(hProcess, &packageFullNameLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get package full name length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> packageFullName(packageFullNameLength);
rc = GetPackageFullName(hProcess, &packageFullNameLength, packageFullName.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get package full name. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
// Get the AUMID for the package
UINT32 appModelIdLength = 0;
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get AppUserModelId length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> appModelId(appModelIdLength);
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, appModelId.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get AppUserModelId. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
CloseHandle(hProcess);
return std::wstring(appModelId.data());
}
}

View File

@@ -12,19 +12,16 @@ namespace Utils
PwaHelper();
~PwaHelper() = default;
std::wstring GetAUMIDFromWindow(HWND hWnd) const;
std::optional<std::wstring> GetEdgeAppId(const std::wstring& windowAumid) const;
std::optional<std::wstring> GetChromeAppId(const std::wstring& windowAumid) const;
std::optional<std::wstring> GetChromeAppId(const std::wstring& windowAumid) const;
std::wstring SearchPwaName(const std::wstring& pwaAppId, const std::wstring& windowAumid) const;
private:
void InitAppIds(const std::wstring& browserDataFolder, const std::wstring& browserDirPrefix, const std::function<void(const std::wstring&)>& addingAppIdCallback);
void InitEdgeAppIds();
void InitChromeAppIds();
std::wstring GetAppIdFromCommandLineArgs(const std::wstring& commandLineArgs) const;
std::wstring GetAUMIDFromProcessId(DWORD processId) const;
std::map<std::wstring, std::wstring> m_edgeAppIds;
std::vector<std::wstring> m_chromeAppIds;

View File

@@ -0,0 +1,18 @@
#include "pch.h"
#include "StringUtils.h"
namespace StringUtils
{
bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2)
{
if (str1.size() != str2.size())
{
return false;
}
return std::equal(str1.begin(), str1.end(), str2.begin(), [](wchar_t ch1, wchar_t ch2) {
return towupper(ch1) == towupper(ch2);
});
}
}

View File

@@ -6,15 +6,5 @@
namespace StringUtils
{
inline bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2)
{
if (str1.size() != str2.size())
{
return false;
}
return std::equal(str1.begin(), str1.end(), str2.begin(), [](wchar_t ch1, wchar_t ch2) {
return towupper(ch1) == towupper(ch2);
});
}
bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2);
}

View File

@@ -0,0 +1,105 @@
#include "pch.h"
#include "WindowUtils.h"
#include <filesystem>
#include <appmodel.h>
#include <shellapi.h>
#include <ShlObj.h>
#include <shobjidl.h>
#include <tlhelp32.h>
#include <wrl.h>
#include <propkey.h>
#include <wil/com.h>
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/CommandLineArgsHelper.h>
#include <WorkspacesLib/StringUtils.h>
namespace Utils
{
std::wstring GetAUMIDFromProcessId(DWORD processId)
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
if (hProcess == NULL)
{
Logger::error(L"Failed to open process handle. Error: {}", get_last_error_or_default(GetLastError()));
return {};
}
// Get the package full name for the process
UINT32 packageFullNameLength = 0;
LONG rc = GetPackageFullName(hProcess, &packageFullNameLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get package full name length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> packageFullName(packageFullNameLength);
rc = GetPackageFullName(hProcess, &packageFullNameLength, packageFullName.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get package full name. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
// Get the AUMID for the package
UINT32 appModelIdLength = 0;
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get AppUserModelId length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> appModelId(appModelIdLength);
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, appModelId.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get AppUserModelId. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
CloseHandle(hProcess);
return std::wstring(appModelId.data());
}
std::wstring GetAUMIDFromWindow(HWND hwnd)
{
std::wstring result{};
if (hwnd == NULL)
{
return result;
}
Microsoft::WRL::ComPtr<IPropertyStore> propertyStore;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&propertyStore));
if (FAILED(hr))
{
return result;
}
PROPVARIANT propVariant;
PropVariantInit(&propVariant);
hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &propVariant);
if (SUCCEEDED(hr) && propVariant.vt == VT_LPWSTR && propVariant.pwszVal != nullptr)
{
result = propVariant.pwszVal;
}
PropVariantClear(&propVariant);
Logger::info(L"Found a window with aumid {}", result);
return result;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <functional>
#include <WorkspacesLib/AppUtils.h>
#include <wtypes.h>
namespace Utils
{
std::wstring GetAUMIDFromWindow(HWND hWnd);
std::wstring GetAUMIDFromProcessId(DWORD processId);
};

View File

@@ -87,6 +87,7 @@ namespace WorkspacesData
const static wchar_t* MaximizedID = L"maximized";
const static wchar_t* PositionID = L"position";
const static wchar_t* MonitorID = L"monitor";
const static wchar_t* VersionID = L"version";
}
json::JsonObject ToJson(const WorkspacesProject::Application& data)
@@ -106,6 +107,7 @@ namespace WorkspacesData
json.SetNamedValue(NonLocalizable::MaximizedID, json::value(data.isMaximized));
json.SetNamedValue(NonLocalizable::PositionID, PositionJSON::ToJson(data.position));
json.SetNamedValue(NonLocalizable::MonitorID, json::value(data.monitor));
json.SetNamedValue(NonLocalizable::VersionID, json::value(data.version));
return json;
}
@@ -168,6 +170,11 @@ namespace WorkspacesData
result.position = position.value();
}
if (json.HasKey(NonLocalizable::VersionID))
{
result.version = json.GetNamedString(NonLocalizable::VersionID);
}
}
catch (const winrt::hresult_error&)
{
@@ -286,6 +293,7 @@ namespace WorkspacesData
const static wchar_t* MoveExistingWindowsID = L"move-existing-windows";
const static wchar_t* MonitorConfigurationID = L"monitor-configuration";
const static wchar_t* AppsID = L"applications";
const static wchar_t* Version = L"version";
}
json::JsonObject ToJson(const WorkspacesProject& data)

View File

@@ -33,6 +33,9 @@ namespace WorkspacesData
std::wstring appUserModelId;
std::wstring pwaAppId;
std::wstring commandLineArgs;
// empty to 1,
std::wstring version;
bool isElevated{};
bool canLaunchElevated{};
bool isMinimized{};
@@ -86,7 +89,7 @@ namespace WorkspacesData
{
WorkspacesData::WorkspacesProject::Application application;
HWND window{};
LaunchingState state { LaunchingState::Waiting };
LaunchingState state{ LaunchingState::Waiting };
};
using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;

View File

@@ -45,6 +45,7 @@
<ClInclude Include="StringUtils.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WbemHelper.h" />
<ClInclude Include="WindowUtils.h" />
<ClInclude Include="WorkspacesData.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
@@ -59,8 +60,10 @@
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="SteamGameHelper.cpp" />
<ClCompile Include="StringUtils.cpp" />
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WbemHelper.cpp" />
<ClCompile Include="WindowUtils.cpp" />
<ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>

View File

@@ -56,6 +56,9 @@
<ClInclude Include="SteamHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -94,6 +97,12 @@
<ClCompile Include="SteamGameHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StringUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -11,6 +11,7 @@
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/PwaHelper.h>
#include <WorkspacesLib/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include "Generated Files/resource.h"
@@ -139,7 +140,7 @@ namespace SnapshotUtils
bool isChrome = appData.IsChrome();
if (isEdge || isChrome)
{
auto windowAumid = pwaHelper.GetAUMIDFromWindow(window);
auto windowAumid = Utils::GetAUMIDFromWindow(window);
std::optional<std::wstring> pwaAppId{};
if (isEdge)
@@ -158,6 +159,8 @@ namespace SnapshotUtils
appData.pwaAppId = pwaAppId.value();
appData.name = pwaName + L" (" + appData.name + L")";
// If it's pwa app, appUserModelId should be their own pwa id.
appData.appUserModelId = windowAumid;
}
}
@@ -185,6 +188,7 @@ namespace SnapshotUtils
.appUserModelId = appData.appUserModelId,
.pwaAppId = appData.pwaAppId,
.commandLineArgs = L"",
.version = L"1",
.isElevated = IsProcessElevated(pid),
.canLaunchElevated = appData.canLaunchElevated,
.isMinimized = isMinimized,

View File

@@ -13,6 +13,7 @@
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include <WorkspacesLib/PwaHelper.h>
#include <WorkspacesLib/WindowUtils.h>
namespace NonLocalizable
{
@@ -203,7 +204,7 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
@@ -220,7 +221,7 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
bool isChrome = appData.IsChrome();
if (isEdge || isChrome)
{
auto windowAumid = pwaHelper.GetAUMIDFromWindow(window);
auto windowAumid = Utils::GetAUMIDFromWindow(window);
std::optional<std::wstring> pwaAppId{};
if (isEdge)