[Workspaces] Handle admin windows repositioning. (#34965)

This commit is contained in:
Seraphima Zykova 2024-09-25 12:13:38 +03:00 коммит произвёл GitHub
Родитель 499dc9bb7a
Коммит 1e18e83af6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
65 изменённых файлов: 2531 добавлений и 891 удалений

1
.github/actions/spell-check/expect.txt поставляемый
Просмотреть файл

@ -1861,6 +1861,7 @@ workarounds
WORKSPACESEDITOR
WORKSPACESLAUNCHER
WORKSPACESSNAPSHOTTOOL
WORKSPACESWINDOWARRANGER
wox
wparam
wpf

Просмотреть файл

@ -194,6 +194,7 @@
"PowerToys.WorkspacesSnapshotTool.exe",
"PowerToys.WorkspacesLauncher.exe",
"PowerToys.WorkspacesWindowArranger.exe",
"PowerToys.WorkspacesEditor.exe",
"PowerToys.WorkspacesEditor.dll",
"PowerToys.WorkspacesLauncherUI.exe",

Просмотреть файл

@ -619,6 +619,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\mod
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesWindowArranger", "src\modules\Workspaces\WorkspacesWindowArranger\WorkspacesWindowArranger.vcxproj", "{37D07516-4185-43A4-924F-3C7A5D95ECF6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -2719,6 +2721,18 @@ Global
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.ActiveCfg = Debug|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.Build.0 = Debug|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.ActiveCfg = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.Build.0 = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.ActiveCfg = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.Build.0 = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.ActiveCfg = Release|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.Build.0 = Release|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.ActiveCfg = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.Build.0 = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.ActiveCfg = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -2946,6 +2960,7 @@ Global
{3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

Просмотреть файл

@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 36> processesToTerminate = {
std::array<std::wstring_view, 37> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@ -1259,6 +1259,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.WorkspacesLauncher.exe",
L"PowerToys.WorkspacesLauncherUI.exe",
L"PowerToys.WorkspacesEditor.exe",
L"PowerToys.WorkspacesWindowArranger.exe",
L"PowerToys.exe",
};

Просмотреть файл

@ -72,6 +72,8 @@ struct LogSettings
inline const static std::string newLoggerName = "NewPlus";
inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher";
inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt";
inline const static std::string workspacesWindowArrangerLoggerName = "workspaces-window-arranger";
inline const static std::wstring workspacesWindowArrangerLogPath = L"workspaces-window-arranger-log.txt";
inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt";
inline const static int retention = 30;

Просмотреть файл

@ -1,150 +1,27 @@
#include "pch.h"
#include "AppLauncher.h"
#include <filesystem>
#include <shellapi.h>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <shellapi.h>
#include <ShellScalingApi.h>
#include <filesystem>
#include <workspaces-common/MonitorEnumerator.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/AppUtils.h>
#include <common/Display/dpi_aware.h>
#include <common/utils/winapi_error.h>
#include <LaunchingApp.h>
#include <LauncherUIHelper.h>
#include <RegistryUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Management::Deployment;
namespace FancyZones
namespace AppLauncher
{
inline bool allMonitorsHaveSameDpiScaling()
void UpdatePackagedApps(std::vector<WorkspacesData::WorkspacesProject::Application>& apps, const Utils::Apps::AppList& installedApps)
{
auto monitors = MonitorEnumerator::Enumerate();
if (monitors.size() < 2)
{
return true;
}
UINT firstMonitorDpiX;
UINT firstMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[0].first, MDT_EFFECTIVE_DPI, &firstMonitorDpiX, &firstMonitorDpiY))
{
return false;
}
for (int i = 1; i < monitors.size(); i++)
{
UINT iteratedMonitorDpiX;
UINT iteratedMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[i].first, MDT_EFFECTIVE_DPI, &iteratedMonitorDpiX, &iteratedMonitorDpiY) ||
iteratedMonitorDpiX != firstMonitorDpiX)
{
return false;
}
}
return true;
}
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
DPIAware::Convert(monitor, rect);
auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset);
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
}
inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (isMinimized)
{
placement.showCmd = SW_MINIMIZE;
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
// make sure window is moved to the correct monitor before maximize.
if (isMaximized)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
}
namespace
{
LaunchingApps Prepare(std::vector<WorkspacesData::WorkspacesProject::Application>& apps, const Utils::Apps::AppList& installedApps)
{
LaunchingApps launchedApps{};
launchedApps.reserve(apps.size());
for (auto& app : apps)
{
// Packaged apps have version in the path, it will be outdated after update.
@ -160,322 +37,173 @@ namespace
Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName);
}
}
launchedApps.push_back({ app, nullptr, L"waiting" });
}
return launchedApps;
}
bool AllWindowsFound(const LaunchingApps& launchedApps)
Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
{
return std::find_if(launchedApps.begin(), launchedApps.end(), [&](const LaunchingApp& val) {
return val.window == nullptr;
}) == launchedApps.end();
};
std::wstring dir = std::filesystem::path(appPath).parent_path();
bool AddOpenedWindows(LaunchingApps& launchedApps, const std::vector<HWND>& windows, const Utils::Apps::AppList& installedApps)
{
bool statusChanged = false;
for (HWND window : windows)
SHELLEXECUTEINFO sei = { 0 };
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
sei.lpDirectory = dir.c_str();
sei.nShow = SW_SHOWNORMAL;
if (!ShellExecuteEx(&sei))
{
auto installedAppData = Utils::Apps::GetApp(window, installedApps);
if (!installedAppData.has_value())
{
continue;
}
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
return Error(error);
}
auto insertionIter = launchedApps.end();
for (auto iter = launchedApps.begin(); iter != launchedApps.end(); ++iter)
return Ok(sei);
}
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
{
try
{
PackageManager packageManager;
for (const auto& package : packageManager.FindPackagesForUser({}))
{
if (iter->window == nullptr && installedAppData.value().name == iter->application.name)
if (package.Id().FullName() == packageFullName)
{
insertionIter = iter;
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
auto appEntries = getAppListEntriesOperation.get();
if (appEntries.Size() > 0)
{
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
bool launchResult = launchOperation.get();
return launchResult;
}
else
{
Logger::error(L"No app entries found for the package.");
launchErrors.push_back({ packageFullName, L"No app entries found for the package." });
}
}
// keep the window at the same position if it's already opened
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
UINT dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForMonitor(monitor, dpi);
float x = static_cast<float>(placement.rcNormalPosition.left);
float y = static_cast<float>(placement.rcNormalPosition.top);
float width = static_cast<float>(placement.rcNormalPosition.right - placement.rcNormalPosition.left);
float height = static_cast<float>(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top);
DPIAware::InverseConvert(monitor, x, y);
DPIAware::InverseConvert(monitor, width, height);
WorkspacesData::WorkspacesProject::Application::Position windowPosition{
.x = static_cast<int>(std::round(x)),
.y = static_cast<int>(std::round(y)),
.width = static_cast<int>(std::round(width)),
.height = static_cast<int>(std::round(height)),
};
if (iter->application.position == windowPosition)
{
Logger::debug(L"{} window already found at {} {}.", iter->application.name, iter->application.position.x, iter->application.position.y);
insertionIter = iter;
break;
}
}
if (insertionIter != launchedApps.end())
{
insertionIter->window = window;
insertionIter->state = L"launched";
statusChanged = true;
}
if (AllWindowsFound(launchedApps))
{
break;
}
}
return statusChanged;
}
}
catch (const hresult_error& ex)
{
Logger::error(L"Packaged app launching error: {}", ex.message());
launchErrors.push_back({ packageFullName, ex.message().c_str() });
}
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
SHELLEXECUTEINFO sei = { 0 };
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
sei.lpDirectory = dir.c_str();
sei.nShow = SW_SHOWNORMAL;
if (!ShellExecuteEx(&sei))
{
auto error = GetLastError();
Logger::error(L"Failed to launch process. {}", get_last_error_or_default(error));
launchErrors.push_back({ std::filesystem::path(appPath).filename(), get_last_error_or_default(error) });
return false;
}
return true;
}
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
{
try
bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors)
{
PackageManager packageManager;
for (const auto& package : packageManager.FindPackagesForUser({}))
{
if (package.Id().FullName() == packageFullName)
{
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
auto appEntries = getAppListEntriesOperation.get();
bool launched{ false };
if (appEntries.Size() > 0)
// packaged apps: check protocol in registry
// usage example: Settings with cmd args
if (!app.packageFullName.empty())
{
auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName);
if (!names.empty())
{
Logger::trace(L"Launching packaged by protocol with command line args {}", app.name);
std::wstring uriProtocolName = names[0];
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
auto res = LaunchApp(command, L"", app.isElevated);
if (res.isOk())
{
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
bool launchResult = launchOperation.get();
return launchResult;
launched = true;
}
else
{
Logger::error(L"No app entries found for the package.");
launchErrors.push_back({ packageFullName, L"No app entries found for the package." });
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
}
}
}
catch (const hresult_error& ex)
{
Logger::error(L"Packaged app launching error: {}", ex.message());
launchErrors.push_back({ packageFullName, ex.message().c_str() });
}
return false;
}
bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors)
{
bool launched{ false };
// packaged apps: check protocol in registry
// usage example: Settings with cmd args
if (!app.packageFullName.empty())
{
auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName);
if (!names.empty())
{
Logger::trace(L"Launching packaged by protocol with command line args {}", app.name);
std::wstring uriProtocolName = names[0];
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
launched = LaunchApp(command, L"", app.isElevated, launchErrors);
}
else
{
Logger::info(L"Uri protocol names not found for {}", app.packageFullName);
}
}
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
launched = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated, launchErrors);
}
// packaged apps: try launching by package full name
// doesn't work for elevated apps or apps with command line args
if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated)
{
Logger::trace(L"Launching packaged app {}", app.name);
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
}
if (!launched)
{
Logger::trace(L"Launching {} at {}", app.name, app.path);
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
Logger::error(L"File not found at {}", app.path);
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
return false;
}
launched = LaunchApp(app.path, app.commandLineArgs, app.isElevated, launchErrors);
}
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
return launched;
}
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors)
{
bool launchedSuccessfully{ true };
LauncherUIHelper uiHelper;
uiHelper.LaunchUI();
// Get the set of windows before launching the app
std::vector<HWND> windowsBefore = WindowEnumerator::Enumerate(WindowFilter::Filter);
auto installedApps = Utils::Apps::GetAppsList();
auto launchedApps = Prepare(project.apps, installedApps);
uiHelper.UpdateLaunchStatus(launchedApps);
// Launch apps
for (auto& app : launchedApps)
{
if (!app.window)
{
if (!Launch(app.application, launchErrors))
else
{
Logger::error(L"Failed to launch {}", app.application.name);
app.state = L"failed";
uiHelper.UpdateLaunchStatus(launchedApps);
launchedSuccessfully = false;
Logger::info(L"Uri protocol names not found for {}", app.packageFullName);
}
}
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
}
// packaged apps: try launching by package full name
// doesn't work for elevated apps or apps with command line args
if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated)
{
Logger::trace(L"Launching packaged app {}", app.name);
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
}
if (!launched)
{
Logger::trace(L"Launching {} at {}", app.name, app.path);
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
Logger::error(L"File not found at {}", app.path);
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
return false;
}
auto res = LaunchApp(app.path, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
}
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
return launched;
}
// Get newly opened windows after launching apps, keep retrying for 5 seconds
Logger::trace(L"Find new windows");
for (int attempt = 0; attempt < 50 && !AllWindowsFound(launchedApps); attempt++)
bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors)
{
std::vector<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> windowsDiff{};
std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(windowsBefore.begin(), windowsBefore.end(), window) == windowsBefore.end(); });
if (AddOpenedWindows(launchedApps, windowsDiff, installedApps))
bool launchedSuccessfully{ true };
auto installedApps = Utils::Apps::GetAppsList();
UpdatePackagedApps(project.apps, installedApps);
// Launch apps
for (auto& app : project.apps)
{
uiHelper.UpdateLaunchStatus(launchedApps);
if (!Launch(app, launchErrors))
{
Logger::error(L"Failed to launch {}", app.name);
launchingStatus.Update(app, LaunchingState::Failed);
launchedSuccessfully = false;
}
else
{
launchingStatus.Update(app, LaunchingState::Launched);
}
}
// check if all windows were found
if (AllWindowsFound(launchedApps))
{
Logger::trace(L"All windows found.");
break;
}
else
{
Logger::trace(L"Not all windows found, retry.");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return launchedSuccessfully;
}
// Check single-instance app windows
Logger::trace(L"Find single-instance app windows");
if (!AllWindowsFound(launchedApps))
{
if (AddOpenedWindows(launchedApps, WindowEnumerator::Enumerate(WindowFilter::Filter), installedApps))
{
uiHelper.UpdateLaunchStatus(launchedApps);
}
}
// Place windows
for (const auto& [app, window, status] : launchedApps)
{
if (window == nullptr)
{
Logger::warn(L"{} window not found.", app.name);
launchedSuccessfully = false;
continue;
}
auto snapMonitorIter = std::find_if(project.monitors.begin(), project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (snapMonitorIter == project.monitors.end())
{
Logger::error(L"No monitor saved for launching the app");
continue;
}
bool launchMinimized = app.isMinimized;
bool launchMaximized = app.isMaximized;
HMONITOR currentMonitor{};
UINT currentDpi = DPIAware::DEFAULT_DPI;
auto currentMonitorIter = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (currentMonitorIter != monitors.end())
{
currentMonitor = currentMonitorIter->monitor;
currentDpi = currentMonitorIter->dpi;
}
else
{
currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);
DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi);
launchMinimized = true;
launchMaximized = false;
}
RECT rect = app.position.toRect();
float mult = static_cast<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
else
{
Logger::error(L"Failed placing {}", app.name);
launchedSuccessfully = false;
}
}
return launchedSuccessfully;
}
}

