chore: import upstream snapshot with attribution
Spell checking / Check Spelling (push) Has been cancelled
Spell checking / Update PR (push) Has been cancelled
Publish Dev Docs Website / build (push) Failing after 1s
Publish Dev Docs Website / deploy (push) Has been skipped
Spell checking / Report (Push) (push) Has been cancelled
Spell checking / Report (PR) (push) Has been cancelled
Spell checking / Check Spelling (push) Has been cancelled
Spell checking / Update PR (push) Has been cancelled
Publish Dev Docs Website / build (push) Failing after 1s
Publish Dev Docs Website / deploy (push) Has been skipped
Spell checking / Report (Push) (push) Has been cancelled
Spell checking / Report (PR) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BugReportTool", "BugReportTool\BugReportTool.vcxproj", "{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SettingsAPI", "..\..\src\common\SettingsAPI\SettingsAPI.vcxproj", "{6955446D-23F7-4023-9BB3-8657F904AF99}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
Debug|x64 = Debug|x64
|
||||
Release|ARM64 = Release|ARM64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Debug|x64.Build.0 = Debug|x64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Release|x64.ActiveCfg = Release|x64
|
||||
{99126840-5C30-4E9E-AC6C-E73DDED5C3BD}.Release|x64.Build.0 = Release|x64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.Build.0 = Debug|x64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.ActiveCfg = Release|x64
|
||||
{6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E9F52385-ED21-4F66-9DA4-F4A2749D480D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,40 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../src/common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
#include "winres.h"
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{99126840-5c30-4e9e-ac6c-e73dded5c3bd}</ProjectGuid>
|
||||
<RootNamespace>BugReportTool</RootNamespace>
|
||||
<ProjectName>BugReportTool</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<TargetName>PowerToys.$(ProjectName)</TargetName>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\Tools\</OutDir>
|
||||
</PropertyGroup>
|
||||
<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" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<AdditionalIncludeDirectories>../../../src/</AdditionalIncludeDirectories>
|
||||
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<AdditionalDependencies>Version.lib;Wevtapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="EventViewer.cpp" />
|
||||
<ClCompile Include="InstallationFolder.cpp" />
|
||||
<ClCompile Include="Package.cpp" />
|
||||
<ClCompile Include="ProcessesList.cpp" />
|
||||
<ClCompile Include="ReportGPOValues.cpp" />
|
||||
<ClCompile Include="ReportMonitorInfo.cpp" />
|
||||
<ClCompile Include="Main.cpp" />
|
||||
<ClCompile Include="RegistryUtils.cpp" />
|
||||
<ClCompile Include="XmlDocumentEx.cpp" />
|
||||
<ClCompile Include="ZipTools\ZipFolder.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="EventViewer.h" />
|
||||
<ClInclude Include="InstallationFolder.h" />
|
||||
<ClInclude Include="Package.h" />
|
||||
<ClInclude Include="ReportGPOValues.h" />
|
||||
<ClInclude Include="ReportMonitorInfo.h" />
|
||||
<ClInclude Include="$(RepoRoot)src\common\utils\json.h" />
|
||||
<ClInclude Include="RegistryUtils.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="XmlDocumentEx.h" />
|
||||
<ClInclude Include="ZipTools\ZipFolder.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="BugReportTool.rc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.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.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Main.cpp" />
|
||||
<ClCompile Include="ZipTools\ZipFolder.cpp">
|
||||
<Filter>ZipTools</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ReportMonitorInfo.cpp" />
|
||||
<ClCompile Include="RegistryUtils.cpp" />
|
||||
<ClCompile Include="EventViewer.cpp" />
|
||||
<ClCompile Include="XmlDocumentEx.cpp" />
|
||||
<ClCompile Include="InstallationFolder.cpp" />
|
||||
<ClCompile Include="ProcessesList.cpp" />
|
||||
<ClCompile Include="Package.cpp" />
|
||||
<ClCompile Include="ReportGPOValues.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="ZipTools">
|
||||
<UniqueIdentifier>{3ae1b6aa-4134-47b1-afdf-dfb3b5901dcc}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ZipTools\ZipFolder.h">
|
||||
<Filter>ZipTools</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\..\common\utils\json.h" />
|
||||
<ClInclude Include="ReportMonitorInfo.h" />
|
||||
<ClInclude Include="RegistryUtils.h" />
|
||||
<ClInclude Include="EventViewer.h" />
|
||||
<ClInclude Include="XmlDocumentEx.h" />
|
||||
<ClInclude Include="InstallationFolder.h" />
|
||||
<ClInclude Include="Package.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="ReportGPOValues.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="BugReportTool.rc" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "EventViewer.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <sddl.h>
|
||||
#include <stdio.h>
|
||||
#include <winevt.h>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
#include "XmlDocumentEx.h"
|
||||
|
||||
extern std::vector<std::wstring> processes;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Batch size for number of events queried at once
|
||||
constexpr int BATCH_SIZE = 50;
|
||||
|
||||
class EventViewerReporter
|
||||
{
|
||||
private:
|
||||
// Report last 30 days
|
||||
const long long PERIOD = 10 * 24 * 3600ll * 1000;
|
||||
|
||||
const std::wstring QUERY_BY_PROCESS = L"<QueryList>" \
|
||||
L" <Query Id='0'>" \
|
||||
L" <Select Path='Application'>" \
|
||||
L" *[System[TimeCreated[timediff(@SystemTime)<%I64u]]] " \
|
||||
L" and *[EventData[Data and (Data='%s')]]" \
|
||||
L" </Select>" \
|
||||
L" </Query>" \
|
||||
L"</QueryList>";
|
||||
|
||||
const std::wstring QUERY_BY_CHANNEL = L"<QueryList>" \
|
||||
L" <Query Id='0'>" \
|
||||
L" <Select Path='%s'>" \
|
||||
L" *[System[TimeCreated[timediff(@SystemTime)<%I64u]]]" \
|
||||
L" </Select>" \
|
||||
L" </Query>" \
|
||||
L"</QueryList>";
|
||||
|
||||
|
||||
std::wstring GetQuery(std::wstring processName)
|
||||
{
|
||||
wchar_t buff[1000];
|
||||
memset(buff, 0, sizeof(buff));
|
||||
_snwprintf_s(buff, sizeof(buff), QUERY_BY_PROCESS.c_str(), PERIOD, processName.c_str());
|
||||
return buff;
|
||||
}
|
||||
|
||||
std::wstring GetQueryByChannel(std::wstring channelName)
|
||||
{
|
||||
wchar_t buff[1000];
|
||||
memset(buff, 0, sizeof(buff));
|
||||
_snwprintf_s(buff, sizeof(buff), QUERY_BY_CHANNEL.c_str(), channelName.c_str(), PERIOD);
|
||||
return buff;
|
||||
}
|
||||
|
||||
std::wofstream report;
|
||||
EVT_HANDLE hResults;
|
||||
bool isChannel;
|
||||
|
||||
bool ShouldIncludeEvent(const std::wstring& eventXml)
|
||||
{
|
||||
if (!isChannel)
|
||||
{
|
||||
return true; // Include all events if no filtering
|
||||
}
|
||||
|
||||
// Check if the event contains PowerToys or CommandPalette
|
||||
return (eventXml.find(L"PowerToys") != std::wstring::npos ||
|
||||
eventXml.find(L"CommandPalette") != std::wstring::npos);
|
||||
}
|
||||
|
||||
void PrintEvent(EVT_HANDLE hEvent)
|
||||
{
|
||||
DWORD status = ERROR_SUCCESS;
|
||||
DWORD dwBufferSize = 0;
|
||||
DWORD dwBufferUsed = 0;
|
||||
DWORD dwPropertyCount = 0;
|
||||
LPWSTR pRenderedContent = NULL;
|
||||
|
||||
// The EvtRenderEventXml flag tells EvtRender to render the event as an XML string.
|
||||
if (!EvtRender(NULL, hEvent, EvtRenderEventXml, dwBufferSize, pRenderedContent, &dwBufferUsed, &dwPropertyCount))
|
||||
{
|
||||
if (ERROR_INSUFFICIENT_BUFFER == (status = GetLastError()))
|
||||
{
|
||||
dwBufferSize = dwBufferUsed;
|
||||
pRenderedContent = static_cast<LPWSTR>(malloc(dwBufferSize));
|
||||
if (pRenderedContent)
|
||||
{
|
||||
EvtRender(NULL, hEvent, EvtRenderEventXml, dwBufferSize, pRenderedContent, &dwBufferUsed, &dwPropertyCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (ERROR_SUCCESS != (status = GetLastError()))
|
||||
{
|
||||
report << std::endl << L"EvtRender failed with " << get_last_error_or_default(GetLastError()) << std::endl << std::endl;
|
||||
if (pRenderedContent)
|
||||
{
|
||||
free(pRenderedContent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filtering if needed
|
||||
std::wstring eventContent(pRenderedContent);
|
||||
if (!ShouldIncludeEvent(eventContent))
|
||||
{
|
||||
if (pRenderedContent)
|
||||
{
|
||||
free(pRenderedContent);
|
||||
}
|
||||
return; // Skip this event
|
||||
}
|
||||
|
||||
XmlDocumentEx doc;
|
||||
doc.LoadXml(pRenderedContent);
|
||||
std::wstring formattedXml = L"";
|
||||
try
|
||||
{
|
||||
formattedXml = doc.GetFormatedXml();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
formattedXml = pRenderedContent;
|
||||
}
|
||||
|
||||
report << std::endl << formattedXml << std::endl;
|
||||
if (pRenderedContent)
|
||||
{
|
||||
free(pRenderedContent);
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerate all the events in the result set.
|
||||
void PrintResults(EVT_HANDLE results)
|
||||
{
|
||||
DWORD status = ERROR_SUCCESS;
|
||||
EVT_HANDLE hEvents[BATCH_SIZE];
|
||||
DWORD dwReturned = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get a block of events from the result set.
|
||||
if (!EvtNext(results, BATCH_SIZE, hEvents, INFINITE, 0, &dwReturned))
|
||||
{
|
||||
if (ERROR_NO_MORE_ITEMS != (status = GetLastError()))
|
||||
{
|
||||
report << L"EvtNext failed with " << status << std::endl;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// For each event, call the PrintEvent function which renders the
|
||||
// event for display. PrintEvent is shown in RenderingEvents.
|
||||
for (DWORD i = 0; i < dwReturned; i++)
|
||||
{
|
||||
PrintEvent(hEvents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (DWORD i = 0; i < dwReturned; i++)
|
||||
{
|
||||
if (nullptr != hEvents[i])
|
||||
EvtClose(hEvents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
EventViewerReporter(const std::filesystem::path& tmpDir, std::wstring queryName, std::wstring fileName, bool isChannel)
|
||||
:isChannel(isChannel)
|
||||
{
|
||||
std::wstring query = L"";
|
||||
if (isChannel)
|
||||
{
|
||||
query = GetQueryByChannel(queryName);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = GetQuery(queryName);
|
||||
}
|
||||
|
||||
auto reportPath = tmpDir;
|
||||
reportPath.append(L"EventViewer-" + fileName + L".xml");
|
||||
report = std::wofstream(reportPath);
|
||||
|
||||
hResults = EvtQuery(NULL, NULL, query.c_str(), EvtQueryChannelPath);
|
||||
if (NULL == hResults)
|
||||
{
|
||||
report << "Failed to report info for " << queryName << ". " << get_last_error_or_default(GetLastError()) << std::endl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
~EventViewerReporter()
|
||||
{
|
||||
if (hResults)
|
||||
{
|
||||
EvtClose(hResults);
|
||||
hResults = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Report()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (hResults)
|
||||
{
|
||||
PrintResults(hResults);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
report << "Failed to report info" << std::endl;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void EventViewer::ReportEventViewerInfo(const std::filesystem::path& tmpDir)
|
||||
{
|
||||
for (auto& process : processes)
|
||||
{
|
||||
EventViewerReporter(tmpDir, process, process, false).Report();
|
||||
}
|
||||
}
|
||||
|
||||
void EventViewer::ReportAppXDeploymentLogs(const std::filesystem::path& tmpDir)
|
||||
{
|
||||
EventViewerReporter(tmpDir, L"Microsoft-Windows-AppXDeploymentServer/Operational", L"AppXDeploymentServerEventLog", true).Report();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
namespace EventViewer
|
||||
{
|
||||
void ReportEventViewerInfo(const std::filesystem::path& tmpDir);
|
||||
void ReportAppXDeploymentLogs(const std::filesystem::path& tmpDir);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "InstallationFolder.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <Windows.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
using namespace std;
|
||||
using std::filesystem::directory_iterator;
|
||||
using std::filesystem::path;
|
||||
|
||||
wstring GetVersion(path filePath)
|
||||
{
|
||||
DWORD verHandle = 0;
|
||||
UINT size = 0;
|
||||
LPVOID lpBuffer = nullptr;
|
||||
DWORD verSize = GetFileVersionInfoSize(filePath.c_str(), &verHandle);
|
||||
wstring version = L"None";
|
||||
|
||||
if (verSize != 0)
|
||||
{
|
||||
LPSTR verData = new char[verSize];
|
||||
|
||||
if (GetFileVersionInfo(filePath.c_str(), verHandle, verSize, verData))
|
||||
{
|
||||
if (VerQueryValue(verData, L"\\", &lpBuffer, &size))
|
||||
{
|
||||
if (size)
|
||||
{
|
||||
VS_FIXEDFILEINFO* verInfo = static_cast<VS_FIXEDFILEINFO*>(lpBuffer);
|
||||
if (verInfo->dwSignature == 0xfeef04bd)
|
||||
{
|
||||
version =
|
||||
std::to_wstring((verInfo->dwFileVersionMS >> 16) & 0xffff) + L"." +
|
||||
std::to_wstring((verInfo->dwFileVersionMS >> 0) & 0xffff) + L"." +
|
||||
std::to_wstring((verInfo->dwFileVersionLS >> 16) & 0xffff) + L"." +
|
||||
std::to_wstring((verInfo->dwFileVersionLS >> 0) & 0xffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete[] verData;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
optional<path> GetRootPath()
|
||||
{
|
||||
WCHAR modulePath[MAX_PATH];
|
||||
if (!GetModuleFileName(NULL, modulePath, MAX_PATH))
|
||||
{
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
path rootPath = path(modulePath);
|
||||
rootPath = rootPath.remove_filename();
|
||||
rootPath = rootPath.append("..");
|
||||
return std::filesystem::canonical(rootPath);
|
||||
}
|
||||
|
||||
wstring GetChecksum(path filePath)
|
||||
{
|
||||
BOOL bResult = FALSE;
|
||||
HCRYPTPROV hProv = 0;
|
||||
HCRYPTHASH hHash = 0;
|
||||
HANDLE hFile = NULL;
|
||||
constexpr int bufferSize = 1024;
|
||||
BYTE rgbFile[bufferSize];
|
||||
DWORD cbRead = 0;
|
||||
constexpr int md5Length = 16;
|
||||
BYTE rgbHash[md5Length];
|
||||
DWORD cbHash = 0;
|
||||
CHAR rgbDigits[] = "0123456789abcdef";
|
||||
LPCWSTR filename = filePath.c_str();
|
||||
hFile = CreateFile(filename,
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_SEQUENTIAL_SCAN,
|
||||
NULL);
|
||||
|
||||
if (INVALID_HANDLE_VALUE == hFile)
|
||||
{
|
||||
return L"CreateFile() failed. " + get_last_error_or_default(GetLastError());
|
||||
}
|
||||
|
||||
// Get handle to the crypto provider
|
||||
if (!CryptAcquireContext(&hProv,
|
||||
NULL,
|
||||
NULL,
|
||||
PROV_RSA_FULL,
|
||||
CRYPT_VERIFYCONTEXT))
|
||||
{
|
||||
CloseHandle(hFile);
|
||||
return L"CryptAcquireContext() failed. " + get_last_error_or_default(GetLastError());
|
||||
}
|
||||
|
||||
if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
|
||||
{
|
||||
CloseHandle(hFile);
|
||||
CryptReleaseContext(hProv, 0);
|
||||
return L"CryptCreateHash() failed. " + get_last_error_or_default(GetLastError());
|
||||
}
|
||||
|
||||
bResult = ReadFile(hFile, rgbFile, bufferSize, &cbRead, NULL);
|
||||
while (bResult)
|
||||
{
|
||||
if (0 == cbRead)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!CryptHashData(hHash, rgbFile, cbRead, 0))
|
||||
{
|
||||
CryptReleaseContext(hProv, 0);
|
||||
CryptDestroyHash(hHash);
|
||||
CloseHandle(hFile);
|
||||
return L"CryptHashData() failed. " + get_last_error_or_default(GetLastError());;
|
||||
}
|
||||
|
||||
bResult = ReadFile(hFile, rgbFile, bufferSize, &cbRead, NULL);
|
||||
}
|
||||
|
||||
if (!bResult)
|
||||
{
|
||||
CryptReleaseContext(hProv, 0);
|
||||
CryptDestroyHash(hHash);
|
||||
CloseHandle(hFile);
|
||||
return L"ReadFile() failed. " + get_last_error_or_default(GetLastError());;
|
||||
}
|
||||
|
||||
cbHash = md5Length;
|
||||
std::wstring result = L"";
|
||||
if (CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0))
|
||||
{
|
||||
for (DWORD i = 0; i < cbHash; i++)
|
||||
{
|
||||
result += rgbDigits[rgbHash[i] >> 4];
|
||||
result += rgbDigits[rgbHash[i] & 0xf];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = L"CryptGetHashParam() failed. " + get_last_error_or_default(GetLastError());;
|
||||
}
|
||||
|
||||
CryptDestroyHash(hHash);
|
||||
CryptReleaseContext(hProv, 0);
|
||||
CloseHandle(hFile);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class Reporter
|
||||
{
|
||||
private:
|
||||
std::wofstream os;
|
||||
std::wofstream GetOutputStream(const path& tmpDir)
|
||||
{
|
||||
auto path = tmpDir;
|
||||
path += "installationFolderStructure.txt";
|
||||
std::wofstream out_s = std::wofstream(path);
|
||||
return out_s;
|
||||
}
|
||||
public:
|
||||
Reporter(const path& tmpDir)
|
||||
{
|
||||
os = GetOutputStream(tmpDir);
|
||||
}
|
||||
|
||||
void Report(path dirPath, int indentation = 0)
|
||||
{
|
||||
set<pair<path, bool>> paths;
|
||||
try
|
||||
{
|
||||
directory_iterator end_it;
|
||||
for (directory_iterator it(dirPath); it != end_it; ++it)
|
||||
{
|
||||
paths.insert({ it->path(), it->is_directory() });
|
||||
}
|
||||
}
|
||||
catch (filesystem::filesystem_error err)
|
||||
{
|
||||
os << err.what() << endl;
|
||||
}
|
||||
|
||||
for (auto filePair : paths)
|
||||
{
|
||||
auto filePath = filePair.first;
|
||||
auto isDirectory = filePair.second;
|
||||
|
||||
auto fileName = filePath.wstring().substr(dirPath.wstring().size() + 1);
|
||||
os << wstring(indentation, ' ') << fileName << " ";
|
||||
if (!isDirectory)
|
||||
{
|
||||
os << GetVersion(filePath) << " " << GetChecksum(filePath);
|
||||
}
|
||||
|
||||
os << endl;
|
||||
if (isDirectory)
|
||||
{
|
||||
Report(filePath, indentation + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void InstallationFolder::ReportStructure(const path& tmpDir)
|
||||
{
|
||||
auto rootPath = GetRootPath();
|
||||
if (rootPath)
|
||||
{
|
||||
Reporter(tmpDir).Report(rootPath.value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
namespace InstallationFolder
|
||||
{
|
||||
void ReportStructure(const std::filesystem::path& tmpDir);
|
||||
};
|
||||
@@ -0,0 +1,406 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Shlobj.h>
|
||||
#include <winrt/Windows.Data.Json.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.System.UserProfile.h>
|
||||
#include <winrt/Windows.Globalization.h>
|
||||
|
||||
#include "ZipTools/ZipFolder.h"
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <common/utils/json.h>
|
||||
#include <common/utils/timeutil.h>
|
||||
#include <common/utils/exec.h>
|
||||
|
||||
#include "Package.h"
|
||||
#include "ReportMonitorInfo.h"
|
||||
#include "RegistryUtils.h"
|
||||
#include "EventViewer.h"
|
||||
#include "InstallationFolder.h"
|
||||
#include "ReportGPOValues.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::filesystem;
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
map<wstring, vector<wstring>> escapeInfo = {
|
||||
{ L"FancyZones\\app-zone-history.json", { L"app-zone-history/app-path" } },
|
||||
{ L"FancyZones\\settings.json", { L"properties/fancyzones_excluded_apps" } },
|
||||
{ L"MouseWithoutBorders\\settings.json", { L"properties/SecurityKey" } }, // avoid leaking connection key
|
||||
{ L"Keyboard Manager\\default.json", {
|
||||
L"remapKeysToText",
|
||||
L"remapShortcutsToText",
|
||||
L"remapShortcuts/global/runProgramFilePath",
|
||||
L"remapShortcuts/global/runProgramArgs",
|
||||
L"remapShortcuts/global/runProgramStartInDir",
|
||||
L"remapShortcuts/global/openUri",
|
||||
L"remapShortcuts/appSpecific/runProgramFilePath",
|
||||
L"remapShortcuts/appSpecific/runProgramArgs",
|
||||
L"remapShortcuts/appSpecific/runProgramStartInDir",
|
||||
L"remapShortcuts/appSpecific/openUri",
|
||||
} }, // avoid leaking personal information from text, URI or application mappings
|
||||
{ L"Workspaces/workspaces.json", { L"workspaces/applications/command-line-arguments" } },
|
||||
{ L"AdvancedPaste/settings.json", {
|
||||
L"properties/custom-actions/value/name",
|
||||
L"properties/custom-actions/value/prompt"
|
||||
} },
|
||||
};
|
||||
|
||||
vector<wstring> filesToDelete = {
|
||||
L"AdvancedPaste\\lastQuery.json",
|
||||
L"AdvancedPaste\\kernelQueryCache.json",
|
||||
L"PowerToys Run\\Cache",
|
||||
L"PowerRename\\replace-mru.json",
|
||||
L"PowerRename\\search-mru.json",
|
||||
L"PowerToys Run\\Settings\\UserSelectedRecord.json",
|
||||
L"PowerToys Run\\Settings\\QueryHistory.json",
|
||||
L"NewPlus\\Templates",
|
||||
L"etw",
|
||||
};
|
||||
|
||||
vector<wstring> GetXpathArray(wstring xpath)
|
||||
{
|
||||
vector<wstring> result;
|
||||
wstring cur = L"";
|
||||
for (auto ch : xpath)
|
||||
{
|
||||
if (ch == L'/')
|
||||
{
|
||||
result.push_back(cur);
|
||||
cur = L"";
|
||||
continue;
|
||||
}
|
||||
|
||||
cur += ch;
|
||||
}
|
||||
|
||||
if (!cur.empty())
|
||||
{
|
||||
result.push_back(cur);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void HideByXPath(IJsonValue& val, vector<wstring>& xpathArray, int p)
|
||||
{
|
||||
if (val.ValueType() == JsonValueType::Array)
|
||||
{
|
||||
for (auto it : val.GetArray())
|
||||
{
|
||||
HideByXPath(it, xpathArray, p);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (p == xpathArray.size() - 1)
|
||||
{
|
||||
if (val.ValueType() == JsonValueType::Object)
|
||||
{
|
||||
auto obj = val.GetObjectW();
|
||||
if (obj.HasKey(xpathArray[p]))
|
||||
{
|
||||
auto privateDatavalue = JsonValue::CreateStringValue(L"<private_data>");
|
||||
obj.SetNamedValue(xpathArray[p], privateDatavalue);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (val.ValueType() == JsonValueType::Object)
|
||||
{
|
||||
IJsonValue newVal;
|
||||
try
|
||||
{
|
||||
newVal = val.GetObjectW().GetNamedValue(xpathArray[p]);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HideByXPath(newVal, xpathArray, p + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void HideForFile(const path& dir, const wstring& relativePath)
|
||||
{
|
||||
path jsonPath = dir;
|
||||
jsonPath.append(relativePath);
|
||||
auto jObject = json::from_file(jsonPath.wstring());
|
||||
if (!jObject.has_value())
|
||||
{
|
||||
wprintf(L"Failed to parse file %s\n", jsonPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonValue jValue = json::value(jObject.value());
|
||||
for (auto xpath : escapeInfo[relativePath])
|
||||
{
|
||||
vector<wstring> xpathArray = GetXpathArray(xpath);
|
||||
HideByXPath(jValue, xpathArray, 0);
|
||||
}
|
||||
|
||||
json::to_file(jsonPath.wstring(), jObject.value());
|
||||
}
|
||||
|
||||
bool DeleteFolder(wstring path)
|
||||
{
|
||||
error_code err;
|
||||
remove_all(path, err);
|
||||
if (err.value() != 0)
|
||||
{
|
||||
wprintf_s(L"Failed to delete %s. Error code: %d\n", path.c_str(), err.value());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HideUserPrivateInfo(const filesystem::path& dir)
|
||||
{
|
||||
// Replace data in json files
|
||||
for (auto& it : escapeInfo)
|
||||
{
|
||||
HideForFile(dir, it.first);
|
||||
}
|
||||
|
||||
// Delete files
|
||||
for (auto it : filesToDelete)
|
||||
{
|
||||
auto path = dir;
|
||||
path = path.append(it);
|
||||
DeleteFolder(path);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportWindowsVersion(const filesystem::path& tmpDir)
|
||||
{
|
||||
auto versionReportPath = tmpDir;
|
||||
versionReportPath = versionReportPath.append("windows-version.txt");
|
||||
OSVERSIONINFOEXW osInfo{};
|
||||
|
||||
try
|
||||
{
|
||||
NTSTATUS(WINAPI * RtlGetVersion)
|
||||
(LPOSVERSIONINFOEXW) = nullptr;
|
||||
*reinterpret_cast<FARPROC*>(& RtlGetVersion) = GetProcAddress(GetModuleHandleA("ntdll"), "RtlGetVersion");
|
||||
if (RtlGetVersion)
|
||||
{
|
||||
osInfo.dwOSVersionInfoSize = sizeof(osInfo);
|
||||
RtlGetVersion(&osInfo);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to get windows version info\n");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
wofstream versionReport(versionReportPath);
|
||||
versionReport << "MajorVersion: " << osInfo.dwMajorVersion << endl;
|
||||
versionReport << "MinorVersion: " << osInfo.dwMinorVersion << endl;
|
||||
versionReport << "BuildNumber: " << osInfo.dwBuildNumber << endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to write to %s\n", versionReportPath.string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ReportWindowsSettings(const filesystem::path& tmpDir)
|
||||
{
|
||||
std::wstring userLanguage;
|
||||
std::wstring userLocale;
|
||||
try
|
||||
{
|
||||
const auto lang = winrt::Windows::System::UserProfile::GlobalizationPreferences::Languages().GetAt(0);
|
||||
userLanguage = winrt::Windows::Globalization::Language{ lang }.DisplayName().c_str();
|
||||
wchar_t localeName[LOCALE_NAME_MAX_LENGTH]{};
|
||||
if (!LCIDToLocaleName(GetThreadLocale(), localeName, LOCALE_NAME_MAX_LENGTH, 0))
|
||||
{
|
||||
throw -1;
|
||||
}
|
||||
userLocale = localeName;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to get windows settings\n");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
wofstream settingsReport(tmpDir / "windows-settings.txt");
|
||||
settingsReport << "Preferred user language: " << userLanguage << endl;
|
||||
settingsReport << "User locale: " << userLocale << endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to write windows settings\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ReportDotNetInstallationInfo(const filesystem::path& tmpDir)
|
||||
{
|
||||
auto dotnetInfoPath = tmpDir;
|
||||
dotnetInfoPath.append("dotnet-installation-info.txt");
|
||||
try
|
||||
{
|
||||
wofstream dotnetReport(dotnetInfoPath);
|
||||
auto dotnetInfo = exec_and_read_output(LR"(dotnet --list-runtimes)");
|
||||
if (!dotnetInfo.has_value())
|
||||
{
|
||||
printf("Failed to get dotnet installation information\n");
|
||||
return;
|
||||
}
|
||||
|
||||
dotnetReport << dotnetInfo.value().c_str();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to report dotnet installation information");
|
||||
}
|
||||
}
|
||||
|
||||
void ReportInstallerLogs(const filesystem::path& tmpDir, const filesystem::path& reportDir)
|
||||
{
|
||||
const char* bootstrapperLogFilePrefix = "powertoys-bootstrapper-msi-";
|
||||
const char* PTLogFilePrefix = "PowerToysMSIInstaller_";
|
||||
|
||||
for (auto& entry : directory_iterator{ tmpDir })
|
||||
{
|
||||
std::error_code ec;
|
||||
if (entry.is_directory(ec) || !entry.path().has_filename())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto fileName = entry.path().filename().string();
|
||||
if (!fileName.starts_with(bootstrapperLogFilePrefix) && !fileName.starts_with(PTLogFilePrefix))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
copy(entry.path(), reportDir / fileName, ec);
|
||||
}
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t* argv[], wchar_t*)
|
||||
{
|
||||
// Get path to save zip
|
||||
wstring saveZipPath;
|
||||
if (argc > 1)
|
||||
{
|
||||
saveZipPath = argv[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
if (SHGetSpecialFolderPath(HWND_DESKTOP, buffer, CSIDL_DESKTOP, FALSE))
|
||||
{
|
||||
saveZipPath = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Failed to retrieve the desktop path. Error code: %d\n", GetLastError());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
auto settingsRootPath = PTSettingsHelper::get_root_save_folder_location();
|
||||
settingsRootPath += L"\\";
|
||||
|
||||
auto localLowPath = PTSettingsHelper::get_local_low_folder_location();
|
||||
localLowPath += L"\\logs\\";
|
||||
|
||||
const auto tempDir = temp_directory_path();
|
||||
auto reportDir = temp_directory_path() / "PowerToys\\";
|
||||
if (!DeleteFolder(reportDir))
|
||||
{
|
||||
printf("Failed to delete temp folder\n");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
copy(settingsRootPath, reportDir, copy_options::recursive);
|
||||
|
||||
// Remove updates folder contents
|
||||
DeleteFolder(reportDir / "Updates");
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to copy PowerToys folder\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
copy(localLowPath, reportDir, copy_options::recursive);
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to copy logs saved in LocalLow\n");
|
||||
}
|
||||
|
||||
#ifndef _DEBUG
|
||||
InstallationFolder::ReportStructure(reportDir);
|
||||
#endif
|
||||
|
||||
// Hide sensitive information
|
||||
HideUserPrivateInfo(reportDir);
|
||||
|
||||
// Write windows settings to the temporary folder
|
||||
ReportWindowsSettings(reportDir);
|
||||
|
||||
// Write monitors info to the temporary folder
|
||||
ReportMonitorInfo(reportDir);
|
||||
|
||||
// Write windows version info to the temporary folder
|
||||
ReportWindowsVersion(reportDir);
|
||||
|
||||
// Write dotnet installation info to the temporary folder
|
||||
ReportDotNetInstallationInfo(reportDir);
|
||||
|
||||
// Write registry to the temporary folder
|
||||
ReportRegistry(reportDir);
|
||||
|
||||
// Write gpo policies to the temporary folder
|
||||
ReportGPOValues(reportDir);
|
||||
|
||||
// Write compatibility tab info to the temporary folder
|
||||
ReportCompatibilityTab(reportDir);
|
||||
|
||||
// Write event viewer logs info to the temporary folder
|
||||
EventViewer::ReportEventViewerInfo(reportDir);
|
||||
|
||||
// Write AppXDeployment-Server event logs to the temporary folder
|
||||
EventViewer::ReportAppXDeploymentLogs(reportDir);
|
||||
|
||||
ReportInstallerLogs(tempDir, reportDir);
|
||||
|
||||
ReportInstalledContextMenuPackages(reportDir);
|
||||
|
||||
// Zip folder
|
||||
auto zipPath = path::path(saveZipPath);
|
||||
|
||||
try
|
||||
{
|
||||
ZipFolder(zipPath, reportDir);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to zip folder\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
DeleteFolder(reportDir);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "Package.h"
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
|
||||
std::optional<winrt::Windows::ApplicationModel::Package> GetPackage(std::wstring packageDisplayName)
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
for (auto const& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
|
||||
if (packageFullName.contains(packageDisplayName))
|
||||
{
|
||||
return { package };
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstringstream GetPackageInfo(winrt::Windows::ApplicationModel::Package package) {
|
||||
std::wstringstream packageInfo;
|
||||
packageInfo << L"Display name: " << std::wstring(package.DisplayName()) << std::endl;
|
||||
packageInfo << L"Full name: " << std::wstring(package.Id().FullName()) << std::endl;
|
||||
packageInfo << L"Version: " << package.Id().Version().Major << L"."
|
||||
<< package.Id().Version().Minor << L"."
|
||||
<< package.Id().Version().Build << L"."
|
||||
<< package.Id().Version().Revision << L"." << std::endl;
|
||||
packageInfo << L"Publisher: " << std::wstring(package.Id().Publisher()) << std::endl;
|
||||
packageInfo << L"Status: " << (package.Status().VerifyIsOK() ? std::wstring(L"OK") : std::wstring(L"Not OK")) << std::endl;
|
||||
|
||||
return packageInfo;
|
||||
}
|
||||
|
||||
void ReportInstalledContextMenuPackages(const std::filesystem::path& reportDir)
|
||||
{
|
||||
const wchar_t* ImageResizerContextMenuPackageDisplayName = L"ImageResizerContextMenu";
|
||||
const wchar_t* PowerRenameContextMenuPackageDisplayName = L"PowerRenameContextMenu";
|
||||
|
||||
auto reportPath = reportDir;
|
||||
reportPath.append("context-menu-packages.txt");
|
||||
|
||||
std::wofstream packagesReport(reportPath);
|
||||
|
||||
try
|
||||
{
|
||||
auto imageResizerPackage = GetPackage(ImageResizerContextMenuPackageDisplayName);
|
||||
if (imageResizerPackage)
|
||||
{
|
||||
packagesReport << GetPackageInfo(*imageResizerPackage).str() << std::endl;
|
||||
}
|
||||
|
||||
auto powerRenamePackage = GetPackage(PowerRenameContextMenuPackageDisplayName);
|
||||
if (powerRenamePackage)
|
||||
{
|
||||
packagesReport << GetPackageInfo(*powerRenamePackage).str() << std::endl;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to report installed context menu packages");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
|
||||
std::optional<winrt::Windows::ApplicationModel::Package> GetPackage(std::wstring packageDisplayName);
|
||||
std::wstringstream GetPackageInfo(winrt::Windows::ApplicationModel::Package package);
|
||||
void ReportInstalledContextMenuPackages(const std::filesystem::path& reportDir);
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
std::vector<std::wstring> processes =
|
||||
{
|
||||
L"PowerToys.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.ColorPickerUI.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
L"PowerToys.Awake.exe",
|
||||
L"PowerToys.FancyZonesEditor.exe",
|
||||
L"PowerToys.FancyZones.exe",
|
||||
L"PowerToys.FileLocksmithUI.exe",
|
||||
L"PowerToys.LightSwitch.exe",
|
||||
L"PowerToys.KeyboardManagerEngine.exe",
|
||||
L"PowerToys.KeyboardManagerEditor.exe",
|
||||
L"PowerToys.PowerAccent.exe",
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.PowerOCR.exe",
|
||||
L"PowerToys.MouseJumpUI.exe",
|
||||
L"PowerToys.MeasureToolUI.exe",
|
||||
L"PowerToys.ShortcutGuide.exe",
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.Update.exe",
|
||||
L"PowerToys.ActionRunner.exe",
|
||||
L"PowerToys.AlwaysOnTop.exe",
|
||||
L"PowerToys.Hosts.exe",
|
||||
L"PowerToys.GcodePreviewHandler.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodePreviewHandler.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.MarkdownPreviewHandler.exe",
|
||||
L"PowerToys.MonacoPreviewHandler.exe",
|
||||
L"PowerToys.PdfPreviewHandler.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
L"PowerToys.QoiPreviewHandler.exe",
|
||||
L"PowerToys.QoiThumbnailProvider.exe",
|
||||
L"PowerToys.StlThumbnailProvider.exe",
|
||||
L"PowerToys.SvgPreviewHandler.exe",
|
||||
L"PowerToys.SvgThumbnailProvider.exe",
|
||||
L"PowerToys.RegistryPreview.exe",
|
||||
L"PowerToys.MouseWithoutBorders.exe",
|
||||
L"PowerToys.MouseWithoutBordersHelper.exe",
|
||||
L"PowerToys.MouseWithoutBordersService.exe",
|
||||
L"PowerToys.Peek.UI.exe",
|
||||
L"PowerToys.CropAndLock.exe",
|
||||
L"PowerToys.EnvironmentVariables.exe",
|
||||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.ZoomIt.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
};
|
||||
@@ -0,0 +1,318 @@
|
||||
#include "RegistryUtils.h"
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
extern std::vector<std::wstring> processes;
|
||||
|
||||
namespace
|
||||
{
|
||||
vector<pair<HKEY, wstring>> registryKeys = {
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{DD5CACDA-7C2E-4997-A62A-04A597B58F76}" },
|
||||
{ HKEY_CLASSES_ROOT, L"powertoys" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{ddee2b8a-6807-48a6-bb20-2338174ff779}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{36B27788-A8BB-4698-A756-DF9F11F64F84}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{45769bcc-e8fd-42d0-947e-02beef77a1f5}" },
|
||||
{ HKEY_CLASSES_ROOT, L"AppID\\{CF142243-F059-45AF-8842-DBBE9783DB14}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{07665729-6243-4746-95b7-79579308d1b2}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{ec52dea8-7c9f-4130-a77b-1737d0418507}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{dd8de316-7b01-48e7-ba21-e92c646704af}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{8AA07897-C30B-4543-865B-00A0E5A1B32D}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{BCC13D15-9720-4CC4-8371-EA74A274741E}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{BFEE99B4-B74D-4348-BCA5-E757029647FF}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{c28761a0-8420-43ad-bff3-40400543e2d4}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{8BC8AFC2-4E7C-4695-818E-8C1FFDCEA2AF}" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{51B4D7E5-7568-4234-B4BB-47FB3C016A69}\\InprocServer32" },
|
||||
{ HKEY_CLASSES_ROOT, L"CLSID\\{0440049F-D1DC-4E46-B27B-98393D79486B}" },
|
||||
{ HKEY_CLASSES_ROOT, L"AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt" },
|
||||
{ HKEY_CLASSES_ROOT, L".svg\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".svg\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" },
|
||||
{ HKEY_CLASSES_ROOT, L".md\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".pdf\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".pdf\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" },
|
||||
{ HKEY_CLASSES_ROOT, L".qoi\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".qoi\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" },
|
||||
{ HKEY_CLASSES_ROOT, L".gcode\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".gcode\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" },
|
||||
{ HKEY_CLASSES_ROOT, L".bgcode\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" },
|
||||
{ HKEY_CLASSES_ROOT, L".bgcode\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" },
|
||||
{ HKEY_CLASSES_ROOT, L".stl\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" }
|
||||
};
|
||||
|
||||
vector<tuple<HKEY, wstring, wstring>> registryValues = {
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{ddee2b8a-6807-48a6-bb20-2338174ff779}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{45769bcc-e8fd-42d0-947e-02beef77a1f5}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{07665729-6243-4746-95b7-79579308d1b2}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{ec52dea8-7c9f-4130-a77b-1737d0418507}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{dd8de316-7b01-48e7-ba21-e92c646704af}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\PreviewHandlers", L"{8AA07897-C30B-4543-865B-00A0E5A1B32D}" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Internet Explorer\\Main\\FeatureControl\\FEATURE_BROWSER_EMULATION", L"prevhost.exe" },
|
||||
{ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Internet Explorer\\Main\\FeatureControl\\FEATURE_BROWSER_EMULATION", L"dllhost.exe" }
|
||||
};
|
||||
|
||||
// Is there a Windows API for this?
|
||||
std::unordered_map<HKEY, wstring> hKeyToString = {
|
||||
{ HKEY_CLASSES_ROOT, L"HKEY_CLASSES_ROOT" },
|
||||
{ HKEY_CURRENT_USER, L"HKEY_CURRENT_USER" },
|
||||
{ HKEY_LOCAL_MACHINE, L"HKEY_LOCAL_MACHINE" },
|
||||
{ HKEY_PERFORMANCE_DATA, L"HKEY_PERFORMANCE_DATA" },
|
||||
{ HKEY_PERFORMANCE_NLSTEXT, L"HKEY_PERFORMANCE_NLSTEXT"},
|
||||
{ HKEY_PERFORMANCE_TEXT, L"HKEY_PERFORMANCE_TEXT"},
|
||||
{ HKEY_USERS, L"HKEY_USERS"},
|
||||
};
|
||||
|
||||
vector<pair<wstring, wstring>> QueryValues(HKEY key)
|
||||
{
|
||||
DWORD cValues;
|
||||
RegQueryInfoKeyW(key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &cValues, nullptr, nullptr, nullptr, nullptr);
|
||||
TCHAR achValue[255];
|
||||
DWORD cchValue = 255;
|
||||
LPBYTE value;
|
||||
vector<pair<wstring, wstring>> results;
|
||||
// Values
|
||||
if (cValues)
|
||||
{
|
||||
for (DWORD i = 0, retCode = ERROR_SUCCESS; i < cValues; i++)
|
||||
{
|
||||
cchValue = 255;
|
||||
achValue[0] = '\0';
|
||||
value = new BYTE[16383];
|
||||
retCode = RegEnumValueW(key, i, achValue, &cchValue, NULL, NULL, value, &cchValue);
|
||||
|
||||
if (retCode == ERROR_SUCCESS)
|
||||
{
|
||||
results.push_back({ achValue, (LPCTSTR)value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void QueryKey(HKEY key, wostream& stream, int indent = 1)
|
||||
{
|
||||
TCHAR achKey[255];
|
||||
DWORD cbName;
|
||||
TCHAR achClass[MAX_PATH] = TEXT("");
|
||||
DWORD cchClassName = MAX_PATH;
|
||||
DWORD cSubKeys = 0;
|
||||
DWORD cbMaxSubKey;
|
||||
DWORD cchMaxClass;
|
||||
DWORD cValues;
|
||||
DWORD cchMaxValue;
|
||||
DWORD cbMaxValueData;
|
||||
|
||||
DWORD i, retCode;
|
||||
|
||||
TCHAR achValue[255];
|
||||
DWORD cchValue = 255;
|
||||
LPBYTE value;
|
||||
|
||||
// Get the class name and the value count.
|
||||
retCode = RegQueryInfoKeyW(key, achClass, &cchClassName, NULL, &cSubKeys, &cbMaxSubKey, &cchMaxClass, &cValues, &cchMaxValue, &cbMaxValueData, NULL, NULL);
|
||||
|
||||
// Values
|
||||
if (cValues)
|
||||
{
|
||||
for (i = 0, retCode = ERROR_SUCCESS; i < cValues; i++)
|
||||
{
|
||||
cchValue = 255;
|
||||
achValue[0] = '\0';
|
||||
value = new BYTE[16383];
|
||||
retCode = RegEnumValueW(key, i, achValue, &cchValue, NULL, NULL, value, &cchValue);
|
||||
|
||||
if (retCode == ERROR_SUCCESS)
|
||||
{
|
||||
stream << wstring(indent, '\t');
|
||||
if (achValue[0] == '\0')
|
||||
{
|
||||
stream << "Default";
|
||||
}
|
||||
else
|
||||
{
|
||||
stream << achValue;
|
||||
}
|
||||
|
||||
stream << " > " << reinterpret_cast<LPCTSTR>(value) << "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
stream << "Error " << retCode << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keys
|
||||
if (cSubKeys)
|
||||
{
|
||||
std::vector<wstring> vecKeys;
|
||||
vecKeys.reserve(cSubKeys);
|
||||
|
||||
for (i = 0; i < cSubKeys; ++i)
|
||||
{
|
||||
cbName = 255;
|
||||
retCode = RegEnumKeyExW(key, i, achKey, &cbName, NULL, NULL, NULL, NULL);
|
||||
if (retCode == ERROR_SUCCESS)
|
||||
{
|
||||
vecKeys.push_back(achKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing subkeys recursively
|
||||
for (auto& child : vecKeys)
|
||||
{
|
||||
HKEY hTestKey;
|
||||
if (RegOpenKeyExW(key, child.c_str(), 0, KEY_READ, &hTestKey) == ERROR_SUCCESS)
|
||||
{
|
||||
stream << wstring(indent, '\t') << child << "\n";
|
||||
QueryKey(hTestKey, stream, indent + 1);
|
||||
RegCloseKey(hTestKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream << "Error " << retCode << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReportCompatibilityTab(HKEY key, wofstream& report)
|
||||
{
|
||||
map<wstring, wstring> flags;
|
||||
for (auto app : processes)
|
||||
{
|
||||
flags[app] = L"";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HKEY outKey;
|
||||
LONG result = RegOpenKeyExW(key, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers", 0, KEY_READ, &outKey);
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
auto values = QueryValues(outKey);
|
||||
for (auto value : values)
|
||||
{
|
||||
for (auto app : processes)
|
||||
{
|
||||
if (value.first.find(app) != wstring::npos)
|
||||
{
|
||||
flags[app] += value.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
report << "Failed to get the report. " << get_last_error_or_default(GetLastError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
report << "Failed to get the report";
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto flag : flags)
|
||||
{
|
||||
report << flag.first << ": " << flag.second << endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ReportCompatibilityTab(const std::filesystem::path& tmpDir)
|
||||
{
|
||||
auto reportPath = tmpDir;
|
||||
reportPath.append(L"compatibility-tab-info.txt");
|
||||
wofstream report(reportPath);
|
||||
report << "Current user report" << endl;
|
||||
ReportCompatibilityTab(HKEY_CURRENT_USER, report);
|
||||
report << endl << endl;
|
||||
report << "Local machine report" << endl;
|
||||
ReportCompatibilityTab(HKEY_LOCAL_MACHINE, report);
|
||||
}
|
||||
|
||||
void ReportRegistry(const filesystem::path& tmpDir)
|
||||
{
|
||||
auto registryReportPath = tmpDir;
|
||||
registryReportPath.append("registry-report-info.txt");
|
||||
|
||||
wofstream registryReport(registryReportPath);
|
||||
try
|
||||
{
|
||||
for (auto [rootKey, subKey] : registryKeys)
|
||||
{
|
||||
registryReport << hKeyToString[rootKey] << "\\" << subKey << "\n";
|
||||
|
||||
HKEY outKey;
|
||||
LONG result = RegOpenKeyExW(rootKey, subKey.c_str(), 0, KEY_READ, &outKey);
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
QueryKey(outKey, registryReport);
|
||||
RegCloseKey(rootKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
registryReport << "ERROR " << result << "\n";
|
||||
}
|
||||
|
||||
registryReport << "\n";
|
||||
}
|
||||
|
||||
for (auto [rootKey, subKey, value] : registryValues)
|
||||
{
|
||||
registryReport << hKeyToString[rootKey] << "\\" << subKey << "\n";
|
||||
|
||||
// Reading size
|
||||
DWORD dataSize = 0;
|
||||
DWORD flags = RRF_RT_ANY;
|
||||
DWORD type;
|
||||
LONG result = RegGetValueW(rootKey, subKey.c_str(), value.c_str(), flags, &type, NULL, &dataSize);
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
// Reading value
|
||||
if (type == REG_SZ) // string
|
||||
{
|
||||
std::wstring data(dataSize / sizeof(wchar_t) + 1, L' ');
|
||||
result = RegGetValueW(rootKey, subKey.c_str(), value.c_str(), flags, &type, &data[0], &dataSize);
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
registryReport << "\t" << value << " > " << data << "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
registryReport << "ERROR " << result << "\n";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD data = 0;
|
||||
dataSize = sizeof(data);
|
||||
result = RegGetValueW(rootKey, subKey.c_str(), value.c_str(), flags, &type, &data, &dataSize);
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
registryReport << "\t" << value << " > " << data << "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
registryReport << "ERROR " << result << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
RegCloseKey(rootKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
registryReport << "ERROR " << result << "\n";
|
||||
}
|
||||
|
||||
registryReport << "\n";
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to get registry keys\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <Windows.h>
|
||||
|
||||
void ReportRegistry(const std::filesystem::path& tmpDir);
|
||||
void ReportCompatibilityTab(const std::filesystem::path& tmpDir);
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "ReportGPOValues.h"
|
||||
#include <common/utils/gpo.h>
|
||||
#include <fstream>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <regex>
|
||||
|
||||
std::wstring gpo_rule_configured_to_string(powertoys_gpo::gpo_rule_configured_t gpo_rule)
|
||||
{
|
||||
switch (gpo_rule)
|
||||
{
|
||||
case powertoys_gpo::gpo_rule_configured_wrong_value:
|
||||
return L"wrong_value";
|
||||
case powertoys_gpo::gpo_rule_configured_unavailable:
|
||||
return L"can't_access";
|
||||
case powertoys_gpo::gpo_rule_configured_not_configured:
|
||||
return L"not_configured";
|
||||
case powertoys_gpo::gpo_rule_configured_disabled:
|
||||
return L"disabled";
|
||||
case powertoys_gpo::gpo_rule_configured_enabled:
|
||||
return L"enabled";
|
||||
default:
|
||||
return L"Unrecognized gpo_rule_configured_t value.";
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring gpo_string_to_string(const std::wstring &gpo_value)
|
||||
{
|
||||
if (gpo_value == L"")
|
||||
{
|
||||
return L"not_configured";
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::regex_replace(gpo_value, std::wregex(L"\r\n"), std::wstring(L"|"));
|
||||
}
|
||||
}
|
||||
|
||||
void ReportGPOValues(const std::filesystem::path &tmpDir)
|
||||
{
|
||||
auto reportPath = tmpDir;
|
||||
reportPath.append(L"gpo-configuration-info.txt");
|
||||
std::wofstream report(reportPath);
|
||||
report << "GPO policies configuration" << std::endl;
|
||||
report << "getConfiguredAdvancedPasteEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredAdvancedPasteEnabledValue()) << std::endl;
|
||||
report << "getConfiguredAlwaysOnTopEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredAlwaysOnTopEnabledValue()) << std::endl;
|
||||
report << "getConfiguredAwakeEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredAwakeEnabledValue()) << std::endl;
|
||||
report << "getConfiguredCmdPalEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredCmdPalEnabledValue()) << std::endl;
|
||||
report << "getConfiguredColorPickerEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredColorPickerEnabledValue()) << std::endl;
|
||||
report << "getConfiguredCropAndLockEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredCropAndLockEnabledValue()) << std::endl;
|
||||
report << "getConfiguredFancyZonesEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredFancyZonesEnabledValue()) << std::endl;
|
||||
report << "getConfiguredFileLocksmithEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredFileLocksmithEnabledValue()) << std::endl;
|
||||
report << "getConfiguredLightSwitchEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredLightSwitchEnabledValue()) << std::endl;
|
||||
report << "getConfiguredGrabAndMoveEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredGrabAndMoveEnabledValue()) << std::endl;
|
||||
report << "getConfiguredSvgPreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredSvgPreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMarkdownPreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMarkdownPreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMonacoPreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMonacoPreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredPdfPreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPdfPreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredGcodePreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredGcodePreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredBgcodePreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredBgcodePreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredSvgThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredSvgThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredPdfThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPdfThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredGcodeThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredGcodeThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredBgcodeThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredBgcodeThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredStlThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredStlThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredHostsFileEditorEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredHostsFileEditorEnabledValue()) << std::endl;
|
||||
report << "getConfiguredImageResizerEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredImageResizerEnabledValue()) << std::endl;
|
||||
report << "getConfiguredKeyboardManagerEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredKeyboardManagerEnabledValue()) << std::endl;
|
||||
report << "getConfiguredFindMyMouseEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredFindMyMouseEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMouseHighlighterEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMouseHighlighterEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMouseJumpEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMouseJumpEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMousePointerCrosshairsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMouseWithoutBordersEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMouseWithoutBordersEnabledValue()) << std::endl;
|
||||
report << "getConfiguredPowerRenameEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPowerRenameEnabledValue()) << std::endl;
|
||||
report << "getConfiguredPowerLauncherEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPowerLauncherEnabledValue()) << std::endl;
|
||||
report << "getConfiguredWorkspacesEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredWorkspacesEnabledValue()) << std::endl;
|
||||
report << "getConfiguredQuickAccentEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredQuickAccentEnabledValue()) << std::endl;
|
||||
report << "getConfiguredScreenRulerEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredScreenRulerEnabledValue()) << std::endl;
|
||||
report << "getConfiguredShortcutGuideEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredShortcutGuideEnabledValue()) << std::endl;
|
||||
report << "getConfiguredTextExtractorEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredTextExtractorEnabledValue()) << std::endl;
|
||||
report << "getConfiguredPeekEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPeekEnabledValue()) << std::endl;
|
||||
report << "getConfiguredZoomItEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredZoomItEnabledValue()) << std::endl;
|
||||
report << "getDisableAutomaticUpdateDownloadValue: " << gpo_rule_configured_to_string(powertoys_gpo::getDisableAutomaticUpdateDownloadValue()) << std::endl;
|
||||
report << "getSuspendNewUpdateToastValue: " << gpo_rule_configured_to_string(powertoys_gpo::getSuspendNewUpdateToastValue()) << std::endl;
|
||||
report << "getDisableNewUpdateToastValue: " << gpo_rule_configured_to_string(powertoys_gpo::getDisableNewUpdateToastValue()) << std::endl;
|
||||
report << "getDisableShowWhatsNewAfterUpdatesValue: " << gpo_rule_configured_to_string(powertoys_gpo::getDisableShowWhatsNewAfterUpdatesValue()) << std::endl;
|
||||
report << "getAllowExperimentationValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowExperimentationValue()) << std::endl;
|
||||
report << "getConfiguredQoiPreviewEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredQoiPreviewEnabledValue()) << std::endl;
|
||||
report << "getConfiguredQoiThumbnailsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredQoiThumbnailsEnabledValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteOnlineAIModelsValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteOpenAIValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteOpenAIValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteAzureOpenAIValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteAzureOpenAIValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteAzureAIInferenceValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteAzureAIInferenceValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteMistralValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteMistralValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteGoogleValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteGoogleValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteOllamaValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteOllamaValue()) << std::endl;
|
||||
report << "getAllowedAdvancedPasteFoundryLocalValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowedAdvancedPasteFoundryLocalValue()) << std::endl;
|
||||
report << "getConfiguredMwbClipboardSharingEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbClipboardSharingEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMwbFileTransferEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbFileTransferEnabledValue()) << std::endl;
|
||||
report << "getConfiguredMwbUseOriginalUserInterfaceValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbUseOriginalUserInterfaceValue()) << std::endl;
|
||||
report << "getConfiguredMwbDisallowBlockingScreensaverValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbDisallowBlockingScreensaverValue()) << std::endl;
|
||||
report << "getConfiguredMwbAllowServiceModeValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbAllowServiceModeValue()) << std::endl;
|
||||
report << "getConfiguredMwbSameSubnetOnlyValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbSameSubnetOnlyValue()) << std::endl;
|
||||
report << "getConfiguredMwbValidateRemoteIpValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbValidateRemoteIpValue()) << std::endl;
|
||||
report << "getConfiguredMwbDisableUserDefinedIpMappingRulesValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMwbDisableUserDefinedIpMappingRulesValue()) << std::endl;
|
||||
report << "getConfiguredMwbPolicyDefinedIpMappingRules: " << gpo_string_to_string(powertoys_gpo::getConfiguredMwbPolicyDefinedIpMappingRules()) << std::endl;
|
||||
report << "getConfiguredNewPlusEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredNewPlusEnabledValue()) << std::endl;
|
||||
report << "getConfiguredNewPlusHideTemplateFilenameExtensionValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredNewPlusHideTemplateFilenameExtensionValue()) << std::endl;
|
||||
report << "getAllowDataDiagnosticsValue: " << gpo_rule_configured_to_string(powertoys_gpo::getAllowDataDiagnosticsValue()) << std::endl;
|
||||
report << "getConfiguredRunAtStartupValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredRunAtStartupValue()) << std::endl;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
void ReportGPOValues(const std::filesystem::path& tmpDir);
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
#include "ReportMonitorInfo.h"
|
||||
#include <Windows.h>
|
||||
#include <filesystem>
|
||||
#include "../../../src/common/utils/winapi_error.h"
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
int BuildMonitorInfoReport(std::wostream& os)
|
||||
{
|
||||
struct capture
|
||||
{
|
||||
std::wostream* os = nullptr;
|
||||
};
|
||||
|
||||
auto callback = [](HMONITOR monitor, HDC, RECT*, LPARAM prm) -> BOOL {
|
||||
std::wostream& os = *(reinterpret_cast<capture*>(prm))->os;
|
||||
MONITORINFOEX mi;
|
||||
mi.cbSize = sizeof(mi);
|
||||
|
||||
if (GetMonitorInfoW(monitor, &mi))
|
||||
{
|
||||
os << "GetMonitorInfo OK\n";
|
||||
DISPLAY_DEVICE displayDevice = { sizeof(displayDevice) };
|
||||
|
||||
DWORD i = 0;
|
||||
while (EnumDisplayDevicesW(mi.szDevice, i++, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
|
||||
{
|
||||
const bool active = displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE;
|
||||
const bool mirroring = displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER;
|
||||
os << "EnumDisplayDevices OK:\n"
|
||||
<< "\tMirroring = " << mirroring << '\n'
|
||||
<< "\tActive = " << active << '\n'
|
||||
<< "\tDeviceID = " << displayDevice.DeviceID << '\n'
|
||||
<< "\tDeviceKey = " << displayDevice.DeviceKey << '\n'
|
||||
<< "\tDeviceName = " << displayDevice.DeviceName << '\n'
|
||||
<< "\tDeviceString = " << displayDevice.DeviceString << '\n';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto message = get_last_error_message(GetLastError());
|
||||
os << "GetMonitorInfo FAILED: " << (message.has_value() ? message.value() : L"") << '\n';
|
||||
}
|
||||
return TRUE;
|
||||
};
|
||||
|
||||
capture c;
|
||||
c.os = &os;
|
||||
if (EnumDisplayMonitors(nullptr, nullptr, callback, reinterpret_cast<LPARAM>(& c)))
|
||||
{
|
||||
os << "EnumDisplayMonitors OK\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
auto message = get_last_error_message(GetLastError());
|
||||
os << "EnumDisplayMonitors FAILED: " << (message.has_value() ? message.value() : L"") << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ReportMonitorInfo(const filesystem::path& tmpDir)
|
||||
{
|
||||
auto monitorReportPath = tmpDir;
|
||||
monitorReportPath.append("monitor-report-info.txt");
|
||||
|
||||
try
|
||||
{
|
||||
wofstream monitorReport(monitorReportPath);
|
||||
monitorReport << "GetSystemMetrics = " << GetSystemMetrics(SM_CMONITORS) << '\n';
|
||||
BuildMonitorInfoReport(monitorReport);
|
||||
}
|
||||
catch (std::exception& ex)
|
||||
{
|
||||
printf("Failed to report monitor info. %s\n", ex.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
printf("Failed to report monitor info\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <fstream>
|
||||
|
||||
void ReportMonitorInfo(const std::filesystem::path& tmpDir);
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "XmlDocumentEx.h"
|
||||
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
std::wstring XmlDocumentEx::GetFormatedXml()
|
||||
{
|
||||
stream.clear();
|
||||
Print(FirstChild(), 0);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void XmlDocumentEx::Print(winrt::Windows::Data::Xml::Dom::IXmlNode node, int indentation)
|
||||
{
|
||||
for (int i = 0; i < indentation; i++)
|
||||
{
|
||||
stream << " ";
|
||||
}
|
||||
|
||||
PrintTagWithAttributes(node);
|
||||
if (!node.HasChildNodes())
|
||||
{
|
||||
stream << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.ChildNodes().Size() == 1 && !node.FirstChild().HasChildNodes())
|
||||
{
|
||||
stream << node.InnerText().c_str() << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
stream << std::endl;
|
||||
auto child = node.FirstChild();
|
||||
do
|
||||
{
|
||||
Print(child, indentation + 2);
|
||||
} while (child = child.NextSibling());
|
||||
|
||||
for (int i = 0; i < indentation; i++)
|
||||
{
|
||||
stream << " ";
|
||||
}
|
||||
stream << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
}
|
||||
|
||||
void XmlDocumentEx::PrintTagWithAttributes(winrt::Windows::Data::Xml::Dom::IXmlNode node)
|
||||
{
|
||||
stream << L"<" << node.NodeName().c_str();
|
||||
for (int i = 0; i < (int)node.Attributes().Size(); i++)
|
||||
{
|
||||
auto attr = node.Attributes().GetAt(i);
|
||||
stream << L" " << attr.NodeName().c_str() << L"='" << attr.InnerText().c_str() << L"'";
|
||||
}
|
||||
|
||||
stream << L">";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <winrt/Windows.Data.Xml.Dom.h>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
class XmlDocumentEx : public winrt::Windows::Data::Xml::Dom::XmlDocument
|
||||
{
|
||||
private:
|
||||
std::wstringstream stream;
|
||||
void Print(winrt::Windows::Data::Xml::Dom::IXmlNode node, int indentation);
|
||||
void PrintTagWithAttributes(winrt::Windows::Data::Xml::Dom::IXmlNode node);
|
||||
|
||||
public:
|
||||
std::wstring GetFormatedXml();
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "ZipFolder.h"
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <format>
|
||||
#include <wil/stl.h>
|
||||
#include <wil/win32_helpers.h>
|
||||
|
||||
void ZipFolder(std::filesystem::path zipPath, std::filesystem::path folderPath)
|
||||
{
|
||||
const auto reportFilename{
|
||||
std::format("PowerToysReport_{0}.zip",
|
||||
timeutil::format_as_local("%F-%H-%M-%S", timeutil::now()))
|
||||
};
|
||||
const auto finalReportFullPath{ zipPath / reportFilename };
|
||||
|
||||
const auto tempReportFilename{ reportFilename + ".tmp" };
|
||||
const auto tempReportFullPath{ zipPath / tempReportFilename };
|
||||
|
||||
// tar -c --format=zip -f "ReportFile.zip" *
|
||||
const auto executable{ wil::ExpandEnvironmentStringsW<std::wstring>(LR"(%WINDIR%\System32\tar.exe)") };
|
||||
auto commandline{ std::format(LR"("{0}" -c --format=zip -f "{1}" *)", executable, tempReportFullPath.wstring()) };
|
||||
|
||||
const auto folderPathAsString{ folderPath.lexically_normal().wstring() };
|
||||
|
||||
wil::unique_process_information pi;
|
||||
STARTUPINFOW si{ .cb = sizeof(STARTUPINFOW) };
|
||||
if (!CreateProcessW(executable.c_str(),
|
||||
commandline.data() /* must be mutable */,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
DETACHED_PROCESS,
|
||||
nullptr,
|
||||
folderPathAsString.c_str(),
|
||||
&si,
|
||||
&pi))
|
||||
{
|
||||
printf("Cannot open zip.");
|
||||
throw -1;
|
||||
}
|
||||
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
|
||||
std::error_code err{};
|
||||
std::filesystem::rename(tempReportFullPath, finalReportFullPath, err);
|
||||
if (err.value() != 0)
|
||||
{
|
||||
wprintf_s(L"Failed to rename %s. Error code: %d\n", tempReportFullPath.native().c_str(), err.value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
void ZipFolder(std::filesystem::path zipPath, std::filesystem::path folderPath);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -0,0 +1,13 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by BugReportTool.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys BugReport Tool"
|
||||
#define INTERNAL_NAME "PowerToys.BugReportTool"
|
||||
#define ORIGINAL_FILENAME "PowerToys.BugReportTool.exe"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29613.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZone_HitTest", "FancyZone_HitTest\FancyZone_HitTest.csproj", "{4589E362-908E-4277-B2CE-F2257469356D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4589E362-908E-4277-B2CE-F2257469356D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4589E362-908E-4277-B2CE-F2257469356D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4589E362-908E-4277-B2CE-F2257469356D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4589E362-908E-4277-B2CE-F2257469356D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {10EA2631-C13F-4C50-9389-5BE4FCC66F2A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,8 @@
|
||||
<Application
|
||||
x:Class="FancyZone_HitTest.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:FancyZone_HitTest"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources />
|
||||
</Application>
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace FancyZone_HitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None,
|
||||
ResourceDictionaryLocation.SourceAssembly)]
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,61 @@
|
||||
<Window
|
||||
x:Class="FancyZone_HitTest.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:FancyZone_HitTest"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="MainWindow"
|
||||
Width="1024"
|
||||
Height="768"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Name="hitTestGrid" MouseMove="Grid_MouseMove">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Rectangle
|
||||
Name="a"
|
||||
Grid.Column="0"
|
||||
Fill="#ff0000" />
|
||||
<Rectangle
|
||||
Name="b"
|
||||
Grid.Column="1"
|
||||
Fill="#00ff00" />
|
||||
<Rectangle
|
||||
Name="c"
|
||||
Grid.Column="2"
|
||||
Fill="#0000ff" />
|
||||
<Rectangle
|
||||
Name="d"
|
||||
Grid.ColumnSpan="3"
|
||||
Width="550"
|
||||
Height="500"
|
||||
Fill="#aabbff" />
|
||||
<!-- Height="600" -->
|
||||
<Rectangle
|
||||
Name="f"
|
||||
Grid.ColumnSpan="3"
|
||||
Width="400"
|
||||
Height="400"
|
||||
Fill="#aabbff" />
|
||||
<Rectangle
|
||||
Name="e"
|
||||
Grid.ColumnSpan="3"
|
||||
Fill="Orange" />
|
||||
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock>Calculations</TextBlock>
|
||||
<TextBlock x:Name="itemsHit" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace FancyZone_HitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static ArrayList _hitResultsList = new ArrayList();
|
||||
private static ArrayList _visualCalculationList = new ArrayList();
|
||||
|
||||
private void Grid_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
// Retrieve the coordinate of the mouse position.
|
||||
Point gridMouseLocation = e.GetPosition((UIElement)sender);
|
||||
|
||||
// Clear the contents of the list used for hit test results.
|
||||
_hitResultsList.Clear();
|
||||
_visualCalculationList.Clear();
|
||||
|
||||
// Set up a callback to receive the hit test result enumeration.
|
||||
VisualTreeHelper.HitTest(hitTestGrid, null, new HitTestResultCallback(MyHitTestResult), new PointHitTestParameters(gridMouseLocation));
|
||||
|
||||
// Perform actions on the hit test results list.
|
||||
if (_hitResultsList.Count > 0)
|
||||
{
|
||||
foreach (Rectangle item in hitTestGrid.Children.OfType<Rectangle>())
|
||||
{
|
||||
item.Opacity = 0.25;
|
||||
item.StrokeThickness = 0;
|
||||
}
|
||||
|
||||
itemsHit.Text = string.Format("Number of Visuals Hit: {0}{1}", _hitResultsList.Count, Environment.NewLine);
|
||||
itemsHit.Text += string.Format("Grid mouse: {0}{1}", gridMouseLocation, Environment.NewLine);
|
||||
itemsHit.Text += Environment.NewLine;
|
||||
|
||||
foreach (Shape item in _hitResultsList)
|
||||
{
|
||||
_visualCalculationList.Add(new VisualData(item, e, hitTestGrid));
|
||||
}
|
||||
|
||||
var reorderedVisualData = _visualCalculationList.Cast<VisualData>().OrderBy(x => x, new VisualDataComparer<VisualData>());
|
||||
|
||||
foreach (VisualData item in reorderedVisualData)
|
||||
{
|
||||
itemsHit.Text += string.Format(
|
||||
"Name: {7}{0}" +
|
||||
"C Mouse: {4}{0}" +
|
||||
"Rel Mouse: {3}{0}" +
|
||||
"Edge Mouse: {8}{0}" +
|
||||
"Abs TL: {1}{0}" +
|
||||
"Center: {2}{0}" +
|
||||
"Area: {5}{0}" +
|
||||
"Edge %: {9}{0}" +
|
||||
"a/d: {6}{0}",
|
||||
Environment.NewLine,
|
||||
item.TopLeft, // 1
|
||||
item.CenterMass, // 2
|
||||
item.RelativeMouseLocation, // 3
|
||||
item.MouseDistanceFromCenter, // 4
|
||||
item.Area, // 5
|
||||
item.Area / item.MouseDistanceFromCenter, // 6
|
||||
item.Name, // 7
|
||||
item.DistanceFromEdge, // 8
|
||||
item.DistanceFromEdgePercentage); // 9
|
||||
itemsHit.Text += Environment.NewLine;
|
||||
}
|
||||
|
||||
if (reorderedVisualData.Count() > 0)
|
||||
{
|
||||
var rect = hitTestGrid.FindName(reorderedVisualData.First().Name) as Rectangle;
|
||||
rect.Opacity = .75;
|
||||
rect.Stroke = Brushes.Black;
|
||||
rect.StrokeThickness = 5;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
itemsHit.Text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static HitTestResultBehavior MyHitTestResult(HitTestResult result)
|
||||
{
|
||||
// Add the hit test result to the list that will be processed after the enumeration.
|
||||
_hitResultsList.Add(result.VisualHit);
|
||||
|
||||
// Set the behavior to return visuals at all z-order levels.
|
||||
return HitTestResultBehavior.Continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace FancyZone_HitTest
|
||||
{
|
||||
public static class Utilities
|
||||
{
|
||||
public static Point GetPosition(Visual element, Visual root)
|
||||
{
|
||||
var positionTransform = element.TransformToAncestor(root);
|
||||
var areaPosition = positionTransform.Transform(new Point(0, 0));
|
||||
|
||||
return areaPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace FancyZone_HitTest
|
||||
{
|
||||
public class VisualData
|
||||
{
|
||||
public Point RelativeMouseLocation { get; set; }
|
||||
|
||||
public Point CenterMass { get; set; }
|
||||
|
||||
public Point TopLeft { get; set; }
|
||||
|
||||
public double MouseDistanceFromCenter { get; set; }
|
||||
|
||||
public int Area { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public double DistanceFromEdge { get; set; }
|
||||
|
||||
public double DistanceFromEdgePercentage { get; set; }
|
||||
|
||||
public VisualData(Shape item, MouseEventArgs e, Visual root)
|
||||
{
|
||||
Name = item.Name;
|
||||
|
||||
int width = (int)Math.Floor(item.ActualWidth);
|
||||
int height = (int)Math.Floor(item.ActualHeight);
|
||||
|
||||
TopLeft = Utilities.GetPosition(item, root);
|
||||
CenterMass = new Point(width / 2, height / 2);
|
||||
Area = width * height;
|
||||
RelativeMouseLocation = e.GetPosition(item);
|
||||
MouseDistanceFromCenter = Point.Subtract(RelativeMouseLocation, CenterMass).Length;
|
||||
|
||||
var mouseX = Math.Floor(RelativeMouseLocation.X);
|
||||
var mouseY = Math.Floor(RelativeMouseLocation.Y);
|
||||
|
||||
var horDist = (RelativeMouseLocation.X < CenterMass.X) ? RelativeMouseLocation.X : width - mouseX;
|
||||
var vertDist = (RelativeMouseLocation.Y < CenterMass.Y) ? RelativeMouseLocation.Y : height - mouseY;
|
||||
|
||||
var isHorCalc = horDist < vertDist;
|
||||
DistanceFromEdge = Math.Floor(isHorCalc ? horDist : vertDist);
|
||||
|
||||
if (isHorCalc)
|
||||
{
|
||||
DistanceFromEdgePercentage = (width - DistanceFromEdge) / width;
|
||||
}
|
||||
else
|
||||
{
|
||||
DistanceFromEdgePercentage = (height - DistanceFromEdge) / height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.Collections.Generic;
|
||||
|
||||
namespace FancyZone_HitTest
|
||||
{
|
||||
public class VisualDataComparer<T> : IComparer<VisualData>
|
||||
{
|
||||
int IComparer<VisualData>.Compare(VisualData x, VisualData y)
|
||||
{
|
||||
// has quirks but workable
|
||||
if (x.DistanceFromEdge == y.DistanceFromEdge)
|
||||
{
|
||||
return y.Area.CompareTo(x.Area);
|
||||
}
|
||||
else
|
||||
{
|
||||
return x.DistanceFromEdge.CompareTo(y.DistanceFromEdge);
|
||||
}
|
||||
|
||||
// entire screen won't work
|
||||
/*
|
||||
if (x.MouseDistanceFromCenter == y.MouseDistanceFromCenter)
|
||||
{
|
||||
return y.Area.CompareTo(x.Area);
|
||||
}
|
||||
else
|
||||
{
|
||||
return x.MouseDistanceFromCenter.CompareTo(y.MouseDistanceFromCenter);
|
||||
}
|
||||
|
||||
if (x.DistanceFromEdgePercentage == y.DistanceFromEdgePercentage)
|
||||
{
|
||||
return y.Area.CompareTo(x.Area);
|
||||
}
|
||||
else
|
||||
{
|
||||
return x.DistanceFromEdgePercentage.CompareTo(y.DistanceFromEdgePercentage);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "framework.h"
|
||||
#include "FancyZones_DrawLayoutTest.h"
|
||||
|
||||
#include <Uxtheme.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
#include <Dwmapi.h>
|
||||
#include <shellscalingapi.h>
|
||||
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
#pragma comment (lib,"Gdiplus.lib")
|
||||
#pragma comment (lib,"UXTheme.lib")
|
||||
#pragma comment (lib,"Dwmapi.lib")
|
||||
#pragma comment (lib,"Shcore.lib")
|
||||
|
||||
constexpr int ZONE_COUNT = 4;
|
||||
constexpr int ANIMATION_TIME = 200; // milliseconds
|
||||
constexpr int DISPLAY_REFRESH_TIME = 10; // milliseconds
|
||||
constexpr DWORD Q_KEY_CODE = 0x51;
|
||||
constexpr DWORD W_KEY_CODE = 0x57;
|
||||
|
||||
HWND mainWindow;
|
||||
HHOOK keyboardHook;
|
||||
bool showZoneLayout = false;
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
std::vector<RECT> zones{};
|
||||
std::vector<bool> highlighted{};
|
||||
|
||||
inline const int RectWidth(const RECT& rect)
|
||||
{
|
||||
return rect.right - rect.left;
|
||||
}
|
||||
|
||||
inline const int RectHeight(const RECT& rect)
|
||||
{
|
||||
return rect.bottom - rect.top;
|
||||
}
|
||||
|
||||
std::vector<RECT> BuildColumnZoneLayout(int zoneCount, const RECT& workArea)
|
||||
{
|
||||
// Builds column layout with specified number of zones (columns).
|
||||
int zoneWidth = RectWidth(workArea) / zoneCount;
|
||||
int zoneHeight = RectHeight(workArea);
|
||||
std::vector<RECT> zones(zoneCount);
|
||||
for (int i = 0; i < zoneCount; ++i)
|
||||
{
|
||||
int left = workArea.left + i * zoneWidth;
|
||||
int top = workArea.top;
|
||||
int right = left + zoneWidth;
|
||||
int bottom = top + zoneHeight;
|
||||
|
||||
zones[i] = { left, top, right, bottom };
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
int GetHighlightedZoneIdx(const std::vector<RECT>& zones, const POINT& cursorPosition)
|
||||
{
|
||||
// Determine which zone should be highlighted based on cursor position.
|
||||
for (size_t i = 0; i < zones.size(); ++i)
|
||||
{
|
||||
if (cursorPosition.x >= zones[i].left && cursorPosition.x < zones[i].right)
|
||||
{
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ShowZonesOverlay()
|
||||
{
|
||||
// InvalidateRect will essentially send WM_PAINT to main window.
|
||||
UINT flags = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE;
|
||||
SetWindowPos(mainWindow, nullptr, 0, 0, 0, 0, flags);
|
||||
|
||||
std::thread{ [=]() {
|
||||
AnimateWindow(mainWindow, ANIMATION_TIME, AW_BLEND);
|
||||
InvalidateRect(mainWindow, nullptr, true);
|
||||
} }.detach();
|
||||
}
|
||||
|
||||
void HideZonesOverlay()
|
||||
{
|
||||
highlighted = std::vector<bool>(ZONE_COUNT, false);
|
||||
ShowWindow(mainWindow, SW_HIDE);
|
||||
}
|
||||
|
||||
void RefreshMainWindow()
|
||||
{
|
||||
while (1)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(DISPLAY_REFRESH_TIME));
|
||||
|
||||
POINT cursorPosition{};
|
||||
if (GetCursorPos(&cursorPosition))
|
||||
{
|
||||
if (showZoneLayout)
|
||||
{
|
||||
int idx = GetHighlightedZoneIdx(zones, cursorPosition);
|
||||
if (idx != -1)
|
||||
{
|
||||
if (highlighted[idx]) {
|
||||
// Same zone is active as in previous check, skip invalidating rect.
|
||||
}
|
||||
else
|
||||
{
|
||||
highlighted = std::vector<bool>(ZONE_COUNT, false);
|
||||
highlighted[idx] = true;
|
||||
|
||||
ShowZonesOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode == HC_ACTION && wParam == WM_KEYDOWN)
|
||||
{
|
||||
PKBDLLHOOKSTRUCT info = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if (info->vkCode == Q_KEY_CODE)
|
||||
{
|
||||
PostQuitMessage(0);
|
||||
return 1;
|
||||
}
|
||||
else if (info->vkCode == W_KEY_CODE)
|
||||
{
|
||||
// Toggle zone layout display.
|
||||
showZoneLayout = !showZoneLayout;
|
||||
if (showZoneLayout)
|
||||
{
|
||||
ShowZonesOverlay();
|
||||
}
|
||||
else
|
||||
{
|
||||
HideZonesOverlay();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
void StartLowLevelKeyboardHook()
|
||||
{
|
||||
keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(nullptr), 0);
|
||||
}
|
||||
|
||||
void StopLowLevelKeyboardHook()
|
||||
{
|
||||
if (keyboardHook)
|
||||
{
|
||||
UnhookWindowsHookEx(keyboardHook);
|
||||
keyboardHook = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inline void MakeWindowTransparent(HWND window)
|
||||
{
|
||||
int const pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
|
||||
if (HRGN hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) })
|
||||
{
|
||||
DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn, FALSE };
|
||||
DwmEnableBlurBehindWindow(window, &bh);
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterClass(HINSTANCE hInstance)
|
||||
{
|
||||
WNDCLASSEXW wcex{};
|
||||
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.lpfnWndProc = WndProc;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.lpszClassName = L"DrawRectangle_Test";
|
||||
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
|
||||
RegisterClassExW(&wcex);
|
||||
}
|
||||
|
||||
bool InitInstance(HINSTANCE hInstance, int nCmdShow)
|
||||
{
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof(mi);
|
||||
if (!GetMonitorInfo(MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mainWindow = CreateWindowExW(WS_EX_TOOLWINDOW,
|
||||
L"DrawRectangle_Test",
|
||||
L"",
|
||||
WS_POPUP,
|
||||
mi.rcWork.left,
|
||||
mi.rcWork.top,
|
||||
RectWidth(mi.rcWork),
|
||||
RectHeight(mi.rcWork),
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
nullptr);
|
||||
|
||||
if (mainWindow)
|
||||
{
|
||||
MakeWindowTransparent(mainWindow);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
|
||||
_In_opt_ HINSTANCE hPrevInstance,
|
||||
_In_ LPWSTR lpCmdLine,
|
||||
_In_ int nCmdShow)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(hPrevInstance);
|
||||
UNREFERENCED_PARAMETER(lpCmdLine);
|
||||
|
||||
GdiplusStartupInput gdiplusStartupInput;
|
||||
ULONG_PTR gdiplusToken;
|
||||
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
|
||||
|
||||
SetProcessDpiAwareness(PROCESS_DPI_UNAWARE);
|
||||
StartLowLevelKeyboardHook();
|
||||
|
||||
RegisterClass(hInstance);
|
||||
|
||||
if (!InitInstance(hInstance, nCmdShow))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
RECT clientRect{};
|
||||
GetClientRect(mainWindow, &clientRect);
|
||||
zones = BuildColumnZoneLayout(ZONE_COUNT, clientRect);
|
||||
highlighted = std::vector<bool>(ZONE_COUNT, false);
|
||||
|
||||
// Invoke main window re-drawing from separate thread (based on changes in cursor position).
|
||||
std::thread refreshThread = std::thread(RefreshMainWindow);
|
||||
refreshThread.detach();
|
||||
|
||||
|
||||
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FANCYZONESDRAWLAYOUTTEST));
|
||||
MSG msg{};
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
StopLowLevelKeyboardHook();
|
||||
GdiplusShutdown(gdiplusToken);
|
||||
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
struct ColorSetting
|
||||
{
|
||||
BYTE fillAlpha{};
|
||||
COLORREF fill{};
|
||||
BYTE borderAlpha{};
|
||||
COLORREF border{};
|
||||
int thickness{};
|
||||
};
|
||||
|
||||
inline void InitRGB(_Out_ RGBQUAD* quad, BYTE alpha, COLORREF color)
|
||||
{
|
||||
ZeroMemory(quad, sizeof(*quad));
|
||||
quad->rgbReserved = alpha;
|
||||
quad->rgbRed = GetRValue(color) * alpha / 255;
|
||||
quad->rgbGreen = GetGValue(color) * alpha / 255;
|
||||
quad->rgbBlue = GetBValue(color) * alpha / 255;
|
||||
}
|
||||
|
||||
inline void FillRectARGB(HDC hdc, const RECT& prcFill, BYTE alpha, COLORREF color, bool blendAlpha)
|
||||
{
|
||||
BITMAPINFO bi;
|
||||
ZeroMemory(&bi, sizeof(bi));
|
||||
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bi.bmiHeader.biWidth = 1;
|
||||
bi.bmiHeader.biHeight = 1;
|
||||
bi.bmiHeader.biPlanes = 1;
|
||||
bi.bmiHeader.biBitCount = 32;
|
||||
bi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
RECT fillRect;
|
||||
CopyRect(&fillRect, &prcFill);
|
||||
|
||||
RGBQUAD bitmapBits;
|
||||
InitRGB(&bitmapBits, alpha, color);
|
||||
StretchDIBits(
|
||||
hdc,
|
||||
fillRect.left,
|
||||
fillRect.top,
|
||||
fillRect.right - fillRect.left,
|
||||
fillRect.bottom - fillRect.top,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
&bitmapBits,
|
||||
&bi,
|
||||
DIB_RGB_COLORS,
|
||||
SRCCOPY);
|
||||
}
|
||||
|
||||
void DrawBackdrop(HDC& hdc, const RECT& clientRect)
|
||||
{
|
||||
FillRectARGB(hdc, clientRect, 0, RGB(0, 0, 0), false);
|
||||
}
|
||||
|
||||
void DrawIndex(HDC hdc, const RECT& rect, size_t index)
|
||||
{
|
||||
Gdiplus::Graphics g(hdc);
|
||||
|
||||
Gdiplus::FontFamily fontFamily(L"Segoe ui");
|
||||
Gdiplus::Font font(&fontFamily, 80, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
|
||||
Gdiplus::SolidBrush solidBrush(Gdiplus::Color(255, 0, 0, 0));
|
||||
|
||||
std::wstring text = std::to_wstring(index);
|
||||
|
||||
g.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
|
||||
Gdiplus::StringFormat stringFormat = new Gdiplus::StringFormat();
|
||||
stringFormat.SetAlignment(Gdiplus::StringAlignmentCenter);
|
||||
stringFormat.SetLineAlignment(Gdiplus::StringAlignmentCenter);
|
||||
|
||||
Gdiplus::RectF gdiRect(
|
||||
static_cast<Gdiplus::REAL>(rect.left),
|
||||
static_cast<Gdiplus::REAL>(rect.top),
|
||||
static_cast<Gdiplus::REAL>(RectWidth(rect)),
|
||||
static_cast<Gdiplus::REAL>(RectHeight(rect)));
|
||||
|
||||
g.DrawString(text.c_str(), -1, &font, gdiRect, &stringFormat, &solidBrush);
|
||||
}
|
||||
|
||||
void DrawZone(HDC hdc, const ColorSetting& colorSetting, const RECT& rect, size_t index)
|
||||
{
|
||||
Gdiplus::Graphics g(hdc);
|
||||
Gdiplus::Color fillColor(colorSetting.fillAlpha, GetRValue(colorSetting.fill), GetGValue(colorSetting.fill), GetBValue(colorSetting.fill));
|
||||
Gdiplus::Color borderColor(colorSetting.borderAlpha, GetRValue(colorSetting.border), GetGValue(colorSetting.border), GetBValue(colorSetting.border));
|
||||
|
||||
Gdiplus::Rect rectangle(rect.left, rect.top, RectWidth(rect), RectHeight(rect));
|
||||
|
||||
Gdiplus::Pen pen(borderColor, static_cast<Gdiplus::REAL>(colorSetting.thickness));
|
||||
g.FillRectangle(new Gdiplus::SolidBrush(fillColor), rectangle);
|
||||
g.DrawRectangle(&pen, rectangle);
|
||||
|
||||
DrawIndex(hdc, rect, index);
|
||||
}
|
||||
|
||||
constexpr inline BYTE OpacitySettingToAlpha(int opacity)
|
||||
{
|
||||
return static_cast<BYTE>(opacity * 2.55);
|
||||
}
|
||||
|
||||
COLORREF ParseColor(const std::wstring& zoneColor)
|
||||
{
|
||||
// Skip the leading # and convert to long
|
||||
const auto color = zoneColor;
|
||||
const auto tmp = std::stol(color.substr(1), nullptr, 16);
|
||||
const auto nR = (tmp & 0xFF0000) >> 16;
|
||||
const auto nG = (tmp & 0xFF00) >> 8;
|
||||
const auto nB = (tmp & 0xFF);
|
||||
return RGB(nR, nG, nB);
|
||||
}
|
||||
|
||||
static int highlightedIdx = -1;
|
||||
|
||||
void OnPaint(HDC hdc)
|
||||
{
|
||||
int zoneOpacity = 50;
|
||||
std::wstring zoneColor = L"#0078D7";
|
||||
std::wstring zoneBorderColor = L"#FFFFFF";
|
||||
std::wstring zoneHighlightColor = L"#F5FCFF";
|
||||
|
||||
ColorSetting color{ OpacitySettingToAlpha(zoneOpacity),
|
||||
ParseColor(zoneColor),
|
||||
255,
|
||||
ParseColor(zoneBorderColor),
|
||||
-2 };
|
||||
|
||||
ColorSetting highlight{ OpacitySettingToAlpha(zoneOpacity),
|
||||
ParseColor(zoneHighlightColor),
|
||||
255,
|
||||
ParseColor(zoneBorderColor),
|
||||
-2 };
|
||||
|
||||
HMONITOR monitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
|
||||
MONITORINFOEX mi;
|
||||
mi.cbSize = sizeof(mi);
|
||||
GetMonitorInfo(monitor, &mi);
|
||||
|
||||
HDC hdcMem{ nullptr };
|
||||
HPAINTBUFFER bufferedPaint = BeginBufferedPaint(hdc, &mi.rcWork, BPBF_TOPDOWNDIB, nullptr, &hdcMem);
|
||||
if (bufferedPaint)
|
||||
{
|
||||
DrawBackdrop(hdcMem, mi.rcWork);
|
||||
for (size_t i = 0; i < zones.size(); ++i)
|
||||
{
|
||||
if (highlighted[i])
|
||||
{
|
||||
DrawZone(hdcMem, color, zones[i], i);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawZone(hdcMem, highlight, zones[i], i);
|
||||
}
|
||||
}
|
||||
EndBufferedPaint(bufferedPaint, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_NCDESTROY:
|
||||
{
|
||||
DefWindowProc(mainWindow, message, wParam, lParam);
|
||||
SetWindowLongPtr(mainWindow, GWLP_USERDATA, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_PRINTCLIENT:
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hWnd, &ps);
|
||||
OnPaint(hdc);
|
||||
EndPaint(hWnd, &ps);
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "Resource.h"
|
||||
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30413.136
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones_DrawLayoutTest", "FancyZones_DrawLayoutTest.vcxproj", "{AB0F7153-208E-433D-A14F-311E09FDC0A0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Debug|x64.Build.0 = Debug|x64
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Debug|x86.Build.0 = Debug|Win32
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Release|x64.ActiveCfg = Release|x64
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Release|x64.Build.0 = Release|x64
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Release|x86.ActiveCfg = Release|Win32
|
||||
{AB0F7153-208E-433D-A14F-311E09FDC0A0}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B8997617-5BA5-4762-9DD0-E133E4138D08}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{ab0f7153-208e-433d-a14f-311e09fdc0a0}</ProjectGuid>
|
||||
<RootNamespace>FancyZonesDrawLayoutTest</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</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 Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="FancyZones_DrawLayoutTest.h" />
|
||||
<ClInclude Include="framework.h" />
|
||||
<ClInclude Include="Resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="FancyZones_DrawLayoutTest.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="FancyZones_DrawLayoutTest.rc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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;c++;cppm;ixx;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;h++;hm;inl;inc;ipp;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="framework.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FancyZones_DrawLayoutTest.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="FancyZones_DrawLayoutTest.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="FancyZones_DrawLayoutTest.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
#define IDS_APP_TITLE 101
|
||||
#define IDM_ABOUT 102
|
||||
#define IDC_FANCYZONESDRAWLAYOUTTEST 103
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
// Windows Header Files
|
||||
#include <windows.h>
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29519.87
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones_zonable_tester", "FancyZones_zonable_tester.vcxproj", "{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Debug|x64.Build.0 = Debug|x64
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Debug|x86.Build.0 = Debug|Win32
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Release|x64.ActiveCfg = Release|x64
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Release|x64.Build.0 = Release|x64
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Release|x86.ActiveCfg = Release|Win32
|
||||
{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FB4BCA3C-13A3-4CFD-AF4C-7A6AF4D73895}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{784E01FE-25D9-4E7B-9F2D-30B71B66FB05}</ProjectGuid>
|
||||
<RootNamespace>FancyZoneszonabletester</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</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 />
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,262 @@
|
||||
#include <Windows.h>
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
auto process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid);
|
||||
std::wstring name;
|
||||
if (process != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD name_length = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process, 0, static_cast<LPWSTR>(name.data()), &name_length) == 0)
|
||||
{
|
||||
name_length = 0;
|
||||
}
|
||||
name.resize(name_length);
|
||||
CloseHandle(process);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
if (name.length() >= app_frame_host.length() &&
|
||||
name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
|
||||
{
|
||||
// It is a UWP app. We will enumerate the windows and look for one created
|
||||
// by something with a different PID
|
||||
DWORD new_pid = pid;
|
||||
EnumChildWindows(window, [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != *new_pid_ptr)
|
||||
{
|
||||
*new_pid_ptr = pid;
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}, reinterpret_cast<LPARAM>(&new_pid));
|
||||
// If we have a new pid, get the new name.
|
||||
if (new_pid != pid)
|
||||
{
|
||||
return get_process_path(new_pid);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string window_styles(LONG style)
|
||||
{
|
||||
std::string result;
|
||||
if (style == 0)
|
||||
result = "WS_OVERLAPPED ";
|
||||
#define TEST_STYLE(x) if ((style & x) == x) result += #x " ";
|
||||
TEST_STYLE(WS_POPUP);
|
||||
TEST_STYLE(WS_CHILD);
|
||||
TEST_STYLE(WS_MINIMIZE);
|
||||
TEST_STYLE(WS_VISIBLE);
|
||||
TEST_STYLE(WS_DISABLED);
|
||||
TEST_STYLE(WS_CLIPSIBLINGS);
|
||||
TEST_STYLE(WS_CLIPCHILDREN);
|
||||
TEST_STYLE(WS_MAXIMIZE);
|
||||
TEST_STYLE(WS_CAPTION);
|
||||
TEST_STYLE(WS_BORDER);
|
||||
TEST_STYLE(WS_DLGFRAME);
|
||||
TEST_STYLE(WS_VSCROLL);
|
||||
TEST_STYLE(WS_HSCROLL);
|
||||
TEST_STYLE(WS_SYSMENU);
|
||||
TEST_STYLE(WS_THICKFRAME);
|
||||
TEST_STYLE(WS_GROUP);
|
||||
TEST_STYLE(WS_TABSTOP);
|
||||
TEST_STYLE(WS_MINIMIZEBOX);
|
||||
TEST_STYLE(WS_MAXIMIZEBOX);
|
||||
TEST_STYLE(WS_ICONIC);
|
||||
TEST_STYLE(WS_SIZEBOX);
|
||||
TEST_STYLE(WS_TILEDWINDOW);
|
||||
TEST_STYLE(WS_OVERLAPPEDWINDOW);
|
||||
TEST_STYLE(WS_POPUPWINDOW);
|
||||
TEST_STYLE(WS_CHILDWINDOW);
|
||||
#undef TEST_STYLE
|
||||
if (result.size() > 0)
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string window_exstyles(LONG style)
|
||||
{
|
||||
std::string result;
|
||||
#define TEST_STYLE(x) if ((style & x) == x) result += #x " ";
|
||||
TEST_STYLE(WS_EX_DLGMODALFRAME);
|
||||
TEST_STYLE(WS_EX_NOPARENTNOTIFY);
|
||||
TEST_STYLE(WS_EX_TOPMOST);
|
||||
TEST_STYLE(WS_EX_ACCEPTFILES);
|
||||
TEST_STYLE(WS_EX_TRANSPARENT);
|
||||
TEST_STYLE(WS_EX_MDICHILD);
|
||||
TEST_STYLE(WS_EX_TOOLWINDOW);
|
||||
TEST_STYLE(WS_EX_WINDOWEDGE);
|
||||
TEST_STYLE(WS_EX_CLIENTEDGE);
|
||||
TEST_STYLE(WS_EX_CONTEXTHELP);
|
||||
TEST_STYLE(WS_EX_RIGHT);
|
||||
TEST_STYLE(WS_EX_LEFT);
|
||||
TEST_STYLE(WS_EX_RTLREADING);
|
||||
TEST_STYLE(WS_EX_LTRREADING);
|
||||
TEST_STYLE(WS_EX_LEFTSCROLLBAR);
|
||||
TEST_STYLE(WS_EX_RIGHTSCROLLBAR);
|
||||
TEST_STYLE(WS_EX_CONTROLPARENT);
|
||||
TEST_STYLE(WS_EX_STATICEDGE);
|
||||
TEST_STYLE(WS_EX_APPWINDOW);
|
||||
TEST_STYLE(WS_EX_OVERLAPPEDWINDOW);
|
||||
TEST_STYLE(WS_EX_PALETTEWINDOW);
|
||||
TEST_STYLE(WS_EX_LAYERED);
|
||||
TEST_STYLE(WS_EX_NOINHERITLAYOUT);
|
||||
TEST_STYLE(WS_EX_NOREDIRECTIONBITMAP);
|
||||
TEST_STYLE(WS_EX_LAYOUTRTL);
|
||||
TEST_STYLE(WS_EX_COMPOSITED);
|
||||
#undef TEST_STYLE
|
||||
if (result.size() > 0)
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool is_system_window(HWND hwnd, const char* class_name)
|
||||
{
|
||||
static auto system_classes = { "SysListView32", "WorkerW", "Shell_TrayWnd", "Shell_SecondaryTrayWnd", "Progman" };
|
||||
static auto system_hwnds = { GetDesktopWindow(), GetShellWindow() };
|
||||
for (auto system_hwnd : system_hwnds)
|
||||
{
|
||||
if (hwnd == system_hwnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto& system_class : system_classes)
|
||||
{
|
||||
if (strcmp(system_class, class_name) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool no_visible_owner(HWND window) noexcept
|
||||
{
|
||||
auto owner = GetWindow(window, GW_OWNER);
|
||||
if (owner == nullptr)
|
||||
{
|
||||
return true; // There is no owner at all
|
||||
}
|
||||
if (!IsWindowVisible(owner))
|
||||
{
|
||||
return true; // Owner is invisible
|
||||
}
|
||||
RECT rect;
|
||||
if (!GetWindowRect(owner, &rect))
|
||||
{
|
||||
return false; // Could not get the rect, return true (and filter out the window) just in case
|
||||
}
|
||||
// Return false (and allow the window to be zonable) if the owner window size is zero
|
||||
// It is enough that the window is zero-sized in one dimension only.
|
||||
return rect.top == rect.bottom || rect.left == rect.right;
|
||||
}
|
||||
|
||||
#define TEST_IF(condition) std::cout<<"\t" << #condition <<": " <<((condition) ? "true, window not zonable" : "false")<<"\n"; if (condition) rv = false;
|
||||
|
||||
bool test_window(HWND window)
|
||||
{
|
||||
std::cout << "\n";
|
||||
std::cout << "HWND: 0x" << window << "\n";
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
std::cout << "PID: 0x" << std::hex << pid << "\n";
|
||||
std::cout << "FOREGROUND: 0x" << GetForegroundWindow() << "\n";
|
||||
|
||||
auto style = GetWindowLongPtr(window, GWL_STYLE);
|
||||
auto exStyle = GetWindowLongPtr(window, GWL_EXSTYLE);
|
||||
std::cout << "style: 0x" << std::hex << style << ": " << window_styles(static_cast<LONG>(style)) << "\n";
|
||||
std::cout << "exStyle: 0x" << std::hex << exStyle << ": " << window_exstyles(static_cast<LONG>(exStyle)) << " \n";
|
||||
std::array<char, 256> class_name;
|
||||
GetClassNameA(window, class_name.data(), static_cast<int>(class_name.size()));
|
||||
std::cout << "Window class: '" << class_name.data() << "' equals:\n";
|
||||
auto process_path = get_process_path(window);
|
||||
std::wcout<< L"Process path: " << process_path << L"\n";
|
||||
bool rv = true;
|
||||
std::cout << "Testing if the window is zonable:\n";
|
||||
TEST_IF(GetAncestor(window, GA_ROOT) != window);
|
||||
TEST_IF(!IsWindowVisible(window));
|
||||
if ((style & WS_POPUP) == WS_POPUP &&
|
||||
(style & WS_THICKFRAME) == 0 &&
|
||||
(style & WS_MINIMIZEBOX) == 0 &&
|
||||
(style & WS_MAXIMIZEBOX) == 0)
|
||||
{
|
||||
std::cout << "\t(style & WS_POPUP) && no frame nor max/min buttons: true, window not zonable\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "\t(style & WS_POPUP) && no frame nor max/min buttons: false\n";
|
||||
}
|
||||
TEST_IF((style & WS_CHILD) == WS_CHILD);
|
||||
TEST_IF((style & WS_DISABLED) == WS_DISABLED);
|
||||
TEST_IF((exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW);
|
||||
TEST_IF((exStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE);
|
||||
TEST_IF(is_system_window(window, class_name.data()));
|
||||
if (strcmp(class_name.data(), "Windows.UI.Core.CoreWindow") == 0 &&
|
||||
process_path.ends_with(L"SearchUI.exe"))
|
||||
{
|
||||
std::cout << "\tapp is Cortana: true, window not zonable\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "\tapp is Cortana: false\n";
|
||||
}
|
||||
TEST_IF(!no_visible_owner(window));
|
||||
return rv;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
static HWND hwnd = nullptr;
|
||||
if (nCode == HC_ACTION)
|
||||
{
|
||||
POINT point;
|
||||
GetCursorPos(&point);
|
||||
auto new_hwnd = WindowFromPoint(point);
|
||||
if (hwnd != new_hwnd) {
|
||||
hwnd = new_hwnd;
|
||||
if (test_window(hwnd))
|
||||
{
|
||||
std::cout << "Window is zonable\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Window is NOT zonable\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
HHOOK hhkLowLevelKybd = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, 0, 0);
|
||||
MSG msg;
|
||||
while (!GetMessage(&msg, NULL, NULL, NULL))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
UnhookWindowsHookEx(hhkLowLevelKybd);
|
||||
return(0);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <Windows.h>
|
||||
|
||||
inline std::optional<std::wstring> get_last_error_message(const DWORD dw)
|
||||
{
|
||||
std::optional<std::wstring> message;
|
||||
try
|
||||
{
|
||||
const auto msg = std::system_category().message(dw);
|
||||
message.emplace(begin(msg), end(msg));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
inline std::wstring get_last_error_or_default(const DWORD dw)
|
||||
{
|
||||
auto message = get_last_error_message(dw);
|
||||
return message.has_value() ? message.value() : L"";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
#include <shlobj.h>
|
||||
|
||||
std::filesystem::path get_desktop_path()
|
||||
{
|
||||
wchar_t* p;
|
||||
if (S_OK != SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &p)) return "";
|
||||
|
||||
std::filesystem::path result = p;
|
||||
CoTaskMemFree(p);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class Logger
|
||||
{
|
||||
private:
|
||||
inline static std::wofstream logger;
|
||||
|
||||
public:
|
||||
~Logger()
|
||||
{
|
||||
logger.close();
|
||||
}
|
||||
|
||||
static void init(std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(get_desktop_path());
|
||||
|
||||
auto logsPath = rootFolder;
|
||||
logsPath.append(L"monitor_ids.txt");
|
||||
|
||||
logger.open(logsPath.string(), std::ios_base::out | std::ios_base::app);
|
||||
}
|
||||
|
||||
template<typename FormatString, typename... Args>
|
||||
static void log(FormatString fmt, Args&&... args)
|
||||
{
|
||||
logger << std::vformat(fmt, std::make_wformat_args(args...)) << std::endl;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,541 @@
|
||||
#include "pch.h"
|
||||
#include "MonitorReportTool.h"
|
||||
|
||||
#include <WbemCli.h>
|
||||
#include <dwmapi.h>
|
||||
#include <comutil.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "ErrorMessage.h"
|
||||
#include "Logger.h"
|
||||
|
||||
namespace FancyZonesUtils
|
||||
{
|
||||
template<RECT MONITORINFO::* member>
|
||||
std::vector<std::pair<HMONITOR, MONITORINFOEX>> GetAllMonitorInfo()
|
||||
{
|
||||
using result_t = std::vector<std::pair<HMONITOR, MONITORINFOEX>>;
|
||||
result_t result;
|
||||
|
||||
auto enumMonitors = [](HMONITOR monitor, HDC, LPRECT, LPARAM param) -> BOOL {
|
||||
MONITORINFOEX mi;
|
||||
mi.cbSize = sizeof(mi);
|
||||
result_t& result = *reinterpret_cast<result_t*>(param);
|
||||
if (GetMonitorInfo(monitor, &mi))
|
||||
{
|
||||
result.push_back({ monitor, mi });
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
};
|
||||
|
||||
EnumDisplayMonitors(NULL, NULL, enumMonitors, reinterpret_cast<LPARAM>(&result));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void LogEnumDisplayMonitors()
|
||||
{
|
||||
Logger::log(L" ---- EnumDisplayMonitors as in FancyZones ---- ");
|
||||
|
||||
auto allMonitors = FancyZonesUtils::GetAllMonitorInfo<&MONITORINFOEX::rcWork>();
|
||||
std::unordered_map<std::wstring, DWORD> displayDeviceIdxMap;
|
||||
|
||||
for (auto& monitorData : allMonitors)
|
||||
{
|
||||
auto monitorInfo = monitorData.second;
|
||||
|
||||
DISPLAY_DEVICE displayDevice{ .cb = sizeof(displayDevice) };
|
||||
std::wstring deviceId;
|
||||
auto enumRes = EnumDisplayDevicesW(monitorInfo.szDevice, displayDeviceIdxMap[monitorInfo.szDevice], &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME);
|
||||
|
||||
if (enumRes == 0)
|
||||
{
|
||||
Logger::log(get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::log(L"DeviceId: {}", std::wstring(displayDevice.DeviceID));
|
||||
Logger::log(L"DeviceKey: {}", std::wstring(displayDevice.DeviceKey));
|
||||
Logger::log(L"DeviceName: {}", std::wstring(displayDevice.DeviceName));
|
||||
Logger::log(L"DeviceString: {}", std::wstring(displayDevice.DeviceString));
|
||||
Logger::log(L"");
|
||||
}
|
||||
}
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogPrintDisplayDevice(const DISPLAY_DEVICE& displayDevice, bool internal)
|
||||
{
|
||||
const bool active = displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE;
|
||||
const bool mirroring = displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER;
|
||||
const bool modesPruned = displayDevice.StateFlags & DISPLAY_DEVICE_MODESPRUNED;
|
||||
const bool primaryDevice = displayDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE;
|
||||
const bool removable = displayDevice.StateFlags & DISPLAY_DEVICE_REMOVABLE;
|
||||
const bool VGA_Compatible = displayDevice.StateFlags & DISPLAY_DEVICE_VGA_COMPATIBLE;
|
||||
|
||||
Logger::log(L"{}DeviceId: {}", internal?L"--> ":L"", std::wstring(displayDevice.DeviceID));
|
||||
Logger::log(L"{}DeviceKey: {}", internal?L"--> ":L"", std::wstring(displayDevice.DeviceKey));
|
||||
Logger::log(L"{}DeviceName: {}", internal?L"--> ":L"", std::wstring(displayDevice.DeviceName));
|
||||
Logger::log(L"{}DeviceString: {}", internal?L"--> ":L"", std::wstring(displayDevice.DeviceString));
|
||||
Logger::log(L"{}StateFlags: {}", internal?L"--> ":L"", displayDevice.StateFlags);
|
||||
Logger::log(L"{}active: {}", internal?L"--> ":L"", active);
|
||||
Logger::log(L"{}mirroring: {}", internal?L"--> ":L"", mirroring);
|
||||
Logger::log(L"{}modesPruned: {}", internal?L"--> ":L"", modesPruned);
|
||||
Logger::log(L"{}primaryDevice: {}", internal?L"--> ":L"", primaryDevice);
|
||||
Logger::log(L"{}removable: {}", internal?L"--> ":L"", removable);
|
||||
Logger::log(L"{}VGA_Compatible: {}", internal?L"--> ":L"", VGA_Compatible);
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogExhaustiveDisplayDevices(bool use_EDD_GET_DEVICE_INTERFACE_NAME)
|
||||
{
|
||||
Logger::log(L" ---- Exhaustive EnumDisplayDevicesW {} EDD_GET_DEVICE_INTERFACE_NAME ---- ", use_EDD_GET_DEVICE_INTERFACE_NAME?L"with":L"without");
|
||||
DISPLAY_DEVICE displayDevice{ .cb = sizeof(DISPLAY_DEVICE) };
|
||||
DWORD deviceIdx = 0;
|
||||
while (EnumDisplayDevicesW(nullptr, deviceIdx, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
|
||||
{
|
||||
LogPrintDisplayDevice(displayDevice, false);
|
||||
DISPLAY_DEVICE displayDeviceInternal{ .cb = sizeof(DISPLAY_DEVICE) };
|
||||
DWORD deviceIdxInternal = 0;
|
||||
while (EnumDisplayDevicesW(displayDevice.DeviceName, deviceIdxInternal, &displayDeviceInternal, EDD_GET_DEVICE_INTERFACE_NAME)) {
|
||||
Logger::log(L"Inside {} there's:", displayDevice.DeviceName);
|
||||
LogPrintDisplayDevice(displayDeviceInternal, true);
|
||||
deviceIdxInternal++;
|
||||
}
|
||||
deviceIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
void LogEnumDisplayMonitorsProper()
|
||||
{
|
||||
|
||||
auto allMonitors = FancyZonesUtils::GetAllMonitorInfo<&MONITORINFOEX::rcWork>();
|
||||
|
||||
Logger::log(L" ---- FancyZonesUtils::GetAllMonitorInfo ---- ");
|
||||
for (auto& monitorData : allMonitors)
|
||||
{
|
||||
auto monitorInfo = monitorData.second;
|
||||
Logger::log(L"szDevice: {}", std::wstring(monitorInfo.szDevice));
|
||||
Logger::log(L"cbSize: {}", monitorInfo.cbSize);
|
||||
Logger::log(L"dwFlags: {}", monitorInfo.dwFlags);
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
LogExhaustiveDisplayDevices(true);
|
||||
LogExhaustiveDisplayDevices(false);
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogWMIProp(IWbemClassObject* wbemClassObj, std::wstring_view prop)
|
||||
{
|
||||
if (!wbemClassObj)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VARIANT vtProp{};
|
||||
|
||||
// Get the value of the Name property
|
||||
auto hres = wbemClassObj->Get(prop.data(), 0, &vtProp, 0, 0);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Get {} Error code = {} ", prop, get_last_error_or_default(hres));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (vtProp.vt)
|
||||
{
|
||||
case VT_I2: //short
|
||||
{
|
||||
Logger::log(L"{} : {}", prop, vtProp.iVal);
|
||||
}
|
||||
break;
|
||||
case VT_I4: //int, long
|
||||
{
|
||||
Logger::log(L"{} : {}", prop, vtProp.lVal);
|
||||
}
|
||||
break;
|
||||
case VT_BSTR: //BSTR
|
||||
{
|
||||
Logger::log(L"{} : {}", prop, vtProp.bstrVal);
|
||||
}
|
||||
break;
|
||||
case VT_UI1: //BYTE (unsigned char)
|
||||
{
|
||||
Logger::log(L"{} : {}", prop, vtProp.bVal);
|
||||
}
|
||||
break;
|
||||
case VT_ARRAY: // parray
|
||||
case 8195: // also parray
|
||||
{
|
||||
std::u32string str(static_cast<const char32_t*>(vtProp.parray->pvData));
|
||||
std::wstring wstr;
|
||||
for (const char32_t& c : str)
|
||||
{
|
||||
wstr += (wchar_t)c;
|
||||
}
|
||||
|
||||
Logger::log(L"{} : {}", prop, wstr);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
Logger::log(L"{} : value is empty", prop);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
VariantClear(&vtProp);
|
||||
}
|
||||
|
||||
void LogWMI()
|
||||
{
|
||||
Logger::log(L" ---- WMI ---- ");
|
||||
|
||||
HRESULT hres;
|
||||
|
||||
// Initialize COM.
|
||||
hres = CoInitializeEx(0, COINIT_MULTITHREADED);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to initialize COM library. Error code = ", hres);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
hres = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT,
|
||||
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to initialize security. Error code = ", hres);
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtain the initial locator to Windows Management
|
||||
// on a particular host computer.
|
||||
IWbemLocator* pLocator = 0;
|
||||
|
||||
hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast<LPVOID*>(&pLocator));
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to create IWbemLocator object. Error code = ", hres);
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
IWbemServices* pServices = 0;
|
||||
hres = pLocator->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pServices);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Could not connect WMI server. Error code = ", hres);
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::log(L"Connected to ROOT\\WMI WMI namespace");
|
||||
Logger::log(L"");
|
||||
|
||||
|
||||
// Set the IWbemServices proxy so that impersonation
|
||||
// of the user (client) occurs.
|
||||
hres = CoSetProxyBlanket(pServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
|
||||
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Could not set proxy blanket. Error code = ", hres);
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the IWbemServices pointer to make requests of WMI.
|
||||
// Make requests here:
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
|
||||
hres = pServices->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM WmiMonitorID"),
|
||||
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Query for monitors failed. Error code = ", hres);
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
IWbemClassObject* pClassObject;
|
||||
ULONG uReturn = 0;
|
||||
|
||||
while (pEnumerator)
|
||||
{
|
||||
hres = pEnumerator->Next(WBEM_INFINITE, 1, &pClassObject, &uReturn);
|
||||
|
||||
if (0 == uReturn)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
LPSAFEARRAY pFieldArray = NULL;
|
||||
hres = pClassObject->GetNames(NULL, WBEM_FLAG_ALWAYS, NULL, &pFieldArray);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to get field names. Error code = {}", get_last_error_or_default(hres));
|
||||
break;
|
||||
}
|
||||
|
||||
LogWMIProp(pClassObject, L"InstanceName");
|
||||
|
||||
LogWMIProp(pClassObject, L"YearOfManufacture");
|
||||
LogWMIProp(pClassObject, L"WeekOfManufacture");
|
||||
|
||||
LogWMIProp(pClassObject, L"UserFriendlyNameLength");
|
||||
LogWMIProp(pClassObject, L"UserFriendlyName");
|
||||
LogWMIProp(pClassObject, L"ManufacturerName");
|
||||
|
||||
LogWMIProp(pClassObject, L"SerialNumberID");
|
||||
LogWMIProp(pClassObject, L"ProductCodeID");
|
||||
|
||||
Logger::log(L"");
|
||||
|
||||
pClassObject->Release();
|
||||
pClassObject = NULL;
|
||||
}
|
||||
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
pEnumerator->Release();
|
||||
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
void LogWMICIMV2()
|
||||
{
|
||||
Logger::log(L" ---- WMI ---- ");
|
||||
|
||||
HRESULT hres;
|
||||
|
||||
hres = CoInitializeEx(0, COINIT_MULTITHREADED);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to initialize COM library. Error code = ", hres);
|
||||
return;
|
||||
}
|
||||
|
||||
hres = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT,
|
||||
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to initialize security. Error code = ", hres);
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtain the initial locator to Windows Management
|
||||
// on a particular host computer.
|
||||
IWbemLocator* pLocator = 0;
|
||||
|
||||
hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast<LPVOID*>(&pLocator));
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Failed to create IWbemLocator object. Error code = ", hres);
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
IWbemServices* pServices = 0;
|
||||
|
||||
// Connect to the root\cimv2 namespace with the
|
||||
// current user and obtain pointer pSvc
|
||||
// to make IWbemServices calls.
|
||||
|
||||
hres = pLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0, NULL, 0, 0, &pServices);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Could not connect WMI server. Error code = ", hres);
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::log(L"Connected to ROOT\\CIMV2 WMI namespace");
|
||||
Logger::log(L"");
|
||||
|
||||
// Set the IWbemServices proxy so that impersonation
|
||||
// of the user (client) occurs.
|
||||
hres = CoSetProxyBlanket(pServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
|
||||
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Could not set proxy blanket. Error code = ", hres);
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the IWbemServices pointer to make requests of WMI.
|
||||
// Make requests here:
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
hres = pServices->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM Win32_DesktopMonitor"),
|
||||
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator);
|
||||
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::log(L"Query for monitors failed. Error code = ", hres);
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
CoUninitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
IWbemClassObject* pClassObject;
|
||||
ULONG uReturn = 0;
|
||||
|
||||
while (pEnumerator)
|
||||
{
|
||||
hres = pEnumerator->Next(WBEM_INFINITE, 1, &pClassObject, &uReturn);
|
||||
|
||||
if (0 == uReturn)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
LogWMIProp(pClassObject, L"DeviceID");
|
||||
LogWMIProp(pClassObject, L"Caption");
|
||||
LogWMIProp(pClassObject, L"Description");
|
||||
LogWMIProp(pClassObject, L"MonitorManufacturer");
|
||||
LogWMIProp(pClassObject, L"MonitorType");
|
||||
LogWMIProp(pClassObject, L"Name");
|
||||
LogWMIProp(pClassObject, L"PNPDeviceID");
|
||||
LogWMIProp(pClassObject, L"Status");
|
||||
|
||||
LogWMIProp(pClassObject, L"Availability");
|
||||
|
||||
Logger::log(L"");
|
||||
|
||||
pClassObject->Release();
|
||||
pClassObject = NULL;
|
||||
}
|
||||
|
||||
pServices->Release();
|
||||
pLocator->Release();
|
||||
pEnumerator->Release();
|
||||
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
void LogCCD()
|
||||
{
|
||||
Logger::log(L" ---- CCD ---- ");
|
||||
|
||||
LONG result = ERROR_SUCCESS;
|
||||
std::vector<DISPLAYCONFIG_PATH_INFO> paths;
|
||||
std::vector<DISPLAYCONFIG_MODE_INFO> modes;
|
||||
|
||||
do
|
||||
{
|
||||
UINT32 pathCount{}, modeCount{};
|
||||
auto sizesResult = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS | QDC_INCLUDE_HMD | QDC_VIRTUAL_MODE_AWARE, &pathCount, &modeCount);
|
||||
|
||||
if (sizesResult != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::log(L"GetDisplayConfigBufferSizes error {}", get_last_error_or_default(sizesResult));
|
||||
return;
|
||||
}
|
||||
|
||||
paths.resize(pathCount);
|
||||
paths.resize(modeCount);
|
||||
|
||||
auto result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS | QDC_INCLUDE_HMD | QDC_VIRTUAL_MODE_AWARE, &pathCount, paths.data()
|
||||
, &modeCount, modes.data(), nullptr);
|
||||
|
||||
// The function may have returned fewer paths/modes than estimated
|
||||
paths.resize(pathCount);
|
||||
modes.resize(modeCount);
|
||||
} while (result == ERROR_INSUFFICIENT_BUFFER);
|
||||
|
||||
if (result != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::log(L"QueryDisplayConfig error {}", get_last_error_or_default(result));
|
||||
return;
|
||||
}
|
||||
|
||||
// For each active path
|
||||
for (auto& path : paths)
|
||||
{
|
||||
// Find the target (monitor) friendly name
|
||||
DISPLAYCONFIG_TARGET_DEVICE_NAME targetName = {};
|
||||
targetName.header.adapterId = path.targetInfo.adapterId;
|
||||
targetName.header.id = path.targetInfo.id;
|
||||
targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
|
||||
targetName.header.size = sizeof(targetName);
|
||||
result = DisplayConfigGetDeviceInfo(&targetName.header);
|
||||
|
||||
if (result != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::log(L"DisplayConfigGetDeviceInfo error {}", get_last_error_or_default(result));
|
||||
}
|
||||
|
||||
// Find the adapter device name
|
||||
DISPLAYCONFIG_ADAPTER_NAME adapterName = {};
|
||||
adapterName.header.adapterId = path.targetInfo.adapterId;
|
||||
adapterName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME;
|
||||
adapterName.header.size = sizeof(adapterName);
|
||||
|
||||
result = DisplayConfigGetDeviceInfo(&adapterName.header);
|
||||
|
||||
if (result != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::log(L"DisplayConfigGetDeviceInfo error {}", get_last_error_or_default(result));
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::log(L"Monitor: {} connected to adapter {}"
|
||||
, (targetName.flags.friendlyNameFromEdid ? targetName.monitorFriendlyDeviceName : L"Unknown")
|
||||
, adapterName.adapterDevicePath);
|
||||
}
|
||||
}
|
||||
|
||||
void LogInfo()
|
||||
{
|
||||
Logger::log(L"Timestamp: {}", std::chrono::system_clock::now());
|
||||
Logger::log(L"");
|
||||
|
||||
LogEnumDisplayMonitors();
|
||||
LogEnumDisplayMonitorsProper();
|
||||
LogWMICIMV2();
|
||||
LogWMI();
|
||||
LogCCD();
|
||||
|
||||
Logger::log(L"=======================================");
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
|
||||
_In_opt_ HINSTANCE hPrevInstance,
|
||||
_In_ LPWSTR lpCmdLine,
|
||||
_In_ int nCmdShow)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(hPrevInstance);
|
||||
UNREFERENCED_PARAMETER(lpCmdLine);
|
||||
|
||||
Logger::init("MonitorReportTool");
|
||||
|
||||
LogInfo();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MonitorReportTool", "MonitorReportTool.vcxproj", "{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
Debug|x64 = Debug|x64
|
||||
Release|ARM64 = Release|ARM64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|x64.Build.0 = Debug|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|x64.ActiveCfg = Release|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D271E9C3-5C4D-45E4-8D23-9F1BB5443513}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{0febafaf-cb77-41f0-8ef9-8e3ad4b4f842}</ProjectGuid>
|
||||
<RootNamespace>MonitorReportTool</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</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 Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.$(ProjectName)</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dwmapi.lib;comsuppwd.lib;wbemuuid.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dwmapi.lib;comsuppw.lib;wbemuuid.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ErrorMessage.h" />
|
||||
<ClInclude Include="Logger.h" />
|
||||
<ClInclude Include="MonitorReportTool.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="Resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="MonitorReportTool.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="MonitorReportTool.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="MonitorReportTool.ico" />
|
||||
<Image Include="small.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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;c++;cppm;ixx;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;h++;hm;inl;inc;ipp;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="Resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="MonitorReportTool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Logger.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ErrorMessage.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="MonitorReportTool.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="MonitorReportTool.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="small.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
<Image Include="MonitorReportTool.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by MonitorReportTool.rc
|
||||
|
||||
#define IDR_MAINFRAME 128
|
||||
#define IDD_ABOUTBOX 103
|
||||
#define IDI_WINDOWSTYLESICON 107
|
||||
#define IDI_SMALLICON 108
|
||||
#define IDC_WINDOWSTYLES 109
|
||||
#ifndef IDC_STATIC
|
||||
#define IDC_STATIC -1
|
||||
#endif
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
#define _APS_NO_MFC 130
|
||||
#define _APS_NEXT_RESOURCE_VALUE 129
|
||||
#define _APS_NEXT_COMMAND_VALUE 32771
|
||||
#define _APS_NEXT_CONTROL_VALUE 1000
|
||||
#define _APS_NEXT_SYMED_VALUE 110
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,28 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by StylesReportTool.rc
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys StylesReport Tool"
|
||||
#define INTERNAL_NAME "PowerToys.StylesReportTool"
|
||||
#define ORIGINAL_FILENAME "PowerToys.StylesReportTool.exe"
|
||||
|
||||
#define IDR_MAINFRAME 128
|
||||
#define IDD_ABOUTBOX 103
|
||||
#define IDI_WINDOWSTYLESICON 107
|
||||
#define IDI_SMALLICON 108
|
||||
#define IDC_WINDOWSTYLES 109
|
||||
#ifndef IDC_STATIC
|
||||
#define IDC_STATIC -1
|
||||
#endif
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
#define _APS_NO_MFC 130
|
||||
#define _APS_NEXT_RESOURCE_VALUE 129
|
||||
#define _APS_NEXT_COMMAND_VALUE 32771
|
||||
#define _APS_NEXT_CONTROL_VALUE 1000
|
||||
#define _APS_NEXT_SYMED_VALUE 110
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,425 @@
|
||||
#include "pch.h"
|
||||
#include "StylesReportTool.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <format>
|
||||
|
||||
inline std::optional<std::wstring> get_last_error_message(const DWORD dw)
|
||||
{
|
||||
std::optional<std::wstring> message;
|
||||
try
|
||||
{
|
||||
const auto msg = std::system_category().message(dw);
|
||||
message.emplace(begin(msg), end(msg));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
inline std::wstring get_last_error_or_default(const DWORD dw)
|
||||
{
|
||||
auto message = get_last_error_message(dw);
|
||||
return message.has_value() ? message.value() : L"";
|
||||
}
|
||||
|
||||
std::filesystem::path get_desktop_path()
|
||||
{
|
||||
wchar_t* p;
|
||||
if (S_OK != SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &p)) return "";
|
||||
|
||||
std::filesystem::path result = p;
|
||||
CoTaskMemFree(p);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
auto process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, TRUE, pid);
|
||||
std::wstring name;
|
||||
if (process != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD name_length = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process, 0, static_cast<LPWSTR>(name.data()), &name_length) == 0)
|
||||
{
|
||||
name_length = 0;
|
||||
}
|
||||
name.resize(name_length);
|
||||
CloseHandle(process);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
|
||||
if (name.length() >= app_frame_host.length() &&
|
||||
name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
|
||||
{
|
||||
// It is a UWP app. We will enumerate the windows and look for one created
|
||||
// by something with a different PID
|
||||
DWORD new_pid = pid;
|
||||
|
||||
EnumChildWindows(
|
||||
window, [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != *new_pid_ptr)
|
||||
{
|
||||
*new_pid_ptr = pid;
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&new_pid));
|
||||
|
||||
// If we have a new pid, get the new name.
|
||||
if (new_pid != pid)
|
||||
{
|
||||
return get_process_path(new_pid);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
class Logger
|
||||
{
|
||||
private:
|
||||
inline static std::wofstream logger;
|
||||
|
||||
public:
|
||||
~Logger()
|
||||
{
|
||||
logger.close();
|
||||
}
|
||||
|
||||
static void init(std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(get_desktop_path());
|
||||
|
||||
auto logsPath = rootFolder;
|
||||
logsPath.append(L"window_styles.txt");
|
||||
|
||||
logger.open(logsPath.string(), std::ios_base::out | std::ios_base::app);
|
||||
}
|
||||
|
||||
template<typename FormatString, typename... Args>
|
||||
static void log(FormatString fmt, Args&&... args)
|
||||
{
|
||||
logger << std::vformat(fmt, std::make_wformat_args(args...)) << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
std::map<DWMWINDOWATTRIBUTE, std::wstring> dwmAttributesReadable = {
|
||||
{DWMWINDOWATTRIBUTE::DWMWA_NCRENDERING_ENABLED, L"DWMWA_NCRENDERING_ENABLED"},
|
||||
{DWMWINDOWATTRIBUTE::DWMWA_CAPTION_BUTTON_BOUNDS, L"DWMWA_CAPTION_BUTTON_BOUNDS"},
|
||||
{DWMWINDOWATTRIBUTE::DWMWA_EXTENDED_FRAME_BOUNDS, L"DWMWA_EXTENDED_FRAME_BOUNDS"},
|
||||
{DWMWINDOWATTRIBUTE::DWMWA_CLOAKED, L"DWMWA_CLOAKED"},
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void LogDwmInfo(HWND window, DWMWINDOWATTRIBUTE attr, T& value)
|
||||
{
|
||||
if (DwmGetWindowAttribute(window, attr, &value, sizeof(value)) == S_OK)
|
||||
{
|
||||
Logger::log(L"{}: {} ", dwmAttributesReadable[attr], value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::log(L"Failed to get {}", dwmAttributesReadable[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
void LogDwmRect(HWND window, DWMWINDOWATTRIBUTE attr, RECT& value)
|
||||
{
|
||||
if (DwmGetWindowAttribute(window, attr, &value, sizeof(value)) >= 0)
|
||||
{
|
||||
Logger::log(L"{}: LT({},{}), RB({},{}), [{} x {}] ", dwmAttributesReadable[attr], value.left, value.top, value.right, value.bottom, value.right - value.left, value.bottom - value.top);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::log(L"Failed to get {}", dwmAttributesReadable[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
void LogStyles(HWND window)
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
|
||||
Logger::log(L"------------------ Style --------------------- ");
|
||||
Logger::log(L"");
|
||||
|
||||
Logger::log(L"WS_BORDER {}", ((style & WS_BORDER) == WS_BORDER));
|
||||
Logger::log(L"WS_CAPTION {}", ((style & WS_CAPTION) == WS_CAPTION));
|
||||
Logger::log(L"WS_CHILD {}", ((style & WS_CHILD) == WS_CHILD));
|
||||
Logger::log(L"WS_CHILDWINDOW {}", ((style & WS_CHILDWINDOW) == WS_CHILDWINDOW));
|
||||
Logger::log(L"WS_CLIPCHILDREN {}", ((style & WS_CLIPCHILDREN) == WS_CLIPCHILDREN));
|
||||
Logger::log(L"WS_CLIPSIBLINGS {}", ((style & WS_CLIPSIBLINGS) == WS_CLIPSIBLINGS));
|
||||
Logger::log(L"WS_DISABLED {}", ((style & WS_DISABLED) == WS_DISABLED));
|
||||
Logger::log(L"WS_DLGFRAME {}", ((style & WS_DLGFRAME) == WS_DLGFRAME));
|
||||
Logger::log(L"WS_GROUP {}", ((style & WS_GROUP) == WS_GROUP));
|
||||
Logger::log(L"WS_HSCROLL {}", ((style & WS_HSCROLL) == WS_HSCROLL));
|
||||
Logger::log(L"WS_ICONIC {}", ((style & WS_ICONIC) == WS_ICONIC));
|
||||
Logger::log(L"WS_MAXIMIZE {}", ((style & WS_MAXIMIZE) == WS_MAXIMIZE));
|
||||
Logger::log(L"WS_MAXIMIZEBOX {}", ((style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX));
|
||||
Logger::log(L"WS_MINIMIZE {}", ((style & WS_MINIMIZE) == WS_MINIMIZE));
|
||||
Logger::log(L"WS_MINIMIZEBOX {}", ((style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX));
|
||||
Logger::log(L"WS_OVERLAPPED {}", ((style & WS_OVERLAPPED) == WS_OVERLAPPED));
|
||||
Logger::log(L"WS_OVERLAPPEDWINDOW {}", ((style & WS_OVERLAPPEDWINDOW) == WS_OVERLAPPEDWINDOW));
|
||||
Logger::log(L"WS_POPUP {}", ((style & WS_POPUP) == WS_POPUP));
|
||||
Logger::log(L"WS_POPUPWINDOW {}", ((style & WS_POPUPWINDOW) == WS_POPUPWINDOW));
|
||||
Logger::log(L"WS_SIZEBOX {}", ((style & WS_SIZEBOX) == WS_SIZEBOX));
|
||||
Logger::log(L"WS_SYSMENU {}", ((style & WS_SYSMENU) == WS_SYSMENU));
|
||||
Logger::log(L"WS_TABSTOP {}", ((style & WS_TABSTOP) == WS_TABSTOP));
|
||||
Logger::log(L"WS_THICKFRAME {}", ((style & WS_THICKFRAME) == WS_THICKFRAME));
|
||||
Logger::log(L"WS_TILED {}", ((style & WS_TILED) == WS_TILED));
|
||||
Logger::log(L"WS_TILEDWINDOW {}", ((style & WS_TILEDWINDOW) == WS_TILEDWINDOW));
|
||||
Logger::log(L"WS_VISIBLE {}", ((style & WS_VISIBLE) == WS_VISIBLE));
|
||||
Logger::log(L"WS_VSCROLL {}", ((style & WS_VSCROLL) == WS_VSCROLL));
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogExStyles(HWND window)
|
||||
{
|
||||
auto exStyle = GetWindowLong(window, GWL_EXSTYLE);
|
||||
Logger::log(L"------------------ Exstyle --------------------- ");
|
||||
Logger::log(L"");
|
||||
|
||||
Logger::log(L"WS_EX_ACCEPTFILES {}", (exStyle & WS_EX_ACCEPTFILES) == WS_EX_ACCEPTFILES);
|
||||
Logger::log(L"WS_EX_APPWINDOW {}", (exStyle & WS_EX_APPWINDOW) == WS_EX_APPWINDOW);
|
||||
Logger::log(L"WS_EX_CLIENTEDGE {}", (exStyle & WS_EX_CLIENTEDGE) == WS_EX_CLIENTEDGE);
|
||||
Logger::log(L"WS_EX_COMPOSITED {}", (exStyle & WS_EX_COMPOSITED) == WS_EX_COMPOSITED);
|
||||
Logger::log(L"WS_EX_CONTEXTHELP {}", (exStyle & WS_EX_CONTEXTHELP) == WS_EX_CONTEXTHELP);
|
||||
Logger::log(L"WS_EX_CONTROLPARENT {}", (exStyle & WS_EX_CONTROLPARENT) == WS_EX_CONTROLPARENT);
|
||||
Logger::log(L"WS_EX_DLGMODALFRAME {}", (exStyle & WS_EX_DLGMODALFRAME) == WS_EX_DLGMODALFRAME);
|
||||
Logger::log(L"WS_EX_LAYERED {}", (exStyle & WS_EX_LAYERED) == WS_EX_LAYERED);
|
||||
Logger::log(L"WS_EX_LAYOUTRTL {}", (exStyle & WS_EX_LAYOUTRTL) == WS_EX_LAYOUTRTL);
|
||||
Logger::log(L"WS_EX_LEFT {}", (exStyle & WS_EX_LEFT) == WS_EX_LEFT);
|
||||
Logger::log(L"WS_EX_LEFTSCROLLBAR {}", (exStyle & WS_EX_LEFTSCROLLBAR) == WS_EX_LEFTSCROLLBAR);
|
||||
Logger::log(L"WS_EX_LTRREADING {}", (exStyle & WS_EX_LTRREADING) == WS_EX_LTRREADING);
|
||||
Logger::log(L"WS_EX_MDICHILD {}", (exStyle & WS_EX_MDICHILD) == WS_EX_MDICHILD);
|
||||
Logger::log(L"WS_EX_NOACTIVATE {}", (exStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE);
|
||||
Logger::log(L"WS_EX_NOINHERITLAYOUT {}", (exStyle & WS_EX_NOINHERITLAYOUT) == WS_EX_NOINHERITLAYOUT);
|
||||
Logger::log(L"WS_EX_NOPARENTNOTIFY {}", (exStyle & WS_EX_NOPARENTNOTIFY) == WS_EX_NOPARENTNOTIFY);
|
||||
Logger::log(L"WS_EX_NOREDIRECTIONBITMAP {}", (exStyle & WS_EX_NOREDIRECTIONBITMAP) == WS_EX_NOREDIRECTIONBITMAP);
|
||||
Logger::log(L"WS_EX_OVERLAPPEDWINDOW {}", (exStyle & WS_EX_OVERLAPPEDWINDOW) == WS_EX_OVERLAPPEDWINDOW);
|
||||
Logger::log(L"WS_EX_PALETTEWINDOW {}", (exStyle & WS_EX_PALETTEWINDOW) == WS_EX_PALETTEWINDOW);
|
||||
Logger::log(L"WS_EX_RIGHT {}", (exStyle & WS_EX_RIGHT) == WS_EX_RIGHT);
|
||||
Logger::log(L"WS_EX_RIGHTSCROLLBAR {}", (exStyle & WS_EX_RIGHTSCROLLBAR) == WS_EX_RIGHTSCROLLBAR);
|
||||
Logger::log(L"WS_EX_RTLREADING {}", (exStyle & WS_EX_RTLREADING) == WS_EX_RTLREADING);
|
||||
Logger::log(L"WS_EX_STATICEDGE {}", (exStyle & WS_EX_STATICEDGE) == WS_EX_STATICEDGE);
|
||||
Logger::log(L"WS_EX_TOOLWINDOW {}", (exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW);
|
||||
Logger::log(L"WS_EX_TOPMOST {}", (exStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST);
|
||||
Logger::log(L"WS_EX_TRANSPARENT {}", (exStyle & WS_EX_TRANSPARENT) == WS_EX_TRANSPARENT);
|
||||
Logger::log(L"WS_EX_WINDOWEDGE {}", (exStyle & WS_EX_WINDOWEDGE) == WS_EX_WINDOWEDGE);
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogDwmAttributes(HWND window)
|
||||
{
|
||||
Logger::log(L"------------------ DwmAttributes --------------------- ");
|
||||
Logger::log(L"");
|
||||
|
||||
int intValue{};
|
||||
unsigned int uintValue{};
|
||||
|
||||
LogDwmInfo(window, DWMWINDOWATTRIBUTE::DWMWA_NCRENDERING_ENABLED, intValue);
|
||||
LogDwmInfo(window, DWMWINDOWATTRIBUTE::DWMWA_CLOAKED, intValue);
|
||||
|
||||
RECT rectValue{};
|
||||
LogDwmRect(window, DWMWINDOWATTRIBUTE::DWMWA_CAPTION_BUTTON_BOUNDS, rectValue);
|
||||
LogDwmRect(window, DWMWINDOWATTRIBUTE::DWMWA_EXTENDED_FRAME_BOUNDS, rectValue);
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogVirtualDesktopInfo(HWND window)
|
||||
{
|
||||
Logger::log(L"------------------ VirtualDesktop info --------------------- ");
|
||||
Logger::log(L"");
|
||||
|
||||
IVirtualDesktopManager* vdManager = nullptr;
|
||||
auto res = CoCreateInstance(CLSID_VirtualDesktopManager, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&vdManager));
|
||||
if (FAILED(res))
|
||||
{
|
||||
Logger::log(L"Failed to create VirtualDesktopManager instance");
|
||||
Logger::log(L"");
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isWindowOnCurrentDesktop = false;
|
||||
if (vdManager->IsWindowOnCurrentVirtualDesktop(window, &isWindowOnCurrentDesktop) == S_OK)
|
||||
{
|
||||
Logger::log(L"Window is on current virtual desktop: {}", isWindowOnCurrentDesktop);
|
||||
}
|
||||
|
||||
GUID id{};
|
||||
auto vdIdRes = vdManager->GetWindowDesktopId(window, &id);
|
||||
if (vdIdRes == S_OK)
|
||||
{
|
||||
OLECHAR* guidString;
|
||||
if (StringFromCLSID(id, &guidString) == S_OK)
|
||||
{
|
||||
Logger::log(L"Virtual desktop id: {}", guidString);
|
||||
}
|
||||
|
||||
CoTaskMemFree(guidString);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::log(L"GetWindowDesktopId error: {}", get_last_error_or_default(vdIdRes));
|
||||
}
|
||||
|
||||
if (vdManager)
|
||||
{
|
||||
vdManager->Release();
|
||||
}
|
||||
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
void LogInfo(HWND window)
|
||||
{
|
||||
auto processPath = get_process_path(window);
|
||||
auto app = processPath;
|
||||
auto pos = processPath.find_last_of('\\');
|
||||
if (pos != std::string::npos && pos + 1 < processPath.length())
|
||||
{
|
||||
app = processPath.substr(pos + 1);
|
||||
}
|
||||
|
||||
Logger::log(L"Timestamp: {}", std::chrono::system_clock::now());
|
||||
Logger::log(L"Window: {}", app);
|
||||
|
||||
WCHAR className[256];
|
||||
auto classNameLength = GetClassName(window, className, sizeof(className));
|
||||
if (classNameLength > 0)
|
||||
{
|
||||
Logger::log(L"Class: {}", className);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::log(L"GetClassName error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
Logger::log(L"");
|
||||
|
||||
LogStyles(window);
|
||||
LogExStyles(window);
|
||||
LogDwmAttributes(window);
|
||||
LogVirtualDesktopInfo(window);
|
||||
|
||||
Logger::log(L"=======================================");
|
||||
Logger::log(L"");
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
|
||||
_In_opt_ HINSTANCE hPrevInstance,
|
||||
_In_ LPWSTR lpCmdLine,
|
||||
_In_ int nCmdShow)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(hPrevInstance);
|
||||
UNREFERENCED_PARAMETER(lpCmdLine);
|
||||
|
||||
Logger::init("StylesReportTool");
|
||||
|
||||
WNDCLASSEXW wcex;
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.style = {};
|
||||
wcex.lpfnWndProc = WndProc;
|
||||
wcex.cbClsExtra = 0;
|
||||
wcex.cbWndExtra = 0;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSTYLESICON));
|
||||
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
wcex.lpszMenuName = L"";
|
||||
wcex.lpszClassName = L"StylesReportTool";
|
||||
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALLICON));
|
||||
|
||||
if (!RegisterClassExW(&wcex))
|
||||
{
|
||||
Logger::log(L"Register class error: {}", get_last_error_or_default(GetLastError()));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
HWND hWnd = CreateWindowW(L"StylesReportTool", L"Window Style Report Tool", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 600, 200, nullptr, nullptr, hInstance, nullptr);
|
||||
if (!hWnd)
|
||||
{
|
||||
Logger::log(L"Window creation error: {}", get_last_error_or_default(GetLastError()));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!RegisterHotKey(hWnd, 1, MOD_ALT | MOD_CONTROL | MOD_NOREPEAT, 0x53)) // ctrl + alt + s
|
||||
{
|
||||
Logger::log(L"Failed to register hotkey: {}", get_last_error_or_default(GetLastError()));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ShowWindow(hWnd, nCmdShow);
|
||||
UpdateWindow(hWnd);
|
||||
|
||||
MSG msg{};
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
return (int) msg.wParam;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_HOTKEY:
|
||||
{
|
||||
LogInfo(GetForegroundWindow());
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
break;
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hWnd, &ps);
|
||||
|
||||
LPCWSTR text = L"Please select the target window (using a mouse or Alt+Tab), \r\nand press Ctrl+Alt+S to capture its styles. \r\nYou can find the output file \"window_styles.txt\" on your desktop.";
|
||||
RECT rc{0,50,600,200};
|
||||
DrawText(hdc, text, static_cast<int>(wcslen(text)), &rc, DT_CENTER | DT_WORDBREAK);
|
||||
|
||||
EndPaint(hWnd, &ps);
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StylesReportTool", "StylesReportTool.vcxproj", "{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
Debug|x64 = Debug|x64
|
||||
Release|ARM64 = Release|ARM64
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Debug|x64.Build.0 = Debug|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|x64.ActiveCfg = Release|x64
|
||||
{0FEBAFAF-CB77-41F0-8EF9-8E3AD4B4F842}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D271E9C3-5C4D-45E4-8D23-9F1BB5443513}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{0febafaf-cb77-41f0-8ef9-8e3ad4b4f842}</ProjectGuid>
|
||||
<RootNamespace>StylesReportTool</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</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 Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\$(ProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.$(ProjectName)</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="StylesReportTool.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="Resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="StylesReportTool.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="StylesReportTool.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="StylesReportTool.ico" />
|
||||
<Image Include="small.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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;c++;cppm;ixx;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;h++;hm;inl;inc;ipp;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="Resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="StylesReportTool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="StylesReportTool.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="StylesReportTool.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="small.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
<Image Include="StylesReportTool.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,193 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test the auto-label-product workflow logic locally against real issues.
|
||||
|
||||
.DESCRIPTION
|
||||
Fetches issues with "Needs-Triage" label but no "Product-*" label from the
|
||||
PowerToys repo and simulates what the GitHub Action would do (without actually
|
||||
applying labels). This lets you validate the mapping and AI inference before
|
||||
merging the workflow.
|
||||
|
||||
.PARAMETER Apply
|
||||
Actually apply the labels via `gh issue edit`. Requires gh auth.
|
||||
|
||||
.PARAMETER Limit
|
||||
Number of issues to process (default: 10).
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run - see what would happen
|
||||
.\Test-AutoLabelProduct.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Apply labels to first 5 issues
|
||||
.\Test-AutoLabelProduct.ps1 -Apply -Limit 5
|
||||
|
||||
.NOTES
|
||||
Prerequisites:
|
||||
- gh CLI authenticated: `gh auth login`
|
||||
- PowerShell 7+
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$Apply,
|
||||
[int]$Limit = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ─── Mapping (must match the workflow) ────────────────────────────────────────
|
||||
$AREA_TO_LABEL = @{
|
||||
'Advanced Paste' = 'Product-Advanced Paste'
|
||||
'Always on Top' = 'Product-Always On Top'
|
||||
'Awake' = 'Product-Awake'
|
||||
'ColorPicker' = 'Product-Color Picker'
|
||||
'Command not found' = 'Product-CommandNotFound'
|
||||
'Command Palette' = 'Product-Command Palette'
|
||||
'Crop and Lock' = 'Product-CropAndLock'
|
||||
'Environment Variables' = 'Product-Environment Variables'
|
||||
'FancyZones' = 'Product-FancyZones'
|
||||
'FancyZones Editor' = 'Product-FancyZones'
|
||||
'File Locksmith' = 'Product-File Locksmith'
|
||||
'File Explorer: Preview Pane' = 'Product-File Explorer'
|
||||
'File Explorer: Thumbnail preview' = 'Product-File Explorer'
|
||||
'Hosts File Editor' = 'Product-Hosts File Editor'
|
||||
'Image Resizer' = 'Product-Image Resizer'
|
||||
'Keyboard Manager' = 'Product-Keyboard Shortcut Manager'
|
||||
'Light Switch' = 'Product-LightSwitch'
|
||||
'Mouse Utilities' = 'Product-Mouse Utilities'
|
||||
'Mouse Without Borders' = 'Product-Mouse Without Borders'
|
||||
'New+' = 'Product-New+'
|
||||
'Peek' = 'Product-Peek'
|
||||
'Power Display' = 'Product-PowerDisplay'
|
||||
'PowerRename' = 'Product-PowerRename'
|
||||
'PowerToys Run' = 'Product-PowerToys Run'
|
||||
'Quick Accent' = 'Product-Quick Accent'
|
||||
'Registry Preview' = 'Product-Registry Preview'
|
||||
'Screen ruler' = 'Product-Screen Ruler'
|
||||
'Settings' = 'Product-Settings'
|
||||
'Shortcut Guide' = 'Product-Shortcut Guide'
|
||||
'TextExtractor' = 'Product-Text Extractor'
|
||||
'Workspaces' = 'Product-Workspaces'
|
||||
'ZoomIt' = 'Product-ZoomIt'
|
||||
'General' = 'Product-General'
|
||||
'Grab And Move' = 'Product-Grab And Move'
|
||||
}
|
||||
|
||||
# Non-product areas (no label applied, AI fallback triggers)
|
||||
$NON_PRODUCT_AREAS = @('Installer', 'System tray interaction', 'Welcome / PowerToys Tour window')
|
||||
|
||||
# ─── Fetch issues ────────────────────────────────────────────────────────────
|
||||
Write-Host "`n🔍 Fetching issues with 'Needs-Triage' and no 'Product-*' label (limit: $Limit)..." -ForegroundColor Cyan
|
||||
|
||||
# gh search finds issues with Needs-Triage; we filter out those that already have Product- labels
|
||||
$ghStderrPath = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
$issuesJson = gh issue list --repo microsoft/PowerToys --label "Needs-Triage" --limit 100 --json number,title,body,labels --state open 2> $ghStderrPath
|
||||
$ghExitCode = $LASTEXITCODE
|
||||
$ghErrorOutput = Get-Content -Path $ghStderrPath -Raw
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Path $ghStderrPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($ghExitCode -ne 0) {
|
||||
Write-Host "❌ Failed to fetch issues. Ensure 'gh auth login' is done." -ForegroundColor Red
|
||||
if (-not [string]::IsNullOrWhiteSpace($ghErrorOutput)) {
|
||||
Write-Host $ghErrorOutput
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ghErrorOutput)) {
|
||||
Write-Host "⚠️ gh emitted stderr output while fetching issues:" -ForegroundColor Yellow
|
||||
Write-Host $ghErrorOutput
|
||||
}
|
||||
|
||||
$issues = $issuesJson | ConvertFrom-Json
|
||||
|
||||
# Filter: only issues WITHOUT any Product-* label
|
||||
$issues = $issues | Where-Object {
|
||||
$labels = $_.labels | ForEach-Object { $_.name }
|
||||
-not ($labels | Where-Object { $_ -like 'Product-*' })
|
||||
} | Select-Object -First $Limit
|
||||
|
||||
Write-Host "📋 Found $($issues.Count) issues to process.`n" -ForegroundColor Green
|
||||
|
||||
# ─── Process each issue ──────────────────────────────────────────────────────
|
||||
$results = @()
|
||||
|
||||
foreach ($issue in $issues) {
|
||||
$body = $issue.body
|
||||
$title = $issue.title
|
||||
$number = $issue.number
|
||||
|
||||
Write-Host "--- Issue #${number}: ${title} ---" -ForegroundColor Yellow
|
||||
|
||||
# Parse "Area(s) with issue?" field
|
||||
$selectedAreas = @()
|
||||
if ($body -match '### Area\(s\) with issue\?\s*\r?\n\r?\n([\s\S]*?)(?=\r?\n\r?\n###|\s*$)') {
|
||||
$areaText = $Matches[1].Trim()
|
||||
$selectedAreas = $areaText -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
}
|
||||
|
||||
if ($selectedAreas.Count -eq 0) {
|
||||
Write-Host " ⚠️ No 'Area(s) with issue?' field found in body." -ForegroundColor DarkYellow
|
||||
} else {
|
||||
Write-Host " 📌 Areas selected: $($selectedAreas -join ', ')" -ForegroundColor DarkCyan
|
||||
}
|
||||
|
||||
# Resolve labels
|
||||
$resolvedLabels = @()
|
||||
$unmapped = @()
|
||||
|
||||
foreach ($area in $selectedAreas) {
|
||||
if ($AREA_TO_LABEL.ContainsKey($area)) {
|
||||
$resolvedLabels += $AREA_TO_LABEL[$area]
|
||||
} elseif ($area -notin $NON_PRODUCT_AREAS) {
|
||||
$unmapped += $area
|
||||
}
|
||||
}
|
||||
$resolvedLabels = $resolvedLabels | Sort-Object -Unique
|
||||
|
||||
if ($unmapped.Count -gt 0) {
|
||||
Write-Host " ⚠️ Unmapped areas (need mapping update): $($unmapped -join ', ')" -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
if ($resolvedLabels.Count -eq 0) {
|
||||
Write-Host " 🤖 No deterministic match → AI inference would trigger in workflow" -ForegroundColor Magenta
|
||||
} else {
|
||||
Write-Host " ✅ Would apply: $($resolvedLabels -join ', ')" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Apply if requested
|
||||
if ($Apply -and $resolvedLabels.Count -gt 0) {
|
||||
foreach ($label in $resolvedLabels) {
|
||||
Write-Host " 🏷️ Applying label: $label" -ForegroundColor White
|
||||
gh issue edit $number --repo microsoft/PowerToys --add-label $label 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " ❌ Failed to apply '$label' (may not exist in repo)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results += [PSCustomObject]@{
|
||||
Issue = $number
|
||||
Title = $title.Substring(0, [Math]::Min(60, $title.Length))
|
||||
Areas = ($selectedAreas -join ', ')
|
||||
Labels = ($resolvedLabels -join ', ')
|
||||
NeedsAI = ($resolvedLabels.Count -eq 0)
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
Write-Host "`n═══ SUMMARY ═══" -ForegroundColor Cyan
|
||||
$results | Format-Table -AutoSize -Wrap
|
||||
|
||||
$aiNeeded = ($results | Where-Object { $_.NeedsAI }).Count
|
||||
$mapped = ($results | Where-Object { -not $_.NeedsAI }).Count
|
||||
Write-Host "Deterministic: $mapped | AI fallback needed: $aiNeeded | Total: $($results.Count)" -ForegroundColor Cyan
|
||||
|
||||
if (-not $Apply) {
|
||||
Write-Host "`n💡 This was a DRY RUN. Use -Apply to actually add labels." -ForegroundColor Yellow
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#Requires -Version 7.2
|
||||
|
||||
using namespace System.Management.Automation
|
||||
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
function PublicStaticVoidMain {
|
||||
[CmdletBinding()]
|
||||
param ()
|
||||
|
||||
class TypeHandlerData {
|
||||
[String] $Name
|
||||
[String] $CurrentUserHandler
|
||||
[String] $MachineWideHandler
|
||||
}
|
||||
|
||||
[String[]]$TypesToCheck = @(".markdown", ".mdtext", ".mdtxt", ".mdown", ".mkdn", ".mdwn", ".mkd", ".md", ".svg", ".svgz", ".pdf", ".gcode", ".bgcode", ".stl", ".txt", ".ini", ".qoi")
|
||||
$IPREVIEW_HANDLER_CLSID = '{8895b1c6-b41f-4c1c-a562-0d564250836f}'
|
||||
$PowerToysHandlers = @{
|
||||
'{07665729-6243-4746-95b7-79579308d1b2}' = "PowerToys PDF handler"
|
||||
'{ddee2b8a-6807-48a6-bb20-2338174ff779}' = "PowerToys SVG handler"
|
||||
'{ec52dea8-7c9f-4130-a77b-1737d0418507}' = "PowerToys GCode handler"
|
||||
'{dd8de316-7b01-48e7-ba21-e92c646704af}' = "PowerToys BGCode handler"
|
||||
'{8AA07897-C30B-4543-865B-00A0E5A1B32D}' = "PowerToys QOI handler"
|
||||
'{45769bcc-e8fd-42d0-947e-02beef77a1f5}' = "PowerToys Markdown handler"
|
||||
'{afbd5a44-2520-4ae0-9224-6cfce8fe4400}' = "PowerToys Monaco fallback handler"
|
||||
'{DC6EFB56-9CFA-464D-8880-44885D7DC193}' = "Adobe Acrobat DC"
|
||||
}
|
||||
|
||||
function ResolveHandlerGUIDtoName {
|
||||
param (
|
||||
[Parameter(Mandatory, Position = 0)]
|
||||
[String] $GUID
|
||||
)
|
||||
return $PowerToysHandlers[$GUID] ?? $GUID
|
||||
}
|
||||
|
||||
function WriteMyProgress {
|
||||
param (
|
||||
[Parameter(Mandatory, Position=0)] [Int32] $ItemsPending,
|
||||
[Parameter(Mandatory, Position=1)] [Int32] $ItemsTotal,
|
||||
[switch] $Completed
|
||||
)
|
||||
[Int32] $PercentComplete = ($ItemsPending / $ItemsTotal) * 100
|
||||
if ($PercentComplete -lt 1) { $PercentComplete = 1}
|
||||
Write-Progress -Activity 'Querying Windows Registry' -Status "$ItemsPending of $ItemsTotal" -PercentComplete $PercentComplete -Completed:$Completed
|
||||
}
|
||||
|
||||
$ItemsTotal = $TypesToCheck.Count * 2
|
||||
$ItemsPending = 0
|
||||
WriteMyProgress 0 $ItemsTotal
|
||||
|
||||
$CheckResults = New-Object -TypeName 'System.Collections.Generic.List[TypeHandlerData]'
|
||||
foreach ($item in $TypesToCheck) {
|
||||
$CurrentUserGUID = Get-ItemPropertyValue -Path "HKCU://Software/Classes/$item/shellex/$IPREVIEW_HANDLER_CLSID" -Name '(default)' -ErrorAction SilentlyContinue
|
||||
$ItemsPending += 1
|
||||
WriteMyProgress $ItemsPending $ItemsTotal
|
||||
|
||||
$MachineWideGUID = "Didn't check"
|
||||
# $MachineWideGUID = Get-ItemPropertyValue -Path "HKLM://Software/Classes/$item/shellex/$IPREVIEW_HANDLER_CLSID" -Name '(default)' -ErrorAction SilentlyContinue
|
||||
$ItemsPending += 1
|
||||
WriteMyProgress $ItemsPending $ItemsTotal
|
||||
|
||||
$temp = New-Object -TypeName TypeHandlerData -Property @{
|
||||
Name = $item
|
||||
CurrentUserHandler = ($null -eq $CurrentUserGUID) ? "Nothing" : (ResolveHandlerGUIDtoName ($CurrentUserGUID))
|
||||
MachineWideHandler = ($null -eq $MachineWideGUID) ? "Nothing" : (ResolveHandlerGUIDtoName ($MachineWideGUID))
|
||||
}
|
||||
$CheckResults.Add($temp)
|
||||
|
||||
Clear-Variable 'CurrentUserGUID', 'MachineWideGUID'
|
||||
}
|
||||
WriteMyProgress $ItemsPending $ItemsTotal -Completed
|
||||
$CheckResults | Format-Table -AutoSize
|
||||
}
|
||||
|
||||
PublicStaticVoidMain
|
||||
@@ -0,0 +1,835 @@
|
||||
#Requires -Version 5.1
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies a PowerToys installation by checking all components, registry entries, files, and custom logic.
|
||||
|
||||
.DESCRIPTION
|
||||
This script comprehensively verifies a PowerToys installation by checking:
|
||||
- Registry entries for both per-machine and per-user installations
|
||||
- File and folder structure integrity
|
||||
- Module registration and functionality
|
||||
- WiX installer logic verification
|
||||
- Custom action results
|
||||
- DSC module installation
|
||||
- Command Palette packages
|
||||
|
||||
.PARAMETER InstallScope
|
||||
Specifies the installation scope to verify. Valid values are 'PerMachine' or 'PerUser'.
|
||||
Default is 'PerMachine'.
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Optional. Specifies a custom installation path to verify. If not provided, the script will
|
||||
detect the installation path from the registry.
|
||||
|
||||
.EXAMPLE
|
||||
.\verify-installation-script.ps1 -InstallScope PerMachine
|
||||
|
||||
.EXAMPLE
|
||||
.\verify-installation-script.ps1 -InstallScope PerUser
|
||||
|
||||
.NOTES
|
||||
Author: PowerToys Team
|
||||
Requires: PowerShell 5.1 or later
|
||||
Requires: Administrative privileges for per-machine verification
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet('PerMachine', 'PerUser')]
|
||||
[string]$InstallScope = 'PerMachine',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallPath
|
||||
)
|
||||
|
||||
# Initialize results tracking
|
||||
$script:Results = @{
|
||||
Summary = @{
|
||||
TotalChecks = 0
|
||||
PassedChecks = 0
|
||||
FailedChecks = 0
|
||||
WarningChecks = 0
|
||||
OverallStatus = "Unknown"
|
||||
}
|
||||
Details = @{}
|
||||
Timestamp = Get-Date
|
||||
Computer = $env:COMPUTERNAME
|
||||
User = $env:USERNAME
|
||||
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
|
||||
}
|
||||
|
||||
# PowerToys constants
|
||||
$PowerToysUpgradeCodePerMachine = "{42B84BF7-5FBF-473B-9C8B-049DC16F7708}"
|
||||
$PowerToysUpgradeCodePerUser = "{D8B559DB-4C98-487A-A33F-50A8EEE42726}"
|
||||
$PowerToysRegistryKeyPerMachine = "HKLM:\SOFTWARE\Classes\PowerToys"
|
||||
$PowerToysRegistryKeyPerUser = "HKCU:\SOFTWARE\Classes\PowerToys"
|
||||
|
||||
# Utility functions
|
||||
function Write-StatusMessage {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ValidateSet('Info', 'Success', 'Warning', 'Error')]
|
||||
[string]$Level = 'Info'
|
||||
)
|
||||
|
||||
$color = switch ($Level) {
|
||||
'Info' { 'White' }
|
||||
'Success' { 'Green' }
|
||||
'Warning' { 'Yellow' }
|
||||
'Error' { 'Red' }
|
||||
}
|
||||
|
||||
$prefix = switch ($Level) {
|
||||
'Info' { '[INFO]' }
|
||||
'Success' { '[PASS]' }
|
||||
'Warning' { '[WARN]' }
|
||||
'Error' { '[FAIL]' }
|
||||
}
|
||||
|
||||
Write-Host "$prefix $Message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function Add-CheckResult {
|
||||
param(
|
||||
[string]$Category,
|
||||
[string]$CheckName,
|
||||
[string]$Status,
|
||||
[string]$Message,
|
||||
[object]$Details = $null
|
||||
)
|
||||
|
||||
$script:Results.Summary.TotalChecks++
|
||||
|
||||
switch ($Status) {
|
||||
'Pass' { $script:Results.Summary.PassedChecks++ }
|
||||
'Fail' { $script:Results.Summary.FailedChecks++ }
|
||||
'Warning' { $script:Results.Summary.WarningChecks++ }
|
||||
}
|
||||
|
||||
if (-not $script:Results.Details.ContainsKey($Category)) {
|
||||
$script:Results.Details[$Category] = @{}
|
||||
}
|
||||
|
||||
$checkDetails = @{
|
||||
Status = $Status
|
||||
Message = $Message
|
||||
Details = $Details
|
||||
Timestamp = Get-Date
|
||||
}
|
||||
|
||||
$script:Results.Details[$Category][$CheckName] = $checkDetails
|
||||
|
||||
# Always show all checks with their status
|
||||
$level = switch ($Status) {
|
||||
'Pass' { 'Success' }
|
||||
'Fail' { 'Error' }
|
||||
'Warning' { 'Warning' }
|
||||
}
|
||||
Write-StatusMessage "[$Category] $CheckName - $Message" -Level $level
|
||||
}
|
||||
|
||||
function Test-RegistryKey {
|
||||
param(
|
||||
[string]$Path
|
||||
)
|
||||
try {
|
||||
return Test-Path $Path
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RegistryValue {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Name,
|
||||
[object]$DefaultValue = $null
|
||||
)
|
||||
try {
|
||||
$value = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue
|
||||
return $value.$Name
|
||||
}
|
||||
catch {
|
||||
return $DefaultValue
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PowerToysInstallation {
|
||||
param(
|
||||
[ValidateSet('PerMachine', 'PerUser')]
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
Write-StatusMessage "Verifying PowerToys $Scope installation..." -Level Info
|
||||
|
||||
# Determine registry paths based on scope
|
||||
$registryKey = if ($Scope -eq 'PerMachine') { $PowerToysRegistryKeyPerMachine } else { $PowerToysRegistryKeyPerUser }
|
||||
|
||||
# Check main registry key
|
||||
$mainKeyExists = Test-RegistryKey -Path $registryKey
|
||||
Add-CheckResult -Category "Registry" -CheckName "Main Registry Key ($Scope)" -Status $(if ($mainKeyExists) { 'Pass' } else { 'Fail' }) -Message "Registry key exists: $registryKey"
|
||||
|
||||
if (-not $mainKeyExists) {
|
||||
Add-CheckResult -Category "Installation" -CheckName "Installation Status ($Scope)" -Status 'Fail' -Message "PowerToys $Scope installation not found"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Check install scope value
|
||||
$installScopeValue = Get-RegistryValue -Path $registryKey -Name "InstallScope"
|
||||
$expectedScope = $Scope.ToLower()
|
||||
if ($Scope -eq 'PerMachine') { $expectedScope = 'perMachine' }
|
||||
if ($Scope -eq 'PerUser') { $expectedScope = 'perUser' }
|
||||
|
||||
$scopeCorrect = $installScopeValue -eq $expectedScope
|
||||
Add-CheckResult -Category "Registry" -CheckName "Install Scope Value ($Scope)" -Status $(if ($scopeCorrect) { 'Pass' } else { 'Fail' }) -Message "Install scope: Expected '$expectedScope', Found '$installScopeValue'"
|
||||
|
||||
# Check for uninstall registry entry (this is what makes PowerToys appear in Add/Remove Programs)
|
||||
$uninstallKey = if ($Scope -eq 'PerMachine') {
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
}
|
||||
else {
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
}
|
||||
|
||||
try {
|
||||
$powerToysEntry = Get-ItemProperty -Path $uninstallKey | Where-Object {
|
||||
$_.DisplayName -like "*PowerToys*"
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($powerToysEntry) {
|
||||
Add-CheckResult -Category "Registry" -CheckName "Uninstall Registry Entry ($Scope)" -Status 'Pass' -Message "PowerToys uninstall entry found with DisplayName: $($powerToysEntry.DisplayName)"
|
||||
|
||||
# Note: InstallLocation may or may not be set in the uninstall registry
|
||||
# This is normal behavior as PowerToys uses direct file references for system bindings
|
||||
if ($powerToysEntry.InstallLocation) {
|
||||
Add-CheckResult -Category "Registry" -CheckName "Install Location Registry ($Scope)" -Status 'Pass' -Message "InstallLocation found: $($powerToysEntry.InstallLocation)"
|
||||
}
|
||||
# No need to report missing InstallLocation as it's not required
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Registry" -CheckName "Uninstall Registry Entry ($Scope)" -Status 'Fail' -Message "PowerToys uninstall entry not found in Windows uninstall registry"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Add-CheckResult -Category "Registry" -CheckName "Uninstall Registry Entry ($Scope)" -Status 'Fail' -Message "Failed to read Windows uninstall registry"
|
||||
}
|
||||
|
||||
# Check for installation folder
|
||||
$installFolder = Get-PowerToysInstallPath -Scope $Scope
|
||||
if ($installFolder -and (Test-Path $installFolder)) {
|
||||
Add-CheckResult -Category "Installation" -CheckName "Install Folder ($Scope)" -Status 'Pass' -Message "Installation folder exists: $installFolder"
|
||||
|
||||
# Verify core files
|
||||
Test-CoreFiles -InstallPath $installFolder -Scope $Scope
|
||||
|
||||
# Verify modules
|
||||
Test-ModuleFiles -InstallPath $installFolder -Scope $Scope
|
||||
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Installation" -CheckName "Install Folder ($Scope)" -Status 'Fail' -Message "Installation folder not found or inaccessible: $installFolder"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PowerToysInstallPath {
|
||||
param(
|
||||
[ValidateSet('PerMachine', 'PerUser')]
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
if ($InstallPath) {
|
||||
return $InstallPath
|
||||
}
|
||||
|
||||
# Since InstallLocation may not be reliably set in the uninstall registry,
|
||||
# we'll use the default installation paths based on scope
|
||||
if ($Scope -eq 'PerMachine') {
|
||||
$defaultPath = "${env:ProgramFiles}\PowerToys"
|
||||
}
|
||||
else {
|
||||
$defaultPath = "${env:LOCALAPPDATA}\PowerToys"
|
||||
}
|
||||
|
||||
# Verify the path exists before returning it
|
||||
if (Test-Path $defaultPath) {
|
||||
return $defaultPath
|
||||
}
|
||||
|
||||
# If default path doesn't exist, try to get it from uninstall registry as fallback
|
||||
$uninstallKey = if ($Scope -eq 'PerMachine') {
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
}
|
||||
else {
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
}
|
||||
|
||||
try {
|
||||
$powerToysEntry = Get-ItemProperty -Path $uninstallKey | Where-Object {
|
||||
$_.DisplayName -like "*PowerToys*"
|
||||
} | Select-Object -First 1
|
||||
|
||||
# Check for InstallLocation first, but it may not exist
|
||||
if ($powerToysEntry -and $powerToysEntry.InstallLocation) {
|
||||
return $powerToysEntry.InstallLocation.TrimEnd('\')
|
||||
}
|
||||
|
||||
# Check for UninstallString as alternative source of install path
|
||||
if ($powerToysEntry -and $powerToysEntry.UninstallString) {
|
||||
# Extract directory from uninstall string like "C:\Program Files\PowerToys\unins000.exe"
|
||||
$uninstallExe = $powerToysEntry.UninstallString.Trim('"')
|
||||
$installDir = Split-Path $uninstallExe -Parent
|
||||
if ($installDir -and (Test-Path $installDir)) {
|
||||
return $installDir
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# If registry read fails, fall back to null
|
||||
}
|
||||
|
||||
# If we can't determine the install path, return null
|
||||
return $null
|
||||
}
|
||||
|
||||
function Test-CoreFiles {
|
||||
param(
|
||||
[string]$InstallPath,
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
# Essential core files (must exist for basic functionality)
|
||||
$essentialCoreFiles = @(
|
||||
'PowerToys.exe',
|
||||
'PowerToys.ActionRunner.exe',
|
||||
'License.rtf',
|
||||
'Notice.md'
|
||||
)
|
||||
|
||||
# Critical signed PowerToys executable files (from ESRP signing config)
|
||||
$criticalSignedFiles = @(
|
||||
# Main PowerToys components
|
||||
'PowerToys.exe',
|
||||
'PowerToys.ActionRunner.exe',
|
||||
'PowerToys.Update.exe',
|
||||
'PowerToys.BackgroundActivatorDLL.dll',
|
||||
'PowerToys.FilePreviewCommon.dll',
|
||||
'PowerToys.Interop.dll',
|
||||
|
||||
# Common libraries
|
||||
'CalculatorEngineCommon.dll',
|
||||
'PowerToys.ManagedTelemetry.dll',
|
||||
'PowerToys.ManagedCommon.dll',
|
||||
'PowerToys.ManagedCsWin32.dll',
|
||||
'PowerToys.Common.UI.dll',
|
||||
'PowerToys.Settings.UI.Lib.dll',
|
||||
'PowerToys.GPOWrapper.dll',
|
||||
'PowerToys.GPOWrapperProjection.dll',
|
||||
'PowerToys.AllExperiments.dll',
|
||||
|
||||
# Module executables and libraries
|
||||
'PowerToys.AlwaysOnTop.exe',
|
||||
'PowerToys.AlwaysOnTopModuleInterface.dll',
|
||||
'PowerToys.CmdNotFoundModuleInterface.dll',
|
||||
'PowerToys.ColorPicker.dll',
|
||||
'PowerToys.ColorPickerUI.dll',
|
||||
'PowerToys.ColorPickerUI.exe',
|
||||
'PowerToys.CropAndLockModuleInterface.dll',
|
||||
'PowerToys.CropAndLock.exe',
|
||||
'PowerToys.PowerOCRModuleInterface.dll',
|
||||
'PowerToys.PowerOCR.dll',
|
||||
'PowerToys.PowerOCR.exe',
|
||||
'PowerToys.AdvancedPasteModuleInterface.dll',
|
||||
'PowerToys.AwakeModuleInterface.dll',
|
||||
'PowerToys.Awake.exe',
|
||||
'PowerToys.Awake.dll',
|
||||
|
||||
# FancyZones
|
||||
'PowerToys.FancyZonesEditor.exe',
|
||||
'PowerToys.FancyZonesEditor.dll',
|
||||
'PowerToys.FancyZonesEditorCommon.dll',
|
||||
'PowerToys.FancyZonesModuleInterface.dll',
|
||||
'PowerToys.FancyZones.exe',
|
||||
|
||||
# Preview handlers
|
||||
'PowerToys.GcodePreviewHandler.dll',
|
||||
'PowerToys.GcodePreviewHandler.exe',
|
||||
'PowerToys.GcodePreviewHandlerCpp.dll',
|
||||
'PowerToys.GcodeThumbnailProvider.dll',
|
||||
'PowerToys.GcodeThumbnailProvider.exe',
|
||||
'PowerToys.GcodeThumbnailProviderCpp.dll',
|
||||
'PowerToys.BgcodePreviewHandler.dll',
|
||||
'PowerToys.BgcodePreviewHandler.exe',
|
||||
'PowerToys.BgcodePreviewHandlerCpp.dll',
|
||||
'PowerToys.BgcodeThumbnailProvider.dll',
|
||||
'PowerToys.BgcodeThumbnailProvider.exe',
|
||||
'PowerToys.BgcodeThumbnailProviderCpp.dll',
|
||||
'PowerToys.MarkdownPreviewHandler.dll',
|
||||
'PowerToys.MarkdownPreviewHandler.exe',
|
||||
'PowerToys.MarkdownPreviewHandlerCpp.dll',
|
||||
'PowerToys.MonacoPreviewHandler.dll',
|
||||
'PowerToys.MonacoPreviewHandler.exe',
|
||||
'PowerToys.MonacoPreviewHandlerCpp.dll',
|
||||
'PowerToys.PdfPreviewHandler.dll',
|
||||
'PowerToys.PdfPreviewHandler.exe',
|
||||
'PowerToys.PdfPreviewHandlerCpp.dll',
|
||||
'PowerToys.PdfThumbnailProvider.dll',
|
||||
'PowerToys.PdfThumbnailProvider.exe',
|
||||
'PowerToys.PdfThumbnailProviderCpp.dll',
|
||||
'PowerToys.powerpreview.dll',
|
||||
'PowerToys.PreviewHandlerCommon.dll',
|
||||
'PowerToys.QoiPreviewHandler.dll',
|
||||
'PowerToys.QoiPreviewHandler.exe',
|
||||
'PowerToys.QoiPreviewHandlerCpp.dll',
|
||||
'PowerToys.QoiThumbnailProvider.dll',
|
||||
'PowerToys.QoiThumbnailProvider.exe',
|
||||
'PowerToys.QoiThumbnailProviderCpp.dll',
|
||||
'PowerToys.StlThumbnailProvider.dll',
|
||||
'PowerToys.StlThumbnailProvider.exe',
|
||||
'PowerToys.StlThumbnailProviderCpp.dll',
|
||||
'PowerToys.SvgPreviewHandler.dll',
|
||||
'PowerToys.SvgPreviewHandler.exe',
|
||||
'PowerToys.SvgPreviewHandlerCpp.dll',
|
||||
'PowerToys.SvgThumbnailProvider.dll',
|
||||
'PowerToys.SvgThumbnailProvider.exe',
|
||||
'PowerToys.SvgThumbnailProviderCpp.dll',
|
||||
|
||||
# Image Resizer
|
||||
'PowerToys.ImageResizer.exe',
|
||||
'PowerToys.ImageResizer.dll',
|
||||
'PowerToys.ImageResizerExt.dll',
|
||||
'PowerToys.ImageResizerContextMenu.dll',
|
||||
|
||||
# Keyboard Manager
|
||||
'PowerToys.KeyboardManager.dll',
|
||||
'PowerToys.KeyboardManagerEditorLibraryWrapper.dll',
|
||||
|
||||
# PowerToys Run
|
||||
'PowerToys.Launcher.dll',
|
||||
'PowerToys.PowerLauncher.dll',
|
||||
'PowerToys.PowerLauncher.exe',
|
||||
'PowerToys.PowerLauncher.Telemetry.dll',
|
||||
'Wox.Infrastructure.dll',
|
||||
'Wox.Plugin.dll',
|
||||
|
||||
# Mouse utilities
|
||||
'PowerToys.FindMyMouse.dll',
|
||||
'PowerToys.MouseHighlighter.dll',
|
||||
'PowerToys.MouseJump.dll',
|
||||
'PowerToys.MouseJump.Common.dll',
|
||||
'PowerToys.MousePointerCrosshairs.dll',
|
||||
'PowerToys.MouseJumpUI.dll',
|
||||
'PowerToys.MouseJumpUI.exe',
|
||||
'PowerToys.MouseWithoutBorders.dll',
|
||||
'PowerToys.MouseWithoutBorders.exe',
|
||||
'PowerToys.MouseWithoutBordersModuleInterface.dll',
|
||||
'PowerToys.MouseWithoutBordersService.dll',
|
||||
'PowerToys.MouseWithoutBordersService.exe',
|
||||
'PowerToys.MouseWithoutBordersHelper.dll',
|
||||
'PowerToys.MouseWithoutBordersHelper.exe',
|
||||
|
||||
# PowerAccent - only the runner-loaded module interface ships in the install root.
|
||||
# The app, core, common and keyboard-service binaries moved to WinUI3Apps (see $winUI3SignedFiles).
|
||||
'PowerToys.PowerAccentModuleInterface.dll',
|
||||
|
||||
# Workspaces
|
||||
'PowerToys.WorkspacesSnapshotTool.exe',
|
||||
'PowerToys.WorkspacesLauncher.exe',
|
||||
'PowerToys.WorkspacesWindowArranger.exe',
|
||||
'PowerToys.WorkspacesEditor.exe',
|
||||
'PowerToys.WorkspacesEditor.dll',
|
||||
'PowerToys.WorkspacesLauncherUI.exe',
|
||||
'PowerToys.WorkspacesLauncherUI.dll',
|
||||
'PowerToys.WorkspacesModuleInterface.dll',
|
||||
'PowerToys.WorkspacesCsharpLibrary.dll',
|
||||
|
||||
# Shortcut Guide
|
||||
'PowerToys.ShortcutGuide.exe',
|
||||
'PowerToys.ShortcutGuideModuleInterface.dll',
|
||||
|
||||
# ZoomIt
|
||||
'PowerToys.ZoomIt.exe',
|
||||
'PowerToys.ZoomItModuleInterface.dll',
|
||||
'PowerToys.ZoomItSettingsInterop.dll',
|
||||
|
||||
# Command Palette
|
||||
'PowerToys.CmdPalModuleInterface.dll',
|
||||
'CmdPalKeyboardService.dll'
|
||||
)
|
||||
|
||||
# WinUI3Apps signed files (in WinUI3Apps subdirectory)
|
||||
$winUI3SignedFiles = @(
|
||||
'PowerToys.Settings.dll',
|
||||
'PowerToys.Settings.exe',
|
||||
'PowerToys.AdvancedPaste.exe',
|
||||
'PowerToys.AdvancedPaste.dll',
|
||||
'PowerToys.HostsModuleInterface.dll',
|
||||
'PowerToys.HostsUILib.dll',
|
||||
'PowerToys.Hosts.dll',
|
||||
'PowerToys.Hosts.exe',
|
||||
'PowerToys.FileLocksmithLib.Interop.dll',
|
||||
'PowerToys.FileLocksmithExt.dll',
|
||||
'PowerToys.FileLocksmithUI.exe',
|
||||
'PowerToys.FileLocksmithUI.dll',
|
||||
'PowerToys.FileLocksmithContextMenu.dll',
|
||||
'Peek.Common.dll',
|
||||
'Peek.FilePreviewer.dll',
|
||||
'Powertoys.Peek.UI.dll',
|
||||
'Powertoys.Peek.UI.exe',
|
||||
'Powertoys.Peek.dll',
|
||||
'PowerToys.EnvironmentVariablesModuleInterface.dll',
|
||||
'PowerToys.EnvironmentVariablesUILib.dll',
|
||||
'PowerToys.EnvironmentVariables.dll',
|
||||
'PowerToys.EnvironmentVariables.exe',
|
||||
'PowerToys.MeasureToolModuleInterface.dll',
|
||||
'PowerToys.MeasureToolCore.dll',
|
||||
'PowerToys.MeasureToolUI.dll',
|
||||
'PowerToys.MeasureToolUI.exe',
|
||||
'PowerToys.NewPlus.ShellExtension.dll',
|
||||
'PowerToys.NewPlus.ShellExtension.win10.dll',
|
||||
'PowerToys.PowerRenameExt.dll',
|
||||
'PowerToys.PowerRename.exe',
|
||||
'PowerToys.PowerRenameContextMenu.dll',
|
||||
'PowerToys.RegistryPreviewExt.dll',
|
||||
'PowerToys.RegistryPreviewUILib.dll',
|
||||
'PowerToys.RegistryPreview.dll',
|
||||
'PowerToys.RegistryPreview.exe',
|
||||
|
||||
# PowerAccent (Quick Accent) - moved from the install root to WinUI3Apps
|
||||
'PowerAccent.Core.dll',
|
||||
'PowerAccent.Common.dll',
|
||||
'PowerToys.PowerAccent.dll',
|
||||
'PowerToys.PowerAccent.exe',
|
||||
'PowerToys.PowerAccentKeyboardService.dll'
|
||||
)
|
||||
|
||||
# Tools signed files (in Tools subdirectory)
|
||||
$toolsSignedFiles = @(
|
||||
'PowerToys.BugReportTool.exe'
|
||||
)
|
||||
|
||||
# KeyboardManager signed files (in specific subdirectories)
|
||||
$keyboardManagerFiles = @{
|
||||
'KeyboardManagerEditor\PowerToys.KeyboardManagerEditor.exe' = 'KeyboardManagerEditor'
|
||||
'KeyboardManagerEngine\PowerToys.KeyboardManagerEngine.exe' = 'KeyboardManagerEngine'
|
||||
}
|
||||
|
||||
# Run plugins signed files (in RunPlugins subdirectories)
|
||||
$runPluginFiles = @{
|
||||
'RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.dll' = 'Calculator plugin'
|
||||
'RunPlugins\Folder\Microsoft.Plugin.Folder.dll' = 'Folder plugin'
|
||||
'RunPlugins\Indexer\Microsoft.Plugin.Indexer.dll' = 'Indexer plugin'
|
||||
'RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.dll' = 'OneNote plugin'
|
||||
'RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.dll' = 'History plugin'
|
||||
'RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.dll' = 'PowerToys plugin'
|
||||
'RunPlugins\Program\Microsoft.Plugin.Program.dll' = 'Program plugin'
|
||||
'RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.dll' = 'Registry plugin'
|
||||
'RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.dll' = 'Windows Settings plugin'
|
||||
'RunPlugins\Shell\Microsoft.Plugin.Shell.dll' = 'Shell plugin'
|
||||
'RunPlugins\Uri\Microsoft.Plugin.Uri.dll' = 'URI plugin'
|
||||
'RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.dll' = 'Window Walker plugin'
|
||||
'RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.dll' = 'Unit Converter plugin'
|
||||
'RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.dll' = 'VS Code Workspaces plugin'
|
||||
'RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.dll' = 'Service plugin'
|
||||
'RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.dll' = 'System plugin'
|
||||
'RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.dll' = 'Time Date plugin'
|
||||
'RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.dll' = 'Value Generator plugin'
|
||||
'RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.dll' = 'Web Search plugin'
|
||||
'RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.dll' = 'Windows Terminal plugin'
|
||||
}
|
||||
|
||||
# Check essential core files (must exist)
|
||||
Write-StatusMessage "Checking essential core files..." -Level Info
|
||||
foreach ($file in $essentialCoreFiles) {
|
||||
$filePath = Join-Path $InstallPath $file
|
||||
$exists = Test-Path $filePath
|
||||
Add-CheckResult -Category "Core Files" -CheckName "$file ($Scope)" -Status $(if ($exists) { 'Pass' } else { 'Fail' }) -Message "Essential file: $filePath"
|
||||
}
|
||||
|
||||
# Check critical signed files in root directory
|
||||
Write-StatusMessage "Checking critical signed files in root directory..." -Level Info
|
||||
foreach ($file in $criticalSignedFiles) {
|
||||
$filePath = Join-Path $InstallPath $file
|
||||
$exists = Test-Path $filePath
|
||||
# Most signed files are critical, but some may be optional depending on configuration
|
||||
$status = if ($exists) { 'Pass' } else { 'Warning' }
|
||||
Add-CheckResult -Category "Signed Files" -CheckName "$file ($Scope)" -Status $status -Message "Signed file: $filePath"
|
||||
}
|
||||
|
||||
# Check WinUI3Apps signed files
|
||||
Write-StatusMessage "Checking WinUI3Apps signed files..." -Level Info
|
||||
foreach ($file in $winUI3SignedFiles) {
|
||||
$filePath = Join-Path $InstallPath "WinUI3Apps\$file"
|
||||
$exists = Test-Path $filePath
|
||||
$status = if ($exists) { 'Pass' } else { 'Warning' }
|
||||
Add-CheckResult -Category "Signed Files" -CheckName "WinUI3Apps\$file ($Scope)" -Status $status -Message "WinUI3 signed file: $filePath"
|
||||
}
|
||||
|
||||
# Check Tools signed files
|
||||
Write-StatusMessage "Checking Tools signed files..." -Level Info
|
||||
foreach ($file in $toolsSignedFiles) {
|
||||
$filePath = Join-Path $InstallPath "Tools\$file"
|
||||
$exists = Test-Path $filePath
|
||||
$status = if ($exists) { 'Pass' } else { 'Warning' }
|
||||
Add-CheckResult -Category "Signed Files" -CheckName "Tools\$file ($Scope)" -Status $status -Message "Tools signed file: $filePath"
|
||||
}
|
||||
|
||||
# Check KeyboardManager files
|
||||
Write-StatusMessage "Checking KeyboardManager signed files..." -Level Info
|
||||
foreach ($relativePath in $keyboardManagerFiles.Keys) {
|
||||
$filePath = Join-Path $InstallPath $relativePath
|
||||
$exists = Test-Path $filePath
|
||||
$status = if ($exists) { 'Pass' } else { 'Warning' }
|
||||
$description = $keyboardManagerFiles[$relativePath]
|
||||
Add-CheckResult -Category "Signed Files" -CheckName "$relativePath ($Scope)" -Status $status -Message "KeyboardManager $description signed file: $filePath"
|
||||
}
|
||||
|
||||
# Check Run plugins files
|
||||
Write-StatusMessage "Checking PowerToys Run plugin files..." -Level Info
|
||||
foreach ($relativePath in $runPluginFiles.Keys) {
|
||||
$filePath = Join-Path $InstallPath $relativePath
|
||||
$exists = Test-Path $filePath
|
||||
$status = if ($exists) { 'Pass' } else { 'Warning' }
|
||||
$description = $runPluginFiles[$relativePath]
|
||||
Add-CheckResult -Category "Signed Files" -CheckName "$relativePath ($Scope)" -Status $status -Message "PowerToys Run $description signed file: $filePath"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ModuleFiles {
|
||||
param(
|
||||
[string]$InstallPath,
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
# PowerToys does not actually install modules in a "modules" subfolder.
|
||||
# Instead, modules are integrated directly into the main installation or specific subfolders.
|
||||
# Check for key module directories that should exist:
|
||||
|
||||
# Check KeyboardManager components (installed as separate folders)
|
||||
$keyboardManagerEditor = Join-Path $InstallPath "KeyboardManagerEditor"
|
||||
$keyboardManagerEngine = Join-Path $InstallPath "KeyboardManagerEngine"
|
||||
|
||||
if (Test-Path $keyboardManagerEditor) {
|
||||
Add-CheckResult -Category "Modules" -CheckName "KeyboardManager Editor ($Scope)" -Status 'Pass' -Message "KeyboardManager Editor folder exists: $keyboardManagerEditor"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Modules" -CheckName "KeyboardManager Editor ($Scope)" -Status 'Warning' -Message "KeyboardManager Editor folder not found: $keyboardManagerEditor"
|
||||
}
|
||||
|
||||
if (Test-Path $keyboardManagerEngine) {
|
||||
Add-CheckResult -Category "Modules" -CheckName "KeyboardManager Engine ($Scope)" -Status 'Pass' -Message "KeyboardManager Engine folder exists: $keyboardManagerEngine"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Modules" -CheckName "KeyboardManager Engine ($Scope)" -Status 'Warning' -Message "KeyboardManager Engine folder not found: $keyboardManagerEngine"
|
||||
}
|
||||
|
||||
# Check RunPlugins folder (contains PowerToys Run modules)
|
||||
$runPluginsPath = Join-Path $InstallPath "RunPlugins"
|
||||
if (Test-Path $runPluginsPath) {
|
||||
Add-CheckResult -Category "Modules" -CheckName "Run Plugins Folder ($Scope)" -Status 'Pass' -Message "Run plugins folder exists: $runPluginsPath"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Modules" -CheckName "Run Plugins Folder ($Scope)" -Status 'Warning' -Message "Run plugins folder not found: $runPluginsPath"
|
||||
}
|
||||
|
||||
# Check Tools folder
|
||||
$toolsPath = Join-Path $InstallPath "Tools"
|
||||
if (Test-Path $toolsPath) {
|
||||
Add-CheckResult -Category "Modules" -CheckName "Tools Folder ($Scope)" -Status 'Pass' -Message "Tools folder exists: $toolsPath"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Modules" -CheckName "Tools Folder ($Scope)" -Status 'Warning' -Message "Tools folder not found: $toolsPath"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RegistryHandlers {
|
||||
param(
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
$registryRoot = if ($Scope -eq 'PerMachine') { 'HKLM:' } else { 'HKCU:' }
|
||||
# Test URL protocol handler
|
||||
$protocolPath = "$registryRoot\SOFTWARE\Classes\powertoys"
|
||||
if (Test-RegistryKey -Path $protocolPath) {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "PowerToys URL Protocol ($Scope)" -Status 'Pass' -Message "URL protocol registered"
|
||||
|
||||
# Check command handler
|
||||
$commandPath = "$protocolPath\shell\open\command"
|
||||
if (Test-RegistryKey -Path $commandPath) {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "PowerToys URL Command ($Scope)" -Status 'Pass' -Message "URL command handler registered"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "PowerToys URL Command ($Scope)" -Status 'Fail' -Message "URL command handler not found"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "PowerToys URL Protocol ($Scope)" -Status 'Fail' -Message "URL protocol not registered"
|
||||
}
|
||||
|
||||
# Test CLSID registration for toast notifications
|
||||
$toastClsidPath = "$registryRoot\SOFTWARE\Classes\CLSID\{DD5CACDA-7C2E-4997-A62A-04A597B58F76}"
|
||||
if (Test-RegistryKey -Path $toastClsidPath) {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "Toast Notification CLSID ($Scope)" -Status 'Pass' -Message "Toast notification CLSID registered"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Registry Handlers" -CheckName "Toast Notification CLSID ($Scope)" -Status 'Warning' -Message "Toast notification CLSID not found"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DSCModule {
|
||||
param(
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
if ($Scope -eq 'PerUser') {
|
||||
# For per-user installations, DSC module is installed via custom action to user's Documents
|
||||
$userModulesPath = "$env:USERPROFILE\Documents\PowerShell\Modules\Microsoft.PowerToys.Configure"
|
||||
if (Test-Path $userModulesPath) {
|
||||
Add-CheckResult -Category "DSC Module" -CheckName "DSC Module (PerUser)" -Status 'Pass' -Message "DSC module found in user profile: $userModulesPath"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "DSC Module" -CheckName "DSC Module (PerUser)" -Status 'Fail' -Message "DSC module not found in user profile: $userModulesPath"
|
||||
}
|
||||
}
|
||||
else {
|
||||
# For per-machine installations, DSC module is installed to system WindowsPowerShell modules
|
||||
$systemModulesPath = "${env:ProgramFiles}\WindowsPowerShell\Modules\Microsoft.PowerToys.Configure"
|
||||
if (Test-Path $systemModulesPath) {
|
||||
Add-CheckResult -Category "DSC Module" -CheckName "DSC Module (PerMachine)" -Status 'Pass' -Message "DSC module found in system modules: $systemModulesPath"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "DSC Module" -CheckName "DSC Module (PerMachine)" -Status 'Fail' -Message "DSC module not found in system modules: $systemModulesPath"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Test-CommandPalettePackages {
|
||||
param(
|
||||
[string]$InstallPath
|
||||
)
|
||||
|
||||
$cmdPalPath = Join-Path $InstallPath "WinUI3Apps\CmdPal"
|
||||
if (Test-Path $cmdPalPath) {
|
||||
# Check for MSIX package file (the actual Command Palette installation)
|
||||
$msixFiles = Get-ChildItem $cmdPalPath -Filter "*.msix" -ErrorAction SilentlyContinue
|
||||
if ($msixFiles) {
|
||||
Add-CheckResult -Category "Command Palette" -CheckName "CmdPal MSIX Package" -Status 'Pass' -Message "Found $($msixFiles.Count) Command Palette MSIX package(s)"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Command Palette" -CheckName "CmdPal MSIX Package" -Status 'Warning' -Message "No Command Palette MSIX packages found"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Command Palette" -CheckName "CmdPal Module" -Status 'Warning' -Message "Command Palette module not found at: $cmdPalPath"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ContextMenuPackages {
|
||||
param(
|
||||
[string]$InstallPath
|
||||
)
|
||||
|
||||
# Context menu packages are installed as sparse packages
|
||||
# These MSIX packages should be present in the installation
|
||||
$contextMenuPackages = @{
|
||||
"ImageResizerContextMenuPackage.msix" = @{ Name = "Image Resizer context menu package"; Location = "Root" }
|
||||
"FileLocksmithContextMenuPackage.msix" = @{ Name = "File Locksmith context menu package"; Location = "WinUI3Apps" }
|
||||
"PowerRenameContextMenuPackage.msix" = @{ Name = "PowerRename context menu package"; Location = "WinUI3Apps" }
|
||||
"NewPlusPackage.msix" = @{ Name = "New+ context menu package"; Location = "WinUI3Apps" }
|
||||
}
|
||||
|
||||
# Check for packages based on their expected location
|
||||
foreach ($packageFile in $contextMenuPackages.Keys) {
|
||||
$packageInfo = $contextMenuPackages[$packageFile]
|
||||
|
||||
if ($packageInfo.Location -eq "Root") {
|
||||
$packagePath = Join-Path $InstallPath $packageFile
|
||||
}
|
||||
else {
|
||||
$packagePath = Join-Path $InstallPath "WinUI3Apps\$packageFile"
|
||||
}
|
||||
|
||||
if (Test-Path $packagePath) {
|
||||
Add-CheckResult -Category "Context Menu Packages" -CheckName $packageInfo.Name -Status 'Pass' -Message "Context menu package found: $packagePath"
|
||||
}
|
||||
else {
|
||||
Add-CheckResult -Category "Context Menu Packages" -CheckName $packageInfo.Name -Status 'Fail' -Message "Context menu package not found: $packagePath"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
function Main {
|
||||
Write-StatusMessage "Starting PowerToys Installation Verification" -Level Info
|
||||
Write-StatusMessage "Scope: $InstallScope" -Level Info
|
||||
|
||||
# Check the specified scope - no fallbacks, only what installer should create
|
||||
$installationFound = $false
|
||||
|
||||
if ($InstallScope -eq 'PerMachine') {
|
||||
if (Test-PowerToysInstallation -Scope 'PerMachine') {
|
||||
$installationFound = $true
|
||||
Test-RegistryHandlers -Scope 'PerMachine'
|
||||
Test-DSCModule -Scope 'PerMachine'
|
||||
$installPath = Get-PowerToysInstallPath -Scope 'PerMachine'
|
||||
if ($installPath) {
|
||||
Test-CommandPalettePackages -InstallPath $installPath
|
||||
Test-ContextMenuPackages -InstallPath $installPath
|
||||
}
|
||||
}
|
||||
}
|
||||
else { # PerUser
|
||||
if (Test-PowerToysInstallation -Scope 'PerUser') {
|
||||
$installationFound = $true
|
||||
Test-RegistryHandlers -Scope 'PerUser'
|
||||
Test-DSCModule -Scope 'PerUser'
|
||||
$installPath = Get-PowerToysInstallPath -Scope 'PerUser'
|
||||
if ($installPath) {
|
||||
Test-CommandPalettePackages -InstallPath $installPath
|
||||
Test-ContextMenuPackages -InstallPath $installPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($installationFound) {
|
||||
# Common tests (only run if installation found)
|
||||
# Note: Scheduled tasks are not created by installer, they're created at runtime
|
||||
}
|
||||
|
||||
# Calculate overall status
|
||||
if ($script:Results.Summary.FailedChecks -eq 0) {
|
||||
if ($script:Results.Summary.WarningChecks -eq 0) {
|
||||
$script:Results.Summary.OverallStatus = "Healthy"
|
||||
}
|
||||
else {
|
||||
$script:Results.Summary.OverallStatus = "Healthy with Warnings"
|
||||
}
|
||||
}
|
||||
else {
|
||||
$script:Results.Summary.OverallStatus = "Issues Detected"
|
||||
}
|
||||
|
||||
# Display summary
|
||||
Write-Host "`n" -NoNewline
|
||||
Write-StatusMessage "=== VERIFICATION SUMMARY ===" -Level Info
|
||||
Write-StatusMessage "Total Checks: $($script:Results.Summary.TotalChecks)" -Level Info
|
||||
Write-StatusMessage "Passed: $($script:Results.Summary.PassedChecks)" -Level Success
|
||||
Write-StatusMessage "Failed: $($script:Results.Summary.FailedChecks)" -Level Error
|
||||
Write-StatusMessage "Warnings: $($script:Results.Summary.WarningChecks)" -Level Warning
|
||||
Write-StatusMessage "Overall Status: $($script:Results.Summary.OverallStatus)" -Level $(
|
||||
switch ($script:Results.Summary.OverallStatus) {
|
||||
'Healthy' { 'Success' }
|
||||
'Healthy with Warnings' { 'Warning' }
|
||||
default { 'Error' }
|
||||
}
|
||||
)
|
||||
|
||||
Write-StatusMessage "PowerToys Installation Verification Complete" -Level Info
|
||||
}
|
||||
|
||||
# Run the main function
|
||||
Main
|
||||
@@ -0,0 +1,48 @@
|
||||
# Build scripts – quick guideline
|
||||
|
||||
Use these scripts to build PowerToys locally. They auto-detect your platform (x64/arm64), initialize the Visual Studio developer environment, and write helpful logs on failure.
|
||||
|
||||
## Quick start (from cmd.exe)
|
||||
- Fast essentials (runner + settings) and NuGet restore first:
|
||||
- `tools\build\build-essentials.cmd`
|
||||
- Build projects in the current folder:
|
||||
- `tools\build\build.cmd`
|
||||
|
||||
Tip: Add `D:\PowerToys\tools\build` to your PATH to use the wrappers anywhere.
|
||||
|
||||
## When to use which
|
||||
1) `build-essentials.ps1`
|
||||
- Restores NuGet for `PowerToys.slnx` and builds essentials (runner, settings).
|
||||
- Auto-detects Platform; initializes VS Dev environment automatically.
|
||||
- Example (PowerShell):
|
||||
- `./tools/build/build-essentials.ps1`
|
||||
- `./tools/build/build-essentials.ps1 -Platform arm64 -Configuration Release`
|
||||
|
||||
2) `build.ps1` (from any folder)
|
||||
- Builds any `.sln/.csproj/.vcxproj` in the current directory.
|
||||
- Auto-detects Platform; initializes VS Dev environment automatically.
|
||||
- Accepts extra MSBuild args (forwarded to msbuild):
|
||||
- `./tools/build/build.ps1 '/p:CIBuild=true' '/p:SomeProp=Value'`
|
||||
- Restore only:
|
||||
- `./tools/build/build.ps1 -RestoreOnly`
|
||||
|
||||
3) `build-installer.ps1` (use with caution)
|
||||
- Full local packaging pipeline (restore, build, sign MSIX, WiX v5 MSI/bootstrapper).
|
||||
- Auto-inits VS Dev environment. Cleans some output (keeps *.exe) under `installer/`.
|
||||
- Key options: `-PerUser true|false`, `-InstallerSuffix wix5|vnext`.
|
||||
- Example:
|
||||
- `./tools/build/build-installer.ps1 -Platform x64 -Configuration Release -PerUser true -InstallerSuffix wix5`
|
||||
|
||||
## Logs and troubleshooting
|
||||
- On failure, see logs next to the solution/project being built:
|
||||
- `build.<configuration>.<platform>.all.log` — full text log
|
||||
- `build.<configuration>.<platform>.errors.log` — errors only
|
||||
- `build.<configuration>.<platform>.warnings.log` — warnings only
|
||||
- `build.<configuration>.<platform>.trace.binlog` — open with MSBuild Structured Log Viewer
|
||||
- VS environment init:
|
||||
- Scripts try DevShell first (`Microsoft.VisualStudio.DevShell.dll` / `Enter-VsDevShell`), then fall back to `VsDevCmd.bat`.
|
||||
- If VS isn't found, run from "Developer PowerShell for VS 2022" or "Developer PowerShell for VS", or ensure `vswhere.exe` exists under `Program Files (x86)\Microsoft Visual Studio\Installer`.
|
||||
|
||||
## Notes
|
||||
- Override platform explicitly with `-Platform x64|arm64` if needed.
|
||||
- CMD wrappers: `build.cmd`, `build-essentials.cmd` forward all arguments to the PowerShell scripts.
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%Delete-Worktree.ps1" %*
|
||||
@@ -0,0 +1,130 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Remove a git worktree (and optionally its local branch and orphan fork remote).
|
||||
|
||||
.DESCRIPTION
|
||||
Locates a worktree by branch/path pattern (supports wildcards). Ensures the primary repository
|
||||
root is never removed. Optionally discards local changes with -Force. Deletes associated branch
|
||||
unless -KeepBranch. If the branch tracked a non-origin remote with no remaining tracking
|
||||
branches, that remote is removed unless -KeepRemote.
|
||||
|
||||
.PARAMETER Pattern
|
||||
Branch name or path fragment (wildcards * ? allowed). If multiple matches found they are listed
|
||||
and no deletion occurs.
|
||||
|
||||
.PARAMETER Force
|
||||
Discard uncommitted changes and attempt aggressive cleanup on failure.
|
||||
|
||||
.PARAMETER KeepBranch
|
||||
Preserve the local branch (only remove the worktree directory entry).
|
||||
|
||||
.PARAMETER KeepRemote
|
||||
Preserve any orphan fork remote even if no branches still track it.
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern feature/login
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern fork-user-featureX -Force
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern hotfix -KeepBranch
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git worktree list --porcelain
|
||||
git worktree prune
|
||||
Remove-Item -LiteralPath <path> -Recurse -Force
|
||||
git branch -D <branch>
|
||||
git remote remove <remote>
|
||||
git worktree prune
|
||||
#>
|
||||
|
||||
param(
|
||||
[string] $Pattern,
|
||||
[switch] $Force,
|
||||
[switch] $KeepBranch,
|
||||
[switch] $KeepRemote,
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
if ($Help -or -not $Pattern) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
try {
|
||||
$repoRoot = Get-RepoRoot
|
||||
$entries = Get-WorktreeEntries
|
||||
if (-not $entries -or $entries.Count -eq 0) { throw 'No worktrees found.' }
|
||||
$hasWildcard = $Pattern -match '[\*\?]'
|
||||
$matchPattern = if ($hasWildcard) { $Pattern } else { "*${Pattern}*" }
|
||||
$found = $entries | Where-Object { $_.Branch -and ( $_.Branch -like $matchPattern -or $_.Path -like $matchPattern ) }
|
||||
if (-not $found -or $found.Count -eq 0) { throw "No worktree matches pattern '$Pattern'" }
|
||||
if ($found.Count -gt 1) {
|
||||
Warn 'Pattern matches multiple worktrees:'
|
||||
$found | ForEach-Object { Info (" {0} {1}" -f $_.Branch, $_.Path) }
|
||||
return
|
||||
}
|
||||
$target = $found | Select-Object -First 1
|
||||
$branch = $target.Branch
|
||||
$folder = $target.Path
|
||||
if (-not $branch) { throw 'Resolved worktree has no branch (detached); refusing removal.' }
|
||||
try { $folder = (Resolve-Path -LiteralPath $folder -ErrorAction Stop).ProviderPath } catch {}
|
||||
$primary = (Resolve-Path -LiteralPath $repoRoot).ProviderPath
|
||||
if ([IO.Path]::GetFullPath($folder).TrimEnd('\\/') -ieq [IO.Path]::GetFullPath($primary).TrimEnd('\\/')) { throw 'Refusing to remove the primary worktree (repository root).' }
|
||||
$status = git -C $folder status --porcelain 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Unable to get git status for $folder" }
|
||||
if (-not $Force -and $status) { throw 'Worktree has uncommitted changes. Use -Force to discard.' }
|
||||
if ($Force -and $status) {
|
||||
Warn '[Force] Discarding local changes'
|
||||
git -C $folder reset --hard HEAD | Out-Null
|
||||
git -C $folder clean -fdx | Out-Null
|
||||
}
|
||||
if ($Force) { git worktree remove --force $folder } else { git worktree remove $folder }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$exit1 = $LASTEXITCODE
|
||||
$errMsg = "git worktree remove failed (exit $exit1)"
|
||||
if ($Force) {
|
||||
Warn 'Primary removal failed; performing aggressive fallback (Force implies brute).'
|
||||
try { git -C $folder submodule deinit -f --all 2>$null | Out-Null } catch {}
|
||||
try { git -C $folder clean -dfx 2>$null | Out-Null } catch {}
|
||||
try { Get-ChildItem -LiteralPath $folder -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { try { $_.IsReadOnly = $false } catch {} } } catch {}
|
||||
if (Test-Path $folder) { try { Remove-Item -LiteralPath $folder -Recurse -Force -ErrorAction Stop } catch { Err "Manual directory removal failed: $($_.Exception.Message)" } }
|
||||
git worktree prune 2>$null | Out-Null
|
||||
if (Test-Path $folder) { throw "$errMsg and aggressive cleanup did not fully remove directory: $folder" } else { Info "Aggressive cleanup removed directory $folder." }
|
||||
} else {
|
||||
throw "$errMsg. Rerun with -Force to attempt aggressive cleanup."
|
||||
}
|
||||
}
|
||||
# Determine upstream before potentially deleting branch
|
||||
$upRemote = Get-BranchUpstreamRemote -Branch $branch
|
||||
$looksForkName = $branch -like 'fork-*'
|
||||
|
||||
if (-not $KeepBranch) {
|
||||
git branch -D $branch 2>$null | Out-Null
|
||||
if (-not $KeepRemote -and $upRemote -and $upRemote -ne 'origin') {
|
||||
$otherTracking = git for-each-ref --format='%(refname:short)|%(upstream:short)' refs/heads 2>$null |
|
||||
Where-Object { $_ -and ($_ -notmatch "^$branch\|") } |
|
||||
ForEach-Object { $parts = $_.Split('|',2); if ($parts[1] -match '^(?<r>[^/]+)/'){ $parts[0],$Matches.r } } |
|
||||
Where-Object { $_[1] -eq $upRemote }
|
||||
if (-not $otherTracking) {
|
||||
Warn "Removing orphan remote '$upRemote' (no more tracking branches)"
|
||||
git remote remove $upRemote 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Failed to remove remote '$upRemote' (you may remove manually)." }
|
||||
} else { Info "Remote '$upRemote' retained (other branches still track it)." }
|
||||
} elseif ($looksForkName -and -not $KeepRemote -and -not $upRemote) {
|
||||
Warn 'Branch looks like a fork branch (name pattern), but has no upstream remote; nothing to clean.'
|
||||
}
|
||||
}
|
||||
|
||||
Info "Removed worktree ($branch) at $folder."; if (-not $KeepBranch) { Info 'Branch deleted.' }
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $branch
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual cleanup guidelines:'
|
||||
Info ' git worktree list --porcelain'
|
||||
Info ' git worktree prune'
|
||||
Info ' # If still present:'
|
||||
Info ' Remove-Item -LiteralPath <path> -Recurse -Force'
|
||||
Info ' git branch -D <branch> (if you also want to drop local branch)'
|
||||
Info ' git remote remove <remote> (if orphan fork remote remains)'
|
||||
Info ' git worktree prune'
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromBranch.ps1" %*
|
||||
@@ -0,0 +1,78 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree for an existing local or remote (origin) branch.
|
||||
|
||||
.DESCRIPTION
|
||||
Normalizes origin/<name> to <name>. If the branch does not exist locally (and -NoFetch is not
|
||||
provided) it will fetch and create a tracking branch from origin. Reuses any existing worktree
|
||||
bound to the branch; otherwise creates a new one adjacent to the repository root.
|
||||
|
||||
.PARAMETER Branch
|
||||
Branch name (local or origin/<name> form) to materialize as a worktree.
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to open (Default).
|
||||
|
||||
.PARAMETER NoFetch
|
||||
Skip fetch if branch missing locally; script will error instead of creating it.
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch feature/login
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch origin/bugfix/nullref
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch release/v1 -NoFetch
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git fetch origin && git checkout <branch>
|
||||
git worktree add ../RepoName-XX <branch>
|
||||
code ../RepoName-XX --profile Default
|
||||
#>
|
||||
|
||||
param(
|
||||
[string] $Branch,
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $NoFetch,
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
|
||||
if ($Help -or -not $Branch) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
|
||||
# Normalize origin/<name> to <name>
|
||||
if ($Branch -match '^(origin|upstream|main|master)/.+') {
|
||||
if ($Branch -match '^(origin|upstream)/(.+)$') { $Branch = $Matches[2] }
|
||||
}
|
||||
|
||||
try {
|
||||
git show-ref --verify --quiet "refs/heads/$Branch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if (-not $NoFetch) {
|
||||
Warn "Local branch '$Branch' not found; attempting remote fetch..."
|
||||
git fetch --all --prune 2>$null | Out-Null
|
||||
$remoteRef = "origin/$Branch"
|
||||
git show-ref --verify --quiet "refs/remotes/$remoteRef"
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
git branch --track $Branch $remoteRef 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create tracking branch '$Branch' from $remoteRef" }
|
||||
Info "Created local tracking branch '$Branch' from $remoteRef."
|
||||
} else { throw "Branch '$Branch' not found locally or on origin. Use git fetch or specify a valid branch." }
|
||||
} else { throw "Branch '$Branch' does not exist locally (remote fetch disabled with -NoFetch)." }
|
||||
}
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $Branch -VSCodeProfile $VSCodeProfile
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $Branch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $Branch -WorktreePath $path
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info ' git fetch origin'
|
||||
Info " git checkout $Branch (or: git branch --track $Branch origin/$Branch)"
|
||||
Info ' git worktree add ../<Repo>-XX <branch>'
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromFork.ps1" %*
|
||||
@@ -0,0 +1,127 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree from a branch in a personal fork: <ForkUser>:<ForkBranch>.
|
||||
|
||||
.DESCRIPTION
|
||||
Adds a transient uniquely named fork remote (fork-xxxxx) unless -RemoteName specified.
|
||||
Fetches only the target branch (fallback full fetch once if needed), creates a local tracking
|
||||
branch (fork-<user>-<sanitized-branch> or custom alias), and delegates worktree creation/reuse
|
||||
to shared helpers in WorktreeLib.
|
||||
|
||||
.PARAMETER Spec
|
||||
Fork spec in the form <ForkUser>:<ForkBranch>.
|
||||
|
||||
.PARAMETER ForkRepo
|
||||
Repository name in the fork (default: PowerToys).
|
||||
|
||||
.PARAMETER RemoteName
|
||||
Desired remote name; if left as 'fork' a unique suffix will be generated.
|
||||
|
||||
.PARAMETER BranchAlias
|
||||
Optional local branch name override; defaults to fork-<user>-<sanitized-branch>.
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to pass through to worktree opening (Default profile by default).
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromFork.ps1 -Spec alice:feature/new-ui
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromFork.ps1 -Spec bob:bugfix/crash -BranchAlias fork-bob-crash
|
||||
|
||||
.NOTES
|
||||
Manual equivalent if this script fails:
|
||||
git remote add fork-temp https://github.com/<user>/<repo>.git
|
||||
git fetch fork-temp
|
||||
git branch --track fork-<user>-<branch> fork-temp/<branch>
|
||||
git worktree add ../Repo-XX fork-<user>-<branch>
|
||||
code ../Repo-XX
|
||||
#>
|
||||
param(
|
||||
[string] $Spec,
|
||||
[string] $ForkRepo = 'PowerToys',
|
||||
[string] $RemoteName = 'fork',
|
||||
[string] $BranchAlias,
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $Help
|
||||
)
|
||||
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
if ($Help -or -not $Spec) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
|
||||
$repoRoot = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $repoRoot) { throw 'Not inside a git repository.' }
|
||||
|
||||
# Parse spec
|
||||
if ($Spec -notmatch '^[^:]+:.+$') { throw "Spec must be <ForkUser>:<ForkBranch>, got '$Spec'" }
|
||||
$ForkUser,$ForkBranch = $Spec.Split(':',2)
|
||||
|
||||
$forkUrl = "https://github.com/$ForkUser/$ForkRepo.git"
|
||||
|
||||
# Auto-suffix remote name if user left default 'fork'
|
||||
$allRemotes = @(git remote 2>$null)
|
||||
if ($RemoteName -eq 'fork') {
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
do {
|
||||
$suffix = -join ((1..5) | ForEach-Object { $chars[(Get-Random -Max $chars.Length)] })
|
||||
$candidate = "fork-$suffix"
|
||||
} while ($allRemotes -contains $candidate)
|
||||
$RemoteName = $candidate
|
||||
Info "Assigned unique remote name: $RemoteName"
|
||||
}
|
||||
|
||||
$existing = $allRemotes | Where-Object { $_ -eq $RemoteName }
|
||||
if (-not $existing) {
|
||||
Info "Adding remote $RemoteName -> $forkUrl"
|
||||
git remote add $RemoteName $forkUrl | Out-Null
|
||||
} else {
|
||||
$currentUrl = git remote get-url $RemoteName 2>$null
|
||||
if ($currentUrl -ne $forkUrl) { Warn "Remote $RemoteName points to $currentUrl (expected $forkUrl). Using existing." }
|
||||
}
|
||||
|
||||
## Note: Verbose fetch & stale lock auto-clean removed for simplicity.
|
||||
|
||||
try {
|
||||
Info "Fetching branch '$ForkBranch' from $RemoteName..."
|
||||
& git fetch $RemoteName $ForkBranch 1>$null 2>$null
|
||||
$fetchExit = $LASTEXITCODE
|
||||
if ($fetchExit -ne 0) {
|
||||
# Retry full fetch silently once (covers servers not supporting branch-only fetch syntax)
|
||||
& git fetch $RemoteName 1>$null 2>$null
|
||||
$fetchExit = $LASTEXITCODE
|
||||
}
|
||||
if ($fetchExit -ne 0) { throw "Fetch failed for remote $RemoteName (branch $ForkBranch)." }
|
||||
|
||||
$remoteRef = "refs/remotes/$RemoteName/$ForkBranch"
|
||||
git show-ref --verify --quiet $remoteRef
|
||||
if ($LASTEXITCODE -ne 0) { throw "Remote branch not found: $RemoteName/$ForkBranch" }
|
||||
|
||||
$sanitizedBranch = ($ForkBranch -replace '[\\/:*?"<>|]','-')
|
||||
if ($BranchAlias) { $localBranch = $BranchAlias } else { $localBranch = "fork-$ForkUser-$sanitizedBranch" }
|
||||
|
||||
git show-ref --verify --quiet "refs/heads/$localBranch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Info "Creating local tracking branch $localBranch from $RemoteName/$ForkBranch"
|
||||
git branch --track $localBranch "$RemoteName/$ForkBranch" 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create local tracking branch $localBranch" }
|
||||
} else { Info "Local branch $localBranch already exists." }
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $localBranch -VSCodeProfile $VSCodeProfile
|
||||
# Ensure upstream so future 'git push' works
|
||||
Set-BranchUpstream -LocalBranch $localBranch -RemoteName $RemoteName -RemoteBranchPath $ForkBranch
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $localBranch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $localBranch -WorktreePath $path
|
||||
Warn "Remote $RemoteName ready (URL: $forkUrl)"
|
||||
$hasUp = git rev-parse --abbrev-ref --symbolic-full-name "$localBranch@{upstream}" 2>$null
|
||||
if ($hasUp) { Info "Push with: git push (upstream: $hasUp)" } else { Warn 'Upstream not set; run: git push -u <remote> <local>:<remoteBranch>' }
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info " git remote add temp-fork $forkUrl"
|
||||
Info " git fetch temp-fork"
|
||||
Info " git branch --track fork-<user>-<branch> temp-fork/$ForkBranch"
|
||||
Info ' git worktree add ../<Repo>-XX fork-<user>-<branch>'
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromIssue.ps1" %*
|
||||
@@ -0,0 +1,78 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree for a new issue branch derived from a base ref.
|
||||
|
||||
.DESCRIPTION
|
||||
Composes a branch name as issue/<number> or issue/<number>-<slug> (slug from optional -Title).
|
||||
If the branch does not already exist, it is created from -Base (default origin/main). Then a
|
||||
worktree is created or reused.
|
||||
|
||||
.PARAMETER Number
|
||||
Issue number used to construct the branch name.
|
||||
|
||||
.PARAMETER Title
|
||||
Optional descriptive title; slug into the branch name.
|
||||
|
||||
.PARAMETER Base
|
||||
Base ref to branch from (default origin/main).
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to open (Default).
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromIssue.ps1 -Number 1234 -Title "Crash on launch"
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromIssue.ps1 -Number 42 -Base origin/develop
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git fetch origin
|
||||
git checkout -b issue/<num>-<slug> <base>
|
||||
git worktree add ../Repo-XX issue/<num>-<slug>
|
||||
code ../Repo-XX
|
||||
#>
|
||||
|
||||
param(
|
||||
[int] $Number,
|
||||
[string] $Title,
|
||||
[string] $Base = 'origin/main',
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
if ($Help -or -not $Number) { Show-FileEmbeddedHelp -ScriptPath $scriptPath; return }
|
||||
|
||||
# Compose branch name
|
||||
if ($Title) {
|
||||
$slug = ($Title -replace '[^\w\- ]','').ToLower() -replace ' +','-'
|
||||
$branch = "issue/$Number-$slug"
|
||||
} else {
|
||||
$branch = "issue/$Number"
|
||||
}
|
||||
|
||||
try {
|
||||
# Create branch if missing
|
||||
git show-ref --verify --quiet "refs/heads/$branch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Info "Creating branch $branch from $Base"
|
||||
git branch $branch $Base 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create branch $branch from $Base" }
|
||||
} else {
|
||||
Info "Branch $branch already exists locally."
|
||||
}
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $branch -VSCodeProfile $VSCodeProfile
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $branch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $branch -WorktreePath $path
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info " git fetch origin"
|
||||
Info " git checkout -b $branch $Base (if branch missing)"
|
||||
Info " git worktree add ../<Repo>-XX $branch"
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# PowerToys Worktree Helper Scripts
|
||||
|
||||
This folder contains helper scripts to create and manage parallel Git worktree for developing multiple changes (including Copilot suggestions) concurrently without cloning the full repository each time.
|
||||
|
||||
## Why worktree?
|
||||
Git worktree let you have several checked‑out branches sharing a single `.git` object store. Benefits:
|
||||
- Fast context switching: no re-clone, no duplicate large binary/object downloads.
|
||||
- Lower disk usage versus multiple full clones.
|
||||
- Keeps each change isolated in its own folder so you can run builds/tests independently.
|
||||
- Enables working in parallel with Copilot generated branches (e.g., feature + quick fix + perf experiment) while the main clone stays clean.
|
||||
|
||||
Recommended: keep active parallel worktree(s) to **≤ 3** per developer to reduce cognitive load and avoid excessive incremental build invalidations.
|
||||
|
||||
## Scripts Overview
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `New-WorktreeFromFork.ps1/.cmd` | Create a worktree from a branch in a personal fork (`<User>:<branch>` spec). Adds a temporary unique remote (e.g. `fork-abc12`). |
|
||||
| `New-WorktreeFromBranch.ps1/.cmd` | Create/reuse a worktree for an existing local or remote (origin) branch. Can normalize `origin/branch` to `branch`. |
|
||||
| `New-WorktreeFromIssue.ps1/.cmd` | Start a new issue branch from a base (default `origin/main`) using naming `issue/<number>-<slug>`. |
|
||||
| `Delete-Worktree.ps1/.cmd` | Remove a worktree and optionally its local branch / orphan fork remote. |
|
||||
| `WorktreeLib.ps1` | Shared helpers: unique folder naming, worktree listing, upstream setup, summary output, logging helpers. |
|
||||
|
||||
## Typical Flows
|
||||
### 1. Create from a fork branch
|
||||
```
|
||||
./New-WorktreeFromFork.ps1 -Spec alice:feature/perf-tweak
|
||||
```
|
||||
Creates remote `fork-xxxxx`, fetches just that branch, creates local branch `fork-alice-feature-perf-tweak`, makes a new worktree beside the repo root.
|
||||
|
||||
### 2. Create from an existing or remote branch
|
||||
```
|
||||
./New-WorktreeFromBranch.ps1 -Branch origin/feature/new-ui
|
||||
```
|
||||
Fetches if needed and creates a tracking branch if missing, then creates/reuses the worktree.
|
||||
|
||||
### 3. Start a new issue branch
|
||||
```
|
||||
./New-WorktreeFromIssue.ps1 -Number 1234 -Title "Crash on launch"
|
||||
```
|
||||
Creates branch `issue/1234-crash-on-launch` off `origin/main` (or `-Base`), then worktree.
|
||||
|
||||
### 4. Delete a worktree when done
|
||||
```
|
||||
./Delete-Worktree.ps1 -Pattern feature/perf-tweak
|
||||
```
|
||||
If only one match, removes the worktree directory. Add `-Force` to discard local changes. Use `-KeepBranch` if you still need the branch, `-KeepRemote` to retain a fork remote.
|
||||
|
||||
## After Creating a Worktree
|
||||
Inside the new worktree directory:
|
||||
1. Run the minimal build bootstrap in VSCode terminal:
|
||||
```
|
||||
tools\build\build-essentials.cmd
|
||||
```
|
||||
2. Build only the module(s) you need (e.g., open solution filter or run targeted project build) instead of a full PowerToys build. This speeds iteration and reduces noise.
|
||||
3. Make changes, commit, push.
|
||||
4. Finally delete the worktree when done.
|
||||
|
||||
## Naming & Locations
|
||||
- Worktree is created as sibling folders of the repo root (e.g., `PowerToys` + `PowerToys-ab12`), using a hash/short pattern to avoid collisions.
|
||||
- Fork-based branches get local names `fork-<user>-<sanitized-branch>`.
|
||||
- Issue branches: `issue/<number>` or `issue/<number>-<slug>`.
|
||||
|
||||
## Scenarios Covered / Limitations
|
||||
Covered scenarios:
|
||||
1. From a fork branch (personal fork on GitHub).
|
||||
2. From an existing local or origin remote branch.
|
||||
3. Creating a new branch for an issue.
|
||||
|
||||
Not covered (manual steps needed):
|
||||
- Creating from a non-origin upstream other than a fork (add remote manually then use branch script).
|
||||
- Batch creation of multiple worktree in one command.
|
||||
- Automatic rebase / sync of many worktree at once (do that manually or script separately).
|
||||
|
||||
## Best Practices
|
||||
- Keep ≤ 3 active parallel worktree(s) (e.g., main dev, a long-lived feature, a quick fix / experiment) plus the root clone.
|
||||
- Delete stale worktree early; each adds file watchers & potential incremental build churn.
|
||||
- Avoid editing the same file across multiple worktree simultaneously to reduce merge friction.
|
||||
- Run `git fetch --all --prune` periodically in the primary repo, not in every worktree.
|
||||
|
||||
## Troubleshooting
|
||||
| Symptom | Hint |
|
||||
|---------|------|
|
||||
| Fetch failed for fork remote | Branch name typo or fork private without auth. Try manual `git fetch <remote> <branch>`.
|
||||
| Cannot lock ref *.lock | Stale lock: run `git worktree prune` or manually delete the `.lock` file then retry.
|
||||
| Worktree already exists error | Use `git worktree list` to locate existing path; open that folder instead of creating a duplicate.
|
||||
| Local branch missing for remote | Use `git branch --track <name> origin/<name>` then re-run the branch script.
|
||||
|
||||
## Security & Safety Notes
|
||||
- Scripts avoid force-deleting unless you pass `-Force` (Delete script).
|
||||
- No network credentials are stored; they rely on your existing Git credential helper.
|
||||
- Always review a new fork remote URL before pushing.
|
||||
|
||||
---
|
||||
Maintainers: Keep the scripts lean; avoid adding heavy dependencies or global state. Update this doc if parameters or flows change.
|
||||
@@ -0,0 +1,151 @@
|
||||
# WorktreeLib.ps1 - shared helpers
|
||||
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return $root
|
||||
}
|
||||
|
||||
function Get-WorktreeBasePath {
|
||||
param([string]$RepoRoot)
|
||||
# Always use parent of repo root (folder that contains the main repo directory)
|
||||
$parent = Split-Path -Parent $RepoRoot
|
||||
if (-not (Test-Path $parent)) { throw "Parent path for repo root not found: $parent" }
|
||||
return (Resolve-Path $parent).ProviderPath
|
||||
}
|
||||
|
||||
function Get-ShortHashFromString {
|
||||
param([Parameter(Mandatory)][string]$Text)
|
||||
$md5 = [System.Security.Cryptography.MD5]::Create()
|
||||
try {
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
|
||||
$digest = $md5.ComputeHash($bytes)
|
||||
return -join ($digest[0..1] | ForEach-Object { $_.ToString('x2') })
|
||||
} finally { $md5.Dispose() }
|
||||
}
|
||||
|
||||
function Initialize-SubmodulesIfAny {
|
||||
param([string]$RepoRoot,[string]$WorktreePath)
|
||||
$hasGitmodules = Test-Path (Join-Path $RepoRoot '.gitmodules')
|
||||
if ($hasGitmodules) {
|
||||
git -C $WorktreePath submodule sync --recursive | Out-Null
|
||||
git -C $WorktreePath submodule update --init --recursive | Out-Null
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function New-WorktreeForExistingBranch {
|
||||
param(
|
||||
[Parameter(Mandatory)][string] $Branch,
|
||||
[Parameter(Mandatory)][string] $VSCodeProfile
|
||||
)
|
||||
$repoRoot = Get-RepoRoot
|
||||
git show-ref --verify --quiet "refs/heads/$Branch"; if ($LASTEXITCODE -ne 0) { throw "Branch '$Branch' does not exist locally." }
|
||||
|
||||
# Detect existing worktree for this branch
|
||||
$entries = Get-WorktreeEntries
|
||||
$match = $entries | Where-Object { $_.Branch -eq $Branch } | Select-Object -First 1
|
||||
if ($match) {
|
||||
Info "Reusing existing worktree for '$Branch': $($match.Path)"
|
||||
code --new-window "$($match.Path)" --profile "$VSCodeProfile" | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$safeBranch = ($Branch -replace '[\\/:*?"<>|]','-')
|
||||
$hash = Get-ShortHashFromString -Text $safeBranch
|
||||
$folderName = "$(Split-Path -Leaf $repoRoot)-$hash"
|
||||
$base = Get-WorktreeBasePath -RepoRoot $repoRoot
|
||||
$folder = Join-Path $base $folderName
|
||||
git worktree add $folder $Branch
|
||||
$inited = Initialize-SubmodulesIfAny -RepoRoot $repoRoot -WorktreePath $folder
|
||||
code --new-window "$folder" --profile "$VSCodeProfile" | Out-Null
|
||||
Info "Created worktree for branch '$Branch' at $folder."; if ($inited) { Info 'Submodules initialized.' }
|
||||
}
|
||||
|
||||
function Get-WorktreeEntries {
|
||||
# Returns objects with Path and Branch (branch without refs/heads/ prefix)
|
||||
$lines = git worktree list --porcelain 2>$null
|
||||
if (-not $lines) { return @() }
|
||||
$entries = @(); $current=@{}
|
||||
foreach($l in $lines){
|
||||
if ($l -eq '') { if ($current.path -and $current.branch){ $entries += ,([pscustomobject]@{ Path=$current.path; Branch=($current.branch -replace '^refs/heads/','') }) }; $current=@{}; continue }
|
||||
if ($l -like 'worktree *'){ $current.path = ($l -split ' ',2)[1] }
|
||||
elseif ($l -like 'branch *'){ $current.branch = ($l -split ' ',2)[1].Trim() }
|
||||
}
|
||||
if ($current.path -and $current.branch){ $entries += ,([pscustomobject]@{ Path=$current.path; Branch=($current.branch -replace '^refs/heads/','') }) }
|
||||
return ($entries | Sort-Object Path,Branch -Unique)
|
||||
}
|
||||
|
||||
function Get-BranchUpstreamRemote {
|
||||
param([Parameter(Mandatory)][string]$Branch)
|
||||
# Returns remote name if branch has an upstream, else $null
|
||||
$ref = git rev-parse --abbrev-ref --symbolic-full-name "$Branch@{upstream}" 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $ref) { return $null }
|
||||
if ($ref -match '^(?<remote>[^/]+)/.+$') { return $Matches.remote }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Show-IssueFarmCommonFooter {
|
||||
Info '--- Common Manual Steps ---'
|
||||
Info 'List worktree: git worktree list --porcelain'
|
||||
Info 'List branches: git branch -vv'
|
||||
Info 'List remotes: git remote -v'
|
||||
Info 'Prune worktree: git worktree prune'
|
||||
Info 'Remove worktree dir: Remove-Item -Recurse -Force <path>'
|
||||
Info 'Reset branch: git reset --hard HEAD'
|
||||
}
|
||||
|
||||
function Show-WorktreeExecutionSummary {
|
||||
param(
|
||||
[string]$CurrentBranch,
|
||||
[string]$WorktreePath
|
||||
)
|
||||
Info '--- Summary ---'
|
||||
if ($CurrentBranch) { Info "Branch: $CurrentBranch" }
|
||||
if ($WorktreePath) { Info "Worktree path: $WorktreePath" }
|
||||
$entries = Get-WorktreeEntries
|
||||
if ($entries.Count -gt 0) {
|
||||
Info 'Existing worktrees:'
|
||||
$entries | ForEach-Object { Info (" {0} -> {1}" -f $_.Branch,$_.Path) }
|
||||
}
|
||||
Info 'Remotes:'
|
||||
git remote -v 2>$null | Sort-Object | Get-Unique | ForEach-Object { Info " $_" }
|
||||
}
|
||||
|
||||
function Show-FileEmbeddedHelp {
|
||||
param([string]$ScriptPath)
|
||||
if (-not (Test-Path $ScriptPath)) { throw "Cannot load help; file missing: $ScriptPath" }
|
||||
$content = Get-Content -LiteralPath $ScriptPath -ErrorAction Stop
|
||||
$inBlock=$false
|
||||
foreach($line in $content){
|
||||
if ($line -match '^<#!') { $inBlock=$true; continue }
|
||||
if ($line -match '#>$') { break }
|
||||
if ($inBlock) { Write-Host $line }
|
||||
}
|
||||
Show-IssueFarmCommonFooter
|
||||
}
|
||||
|
||||
function Set-BranchUpstream {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$LocalBranch,
|
||||
[Parameter(Mandatory)][string]$RemoteName,
|
||||
[Parameter(Mandatory)][string]$RemoteBranchPath
|
||||
)
|
||||
$current = git rev-parse --abbrev-ref --symbolic-full-name "$LocalBranch@{upstream}" 2>$null
|
||||
if (-not $current) {
|
||||
Info "Setting upstream: $LocalBranch -> $RemoteName/$RemoteBranchPath"
|
||||
git branch --set-upstream-to "$RemoteName/$RemoteBranchPath" $LocalBranch 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Failed to set upstream automatically. Run: git branch --set-upstream-to $RemoteName/$RemoteBranchPath $LocalBranch" }
|
||||
return
|
||||
}
|
||||
if ($current -ne "$RemoteName/$RemoteBranchPath") {
|
||||
Warn "Upstream mismatch ($current != $RemoteName/$RemoteBranchPath); updating..."
|
||||
git branch --set-upstream-to "$RemoteName/$RemoteBranchPath" $LocalBranch 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Could not update upstream; manual fix: git branch --set-upstream-to $RemoteName/$RemoteBranchPath $LocalBranch" } else { Info 'Upstream corrected.' }
|
||||
} else { Info "Upstream already: $current" }
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shared build helper functions for PowerToys build scripts.
|
||||
|
||||
.DESCRIPTION
|
||||
This file provides reusable helper functions used by the build scripts:
|
||||
- Get-BuildPaths: returns ScriptDir, OriginalCwd, RepoRoot (repo root detection)
|
||||
- RunMSBuild: wrapper around msbuild.exe (accepts optional Platform/Configuration)
|
||||
- RestoreThenBuild: performs restore and optionally builds the solution/project
|
||||
- BuildProjectsInDirectory: discovers and builds local .slnx/.csproj/.vcxproj files
|
||||
- Ensure-VsDevEnvironment: initializes the Visual Studio developer environment when possible.
|
||||
It prefers the DevShell PowerShell module (Microsoft.VisualStudio.DevShell.dll / Enter-VsDevShell),
|
||||
falls back to running VsDevCmd.bat and importing its environment into the current PowerShell session,
|
||||
and restores the caller's working directory after initialization.
|
||||
|
||||
USAGE
|
||||
Dot-source this file from a script to load helpers:
|
||||
. "$PSScriptRoot\build-common.ps1"
|
||||
|
||||
ERROR DETAILS
|
||||
When a build fails, check the logs written next to the solution/project folder:
|
||||
- build.<configuration>.<platform>.all.log — full MSBuild text log
|
||||
- build.<configuration>.<platform>.errors.log — extracted errors only
|
||||
- build.<configuration>.<platform>.warnings.log — extracted warnings only
|
||||
- build.<configuration>.<platform>.trace.binlog — binary log (open with the MSBuild Structured Log Viewer)
|
||||
|
||||
.NOTES
|
||||
Do not execute this file directly; dot-source it from `build.ps1` or `build-installer.ps1` so helpers are available in your script scope.
|
||||
#>
|
||||
|
||||
function RunMSBuild {
|
||||
param (
|
||||
[string]$Solution,
|
||||
[string]$ExtraArgs,
|
||||
[string]$Platform,
|
||||
[string]$Configuration
|
||||
)
|
||||
|
||||
# Prefer the solution's folder for logs; fall back to current directory
|
||||
$logRoot = Split-Path -Path $Solution
|
||||
if (-not $logRoot) { $logRoot = '.' }
|
||||
|
||||
$cfg = $null
|
||||
if ($Configuration) { $cfg = $Configuration.ToLower() } else { $cfg = 'unknown' }
|
||||
$plat = $null
|
||||
if ($Platform) { $plat = $Platform.ToLower() } else { $plat = 'unknown' }
|
||||
|
||||
$allLog = Join-Path $logRoot ("build.{0}.{1}.all.log" -f $cfg, $plat)
|
||||
$warningLog = Join-Path $logRoot ("build.{0}.{1}.warnings.log" -f $cfg, $plat)
|
||||
$errorsLog = Join-Path $logRoot ("build.{0}.{1}.errors.log" -f $cfg, $plat)
|
||||
$binLog = Join-Path $logRoot ("build.{0}.{1}.trace.binlog" -f $cfg, $plat)
|
||||
|
||||
$base = @(
|
||||
$Solution
|
||||
"/p:Platform=$Platform"
|
||||
"/p:Configuration=$Configuration"
|
||||
"/verbosity:normal"
|
||||
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
|
||||
"/fileLoggerParameters:LogFile=$allLog;Verbosity=detailed"
|
||||
"/fileLoggerParameters1:LogFile=$warningLog;WarningsOnly"
|
||||
"/fileLoggerParameters2:LogFile=$errorsLog;ErrorsOnly"
|
||||
"/bl:$binLog"
|
||||
'/nologo'
|
||||
)
|
||||
|
||||
$cmd = $base + ($ExtraArgs -split ' ')
|
||||
Write-Host (("[MSBUILD] {0}" -f ($cmd -join ' ')))
|
||||
|
||||
Push-Location $script:RepoRoot
|
||||
try {
|
||||
& msbuild.exe @cmd
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error (("Build failed: {0} {1}`nSee logs:`n All: {2}`n Errors: {3}`n Binlog: {4}" -f $Solution, $ExtraArgs, $allLog, $errorsLog, $binLog))
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function RestoreThenBuild {
|
||||
param (
|
||||
[string]$Solution,
|
||||
[string]$ExtraArgs,
|
||||
[string]$Platform,
|
||||
[string]$Configuration,
|
||||
[bool]$RestoreOnly=$false
|
||||
)
|
||||
|
||||
$restoreArgs = '/t:restore /p:RestorePackagesConfig=true'
|
||||
if ($ExtraArgs) { $restoreArgs = "$restoreArgs $ExtraArgs" }
|
||||
RunMSBuild $Solution $restoreArgs $Platform $Configuration
|
||||
|
||||
if (-not $RestoreOnly) {
|
||||
$buildArgs = '/m'
|
||||
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
|
||||
RunMSBuild $Solution $buildArgs $Platform $Configuration
|
||||
}
|
||||
}
|
||||
|
||||
function BuildProjectsInDirectory {
|
||||
param(
|
||||
[string]$DirectoryPath,
|
||||
[string]$ExtraArgs,
|
||||
[string]$Platform,
|
||||
[string]$Configuration,
|
||||
[switch]$RestoreOnly
|
||||
)
|
||||
|
||||
if (-not (Test-Path $DirectoryPath)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$files = @()
|
||||
try {
|
||||
$files = Get-ChildItem -Path (Join-Path $DirectoryPath '*') -Include *.sln,*.slnx,*.slnf,*.csproj,*.vcxproj -File -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
$files = @()
|
||||
}
|
||||
|
||||
if (-not $files -or $files.Count -eq 0) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$names = ($files | ForEach-Object { $_.Name }) -join ', '
|
||||
Write-Host ("[LOCAL BUILD] Found {0} project(s) in {1}: {2}" -f $files.Count, $DirectoryPath, $names)
|
||||
|
||||
$preferredOrder = @('.sln', '.csproj', '.vcxproj')
|
||||
$files = $files | Sort-Object @{Expression = { [array]::IndexOf($preferredOrder, $_.Extension.ToLower()) }}
|
||||
|
||||
foreach ($f in $files) {
|
||||
Write-Host ("[LOCAL BUILD] Building {0}" -f $f.FullName)
|
||||
if ($f.Extension -eq '.sln' -or $f.Extension -eq '.slnx') {
|
||||
RestoreThenBuild $f.FullName $ExtraArgs $Platform $Configuration $RestoreOnly
|
||||
} else {
|
||||
$buildArgs = '/m'
|
||||
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
|
||||
RunMSBuild $f.FullName $buildArgs $Platform $Configuration
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-DefaultPlatform {
|
||||
<#
|
||||
Returns a default target platform string based on the host machine (x64, arm64, x86).
|
||||
#>
|
||||
try {
|
||||
$envArch = $env:PROCESSOR_ARCHITECTURE
|
||||
if ($envArch) { $envArch = $envArch.ToLower() }
|
||||
if ($envArch -eq 'amd64' -or $envArch -eq 'x86_64') { return 'x64' }
|
||||
if ($envArch -match 'arm64') { return 'arm64' }
|
||||
if ($envArch -eq 'x86') { return 'x86' }
|
||||
|
||||
if ($env:PROCESSOR_ARCHITEW6432) {
|
||||
$envArch2 = $env:PROCESSOR_ARCHITEW6432.ToLower()
|
||||
if ($envArch2 -eq 'amd64') { return 'x64' }
|
||||
if ($envArch2 -match 'arm64') { return 'arm64' }
|
||||
}
|
||||
|
||||
try {
|
||||
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
||||
switch ($osArch.ToString().ToLower()) {
|
||||
'x64' { return 'x64' }
|
||||
'arm64' { return 'arm64' }
|
||||
'x86' { return 'x86' }
|
||||
}
|
||||
} catch {
|
||||
# ignore - RuntimeInformation may not be available
|
||||
}
|
||||
} catch {
|
||||
# ignore any errors and fall back
|
||||
}
|
||||
|
||||
return 'x64'
|
||||
}
|
||||
|
||||
function Test-VsHasNativeTools {
|
||||
# Returns $true when the current process environment was initialized with a
|
||||
# usable native (C++) toolchain. Any VS instance (full SKU or Build Tools)
|
||||
# without the C++ workload leaves these unset, which is the original
|
||||
# MSB4086 ($(PlatformToolsetVersion) empty) failure mode we are guarding
|
||||
# against.
|
||||
if (-not $env:VCToolsInstallDir) { return $false }
|
||||
if (-not (Test-Path $env:VCToolsInstallDir)) { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
function Ensure-VsDevEnvironment {
|
||||
$OriginalLocationForVsInit = Get-Location
|
||||
try {
|
||||
|
||||
if ($env:VSINSTALLDIR -or $env:VCINSTALLDIR) {
|
||||
Write-Host "[VS] VS developer environment already present"
|
||||
return $true
|
||||
}
|
||||
|
||||
# Locate vswhere if available
|
||||
$vswhereCandidates = @(
|
||||
"$env:ProgramFiles (x86)\Microsoft Visual Studio\Installer\vswhere.exe",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
)
|
||||
$vswhere = $vswhereCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if ($vswhere) { Write-Host "[VS] vswhere found: $vswhere" } else { Write-Host "[VS] vswhere not found" }
|
||||
|
||||
# Probe for a Visual Studio install with the C++ workload. Selection is
|
||||
# capability-based (-requires VC.Tools.x86.x64), not SKU-based: full VS
|
||||
# SKUs and Build Tools are equally valid as long as the C++ workload is
|
||||
# installed. -prerelease lets vswhere see Preview/Insiders alongside GA.
|
||||
$vsProducts = @('Microsoft.VisualStudio.Product.Community',
|
||||
'Microsoft.VisualStudio.Product.Professional',
|
||||
'Microsoft.VisualStudio.Product.Enterprise',
|
||||
'Microsoft.VisualStudio.Product.BuildTools')
|
||||
|
||||
$instPaths = @()
|
||||
if ($vswhere) {
|
||||
# Newest VS instance with the C++ VC tools workload (stable + prerelease).
|
||||
try { $p = & $vswhere -latest -prerelease -products $vsProducts -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null; if ($p) { $instPaths += $p } } catch {}
|
||||
# Last-resort fallback if no instance reports the VC workload (still
|
||||
# validated below by Test-VsHasNativeTools to skip unusable installs).
|
||||
if (-not $instPaths) {
|
||||
try { $p2 = & $vswhere -latest -prerelease -products $vsProducts -property installationPath 2>$null; if ($p2) { $instPaths += $p2 } } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# Add explicit common candidates as a last resort (newest first)
|
||||
if (-not $instPaths) {
|
||||
$explicit = @(
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2026\Insiders",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2026\Preview",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2026\Community",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2026\Professional",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2026\Enterprise",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\18\Insiders",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\18\Preview",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\18\Community",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\18\Professional",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\18\Enterprise",
|
||||
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Preview",
|
||||
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Community",
|
||||
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Professional",
|
||||
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Enterprise",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2022\Preview",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2022\Community",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2022\Professional",
|
||||
"$env:ProgramFiles\Microsoft Visual Studio\2022\Enterprise"
|
||||
)
|
||||
foreach ($c in $explicit) { if (Test-Path $c) { $instPaths += $c } }
|
||||
}
|
||||
|
||||
if (-not $instPaths -or $instPaths.Count -eq 0) {
|
||||
Write-Warning "[VS] Could not locate Visual Studio installation (no candidates found)"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Try each candidate installation path until one yields a working native
|
||||
# toolchain (validated post-init via Test-VsHasNativeTools).
|
||||
foreach ($inst in $instPaths) {
|
||||
if (-not $inst) { continue }
|
||||
Write-Host "[VS] Checking candidate: $inst"
|
||||
|
||||
$devDll = Join-Path $inst 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll'
|
||||
if (Test-Path $devDll) {
|
||||
try {
|
||||
Import-Module $devDll -DisableNameChecking -ErrorAction Stop
|
||||
|
||||
# Call Enter-VsDevShell using only the install path to avoid parameter name differences
|
||||
try {
|
||||
Enter-VsDevShell -VsInstallPath $inst -ErrorAction Stop
|
||||
if (-not (Test-VsHasNativeTools)) {
|
||||
Write-Warning "[VS] DevShell entered $inst but VC tools (VCToolsInstallDir) not present - skipping"
|
||||
continue
|
||||
}
|
||||
Write-Host "[VS] Entered Visual Studio DevShell at $inst"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning ("[VS] DevShell import/Enter-VsDevShell failed: {0}" -f $_)
|
||||
}
|
||||
} catch {
|
||||
Write-Warning ("[VS] DevShell import failed: {0}" -f $_)
|
||||
}
|
||||
}
|
||||
|
||||
$vsDevCmd = Join-Path $inst 'Common7\Tools\VsDevCmd.bat'
|
||||
if (Test-Path $vsDevCmd) {
|
||||
Write-Host "[VS] Running VsDevCmd.bat and importing environment from $vsDevCmd"
|
||||
try {
|
||||
$cmdOut = cmd.exe /c "`"$vsDevCmd`" && set"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "[VS] VsDevCmd.bat exited with code $LASTEXITCODE at $inst"
|
||||
continue
|
||||
}
|
||||
foreach ($line in $cmdOut) {
|
||||
$parts = $line -split('=',2)
|
||||
if ($parts.Length -eq 2) {
|
||||
try { [Environment]::SetEnvironmentVariable($parts[0], $parts[1], 'Process') } catch {}
|
||||
}
|
||||
}
|
||||
if (-not (Test-VsHasNativeTools)) {
|
||||
Write-Warning "[VS] VsDevCmd.bat imported $inst but VC tools (VCToolsInstallDir) not present - skipping"
|
||||
continue
|
||||
}
|
||||
Write-Host "[VS] Imported environment from VsDevCmd.bat at $inst"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning ("[VS] Failed to run/import VsDevCmd.bat at {0}: {1}" -f $inst, $_)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Warning "[VS] Neither DevShell module nor VsDevCmd.bat found in any candidate paths"
|
||||
return $false
|
||||
|
||||
} finally {
|
||||
try { Set-Location $OriginalLocationForVsInit } catch {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
REM Wrapper to run build-essentials.ps1 from cmd.exe
|
||||
set SCRIPT_DIR=%~dp0
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-essentials.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,74 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build essential native PowerToys projects (runner and settings), restoring NuGet packages first.
|
||||
|
||||
.DESCRIPTION
|
||||
Lightweight script to build a small set of essential C++ projects used by PowerToys' runner and native modules. This script first restores NuGet packages for the full solution (`PowerToys.slnx`) and then builds the runner and settings projects. Intended for fast local builds during development.
|
||||
|
||||
.PARAMETER Platform
|
||||
Target platform for the build (for example: 'x64', 'arm64'). If omitted the script will attempt to auto-detect the host platform.
|
||||
|
||||
.PARAMETER Configuration
|
||||
Build configuration (for example: 'Debug' or 'Release'). Default is 'Debug'.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build-essentials.ps1
|
||||
Restores packages for the solution and builds the default set of native projects using the auto-detected platform and Debug configuration.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build-essentials.ps1 -Platform arm64 -Configuration Release
|
||||
Restores packages and builds the essentials in Release mode for ARM64, even if your machine is running on x64.
|
||||
|
||||
.NOTES
|
||||
- This script dot-sources `build-common.ps1` and uses the shared helper `RunMSBuild`.
|
||||
- It will call `RestoreThenBuild 'PowerToys.slnx'` before building the essential projects to ensure NuGet packages are restored.
|
||||
- The script attempts to locate the repository root automatically and can be run from any folder inside the repo.
|
||||
#>
|
||||
|
||||
param (
|
||||
[string]$Platform = '',
|
||||
[string]$Configuration = 'Debug'
|
||||
)
|
||||
|
||||
# Find repository root starting from the script location
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$repoRoot = $ScriptDir
|
||||
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
|
||||
$parent = Split-Path -Parent $repoRoot
|
||||
if ($parent -eq $repoRoot) {
|
||||
Write-Error "Could not find PowerToys repository root."
|
||||
exit 1
|
||||
}
|
||||
$repoRoot = $parent
|
||||
}
|
||||
|
||||
# Export script-scope variables used by build-common helpers
|
||||
Set-Variable -Name RepoRoot -Value $repoRoot -Scope Script -Force
|
||||
|
||||
# Load shared helpers
|
||||
. "$PSScriptRoot\build-common.ps1"
|
||||
|
||||
# Initialize Visual Studio dev environment
|
||||
if (-not (Ensure-VsDevEnvironment)) { exit 1 }
|
||||
|
||||
# If platform not provided, auto-detect from host
|
||||
if (-not $Platform -or $Platform -eq '') {
|
||||
try {
|
||||
$Platform = Get-DefaultPlatform
|
||||
Write-Host ("[AUTO-PLATFORM] Detected platform: {0}" -f $Platform)
|
||||
} catch {
|
||||
Write-Warning "Failed to auto-detect platform; defaulting to 'x64'"
|
||||
$Platform = 'x64'
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure solution packages are restored
|
||||
RestoreThenBuild 'PowerToys.slnx' '' $Platform $Configuration $true
|
||||
|
||||
# Build both runner and settings
|
||||
$ProjectsToBuild = @(".\src\runner\runner.vcxproj", ".\src\settings-ui\Settings.UI\PowerToys.Settings.csproj")
|
||||
$ExtraArgs = "/p:SolutionDir=$repoRoot\"
|
||||
foreach ($proj in $ProjectsToBuild) {
|
||||
Write-Host ("[BUILD-ESSENTIALS] Building {0}" -f $proj)
|
||||
RunMSBuild $proj $ExtraArgs $Platform $Configuration
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds and packages PowerToys (CmdPal and installer) locally. Handles solution build, signing, and WiX installer generation.
|
||||
|
||||
.PARAMETER Platform
|
||||
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'x64'.
|
||||
|
||||
.PARAMETER Configuration
|
||||
Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
|
||||
|
||||
.PARAMETER PerUser
|
||||
Specifies whether to build a per-user installer (true) or machine-wide installer (false). Default is true (per-user).
|
||||
|
||||
.EXAMPLE
|
||||
.\build-installer.ps1
|
||||
Runs the installer build pipeline for x64 Release.
|
||||
|
||||
.EXAMPLE
|
||||
.\build-installer.ps1 -Platform x64 -Configuration Release
|
||||
Runs the pipeline for x64 Release.
|
||||
|
||||
.EXAMPLE
|
||||
.\build-installer.ps1 -Platform x64 -Configuration Release -PerUser false
|
||||
Runs the pipeline for x64 Release with machine-wide installer.
|
||||
|
||||
.NOTES
|
||||
- Generated MSIX files will be signed using cert-sign-package.ps1.
|
||||
- If the working tree is not clean, the script will prompt before continuing (use -Force to skip the prompt).
|
||||
- Use the -Clean parameter to clean build outputs (bin/obj) and MSBuild outputs.
|
||||
- The built installer will be placed under: installer/PowerToysSetupVNext/[Platform]/[Configuration]/User[Machine]Setup
|
||||
relative to the solution root directory.
|
||||
- To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages.
|
||||
And trust the cert in the target machine.
|
||||
#>
|
||||
|
||||
param (
|
||||
[string]$Platform = 'x64',
|
||||
[string]$Configuration = 'Release',
|
||||
[string]$PerUser = 'true',
|
||||
[string]$Version,
|
||||
[switch]$Force,
|
||||
[switch]$EnableCmdPalAOT,
|
||||
[switch]$Clean,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: .\build-installer.ps1 [-Platform <x64|arm64>] [-Configuration <Release|Debug>] [-PerUser <true|false>] [-Version <0.0.1>] [-Force] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]"
|
||||
Write-Host " -Platform Target platform (default: auto-detect or x64)"
|
||||
Write-Host " -Configuration Build configuration (default: Release)"
|
||||
Write-Host " -PerUser Build per-user installer (default: true)"
|
||||
Write-Host " -Version Sets the PowerToys version (default: from src\Version.props)"
|
||||
Write-Host " -Force Continue even if the git working tree is not clean (skips the interactive prompt)."
|
||||
Write-Host " -EnableCmdPalAOT Enable AOT compilation for CmdPal (slower build)"
|
||||
Write-Host " -Clean Clean output directories before building"
|
||||
Write-Host " -SkipBuild Skip building the main solution and tools (assumes they are already built)"
|
||||
Write-Host " -Help Show this help message"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Ensure helpers are available
|
||||
. "$PSScriptRoot\build-common.ps1"
|
||||
|
||||
# Initialize Visual Studio dev environment
|
||||
if (-not (Ensure-VsDevEnvironment)) { exit 1 }
|
||||
|
||||
# Auto-detect platform when not provided
|
||||
if (-not $Platform -or $Platform -eq '') {
|
||||
try {
|
||||
$Platform = Get-DefaultPlatform
|
||||
Write-Host ("[AUTO-PLATFORM] Detected platform: {0}" -f $Platform)
|
||||
} catch {
|
||||
Write-Warning "Failed to auto-detect platform; defaulting to x64"
|
||||
$Platform = 'x64'
|
||||
}
|
||||
}
|
||||
|
||||
# Find the PowerToys repository root automatically
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$repoRoot = $scriptDir
|
||||
|
||||
# Navigate up from the script location to find the repo root
|
||||
# Script is typically in tools\build, so go up two levels
|
||||
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
|
||||
$parentDir = Split-Path -Parent $repoRoot
|
||||
if ($parentDir -eq $repoRoot) {
|
||||
# Reached the root of the drive, PowerToys.slnx not found
|
||||
Write-Error "Could not find PowerToys repository root. Make sure this script is in the PowerToys repository."
|
||||
exit 1
|
||||
}
|
||||
$repoRoot = $parentDir
|
||||
}
|
||||
|
||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
|
||||
Write-Error "Could not locate PowerToys.slnx. Please ensure this script is run from within the PowerToys repository."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "PowerToys repository root detected: $repoRoot"
|
||||
|
||||
# Safety check: avoid mixing build outputs with existing local changes unless the user confirms.
|
||||
if (-not $Force) {
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
$gitStatus = $null
|
||||
$gitRelevantStatus = @()
|
||||
try {
|
||||
$gitStatus = git status --porcelain=v1 --untracked-files=all --ignore-submodules=all
|
||||
} catch {
|
||||
Write-Warning ("[GIT] Failed to query git status: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
if ($gitStatus -and $gitStatus.Length -gt 0) {
|
||||
foreach ($line in $gitStatus) {
|
||||
if (-not $line) { continue }
|
||||
|
||||
# Porcelain v1 format: XY <path>
|
||||
# We only care about changes that affect the working tree (Y != ' ') or untracked files (??).
|
||||
# Index-only changes (staged, Y == ' ') are ignored per user request.
|
||||
if ($line.StartsWith('??')) {
|
||||
$gitRelevantStatus += $line
|
||||
continue
|
||||
}
|
||||
|
||||
if ($line.StartsWith('!!')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($line.Length -ge 2) {
|
||||
$workTreeStatus = $line[1]
|
||||
if ($workTreeStatus -ne ' ') {
|
||||
$gitRelevantStatus += $line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($gitRelevantStatus.Count -gt 0) {
|
||||
Write-Warning "[GIT] Working tree is NOT clean."
|
||||
Write-Warning "[GIT] This build will generate untracked files and may modify tracked files, which can mix with your current changes."
|
||||
Write-Host "[GIT] Unstaged/untracked status (first 50 lines):"
|
||||
$gitRelevantStatus | Select-Object -First 50 | ForEach-Object { Write-Host (" {0}" -f $_) }
|
||||
|
||||
$shouldContinue = $false
|
||||
try {
|
||||
$choices = [System.Management.Automation.Host.ChoiceDescription[]]@(
|
||||
(New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue the build."),
|
||||
(New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Cancel the build.")
|
||||
)
|
||||
$decision = $Host.UI.PromptForChoice("Working tree not clean", "Continue anyway?", $choices, 1)
|
||||
$shouldContinue = ($decision -eq 0)
|
||||
} catch {
|
||||
Write-Warning "[GIT] Interactive prompt not available."
|
||||
Write-Error "Refusing to proceed with a dirty working tree. Re-run with -Force to continue anyway."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $shouldContinue) {
|
||||
Write-Host "[GIT] Cancelled by user."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
|
||||
$buildOutputPath = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
|
||||
# Clean should be done first before any other steps
|
||||
if ($Clean) {
|
||||
if (Test-Path $cmdpalOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
|
||||
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
if (Test-Path $buildOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous build output: $buildOutputPath"
|
||||
Remove-Item $buildOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
|
||||
Write-Host "[CLEAN] Cleaning solution (msbuild /t:Clean)..."
|
||||
RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Version) {
|
||||
Write-Host "[VERSION] Setting PowerToys version to $Version using versionSetting.ps1..."
|
||||
$versionScript = Join-Path $repoRoot ".pipelines\versionSetting.ps1"
|
||||
if (Test-Path $versionScript) {
|
||||
& $versionScript -versionNumber $Version -DevEnvironment 'Local'
|
||||
if (-not $?) {
|
||||
Write-Error "versionSetting.ps1 failed"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Error "Could not find versionSetting.ps1 at: $versionScript"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[VERSION] Setting up versioning using Microsoft.Windows.Terminal.Versioning..."
|
||||
|
||||
# Check for nuget.exe - download to AppData if not available
|
||||
$nugetDownloaded = $false
|
||||
$nugetPath = $null
|
||||
if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "nuget.exe not found in PATH. Attempting to download..."
|
||||
$nugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
|
||||
$nugetDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools"
|
||||
if (-not (Test-Path $nugetDir)) { New-Item -ItemType Directory -Path $nugetDir -Force | Out-Null }
|
||||
$nugetPath = Join-Path $nugetDir "nuget.exe"
|
||||
if (-not (Test-Path $nugetPath)) {
|
||||
try {
|
||||
Invoke-WebRequest $nugetUrl -OutFile $nugetPath
|
||||
$nugetDownloaded = $true
|
||||
} catch {
|
||||
Write-Error "Failed to download nuget.exe. Please install it manually and add to PATH."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
$env:Path += ";$nugetDir"
|
||||
}
|
||||
|
||||
# Install Terminal versioning package to AppData
|
||||
$versioningDir = Join-Path $env:LOCALAPPDATA "PowerToys\BuildTools\.versioning"
|
||||
if (-not (Test-Path $versioningDir)) { New-Item -ItemType Directory -Path $versioningDir -Force | Out-Null }
|
||||
|
||||
$configFile = Join-Path $repoRoot ".pipelines\release-nuget.config"
|
||||
|
||||
# Install the package
|
||||
# Use -ExcludeVersion to make the path predictable
|
||||
nuget install Microsoft.Windows.Terminal.Versioning -ConfigFile $configFile -OutputDirectory $versioningDir -ExcludeVersion -NonInteractive
|
||||
|
||||
$versionRoot = Join-Path $versioningDir "Microsoft.Windows.Terminal.Versioning"
|
||||
$setupScript = Join-Path $versionRoot "build\Setup.ps1"
|
||||
|
||||
if (Test-Path $setupScript) {
|
||||
& $setupScript -ProjectDirectory (Join-Path $repoRoot "src\modules\cmdpal") -Verbose
|
||||
} else {
|
||||
Write-Error "Could not find Setup.ps1 in $versionRoot"
|
||||
}
|
||||
|
||||
# WiX v5 projects use WixToolset.Sdk via NuGet/MSBuild; no separate WiX installation is required.
|
||||
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser)
|
||||
Write-Host ''
|
||||
|
||||
$commonArgs = '/p:CIBuild=true /p:IsPipeline=true'
|
||||
|
||||
if ($EnableCmdPalAOT) {
|
||||
$commonArgs += " /p:EnableCmdPalAOT=true"
|
||||
}
|
||||
|
||||
# No local projects found (or continuing) - build full solution and tools
|
||||
if (-not $SkipBuild) {
|
||||
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
|
||||
}
|
||||
|
||||
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
|
||||
Select-Object -ExpandProperty FullName
|
||||
|
||||
if ($msixFiles.Count) {
|
||||
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
|
||||
& (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles
|
||||
}
|
||||
else {
|
||||
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
|
||||
}
|
||||
|
||||
# Generate DSC v2 manifests (PowerToys.Settings.DSC.Schema.Generator)
|
||||
# The csproj PostBuild event is skipped on ARM64, so we run it manually here if needed.
|
||||
if ($Platform -eq 'arm64') {
|
||||
Write-Host "[DSC] Manually generating DSC v2 manifests for ARM64..."
|
||||
|
||||
# 1. Get Version
|
||||
$versionPropsPath = Join-Path $repoRoot "src\Version.props"
|
||||
[xml]$versionProps = Get-Content $versionPropsPath
|
||||
$ptVersion = $versionProps.Project.PropertyGroup.Version
|
||||
# Directory.Build.props appends .0 to the version for .csproj files
|
||||
$ptVersionFull = "$ptVersion.0"
|
||||
|
||||
# 2. Build the Generator
|
||||
$generatorProj = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj"
|
||||
RunMSBuild $generatorProj "/t:Build" $Platform $Configuration
|
||||
|
||||
# 3. Define paths
|
||||
# The generator output path is in the project's bin folder
|
||||
$generatorExe = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\bin\$Platform\$Configuration\PowerToys.Settings.DSC.Schema.Generator.exe"
|
||||
|
||||
if (-not (Test-Path $generatorExe)) {
|
||||
Write-Warning "Could not find generator at expected path: $generatorExe"
|
||||
Write-Warning "Searching in build output..."
|
||||
$found = Get-ChildItem -Path (Join-Path $repoRoot "$Platform\$Configuration") -Filter "PowerToys.Settings.DSC.Schema.Generator.exe" -Recurse | Select-Object -First 1
|
||||
if ($found) {
|
||||
$generatorExe = $found.FullName
|
||||
}
|
||||
}
|
||||
|
||||
$settingsLibDll = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\PowerToys.Settings.UI.Lib.dll"
|
||||
|
||||
$dscGenDir = Join-Path $repoRoot "src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$ptVersionFull"
|
||||
if (-not (Test-Path $dscGenDir)) {
|
||||
New-Item -ItemType Directory -Path $dscGenDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$outPsm1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psm1"
|
||||
$outPsd1 = Join-Path $dscGenDir "Microsoft.PowerToys.Configure.psd1"
|
||||
|
||||
# 4. Run Generator
|
||||
if (Test-Path $generatorExe) {
|
||||
Write-Host "[DSC] Executing: $generatorExe"
|
||||
|
||||
$generatorDir = Split-Path -Parent $generatorExe
|
||||
$winUI3AppsDir = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps"
|
||||
|
||||
# Copy dependencies from WinUI3Apps to Generator directory to satisfy WinRT/WinUI3 dependencies
|
||||
# This avoids "Class not registered" errors without polluting the WinUI3Apps directory which is used for packaging.
|
||||
if (Test-Path $winUI3AppsDir) {
|
||||
Write-Host "[DSC] Copying dependencies from $winUI3AppsDir to $generatorDir"
|
||||
Get-ChildItem -Path $winUI3AppsDir -Filter "*.dll" | ForEach-Object {
|
||||
$destPath = Join-Path $generatorDir $_.Name
|
||||
if (-not (Test-Path $destPath)) {
|
||||
Copy-Item -Path $_.FullName -Destination $destPath -Force
|
||||
}
|
||||
}
|
||||
# Also copy resources.pri if it exists, as it might be needed for resource lookup
|
||||
$priFile = Join-Path $winUI3AppsDir "resources.pri"
|
||||
if (Test-Path $priFile) {
|
||||
Copy-Item -Path $priFile -Destination $generatorDir -Force
|
||||
}
|
||||
}
|
||||
|
||||
Push-Location $generatorDir
|
||||
try {
|
||||
# Now we can use the local DLLs
|
||||
$localSettingsLibDll = Join-Path $generatorDir "PowerToys.Settings.UI.Lib.dll"
|
||||
|
||||
if (Test-Path $localSettingsLibDll) {
|
||||
Write-Host "[DSC] Using local DLL: $localSettingsLibDll"
|
||||
& $generatorExe $localSettingsLibDll $outPsm1 $outPsd1
|
||||
} else {
|
||||
# Fallback (shouldn't happen if copy succeeded or build was correct)
|
||||
Write-Warning "[DSC] Local DLL not found, falling back to: $settingsLibDll"
|
||||
& $generatorExe $settingsLibDll $outPsm1 $outPsd1
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "DSC v2 generation failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host "[DSC] DSC v2 manifests generated successfully."
|
||||
} else {
|
||||
Write-Error "Could not find generator executable at $generatorExe"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate DSC manifest files
|
||||
Write-Host '[DSC] Generating DSC manifest files...'
|
||||
$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
|
||||
if (Test-Path $dscScriptPath) {
|
||||
& $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
Write-Host '[DSC] DSC manifest files generated successfully'
|
||||
} else {
|
||||
Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
|
||||
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
|
||||
}
|
||||
|
||||
# Set NUGET_PACKAGES environment variable if not set, to help wixproj find heat.exe
|
||||
if (-not $env:NUGET_PACKAGES) {
|
||||
$env:NUGET_PACKAGES = Join-Path $env:USERPROFILE ".nuget\packages"
|
||||
Write-Host "[ENV] Set NUGET_PACKAGES to $env:NUGET_PACKAGES"
|
||||
}
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
# Fix: WiX v5 locally puts the MSI in an 'en-us' subfolder, but the Bootstrapper expects it in the root of UserSetup/MachineSetup.
|
||||
# We move it up one level to match expectations.
|
||||
$setupType = if ($PerUser -eq 'true') { 'UserSetup' } else { 'MachineSetup' }
|
||||
$msiParentDir = Join-Path $repoRoot "installer\PowerToysSetupVNext\$Platform\$Configuration\$setupType"
|
||||
$msiEnUsDir = Join-Path $msiParentDir "en-us"
|
||||
|
||||
if (Test-Path $msiEnUsDir) {
|
||||
Write-Host "[FIX] Moving MSI files from $msiEnUsDir to $msiParentDir"
|
||||
Get-ChildItem -Path $msiEnUsDir -Filter *.msi | Move-Item -Destination $msiParentDir -Force
|
||||
}
|
||||
|
||||
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
|
||||
|
||||
} finally {
|
||||
# No git cleanup; leave workspace state as-is.
|
||||
}
|
||||
|
||||
Write-Host '[PIPELINE] Completed'
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
REM Wrapper to run the PowerShell build script from cmd.exe
|
||||
set SCRIPT_DIR=%~dp0
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,104 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Light-weight wrapper to build local projects (solutions/projects) in the current working directory using helpers in build-common.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
This script is intended for quick local builds. It dot-sources `build-common.ps1` and calls `BuildProjectsInDirectory` against the current directory. Use `-RestoreOnly` to only restore packages for local projects. If `-Platform` is omitted the script attempts to auto-detect the host platform.
|
||||
|
||||
.PARAMETER Platform
|
||||
Target platform (e.g., 'x64', 'arm64'). If omitted the script will try to detect the host platform automatically.
|
||||
|
||||
.PARAMETER Configuration
|
||||
Build configuration (e.g., 'Debug', 'Release'). Default: 'Debug'.
|
||||
|
||||
.PARAMETER Path
|
||||
Optional directory path containing projects to build. If not specified, uses the current working directory.
|
||||
|
||||
.PARAMETER RestoreOnly
|
||||
If specified, only perform package restore for local projects and skip the build steps for a solution file (i.e. .sln).
|
||||
|
||||
.PARAMETER ExtraArgs
|
||||
Any remaining, positional arguments passed to the script are forwarded to MSBuild as additional arguments (e.g., '/p:CIBuild=true').
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build.ps1
|
||||
Builds any .sln/.csproj/.vcxproj in the current working directory (auto-detects Platform).
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build.ps1 -Platform x64 -Configuration Release -Path "C:\MyProject\src"
|
||||
Builds local projects in the specified directory for x64 Release.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build.ps1 -Platform x64 -Configuration Release
|
||||
Builds local projects for x64 Release.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build.ps1 '/p:CIBuild=true' '/p:SomeOther=Value'
|
||||
Pass additional MSBuild arguments; these are forwarded to the underlying msbuild calls.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\build.ps1 -RestoreOnly '/p:CIBuild=true'
|
||||
Only restores packages for local projects; ExtraArgs still forwarded to msbuild's restore phase.
|
||||
|
||||
.NOTES
|
||||
- This file expects `build-common.ps1` to be located in the same folder and dot-sources it to load helper functions.
|
||||
- ExtraArgs are captured using PowerShell's ValueFromRemainingArguments and joined before being passed to the helpers.
|
||||
#>
|
||||
|
||||
param (
|
||||
[string]$Platform = '',
|
||||
[string]$Configuration = 'Debug',
|
||||
[string]$Path = '',
|
||||
[switch]$RestoreOnly,
|
||||
[Parameter(ValueFromRemainingArguments=$true)]
|
||||
[string[]]$ExtraArgs
|
||||
)
|
||||
|
||||
. "$PSScriptRoot\build-common.ps1"
|
||||
|
||||
# Initialize Visual Studio dev environment
|
||||
if (-not (Ensure-VsDevEnvironment)) { exit 1 }
|
||||
|
||||
# If user passed MSBuild-style args (e.g. './build.ps1 /p:CIBuild=true'),
|
||||
# those will bind to $Platform/$Configuration; detect those and move them to ExtraArgs.
|
||||
$positionalExtra = @()
|
||||
if ($Platform -and $Platform -match '^[\/-]') {
|
||||
$positionalExtra += $Platform
|
||||
$Platform = ''
|
||||
}
|
||||
if ($Configuration -and $Configuration -match '^[\/-]') {
|
||||
$positionalExtra += $Configuration
|
||||
$Configuration = 'Debug'
|
||||
}
|
||||
if ($positionalExtra.Count -gt 0) {
|
||||
if (-not $ExtraArgs) { $ExtraArgs = @() }
|
||||
$ExtraArgs = $positionalExtra + $ExtraArgs
|
||||
}
|
||||
|
||||
# Auto-detect platform when not provided
|
||||
if (-not $Platform -or $Platform -eq '') {
|
||||
try {
|
||||
$Platform = Get-DefaultPlatform
|
||||
Write-Host ("[AUTO-PLATFORM] Detected platform: {0}" -f $Platform)
|
||||
} catch {
|
||||
Write-Warning "Failed to auto-detect platform; defaulting to x64"
|
||||
$Platform = 'x64'
|
||||
}
|
||||
}
|
||||
|
||||
$cwd = if ($Path -ne '') {
|
||||
(Resolve-Path $Path).ProviderPath
|
||||
} else {
|
||||
(Get-Location).ProviderPath
|
||||
}
|
||||
$extraArgsString = $null
|
||||
if ($ExtraArgs -and $ExtraArgs.Count -gt 0) { $extraArgsString = ($ExtraArgs -join ' ') }
|
||||
|
||||
$built = BuildProjectsInDirectory -DirectoryPath $cwd -ExtraArgs $extraArgsString -Platform $Platform -Configuration $Configuration -RestoreOnly:$RestoreOnly
|
||||
if ($built) {
|
||||
Write-Host "[BUILD] Local projects built; exiting."
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "[BUILD] No local projects found in $cwd"
|
||||
exit 0
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensures a code signing certificate exists and is trusted in all necessary certificate stores.
|
||||
|
||||
.DESCRIPTION
|
||||
This script provides two functions:
|
||||
|
||||
1. EnsureCertificate:
|
||||
- Searches for an existing code signing certificate by subject name.
|
||||
- If not found, creates a new self-signed certificate.
|
||||
- Exports the certificate and attempts to import it into:
|
||||
- CurrentUser\TrustedPeople
|
||||
- CurrentUser\Root
|
||||
- LocalMachine\Root (admin privileges may be required)
|
||||
|
||||
2. ImportAndVerifyCertificate:
|
||||
- Imports a `.cer` file into the specified certificate store if not already present.
|
||||
- Verifies the certificate is successfully imported by checking thumbprint.
|
||||
|
||||
This is useful in build or signing pipelines to ensure a valid and trusted certificate is available before signing MSIX or executable files.
|
||||
|
||||
.PARAMETER certSubject
|
||||
The subject name of the certificate to search for or create. Default is:
|
||||
"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
|
||||
.PARAMETER cerPath
|
||||
(ImportAndVerifyCertificate only) The file path to a `.cer` certificate file to import.
|
||||
|
||||
.PARAMETER storePath
|
||||
(ImportAndVerifyCertificate only) The destination certificate store path (e.g. Cert:\CurrentUser\Root).
|
||||
|
||||
.EXAMPLE
|
||||
$cert = EnsureCertificate
|
||||
|
||||
Ensures the default certificate exists and is trusted, and returns the certificate object.
|
||||
|
||||
.EXAMPLE
|
||||
ImportAndVerifyCertificate -cerPath "$env:TEMP\temp_cert.cer" -storePath "Cert:\CurrentUser\Root"
|
||||
|
||||
Imports a certificate into the CurrentUser Root store and verifies its presence.
|
||||
|
||||
.NOTES
|
||||
- For full trust, administrative privileges may be needed to import into LocalMachine\Root.
|
||||
- Certificates are created using RSA and SHA256 and marked as CodeSigningCert.
|
||||
#>
|
||||
|
||||
function ImportAndVerifyCertificate {
|
||||
param (
|
||||
[string]$cerPath,
|
||||
[string]$storePath
|
||||
)
|
||||
|
||||
$thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint
|
||||
|
||||
$existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
if ($existingCert) {
|
||||
Write-Host "Certificate already exists in $storePath"
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
|
||||
} catch {
|
||||
if ($_.Exception.Message -match "Access is denied" -or $_.Exception.InnerException.Message -match "Access is denied") {
|
||||
Write-Warning "Access denied to $storePath. Attempting to import with admin privileges..."
|
||||
try {
|
||||
Start-Process powershell -ArgumentList "-NoProfile", "-Command", "& { Import-Certificate -FilePath '$cerPath' -CertStoreLocation '$storePath' }" -Verb RunAs -Wait
|
||||
} catch {
|
||||
Write-Warning "Failed to request admin privileges: $_"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Failed to import certificate to $storePath : $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
if ($imported) {
|
||||
Write-Host "Certificate successfully imported to $storePath"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Certificate not found in $storePath after import"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function EnsureCertificate {
|
||||
param (
|
||||
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
)
|
||||
|
||||
$cert = Get-ChildItem -Path Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Subject -eq $certSubject } |
|
||||
Sort-Object NotAfter -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Host "Certificate not found. Creating a new one..."
|
||||
|
||||
$cert = New-SelfSignedCertificate -Subject $certSubject `
|
||||
-CertStoreLocation "Cert:\CurrentUser\My" `
|
||||
-KeyAlgorithm RSA `
|
||||
-Type CodeSigningCert `
|
||||
-HashAlgorithm SHA256
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Error "Failed to create a new certificate."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Host "New certificate created with thumbprint: $($cert.Thumbprint)"
|
||||
}
|
||||
else {
|
||||
Write-Host "Using existing certificate with thumbprint: $($cert.Thumbprint)"
|
||||
}
|
||||
|
||||
$cerPath = "$env:TEMP\temp_cert.cer"
|
||||
[void](Export-Certificate -Cert $cert -FilePath $cerPath -Force)
|
||||
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) { return $null }
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) { return $null }
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) {
|
||||
Write-Warning "Failed to import to LocalMachine\Root (admin may be required)"
|
||||
return $null
|
||||
}
|
||||
|
||||
return $cert
|
||||
}
|
||||
|
||||
function Export-CertificateFiles {
|
||||
param (
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||
[string]$CerPath,
|
||||
[string]$PfxPath,
|
||||
[securestring]$PfxPassword
|
||||
)
|
||||
|
||||
if (-not $Certificate) {
|
||||
Write-Error "No certificate provided to export."
|
||||
return
|
||||
}
|
||||
|
||||
if ($CerPath) {
|
||||
try {
|
||||
Export-Certificate -Cert $Certificate -FilePath $CerPath -Force | Out-Null
|
||||
Write-Host "Exported CER to: $CerPath"
|
||||
} catch {
|
||||
Write-Warning "Failed to export CER file: $_"
|
||||
}
|
||||
}
|
||||
|
||||
if ($PfxPath -and $PfxPassword) {
|
||||
try {
|
||||
Export-PfxCertificate -Cert $Certificate -FilePath $PfxPath -Password $PfxPassword -Force | Out-Null
|
||||
Write-Host "Exported PFX to: $PfxPath"
|
||||
} catch {
|
||||
Write-Warning "Failed to export PFX file: $_"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $CerPath -and -not $PfxPath) {
|
||||
Write-Warning "No output path specified. Nothing was exported."
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution when script is run directly
|
||||
if ($MyInvocation.InvocationName -ne '.') {
|
||||
Write-Host "=== PowerToys Certificate Management ===" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Ensure certificate exists and is trusted
|
||||
Write-Host "Checking for existing certificate or creating new one..." -ForegroundColor Yellow
|
||||
$cert = EnsureCertificate
|
||||
|
||||
if ($cert) {
|
||||
# Export the certificate to a .cer file
|
||||
$exportPath = Join-Path (Get-Location) "PowerToys-CodeSigning.cer"
|
||||
Write-Host ""
|
||||
Write-Host "Exporting certificate..." -ForegroundColor Yellow
|
||||
Export-CertificateFiles -Certificate $cert -CerPath $exportPath
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== IMPORTANT NOTES ===" -ForegroundColor Red
|
||||
Write-Host "The certificate has been exported to: $exportPath" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "To use this certificate for code signing, you need to:" -ForegroundColor Yellow
|
||||
Write-Host "1. Import this certificate into 'Trusted People' store" -ForegroundColor White
|
||||
Write-Host "2. Import this certificate into 'Trusted Root Certification Authorities' store" -ForegroundColor White
|
||||
Write-Host "Certificate Details:" -ForegroundColor Green
|
||||
Write-Host "Subject: $($cert.Subject)" -ForegroundColor White
|
||||
Write-Host "Thumbprint: $($cert.Thumbprint)" -ForegroundColor White
|
||||
Write-Host "Valid Until: $($cert.NotAfter)" -ForegroundColor White
|
||||
} else {
|
||||
Write-Error "Failed to create or find certificate. Please check the error messages above."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
param (
|
||||
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
|
||||
[string[]]$TargetPaths = "C:\PowerToys\ARM64\Release\WinUI3Apps\CmdPal\AppPackages\Microsoft.CmdPal.UI_0.0.1.0_Test\Microsoft.CmdPal.UI_0.0.1.0_arm64.msix"
|
||||
)
|
||||
|
||||
. "$PSScriptRoot\cert-management.ps1"
|
||||
$cert = EnsureCertificate -certSubject $certSubject
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Error "Failed to prepare certificate."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Certificate ready: $($cert.Thumbprint)"
|
||||
|
||||
if (-not $TargetPaths -or $TargetPaths.Count -eq 0) {
|
||||
Write-Error "No target files provided to sign."
|
||||
exit 1
|
||||
}
|
||||
|
||||
foreach ($filePath in $TargetPaths) {
|
||||
if (-not (Test-Path $filePath)) {
|
||||
Write-Warning "Skipping: File does not exist - $filePath"
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Signing: $filePath"
|
||||
& signtool sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com "$filePath"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Cleans PowerToys build artifacts to resolve build errors.
|
||||
|
||||
.DESCRIPTION
|
||||
Use this script when you encounter build errors about missing image files or corrupted
|
||||
build state. It removes build output folders and optionally runs MSBuild Clean.
|
||||
|
||||
.PARAMETER SkipMSBuildClean
|
||||
Skip running MSBuild Clean target, only delete folders.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\clean-artifacts.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\clean-artifacts.ps1 -SkipMSBuildClean
|
||||
#>
|
||||
|
||||
param (
|
||||
[switch]$SkipMSBuildClean
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$scriptDir = $PSScriptRoot
|
||||
$repoRoot = (Resolve-Path "$scriptDir\..\..").Path
|
||||
|
||||
Write-Host "Cleaning build artifacts..."
|
||||
Write-Host ""
|
||||
|
||||
# Run MSBuild Clean
|
||||
if (-not $SkipMSBuildClean) {
|
||||
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vsWhere) {
|
||||
$vsPath = & $vsWhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath
|
||||
if ($vsPath) {
|
||||
$msbuildPath = Join-Path $vsPath "MSBuild\Current\Bin\MSBuild.exe"
|
||||
if (Test-Path $msbuildPath) {
|
||||
$solutionFile = Join-Path $repoRoot "PowerToys.sln"
|
||||
if (-not (Test-Path $solutionFile)) {
|
||||
$solutionFile = Join-Path $repoRoot "PowerToys.slnx"
|
||||
}
|
||||
|
||||
if (Test-Path $solutionFile) {
|
||||
Write-Host " Running MSBuild Clean..."
|
||||
foreach ($plat in @('x64', 'ARM64')) {
|
||||
foreach ($config in @('Debug', 'Release')) {
|
||||
& $msbuildPath $solutionFile /t:Clean /p:Platform=$plat /p:Configuration=$config /verbosity:quiet 2>&1 | Out-Null
|
||||
}
|
||||
}
|
||||
Write-Host " Done."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Delete build folders
|
||||
$folders = @('x64', 'ARM64', 'Debug', 'Release', 'packages')
|
||||
$deleted = @()
|
||||
|
||||
foreach ($folder in $folders) {
|
||||
$fullPath = Join-Path $repoRoot $folder
|
||||
if (Test-Path $fullPath) {
|
||||
Write-Host " Removing $folder/"
|
||||
try {
|
||||
Remove-Item -Path $fullPath -Recurse -Force -ErrorAction Stop
|
||||
$deleted += $folder
|
||||
} catch {
|
||||
Write-Host " Failed to remove $folder/: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if ($deleted.Count -gt 0) {
|
||||
Write-Host "Removed: $($deleted -join ', ')"
|
||||
} else {
|
||||
Write-Host "No build folders found to remove."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "To rebuild, run:"
|
||||
Write-Host " msbuild -restore -p:RestorePackagesConfig=true -p:Platform=x64 -m PowerToys.slnx"
|
||||
@@ -0,0 +1,213 @@
|
||||
# This script is used to move the resources from all the resx files in the directory (args[0]) to a .rc and .h file for use in C++ projects.
|
||||
|
||||
# Root directory which contains the resx files
|
||||
$parentDirectory = $args[0]
|
||||
|
||||
# File name of the base resource.h which contains all the non-localized resource definitions
|
||||
$baseHeaderFileName = $args[1]
|
||||
|
||||
# Target file name of the resource header file, which will be used in code - Example: resource.h
|
||||
$generatedHeaderFileName = $args[2]
|
||||
|
||||
# File name of the base ProjectName.rc which contains all the non-localized resources
|
||||
$baseRCFileName = $args[3]
|
||||
|
||||
# Target file name of the resource rc file, which will be used in code - Example: ProjectName.rc
|
||||
$generatedRCFileName = $args[4]
|
||||
|
||||
# Optional argument: Initial resource id in the resource header file. By default it is 101
|
||||
if ($args.Count -eq 6)
|
||||
{
|
||||
$initResourceID = $args[5]
|
||||
}
|
||||
else
|
||||
{
|
||||
$initResourceID = 101
|
||||
}
|
||||
|
||||
# Flags to check if the first updated has occurred
|
||||
$headerFileUpdated = $false
|
||||
$rcFileUpdated = $false
|
||||
|
||||
# Output folder for the new resource files. It will be in ProjectDir\Generated Files so that the files are ignored by .gitignore
|
||||
$generatedFilesFolder = $parentDirectory + "\Generated Files"
|
||||
|
||||
# Create Generated Files folder if it doesn't exist
|
||||
if (!(Test-Path -Path $generatedFilesFolder))
|
||||
{
|
||||
$paramNewItem = @{
|
||||
Path = $generatedFilesFolder
|
||||
ItemType = 'Directory'
|
||||
Force = $true
|
||||
}
|
||||
|
||||
New-Item @paramNewItem
|
||||
}
|
||||
|
||||
# Hash table to get the language codes from the code used in the file name
|
||||
$languageHashTable = @{ "en" = @("ENU", "ENGLISH", "ENGLISH_US", "English (United States)");
|
||||
"zh-Hans" = @("CHS", "CHINESE", "NEUTRAL", "Chinese (Simplified)");
|
||||
"zh-CN" = @("CHS", "CHINESE", "NEUTRAL", "Chinese (Simplified)");
|
||||
"cs" = @("CSY", "CZECH", "NEUTRAL", "Czech");
|
||||
"hu" = @("HUN", "HUNGARIAN", "NEUTRAL", "Hungarian");
|
||||
"pl" = @("PLK", "POLISH", "NEUTRAL", "Polish");
|
||||
"ro" = @("ROM", "ROMANIAN", "NEUTRAL", "Romanian");
|
||||
"sk" = @("SKY", "SLOVAK", "NEUTRAL", "Slovak");
|
||||
"bg" = @("BGR", "BULGARIAN", "NEUTRAL", "Bulgarian");
|
||||
"ru" = @("RUS", "RUSSIAN", "NEUTRAL", "Russian");
|
||||
"ca" = @("CAT", "CATALAN", "NEUTRAL", "Catalan");
|
||||
"de" = @("DEU", "GERMAN", "NEUTRAL", "German");
|
||||
"es" = @("ESN", "SPANISH", "NEUTRAL", "Spanish");
|
||||
"fr" = @("FRA", "FRENCH", "NEUTRAL", "French");
|
||||
"it" = @("ITA", "ITALIAN", "NEUTRAL", "Italian");
|
||||
"nl" = @("NLD", "DUTCH", "NEUTRAL", "Dutch");
|
||||
"nb-NO" = @("NOR", "NORWEGIAN", "NORWEGIAN_BOKMAL", "Norwegian Bokmål (Norway)");
|
||||
"pt-BR" = @("PTB", "PORTUGUESE", "PORTUGUESE_BRAZILIAN", "Portuguese (Brazil)");
|
||||
"eu-ES" = @("EUQ", "BASQUE", "DEFAULT", "Basque (Basque)");
|
||||
"tr" = @("TRK", "TURKISH", "NEUTRAL", "Turkish");
|
||||
"he" = @("HEB", "HEBREW", "NEUTRAL", "Hebrew");
|
||||
"ar" = @("ARA", "ARABIC", "NEUTRAL", "Arabic");
|
||||
"ja" = @("JPN", "JAPANESE", "NEUTRAL", "Japanese");
|
||||
"ko" = @("KOR", "KOREAN", "NEUTRAL", "Korean");
|
||||
"sv" = @("SVE", "SWEDISH", "NEUTRAL", "Swedish");
|
||||
"pt-PT" = @("PTG", "PORTUGUESE", "PORTUGUESE", "Portuguese (Portugal)");
|
||||
"zh-Hant" = @("CHT", "CHINESE", "CHINESE_TRADITIONAL", "Chinese (Traditional)")
|
||||
"zh-TW" = @("CHT", "CHINESE", "CHINESE_TRADITIONAL", "Chinese (Traditional)")
|
||||
}
|
||||
|
||||
# Store the content to be written to a buffer
|
||||
$headerFileContent = ""
|
||||
$rcFileContent = ""
|
||||
|
||||
# Iterate over all resx files in parent directory
|
||||
Get-ChildItem $parentDirectory -Recurse -Filter *.resx |
|
||||
Foreach-Object {
|
||||
Write-Host "Processing $($_.FullName)"
|
||||
$xmlDocument = $null
|
||||
try {
|
||||
$xmlDocument = [xml](Get-Content $_.FullName -ErrorAction:Stop)
|
||||
}
|
||||
catch {
|
||||
Write-Host "Failed to load $($_.FullName)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Get language code from file name
|
||||
$lang = "en"
|
||||
$tokens = $_.Name -split "\."
|
||||
if ($tokens.Count -eq 3) {
|
||||
$lang = $tokens[1]
|
||||
} else {
|
||||
$d = $_.Directory.Name
|
||||
If ($d.Contains('-')) { # Looks like a language directory
|
||||
$lang = $d
|
||||
}
|
||||
}
|
||||
|
||||
$langData = $languageHashTable[$lang]
|
||||
if ($null -eq $langData -and $lang.Contains('-')) {
|
||||
# Modern Localization comes in with language + country tuples;
|
||||
# we want to detect the language alone if we don't support the language-country
|
||||
# version.
|
||||
$lang = ($lang -split "-")[0]
|
||||
$langData = $languageHashTable[$lang]
|
||||
}
|
||||
if ($null -eq $langData) {
|
||||
Write-Warning "Unknown language $lang"
|
||||
Return
|
||||
}
|
||||
|
||||
$newLinesForRCFile = ""
|
||||
$newLinesForHeaderFile = ""
|
||||
$count = $initResourceID
|
||||
|
||||
try {
|
||||
foreach ($entry in $xmlDocument.root.data) {
|
||||
$culture = [System.Globalization.CultureInfo]::GetCultureInfo('en-US')
|
||||
# Each resource is named as IDS_ResxResourceName, in uppercase. Escape occurrences of double quotes in the string
|
||||
$lineInRCFormat = "IDS_" + $entry.name.ToUpper($culture) + " L`"" + $entry.value.Replace("`"", "`"`"") + "`""
|
||||
$newLinesForRCFile = $newLinesForRCFile + "`r`n " + $lineInRCFormat
|
||||
|
||||
# Resource header file needs to be updated only for one language
|
||||
if (!$headerFileUpdated) {
|
||||
$lineInHeaderFormat = "#define IDS_" + $entry.name.ToUpper($culture) + " " + $count.ToString()
|
||||
$newLinesForHeaderFile = $newLinesForHeaderFile + "`r`n" + $lineInHeaderFormat
|
||||
$count++
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
echo "Failed to read XML document."
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($newLinesForRCFile -ne "") {
|
||||
# Add string table syntax
|
||||
$newLinesForRCFile = "`r`nSTRINGTABLE`r`nBEGIN" + $newLinesForRCFile + "`r`nEND"
|
||||
|
||||
$langStart = "`r`n/////////////////////////////////////////////////////////////////////////////`r`n// " + $langData[3] + " resources`r`n`r`n"
|
||||
$langStart += "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_" + $langData[0] + ")`r`nLANGUAGE LANG_" + $langData[1] + ", SUBLANG_" + $langData[2] + "`r`n"
|
||||
|
||||
$langEnd = "`r`n`r`n#endif // " + $langData[3] + " resources`r`n/////////////////////////////////////////////////////////////////////////////`r`n"
|
||||
|
||||
$newLinesForRCFile = $langStart + $newLinesForRCFile + $langEnd
|
||||
}
|
||||
|
||||
# Initialize the rc file with an auto-generation warning and content from the base rc
|
||||
if (!$rcFileUpdated) {
|
||||
$rcFileContent = "// This file was auto-generated. Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.`r`n"
|
||||
try {
|
||||
$rcFileContent += (Get-Content $parentDirectory\$baseRCFileName -Raw)
|
||||
}
|
||||
catch {
|
||||
echo "Failed to read base rc file."
|
||||
exit 0
|
||||
}
|
||||
$rcFileUpdated = $true
|
||||
}
|
||||
|
||||
# Add in the new string table to the rc file
|
||||
$rcFileContent += $newLinesForRCFile
|
||||
|
||||
# Resource header file needs to be set only once, with an auto-generation warning, content from the base resource header followed by #define for all the resources
|
||||
if (!$headerFileUpdated) {
|
||||
$headerFileContent = "// This file was auto-generated. Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.`r`n"
|
||||
try {
|
||||
$headerFileContent += (Get-Content $parentDirectory\$baseHeaderFileName -Raw)
|
||||
}
|
||||
catch {
|
||||
echo "Failed to read base header file."
|
||||
exit 0
|
||||
}
|
||||
$headerFileContent += $newLinesForHeaderFile
|
||||
$headerFileUpdated = $true
|
||||
}
|
||||
}
|
||||
|
||||
# Write to header file if the content has changed or if the file doesnt exist
|
||||
try {
|
||||
if (!(Test-Path -Path $generatedFilesFolder\$generatedHeaderFileName) -or (($headerFileContent + "`r`n") -ne (Get-Content $generatedFilesFolder\$generatedHeaderFileName -Raw))) {
|
||||
Set-Content -Path $generatedFilesFolder\$generatedHeaderFileName -Value $headerFileContent
|
||||
}
|
||||
else {
|
||||
# echo "Skipping write to generated header file"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
echo "Failed to access generated header file."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Write to rc file if the content has changed or if the file doesnt exist
|
||||
try {
|
||||
if (!(Test-Path -Path $generatedFilesFolder\$generatedRCFileName) -or (($rcFileContent + "`r`n") -ne (Get-Content $generatedFilesFolder\$generatedRCFileName -Raw))) {
|
||||
Set-Content -Path $generatedFilesFolder\$generatedRCFileName -Value $rcFileContent -Encoding unicode
|
||||
}
|
||||
else {
|
||||
# echo "Skipping write to generated rc file"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
echo "Failed to access generated rc file."
|
||||
exit 0
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# This script is used to move the resources from a string table txt file to a resx file
|
||||
|
||||
# File containing only the rows of the string table
|
||||
$stringTableFile = $args[0]
|
||||
|
||||
# Output resx file
|
||||
$resxFile = $args[1]
|
||||
|
||||
# Temporary text file used by resgen
|
||||
$tempFile = "temporaryResourceFile.txt"
|
||||
|
||||
$newLinesForTempFile = ""
|
||||
foreach ($line in Get-Content $stringTableFile) {
|
||||
# Each line of string table text file is of the form IDS_ResName L"ResourceValue" where there can be any number of spaces between the two.
|
||||
$content = $line.Trim() -split "\s+", 2
|
||||
|
||||
# Each line of the resgen text input needs to be of the form ResourceName=ResourceValue with no spaces.
|
||||
# For the resource name for the resx file, we remove the IDS_ prefix and convert the words to title case. This can be imperfect since the parts between underscores may also comprise of multiple words, so that will have to be manually tweaked
|
||||
# For the resource value we only keep the content inside L""
|
||||
$lineInTempFileFormat = (Get-Culture).TextInfo.ToTitleCase($content[0].Substring(4).Replace("_", " ").ToLower()).Replace(" ", "_") + "=" + $content[1].Substring(2, $content[1].Length - 3)
|
||||
$newLinesForTempFile = $newLinesForTempFile + "`r`n" + $lineInTempFileFormat
|
||||
}
|
||||
|
||||
# Save the text to a file
|
||||
Set-Content -Path $tempFile -Value $newLinesForTempFile
|
||||
|
||||
# Use resgen to parse the txt to resx. More details at https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#converting-between-resource-file-types
|
||||
resgen $tempFile $resxFile
|
||||
|
||||
# Delete temporary text file used by resgen
|
||||
Remove-Item $tempFile
|
||||
@@ -0,0 +1,123 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BuildPlatform,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BuildConfiguration,
|
||||
|
||||
[Parameter()]
|
||||
[string]$RepoRoot = (Get-Location).Path,
|
||||
|
||||
[switch]$ForceRebuildExecutable
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Resolve-PlatformDirectory {
|
||||
param(
|
||||
[string]$Root,
|
||||
[string]$Platform
|
||||
)
|
||||
|
||||
$normalized = $Platform.Trim()
|
||||
$candidates = @()
|
||||
$candidates += Join-Path $Root $normalized
|
||||
$candidates += Join-Path $Root ($normalized.ToUpperInvariant())
|
||||
$candidates += Join-Path $Root ($normalized.ToLowerInvariant())
|
||||
$candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates[0]
|
||||
}
|
||||
|
||||
Write-Host "Repo root: $RepoRoot"
|
||||
Write-Host "Requested build platform: $BuildPlatform"
|
||||
Write-Host "Requested configuration: $BuildConfiguration"
|
||||
|
||||
# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
|
||||
$exePlatform = 'x64'
|
||||
$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
|
||||
$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
|
||||
$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
|
||||
|
||||
Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
|
||||
|
||||
if ($ForceRebuildExecutable -or -not (Test-Path $exePath)) {
|
||||
Write-Host "PowerToys.DSC.exe not found at '$exePath'. Building x64 binary..."
|
||||
|
||||
$msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue
|
||||
if ($null -eq $msbuild) {
|
||||
throw "msbuild.exe was not found on the PATH."
|
||||
}
|
||||
|
||||
$projectPath = Join-Path $RepoRoot 'src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj'
|
||||
$msbuildArgs = @(
|
||||
$projectPath,
|
||||
'/t:Build',
|
||||
'/m',
|
||||
"/p:Configuration=$BuildConfiguration",
|
||||
"/p:Platform=x64",
|
||||
'/restore'
|
||||
)
|
||||
|
||||
& $msbuild.Path @msbuildArgs
|
||||
$msbuildExitCode = $LASTEXITCODE
|
||||
|
||||
if ($msbuildExitCode -ne 0) {
|
||||
throw "msbuild build failed with exit code $msbuildExitCode"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $exePath)) {
|
||||
throw "Expected PowerToys.DSC.exe at '$exePath' after build but it was not found."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Using existing PowerToys.DSC.exe at '$exePath'."
|
||||
}
|
||||
|
||||
# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
|
||||
$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
|
||||
if (-not (Test-Path $outputRoot)) {
|
||||
Write-Host "Creating missing platform output root at '$outputRoot'."
|
||||
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$outputDir = Join-Path $outputRoot $BuildConfiguration
|
||||
if (-not (Test-Path $outputDir)) {
|
||||
Write-Host "Creating missing configuration output directory at '$outputDir'."
|
||||
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# DSC v3 manifests go to DSCModules subfolder
|
||||
$dscOutputDir = Join-Path $outputDir 'DSCModules'
|
||||
if (-not (Test-Path $dscOutputDir)) {
|
||||
Write-Host "Creating DSCModules subfolder at '$dscOutputDir'."
|
||||
New-Item -Path $dscOutputDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "DSC manifests will be generated to: '$dscOutputDir'"
|
||||
|
||||
Write-Host "Cleaning previously generated DSC manifest files from '$dscOutputDir'."
|
||||
Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
|
||||
|
||||
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $dscOutputDir)
|
||||
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
|
||||
& $exePath @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
|
||||
if ($generatedFiles.Count -eq 0) {
|
||||
throw "No DSC manifest files were generated in '$dscOutputDir'."
|
||||
}
|
||||
|
||||
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
|
||||
foreach ($file in $generatedFiles) {
|
||||
Write-Host " - $($file.FullName)"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[CmdletBinding()]
|
||||
|
||||
# This script finds all C#/.NET resx files and renames them from
|
||||
# Folder/Language/x.resw to Folder/x.Language.resw, with language
|
||||
# mapping. This is required because Touchdown localization uses a
|
||||
# different directory structure.
|
||||
$Items = Get-ChildItem . -Recurse -Filter *.resx
|
||||
|
||||
# Each of the projects we care about stores its resources
|
||||
# in a Properties directory. We **DO NOT** want to move
|
||||
# resource files from other projects (since we use resx files
|
||||
# in standard Win32 projects as well.)
|
||||
$Items = $Items | Where-Object {
|
||||
$_.Directory.Parent.Name -Eq "Properties"
|
||||
}
|
||||
|
||||
If ($Items.Count -Le 0) {
|
||||
# Nothing to do.
|
||||
Write-Verbose "Nothing to do."
|
||||
Exit 0
|
||||
}
|
||||
|
||||
ForEach($Item in $Items) {
|
||||
$PropertiesRoot = $Item.Directory.Parent
|
||||
$Language = $Item.Directory.Name
|
||||
$Destination = Join-Path $PropertiesRoot.FullName ("{0}.{1}{2}" -F ($Item.BaseName, $Language, $Item.Extension))
|
||||
Write-Verbose "Renaming $($Item.FullName) to $Destination"
|
||||
Move-Item $Item.FullName $Destination
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[CmdletBinding()]
|
||||
|
||||
# This script finds all UWP resw files that are emitted by Touchdown
|
||||
# with a bad path (en-us/lang-lang) and moves them to the correct
|
||||
# location.
|
||||
$Items = Get-ChildItem . -Recurse -Filter *.resw |
|
||||
Where-Object FullName -Like "*\en-US\*\*.resw"
|
||||
|
||||
If ($Items.Count -Le 0) {
|
||||
# Nothing to do.
|
||||
Write-Verbose "Nothing to do."
|
||||
Exit 0
|
||||
}
|
||||
|
||||
$Items | ForEach-Object {
|
||||
# Move each resw file's parent folder into its parent's parent's folder.
|
||||
Move-Item -Verbose $_.Directory.FullName $_.Directory.Parent.Parent.FullName -EA:Ignore
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
#https://learn.microsoft.com/en-us/windows/msix/package/signing-known-issues
|
||||
# 1. Build the powertoys as usual.
|
||||
# 2. Call this script to sign the msix package.
|
||||
# First time run needs admin permission to trust the certificate.
|
||||
|
||||
param (
|
||||
[string]$architecture = "x64", # Default to x64 if not provided
|
||||
[string]$buildConfiguration = "Debug" # Default to Debug if not provided
|
||||
)
|
||||
|
||||
$signToolPath = $null
|
||||
$kitsRootPaths = @(
|
||||
"C:\Program Files (x86)\Windows Kits\10\bin",
|
||||
"C:\Program Files\Windows Kits\10\bin"
|
||||
)
|
||||
|
||||
$signToolAvailable = Get-Command "signtool" -ErrorAction SilentlyContinue
|
||||
if ($signToolAvailable) {
|
||||
Write-Host "SignTool is available in the system PATH."
|
||||
$signToolPath = "signtool"
|
||||
}
|
||||
else {
|
||||
Write-Host "Searching for latest SignTool matching architecture: $architecture"
|
||||
|
||||
foreach ($root in $kitsRootPaths) {
|
||||
if (Test-Path $root) {
|
||||
$versions = Get-ChildItem -Path $root -Directory | Where-Object {
|
||||
$_.Name -match '^\d+\.\d+\.\d+\.\d+$'
|
||||
} | Sort-Object Name -Descending
|
||||
|
||||
foreach ($version in $versions) {
|
||||
$candidatePath = Join-Path -Path $version.FullName -ChildPath "x86"
|
||||
$exePath = Join-Path -Path $candidatePath -ChildPath "signtool.exe"
|
||||
if (Test-Path $exePath) {
|
||||
Write-Host "Found SignTool at: $exePath"
|
||||
$signToolPath = $exePath
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($signToolPath) { break }
|
||||
}
|
||||
}
|
||||
|
||||
if (!$signToolPath) {
|
||||
Write-Host "SignTool not found. Please ensure Windows SDK is installed."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nUsing SignTool: $signToolPath"
|
||||
|
||||
# Set the certificate subject and the ECDSA curve
|
||||
$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
|
||||
# Check if the certificate already exists in the current user's certificate store
|
||||
$existingCert = Get-ChildItem -Path Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Subject -eq $certSubject } |
|
||||
Sort-Object NotAfter -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($existingCert) {
|
||||
# If the certificate exists, use the existing certificate
|
||||
Write-Host "Certificate already exists, using the existing certificate"
|
||||
$cert = $existingCert
|
||||
}
|
||||
else {
|
||||
# If the certificate doesn't exist, create a new self-signed certificate
|
||||
Write-Host "Certificate does not exist, creating a new certificate..."
|
||||
$cert = New-SelfSignedCertificate -Subject $certSubject `
|
||||
-CertStoreLocation "Cert:\CurrentUser\My" `
|
||||
-KeyAlgorithm RSA `
|
||||
-Type CodeSigningCert `
|
||||
-HashAlgorithm SHA256
|
||||
}
|
||||
|
||||
function Import-And-VerifyCertificate {
|
||||
param (
|
||||
[string]$cerPath,
|
||||
[string]$storePath
|
||||
)
|
||||
|
||||
$thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint
|
||||
|
||||
# ✅ Step 1: Check if already exists in store
|
||||
$existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
|
||||
if ($existingCert) {
|
||||
Write-Host "✅ Certificate already exists in $storePath"
|
||||
return $true
|
||||
}
|
||||
|
||||
# 🚀 Step 2: Try to import if not already there
|
||||
try {
|
||||
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Warning "❌ Failed to import certificate to $storePath : $_"
|
||||
return $false
|
||||
}
|
||||
|
||||
# 🔁 Step 3: Verify again
|
||||
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
|
||||
if ($imported) {
|
||||
Write-Host "✅ Certificate successfully imported to $storePath"
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
Write-Warning "❌ Certificate not found in $storePath after import"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$cerPath = "$env:TEMP\temp_cert.cer"
|
||||
Export-Certificate -Cert $cert -FilePath $cerPath -Force
|
||||
# used for sign code/msix
|
||||
# CurrentUser\TrustedPeople
|
||||
if (-not (Import-And-VerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# CurrentUser\Root
|
||||
if (-not (Import-And-VerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# LocalMachine\Root
|
||||
if (-not (Import-And-VerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) {
|
||||
Write-Warning "⚠️ Failed to import to LocalMachine\Root (admin may be required)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
# Output the thumbprint of the certificate (to confirm which certificate is being used)
|
||||
Write-Host "Using certificate with thumbprint: $($cert.Thumbprint)"
|
||||
|
||||
|
||||
$rootDirectory = (Split-Path -Parent(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)))
|
||||
|
||||
# Dynamically build the directory path based on architecture and build configuration
|
||||
# $directoryPath = Join-Path $rootDirectory "$architecture\$buildConfiguration\WinUI3Apps\CmdPal\"
|
||||
$directoryPath = Join-Path $rootDirectory "$architecture\$buildConfiguration\WinUI3Apps\CmdPal\"
|
||||
|
||||
if (-not (Test-Path $directoryPath)) {
|
||||
Write-Error "Path to search for msix files does not exist: $directoryPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Directory path to search for .msix and .appx files: $directoryPath"
|
||||
|
||||
# Get all .msix and .appx files from the specified directory
|
||||
$filePaths = Get-ChildItem -Path $directoryPath -Recurse | Where-Object {
|
||||
($_.Extension -eq ".msix" -or $_.Extension -eq ".appx") -and
|
||||
($_.Name -like "*$architecture*")
|
||||
}
|
||||
|
||||
if ($filePaths.Count -eq 0) {
|
||||
Write-Host "No .msix or .appx files found in the directory."
|
||||
}
|
||||
else {
|
||||
# Iterate through each file and sign it
|
||||
foreach ($file in $filePaths) {
|
||||
Write-Host "Signing file: $($file.FullName)"
|
||||
|
||||
# Use SignTool to sign the file
|
||||
$signToolCommand = "& `"$signToolPath`" sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com `"$($file.FullName)`""
|
||||
|
||||
# Execute the sign command
|
||||
try {
|
||||
Invoke-Expression $signToolCommand
|
||||
}
|
||||
catch {
|
||||
Write-Host "Error signing file $($file.Name): $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Signing process completed."
|
||||
@@ -0,0 +1,318 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up the development environment for building PowerToys.
|
||||
|
||||
.DESCRIPTION
|
||||
This script automates the setup of prerequisites needed to build PowerToys locally:
|
||||
- Enables Windows long path support (requires elevation)
|
||||
- Enables Windows Developer Mode (requires elevation)
|
||||
- Installs required Visual Studio workloads from .vsconfig
|
||||
- Initializes git submodules
|
||||
|
||||
Run this script once after cloning the repository to prepare your development environment.
|
||||
|
||||
.PARAMETER SkipLongPaths
|
||||
Skip enabling long path support in Windows.
|
||||
|
||||
.PARAMETER SkipDevMode
|
||||
Skip enabling Windows Developer Mode.
|
||||
|
||||
.PARAMETER SkipVSComponents
|
||||
Skip installing Visual Studio components from .vsconfig.
|
||||
|
||||
.PARAMETER SkipSubmodules
|
||||
Skip initializing git submodules.
|
||||
|
||||
.PARAMETER VSInstallPath
|
||||
Path to Visual Studio installation. Default: auto-detected.
|
||||
|
||||
.PARAMETER Help
|
||||
Show this help message.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\setup-dev-environment.ps1
|
||||
Runs the full setup process.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\setup-dev-environment.ps1 -SkipVSComponents
|
||||
Runs setup but skips Visual Studio component installation.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build\setup-dev-environment.ps1 -VSInstallPath "C:\Program Files\Microsoft Visual Studio\18\Enterprise"
|
||||
Runs setup with a custom Visual Studio installation path.
|
||||
|
||||
.NOTES
|
||||
- Some operations require administrator privileges (long paths, VS component installation).
|
||||
- If not running as administrator, the script will prompt for elevation for those steps.
|
||||
- The script is idempotent and safe to run multiple times.
|
||||
#>
|
||||
|
||||
param (
|
||||
[switch]$SkipLongPaths,
|
||||
[switch]$SkipDevMode,
|
||||
[switch]$SkipVSComponents,
|
||||
[switch]$SkipSubmodules,
|
||||
[string]$VSInstallPath = '',
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Get-Help $MyInvocation.MyCommand.Path -Detailed
|
||||
exit 0
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Find repository root
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$repoRoot = $scriptDir
|
||||
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
|
||||
$parent = Split-Path -Parent $repoRoot
|
||||
if ($parent -eq $repoRoot) {
|
||||
Write-Error "Could not find PowerToys repository root. Ensure this script is in the PowerToys repository."
|
||||
exit 1
|
||||
}
|
||||
$repoRoot = $parent
|
||||
}
|
||||
|
||||
Write-Host "Repository: $repoRoot"
|
||||
Write-Host ""
|
||||
|
||||
function Test-Administrator {
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
$isAdmin = Test-Administrator
|
||||
|
||||
# Step 1: Enable Long Paths
|
||||
if (-not $SkipLongPaths) {
|
||||
Write-Host "[1/4] Checking Windows long path support"
|
||||
|
||||
$longPathsEnabled = $false
|
||||
try {
|
||||
$regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -ErrorAction SilentlyContinue
|
||||
$longPathsEnabled = ($regValue.LongPathsEnabled -eq 1)
|
||||
}
|
||||
catch {
|
||||
$longPathsEnabled = $false
|
||||
}
|
||||
|
||||
if ($longPathsEnabled) {
|
||||
Write-Host " Long paths already enabled" -ForegroundColor Green
|
||||
}
|
||||
elseif ($isAdmin) {
|
||||
Write-Host " Enabling long paths..."
|
||||
try {
|
||||
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWord
|
||||
Write-Host " Long paths enabled" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Warning " Failed to enable long paths: $_"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning " Long paths not enabled. Run as Administrator to enable, or run manually:"
|
||||
Write-Host " Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[1/4] Skipping long path check" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Step 2: Enable Developer Mode
|
||||
if (-not $SkipDevMode) {
|
||||
Write-Host "[2/4] Checking Windows Developer Mode"
|
||||
|
||||
$devModeEnabled = $false
|
||||
try {
|
||||
$regValue = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -Name "AllowDevelopmentWithoutDevLicense" -ErrorAction SilentlyContinue
|
||||
$devModeEnabled = ($regValue.AllowDevelopmentWithoutDevLicense -eq 1)
|
||||
}
|
||||
catch {
|
||||
$devModeEnabled = $false
|
||||
}
|
||||
|
||||
if ($devModeEnabled) {
|
||||
Write-Host " Developer Mode already enabled" -ForegroundColor Green
|
||||
}
|
||||
elseif ($isAdmin) {
|
||||
Write-Host " Enabling Developer Mode..."
|
||||
try {
|
||||
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
if (-not (Test-Path $regPath)) {
|
||||
New-Item -Path $regPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $regPath -Name "AllowDevelopmentWithoutDevLicense" -Value 1 -Type DWord
|
||||
Write-Host " Developer Mode enabled" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Warning " Failed to enable Developer Mode: $_"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning " Developer Mode not enabled. Run as Administrator to enable, or enable manually:"
|
||||
Write-Host " Settings > System > For developers > Developer Mode" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[2/4] Skipping Developer Mode check" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Step 3: Install Visual Studio Components
|
||||
if (-not $SkipVSComponents) {
|
||||
Write-Host "[3/4] Checking Visual Studio components"
|
||||
|
||||
$vsConfigPath = Join-Path $repoRoot ".vsconfig"
|
||||
if (-not (Test-Path $vsConfigPath)) {
|
||||
Write-Warning " .vsconfig not found at $vsConfigPath"
|
||||
}
|
||||
else {
|
||||
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
|
||||
if (-not $VSInstallPath -and (Test-Path $vsWhere)) {
|
||||
$VSInstallPath = & $vsWhere -latest -prerelease -property installationPath 2>$null
|
||||
}
|
||||
|
||||
if (-not $VSInstallPath) {
|
||||
$commonPaths = @(
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\18\Enterprise",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\18\Professional",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\18\Community",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional",
|
||||
"${env:ProgramFiles}\Microsoft Visual Studio\2022\Community"
|
||||
)
|
||||
foreach ($path in $commonPaths) {
|
||||
if (Test-Path $path) {
|
||||
$VSInstallPath = $path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $VSInstallPath -or -not (Test-Path $VSInstallPath)) {
|
||||
Write-Warning " Could not find Visual Studio installation"
|
||||
Write-Warning " Please install Visual Studio 2026 (or 2022 17.4+) and try again, or import .vsconfig manually"
|
||||
}
|
||||
else {
|
||||
Write-Host " Found: $VSInstallPath" -ForegroundColor DarkGray
|
||||
|
||||
$vsInstaller = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe"
|
||||
|
||||
if (Test-Path $vsInstaller) {
|
||||
Write-Host ""
|
||||
Write-Host " To install required components:"
|
||||
Write-Host ""
|
||||
Write-Host " Option A - Visual Studio Installer GUI:"
|
||||
Write-Host " 1. Open Visual Studio Installer"
|
||||
Write-Host " 2. Click 'More' > 'Import configuration'"
|
||||
Write-Host " 3. Select: $vsConfigPath"
|
||||
Write-Host ""
|
||||
Write-Host " Option B - Command line (close VS first):"
|
||||
Write-Host " & `"$vsInstaller`" modify --installPath `"$VSInstallPath`" --config `"$vsConfigPath`"" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
$choices = @(
|
||||
[System.Management.Automation.Host.ChoiceDescription]::new("&Install", "Run VS Installer now"),
|
||||
[System.Management.Automation.Host.ChoiceDescription]::new("&Skip", "Continue without installing")
|
||||
)
|
||||
|
||||
try {
|
||||
$decision = $Host.UI.PromptForChoice("", "Install VS components now?", $choices, 1)
|
||||
|
||||
if ($decision -eq 0) {
|
||||
# Check if VS Installer is already running (it runs as setup.exe from the Installer folder)
|
||||
$vsInstallerDir = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer"
|
||||
$vsInstallerRunning = Get-Process -Name "setup" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Path -and $_.Path.StartsWith($vsInstallerDir, [System.StringComparison]::OrdinalIgnoreCase) }
|
||||
if ($vsInstallerRunning) {
|
||||
Write-Warning " Visual Studio Installer is already running"
|
||||
Write-Host " Close it and run this script again, or import .vsconfig manually" -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
Write-Host " Launching Visual Studio Installer..."
|
||||
Write-Host " Close Visual Studio if it's running." -ForegroundColor DarkGray
|
||||
$process = Start-Process -FilePath $vsInstaller -ArgumentList "modify", "--installPath", "`"$VSInstallPath`"", "--config", "`"$vsConfigPath`"" -Wait -PassThru
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Host " VS component installation completed" -ForegroundColor Green
|
||||
}
|
||||
elseif ($process.ExitCode -eq 3010) {
|
||||
Write-Host " VS component installation completed (restart may be required)" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Warning " VS Installer exited with code $($process.ExitCode)"
|
||||
Write-Host " You may need to run the installer manually" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host " Skipped VS component installation"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " Non-interactive mode. Run the command above manually if needed." -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning " Visual Studio Installer not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[3/4] Skipping VS component check" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Step 4: Initialize Git Submodules
|
||||
if (-not $SkipSubmodules) {
|
||||
Write-Host "[4/4] Initializing git submodules"
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
$submoduleStatus = git submodule status 2>&1
|
||||
$uninitializedCount = ($submoduleStatus | Where-Object { $_ -match '^\-' }).Count
|
||||
|
||||
if ($uninitializedCount -eq 0 -and $submoduleStatus) {
|
||||
Write-Host " Submodules already initialized" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host " Running: git submodule update --init --recursive" -ForegroundColor DarkGray
|
||||
git submodule update --init --recursive
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " Submodules initialized" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Warning " Submodule initialization may have encountered issues (exit code: $LASTEXITCODE)"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning " Failed to initialize submodules: $_"
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[4/4] Skipping submodule initialization" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Setup complete" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host " 1. Open PowerToys.slnx in Visual Studio"
|
||||
Write-Host " 2. If prompted to install additional components, click Install"
|
||||
Write-Host " 3. Build the solution (Ctrl+Shift+B)"
|
||||
Write-Host ""
|
||||
Write-Host "Or build from command line:"
|
||||
Write-Host " .\tools\build\build.ps1" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user