Просмотреть файл

@ -1,7 +1,16 @@
#pragma once
#include <shellapi.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h>
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
namespace AppLauncher
{
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors);
Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated);
bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors);
}

Просмотреть файл

@ -0,0 +1,123 @@
#include "pch.h"
#include "Launcher.h"
#include <common/utils/json.h>
#include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/trace.h>
#include <AppLauncher.h>
Launcher::Launcher(const WorkspacesData::WorkspacesProject& project,
std::vector<WorkspacesData::WorkspacesProject>& workspaces,
InvokePoint invokePoint) :
m_project(project),
m_workspaces(workspaces),
m_invokePoint(invokePoint),
m_start(std::chrono::high_resolution_clock::now()),
m_uiHelper(std::make_unique<LauncherUIHelper>()),
m_windowArrangerHelper(std::make_unique<WindowArrangerHelper>(std::bind(&Launcher::handleWindowArrangerMessage, this, std::placeholders::_1))),
m_launchingStatus(m_project, std::bind(&LauncherUIHelper::UpdateLaunchStatus, m_uiHelper.get(), std::placeholders::_1))
{
m_uiHelper->LaunchUI();
m_uiHelper->UpdateLaunchStatus(m_launchingStatus.Get());
bool launchElevated = std::find_if(m_project.apps.begin(), m_project.apps.end(), [](const WorkspacesData::WorkspacesProject::Application& app) { return app.isElevated; }) != m_project.apps.end();
m_windowArrangerHelper->Launch(m_project.id, launchElevated, [&]() -> bool
{
if (m_launchingStatus.AllLaunchedAndMoved())
{
return false;
}
if (m_launchingStatus.AllLaunched())
{
static auto arrangerTimeDelay = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> timeDiff = currentTime - arrangerTimeDelay;
if (timeDiff.count() >= 5)
{
return false;
}
}
return true;
});
}
Launcher::~Launcher()
{
Logger::trace(L"Finalizing launch");
// update last-launched time
if (m_invokePoint != InvokePoint::LaunchAndEdit)
{
time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
m_project.lastLaunchedTime = launchedTime;
for (int i = 0; i < m_workspaces.size(); i++)
{
if (m_workspaces[i].id == m_project.id)
{
m_workspaces[i] = m_project;
break;
}
}
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(m_workspaces));
}
// telemetry
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - m_start;
Logger::trace(L"Launching time: {} s", duration.count());
auto monitors = MonitorUtils::IdentifyMonitors();
bool differentSetup = monitors.size() != m_project.monitors.size();
if (!differentSetup)
{
for (const auto& monitor : m_project.monitors)
{
auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; });
if (setup == monitors.end())
{
differentSetup = true;
break;
}
}
}
Trace::Workspaces::Launch(m_launchedSuccessfully, m_project, m_invokePoint, duration.count(), differentSetup, m_launchErrors);
}
void Launcher::Launch()
{
Logger::info(L"Launch Workspace {} : {}", m_project.name, m_project.id);
m_launchedSuccessfully = AppLauncher::Launch(m_project, m_launchingStatus, m_launchErrors);
}
void Launcher::handleWindowArrangerMessage(const std::wstring& msg)
{
if (msg == L"ready")
{
Launch();
}
else
{
try
{
auto data = WorkspacesData::AppLaunchInfoJSON::FromJson(json::JsonValue::Parse(msg).GetObjectW());
if (data.has_value())
{
m_launchingStatus.Update(data.value().application, data.value().state);
}
else
{
Logger::error(L"Failed to parse message from WorkspacesWindowArranger");
}
}
catch (const winrt::hresult_error&)
{
Logger::error(L"Failed to parse message from WorkspacesWindowArranger");
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
#pragma once
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <workspaces-common/InvokePoint.h>
#include <LauncherUIHelper.h>
#include <WindowArrangerHelper.h>
class Launcher
{
public:
Launcher(const WorkspacesData::WorkspacesProject& project, std::vector<WorkspacesData::WorkspacesProject>& workspaces, InvokePoint invokePoint);
~Launcher();
void Launch();
private:
WorkspacesData::WorkspacesProject m_project;
std::vector<WorkspacesData::WorkspacesProject>& m_workspaces;
const InvokePoint m_invokePoint;
const std::chrono::steady_clock::time_point m_start;
std::unique_ptr<LauncherUIHelper> m_uiHelper;
std::unique_ptr<WindowArrangerHelper> m_windowArrangerHelper;
LaunchingStatus m_launchingStatus;
bool m_launchedSuccessfully{};
std::vector<std::pair<std::wstring, std::wstring>> m_launchErrors{};
void handleWindowArrangerMessage(const std::wstring& msg);
};

Просмотреть файл

@ -7,12 +7,22 @@
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
#include <AppLauncher.h>
LauncherUIHelper::LauncherUIHelper() :
m_processId{},
m_ipcHelper(IPCHelperStrings::LauncherUIPipeName, IPCHelperStrings::UIPipeName, nullptr)
{
}
LauncherUIHelper::~LauncherUIHelper()
{
OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, uiProcessId);
Logger::info(L"Stopping WorkspacesLauncherUI with pid {}", m_processId);
HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId);
if (uiProcess)
{
bool res = TerminateProcess(uiProcess, 0);
@ -25,54 +35,39 @@ LauncherUIHelper::~LauncherUIHelper()
{
Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError()));
}
std::filesystem::remove(WorkspacesData::LaunchWorkspacesFile());
} }).wait();
}
void LauncherUIHelper::LaunchUI()
{
Logger::trace(L"Starting WorkspacesLauncherUI");
STARTUPINFO info = { sizeof(info) };
PROCESS_INFORMATION pi = { 0 };
TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring path = std::filesystem::path(buffer).parent_path();
path.append(L"\\PowerToys.WorkspacesLauncherUI.exe");
auto succeeded = CreateProcessW(path.c_str(), nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &info, &pi);
if (succeeded)
auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesLauncherUI.exe", L"", false);
if (res.isOk())
{
if (pi.hProcess)
{
uiProcessId = pi.dwProcessId;
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
auto value = res.value();
m_processId = GetProcessId(value.hProcess);
CloseHandle(value.hProcess);
Logger::info(L"WorkspacesLauncherUI started with pid {}", m_processId);
}
else
{
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
Logger::error(L"Failed to launch PowerToys.WorkspacesLauncherUI: {}", res.error());
}
}
void LauncherUIHelper::UpdateLaunchStatus(LaunchingApps launchedApps)
void LauncherUIHelper::UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const
{
WorkspacesData::AppLaunchData appData = WorkspacesData::AppLaunchData();
appData.appLaunchInfoList.reserve(launchedApps.size());
WorkspacesData::AppLaunchData appData;
appData.launcherProcessID = GetCurrentProcessId();
for (auto& app : launchedApps)
for (auto& [app, data] : launchedApps)
{
WorkspacesData::AppLaunchInfo appLaunchInfo = WorkspacesData::AppLaunchInfo();
appLaunchInfo.name = app.application.name;
appLaunchInfo.path = app.application.path;
appLaunchInfo.state = app.state;
appData.appLaunchInfoList.push_back(appLaunchInfo);
appData.appsStateList.insert({ app, { app, nullptr, data.state } });
}
json::to_file(WorkspacesData::LaunchWorkspacesFile(), WorkspacesData::AppLaunchDataJSON::ToJson(appData));
m_ipcHelper.send(WorkspacesData::AppLaunchDataJSON::ToJson(appData).ToString().c_str());
}

Просмотреть файл

@ -1,16 +1,18 @@
#pragma once
#include <LaunchingApp.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <WorkspacesLib/IPCHelper.h>
class LauncherUIHelper
{
public:
LauncherUIHelper() = default;
LauncherUIHelper();
~LauncherUIHelper();
void LaunchUI();
void UpdateLaunchStatus(LaunchingApps launchedApps);
void UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const;
private:
DWORD uiProcessId;
DWORD m_processId;
IPCHelper m_ipcHelper;
};

Просмотреть файл

@ -1,13 +0,0 @@
#pragma once
#include <Windows.h>
#include <WorkspacesLib/WorkspacesData.h>
struct LaunchingApp
{
WorkspacesData::WorkspacesProject::Application application;
HWND window;
std::wstring state;
};
using LaunchingApps = std::vector<LaunchingApp>;

Просмотреть файл

@ -0,0 +1,71 @@
#include "pch.h"
#include "WindowArrangerHelper.h"
#include <filesystem>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <AppLauncher.h>
WindowArrangerHelper::WindowArrangerHelper(std::function<void(const std::wstring&)> ipcCallback) :
m_processId{},
m_ipcHelper(IPCHelperStrings::LauncherArrangerPipeName, IPCHelperStrings::WindowArrangerPipeName, ipcCallback)
{
}
WindowArrangerHelper::~WindowArrangerHelper()
{
Logger::info(L"Stopping WorkspacesWindowArranger with pid {}", m_processId);
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId);
if (process)
{
bool res = TerminateProcess(process, 0);
if (!res)
{
Logger::error(L"Unable to terminate PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError()));
}
}
else
{
Logger::error(L"Unable to find PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError()));
}
}
void WindowArrangerHelper::Launch(const std::wstring& projectId, bool elevated, std::function<bool()> keepWaitingCallback)
{
Logger::trace(L"Starting WorkspacesWindowArranger");
TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring path = std::filesystem::path(buffer).parent_path();
auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesWindowArranger.exe", projectId, elevated);
if (res.isOk())
{
auto value = res.value();
m_processId = GetProcessId(value.hProcess);
Logger::info(L"WorkspacesWindowArranger started with pid {}", m_processId);
std::atomic_bool timeoutExpired = false;
m_threadExecutor.submit(OnThreadExecutor::task_t{
[&] {
HANDLE process = value.hProcess;
while (keepWaitingCallback())
{
WaitForSingleObject(process, 100);
}
Logger::trace(L"Finished waiting WorkspacesWindowArranger");
CloseHandle(process);
}}).wait();
timeoutExpired = true;
}
else
{
Logger::error(L"Failed to launch PowerToys.WorkspacesWindowArranger: {}", res.error());
}
}

Просмотреть файл

@ -0,0 +1,20 @@
#pragma once
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/utils/OnThreadExecutor.h>
class WindowArrangerHelper
{
public:
WindowArrangerHelper(std::function<void(const std::wstring&)> ipcCallback);
~WindowArrangerHelper();
void Launch(const std::wstring& projectId, bool elevated, std::function<bool()> keepWaitingCallback);
private:
DWORD m_processId;
IPCHelper m_ipcHelper;
OnThreadExecutor m_threadExecutor;
};

Просмотреть файл

@ -127,21 +127,23 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AppLauncher.cpp" />
<ClCompile Include="Launcher.cpp" />
<ClCompile Include="LauncherUIHelper.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="RegistryUtils.cpp" />
<ClCompile Include="WindowArrangerHelper.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="AppLauncher.h" />
<ClInclude Include="Launcher.h" />
<ClInclude Include="LauncherUIHelper.h" />
<ClInclude Include="LaunchingApp.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="RegistryUtils.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WindowArrangerHelper.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

Просмотреть файл

@ -27,13 +27,13 @@
<ClInclude Include="RegistryUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LauncherUIHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingApp.h">
<ClInclude Include="WindowArrangerHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Launcher.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
@ -53,6 +53,12 @@
<ClCompile Include="LauncherUIHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowArrangerHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Launcher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

Просмотреть файл

@ -1,16 +1,12 @@
#include "pch.h"
#include "pch.h"
#include <WorkspacesLib/WorkspacesData.h>
#include <WorkspacesLib/trace.h>
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/utils.h>
#include <AppLauncher.h>
#include <utils.h>
#include <Launcher.h>
#include <Generated Files/resource.h>
#include <workspaces-common/InvokePoint.h>
#include <workspaces-common/MonitorUtils.h>
#include <common/utils/elevation.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
@ -58,16 +54,16 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
std::string cmdLineStr(cmdline);
auto cmdArgs = split(cmdLineStr, " ");
if (cmdArgs.size() < 1)
std::wstring cmdLineStr{ GetCommandLineW() };
auto cmdArgs = split(cmdLineStr, L" ");
if (cmdArgs.size() < 2)
{
Logger::warn("Incorrect command line arguments");
MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
std::wstring id(cmdArgs[0].begin(), cmdArgs[0].end());
std::wstring id(cmdArgs[1].begin(), cmdArgs[1].end());
if (id.empty())
{
Logger::warn("Incorrect command line arguments: no workspace id");
@ -76,11 +72,11 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
}
InvokePoint invokePoint = InvokePoint::EditorButton;
if (cmdArgs.size() > 1)
if (cmdArgs.size() > 2)
{
try
{
invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[1]));
invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[2]));
}
catch (std::exception)
{
@ -95,79 +91,51 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (invokePoint == InvokePoint::LaunchAndEdit)
{
// check the temp file in case the project is just created and not saved to the workspaces.json yet
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile()))
auto file = WorkspacesData::TempWorkspacesFile();
auto res = JsonUtils::ReadSingleWorkspace(file);
if (res.isOk() && projectToLaunch.id == id)
{
try
projectToLaunch = res.getValue();
}
else
{
std::wstring formattedMessage{};
switch (res.error())
{
auto savedWorkspacesJson = json::from_file(WorkspacesData::TempWorkspacesFile());
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesProjectJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
if (savedWorkspaces.value().id == id)
{
projectToLaunch = savedWorkspaces.value();
}
}
else
{
Logger::critical("Incorrect Workspaces file");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
else
{
Logger::critical("Incorrect Workspaces file");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempWorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
case JsonUtils::WorkspacesFileError::FileReadingError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
break;
case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
}
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
if (projectToLaunch.id.empty())
{
try
auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{
auto savedWorkspacesJson = json::from_file(WorkspacesData::WorkspacesFile());
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
workspaces = savedWorkspaces.value();
}
else
{
Logger::critical("Incorrect Workspaces file");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
else
{
Logger::critical("Incorrect Workspaces file");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
workspaces = res.getValue();
}
catch (std::exception ex)
else
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::WorkspacesFile());
std::wstring formattedMessage{};
switch (res.error())
{
case JsonUtils::WorkspacesFileError::FileReadingError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
break;
case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
}
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
@ -175,7 +143,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (workspaces.empty())
{
Logger::warn("Workspaces file is empty");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), WorkspacesData::WorkspacesFile());
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), file);
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
@ -198,50 +166,9 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
return 1;
}
// launch apps
Logger::info(L"Launch Workspace {} : {}", projectToLaunch.name, projectToLaunch.id);
auto monitors = MonitorUtils::IdentifyMonitors();
std::vector<std::pair<std::wstring, std::wstring>> launchErrors{};
auto start = std::chrono::high_resolution_clock::now();
bool launchedSuccessfully = Launch(projectToLaunch, monitors, launchErrors);
// update last-launched time
if (invokePoint != InvokePoint::LaunchAndEdit)
{
time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
projectToLaunch.lastLaunchedTime = launchedTime;
for (int i = 0; i < workspaces.size(); i++)
{
if (workspaces[i].id == projectToLaunch.id)
{
workspaces[i] = projectToLaunch;
break;
}
}
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(workspaces));
}
// telemetry
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
Logger::trace(L"Launching time: {} s", duration.count());
bool differentSetup = monitors.size() != projectToLaunch.monitors.size();
if (!differentSetup)
{
for (const auto& monitor : projectToLaunch.monitors)
{
auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; });
if (setup == monitors.end())
{
differentSetup = true;
break;
}
}
}
Trace::Workspaces::Launch(launchedSuccessfully, projectToLaunch, invokePoint, duration.count(), differentSetup, launchErrors);
Launcher launcher(projectToLaunch, workspaces, invokePoint);
Logger::trace("Finished");
CoUninitialize();
return 0;
}

Просмотреть файл

@ -5,11 +5,9 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Forms.Design.Behavior;
using Common.UI;
using ManagedCommon;
using WorkspacesLauncherUI.Utils;
using PowerToys.Interop;
using WorkspacesLauncherUI.ViewModels;
namespace WorkspacesLauncherUI
@ -21,6 +19,9 @@ namespace WorkspacesLauncherUI
{
private static Mutex _instanceMutex;
// Create an instance of the IPC wrapper.
private static TwoWayPipeMessageIPCManaged ipcmanager;
private StatusWindow _mainWindow;
private MainViewModel _mainViewModel;
@ -29,21 +30,23 @@ namespace WorkspacesLauncherUI
private bool _isDisposed;
public static Action<string> IPCMessageReceivedCallback { get; set; }
public App()
{
}
private void OnStartup(object sender, StartupEventArgs e)
{
Logger.InitializeLogger("\\Workspaces\\Logs");
Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI");
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
const string appName = "Local\\PowerToys_Workspaces_Launcher_InstanceMutex";
const string appName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex";
bool createdNew;
_instanceMutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
Logger.LogWarning("Another instance of Workspaces Launcher is already running. Exiting this instance.");
Logger.LogWarning("Another instance of Workspaces Launcher UI is already running. Exiting this instance.");
_instanceMutex = null;
Shutdown(0);
return;
@ -56,6 +59,15 @@ namespace WorkspacesLauncherUI
return;
}
ipcmanager = new TwoWayPipeMessageIPCManaged("\\\\.\\pipe\\powertoys_workspaces_ui_", "\\\\.\\pipe\\powertoys_workspaces_launcher_ui_", (string message) =>
{
if (IPCMessageReceivedCallback != null && message.Length > 0)
{
IPCMessageReceivedCallback(message);
}
});
ipcmanager.Start();
ThemeManager = new ThemeManager(this);
if (_mainViewModel == null)
@ -97,6 +109,10 @@ namespace WorkspacesLauncherUI
if (disposing)
{
ThemeManager?.Dispose();
ipcmanager?.End();
ipcmanager?.Dispose();
_instanceMutex?.Dispose();
}

Просмотреть файл

@ -1,33 +1,16 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Workspaces.Data;
using WorkspacesLauncherUI.Utils;
using static WorkspacesLauncherUI.Data.AppLaunchData;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
internal sealed class AppLaunchData : WorkspacesEditorData<AppLaunchDataWrapper>
public class AppLaunchData : WorkspacesUIData<AppLaunchDataWrapper>
{
public static string File
{
get
{
return FolderUtils.DataFolder() + "\\launch-workspaces.json";
}
}
public struct AppLaunchDataWrapper
{
[JsonPropertyName("apps")]

Просмотреть файл

@ -1,32 +1,23 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Workspaces.Data;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
namespace WorkspacesLauncherUI.Data
{
public class AppLaunchInfoData : WorkspacesEditorData<AppLaunchInfoWrapper>
public class AppLaunchInfoData : WorkspacesUIData<AppLaunchInfoWrapper>
{
public struct AppLaunchInfoWrapper
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("path")]
public string Path { get; set; }
[JsonPropertyName("application")]
public ApplicationWrapper Application { get; set; }
[JsonPropertyName("state")]
public string State { get; set; }
public LaunchingState State { get; set; }
}
}
}

Просмотреть файл

@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@ -13,7 +12,7 @@ using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
public class AppLaunchInfosData : WorkspacesEditorData<AppLaunchInfoListWrapper>
public class AppLaunchInfosData : WorkspacesUIData<AppLaunchInfoListWrapper>
{
public struct AppLaunchInfoListWrapper
{

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
public struct ApplicationWrapper
{
public string Application { get; set; }
public string ApplicationPath { get; set; }
public string Title { get; set; }
public string PackageFullName { get; set; }
public string AppUserModelId { get; set; }
public string CommandLineArguments { get; set; }
public bool IsElevated { get; set; }
public bool CanLaunchElevated { get; set; }
public bool Minimized { get; set; }
public bool Maximized { get; set; }
public PositionWrapper Position { get; set; }
public int Monitor { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
// sync with WorkspacesLib : LaunchingStateEnum.h
public enum LaunchingState
{
Waiting = 0,
Launched,
LaunchedAndMoved,
Failed,
}
}

Просмотреть файл

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
public struct PositionWrapper
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public static bool operator ==(PositionWrapper left, PositionWrapper right)
{
return left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height;
}
public static bool operator !=(PositionWrapper left, PositionWrapper right)
{
return left.X != right.X || left.Y != right.Y || left.Width != right.Width || left.Height != right.Height;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
PositionWrapper pos = (PositionWrapper)obj;
return X == pos.X && Y == pos.Y && Width == pos.Width && Height == pos.Height;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
}

Просмотреть файл

@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils;
namespace Workspaces.Data
{
public class WorkspacesEditorData<T>
public class WorkspacesUIData<T>
{
protected JsonSerializerOptions JsonOptions
{
@ -22,10 +22,8 @@ namespace Workspaces.Data
}
}
public T Read(string file)
public T Deserialize(string data)
{
IOUtils ioUtils = new IOUtils();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, JsonOptions);
}

Просмотреть файл

@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;

Просмотреть файл

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@ -13,9 +13,9 @@ using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ManagedCommon;
using Windows.Management.Deployment;
using WorkspacesLauncherUI.Data;
namespace WorkspacesLauncherUI.Models
{
@ -28,9 +28,9 @@ namespace WorkspacesLauncherUI.Models
PropertyChanged?.Invoke(this, e);
}
public string AppPath { get; set; }
public ApplicationWrapper Application { get; set; }
public bool Loading => LaunchState == "waiting";
public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched;
private Icon _icon;
@ -51,12 +51,12 @@ namespace WorkspacesLauncherUI.Models
}
else
{
_icon = Icon.ExtractAssociatedIcon(AppPath);
_icon = Icon.ExtractAssociatedIcon(Application.ApplicationPath);
}
}
catch (Exception)
{
Logger.LogWarning($"Icon not found on app path: {AppPath}. Using default icon");
Logger.LogWarning($"Icon not found on app path: {Application.ApplicationPath}. Using default icon");
IsNotFound = true;
_icon = new Icon(@"images\DefaultIcon.ico");
}
@ -66,16 +66,22 @@ namespace WorkspacesLauncherUI.Models
}
}
public string Name { get; set; }
public string Name
{
get
{
return Application.Application;
}
}
public string LaunchState { get; set; }
public LaunchingState LaunchState { get; set; }
public string StateGlyph
{
get => LaunchState switch
{
"launched" => "\U0000F78C",
"failed" => "\U0000EF2C",
LaunchingState.LaunchedAndMoved => "\U0000F78C",
LaunchingState.Failed => "\U0000EF2C",
_ => "\U0000EF2C",
};
}
@ -84,8 +90,8 @@ namespace WorkspacesLauncherUI.Models
{
get => LaunchState switch
{
"launched" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
"failed" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
LaunchingState.LaunchedAndMoved => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
LaunchingState.Failed => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
_ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
};
}
@ -139,13 +145,13 @@ namespace WorkspacesLauncherUI.Models
{
if (_isPackagedApp == null)
{
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
if (!Application.ApplicationPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
{
_isPackagedApp = false;
}
else
{
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
string appPath = Application.ApplicationPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match match = packagedAppPathRegex.Match(appPath);
_isPackagedApp = match.Success;
@ -200,7 +206,7 @@ namespace WorkspacesLauncherUI.Models
}
catch (Exception e)
{
Logger.LogError($"Exception while drawing icon for app with path: {AppPath}. Exception message: {e.Message}");
Logger.LogError($"Exception while drawing icon for app with path: {Application.ApplicationPath}. Exception message: {e.Message}");
}
}

Просмотреть файл

@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace WorkspacesLauncherUI.Utils
{
public class FolderUtils
{
public static string Desktop()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public static string Temp()
{
return Path.GetTempPath();
}
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}
}

Просмотреть файл

@ -1,54 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace WorkspacesLauncherUI.Utils
{
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public IOUtils()
{
}
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
var attempts = 0;
while (attempts < 10)
{
try
{
using (Stream inputStream = _fileSystem.File.Open(fileName, FileMode.Open))
using (StreamReader reader = new StreamReader(inputStream))
{
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}
}

Просмотреть файл

@ -7,9 +7,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Abstractions;
using ManagedCommon;
using WorkspacesLauncherUI.Data;
using WorkspacesLauncherUI.Models;
@ -20,8 +17,6 @@ namespace WorkspacesLauncherUI.ViewModels
{
public ObservableCollection<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>();
private IFileSystemWatcher _watcher;
private System.Timers.Timer selfDestroyTimer;
private StatusWindow _snapshotWindow;
private int launcherProcessID;
private bool _exiting;
@ -36,60 +31,43 @@ namespace WorkspacesLauncherUI.ViewModels
public MainViewModel()
{
_exiting = false;
LoadAppLaunchInfos();
string fileName = Path.GetFileName(AppLaunchData.File);
_watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher("Workspaces", fileName, () => AppLaunchInfoStateChanged());
// receive IPC Message
App.IPCMessageReceivedCallback = (string msg) =>
{
try
{
AppLaunchData parser = new AppLaunchData();
AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Deserialize(msg);
HandleAppLaunchingState(appLaunchData);
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
};
}
private void AppLaunchInfoStateChanged()
{
LoadAppLaunchInfos();
}
private void LoadAppLaunchInfos()
private void HandleAppLaunchingState(AppLaunchData.AppLaunchDataWrapper appLaunchData)
{
if (_exiting)
{
return;
}
AppLaunchData parser = new AppLaunchData();
if (!File.Exists(AppLaunchData.File))
{
Logger.LogWarning($"AppLaunchInfosData storage file not found: {AppLaunchData.File}");
return;
}
AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Read(AppLaunchData.File);
launcherProcessID = appLaunchData.LauncherProcessID;
List<AppLaunching> appLaunchingList = new List<AppLaunching>();
bool allLaunched = true;
foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList)
{
appLaunchingList.Add(new AppLaunching()
{
Name = app.Name,
AppPath = app.Path,
Application = app.Application,
LaunchState = app.State,
});
if (app.State != "launched" && app.State != "failed")
{
allLaunched = false;
}
}
AppsListed = new ObservableCollection<AppLaunching>(appLaunchingList);
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppsListed)));
if (allLaunched)
{
selfDestroyTimer = new System.Timers.Timer();
selfDestroyTimer.Interval = 1000;
selfDestroyTimer.Elapsed += SelfDestroy;
selfDestroyTimer.Start();
}
}
private void SelfDestroy(object source, System.Timers.ElapsedEventArgs e)
@ -113,7 +91,6 @@ namespace WorkspacesLauncherUI.ViewModels
internal void CancelLaunch()
{
_exiting = true;
_watcher.Dispose();
Process proc = Process.GetProcessById(launcherProcessID);
proc.Kill();
}

Просмотреть файл

@ -0,0 +1,42 @@
#include "pch.h"
#include "IPCHelper.h"
#include <common/logger/logger.h>
IPCHelper::IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function<void(const std::wstring&)> messageCallback) :
callback(messageCallback)
{
HANDLE hToken = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
{
Logger::error("Failed to get process token");
return;
}
std::unique_lock lock{ ipcMutex };
ipc = make_unique<TwoWayPipeMessageIPC>(currentPipeName, receiverPipeName, std::bind(&IPCHelper::receive, this, std::placeholders::_1));
ipc->start(hToken);
}
IPCHelper::~IPCHelper()
{
std::unique_lock lock{ ipcMutex };
if (ipc)
{
ipc->end();
ipc = nullptr;
}
}
void IPCHelper::send(const std::wstring& message) const
{
ipc->send(message);
}
void IPCHelper::receive(const std::wstring& msg)
{
if (callback)
{
callback(msg);
}
}

Просмотреть файл

@ -0,0 +1,29 @@
#pragma once
#include <mutex>
#include <common/interop/two_way_pipe_message_ipc.h>
namespace IPCHelperStrings
{
static std::wstring LauncherUIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_ui_");
static std::wstring UIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_ui_");
static std::wstring LauncherArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_arranger_");
static std::wstring WindowArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_window_arranger_");
}
class IPCHelper
{
public:
IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function<void(const std::wstring&)> messageCallback);
~IPCHelper();
void send(const std::wstring& message) const;
private:
void receive(const std::wstring& msg);
std::unique_ptr<TwoWayPipeMessageIPC> ipc;
std::mutex ipcMutex;
std::function<void(const std::wstring&)> callback;
};

Просмотреть файл

@ -0,0 +1,106 @@
#include "pch.h"
#include "JsonUtils.h"
#include <filesystem>
#include <common/logger/logger.h>
namespace JsonUtils
{
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName)
{
if (std::filesystem::exists(fileName))
{
try
{
auto tempWorkspacesJson = json::from_file(fileName);
if (tempWorkspacesJson.has_value())
{
auto tempWorkspace = WorkspacesData::WorkspacesProjectJSON::FromJson(tempWorkspacesJson.value());
if (tempWorkspace.has_value())
{
return Ok(tempWorkspace.value());
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
return Error(WorkspacesFileError::FileReadingError);
}
}
return Ok(WorkspacesData::WorkspacesProject{});
}
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName)
{
try
{
auto savedWorkspacesJson = json::from_file(fileName);
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
return Ok(savedWorkspaces.value());
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
return Error(WorkspacesFileError::FileReadingError);
}
}
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
return false;
}
return true;
}
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
return false;
}
return true;
}
}

Просмотреть файл

@ -0,0 +1,19 @@
#pragma once
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h>
namespace JsonUtils
{
enum class WorkspacesFileError
{
FileReadingError,
IncorrectFileError,
};
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName);
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName);
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects);
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project);
}

Просмотреть файл

@ -0,0 +1,10 @@
#pragma once
// sync with WorkspacesLauncherUI : Data : LaunchingState.cs
enum class LaunchingState
{
Waiting = 0,
Launched,
LaunchedAndMoved,
Failed
};

Просмотреть файл

@ -0,0 +1,66 @@
#include "pch.h"
#include "LaunchingStatus.h"
#include <common/logger/logger.h>
LaunchingStatus::LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> updateCallback) :
m_updateCallback(updateCallback)
{
std::unique_lock lock(m_mutex);
for (const auto& app : project.apps)
{
m_appsState.insert({ app, { app, nullptr, LaunchingState::Waiting } });
}
}
const WorkspacesData::LaunchingAppStateMap& LaunchingStatus::Get() noexcept
{
std::shared_lock lock(m_mutex);
return m_appsState;
}
bool LaunchingStatus::AllLaunchedAndMoved() noexcept
{
std::shared_lock lock(m_mutex);
for (const auto& [app, data] : m_appsState)
{
if (data.state != LaunchingState::Failed && data.state != LaunchingState::LaunchedAndMoved)
{
Logger::debug(data.state);
return false;
}
}
return true;
}
bool LaunchingStatus::AllLaunched() noexcept
{
std::shared_lock lock(m_mutex);
for (const auto& [app, data] : m_appsState)
{
if (data.state == LaunchingState::Waiting)
{
return false;
}
}
return true;
}
void LaunchingStatus::Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state)
{
std::unique_lock lock(m_mutex);
if (!m_appsState.contains(app))
{
Logger::error(L"Error updating state: app {} is not tracked in the project", app.name);
return;
}
m_appsState[app].state = state;
if (m_updateCallback)
{
m_updateCallback(m_appsState);
}
}

Просмотреть файл

@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <shared_mutex>
#include <WorkspacesLib/WorkspacesData.h>
class LaunchingStatus
{
public:
LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> updateCallback);
~LaunchingStatus() = default;
bool AllLaunchedAndMoved() noexcept;
bool AllLaunched() noexcept;
const WorkspacesData::LaunchingAppStateMap& Get() noexcept;
void Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state);
private:
WorkspacesData::LaunchingAppStateMap m_appsState;
std::function<void(const WorkspacesData::LaunchingAppStateMap&)> m_updateCallback;
std::shared_mutex m_mutex;
};

Просмотреть файл

@ -0,0 +1,53 @@
#pragma once
#include <variant>
template<typename T>
class Ok
{
public:
explicit constexpr Ok(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename T>
class Error
{
public:
explicit constexpr Error(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename OkT, typename ErrT>
class Result
{
public:
using VariantT = std::variant<Ok<OkT>, Error<ErrT>>;
constexpr Result(Ok<OkT> value) :
variant(std::move(value))
{}
constexpr Result(Error<ErrT> value) :
variant(std::move(value))
{}
constexpr bool isOk() const { return std::holds_alternative<Ok<OkT>>(variant); }
constexpr bool isError() const { return std::holds_alternative<Error<ErrT>>(variant); }
constexpr OkT value() const { return std::get<Ok<OkT>>(variant).value; }
constexpr ErrT error() const { return std::get<Error<ErrT>>(variant).value; }
constexpr OkT&& getValue() { return std::get<Ok<OkT>>(variant).get(); }
constexpr ErrT&& getError() { return std::get<Error<ErrT>>(variant).get(); }
VariantT variant;
};

Просмотреть файл

@ -21,12 +21,6 @@ namespace WorkspacesData
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return settingsFolderPath + L"\\temp-workspaces.json";
}
std::wstring LaunchWorkspacesFile()
{
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return settingsFolderPath + L"\\launch-workspaces.json";
}
RECT WorkspacesProject::Application::Position::toRect() const noexcept
{
@ -420,19 +414,40 @@ namespace WorkspacesData
{
namespace NonLocalizable
{
const static wchar_t* NameID = L"name";
const static wchar_t* PathID = L"path";
const static wchar_t* ApplicationID = L"application";
const static wchar_t* StateID = L"state";
}
json::JsonObject ToJson(const AppLaunchInfo& data)
json::JsonObject ToJson(const LaunchingAppState& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::NameID, json::value(data.name));
json.SetNamedValue(NonLocalizable::PathID, json::value(data.path));
json.SetNamedValue(NonLocalizable::StateID, json::value(data.state));
json.SetNamedValue(NonLocalizable::ApplicationID, WorkspacesProjectJSON::ApplicationJSON::ToJson(data.application));
json.SetNamedValue(NonLocalizable::StateID, json::value(static_cast<int>(data.state)));
return json;
}
std::optional<LaunchingAppState> FromJson(const json::JsonObject& json)
{
LaunchingAppState result{};
try
{
auto app = WorkspacesProjectJSON::ApplicationJSON::FromJson(json.GetNamedObject(NonLocalizable::ApplicationID));
if (!app.has_value())
{
return std::nullopt;
}
result.application = app.value();
result.state = static_cast<LaunchingState>(json.GetNamedNumber(NonLocalizable::StateID));
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace AppLaunchInfoListJSON
@ -442,18 +457,46 @@ namespace WorkspacesData
const static wchar_t* AppLaunchInfoID = L"appLaunchInfos";
}
json::JsonObject ToJson(const std::vector<AppLaunchInfo>& data)
json::JsonObject ToJson(const LaunchingAppStateMap& data)
{
json::JsonObject json{};
json::JsonArray appLaunchInfoArray{};
for (const auto& appLaunchInfo : data)
{
appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo));
appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo.second));
}
json.SetNamedValue(NonLocalizable::AppLaunchInfoID, appLaunchInfoArray);
return json;
}
std::optional<LaunchingAppStateMap> FromJson(const json::JsonObject& json)
{
LaunchingAppStateMap result{};
try
{
auto array = json.GetNamedArray(NonLocalizable::AppLaunchInfoID);
for (uint32_t i = 0; i < array.Size(); ++i)
{
auto obj = AppLaunchInfoJSON::FromJson(array.GetObjectAt(i));
if (obj.has_value())
{
result.insert({ obj.value().application, obj.value() });
}
else
{
return std::nullopt;
}
}
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace AppLaunchDataJSON
@ -467,7 +510,7 @@ namespace WorkspacesData
json::JsonObject ToJson(const AppLaunchData& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appLaunchInfoList));
json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appsStateList));
json.SetNamedValue(NonLocalizable::ProcessID, json::value(data.launcherProcessID));
return json;
}

Просмотреть файл

@ -2,11 +2,12 @@
#include <common/utils/json.h>
#include <WorkspacesLib/LaunchingStateEnum.h>
namespace WorkspacesData
{
std::wstring WorkspacesFile();
std::wstring TempWorkspacesFile();
std::wstring LaunchWorkspacesFile();
struct WorkspacesProject
{
@ -21,10 +22,7 @@ namespace WorkspacesData
RECT toRect() const noexcept;
inline bool operator==(const Position& other) const noexcept
{
return x == other.x && y == other.y && width == other.width && height == other.height;
}
auto operator<=>(const Position&) const = default;
};
std::wstring name;
@ -39,6 +37,8 @@ namespace WorkspacesData
bool isMaximized{};
Position position{};
unsigned int monitor{};
auto operator<=>(const Application&) const = default;
};
struct Monitor
@ -80,34 +80,22 @@ namespace WorkspacesData
std::vector<WorkspacesProject> projects;
};
struct AppLaunchInfo
struct LaunchingAppState
{
std::wstring name;
std::wstring path;
std::wstring state;
WorkspacesData::WorkspacesProject::Application application;
HWND window{};
LaunchingState state { LaunchingState::Waiting };
};
namespace AppLaunchInfoJSON
{
json::JsonObject ToJson(const AppLaunchInfo& data);
}
namespace AppLaunchInfoListJSON
{
json::JsonObject ToJson(const std::vector<AppLaunchInfo>& data);
}
using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;
using LaunchingAppStateList = std::vector<std::pair<WorkspacesData::WorkspacesProject::Application, LaunchingState>>;
struct AppLaunchData
{
std::vector<AppLaunchInfo> appLaunchInfoList;
LaunchingAppStateMap appsStateList;
int launcherProcessID = 0;
};
namespace AppLaunchDataJSON
{
json::JsonObject ToJson(const AppLaunchData& data);
}
namespace WorkspacesProjectJSON
{
namespace ApplicationJSON
@ -143,4 +131,22 @@ namespace WorkspacesData
json::JsonObject ToJson(const std::vector<WorkspacesProject>& data);
std::optional<std::vector<WorkspacesProject>> FromJson(const json::JsonObject& json);
}
namespace AppLaunchInfoJSON
{
json::JsonObject ToJson(const LaunchingAppState& data);
std::optional<LaunchingAppState> FromJson(const json::JsonObject& json);
}
namespace AppLaunchInfoListJSON
{
json::JsonObject ToJson(const LaunchingAppStateMap& data);
std::optional<LaunchingAppStateMap> FromJson(const json::JsonObject& json);
}
namespace AppLaunchDataJSON
{
json::JsonObject ToJson(const AppLaunchData& data);
}
};

Просмотреть файл

@ -33,19 +33,32 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="AppUtils.h" />
<ClInclude Include="IPCHelper.h" />
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="LaunchingStateEnum.h" />
<ClInclude Include="LaunchingStatus.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WorkspacesData.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="AppUtils.cpp" />
<ClCompile Include="IPCHelper.cpp" />
<ClCompile Include="JsonUtils.cpp" />
<ClCompile Include="LaunchingStatus.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj">
<Project>{f055103b-f80b-4d0c-bf48-057c55620033}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>

Просмотреть файл

@ -23,6 +23,24 @@
<ClInclude Include="AppUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Result.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IPCHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStateEnum.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStatus.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@ -37,6 +55,18 @@
<ClCompile Include="AppUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="JsonUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="IPCHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LaunchingStatus.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

Просмотреть файл

@ -0,0 +1,469 @@
#include "pch.h"
#include <common/interop/two_way_pipe_message_ipc_impl.h>
#include <iterator>
constexpr DWORD BUFSIZE = 1024;
TwoWayPipeMessageIPC::TwoWayPipeMessageIPC(
std::wstring _input_pipe_name,
std::wstring _output_pipe_name,
callback_function p_func) :
impl(new TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl(
_input_pipe_name,
_output_pipe_name,
p_func))
{
}
TwoWayPipeMessageIPC::~TwoWayPipeMessageIPC()
{
delete impl;
}
void TwoWayPipeMessageIPC::send(std::wstring msg)
{
impl->send(msg);
}
void TwoWayPipeMessageIPC::start(HANDLE _restricted_pipe_token)
{
impl->start(_restricted_pipe_token);
}
void TwoWayPipeMessageIPC::end()
{
impl->end();
}
TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::TwoWayPipeMessageIPCImpl(
std::wstring _input_pipe_name,
std::wstring _output_pipe_name,
callback_function p_func)
{
input_pipe_name = _input_pipe_name;
output_pipe_name = _output_pipe_name;
dispatch_inc_message_function = p_func;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send(std::wstring msg)
{
output_queue.queue_message(msg);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start(HANDLE _restricted_pipe_token)
{
output_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_output_queue_thread, this);
input_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_input_queue_thread, this);
input_pipe_thread = std::thread(&TwoWayPipeMessageIPCImpl::start_named_pipe_server, this, _restricted_pipe_token);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::end()
{
closed = true;
input_queue.interrupt();
input_queue_thread.join();
output_queue.interrupt();
output_queue_thread.join();
pipe_connect_handle_mutex.lock();
if (current_connect_pipe_handle != NULL)
{
//Cancels the Pipe currently waiting for a connection.
CancelIoEx(current_connect_pipe_handle, NULL);
}
pipe_connect_handle_mutex.unlock();
input_pipe_thread.join();
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send_pipe_message(std::wstring message)
{
// Adapted from https://learn.microsoft.com/windows/win32/ipc/named-pipe-client
HANDLE output_pipe_handle;
const wchar_t* message_send = message.c_str();
BOOL fSuccess = FALSE;
DWORD cbToWrite, cbWritten, dwMode;
const wchar_t* lpszPipename = output_pipe_name.c_str();
// Try to open a named pipe; wait for it, if necessary.
while (1)
{
output_pipe_handle = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// Break if the pipe handle is valid.
if (output_pipe_handle != INVALID_HANDLE_VALUE)
break;
// Exit if an error other than ERROR_PIPE_BUSY occurs.
DWORD curr_error = 0;
if ((curr_error = GetLastError()) != ERROR_PIPE_BUSY)
{
return;
}
// All pipe instances are busy, so wait for 20 seconds.
if (!WaitNamedPipe(lpszPipename, 20000))
{
return;
}
}
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
output_pipe_handle, // pipe handle
&dwMode, // new pipe mode
NULL, // don't set maximum bytes
NULL); // don't set maximum time
if (!fSuccess)
{
return;
}
// Send a message to the pipe server.
cbToWrite = (lstrlen(message_send)) * sizeof(WCHAR); // no need to send final '\0'. Pipe is in message mode.
fSuccess = WriteFile(
output_pipe_handle, // pipe handle
message_send, // message
cbToWrite, // message length
&cbWritten, // bytes written
NULL); // not overlapped
if (!fSuccess)
{
return;
}
CloseHandle(output_pipe_handle);
return;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_output_queue_thread()
{
while (!closed)
{
std::wstring message = output_queue.pop_message();
if (message.length() == 0)
{
break;
}
send_pipe_message(message);
}
}
BOOL TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::GetLogonSID(HANDLE hToken, PSID* ppsid)
{
// From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85)
BOOL bSuccess = FALSE;
DWORD dwIndex;
DWORD dwLength = 0;
PTOKEN_GROUPS ptg = NULL;
// Verify the parameter passed in is not NULL.
if (NULL == ppsid)
goto Cleanup;
// Get required buffer size and allocate the TOKEN_GROUPS buffer.
if (!GetTokenInformation(
hToken, // handle to the access token
TokenGroups, // get information about the token's groups
static_cast<LPVOID>(ptg), // pointer to TOKEN_GROUPS buffer
0, // size of buffer
&dwLength // receives required buffer size
))
{
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
goto Cleanup;
ptg = static_cast<PTOKEN_GROUPS>(HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
dwLength));
if (ptg == NULL)
goto Cleanup;
}
// Get the token group information from the access token.
if (!GetTokenInformation(
hToken, // handle to the access token
TokenGroups, // get information about the token's groups
static_cast<LPVOID>(ptg), // pointer to TOKEN_GROUPS buffer
dwLength, // size of buffer
&dwLength // receives required buffer size
))
{
goto Cleanup;
}
// Loop through the groups to find the logon SID.
for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++)
if ((ptg->Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID)
{
// Found the logon SID; make a copy of it.
dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid);
*ppsid = static_cast<PSID>(HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
dwLength));
if (*ppsid == NULL)
goto Cleanup;
if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid))
{
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(*ppsid));
goto Cleanup;
}
break;
}
bSuccess = TRUE;
Cleanup:
// Free the buffer for the token groups.
if (ptg != NULL)
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(ptg));
return bSuccess;
}
VOID TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::FreeLogonSID(PSID* ppsid)
{
// From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85)
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(*ppsid));
}
int TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::change_pipe_security_allow_restricted_token(HANDLE handle, HANDLE token)
{
PACL old_dacl, new_dacl;
PSECURITY_DESCRIPTOR sd;
EXPLICIT_ACCESS ea;
PSID user_restricted;
int error;
if (!GetLogonSID(token, &user_restricted))
{
error = 5; // No access error.
goto Ldone;
}
if (GetSecurityInfo(handle,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
NULL,
NULL,
&old_dacl,
NULL,
&sd))
{
error = GetLastError();
goto Lclean_sid;
}
memset(&ea, 0, sizeof(EXPLICIT_ACCESS));
ea.grfAccessPermissions |= GENERIC_READ | FILE_WRITE_ATTRIBUTES;
ea.grfAccessPermissions |= GENERIC_WRITE | FILE_READ_ATTRIBUTES;
ea.grfAccessPermissions |= SYNCHRONIZE;
ea.grfAccessMode = SET_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
ea.Trustee.ptstrName = static_cast<LPTSTR>(user_restricted);
if (SetEntriesInAcl(1, &ea, old_dacl, &new_dacl))
{
error = GetLastError();
goto Lclean_sd;
}
if (SetSecurityInfo(handle,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
NULL,
NULL,
new_dacl,
NULL))
{
error = GetLastError();
goto Lclean_dacl;
}
error = 0;
Lclean_dacl:
LocalFree(static_cast<HLOCAL>(new_dacl));
Lclean_sd:
LocalFree(static_cast<HLOCAL>(sd));
Lclean_sid:
FreeLogonSID(&user_restricted);
Ldone:
return error;
}
HANDLE TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::create_medium_integrity_token()
{
HANDLE restricted_token_handle;
SAFER_LEVEL_HANDLE level_handle = NULL;
DWORD sid_size = SECURITY_MAX_SID_SIZE;
BYTE medium_sid[SECURITY_MAX_SID_SIZE];
if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &level_handle, NULL))
{
return NULL;
}
if (!SaferComputeTokenFromLevel(level_handle, NULL, &restricted_token_handle, 0, NULL))
{
SaferCloseLevel(level_handle);
return NULL;
}
SaferCloseLevel(level_handle);
if (!CreateWellKnownSid(WinMediumLabelSid, nullptr, medium_sid, &sid_size))
{
CloseHandle(restricted_token_handle);
return NULL;
}
TOKEN_MANDATORY_LABEL integrity_level = { 0 };
integrity_level.Label.Attributes = SE_GROUP_INTEGRITY;
integrity_level.Label.Sid = reinterpret_cast<PSID>(medium_sid);
if (!SetTokenInformation(restricted_token_handle, TokenIntegrityLevel, &integrity_level, sizeof(integrity_level)))
{
CloseHandle(restricted_token_handle);
return NULL;
}
return restricted_token_handle;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::handle_pipe_connection(HANDLE input_pipe_handle)
{
if (!input_pipe_handle)
{
return;
}
constexpr DWORD readBlockBytes = BUFSIZE;
std::wstring message;
size_t iBlock = 0;
message.reserve(BUFSIZE);
bool ok;
do
{
constexpr size_t charsPerBlock = readBlockBytes / sizeof(message[0]);
message.resize(message.size() + charsPerBlock);
DWORD bytesRead = 0;
ok = ReadFile(
input_pipe_handle,
// read the message directly into the string block by block simultaneously resizing it
message.data() + iBlock * charsPerBlock,
readBlockBytes,
&bytesRead,
nullptr);
if (!ok && GetLastError() != ERROR_MORE_DATA)
{
break;
}
iBlock++;
} while (!ok);
// trim the message's buffer
const auto nullCharPos = message.find_last_not_of(L'\0');
if (nullCharPos != std::wstring::npos)
{
message.resize(nullCharPos + 1);
}
input_queue.queue_message(std::move(message));
// Flush the pipe to allow the client to read the pipe's contents
// before disconnecting. Then disconnect the pipe, and close the
// handle to this pipe instance.
FlushFileBuffers(input_pipe_handle);
DisconnectNamedPipe(input_pipe_handle);
CloseHandle(input_pipe_handle);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start_named_pipe_server(HANDLE token)
{
// Adapted from https://learn.microsoft.com/windows/win32/ipc/multithreaded-pipe-server
const wchar_t* pipe_name = input_pipe_name.c_str();
BOOL connected = FALSE;
HANDLE connect_pipe_handle = INVALID_HANDLE_VALUE;
while (!closed)
{
{
std::unique_lock lock(pipe_connect_handle_mutex);
connect_pipe_handle = CreateNamedPipe(
pipe_name,
PIPE_ACCESS_DUPLEX |
WRITE_DAC,
PIPE_TYPE_MESSAGE |
PIPE_READMODE_MESSAGE |
PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFSIZE,
BUFSIZE,
0,
NULL);
if (connect_pipe_handle == INVALID_HANDLE_VALUE)
{
return;
}
if (token != NULL)
{
change_pipe_security_allow_restricted_token(connect_pipe_handle, token);
}
current_connect_pipe_handle = connect_pipe_handle;
}
connected = ConnectNamedPipe(connect_pipe_handle, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
{
std::unique_lock lock(pipe_connect_handle_mutex);
current_connect_pipe_handle = NULL;
}
if (connected)
{
std::thread(&TwoWayPipeMessageIPCImpl::handle_pipe_connection, this, connect_pipe_handle).detach();
}
else
{
// Client could not connect.
CloseHandle(connect_pipe_handle);
}
}
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_input_queue_thread()
{
while (!closed)
{
outgoing_message = L"";
std::wstring message = input_queue.pop_message();
if (message.length() == 0)
{
break;
}
// Check if callback method exists first before trying to call it.
// otherwise just store the response message in a variable.
if (dispatch_inc_message_function != nullptr)
{
dispatch_inc_message_function(message);
}
outgoing_message = message;
}
}

Просмотреть файл

@ -3,12 +3,12 @@
#include <vector>
#include <string>
std::vector<std::string> split(std::string s, const std::string& delimiter)
std::vector<std::wstring> split(std::wstring s, const std::wstring& delimiter)
{
std::vector<std::string> tokens;
std::vector<std::wstring> tokens;
size_t pos = 0;
std::string token;
while ((pos = s.find(delimiter)) != std::string::npos)
std::wstring token;
while ((pos = s.find(delimiter)) != std::wstring::npos)
{
token = s.substr(0, pos);
tokens.push_back(token);

Просмотреть файл

@ -20,6 +20,7 @@
// Non-localizable
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
const std::wstring workspacesWindowArrangerPath = L"PowerToys.WorkspacesWindowArranger.exe";
const std::wstring workspacesSnapshotToolPath = L"PowerToys.WorkspacesSnapshotTool.exe";
const std::wstring workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe";

Просмотреть файл

@ -1,57 +0,0 @@
#pragma once
#include <vector>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/logger/logger.h>
namespace WorkspacesJsonUtils
{
inline std::vector<WorkspacesData::WorkspacesProject> Read(const std::wstring& fileName)
{
std::vector<WorkspacesData::WorkspacesProject> projects{};
try
{
auto savedProjectsJson = json::from_file(fileName);
if (savedProjectsJson.has_value())
{
auto savedProjects = WorkspacesData::WorkspacesListJSON::FromJson(savedProjectsJson.value());
if (savedProjects.has_value())
{
projects = savedProjects.value();
}
}
}
catch (std::exception ex)
{
Logger::error("Error reading workspaces file. {}", ex.what());
}
return projects;
}
inline void Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
}
}
inline void Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
}
}
}

Просмотреть файл

@ -133,7 +133,6 @@
<ClCompile Include="SnapshotUtils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.base.h" />
<ClInclude Include="SnapshotUtils.h" />

Просмотреть файл

@ -18,9 +18,6 @@
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SnapshotUtils.h">
<Filter>Header Files</Filter>
</ClInclude>

Просмотреть файл

@ -5,21 +5,21 @@
#include <workspaces-common/GuidUtils.h>
#include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <JsonUtils.h>
#include <SnapshotUtils.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
const std::wstring moduleName = L"Workspaces\\ProjectsSnapshotTool";
const std::wstring moduleName = L"Workspaces\\WorkspacesSnapshotTool";
const std::wstring internalPath = L"";
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cmdShow)
{
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesLauncherLoggerName);
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesSnapshotToolLoggerName);
InitUnhandledExceptionHandler();
if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
@ -46,14 +46,6 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
return -1;
}
std::wstring fileName = WorkspacesData::WorkspacesFile();
std::string cmdLineStr(cmdLine);
if (!cmdLineStr.empty())
{
std::wstring fileNameParam(cmdLineStr.begin(), cmdLineStr.end());
fileName = fileNameParam;
}
// create new project
time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
WorkspacesData::WorkspacesProject project{ .id = CreateGuidString(), .creationTime = creationTime };
@ -75,7 +67,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
return monitorNumber;
});
WorkspacesJsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project);
JsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project);
Logger::trace(L"WorkspacesProject {}:{} created", project.name, project.id);
CoUninitialize();

Просмотреть файл

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

Просмотреть файл

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

Просмотреть файл

@ -0,0 +1,257 @@
#include "pch.h"
#include "WindowArranger.h"
#include <common/logger/logger.h>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <workspaces-common/MonitorUtils.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <workspaces-common/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
namespace FancyZones
{
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
DPIAware::Convert(monitor, rect);
auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset);
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
}
inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (isMinimized)
{
placement.showCmd = SW_MINIMIZE;
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
// make sure window is moved to the correct monitor before maximize.
if (isMaximized)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
}
WindowArranger::WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper) :
m_project(project),
m_windowsBefore(WindowEnumerator::Enumerate(WindowFilter::Filter)),
m_monitors(MonitorUtils::IdentifyMonitors()),
m_installedApps(Utils::Apps::GetAppsList()),
//m_windowCreationHandler(std::bind(&WindowArranger::onWindowCreated, this, std::placeholders::_1)),
m_ipcHelper(ipcHelper)
{
for (auto& app : project.apps)
{
m_launchingApps.insert({ app, { app, nullptr } });
}
m_ipcHelper.send(L"ready");
for (int attempt = 0; attempt < 50 && !allWindowsFound(); attempt++)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::vector<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> windowsDiff{};
std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(m_windowsBefore.begin(), m_windowsBefore.end(), window) == m_windowsBefore.end(); });
for (HWND window : windowsDiff)
{
processWindow(window);
}
}
bool allFound = allWindowsFound();
Logger::info(L"Finished moving new windows, all windows found: {}", allFound);
if (!allFound)
{
std::vector<HWND> allWindows = WindowEnumerator::Enumerate(WindowFilter::Filter);
for (HWND window : allWindows)
{
processWindow(window);
}
}
}
//void WindowArranger::onWindowCreated(HWND window)
//{
// if (!WindowFilter::Filter(window))
// {
// return;
// }
//
// processWindow(window);
//}
void WindowArranger::processWindow(HWND window)
{
// check if this window is already handled
auto windowIter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val) { return val.second.window == window; });
if (windowIter != m_launchingApps.end())
{
return;
}
RECT rect = WindowUtils::GetWindowRect(window);
if (rect.right - rect.left <= 0 || rect.bottom - rect.top <= 0)
{
return;
}
std::wstring title = WindowUtils::GetWindowTitle(window);
if (title.empty())
{
return;
}
std::wstring processPath = get_process_path(window);
if (processPath.empty())
{
return;
}
auto data = Utils::Apps::GetApp(processPath, m_installedApps);
if (!data.has_value() || data->name.empty())
{
return;
}
auto iter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val)
{ return val.second.state == LaunchingState::Waiting && val.first.name == data.value().name; });
if (iter == m_launchingApps.end())
{
Logger::info(L"A window of {} is not in the project", processPath);
return;
}
Logger::debug(L"Move {}", title);
iter->second.window = window;
if (moveWindow(window, iter->first))
{
iter->second.state = LaunchingState::LaunchedAndMoved;
}
else
{
iter->second.state = LaunchingState::Failed;
}
m_ipcHelper.send(WorkspacesData::AppLaunchInfoJSON::ToJson({iter->first, nullptr, iter->second.state}).ToString().c_str());
}
bool WindowArranger::moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app)
{
auto snapMonitorIter = std::find_if(m_project.monitors.begin(), m_project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (snapMonitorIter == m_project.monitors.end())
{
Logger::error(L"No monitor saved for launching the app");
return false;
}
bool launchMinimized = app.isMinimized;
bool launchMaximized = app.isMaximized;
HMONITOR currentMonitor{};
UINT currentDpi = DPIAware::DEFAULT_DPI;
auto currentMonitorIter = std::find_if(m_monitors.begin(), m_monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (currentMonitorIter != m_monitors.end())
{
currentMonitor = currentMonitorIter->monitor;
currentDpi = currentMonitorIter->dpi;
}
else
{
currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);
DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi);
launchMinimized = true;
launchMaximized = false;
}
RECT rect = app.position.toRect();
float mult = static_cast<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
return true;
}
else
{
Logger::error(L"Failed placing {}", app.name);
return false;
}
}
bool WindowArranger::allWindowsFound() const
{
return std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const std::pair<WorkspacesData::WorkspacesProject::Application, WorkspacesData::LaunchingAppState>& val) {
return val.second.window == nullptr;
}) == m_launchingApps.end();
}

Просмотреть файл

@ -0,0 +1,30 @@
#pragma once
#include <WindowCreationHandler.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
class WindowArranger
{
public:
WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper);
~WindowArranger() = default;
private:
const WorkspacesData::WorkspacesProject m_project;
const std::vector<HWND> m_windowsBefore;
const std::vector<WorkspacesData::WorkspacesProject::Monitor> m_monitors;
const Utils::Apps::AppList m_installedApps;
//const WindowCreationHandler m_windowCreationHandler;
const IPCHelper& m_ipcHelper;
WorkspacesData::LaunchingAppStateMap m_launchingApps{};
//void onWindowCreated(HWND window);
void processWindow(HWND window);
bool moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app);
bool allWindowsFound() const;
};

Просмотреть файл

@ -0,0 +1,60 @@
#include "pch.h"
#include "WindowCreationHandler.h"
WindowCreationHandler::WindowCreationHandler(std::function<void(HWND)> windowCreatedCallback) :
m_windowCreatedCallback(windowCreatedCallback)
{
s_instance = this;
InitHooks();
}
WindowCreationHandler::~WindowCreationHandler()
{
m_staticWinEventHooks.erase(std::remove_if(begin(m_staticWinEventHooks),
end(m_staticWinEventHooks),
[](const HWINEVENTHOOK hook) {
return UnhookWinEvent(hook);
}),
end(m_staticWinEventHooks));
}
void WindowCreationHandler::InitHooks()
{
std::array<DWORD, 3> events_to_subscribe = {
EVENT_OBJECT_UNCLOAKED,
EVENT_OBJECT_SHOW,
EVENT_OBJECT_CREATE
};
for (const auto event : events_to_subscribe)
{
auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (hook)
{
m_staticWinEventHooks.emplace_back(hook);
}
else
{
Logger::error(L"Failed to initialize win event hooks");
}
}
}
void WindowCreationHandler::HandleWinHookEvent(DWORD event, HWND window) noexcept
{
switch (event)
{
//case EVENT_OBJECT_UNCLOAKED:
//case EVENT_OBJECT_SHOW:
case EVENT_OBJECT_CREATE:
{
if (m_windowCreatedCallback)
{
m_windowCreatedCallback(window);
}
}
break;
default:
break;
}
}

Просмотреть файл

@ -0,0 +1,30 @@
#pragma once
class WindowCreationHandler
{
public:
WindowCreationHandler(std::function<void(HWND)> windowCreatedCallback);
~WindowCreationHandler();
private:
static inline WindowCreationHandler* s_instance = nullptr;
std::vector<HWINEVENTHOOK> m_staticWinEventHooks;
std::function<void(HWND)> m_windowCreatedCallback;
void InitHooks();
void HandleWinHookEvent(DWORD event, HWND window) noexcept;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,
DWORD event,
HWND window,
LONG object,
LONG child,
DWORD eventThread,
DWORD eventTime)
{
if (s_instance)
{
s_instance->HandleWinHookEvent(event, window);
}
}
};

Просмотреть файл

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Project configurations -->
<!-- Props that should be disabled while building on CI server -->
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesWindowArrangerResource.base.rc WorkspacesWindowArrangerResource.rc" />
</Target>
<!-- C++ source compile-specific things for all configurations -->
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
</Link>
<Lib>
<TreatLibWarningAsErrors>true</TreatLibWarningAsErrors>
</Lib>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for Debug/Release configurations -->
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{37D07516-4185-43A4-924F-3C7A5D95ECF6}</ProjectGuid>
<RootNamespace>WorkspacesWindowArranger</RootNamespace>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<SpectreMitigation>Spectre</SpectreMitigation>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>PowerToys.$(MSBuildProjectName)</TargetName>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="WindowArranger.cpp" />
<ClCompile Include="WindowCreationHandler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="WindowArranger.h" />
<ClInclude Include="WindowCreationHandler.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\WorkspacesLib\WorkspacesLib.vcxproj">
<Project>{b31fcc55-b5a4-4ea7-b414-2dceae6af332}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc" />
<None Include="WorkspacesWindowArrangerResource.base.rc" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowCreationHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowArranger.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowCreationHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowArranger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="WorkspacesWindowArrangerResource.base.rc">
<Filter>Resource Files</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<Filter>Resource Files</Filter>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,115 @@
#include "pch.h"
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/utils.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
#include <common/utils/window.h>
#include <WindowArranger.h>
const std::wstring moduleName = L"Workspaces\\WorkspacesWindowArranger";
const std::wstring internalPath = L"";
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cmdShow)
{
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesWindowArrangerLoggerName);
InitUnhandledExceptionHandler();
if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
{
Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
return 0;
}
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
std::wstring commandLine{ GetCommandLineW() };
if (commandLine.empty())
{
Logger::warn("Empty command line arguments");
return 1;
}
auto args = split(commandLine, L" ");
std::wstring id{};
if (args.size() == 1)
{
id = args[0];
}
else if (args.size() == 2)
{
id = args[1];
}
if (id.empty())
{
Logger::warn("Incorrect command line arguments: no workspace id");
return 1;
}
// read workspaces
std::vector<WorkspacesData::WorkspacesProject> workspaces;
WorkspacesData::WorkspacesProject projectToLaunch{};
// check the temp file in case the project is just created and not saved to the workspaces.json yet
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile()))
{
auto file = WorkspacesData::TempWorkspacesFile();
auto res = JsonUtils::ReadSingleWorkspace(file);
if (res.isOk() && res.value().id == id)
{
projectToLaunch = res.getValue();
}
else
{
Logger::error(L"Error reading temp file");
return 1;
}
}
if (projectToLaunch.id.empty())
{
auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{
workspaces = res.getValue();
}
else
{
return 1;
}
for (const auto& proj : workspaces)
{
if (proj.id == id)
{
projectToLaunch = proj;
break;
}
}
}
if (projectToLaunch.id.empty())
{
Logger::critical(L"Workspace {} not found", id);
return 1;
}
// IPC
IPCHelper ipc(IPCHelperStrings::WindowArrangerPipeName, IPCHelperStrings::LauncherArrangerPipeName, nullptr);
// arrange windows
Logger::info(L"Arrange windows from Workspace {} : {}", projectToLaunch.name, projectToLaunch.id);
WindowArranger windowArranger(projectToLaunch, ipc);
//run_message_loop();
Logger::debug(L"Arranger finished");
CoUninitialize();
return 0;
}

Просмотреть файл

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

Просмотреть файл

@ -0,0 +1 @@
#include "pch.h"

Просмотреть файл

@ -0,0 +1,6 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <common/logger/logger.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

Просмотреть файл

@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by WorkspacesWindowArrangerResource.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Workspaces Window Arranger"
#define INTERNAL_NAME "PowerToys.WorkspacesWindowArranger"
#define ORIGINAL_FILENAME "PowerToys.WorkspacesWindowArranger.exe"
// Non-localizable
//////////////////////////////

Просмотреть файл

@ -21,6 +21,7 @@ namespace WindowUtils
const wchar_t WorkspacesSnapshotTool[] = L"POWERTOYS.WORKSPACESSNAPSHOTTOOL";
const wchar_t WorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR";
const wchar_t WorkspacesLauncher[] = L"POWERTOYS.WORKSPACESLAUNCHER";
const wchar_t WorkspacesWindowArranger[] = L"POWERTOYS.WORKSPACESWINDOWARRANGER";
}
inline bool IsRoot(HWND window) noexcept
@ -79,7 +80,8 @@ namespace WindowUtils
NonLocalizable::SearchUI,
NonLocalizable::HelpWindow,
NonLocalizable::WorkspacesEditor,
NonLocalizable::WorkspacesLauncher,
NonLocalizable::WorkspacesLauncher,
NonLocalizable::WorkspacesWindowArranger,
NonLocalizable::WorkspacesSnapshotTool,
};
return (check_excluded_app(window, processPathUpper, defaultExcludedApps));

Просмотреть файл

@ -46,5 +46,6 @@ std::vector<std::wstring> processes =
L"PowerToys.WorkspacesSnapshotTool.exe",
L"PowerToys.WorkspacesLauncher.exe",
L"PowerToys.WorkspacesLauncherUI.exe",
L"PowerToys.WorkspacesWindowArranger.exe",
L"PowerToys.WorkspacesEditor.exe",
};