From 2e00bcd038f5a7c41f246351818cad1937d9de1d Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 10 Nov 2025 17:17:58 -0800 Subject: [PATCH 01/13] create project --- src/AppInstallerCLI.sln | 27 + src/ComInprocTestbed/ComInprocTestbed.vcxproj | 139 ++++ .../ComInprocTestbed.vcxproj.filters | 22 + src/ComInprocTestbed/main.cpp | 666 ++++++++++++++++++ 4 files changed, 854 insertions(+) create mode 100644 src/ComInprocTestbed/ComInprocTestbed.vcxproj create mode 100644 src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters create mode 100644 src/ComInprocTestbed/main.cpp diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index d9632733c8..06705c9132 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -224,6 +224,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F49C PowerShell\scripts\Initialize-LocalWinGetModules.ps1 = PowerShell\scripts\Initialize-LocalWinGetModules.ps1 EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ComInprocTestbed", "ComInprocTestbed\ComInprocTestbed.vcxproj", "{E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1020,6 +1022,30 @@ Global {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x64.Build.0 = Release|x64 {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.ActiveCfg = Release|x86 {33745E4A-39E2-676F-7E23-50FB43848D25}.ReleaseStatic|x86.Build.0 = Release|x86 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|ARM64.Build.0 = Debug|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x64.ActiveCfg = Debug|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x64.Build.0 = Debug|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x86.ActiveCfg = Debug|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Debug|x86.Build.0 = Debug|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|ARM64.Build.0 = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x64.ActiveCfg = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x64.Build.0 = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x86.ActiveCfg = Release|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Fuzzing|x86.Build.0 = Release|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|ARM64.ActiveCfg = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|ARM64.Build.0 = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x64.ActiveCfg = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x64.Build.0 = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x86.ActiveCfg = Release|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.Release|x86.Build.0 = Release|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|ARM64.ActiveCfg = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|ARM64.Build.0 = Release|ARM64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x64.ActiveCfg = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x64.Build.0 = Release|x64 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x86.ActiveCfg = Release|Win32 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94}.ReleaseStatic|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1056,6 +1082,7 @@ Global {A33223D2-550B-4D99-A53D-488B1F68683E} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {7139ED6E-8FBC-0B61-3E3A-AA2A23CC4D6A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {F49C4C89-447E-4D15-B38B-5A8DCFB134AF} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj b/src/ComInprocTestbed/ComInprocTestbed.vcxproj new file mode 100644 index 0000000000..e5c0409ec4 --- /dev/null +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj @@ -0,0 +1,139 @@ + + + + 15.0 + {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94} + Win32Proj + ComInprocTestbed + 10.0.26100.0 + 10.0.17763.0 + true + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + Application + v140 + v141 + v142 + v143 + Unicode + + + true + $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + + + true + $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + + + true + $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ + + + false + $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ + + + false + $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + + + false + $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + + + + + + + + + + + + + + NotUsing + pch.h + $(IntDir)pch.pch + _CONSOLE;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /permissive- /bigobj /Zi + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + true + true + stdcpp17 + stdcpp17 + MultiThreadedDebugDLL + + + Console + false + + + + + WIN32;%(PreprocessorDefinitions) + true + stdcpp17 + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + true + true + true + stdcpp17 + stdcpp17 + stdcpp17 + + + Console + true + true + false + + + + + + + + + \ No newline at end of file diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters new file mode 100644 index 0000000000..128a386645 --- /dev/null +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp new file mode 100644 index 0000000000..f0e86644cd --- /dev/null +++ b/src/ComInprocTestbed/main.cpp @@ -0,0 +1,666 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::filesystem; + +std::wstring_view RegistrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller"; +std::wstring_view DefaultDisplayVersion = L"1.0.0.0"; +std::wstring_view DscSubDirectoryName = L"SubDirectory"; + +void WriteModifyRepairScript(std::wofstream& script, const path& repairCompletedTextFilePath, bool isModifyScript) { + std::wstring scriptName = isModifyScript ? L"Modify" : L"Uninstaller"; + script << L" if /I \"%%A\"==\"/repair\" (\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" + << L" EXIT /B 0\n" + << L" ) else if /I \"%%A\"==\"/r\" (\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" + << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" + << L" EXIT /B 0\n" + << L" )"; +} + +void WriteModifyUninstallScript(std::wofstream& script) { + script << L" else if /I \"%%A\"==\"/uninstall\" (\n" + << L" call UninstallTestExe.bat\n" + << L" EXIT /B 0\n" + << L" ) else if /I \"%%A\"==\"/X\" (\n" + << L" call UninstallTestExe.bat\n" + << L" EXIT /B 0\n" + << L" )\n"; +} + +void WriteModifyInvalidOperationScript(std::wofstream& script) { + script << L"echo Invalid operation\n" + << L"EXIT /B 1\n"; +} + +void WriteUninstallerScript( + std::wofstream& uninstallerScript, + const path& uninstallerOutputTextFilePath, + const std::wstring& registryKey, + std::initializer_list paths) { + uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; + uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; + uninstallerScript << "REG DELETE " << registryKey << " /f\n"; + + for (const auto& path : paths) + { + std::wstring pathString = path.wstring(); + uninstallerScript << "if exist \"" << pathString << "\" del \"" << pathString << "\"\n"; + } +} + +path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID, bool useHKLM) +{ + path uninstallerPath = installDirectory; + uninstallerPath /= "UninstallTestExe.bat"; + + out << "Uninstaller located at path: " << uninstallerPath << std::endl; + + path uninstallerOutputTextFilePath = installDirectory; + uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; + + path repairCompletedTextFilePath = installDirectory; + repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; + + std::wstring registryKey{ useHKLM ? L"HKEY_LOCAL_MACHINE\\" : L"HKEY_CURRENT_USER\\" }; + registryKey += RegistrySubkey; + if (!productID.empty()) + { + registryKey += productID; + } + else + { + registryKey += DefaultProductID; + } + + std::wofstream uninstallerScript(uninstallerPath); + uninstallerScript << "@echo off\n"; + uninstallerScript << L"for %%A in (%*) do (\n"; + WriteModifyRepairScript(uninstallerScript, repairCompletedTextFilePath, false /*isModifyScript*/); + uninstallerScript << ")\n"; + WriteUninstallerScript(uninstallerScript, uninstallerOutputTextFilePath, registryKey, + { + installDirectory / "ModifyTestExe.bat", + repairCompletedTextFilePath, + installDirectory / "AppInstallerTestResource.exe", + installDirectory / "AppInstallerTest.dsc.resource.json", + installDirectory / DscSubDirectoryName / "AppInstallerTestResource.exe", + installDirectory / DscSubDirectoryName / "AppInstallerTest.dsc.resource.json", + }); + + uninstallerScript.close(); + + return uninstallerPath; +} + +path GenerateModifyPath(const path& installDirectory) +{ + path modifyScriptPath = installDirectory; + modifyScriptPath /= "ModifyTestExe.bat"; + + path repairCompletedTextFilePath = installDirectory; + repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; + + std::wofstream modifyScript(modifyScriptPath); + + modifyScript << L"@echo off\n"; + modifyScript << L"for %%A in (%*) do (\n"; + WriteModifyRepairScript(modifyScript, repairCompletedTextFilePath, true /*isModifyScript*/); + WriteModifyUninstallScript(modifyScript); + modifyScript << L")\n"; + WriteModifyInvalidOperationScript(modifyScript); + + modifyScript.close(); + + return modifyScriptPath; +} + +void GenerateDSCv3ProviderFiles(const path& installDirectory, const std::wstring_view subDirectory) +{ + path dscResourceExecutablePath = installDirectory; + if (!subDirectory.empty()) + { + dscResourceExecutablePath /= subDirectory; + std::filesystem::create_directories(dscResourceExecutablePath); + } + dscResourceExecutablePath /= "AppInstallerTestResource.exe"; + + WCHAR currentExecutable[MAX_PATH]; + GetModuleFileName(nullptr, currentExecutable, MAX_PATH); + path currentExecutablePath{ currentExecutable }; + copy_file(currentExecutablePath, dscResourceExecutablePath); + + path dscResourceManifestPath = installDirectory; + if (!subDirectory.empty()) + { + dscResourceManifestPath /= subDirectory; + } + dscResourceManifestPath /= "AppInstallerTest.dsc.resource.json"; + + std::wstring DscResourceJsonContent = + LR"( + { + "$schema" : "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "description" : "AppInstallerTest dsc Resource.", + "export" : + { + "args" : + [ + "/DscExport" + ], + "executable" : "AppInstallerTestResource.exe" + }, + "get" : + { + "args" : + [ + "/DscGet" + ], + "executable" : "AppInstallerTestResource.exe", + "input" : "stdin" + }, + "set" : + { + "args" : + [ + "/DscSet" + ] , + "executable" : "AppInstallerTestResource.exe", + "handlesExist" : true, + "implementsPretest" : true, + "input" : "stdin", + "return" : "state" + }, + "test" : + { + "args" : + [ + "/DscTest" + ] , + "executable" : "AppInstallerTestResource.exe", + "input" : "stdin", + "return" : "state" + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppInstallerTestResource", + "description": "App Installer Test Resource", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "_inDesiredState": { + "description": "Indicates whether an instance is in the desired state.", + "type": "boolean" + }, + "data": { + "type": "string", + "description": "Test data." + } + } + } + }, + "type" : "AppInstallerTest/TestResource)"; + + if (!subDirectory.empty()) + { + DscResourceJsonContent += '.'; + DscResourceJsonContent += subDirectory; + } + + DscResourceJsonContent += LR"(", + "version" : "1.0.0" + } + )"; + + + std::wofstream dscResourceJson(dscResourceManifestPath); + dscResourceJson << DscResourceJsonContent; + dscResourceJson.close(); +} + +void WriteToUninstallRegistry( + std::wostream& out, + const std::wstring& productID, + const path& uninstallerPath, + const path& modifyPath, + const std::wstring& displayName, + const std::wstring& displayVersion, + const std::wstring& installLocation, + bool useHKLM, + bool noRepair, + bool noModify) +{ + HKEY hkey; + LONG lReg; + + // String inputs to registry must be of wide char type + const wchar_t* publisher = L"Microsoft Corporation"; + std::wstring uninstallString = uninstallerPath.wstring(); + std::wstring modifyPathString = modifyPath.wstring(); + + DWORD version = 1; + + std::wstring registryKey{ RegistrySubkey }; + + if (!productID.empty()) + { + registryKey += productID; + out << "Product Code overridden to: " << registryKey << std::endl; + } + else + { + registryKey += DefaultProductID; + out << "Default Product Code used: " << registryKey << std::endl; + } + + lReg = RegCreateKeyEx( + useHKLM ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, + registryKey.c_str(), + 0, + NULL, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, + NULL, + &hkey, + NULL); + + if (lReg == ERROR_SUCCESS) + { + out << "Successfully opened registry key" << std::endl; + + // Set Display Name Property Value + if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName.c_str(), (DWORD)(displayName.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write DisplayName value. Error Code: " << res << std::endl; + } + + // Set Display Version Property Value + if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion.c_str(), (DWORD)(displayVersion.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write DisplayVersion value. Error Code: " << res << std::endl; + } + + // Set Publisher Property Value + if (LONG res = RegSetValueEx(hkey, L"Publisher", NULL, REG_SZ, (LPBYTE)publisher, (DWORD)(wcslen(publisher) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write Publisher value. Error Code: " << res << std::endl; + } + + // Set UninstallString Property Value + if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString.c_str(), (DWORD)(uninstallString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write UninstallString value. Error Code: " << res << std::endl; + } + + // Set Version Property Value + if (LONG res = RegSetValueEx(hkey, L"Version", NULL, REG_DWORD, (LPBYTE)&version, sizeof(version)) != ERROR_SUCCESS) + { + out << "Failed to write Version value. Error Code: " << res << std::endl; + } + + // Set InstallLocation Property Value + if (LONG res = RegSetValueEx(hkey, L"InstallLocation", NULL, REG_SZ, (LPBYTE)installLocation.c_str(), (DWORD)(installLocation.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write InstallLocation value. Error Code: " << res << std::endl; + } + + // Set ModifyPath Property Value + if (LONG res = RegSetValueEx(hkey, L"ModifyPath", NULL, REG_EXPAND_SZ, (LPBYTE)modifyPathString.c_str(), (DWORD)(modifyPathString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write ModifyPath value. Error Code: " << res << std::endl; + } + + if(noRepair) + { + // Set NoRepair Property Value + DWORD noRepairValue = 1; + if (LONG res = RegSetValueEx(hkey, L"NoRepair", NULL, REG_DWORD, (LPBYTE)&noRepairValue, sizeof(noRepairValue)) != ERROR_SUCCESS) + { + out << "Failed to write NoRepair value. Error Code: " << res << std::endl; + } + } + + if(noModify) + { + // Set NoModify Property Value + DWORD noModifyValue = 1; + if (LONG res = RegSetValueEx(hkey, L"NoModify", NULL, REG_DWORD, (LPBYTE)&noModifyValue, sizeof(noModifyValue)) != ERROR_SUCCESS) + { + out << "Failed to write NoModify value. Error Code: " << res << std::endl; + } + } + + out << "Write to registry key completed" << std::endl; + } + else { + out << "Key Creation Failed" << std::endl; + } + + RegCloseKey(hkey); +} + +void WriteToFile(const path& filePath, const std::wstringstream& content) +{ + std::wofstream file(filePath, std::ofstream::out); + file << content.str(); + file.close(); +} + +void HandleRepairOperation(const std::wstring& productID, const std::wstringstream& outContent, bool useHKLM) +{ + path installDirectory; + + // Open the registry key + HKEY hKey; + std::wstring registryPath = std::wstring(RegistrySubkey); + + if (!productID.empty()) + { + registryPath += productID; + } + else + { + registryPath += DefaultProductID; + } + + LONG lReg = RegOpenKeyEx(useHKLM ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, registryPath.c_str(), 0, KEY_READ, &hKey); + + if (lReg == ERROR_SUCCESS) + { + // Query the value of the InstallLocation + wchar_t regInstallLocation[MAX_PATH]; + DWORD bufferSize = sizeof(regInstallLocation); + lReg = RegQueryValueEx(hKey, L"InstallLocation", NULL, NULL, (LPBYTE)regInstallLocation, &bufferSize); + + if (lReg == ERROR_SUCCESS) + { + // Convert the InstallLocation to a path + installDirectory = std::wstring(regInstallLocation); + } + + // Close the registry key + RegCloseKey(hKey); + + if(installDirectory.empty()) + { + // We could not find the install location, so we cannot repair + return; + } + } + else + { + // We could not find the uninstall APR registry key, so we cannot repair + return; + } + + path outFilePath = installDirectory; + outFilePath /= "TestExeRepairCompleted.txt"; + WriteToFile(outFilePath, outContent); +} + +void HandleInstallationOperation( + std::wostream& out, + const path& installDirectory, + const std::wstringstream& outContent, + const std::wstring& productCode, + bool useHKLM, + const std::wstring& displayName, + const std::wstring& displayVersion, + bool noRepair, + bool noModify, + bool generateDscResourceFiles) +{ + path outFilePath = installDirectory; + outFilePath /= "TestExeInstalled.txt"; + + std::wofstream file(outFilePath, std::ofstream::out); + file << outContent.str(); + file.close(); + + if (generateDscResourceFiles) + { + GenerateDSCv3ProviderFiles(installDirectory, {}); + GenerateDSCv3ProviderFiles(installDirectory, DscSubDirectoryName); + } + + path uninstallerPath = GenerateUninstaller(out, installDirectory, productCode, useHKLM); + path modifyPath = GenerateModifyPath(installDirectory); + + WriteToUninstallRegistry(out, productCode, uninstallerPath, modifyPath, displayName, displayVersion, installDirectory.wstring(), useHKLM, noRepair, noModify); +} + +// The installer prints all args to an output file and writes to the Uninstall registry key +int wmain(int argc, const wchar_t** argv) +{ + path installDirectory = temp_directory_path(); + std::wstringstream outContent; + std::wstring productCode; + std::wstring displayName; + std::wstring displayVersion; + std::wstring aliasToExecute; + std::wstring aliasArguments; + bool useHKLM = false; + bool noOperation = false; + int exitCode = 0; + bool isRepair = false; + bool noRepair = false; + bool noModify = false; + bool generateDscResourceFiles = false; + + // Output to cout by default, but swap to a file if requested + std::wostream* out = &std::wcout; + std::wofstream logFile; + + for (int i = 1; i < argc; i++) + { + outContent << argv[i] << ' '; + + // Supports custom install path. + if (_wcsicmp(argv[i], L"/InstallDir") == 0) + { + if (++i < argc) + { + installDirectory = argv[i]; + std::filesystem::create_directories(installDirectory); + outContent << argv[i] << ' '; + } + } + + // Supports custom exit code + else if (_wcsicmp(argv[i], L"/ExitCode") == 0) + { + if (++i < argc) + { + exitCode = static_cast(std::stoll(argv[i], 0, 0)); + outContent << argv[i] << ' '; + } + } + + // Supports custom product code ID + else if (_wcsicmp(argv[i], L"/ProductID") == 0) + { + if (++i < argc) + { + productCode = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Supports custom DisplayName + else if (_wcsicmp(argv[i], L"/DisplayName") == 0) + { + if (++i < argc) + { + displayName = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Supports custom version + else if (_wcsicmp(argv[i], L"/Version") == 0) + { + if (++i < argc) + { + displayVersion = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Supports log file + else if (_wcsicmp(argv[i], L"/LogFile") == 0) + { + if (++i < argc) + { + logFile = std::wofstream(argv[i], std::wofstream::out | std::wofstream::trunc); + out = &logFile; + outContent << argv[i] << ' '; + } + } + + // Writes to HKLM + else if (_wcsicmp(argv[i], L"/UseHKLM") == 0) + { + useHKLM = true; + } + + // Executes a command alias during installation + else if (_wcsicmp(argv[i], L"/AliasToExecute") == 0) + { + if (++i < argc) + { + aliasToExecute = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Additional arguments to include when executing the command alias during installation + else if (_wcsicmp(argv[i], L"/AliasArguments") == 0) + { + if (++i < argc) + { + aliasArguments = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Supports /repair and /r to emulate repair operation using installer. + else if (_wcsicmp(argv[i], L"/repair") == 0 + || _wcsicmp(argv[i], L"/r") == 0) + { + isRepair = true; + } + + else if (_wcsicmp(argv[i], L"/NoRepair") == 0) + { + noRepair = true; + } + + else if (_wcsicmp(argv[i], L"/NoModify") == 0) + { + noModify = true; + } + + // Returns the success exit code to emulate being invoked by another caller. + else if (_wcsicmp(argv[i], L"/NoOperation") == 0) + { + noOperation = true; + } + + // Also output dsc resource files + else if (_wcsicmp(argv[i], L"/GenerateDscResourceFiles") == 0) + { + generateDscResourceFiles = true; + } + + // Dsc resource get + else if (_wcsicmp(argv[i], L"/DscGet") == 0) + { + std::cout << R"({"data":"TestData"})" << std::endl; + return 0; + } + + // Dsc resource set + else if (_wcsicmp(argv[i], L"/DscSet") == 0) + { + std::cout << R"({"_inDesiredState":true})" << std::endl; + return 0; + } + + // Dsc resource test + else if (_wcsicmp(argv[i], L"/DscTest") == 0) + { + std::cout << R"({"_inDesiredState":true})" << std::endl; + return 0; + } + + // Dsc resource export + else if (_wcsicmp(argv[i], L"/DscExport") == 0) + { + std::cout << R"({"data":"TestData"})" << std::endl; + return 0; + } + } + + if (noOperation) + { + return exitCode; + } + + if (!aliasToExecute.empty()) + { + SHELLEXECUTEINFOW execInfo = { 0 }; + execInfo.cbSize = sizeof(execInfo); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.lpFile = aliasToExecute.c_str(); + + if (!aliasArguments.empty()) + { + execInfo.lpParameters = aliasArguments.c_str(); + } + execInfo.nShow = SW_SHOW; + + if (!ShellExecuteExW(&execInfo) || !execInfo.hProcess) + { + return -1; + } + } + + if (displayName.empty()) + { + displayName = DefaultDisplayName; + } + + if (displayVersion.empty()) + { + displayVersion = DefaultDisplayVersion; + } + + path outFilePath = installDirectory; + + if (isRepair) + { + outContent << L"\nInstaller Repair operation for AppInstallerTestExeInstaller.exe completed successfully."; + HandleRepairOperation(productCode, outContent, useHKLM); + } + else + { + HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion, noRepair, noModify, generateDscResourceFiles); + } + + return exitCode; +} From cb16ad7b7e3e3578a68e7eba12dbf5c7281db4b0 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 18 Nov 2025 15:28:52 -0800 Subject: [PATCH 02/13] Get project set up, needs COM activation by CLSID? --- .../ComInprocTestbed.manifest | 36 + src/ComInprocTestbed/ComInprocTestbed.vcxproj | 41 ++ .../ComInprocTestbed.vcxproj.filters | 9 + src/ComInprocTestbed/main.cpp | 661 +----------------- src/ComInprocTestbed/packages.config | 4 + 5 files changed, 96 insertions(+), 655 deletions(-) create mode 100644 src/ComInprocTestbed/ComInprocTestbed.manifest create mode 100644 src/ComInprocTestbed/packages.config diff --git a/src/ComInprocTestbed/ComInprocTestbed.manifest b/src/ComInprocTestbed/ComInprocTestbed.manifest new file mode 100644 index 0000000000..fbc69addfe --- /dev/null +++ b/src/ComInprocTestbed/ComInprocTestbed.manifest @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj b/src/ComInprocTestbed/ComInprocTestbed.vcxproj index e5c0409ec4..ebd89eec62 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj @@ -1,5 +1,6 @@ + 15.0 {E5BCFF58-7D0C-4770-ABB9-AECE1027CD94} @@ -133,7 +134,47 @@ + + + + + + {9ac3c6a4-1875-4d3e-bf9c-c31e81eff6b4} + false + false + true + Content + PreserveNewest + + + {1cc41a9a-ae66-459d-9210-1e572dd7be69} + + + {2046b5af-666d-4ce8-8d3e-c32c57908a56} + false + false + true + Content + PreserveNewest + + + + + true + + + + + + + + + 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}. + + + + \ No newline at end of file diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters index 128a386645..cf29dba865 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters @@ -19,4 +19,13 @@ Source Files + + + + + + + + + \ No newline at end of file diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index f0e86644cd..1bb89a4d0b 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -2,665 +2,16 @@ // Licensed under the MIT License. #include -#include -#include -#include -#include -#include -#include -#include +#include -using namespace std::filesystem; +using namespace winrt::Microsoft::Management::Deployment; -std::wstring_view RegistrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; -std::wstring_view DefaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; -std::wstring_view DefaultDisplayName = L"AppInstallerTestExeInstaller"; -std::wstring_view DefaultDisplayVersion = L"1.0.0.0"; -std::wstring_view DscSubDirectoryName = L"SubDirectory"; - -void WriteModifyRepairScript(std::wofstream& script, const path& repairCompletedTextFilePath, bool isModifyScript) { - std::wstring scriptName = isModifyScript ? L"Modify" : L"Uninstaller"; - script << L" if /I \"%%A\"==\"/repair\" (\n" - << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" - << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" - << L" EXIT /B 0\n" - << L" ) else if /I \"%%A\"==\"/r\" (\n" - << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully > \"" << repairCompletedTextFilePath.wstring() << "\"\n" - << L" ECHO " << scriptName << L" Repair operation for AppInstallerTestExeInstaller.exe completed successfully\n" - << L" EXIT /B 0\n" - << L" )"; -} - -void WriteModifyUninstallScript(std::wofstream& script) { - script << L" else if /I \"%%A\"==\"/uninstall\" (\n" - << L" call UninstallTestExe.bat\n" - << L" EXIT /B 0\n" - << L" ) else if /I \"%%A\"==\"/X\" (\n" - << L" call UninstallTestExe.bat\n" - << L" EXIT /B 0\n" - << L" )\n"; -} - -void WriteModifyInvalidOperationScript(std::wofstream& script) { - script << L"echo Invalid operation\n" - << L"EXIT /B 1\n"; -} - -void WriteUninstallerScript( - std::wofstream& uninstallerScript, - const path& uninstallerOutputTextFilePath, - const std::wstring& registryKey, - std::initializer_list paths) { - uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; - uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; - uninstallerScript << "REG DELETE " << registryKey << " /f\n"; - - for (const auto& path : paths) - { - std::wstring pathString = path.wstring(); - uninstallerScript << "if exist \"" << pathString << "\" del \"" << pathString << "\"\n"; - } -} - -path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID, bool useHKLM) -{ - path uninstallerPath = installDirectory; - uninstallerPath /= "UninstallTestExe.bat"; - - out << "Uninstaller located at path: " << uninstallerPath << std::endl; - - path uninstallerOutputTextFilePath = installDirectory; - uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; - - path repairCompletedTextFilePath = installDirectory; - repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; - - std::wstring registryKey{ useHKLM ? L"HKEY_LOCAL_MACHINE\\" : L"HKEY_CURRENT_USER\\" }; - registryKey += RegistrySubkey; - if (!productID.empty()) - { - registryKey += productID; - } - else - { - registryKey += DefaultProductID; - } - - std::wofstream uninstallerScript(uninstallerPath); - uninstallerScript << "@echo off\n"; - uninstallerScript << L"for %%A in (%*) do (\n"; - WriteModifyRepairScript(uninstallerScript, repairCompletedTextFilePath, false /*isModifyScript*/); - uninstallerScript << ")\n"; - WriteUninstallerScript(uninstallerScript, uninstallerOutputTextFilePath, registryKey, - { - installDirectory / "ModifyTestExe.bat", - repairCompletedTextFilePath, - installDirectory / "AppInstallerTestResource.exe", - installDirectory / "AppInstallerTest.dsc.resource.json", - installDirectory / DscSubDirectoryName / "AppInstallerTestResource.exe", - installDirectory / DscSubDirectoryName / "AppInstallerTest.dsc.resource.json", - }); - - uninstallerScript.close(); - - return uninstallerPath; -} - -path GenerateModifyPath(const path& installDirectory) -{ - path modifyScriptPath = installDirectory; - modifyScriptPath /= "ModifyTestExe.bat"; - - path repairCompletedTextFilePath = installDirectory; - repairCompletedTextFilePath /= "TestExeRepairCompleted.txt"; - - std::wofstream modifyScript(modifyScriptPath); - - modifyScript << L"@echo off\n"; - modifyScript << L"for %%A in (%*) do (\n"; - WriteModifyRepairScript(modifyScript, repairCompletedTextFilePath, true /*isModifyScript*/); - WriteModifyUninstallScript(modifyScript); - modifyScript << L")\n"; - WriteModifyInvalidOperationScript(modifyScript); - - modifyScript.close(); - - return modifyScriptPath; -} - -void GenerateDSCv3ProviderFiles(const path& installDirectory, const std::wstring_view subDirectory) -{ - path dscResourceExecutablePath = installDirectory; - if (!subDirectory.empty()) - { - dscResourceExecutablePath /= subDirectory; - std::filesystem::create_directories(dscResourceExecutablePath); - } - dscResourceExecutablePath /= "AppInstallerTestResource.exe"; - - WCHAR currentExecutable[MAX_PATH]; - GetModuleFileName(nullptr, currentExecutable, MAX_PATH); - path currentExecutablePath{ currentExecutable }; - copy_file(currentExecutablePath, dscResourceExecutablePath); - - path dscResourceManifestPath = installDirectory; - if (!subDirectory.empty()) - { - dscResourceManifestPath /= subDirectory; - } - dscResourceManifestPath /= "AppInstallerTest.dsc.resource.json"; - - std::wstring DscResourceJsonContent = - LR"( - { - "$schema" : "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", - "description" : "AppInstallerTest dsc Resource.", - "export" : - { - "args" : - [ - "/DscExport" - ], - "executable" : "AppInstallerTestResource.exe" - }, - "get" : - { - "args" : - [ - "/DscGet" - ], - "executable" : "AppInstallerTestResource.exe", - "input" : "stdin" - }, - "set" : - { - "args" : - [ - "/DscSet" - ] , - "executable" : "AppInstallerTestResource.exe", - "handlesExist" : true, - "implementsPretest" : true, - "input" : "stdin", - "return" : "state" - }, - "test" : - { - "args" : - [ - "/DscTest" - ] , - "executable" : "AppInstallerTestResource.exe", - "input" : "stdin", - "return" : "state" - }, - "schema": { - "embedded": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AppInstallerTestResource", - "description": "App Installer Test Resource", - "type": "object", - "required": [], - "additionalProperties": false, - "properties": { - "_inDesiredState": { - "description": "Indicates whether an instance is in the desired state.", - "type": "boolean" - }, - "data": { - "type": "string", - "description": "Test data." - } - } - } - }, - "type" : "AppInstallerTest/TestResource)"; - - if (!subDirectory.empty()) - { - DscResourceJsonContent += '.'; - DscResourceJsonContent += subDirectory; - } - - DscResourceJsonContent += LR"(", - "version" : "1.0.0" - } - )"; - - - std::wofstream dscResourceJson(dscResourceManifestPath); - dscResourceJson << DscResourceJsonContent; - dscResourceJson.close(); -} - -void WriteToUninstallRegistry( - std::wostream& out, - const std::wstring& productID, - const path& uninstallerPath, - const path& modifyPath, - const std::wstring& displayName, - const std::wstring& displayVersion, - const std::wstring& installLocation, - bool useHKLM, - bool noRepair, - bool noModify) -{ - HKEY hkey; - LONG lReg; - - // String inputs to registry must be of wide char type - const wchar_t* publisher = L"Microsoft Corporation"; - std::wstring uninstallString = uninstallerPath.wstring(); - std::wstring modifyPathString = modifyPath.wstring(); - - DWORD version = 1; - - std::wstring registryKey{ RegistrySubkey }; - - if (!productID.empty()) - { - registryKey += productID; - out << "Product Code overridden to: " << registryKey << std::endl; - } - else - { - registryKey += DefaultProductID; - out << "Default Product Code used: " << registryKey << std::endl; - } - - lReg = RegCreateKeyEx( - useHKLM ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, - registryKey.c_str(), - 0, - NULL, - REG_OPTION_NON_VOLATILE, - KEY_ALL_ACCESS, - NULL, - &hkey, - NULL); - - if (lReg == ERROR_SUCCESS) - { - out << "Successfully opened registry key" << std::endl; - - // Set Display Name Property Value - if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName.c_str(), (DWORD)(displayName.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write DisplayName value. Error Code: " << res << std::endl; - } - - // Set Display Version Property Value - if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion.c_str(), (DWORD)(displayVersion.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write DisplayVersion value. Error Code: " << res << std::endl; - } - - // Set Publisher Property Value - if (LONG res = RegSetValueEx(hkey, L"Publisher", NULL, REG_SZ, (LPBYTE)publisher, (DWORD)(wcslen(publisher) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write Publisher value. Error Code: " << res << std::endl; - } - - // Set UninstallString Property Value - if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString.c_str(), (DWORD)(uninstallString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write UninstallString value. Error Code: " << res << std::endl; - } - - // Set Version Property Value - if (LONG res = RegSetValueEx(hkey, L"Version", NULL, REG_DWORD, (LPBYTE)&version, sizeof(version)) != ERROR_SUCCESS) - { - out << "Failed to write Version value. Error Code: " << res << std::endl; - } - - // Set InstallLocation Property Value - if (LONG res = RegSetValueEx(hkey, L"InstallLocation", NULL, REG_SZ, (LPBYTE)installLocation.c_str(), (DWORD)(installLocation.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write InstallLocation value. Error Code: " << res << std::endl; - } - - // Set ModifyPath Property Value - if (LONG res = RegSetValueEx(hkey, L"ModifyPath", NULL, REG_EXPAND_SZ, (LPBYTE)modifyPathString.c_str(), (DWORD)(modifyPathString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) - { - out << "Failed to write ModifyPath value. Error Code: " << res << std::endl; - } - - if(noRepair) - { - // Set NoRepair Property Value - DWORD noRepairValue = 1; - if (LONG res = RegSetValueEx(hkey, L"NoRepair", NULL, REG_DWORD, (LPBYTE)&noRepairValue, sizeof(noRepairValue)) != ERROR_SUCCESS) - { - out << "Failed to write NoRepair value. Error Code: " << res << std::endl; - } - } - - if(noModify) - { - // Set NoModify Property Value - DWORD noModifyValue = 1; - if (LONG res = RegSetValueEx(hkey, L"NoModify", NULL, REG_DWORD, (LPBYTE)&noModifyValue, sizeof(noModifyValue)) != ERROR_SUCCESS) - { - out << "Failed to write NoModify value. Error Code: " << res << std::endl; - } - } - - out << "Write to registry key completed" << std::endl; - } - else { - out << "Key Creation Failed" << std::endl; - } - - RegCloseKey(hkey); -} - -void WriteToFile(const path& filePath, const std::wstringstream& content) -{ - std::wofstream file(filePath, std::ofstream::out); - file << content.str(); - file.close(); -} - -void HandleRepairOperation(const std::wstring& productID, const std::wstringstream& outContent, bool useHKLM) -{ - path installDirectory; - - // Open the registry key - HKEY hKey; - std::wstring registryPath = std::wstring(RegistrySubkey); - - if (!productID.empty()) - { - registryPath += productID; - } - else - { - registryPath += DefaultProductID; - } - - LONG lReg = RegOpenKeyEx(useHKLM ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, registryPath.c_str(), 0, KEY_READ, &hKey); - - if (lReg == ERROR_SUCCESS) - { - // Query the value of the InstallLocation - wchar_t regInstallLocation[MAX_PATH]; - DWORD bufferSize = sizeof(regInstallLocation); - lReg = RegQueryValueEx(hKey, L"InstallLocation", NULL, NULL, (LPBYTE)regInstallLocation, &bufferSize); - - if (lReg == ERROR_SUCCESS) - { - // Convert the InstallLocation to a path - installDirectory = std::wstring(regInstallLocation); - } - - // Close the registry key - RegCloseKey(hKey); - - if(installDirectory.empty()) - { - // We could not find the install location, so we cannot repair - return; - } - } - else - { - // We could not find the uninstall APR registry key, so we cannot repair - return; - } - - path outFilePath = installDirectory; - outFilePath /= "TestExeRepairCompleted.txt"; - WriteToFile(outFilePath, outContent); -} - -void HandleInstallationOperation( - std::wostream& out, - const path& installDirectory, - const std::wstringstream& outContent, - const std::wstring& productCode, - bool useHKLM, - const std::wstring& displayName, - const std::wstring& displayVersion, - bool noRepair, - bool noModify, - bool generateDscResourceFiles) -{ - path outFilePath = installDirectory; - outFilePath /= "TestExeInstalled.txt"; - - std::wofstream file(outFilePath, std::ofstream::out); - file << outContent.str(); - file.close(); - - if (generateDscResourceFiles) - { - GenerateDSCv3ProviderFiles(installDirectory, {}); - GenerateDSCv3ProviderFiles(installDirectory, DscSubDirectoryName); - } - - path uninstallerPath = GenerateUninstaller(out, installDirectory, productCode, useHKLM); - path modifyPath = GenerateModifyPath(installDirectory); - - WriteToUninstallRegistry(out, productCode, uninstallerPath, modifyPath, displayName, displayVersion, installDirectory.wstring(), useHKLM, noRepair, noModify); -} - -// The installer prints all args to an output file and writes to the Uninstall registry key int wmain(int argc, const wchar_t** argv) { - path installDirectory = temp_directory_path(); - std::wstringstream outContent; - std::wstring productCode; - std::wstring displayName; - std::wstring displayVersion; - std::wstring aliasToExecute; - std::wstring aliasArguments; - bool useHKLM = false; - bool noOperation = false; - int exitCode = 0; - bool isRepair = false; - bool noRepair = false; - bool noModify = false; - bool generateDscResourceFiles = false; - - // Output to cout by default, but swap to a file if requested - std::wostream* out = &std::wcout; - std::wofstream logFile; - - for (int i = 1; i < argc; i++) - { - outContent << argv[i] << ' '; - - // Supports custom install path. - if (_wcsicmp(argv[i], L"/InstallDir") == 0) - { - if (++i < argc) - { - installDirectory = argv[i]; - std::filesystem::create_directories(installDirectory); - outContent << argv[i] << ' '; - } - } - - // Supports custom exit code - else if (_wcsicmp(argv[i], L"/ExitCode") == 0) - { - if (++i < argc) - { - exitCode = static_cast(std::stoll(argv[i], 0, 0)); - outContent << argv[i] << ' '; - } - } - - // Supports custom product code ID - else if (_wcsicmp(argv[i], L"/ProductID") == 0) - { - if (++i < argc) - { - productCode = argv[i]; - outContent << argv[i] << ' '; - } - } - - // Supports custom DisplayName - else if (_wcsicmp(argv[i], L"/DisplayName") == 0) - { - if (++i < argc) - { - displayName = argv[i]; - outContent << argv[i] << ' '; - } - } - - // Supports custom version - else if (_wcsicmp(argv[i], L"/Version") == 0) - { - if (++i < argc) - { - displayVersion = argv[i]; - outContent << argv[i] << ' '; - } - } - - // Supports log file - else if (_wcsicmp(argv[i], L"/LogFile") == 0) - { - if (++i < argc) - { - logFile = std::wofstream(argv[i], std::wofstream::out | std::wofstream::trunc); - out = &logFile; - outContent << argv[i] << ' '; - } - } - - // Writes to HKLM - else if (_wcsicmp(argv[i], L"/UseHKLM") == 0) - { - useHKLM = true; - } - - // Executes a command alias during installation - else if (_wcsicmp(argv[i], L"/AliasToExecute") == 0) - { - if (++i < argc) - { - aliasToExecute = argv[i]; - outContent << argv[i] << ' '; - } - } - - // Additional arguments to include when executing the command alias during installation - else if (_wcsicmp(argv[i], L"/AliasArguments") == 0) - { - if (++i < argc) - { - aliasArguments = argv[i]; - outContent << argv[i] << ' '; - } - } - - // Supports /repair and /r to emulate repair operation using installer. - else if (_wcsicmp(argv[i], L"/repair") == 0 - || _wcsicmp(argv[i], L"/r") == 0) - { - isRepair = true; - } - - else if (_wcsicmp(argv[i], L"/NoRepair") == 0) - { - noRepair = true; - } - - else if (_wcsicmp(argv[i], L"/NoModify") == 0) - { - noModify = true; - } - - // Returns the success exit code to emulate being invoked by another caller. - else if (_wcsicmp(argv[i], L"/NoOperation") == 0) - { - noOperation = true; - } - - // Also output dsc resource files - else if (_wcsicmp(argv[i], L"/GenerateDscResourceFiles") == 0) - { - generateDscResourceFiles = true; - } - - // Dsc resource get - else if (_wcsicmp(argv[i], L"/DscGet") == 0) - { - std::cout << R"({"data":"TestData"})" << std::endl; - return 0; - } - - // Dsc resource set - else if (_wcsicmp(argv[i], L"/DscSet") == 0) - { - std::cout << R"({"_inDesiredState":true})" << std::endl; - return 0; - } - - // Dsc resource test - else if (_wcsicmp(argv[i], L"/DscTest") == 0) - { - std::cout << R"({"_inDesiredState":true})" << std::endl; - return 0; - } - - // Dsc resource export - else if (_wcsicmp(argv[i], L"/DscExport") == 0) - { - std::cout << R"({"data":"TestData"})" << std::endl; - return 0; - } - } - - if (noOperation) - { - return exitCode; - } - - if (!aliasToExecute.empty()) - { - SHELLEXECUTEINFOW execInfo = { 0 }; - execInfo.cbSize = sizeof(execInfo); - execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; - execInfo.lpFile = aliasToExecute.c_str(); - - if (!aliasArguments.empty()) - { - execInfo.lpParameters = aliasArguments.c_str(); - } - execInfo.nShow = SW_SHOW; - - if (!ShellExecuteExW(&execInfo) || !execInfo.hProcess) - { - return -1; - } - } - - if (displayName.empty()) - { - displayName = DefaultDisplayName; - } - - if (displayVersion.empty()) - { - displayVersion = DefaultDisplayVersion; - } - - path outFilePath = installDirectory; + UNREFERENCED_PARAMETER(argc); + UNREFERENCED_PARAMETER(argv); - if (isRepair) - { - outContent << L"\nInstaller Repair operation for AppInstallerTestExeInstaller.exe completed successfully."; - HandleRepairOperation(productCode, outContent, useHKLM); - } - else - { - HandleInstallationOperation(*out, installDirectory, outContent, productCode, useHKLM, displayName, displayVersion, noRepair, noModify, generateDscResourceFiles); - } + PackageManager packageManager; - return exitCode; + return 0; } diff --git a/src/ComInprocTestbed/packages.config b/src/ComInprocTestbed/packages.config new file mode 100644 index 0000000000..f229371b33 --- /dev/null +++ b/src/ComInprocTestbed/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 92c88d09f5cff3634290dbf557a1ca1165ba1284 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 19 Nov 2025 17:41:15 -0800 Subject: [PATCH 03/13] Basics set up --- .../Public/ShutdownMonitoring.h | 12 +- .../ShutdownMonitoring.cpp | 36 +++--- src/ComInprocTestbed/ComInprocTestbed.vcxproj | 45 ++++++- .../ComInprocTestbed.vcxproj.filters | 20 +++ src/ComInprocTestbed/PackageManager.cpp | 79 ++++++++++++ src/ComInprocTestbed/PackageManager.h | 7 ++ src/ComInprocTestbed/Tests.cpp | 8 ++ src/ComInprocTestbed/Tests.h | 6 + src/ComInprocTestbed/main.cpp | 115 ++++++++++++++++-- src/ComInprocTestbed/pch.cpp | 3 + src/ComInprocTestbed/pch.h | 15 +++ ....Management.Deployment.InProc.dll.manifest | 48 ++++++++ 12 files changed, 364 insertions(+), 30 deletions(-) create mode 100644 src/ComInprocTestbed/PackageManager.cpp create mode 100644 src/ComInprocTestbed/PackageManager.h create mode 100644 src/ComInprocTestbed/Tests.cpp create mode 100644 src/ComInprocTestbed/Tests.h create mode 100644 src/ComInprocTestbed/pch.cpp create mode 100644 src/ComInprocTestbed/pch.h diff --git a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h index 77599c3021..dfaca8fa82 100644 --- a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h +++ b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h @@ -66,7 +66,7 @@ namespace AppInstaller::ShutdownMonitoring }; // Coordinates shutdown across server components - struct ServerShutdownSynchronization : public ICancellable + struct ServerShutdownSynchronization { using ShutdownCompleteCallback = void (*)(); @@ -93,18 +93,20 @@ namespace AppInstaller::ShutdownMonitoring // Waits for the shutdown to complete. static void WaitForShutdown(); - // Listens for a termination signal. - void Cancel(CancelReason reason, bool force) override; - private: - ServerShutdownSynchronization(); + ServerShutdownSynchronization() = default; ~ServerShutdownSynchronization(); + friend TerminationSignalHandler; + static ServerShutdownSynchronization& Instance(); // Runs the actual shutdown process and invokes the callback. void SynchronizeShutdown(CancelReason reason); + // Listens for a termination signal. + void Signal(CancelReason reason); + ShutdownCompleteCallback m_callback = nullptr; std::mutex m_componentsLock; std::vector m_components; diff --git a/src/AppInstallerCLICore/ShutdownMonitoring.cpp b/src/AppInstallerCLICore/ShutdownMonitoring.cpp index c7122bf66f..bcdb3769f7 100644 --- a/src/AppInstallerCLICore/ShutdownMonitoring.cpp +++ b/src/AppInstallerCLICore/ShutdownMonitoring.cpp @@ -183,19 +183,22 @@ namespace AppInstaller::ShutdownMonitoring // Returns FALSE if no contexts attached; TRUE otherwise. BOOL TerminationSignalHandler::InformListeners(CancelReason reason, bool force) { - std::lock_guard lock{ m_listenersLock }; + BOOL result = FALSE; - if (m_listeners.empty()) { - return FALSE; - } + std::lock_guard lock{ m_listenersLock }; + result = m_listeners.empty() ? FALSE : TRUE; - for (auto& listener : m_listeners) - { - listener->Cancel(reason, force); + for (auto& listener : m_listeners) + { + listener->Cancel(reason, force); + } } - return TRUE; + // Notify shutdown synchronization as well + ServerShutdownSynchronization::Instance().Signal(reason); + + return result; } void TerminationSignalHandler::CreateWindowAndStartMessageLoop() @@ -322,8 +325,17 @@ namespace AppInstaller::ShutdownMonitoring instance.m_shutdownComplete.wait(); } - void ServerShutdownSynchronization::Cancel(CancelReason reason, bool) + void ServerShutdownSynchronization::Signal(CancelReason reason) { + { + // Check for registered components before creating a thread to do nothing + std::lock_guard lock{ m_componentsLock }; + if (m_components.empty()) + { + return; + } + } + std::lock_guard lock{ m_threadLock }; if (!m_shutdownThread.joinable()) @@ -332,14 +344,8 @@ namespace AppInstaller::ShutdownMonitoring } } - ServerShutdownSynchronization::ServerShutdownSynchronization() - { - TerminationSignalHandler::Instance()->AddListener(this); - } - ServerShutdownSynchronization::~ServerShutdownSynchronization() { - TerminationSignalHandler::Instance()->RemoveListener(this); if (m_shutdownThread.joinable()) { m_shutdownThread.detach(); diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj b/src/ComInprocTestbed/ComInprocTestbed.vcxproj index ebd89eec62..6a36e82768 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj @@ -48,26 +48,32 @@ true $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + false true $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + false true $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ + false false $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ + false false $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + false false $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ + false @@ -81,7 +87,7 @@ - NotUsing + Use pch.h $(IntDir)pch.pch _CONSOLE;%(PreprocessorDefinitions) @@ -103,6 +109,12 @@ Console false + + ComInprocTestbed.manifest + + + ComInprocTestbed.manifest + @@ -110,6 +122,9 @@ true stdcpp17 + + ComInprocTestbed.manifest + @@ -130,9 +145,28 @@ true false + + ComInprocTestbed.manifest + + + ComInprocTestbed.manifest + + + ComInprocTestbed.manifest + + + + Create + Create + Create + Create + Create + Create + + @@ -144,7 +178,7 @@ false true Content - PreserveNewest + Always {1cc41a9a-ae66-459d-9210-1e572dd7be69} @@ -155,7 +189,7 @@ false true Content - PreserveNewest + Always @@ -166,6 +200,11 @@ + + + + + diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters index cf29dba865..f7d573db19 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj.filters @@ -18,6 +18,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + @@ -28,4 +37,15 @@ + + + Header Files + + + Header Files + + + Header Files + + \ No newline at end of file diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp new file mode 100644 index 0000000000..c01288b553 --- /dev/null +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +using namespace winrt::Microsoft::Management::Deployment; + +PackageCatalog Connect(const PackageCatalogReference& reference, std::string_view name) +{ + auto connectResult = reference.Connect(); + + if (connectResult.Status() != ConnectResultStatus::Ok) + { + std::cout << "Connecting to " << name << " got: " << static_cast(connectResult.Status()) << " [" << connectResult.ExtendedErrorCode() << "]\n"; + return nullptr; + } + + return connectResult.PackageCatalog(); +} + +bool UsePackageManager(std::string_view packageName) +{ + PackageManager packageManager; + + // Force installed cache to be created + auto installedCatalogRef = packageManager.GetLocalPackageCatalog(LocalPackageCatalog::InstalledPackages); + auto installedCatalog = Connect(installedCatalogRef, "Installed Catalog"); + if (!installedCatalog) + { + return false; + } + + // Force TerminationSignalHandler to be created + CreateCompositePackageCatalogOptions options; + options.CompositeSearchBehavior(CompositeSearchBehavior::RemotePackagesFromRemoteCatalogs); + + for (PackageCatalogReference catalogRef : packageManager.GetPackageCatalogs()) + { + options.Catalogs().Append(catalogRef); + } + + auto compositeCatalog = Connect(packageManager.CreateCompositePackageCatalog(options), "Composite Catalog"); + if (!compositeCatalog) + { + return false; + } + + PackageMatchFilter filter; + filter.Field(PackageMatchField::Id); + filter.Option(PackageFieldMatchOption::EqualsCaseInsensitive); + filter.Value(winrt::to_hstring(packageName)); + + FindPackagesOptions findOptions; + findOptions.Filters().Append(filter); + + auto findResult = compositeCatalog.FindPackages(findOptions); + if (findResult.Status() != FindPackagesResultStatus::Ok) + { + std::cout << "Finding package " << packageName << " got: " << static_cast(findResult.Status()) << " [" << findResult.ExtendedErrorCode() << "]\n"; + return false; + } + + if (findResult.Matches().Size() != 1) + { + std::cout << "Finding package " << packageName << " got " << findResult.Matches().Size() << " results.\n"; + return false; + } + + DownloadOptions downloadOptions; + auto downloadOperation = packageManager.DownloadPackageAsync(findResult.Matches().GetAt(0).CatalogPackage(), downloadOptions); + auto downloadResult = downloadOperation.get(); + + if (downloadResult.Status() != DownloadResultStatus::Ok) + { + std::cout << "Downloading package " << packageName << " got: " << static_cast(downloadResult.Status()) << "\n"; + return false; + } + + return true; +} diff --git a/src/ComInprocTestbed/PackageManager.h b/src/ComInprocTestbed/PackageManager.h new file mode 100644 index 0000000000..8c8d7488d6 --- /dev/null +++ b/src/ComInprocTestbed/PackageManager.h @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +// Attempts to instantiate all static objects +bool UsePackageManager(std::string_view packageName); diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp new file mode 100644 index 0000000000..36fc999feb --- /dev/null +++ b/src/ComInprocTestbed/Tests.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +void UnloadAndCheckForLeaks() +{ + CoFreeUnusedLibrariesEx(0, 0); +} diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h new file mode 100644 index 0000000000..ab8fdf35bd --- /dev/null +++ b/src/ComInprocTestbed/Tests.h @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +// Forces COM to unload the module and checks for leaked resources. +void UnloadAndCheckForLeaks(); diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index 1bb89a4d0b..396097c045 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -1,17 +1,118 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#include "pch.h" +#include "PackageManager.h" +#include "Tests.h" -#include -#include +using namespace std::string_view_literals; -using namespace winrt::Microsoft::Management::Deployment; +#define ADVANCE_ARG_PARAMETER if (++i >= argc) { return E_INVALIDARG; } -int wmain(int argc, const wchar_t** argv) +std::string ToLower(std::string_view in) { - UNREFERENCED_PARAMETER(argc); - UNREFERENCED_PARAMETER(argv); + std::string result(in); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return result; +} + +int main(int argc, const char** argv) try +{ + std::string testToRun; + std::string comInit = "mta"; + bool leakCOM = false; + int iterations = 1; + std::string packageName = "Microsoft.Edit"; + // bool preventUnload = false; TODO: Add ability to prevent unload and allow testing here + + for (int i = 0; i < argc; ++i) + { + if ("-test"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + testToRun = ToLower(argv[i]); + } + else if ("-com"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + comInit = ToLower(argv[i]); + } + else if ("-leak-com"sv == argv[i]) + { + leakCOM = true; + } + else if ("-itr"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + iterations = atoi(argv[i]); + } + else if ("-pkg"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + packageName = argv[i]; + } + } + + std::cout << "Running inproc testbed with:\n" + " COM Init: " << comInit << "\n" + " Leak COM: " << (leakCOM ? "true" : "false") << "\n" + " Test : " << testToRun << "\n" + " Package : " << packageName << "\n" + " Passes : " << iterations << std::endl; + + HRESULT hr = S_OK; + + if ("sta"sv == comInit) + { + hr = RoInitialize(RO_INIT_SINGLETHREADED); + } + else if ("mta"sv == comInit) + { + hr = RoInitialize(RO_INIT_MULTITHREADED); + } + // else no COM init means "let C++/WinRT do it" + + if (FAILED(hr)) + { + std::cout << "RoInitialize returned " << hr << std::endl; + return 1; + } + + for (int i = 0; i < iterations; ++i) + { + if (!UsePackageManager(packageName)) + { + return 2; + } - PackageManager packageManager; + if ("unload_check"sv == testToRun) + { + UnloadAndCheckForLeaks(); + } + std::cout << "Iteration " << (i + 1) << " completed" << std::endl; + } + + if (!leakCOM) + { + RoUninitialize(); + } + + std::cout << "Tests completed" << std::endl; return 0; } +catch (const std::exception& e) +{ + std::cout << "Caught std exception: " << e.what() << std::endl; + return 3; +} +catch (const winrt::hresult_error& hre) +{ + std::cout << "Caught winrt exception: " << winrt::to_string(hre.message()) << std::endl; + return 3; +} +catch (...) +{ + std::cout << "Caught unknown exception" << std::endl; + return 3; +} diff --git a/src/ComInprocTestbed/pch.cpp b/src/ComInprocTestbed/pch.cpp new file mode 100644 index 0000000000..b7f2ce9c04 --- /dev/null +++ b/src/ComInprocTestbed/pch.cpp @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" diff --git a/src/ComInprocTestbed/pch.h b/src/ComInprocTestbed/pch.h new file mode 100644 index 0000000000..19555929fa --- /dev/null +++ b/src/ComInprocTestbed/pch.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define NOMINMAX +#include +#include + +#include +#include + +#include +#include +#include +#include diff --git a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest index 4e9725d652..b0f086ad09 100644 --- a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest +++ b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest @@ -11,49 +11,97 @@ clsid="{2DDE4456-64D9-4673-8F7E-A4F19A2E6CC3}" threadingModel="Both" description="PackageManager"/> + + + + + + + + + + + + From ea65b160cffb4f0f1d8432266f9eb61f25de30d6 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 20 Nov 2025 17:42:49 -0800 Subject: [PATCH 04/13] Mostly done but needs generic resource usage monitoring --- .github/actions/spelling/expect.txt | 3 + src/ComInprocTestbed/PackageManager.cpp | 6 ++ src/ComInprocTestbed/PackageManager.h | 3 + src/ComInprocTestbed/Tests.cpp | 97 ++++++++++++++++++- src/ComInprocTestbed/Tests.h | 11 ++- src/ComInprocTestbed/main.cpp | 53 +++++++--- src/ComInprocTestbed/pch.h | 4 + .../dllmain.cpp | 9 -- .../CanUnload.cpp | 18 ++++ .../Microsoft.Management.Deployment.vcxproj | 5 +- ...soft.Management.Deployment.vcxproj.filters | 8 ++ .../PackageManager.idl | 12 ++- .../PackageManagerSettings.cpp | 11 +++ .../PackageManagerSettings.h | 4 + .../Public/CanUnload.h | 12 +++ src/WindowsPackageManager/main.cpp | 23 +++-- 16 files changed, 244 insertions(+), 35 deletions(-) create mode 100644 src/Microsoft.Management.Deployment/CanUnload.cpp create mode 100644 src/Microsoft.Management.Deployment/Public/CanUnload.h diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0cfbb70317..8ae4fef416 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -345,6 +345,7 @@ MSIXSTRM msstore MSZIP mszyml +mta Mugiwara Multideclaration mysource @@ -472,6 +473,7 @@ rgp rgpsz rhs riid +roapi Roblox ronomon rowid @@ -513,6 +515,7 @@ sid Sideload SIGNATUREHASH silentpreferred +SINGLETHREADED Skipx sku SLAPI diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp index c01288b553..dcbe825946 100644 --- a/src/ComInprocTestbed/PackageManager.cpp +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -77,3 +77,9 @@ bool UsePackageManager(std::string_view packageName) return true; } + +void SetUnloadPreference(bool value) +{ + PackageManagerSettings settings; + settings.CanUnloadPreference(value); +} diff --git a/src/ComInprocTestbed/PackageManager.h b/src/ComInprocTestbed/PackageManager.h index 8c8d7488d6..b1646594fb 100644 --- a/src/ComInprocTestbed/PackageManager.h +++ b/src/ComInprocTestbed/PackageManager.h @@ -5,3 +5,6 @@ // Attempts to instantiate all static objects bool UsePackageManager(std::string_view packageName); + +// Sets the module to prevent it from unloading. +void SetUnloadPreference(bool value); diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index 36fc999feb..5793553587 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -1,8 +1,103 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "Tests.h" -void UnloadAndCheckForLeaks() +using namespace std::string_view_literals; + +BOOL CALLBACK CheckForWinGetWindow(HWND hwnd, LPARAM param) +{ + bool* result = reinterpret_cast(param); + + int textLength = GetWindowTextLengthW(hwnd) + 1; + std::wstring windowText(textLength, '\0'); + textLength = GetWindowTextW(hwnd, &windowText[0], textLength); + windowText.resize(textLength); + + if (L"WingetMessageOnlyWindow"sv == windowText) + { + *result = true; + return FALSE; + } + + return TRUE; +} + +// Look for the set of well known objects that should be present after we have spun everything up. +// Returns true if all well known objects are found in the expected state. +bool SearchForWellKnownObjects(bool expectExist) { + bool result = true; + + auto coreApplicationProperties = winrt::Windows::ApplicationModel::Core::CoreApplication::Properties(); + + // COM statics + for (std::wstring_view item : { + L"WindowsPackageManager.CachedInstalledIndex"sv, + L"WindowsPackageManager.TerminationSignalHandler"sv, + }) + { + bool present = coreApplicationProperties.HasKey(item); + + if (present != expectExist) + { + std::cout << "CoreApplication property `" << winrt::to_string(item) << "` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; + result = false; + } + } + + // Shutdown monitoring window + bool foundWindow = false; + EnumWindows(CheckForWinGetWindow, reinterpret_cast(&foundWindow)); + + if (foundWindow != expectExist) + { + std::cout << "WinGet Window `WingetMessageOnlyWindow` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; + result = false; + } + + return result; +} + +Snapshot::Snapshot() +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE, GetCurrentProcessId()); + + // Count threads in this process + // TODO + + // Count modules + // TODO + + // Get memory stats + // TODO + + CloseHandle(snapshot); +} + +// TODO: Move this to be object based and have it just hold all of the snapshots +std::pair> UnloadAndCheckForLeaks(std::optional previousSnapshot, bool expectUnload) +{ + if (!SearchForWellKnownObjects(true)) + { + return { false , std::nullopt }; + } + + Snapshot loadedSnapshot; + CoFreeUnusedLibrariesEx(0, 0); + + Snapshot afterFreeSnapshot; + + if (!SearchForWellKnownObjects(!expectUnload)) + { + return { false , afterFreeSnapshot }; + } + + if (!CompareSnapshots(previousSnapshot, loadedSnapshot, afterFreeSnapshot)) + { + return { false , afterFreeSnapshot }; + } + + return { true, afterFreeSnapshot }; } diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index ab8fdf35bd..60bee84398 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -2,5 +2,14 @@ // Licensed under the MIT License. #pragma once +struct Snapshot +{ + Snapshot(); + + size_t ThreadCount = 0; + size_t ModuleCount = 0; + PROCESS_MEMORY_COUNTERS_EX2 Memory{}; +}; + // Forces COM to unload the module and checks for leaked resources. -void UnloadAndCheckForLeaks(); +std::pair> UnloadAndCheckForLeaks(std::optional previousSnapshot, bool expectUnload); diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index 396097c045..d7b5d63168 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -23,7 +23,7 @@ int main(int argc, const char** argv) try bool leakCOM = false; int iterations = 1; std::string packageName = "Microsoft.Edit"; - // bool preventUnload = false; TODO: Add ability to prevent unload and allow testing here + std::string unloadBehavior = "allow"; for (int i = 0; i < argc; ++i) { @@ -51,14 +51,20 @@ int main(int argc, const char** argv) try ADVANCE_ARG_PARAMETER packageName = argv[i]; } + else if ("-unload"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + unloadBehavior = ToLower(argv[i]); + } } std::cout << "Running inproc testbed with:\n" - " COM Init: " << comInit << "\n" - " Leak COM: " << (leakCOM ? "true" : "false") << "\n" - " Test : " << testToRun << "\n" - " Package : " << packageName << "\n" - " Passes : " << iterations << std::endl; + " COM Init : " << comInit << "\n" + " Leak COM : " << (leakCOM ? "true" : "false") << "\n" + " Unload : " << unloadBehavior << "\n" + " Test : " << testToRun << "\n" + " Package : " << packageName << "\n" + " Passes : " << iterations << std::endl; HRESULT hr = S_OK; @@ -75,19 +81,37 @@ int main(int argc, const char** argv) try if (FAILED(hr)) { std::cout << "RoInitialize returned " << hr << std::endl; - return 1; + return 2; + } + + bool shouldUnload = true; + if ("never"sv == unloadBehavior || "at_exit"sv == unloadBehavior) + { + SetUnloadPreference(false); + shouldUnload = false; } + std::optional snapshot; + for (int i = 0; i < iterations; ++i) { if (!UsePackageManager(packageName)) { - return 2; + return 3; } if ("unload_check"sv == testToRun) { - UnloadAndCheckForLeaks(); + auto [success, currentSnapshot] = UnloadAndCheckForLeaks(snapshot, shouldUnload); + if (!success) + { + return 4; + } + + if (currentSnapshot) + { + snapshot = std::move(currentSnapshot); + } } std::cout << "Iteration " << (i + 1) << " completed" << std::endl; @@ -98,21 +122,26 @@ int main(int argc, const char** argv) try RoUninitialize(); } + if ("at_exit"sv == unloadBehavior) + { + SetUnloadPreference(true); + } + std::cout << "Tests completed" << std::endl; return 0; } catch (const std::exception& e) { std::cout << "Caught std exception: " << e.what() << std::endl; - return 3; + return 1; } catch (const winrt::hresult_error& hre) { std::cout << "Caught winrt exception: " << winrt::to_string(hre.message()) << std::endl; - return 3; + return 1; } catch (...) { std::cout << "Caught unknown exception" << std::endl; - return 3; + return 1; } diff --git a/src/ComInprocTestbed/pch.h b/src/ComInprocTestbed/pch.h index 19555929fa..dbf3bf75f9 100644 --- a/src/ComInprocTestbed/pch.h +++ b/src/ComInprocTestbed/pch.h @@ -4,12 +4,16 @@ #define NOMINMAX #include +#include #include +#include +#include #include #include #include #include +#include #include #include diff --git a/src/Microsoft.Management.Deployment.InProc/dllmain.cpp b/src/Microsoft.Management.Deployment.InProc/dllmain.cpp index 8fee9ecfbe..31fbd30757 100644 --- a/src/Microsoft.Management.Deployment.InProc/dllmain.cpp +++ b/src/Microsoft.Management.Deployment.InProc/dllmain.cpp @@ -19,15 +19,6 @@ EXTERN_C BOOL WINAPI DllMain( } } break; - - case DLL_PROCESS_DETACH: - { - WindowsPackageManagerInProcModuleTerminate(); - } - break; - - default: - return TRUE; } return TRUE; } diff --git a/src/Microsoft.Management.Deployment/CanUnload.cpp b/src/Microsoft.Management.Deployment/CanUnload.cpp new file mode 100644 index 0000000000..20e53ebaf6 --- /dev/null +++ b/src/Microsoft.Management.Deployment/CanUnload.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + static bool s_canUnload = true; + + void SetCanUnload(bool value) + { + s_canUnload = value; + } + + bool GetCanUnload() + { + return s_canUnload; + } +} diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj index a01d0839e5..28b7ac7899 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -192,6 +192,7 @@ + @@ -208,6 +209,7 @@ + @@ -275,5 +277,4 @@ - - + \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters index 78c593d394..f2e381a566 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -46,6 +46,7 @@ + @@ -97,6 +98,9 @@ + + Public + @@ -111,4 +115,8 @@ {9c3907ed-84d9-4485-9b15-04c50717f0ab} + + + + \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index d931ff6b39..34802e3b3f 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(13)] // For version 1.12 + [contractversion(28)] // For version 1.28 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -1640,6 +1640,16 @@ namespace Microsoft.Management.Deployment /// Returns true if successful, false if settingsContent cannot be parsed or UserSettings is already created. /// This is a one time setup, multiple calls will not override existing UserSettings. Boolean SetUserSettings(String settingsContent); + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + // Gets or sets a value indicating whether the caller would prefer the module to stay loaded or not. + // This affects how the DllCanUnloadNow function called by COM behaves. If set to false it will act as if + // there are active objects at all times. If set to true it will allow the unload when there are no + // active objects. + // Defaults to true. + Boolean CanUnloadPreference{ get; set; }; + } } /// Force midl3 to generate vector marshalling info. diff --git a/src/Microsoft.Management.Deployment/PackageManagerSettings.cpp b/src/Microsoft.Management.Deployment/PackageManagerSettings.cpp index 65382e4459..65d58d1314 100644 --- a/src/Microsoft.Management.Deployment/PackageManagerSettings.cpp +++ b/src/Microsoft.Management.Deployment/PackageManagerSettings.cpp @@ -11,6 +11,7 @@ #pragma warning( pop ) #include "PackageManagerSettings.g.cpp" #include "Helpers.h" +#include "Public/CanUnload.h" #include #include @@ -57,5 +58,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation return success; } + bool PackageManagerSettings::CanUnloadPreference() const + { + return GetCanUnload(); + } + + void PackageManagerSettings::CanUnloadPreference(bool value) + { + return SetCanUnload(value); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManagerSettings); } diff --git a/src/Microsoft.Management.Deployment/PackageManagerSettings.h b/src/Microsoft.Management.Deployment/PackageManagerSettings.h index 9504efdaf9..ef2ce09af1 100644 --- a/src/Microsoft.Management.Deployment/PackageManagerSettings.h +++ b/src/Microsoft.Management.Deployment/PackageManagerSettings.h @@ -15,6 +15,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool SetCallerIdentifier(hstring const& callerIdentifier); bool SetStateIdentifier(hstring const& stateIdentifier); bool SetUserSettings(hstring const& settingsContent); + + // Contract 28 + bool CanUnloadPreference() const; + void CanUnloadPreference(bool value); }; } diff --git a/src/Microsoft.Management.Deployment/Public/CanUnload.h b/src/Microsoft.Management.Deployment/Public/CanUnload.h new file mode 100644 index 0000000000..960a9eaf2e --- /dev/null +++ b/src/Microsoft.Management.Deployment/Public/CanUnload.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + // Sets whether the module can unload or not. + void SetCanUnload(bool value); + + // Gets whether the module can unload or not. + bool GetCanUnload(); +} diff --git a/src/WindowsPackageManager/main.cpp b/src/WindowsPackageManager/main.cpp index 01df1ddab7..96d82c08b8 100644 --- a/src/WindowsPackageManager/main.cpp +++ b/src/WindowsPackageManager/main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include using namespace winrt::Microsoft::Management::Deployment; @@ -101,16 +102,20 @@ extern "C" { try { - // The WRL object count is used to track externally visible objects, which largely means objects created with the `wil::details::module_count_wrapper` type wrapper. - // Configuration objects use a composition based tracking that is similar in nature (only when OOP). - // - // In-proc DllCanUnloadNow should not be blocked by our internal objects, but they must be destroyed on unload or a future reload will attempt to destroy them - // and our module may have moved. So when we don't have any more objects that we gave to callers, remove all of our static lifetime objects and indicate - // that we can now be unloaded. - if (::Microsoft::WRL::Module<::Microsoft::WRL::ModuleType::InProc>::GetModule().Terminate()) + // Check whether the caller wants us to allow unloads + if (implementation::GetCanUnload()) { - AppInstaller::WinRT::COMStaticStorageStatics::ResetAll(); - return true; + // The WRL object count is used to track externally visible objects, which largely means objects created with the `wil::details::module_count_wrapper` type wrapper. + // Configuration objects use a composition based tracking that is similar in nature (only when OOP). + // + // In-proc DllCanUnloadNow should not be blocked by our internal objects, but they must be destroyed on unload or a future reload will attempt to destroy them + // and our module may have moved. So when we don't have any more objects that we gave to callers, remove all of our static lifetime objects and indicate + // that we can now be unloaded. + if (::Microsoft::WRL::Module<::Microsoft::WRL::ModuleType::InProc>::GetModule().Terminate()) + { + AppInstaller::WinRT::COMStaticStorageStatics::ResetAll(); + return true; + } } } catch (...) {} From 40d72747dc1744130ea05867775c46c8d1041cfd Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 21 Nov 2025 16:50:18 -0800 Subject: [PATCH 05/13] Better tests design --- .github/actions/spelling/expect.txt | 5 ++ src/ComInprocTestbed/Tests.cpp | 88 ++++++++++++++++++++++++----- src/ComInprocTestbed/Tests.h | 28 ++++++++- src/ComInprocTestbed/main.cpp | 30 ++++++---- src/ComInprocTestbed/pch.h | 2 + 5 files changed, 125 insertions(+), 28 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8ae4fef416..6462f1bfbc 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -434,6 +434,7 @@ PRODUCTICON propkey PROPVARIANT proxystub +psapi pscustomobject pseudocode PSHOST @@ -521,6 +522,8 @@ sku SLAPI SMTO SNAME +SNAPMODULE +SNAPTHREAD sortof sourceforge SOURCESDIRECTORY @@ -555,8 +558,10 @@ thiscouldbeapc threehundred timespan Tlg +tlhelp TLSCAs tombstoned +Toolhelp transitioning trimstart ttl diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index 5793553587..7bd407347e 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -61,43 +61,103 @@ bool SearchForWellKnownObjects(bool expectExist) Snapshot::Snapshot() { - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE, GetCurrentProcessId()); + const DWORD processId = GetCurrentProcessId(); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE, processId); // Count threads in this process - // TODO + THREADENTRY32 threadEntry{}; + threadEntry.dwSize = sizeof(threadEntry); + + if (Thread32First(snapshot, &threadEntry)) + { + do + { + if (processId == threadEntry.th32OwnerProcessID) + { + ++ThreadCount; + } + } while (Thread32Next(snapshot, &threadEntry)); + } // Count modules - // TODO + MODULEENTRY32 moduleEntry{}; + moduleEntry.dwSize = sizeof(moduleEntry); + + if (Module32First(snapshot, &moduleEntry)) + { + do + { + ++ModuleCount; + } while (Module32Next(snapshot, &moduleEntry)); + } // Get memory stats - // TODO + GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&Memory), sizeof(Memory)); CloseHandle(snapshot); } -// TODO: Move this to be object based and have it just hold all of the snapshots -std::pair> UnloadAndCheckForLeaks(std::optional previousSnapshot, bool expectUnload) +UnloadAndCheckForLeaks::UnloadAndCheckForLeaks(bool shouldUnload) : m_shouldUnload(shouldUnload) +{ +} + +bool UnloadAndCheckForLeaks::RunIteration() { if (!SearchForWellKnownObjects(true)) { - return { false , std::nullopt }; + return false; } - Snapshot loadedSnapshot; + Snapshot beforeUnload; CoFreeUnusedLibrariesEx(0, 0); - Snapshot afterFreeSnapshot; + m_iterationSnapshots.emplace_back(beforeUnload, Snapshot{}); + + if (!SearchForWellKnownObjects(!m_shouldUnload)) + { + return false; + } + + return true; +} + +bool UnloadAndCheckForLeaks::RunFinal() +{ + constexpr std::streamsize s_columnWidth = 4; + + bool result = true; + + std::cout << "--- UnloadAndCheckForLeaks results ---\n"; + std::cout << std::setfill(' '); - if (!SearchForWellKnownObjects(!expectUnload)) + // Threads + std::cout << "Thread Count [Initial: " << m_initialSnapshot.ThreadCount << "]\n"; + + std::cout << "Iteration "; + for (size_t i = 0; i < m_iterationSnapshots.size(); ++i) + { + std::cout << std::setw(s_columnWidth) << (i + 1); + } + std::cout << '\n'; + + std::cout << "Pre Unload "; + for (const auto& snapshot : m_iterationSnapshots) { - return { false , afterFreeSnapshot }; + std::cout << std::setw(s_columnWidth) << snapshot.first.ThreadCount; } + std::cout << '\n'; - if (!CompareSnapshots(previousSnapshot, loadedSnapshot, afterFreeSnapshot)) + std::cout << "Post Unload"; + for (const auto& snapshot : m_iterationSnapshots) { - return { false , afterFreeSnapshot }; + std::cout << std::setw(s_columnWidth) << snapshot.second.ThreadCount; } + std::cout << '\n'; - return { true, afterFreeSnapshot }; + // Modules + + // Memory + + return result; } diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index 60bee84398..d6b07b8dfc 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -11,5 +11,29 @@ struct Snapshot PROCESS_MEMORY_COUNTERS_EX2 Memory{}; }; -// Forces COM to unload the module and checks for leaked resources. -std::pair> UnloadAndCheckForLeaks(std::optional previousSnapshot, bool expectUnload); +// Represents a test that will be performed. +struct ITest +{ + virtual ~ITest() = default; + + // Runs an iteration of the test. + virtual bool RunIteration() = 0; + + // Performs the final test validation. + virtual bool RunFinal() = 0; +}; + +// A test that unloads the COM module and looks for resources that were not released. +struct UnloadAndCheckForLeaks : public ITest +{ + UnloadAndCheckForLeaks(bool shouldUnload); + + bool RunIteration() override; + + bool RunFinal() override; + +private: + bool m_shouldUnload; + Snapshot m_initialSnapshot; + std::vector> m_iterationSnapshots; +}; diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index d7b5d63168..438a050348 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -16,6 +16,16 @@ std::string ToLower(std::string_view in) return result; } +std::unique_ptr CreateTest(std::string_view test, bool shouldUnload) +{ + if ("unload_check"sv == test) + { + return std::make_unique(shouldUnload); + } + + return {}; +} + int main(int argc, const char** argv) try { std::string testToRun; @@ -91,7 +101,7 @@ int main(int argc, const char** argv) try shouldUnload = false; } - std::optional snapshot; + auto test = CreateTest(testToRun, shouldUnload); for (int i = 0; i < iterations; ++i) { @@ -100,23 +110,19 @@ int main(int argc, const char** argv) try return 3; } - if ("unload_check"sv == testToRun) + if (test && !test->RunIteration()) { - auto [success, currentSnapshot] = UnloadAndCheckForLeaks(snapshot, shouldUnload); - if (!success) - { - return 4; - } - - if (currentSnapshot) - { - snapshot = std::move(currentSnapshot); - } + return 4; } std::cout << "Iteration " << (i + 1) << " completed" << std::endl; } + if (test && !test->RunFinal()) + { + return 5; + } + if (!leakCOM) { RoUninitialize(); diff --git a/src/ComInprocTestbed/pch.h b/src/ComInprocTestbed/pch.h index dbf3bf75f9..3810d4f9cf 100644 --- a/src/ComInprocTestbed/pch.h +++ b/src/ComInprocTestbed/pch.h @@ -13,7 +13,9 @@ #include #include +#include #include #include #include #include +#include From 325bd80e285d1025c566a4597af1b8da92dfc8fd Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 25 Nov 2025 12:17:49 -0800 Subject: [PATCH 06/13] Refactor test parameters and make activation factories counted objects --- src/ComInprocTestbed/Tests.cpp | 267 +++++++++++++++--- src/ComInprocTestbed/Tests.h | 72 ++++- src/ComInprocTestbed/main.cpp | 113 +------- .../AddPackageCatalogOptions.h | 3 +- .../AuthenticationArguments.h | 5 +- .../CreateCompositePackageCatalogOptions.h | 5 +- .../DownloadOptions.h | 137 ++++----- .../FindPackagesOptions.h | 3 +- .../InstallOptions.h | 3 +- .../PackageManager.h | 3 +- .../PackageManagerSettings.h | 3 +- .../PackageMatchFilter.h | 3 +- ...atableMicrosoftManagementDeploymentClass.h | 6 +- .../RemovePackageCatalogOptions.h | 5 +- .../RepairOptions.h | 3 +- .../UninstallOptions.h | 3 +- 16 files changed, 406 insertions(+), 228 deletions(-) diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index 7bd407347e..cb6c5c4b89 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -2,61 +2,258 @@ // Licensed under the MIT License. #include "pch.h" #include "Tests.h" +#include "PackageManager.h" using namespace std::string_view_literals; -BOOL CALLBACK CheckForWinGetWindow(HWND hwnd, LPARAM param) +namespace { - bool* result = reinterpret_cast(param); +#define ADVANCE_ARG_PARAMETER if (++i >= argc) { winrt::throw_hresult(E_INVALIDARG); } - int textLength = GetWindowTextLengthW(hwnd) + 1; - std::wstring windowText(textLength, '\0'); - textLength = GetWindowTextW(hwnd, &windowText[0], textLength); - windowText.resize(textLength); - - if (L"WingetMessageOnlyWindow"sv == windowText) + std::string ToLower(std::string_view in) { - *result = true; - return FALSE; + std::string result(in); + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return result; } - return TRUE; -} +#define BEGIN_ENUM_PARSE_FUNC(_type_) \ + _type_ Parse ## _type_(const char* input) \ + { \ + auto lower = ToLower(input); \ + if (lower.empty()) {} -// Look for the set of well known objects that should be present after we have spun everything up. -// Returns true if all well known objects are found in the expected state. -bool SearchForWellKnownObjects(bool expectExist) -{ - bool result = true; +#define ITEM_ENUM_PARSE_FUNC(_type_, _value_) \ + else if (ToLower(#_value_) == lower) \ + { \ + return _type_ ## :: ## _value_; \ + } - auto coreApplicationProperties = winrt::Windows::ApplicationModel::Core::CoreApplication::Properties(); +#define END_ENUM_PARSE_FUNC \ + winrt::throw_hresult(E_INVALIDARG); \ + } - // COM statics - for (std::wstring_view item : { - L"WindowsPackageManager.CachedInstalledIndex"sv, - L"WindowsPackageManager.TerminationSignalHandler"sv, - }) +#define BEGIN_ENUM_NAME_FUNC(_type_) \ + std::string_view ToString(_type_); \ + std::ostream& operator<<(std::ostream& o, _type_ input) { return (o << ToString(input)); } \ + std::string_view ToString(_type_ input) \ + { \ + switch (input) { + +#define ITEM_ENUM_NAME_FUNC(_type_, _value_) \ + case _type_ ## :: ## _value_: return #_value_; + +#define END_ENUM_NAME_FUNC \ + } \ + return "Unknown"; \ + } + + BEGIN_ENUM_PARSE_FUNC(ComInitializationType) + ITEM_ENUM_PARSE_FUNC(ComInitializationType, STA) + ITEM_ENUM_PARSE_FUNC(ComInitializationType, MTA) + END_ENUM_PARSE_FUNC + + BEGIN_ENUM_NAME_FUNC(ComInitializationType) + ITEM_ENUM_NAME_FUNC(ComInitializationType, STA) + ITEM_ENUM_NAME_FUNC(ComInitializationType, MTA) + END_ENUM_NAME_FUNC + + BEGIN_ENUM_PARSE_FUNC(UnloadBehavior) + ITEM_ENUM_PARSE_FUNC(UnloadBehavior, Allow) + ITEM_ENUM_PARSE_FUNC(UnloadBehavior, AtExit) + ITEM_ENUM_PARSE_FUNC(UnloadBehavior, Never) + END_ENUM_PARSE_FUNC + + BEGIN_ENUM_NAME_FUNC(UnloadBehavior) + ITEM_ENUM_NAME_FUNC(UnloadBehavior, Allow) + ITEM_ENUM_NAME_FUNC(UnloadBehavior, AtExit) + ITEM_ENUM_NAME_FUNC(UnloadBehavior, Never) + END_ENUM_NAME_FUNC + + BEGIN_ENUM_PARSE_FUNC(ActivationType) + ITEM_ENUM_PARSE_FUNC(ActivationType, ClassName) + ITEM_ENUM_PARSE_FUNC(ActivationType, CLSID_WinRT) + ITEM_ENUM_PARSE_FUNC(ActivationType, CLSID_CoCreateInstance) + END_ENUM_PARSE_FUNC + + BEGIN_ENUM_NAME_FUNC(ActivationType) + ITEM_ENUM_NAME_FUNC(ActivationType, ClassName) + ITEM_ENUM_NAME_FUNC(ActivationType, CLSID_WinRT) + ITEM_ENUM_NAME_FUNC(ActivationType, CLSID_CoCreateInstance) + END_ENUM_NAME_FUNC + + BOOL CALLBACK CheckForWinGetWindow(HWND hwnd, LPARAM param) { - bool present = coreApplicationProperties.HasKey(item); + bool* result = reinterpret_cast(param); + + int textLength = GetWindowTextLengthW(hwnd) + 1; + std::wstring windowText(textLength, '\0'); + textLength = GetWindowTextW(hwnd, &windowText[0], textLength); + windowText.resize(textLength); + + if (L"WingetMessageOnlyWindow"sv == windowText) + { + *result = true; + return FALSE; + } + + return TRUE; + } + + // Look for the set of well known objects that should be present after we have spun everything up. + // Returns true if all well known objects are found in the expected state. + bool SearchForWellKnownObjects(bool expectExist) + { + bool result = true; + + auto coreApplicationProperties = winrt::Windows::ApplicationModel::Core::CoreApplication::Properties(); + + // COM statics + for (std::wstring_view item : { + L"WindowsPackageManager.CachedInstalledIndex"sv, + L"WindowsPackageManager.TerminationSignalHandler"sv, + }) + { + bool present = coreApplicationProperties.HasKey(item); - if (present != expectExist) + if (present != expectExist) + { + std::cout << "CoreApplication property `" << winrt::to_string(item) << "` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; + result = false; + } + } + + // Shutdown monitoring window + bool foundWindow = false; + EnumWindows(CheckForWinGetWindow, reinterpret_cast(&foundWindow)); + + if (foundWindow != expectExist) { - std::cout << "CoreApplication property `" << winrt::to_string(item) << "` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; + std::cout << "WinGet Window `WingetMessageOnlyWindow` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; result = false; } + + return result; } +} - // Shutdown monitoring window - bool foundWindow = false; - EnumWindows(CheckForWinGetWindow, reinterpret_cast(&foundWindow)); +TestParameters::TestParameters(int argc, const char** argv) +{ + for (int i = 0; i < argc; ++i) + { + if ("-test"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + TestToRun = ToLower(argv[i]); + } + else if ("-com"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + ComInit = ParseComInitializationType(argv[i]); + } + else if ("-leak-com"sv == argv[i]) + { + LeakCOM = true; + } + else if ("-itr"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + Iterations = atoi(argv[i]); + } + else if ("-pkg"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + PackageName = argv[i]; + } + else if ("-unload"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + UnloadBehavior = ParseUnloadBehavior(argv[i]); + } + else if ("-activation"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + ActivationType = ParseActivationType(argv[i]); + } + else if ("-clear-factories"sv == argv[i]) + { + ClearFactories = true; + } + } +} + +void TestParameters::OutputDetails() const +{ + std::cout << "Running inproc testbed with:\n" + " COM Init : " << ComInit << "\n" + " Activate : " << ActivationType << "\n" + " Clear : " << std::boolalpha << ClearFactories << "\n" + " Leak COM : " << std::boolalpha << LeakCOM << "\n" + " Unload : " << UnloadBehavior << "\n" + " Test : " << TestToRun << "\n" + " Package : " << PackageName << "\n" + " Passes : " << Iterations << std::endl; +} + +bool TestParameters::InitializeTestState() const +{ + HRESULT hr = S_OK; - if (foundWindow != expectExist) + if (ComInitializationType::STA == ComInit) { - std::cout << "WinGet Window `WingetMessageOnlyWindow` was not in expected state [" << (expectExist ? "should exist" : "should not exist") << "]\n"; - result = false; + hr = RoInitialize(RO_INIT_SINGLETHREADED); + } + else if (ComInitializationType::MTA == ComInit) + { + hr = RoInitialize(RO_INIT_MULTITHREADED); } - return result; + if (FAILED(hr)) + { + std::cout << "RoInitialize returned " << hr << std::endl; + return false; + } + + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior) + { + SetUnloadPreference(false); + } + + return true; +} + +std::unique_ptr TestParameters::CreateTest() const +{ + if ("unload_check"sv == TestToRun) + { + return std::make_unique(*this); + } + + return {}; +} + +void TestParameters::UninitializeTestState() const +{ + if (!LeakCOM) + { + RoUninitialize(); + } + + if (UnloadBehavior::AtExit == UnloadBehavior) + { + SetUnloadPreference(true); + } +} + +bool TestParameters::UnloadExpected() const +{ + bool shouldUnload = true; + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior) + { + shouldUnload = false; + } + return shouldUnload; } Snapshot::Snapshot() @@ -97,7 +294,7 @@ Snapshot::Snapshot() CloseHandle(snapshot); } -UnloadAndCheckForLeaks::UnloadAndCheckForLeaks(bool shouldUnload) : m_shouldUnload(shouldUnload) +UnloadAndCheckForLeaks::UnloadAndCheckForLeaks(const TestParameters& parameters) : m_parameters(parameters) { } @@ -114,7 +311,7 @@ bool UnloadAndCheckForLeaks::RunIteration() m_iterationSnapshots.emplace_back(beforeUnload, Snapshot{}); - if (!SearchForWellKnownObjects(!m_shouldUnload)) + if (!SearchForWellKnownObjects(!m_parameters.UnloadExpected())) { return false; } diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index d6b07b8dfc..e1c4ddee4d 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -1,15 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once - -struct Snapshot -{ - Snapshot(); - - size_t ThreadCount = 0; - size_t ModuleCount = 0; - PROCESS_MEMORY_COUNTERS_EX2 Memory{}; -}; +#include +#include +#include // Represents a test that will be performed. struct ITest @@ -23,17 +17,73 @@ struct ITest virtual bool RunFinal() = 0; }; +enum class ComInitializationType +{ + STA, + MTA, +}; + +enum class UnloadBehavior +{ + Allow, + AtExit, + Never, +}; + +enum class ActivationType +{ + ClassName, + CLSID_WinRT, + CLSID_CoCreateInstance, +}; + +// Test parameters from command line +struct TestParameters +{ + TestParameters(int argc, const char** argv); + + void OutputDetails() const; + + bool InitializeTestState() const; + + std::unique_ptr CreateTest() const; + + void UninitializeTestState() const; + + // Determines if we expect COM to unload the module based on inputs. + bool UnloadExpected() const; + + std::string TestToRun; + ComInitializationType ComInit = ComInitializationType::MTA; + bool LeakCOM = false; + int Iterations = 1; + std::string PackageName = "Microsoft.Edit"; + UnloadBehavior UnloadBehavior = UnloadBehavior::Allow; + ActivationType ActivationType = ActivationType::ClassName; + bool ClearFactories = true; +}; + +// Catpures a snapshot of current resource usage. +struct Snapshot +{ + Snapshot(); + + size_t ThreadCount = 0; + size_t ModuleCount = 0; + PROCESS_MEMORY_COUNTERS_EX2 Memory{}; +}; + // A test that unloads the COM module and looks for resources that were not released. struct UnloadAndCheckForLeaks : public ITest { - UnloadAndCheckForLeaks(bool shouldUnload); + UnloadAndCheckForLeaks(const TestParameters& parameters); bool RunIteration() override; bool RunFinal() override; private: - bool m_shouldUnload; + const TestParameters& m_parameters; Snapshot m_initialSnapshot; std::vector> m_iterationSnapshots; }; diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index 438a050348..77099cab9b 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -6,110 +6,29 @@ using namespace std::string_view_literals; -#define ADVANCE_ARG_PARAMETER if (++i >= argc) { return E_INVALIDARG; } - -std::string ToLower(std::string_view in) -{ - std::string result(in); - std::transform(result.begin(), result.end(), result.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return result; -} - -std::unique_ptr CreateTest(std::string_view test, bool shouldUnload) -{ - if ("unload_check"sv == test) - { - return std::make_unique(shouldUnload); - } - - return {}; -} - int main(int argc, const char** argv) try { - std::string testToRun; - std::string comInit = "mta"; - bool leakCOM = false; - int iterations = 1; - std::string packageName = "Microsoft.Edit"; - std::string unloadBehavior = "allow"; - - for (int i = 0; i < argc; ++i) - { - if ("-test"sv == argv[i]) - { - ADVANCE_ARG_PARAMETER - testToRun = ToLower(argv[i]); - } - else if ("-com"sv == argv[i]) - { - ADVANCE_ARG_PARAMETER - comInit = ToLower(argv[i]); - } - else if ("-leak-com"sv == argv[i]) - { - leakCOM = true; - } - else if ("-itr"sv == argv[i]) - { - ADVANCE_ARG_PARAMETER - iterations = atoi(argv[i]); - } - else if ("-pkg"sv == argv[i]) - { - ADVANCE_ARG_PARAMETER - packageName = argv[i]; - } - else if ("-unload"sv == argv[i]) - { - ADVANCE_ARG_PARAMETER - unloadBehavior = ToLower(argv[i]); - } - } - - std::cout << "Running inproc testbed with:\n" - " COM Init : " << comInit << "\n" - " Leak COM : " << (leakCOM ? "true" : "false") << "\n" - " Unload : " << unloadBehavior << "\n" - " Test : " << testToRun << "\n" - " Package : " << packageName << "\n" - " Passes : " << iterations << std::endl; - - HRESULT hr = S_OK; - - if ("sta"sv == comInit) - { - hr = RoInitialize(RO_INIT_SINGLETHREADED); - } - else if ("mta"sv == comInit) - { - hr = RoInitialize(RO_INIT_MULTITHREADED); - } - // else no COM init means "let C++/WinRT do it" - - if (FAILED(hr)) + const TestParameters testParameters(argc, argv); + testParameters.OutputDetails(); + if (!testParameters.InitializeTestState()) { - std::cout << "RoInitialize returned " << hr << std::endl; return 2; } - bool shouldUnload = true; - if ("never"sv == unloadBehavior || "at_exit"sv == unloadBehavior) - { - SetUnloadPreference(false); - shouldUnload = false; - } - - auto test = CreateTest(testToRun, shouldUnload); + auto test = testParameters.CreateTest(); - for (int i = 0; i < iterations; ++i) + for (int i = 0; i < testParameters.Iterations; ++i) { - if (!UsePackageManager(packageName)) + if (!UsePackageManager(testParameters.PackageName)) { return 3; } + if (testParameters.ClearFactories) + { + winrt::clear_factory_cache(); + } + if (test && !test->RunIteration()) { return 4; @@ -123,15 +42,7 @@ int main(int argc, const char** argv) try return 5; } - if (!leakCOM) - { - RoUninitialize(); - } - - if ("at_exit"sv == unloadBehavior) - { - SetUnloadPreference(true); - } + testParameters.UninitializeTestState(); std::cout << "Tests completed" << std::endl; return 0; diff --git a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h index d7a32b774e..6d2c487c8c 100644 --- a/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/AddPackageCatalogOptions.h @@ -3,6 +3,7 @@ #pragma once #include "AddPackageCatalogOptions.g.h" #include "public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -44,7 +45,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct AddPackageCatalogOptions : AddPackageCatalogOptionsT + struct AddPackageCatalogOptions : AddPackageCatalogOptionsT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/AuthenticationArguments.h b/src/Microsoft.Management.Deployment/AuthenticationArguments.h index e3cf98ae32..1703a2fa75 100644 --- a/src/Microsoft.Management.Deployment/AuthenticationArguments.h +++ b/src/Microsoft.Management.Deployment/AuthenticationArguments.h @@ -3,6 +3,7 @@ #pragma once #include "AuthenticationArguments.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -27,8 +28,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct AuthenticationArguments : AuthenticationArgumentsT + struct AuthenticationArguments : AuthenticationArgumentsT, AppInstaller::WinRT::ModuleCountBase { }; } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.Management.Deployment/CreateCompositePackageCatalogOptions.h b/src/Microsoft.Management.Deployment/CreateCompositePackageCatalogOptions.h index 538b203361..c6ab08a243 100644 --- a/src/Microsoft.Management.Deployment/CreateCompositePackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/CreateCompositePackageCatalogOptions.h @@ -3,6 +3,7 @@ #pragma once #include "CreateCompositePackageCatalogOptions.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -29,7 +30,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct CreateCompositePackageCatalogOptions : CreateCompositePackageCatalogOptionsT + struct CreateCompositePackageCatalogOptions : + CreateCompositePackageCatalogOptionsT, + AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.h b/src/Microsoft.Management.Deployment/DownloadOptions.h index 2909378d58..1243c3e70b 100644 --- a/src/Microsoft.Management.Deployment/DownloadOptions.h +++ b/src/Microsoft.Management.Deployment/DownloadOptions.h @@ -1,70 +1,71 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "DownloadOptions.g.h" -#include "Public/ComClsids.h" - -namespace winrt::Microsoft::Management::Deployment::implementation -{ - [uuid(WINGET_OUTOFPROC_COM_CLSID_DownloadOptions)] - struct DownloadOptions : DownloadOptionsT - { - DownloadOptions(); - - winrt::Microsoft::Management::Deployment::PackageVersionId PackageVersionId(); - void PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value); - winrt::Microsoft::Management::Deployment::PackageInstallScope Scope(); - void Scope(winrt::Microsoft::Management::Deployment::PackageInstallScope const& value); - winrt::Microsoft::Management::Deployment::PackageInstallerType InstallerType(); - void InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value); - winrt::Windows::System::ProcessorArchitecture Architecture(); - void Architecture(winrt::Windows::System::ProcessorArchitecture const& value); - hstring Locale(); - void Locale(hstring const& value); - hstring DownloadDirectory(); - void DownloadDirectory(hstring const& value); - bool AllowHashMismatch(); - void AllowHashMismatch(bool value); - bool SkipDependencies(); - void SkipDependencies(bool value); - bool AcceptPackageAgreements(); - void AcceptPackageAgreements(bool value); - hstring CorrelationData(); +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DownloadOptions.g.h" +#include "Public/ComClsids.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_DownloadOptions)] + struct DownloadOptions : DownloadOptionsT + { + DownloadOptions(); + + winrt::Microsoft::Management::Deployment::PackageVersionId PackageVersionId(); + void PackageVersionId(winrt::Microsoft::Management::Deployment::PackageVersionId const& value); + winrt::Microsoft::Management::Deployment::PackageInstallScope Scope(); + void Scope(winrt::Microsoft::Management::Deployment::PackageInstallScope const& value); + winrt::Microsoft::Management::Deployment::PackageInstallerType InstallerType(); + void InstallerType(winrt::Microsoft::Management::Deployment::PackageInstallerType const& value); + winrt::Windows::System::ProcessorArchitecture Architecture(); + void Architecture(winrt::Windows::System::ProcessorArchitecture const& value); + hstring Locale(); + void Locale(hstring const& value); + hstring DownloadDirectory(); + void DownloadDirectory(hstring const& value); + bool AllowHashMismatch(); + void AllowHashMismatch(bool value); + bool SkipDependencies(); + void SkipDependencies(bool value); + bool AcceptPackageAgreements(); + void AcceptPackageAgreements(bool value); + hstring CorrelationData(); void CorrelationData(hstring const& value); - winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); - void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); - bool SkipMicrosoftStoreLicense(); - void SkipMicrosoftStoreLicense(bool value); - winrt::Microsoft::Management::Deployment::WindowsPlatform Platform(); - void Platform(winrt::Microsoft::Management::Deployment::WindowsPlatform value); - hstring TargetOSVersion(); - void TargetOSVersion(hstring const& value); - -#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) - private: - winrt::Microsoft::Management::Deployment::PackageVersionId m_packageVersionId{ nullptr }; - winrt::Microsoft::Management::Deployment::PackageInstallScope m_scope = winrt::Microsoft::Management::Deployment::PackageInstallScope::Any; - winrt::Microsoft::Management::Deployment::PackageInstallerType m_installerType = winrt::Microsoft::Management::Deployment::PackageInstallerType::Unknown; - winrt::Windows::System::ProcessorArchitecture m_architecture = winrt::Windows::System::ProcessorArchitecture::Unknown; - std::wstring m_locale = L""; - std::wstring m_downloadDirectory = L""; - bool m_allowHashMismatch = false; - bool m_skipDependencies = false; - bool m_acceptPackageAgreements = true; + winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); + void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); + bool SkipMicrosoftStoreLicense(); + void SkipMicrosoftStoreLicense(bool value); + winrt::Microsoft::Management::Deployment::WindowsPlatform Platform(); + void Platform(winrt::Microsoft::Management::Deployment::WindowsPlatform value); + hstring TargetOSVersion(); + void TargetOSVersion(hstring const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PackageVersionId m_packageVersionId{ nullptr }; + winrt::Microsoft::Management::Deployment::PackageInstallScope m_scope = winrt::Microsoft::Management::Deployment::PackageInstallScope::Any; + winrt::Microsoft::Management::Deployment::PackageInstallerType m_installerType = winrt::Microsoft::Management::Deployment::PackageInstallerType::Unknown; + winrt::Windows::System::ProcessorArchitecture m_architecture = winrt::Windows::System::ProcessorArchitecture::Unknown; + std::wstring m_locale = L""; + std::wstring m_downloadDirectory = L""; + bool m_allowHashMismatch = false; + bool m_skipDependencies = false; + bool m_acceptPackageAgreements = true; std::wstring m_correlationData = L""; - winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; - bool m_skipMicrosoftStoreLicense = false; - winrt::Microsoft::Management::Deployment::WindowsPlatform m_platform = winrt::Microsoft::Management::Deployment::WindowsPlatform::Unknown; - std::wstring m_targetOSVersion; -#endif - }; -} - -#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) -namespace winrt::Microsoft::Management::Deployment::factory_implementation -{ - struct DownloadOptions : DownloadOptionsT - { - }; -} -#endif + winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; + bool m_skipMicrosoftStoreLicense = false; + winrt::Microsoft::Management::Deployment::WindowsPlatform m_platform = winrt::Microsoft::Management::Deployment::WindowsPlatform::Unknown; + std::wstring m_targetOSVersion; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct DownloadOptions : DownloadOptionsT, AppInstaller::WinRT::ModuleCountBase + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/FindPackagesOptions.h b/src/Microsoft.Management.Deployment/FindPackagesOptions.h index d9c3bc6a63..7be5e5ff0a 100644 --- a/src/Microsoft.Management.Deployment/FindPackagesOptions.h +++ b/src/Microsoft.Management.Deployment/FindPackagesOptions.h @@ -3,6 +3,7 @@ #pragma once #include "FindPackagesOptions.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -30,7 +31,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct FindPackagesOptions : FindPackagesOptionsT + struct FindPackagesOptions : FindPackagesOptionsT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.h b/src/Microsoft.Management.Deployment/InstallOptions.h index 15913bf5ce..8915e294a8 100644 --- a/src/Microsoft.Management.Deployment/InstallOptions.h +++ b/src/Microsoft.Management.Deployment/InstallOptions.h @@ -3,6 +3,7 @@ #pragma once #include "InstallOptions.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -75,7 +76,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct InstallOptions : InstallOptionsT + struct InstallOptions : InstallOptionsT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index 0969836d6f..17eff85f20 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -3,6 +3,7 @@ #pragma once #include "PackageManager.g.h" #include "Public/ComClsids.h" +#include #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) // Forward declaration @@ -62,7 +63,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct PackageManager : PackageManagerT + struct PackageManager : PackageManagerT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/PackageManagerSettings.h b/src/Microsoft.Management.Deployment/PackageManagerSettings.h index ef2ce09af1..10bed57ac6 100644 --- a/src/Microsoft.Management.Deployment/PackageManagerSettings.h +++ b/src/Microsoft.Management.Deployment/PackageManagerSettings.h @@ -3,6 +3,7 @@ #pragma once #include "PackageManagerSettings.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -25,7 +26,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct PackageManagerSettings : PackageManagerSettingsT + struct PackageManagerSettings : PackageManagerSettingsT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/PackageMatchFilter.h b/src/Microsoft.Management.Deployment/PackageMatchFilter.h index 94d75a8031..db3aa6d38b 100644 --- a/src/Microsoft.Management.Deployment/PackageMatchFilter.h +++ b/src/Microsoft.Management.Deployment/PackageMatchFilter.h @@ -3,6 +3,7 @@ #pragma once #include "PackageMatchFilter.g.h" #include "Public/ComClsids.h" +#include #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace AppInstaller::Repository @@ -41,7 +42,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct PackageMatchFilter : PackageMatchFilterT + struct PackageMatchFilter : PackageMatchFilterT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/Public/CoCreatableMicrosoftManagementDeploymentClass.h b/src/Microsoft.Management.Deployment/Public/CoCreatableMicrosoftManagementDeploymentClass.h index e73db06bef..d43af6915a 100644 --- a/src/Microsoft.Management.Deployment/Public/CoCreatableMicrosoftManagementDeploymentClass.h +++ b/src/Microsoft.Management.Deployment/Public/CoCreatableMicrosoftManagementDeploymentClass.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include namespace winrt::Microsoft::Management::Deployment::implementation @@ -24,5 +25,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation }; #define CoCreatableMicrosoftManagementDeploymentClass(className) \ - CoCreatableClassWithFactory(className, ::winrt::Microsoft::Management::Deployment::implementation::wrl_factory_for_winrt_com_class) + CoCreatableClassWithFactory(className, ::winrt::Microsoft::Management::Deployment::implementation::wrl_factory_for_winrt_com_class) \ + void CoCreatableMicrosoftManagementDeploymentClass_WRL_ModuleCountCheckFor_ ## className() { \ + static_assert(__is_base_of(::AppInstaller::WinRT::ModuleCountBase, ::winrt::Microsoft::Management::Deployment::factory_implementation:: ## className), "Object factories must derive from AppInstaller::WinRT::ModuleCountBase"); \ + } } diff --git a/src/Microsoft.Management.Deployment/RemovePackageCatalogOptions.h b/src/Microsoft.Management.Deployment/RemovePackageCatalogOptions.h index 583043d6f1..6edaf47b2e 100644 --- a/src/Microsoft.Management.Deployment/RemovePackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/RemovePackageCatalogOptions.h @@ -3,6 +3,7 @@ #pragma once #include "RemovePackageCatalogOptions.g.h" #include "public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -28,7 +29,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct RemovePackageCatalogOptions : RemovePackageCatalogOptionsT + struct RemovePackageCatalogOptions : + RemovePackageCatalogOptionsT, + AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/RepairOptions.h b/src/Microsoft.Management.Deployment/RepairOptions.h index 909cf9f6f2..d25837a82f 100644 --- a/src/Microsoft.Management.Deployment/RepairOptions.h +++ b/src/Microsoft.Management.Deployment/RepairOptions.h @@ -3,6 +3,7 @@ #pragma once #include "RepairOptions.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -51,7 +52,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct RepairOptions : RepairOptionsT + struct RepairOptions : RepairOptionsT, AppInstaller::WinRT::ModuleCountBase { }; } diff --git a/src/Microsoft.Management.Deployment/UninstallOptions.h b/src/Microsoft.Management.Deployment/UninstallOptions.h index e19babd7e5..f505dcedbd 100644 --- a/src/Microsoft.Management.Deployment/UninstallOptions.h +++ b/src/Microsoft.Management.Deployment/UninstallOptions.h @@ -3,6 +3,7 @@ #pragma once #include "UninstallOptions.g.h" #include "Public/ComClsids.h" +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -39,7 +40,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) namespace winrt::Microsoft::Management::Deployment::factory_implementation { - struct UninstallOptions : UninstallOptionsT + struct UninstallOptions : UninstallOptionsT, AppInstaller::WinRT::ModuleCountBase { }; } From 26580af696f3d6902100038203d821add11e85d9 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 27 Nov 2025 17:16:43 -0800 Subject: [PATCH 07/13] Implement activation type, update tests --- src/ComInprocTestbed/PackageManager.cpp | 21 ++-- src/ComInprocTestbed/PackageManager.h | 3 +- src/ComInprocTestbed/Tests.cpp | 131 ++++++++++++++++++++++-- src/ComInprocTestbed/Tests.h | 12 ++- src/ComInprocTestbed/main.cpp | 4 +- 5 files changed, 146 insertions(+), 25 deletions(-) diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp index dcbe825946..7673311f5d 100644 --- a/src/ComInprocTestbed/PackageManager.cpp +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "PackageManager.h" using namespace winrt::Microsoft::Management::Deployment; @@ -17,9 +18,9 @@ PackageCatalog Connect(const PackageCatalogReference& reference, std::string_vie return connectResult.PackageCatalog(); } -bool UsePackageManager(std::string_view packageName) +bool UsePackageManager(const TestParameters& testParameters) { - PackageManager packageManager; + PackageManager packageManager = testParameters.CreatePackageManager(); // Force installed cache to be created auto installedCatalogRef = packageManager.GetLocalPackageCatalog(LocalPackageCatalog::InstalledPackages); @@ -30,7 +31,7 @@ bool UsePackageManager(std::string_view packageName) } // Force TerminationSignalHandler to be created - CreateCompositePackageCatalogOptions options; + CreateCompositePackageCatalogOptions options = testParameters.CreateCreateCompositePackageCatalogOptions(); options.CompositeSearchBehavior(CompositeSearchBehavior::RemotePackagesFromRemoteCatalogs); for (PackageCatalogReference catalogRef : packageManager.GetPackageCatalogs()) @@ -44,34 +45,34 @@ bool UsePackageManager(std::string_view packageName) return false; } - PackageMatchFilter filter; + PackageMatchFilter filter = testParameters.CreatePackageMatchFilter(); filter.Field(PackageMatchField::Id); filter.Option(PackageFieldMatchOption::EqualsCaseInsensitive); - filter.Value(winrt::to_hstring(packageName)); + filter.Value(winrt::to_hstring(testParameters.PackageName)); - FindPackagesOptions findOptions; + FindPackagesOptions findOptions = testParameters.CreateFindPackagesOptions(); findOptions.Filters().Append(filter); auto findResult = compositeCatalog.FindPackages(findOptions); if (findResult.Status() != FindPackagesResultStatus::Ok) { - std::cout << "Finding package " << packageName << " got: " << static_cast(findResult.Status()) << " [" << findResult.ExtendedErrorCode() << "]\n"; + std::cout << "Finding package " << testParameters.PackageName << " got: " << static_cast(findResult.Status()) << " [" << findResult.ExtendedErrorCode() << "]\n"; return false; } if (findResult.Matches().Size() != 1) { - std::cout << "Finding package " << packageName << " got " << findResult.Matches().Size() << " results.\n"; + std::cout << "Finding package " << testParameters.PackageName << " got " << findResult.Matches().Size() << " results.\n"; return false; } - DownloadOptions downloadOptions; + DownloadOptions downloadOptions = testParameters.CreateDownloadOptions(); auto downloadOperation = packageManager.DownloadPackageAsync(findResult.Matches().GetAt(0).CatalogPackage(), downloadOptions); auto downloadResult = downloadOperation.get(); if (downloadResult.Status() != DownloadResultStatus::Ok) { - std::cout << "Downloading package " << packageName << " got: " << static_cast(downloadResult.Status()) << "\n"; + std::cout << "Downloading package " << testParameters.PackageName << " got: " << static_cast(downloadResult.Status()) << "\n"; return false; } diff --git a/src/ComInprocTestbed/PackageManager.h b/src/ComInprocTestbed/PackageManager.h index b1646594fb..31c22bef1c 100644 --- a/src/ComInprocTestbed/PackageManager.h +++ b/src/ComInprocTestbed/PackageManager.h @@ -2,9 +2,10 @@ // Licensed under the MIT License. #pragma once #include +#include "Tests.h" // Attempts to instantiate all static objects -bool UsePackageManager(std::string_view packageName); +bool UsePackageManager(const TestParameters& testParameters); // Sets the module to prevent it from unloading. void SetUnloadPreference(bool value); diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index cb6c5c4b89..9af209b45f 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -5,6 +5,7 @@ #include "PackageManager.h" using namespace std::string_view_literals; +using namespace winrt::Microsoft::Management::Deployment; namespace { @@ -73,14 +74,12 @@ namespace BEGIN_ENUM_PARSE_FUNC(ActivationType) ITEM_ENUM_PARSE_FUNC(ActivationType, ClassName) - ITEM_ENUM_PARSE_FUNC(ActivationType, CLSID_WinRT) - ITEM_ENUM_PARSE_FUNC(ActivationType, CLSID_CoCreateInstance) + ITEM_ENUM_PARSE_FUNC(ActivationType, CoCreateInstance) END_ENUM_PARSE_FUNC BEGIN_ENUM_NAME_FUNC(ActivationType) ITEM_ENUM_NAME_FUNC(ActivationType, ClassName) - ITEM_ENUM_NAME_FUNC(ActivationType, CLSID_WinRT) - ITEM_ENUM_NAME_FUNC(ActivationType, CLSID_CoCreateInstance) + ITEM_ENUM_NAME_FUNC(ActivationType, CoCreateInstance) END_ENUM_NAME_FUNC BOOL CALLBACK CheckForWinGetWindow(HWND hwnd, LPARAM param) @@ -136,6 +135,49 @@ namespace return result; } + + std::string GetBytesString(SIZE_T bytes) + { + constexpr SIZE_T s_kilo = 1024; + constexpr std::string_view s_sizes = "BKMG"sv; + size_t i = 0; + + while ((i + 1) < s_sizes.size() && bytes > s_kilo) + { + bytes /= s_kilo; + ++i; + } + + return std::to_string(bytes) + s_sizes[i]; + } + + const CLSID CLSID_PackageManager = { 0x2DDE4456, 0x64D9, 0x4673, 0x8F, 0x7E, 0xA4, 0xF1, 0x9A, 0x2E, 0x6C, 0xC3 }; // 2DDE4456-64D9-4673-8F7E-A4F19A2E6CC3 + const CLSID CLSID_FindPackagesOptions = { 0x96B9A53A, 0x9228, 0x4DA0, 0xB0, 0x13, 0xBB, 0x1B, 0x20, 0x31, 0xAB, 0x3D }; // 96B9A53A-9228-4DA0-B013-BB1B2031AB3D + const CLSID CLSID_CreateCompositePackageCatalogOptions = { 0x768318A6, 0x2EB5, 0x400D, 0x84, 0xD0, 0xDF, 0x35, 0x34, 0xC3, 0x0F, 0x5D }; // 768318A6-2EB5-400D-84D0-DF3534C30F5D + const CLSID CLSID_InstallOptions = { 0xE2AF3BA8, 0x8A88, 0x4766, 0x9D, 0xDA, 0xAE, 0x40, 0x13, 0xAD, 0xE2, 0x86 }; // E2AF3BA8-8A88-4766-9DDA-AE4013ADE286 + const CLSID CLSID_UninstallOptions = { 0x869CB959, 0xEB54, 0x425C, 0xA1, 0xE4, 0x1A, 0x1C, 0x29, 0x1C, 0x64, 0xE9 }; // 869CB959-EB54-425C-A1E4-1A1C291C64E9 + const CLSID CLSID_PackageMatchFilter = { 0x57DC8962, 0x7343, 0x42CD, 0xB9, 0x1C, 0x04, 0xF6, 0xA2, 0x5D, 0xB1, 0xD0 }; // 57DC8962-7343-42CD-B91C-04F6A25DB1D0 + const CLSID CLSID_PackageManagerSettings = { 0x80CF9D63, 0x5505, 0x4342, 0xB9, 0xB4, 0xBB, 0x87, 0x89, 0x5C, 0xA8, 0xBB }; // 80CF9D63-5505-4342-B9B4-BB87895CA8BB + const CLSID CLSID_DownloadOptions = { 0x4288DF96, 0xFDC9, 0x4B68, 0xB4, 0x03, 0x19, 0x3D, 0xBB, 0xF5, 0x6A, 0x24 }; // 4288DF96-FDC9-4B68-B403-193DBBF56A24 + const CLSID CLSID_AuthenticationArguments = { 0x8D593114, 0x1CF1, 0x43B9, 0x87, 0x22, 0x4D, 0xBB, 0x30, 0x10, 0x32, 0x96 }; // 8D593114-1CF1-43B9-8722-4DBB30103296 + const CLSID CLSID_RepairOptions = { 0x30c024c4, 0x852c, 0x4dd4, 0x98, 0x10, 0x13, 0x48, 0xc5, 0x1e, 0xf9, 0xbb }; // {30C024C4-852C-4DD4-9810-1348C51EF9BB} + const CLSID CLSID_AddPackageCatalogOptions = { 0x24e6f1fa, 0xe4c3, 0x4acd, 0x96, 0x5d, 0xdf, 0x21, 0x3f, 0xd5, 0x8f, 0x15 }; // {24E6F1FA-E4C3-4ACD-965D-DF213FD58F15} + const CLSID CLSID_RemovePackageCatalogOptions = { 0x1125d3a6, 0xe2ce, 0x479a, 0x91, 0xd5, 0x71, 0xa3, 0xf6, 0xf8, 0xb0, 0xb }; // {1125D3A6-E2CE-479A-91D5-71A3F6F8B00B} + + template + T CreatePackageManagerObject(ActivationType activationType, const CLSID& clsid) + { + if (ActivationType::ClassName == activationType) + { + return T{}; + } + else if (ActivationType::CoCreateInstance == activationType) + { + return winrt::create_instance(clsid); + } + + winrt::throw_hresult(E_UNEXPECTED); + } } TestParameters::TestParameters(int argc, const char** argv) @@ -176,9 +218,9 @@ TestParameters::TestParameters(int argc, const char** argv) ADVANCE_ARG_PARAMETER ActivationType = ParseActivationType(argv[i]); } - else if ("-clear-factories"sv == argv[i]) + else if ("-keep-factories"sv == argv[i]) { - ClearFactories = true; + SkipClearFactories = true; } } } @@ -188,9 +230,10 @@ void TestParameters::OutputDetails() const std::cout << "Running inproc testbed with:\n" " COM Init : " << ComInit << "\n" " Activate : " << ActivationType << "\n" - " Clear : " << std::boolalpha << ClearFactories << "\n" + " Clear : " << std::boolalpha << !SkipClearFactories << "\n" " Leak COM : " << std::boolalpha << LeakCOM << "\n" " Unload : " << UnloadBehavior << "\n" + " Expect : " << std::boolalpha << UnloadExpected() << "\n" " Test : " << TestToRun << "\n" " Package : " << PackageName << "\n" " Passes : " << Iterations << std::endl; @@ -249,13 +292,39 @@ void TestParameters::UninitializeTestState() const bool TestParameters::UnloadExpected() const { bool shouldUnload = true; - if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior) + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior || + (ActivationType::ClassName == ActivationType && SkipClearFactories)) { shouldUnload = false; } return shouldUnload; } +PackageManager TestParameters::CreatePackageManager() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_PackageManager); +} + +CreateCompositePackageCatalogOptions TestParameters::CreateCreateCompositePackageCatalogOptions() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_CreateCompositePackageCatalogOptions); +} + +PackageMatchFilter TestParameters::CreatePackageMatchFilter() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_PackageMatchFilter); +} + +FindPackagesOptions TestParameters::CreateFindPackagesOptions() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_FindPackagesOptions); +} + +DownloadOptions TestParameters::CreateDownloadOptions() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_DownloadOptions); +} + Snapshot::Snapshot() { const DWORD processId = GetCurrentProcessId(); @@ -321,7 +390,7 @@ bool UnloadAndCheckForLeaks::RunIteration() bool UnloadAndCheckForLeaks::RunFinal() { - constexpr std::streamsize s_columnWidth = 4; + constexpr std::streamsize s_columnWidth = 5; bool result = true; @@ -353,8 +422,52 @@ bool UnloadAndCheckForLeaks::RunFinal() std::cout << '\n'; // Modules + std::cout << "Module Count [Initial: " << m_initialSnapshot.ModuleCount << "]\n"; + + std::cout << "Iteration "; + for (size_t i = 0; i < m_iterationSnapshots.size(); ++i) + { + std::cout << std::setw(s_columnWidth) << (i + 1); + } + std::cout << '\n'; + + std::cout << "Pre Unload "; + for (const auto& snapshot : m_iterationSnapshots) + { + std::cout << std::setw(s_columnWidth) << snapshot.first.ModuleCount; + } + std::cout << '\n'; + + std::cout << "Post Unload"; + for (const auto& snapshot : m_iterationSnapshots) + { + std::cout << std::setw(s_columnWidth) << snapshot.second.ModuleCount; + } + std::cout << '\n'; // Memory + std::cout << "Private Usage [Initial: " << GetBytesString(m_initialSnapshot.Memory.PrivateUsage) << "]\n"; + + std::cout << "Iteration "; + for (size_t i = 0; i < m_iterationSnapshots.size(); ++i) + { + std::cout << std::setw(s_columnWidth) << (i + 1); + } + std::cout << '\n'; + + std::cout << "Pre Unload "; + for (const auto& snapshot : m_iterationSnapshots) + { + std::cout << std::setw(s_columnWidth) << GetBytesString(snapshot.first.Memory.PrivateUsage); + } + std::cout << '\n'; + + std::cout << "Post Unload"; + for (const auto& snapshot : m_iterationSnapshots) + { + std::cout << std::setw(s_columnWidth) << GetBytesString(snapshot.second.Memory.PrivateUsage); + } + std::cout << '\n'; return result; } diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index e1c4ddee4d..cb821fb946 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include +#include #include #include @@ -33,8 +34,7 @@ enum class UnloadBehavior enum class ActivationType { ClassName, - CLSID_WinRT, - CLSID_CoCreateInstance, + CoCreateInstance, }; // Test parameters from command line @@ -53,6 +53,12 @@ struct TestParameters // Determines if we expect COM to unload the module based on inputs. bool UnloadExpected() const; + winrt::Microsoft::Management::Deployment::PackageManager CreatePackageManager() const; + winrt::Microsoft::Management::Deployment::CreateCompositePackageCatalogOptions CreateCreateCompositePackageCatalogOptions() const; + winrt::Microsoft::Management::Deployment::PackageMatchFilter CreatePackageMatchFilter() const; + winrt::Microsoft::Management::Deployment::FindPackagesOptions CreateFindPackagesOptions() const; + winrt::Microsoft::Management::Deployment::DownloadOptions CreateDownloadOptions() const; + std::string TestToRun; ComInitializationType ComInit = ComInitializationType::MTA; bool LeakCOM = false; @@ -60,7 +66,7 @@ struct TestParameters std::string PackageName = "Microsoft.Edit"; UnloadBehavior UnloadBehavior = UnloadBehavior::Allow; ActivationType ActivationType = ActivationType::ClassName; - bool ClearFactories = true; + bool SkipClearFactories = false; }; // Catpures a snapshot of current resource usage. diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index 77099cab9b..b4c0ea8de4 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -19,12 +19,12 @@ int main(int argc, const char** argv) try for (int i = 0; i < testParameters.Iterations; ++i) { - if (!UsePackageManager(testParameters.PackageName)) + if (!UsePackageManager(testParameters)) { return 3; } - if (testParameters.ClearFactories) + if (!testParameters.SkipClearFactories) { winrt::clear_factory_cache(); } From fc5805fffd6dab5fb830feecd8699d6c1cff3ed9 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 2 Dec 2025 17:41:20 -0800 Subject: [PATCH 08/13] Allow for source update and start adding to E2E tests --- .github/actions/spelling/expect.txt | 4 + src/AppInstallerCLIE2ETests/Constants.cs | 1 + .../Helpers/TestCommon.cs | 27 ++++- .../Helpers/TestSetup.cs | 6 + .../Interop/InprocTestbedTests.cs | 77 +++++++++++++ src/ComInprocTestbed/PackageManager.cpp | 47 +++++++- src/ComInprocTestbed/PackageManager.h | 3 + src/ComInprocTestbed/Tests.cpp | 107 ++++++++++++++++-- src/ComInprocTestbed/Tests.h | 7 +- src/ComInprocTestbed/pch.h | 1 + 10 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 6462f1bfbc..f853b8f585 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -57,6 +57,7 @@ BFirst bigcatalog BITMAPINFOHEADER bitmask +BKMG bkup blargle blockedbypolicy @@ -333,6 +334,7 @@ MINORVERSION missingdependency mkgmtime MMmmbbbb +MODULEENTRY mof monicka MPNS @@ -426,6 +428,7 @@ positionals posix postuninstall powershellgallery +PPROCESS pri PRIMARYKEY processthreads @@ -555,6 +558,7 @@ tcs temppath testexampleinstaller thiscouldbeapc +THREADENTRY threehundred timespan Tlg diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index bdb50dddba..70de8b4e19 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -32,6 +32,7 @@ public class Constants public const string PowerShellModulePathParameter = "PowerShellModulePath"; public const string SkipTestSourceParameter = "SkipTestSource"; public const string ForcedExperimentalFeaturesParameter = "ForcedExperimentalFeatures"; + public const string InprocTestbedPathParameter = "InprocTestbedPath"; // Test Sources public const string DefaultWingetSourceName = @"winget"; diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 96ddd96f53..8d1576b47b 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -1162,22 +1162,23 @@ public static string CopyInstallerFileToARPInstallSourceDirectory(string install /// /// Run winget command via direct process. /// + /// The executable to run. /// Command to run. /// Parameters. /// Optional std in. /// Optional timeout. /// Throw on timeout. /// The result of the command. - private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout) + public static RunCommandResult RunProcess(string executablePath, string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout) { - RunCommandResult result = new (); + RunCommandResult result = new(); Process p = new Process(); - p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters); + p.StartInfo = new ProcessStartInfo(executablePath, command + ' ' + parameters); p.StartInfo.UseShellExecute = false; p.StartInfo.StandardOutputEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardOutput = true; - StringBuilder outputData = new (); + StringBuilder outputData = new(); p.OutputDataReceived += (sender, args) => { if (args.Data != null) @@ -1188,7 +1189,7 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command, p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardError = true; - StringBuilder errorData = new (); + StringBuilder errorData = new(); p.ErrorDataReceived += (sender, args) => { if (args.Data != null) @@ -1236,12 +1237,26 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command, } else if (throwOnTimeout) { - throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}"); + throw new TimeoutException($"Direct command run timed out: {command} {parameters}"); } return result; } + /// + /// Run winget command via direct process. + /// + /// Command to run. + /// Parameters. + /// Optional std in. + /// Optional timeout. + /// Throw on timeout. + /// The result of the command. + private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout) + { + return RunProcess(TestSetup.Parameters.AICLIPath, command, parameters, stdIn, timeOut, throwOnTimeout); + } + /// /// Run command result. /// diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs index a3ed192959..cf549b761c 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs @@ -45,6 +45,7 @@ private TestSetup() this.MsixInstallerPath = this.InitializeFileParam(Constants.MsixInstallerPathParameter); this.MsiInstallerV2Path = this.InitializeFileParam(Constants.MsiInstallerV2PathParameter); this.FontPath = this.InitializeFileParam(Constants.FontPathParameter); + this.InprocTestbedPath = this.InitializeFileParam(Constants.InprocTestbedPathParameter); this.ForcedExperimentalFeatures = this.InitializeStringArrayParam(Constants.ForcedExperimentalFeaturesParameter); } @@ -130,6 +131,11 @@ public static TestSetup Parameters /// public string PackageCertificatePath { get; } + /// + /// Gets the inproc testbed executable path. + /// + public string InprocTestbedPath { get; } + /// /// Gets a value indicating whether to skip creating test source. /// diff --git a/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs new file mode 100644 index 0000000000..80ec5fd230 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs @@ -0,0 +1,77 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System; + using System.IO; + using System.Reflection; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// + /// Tests that run the inproc testbed targeting COM lifetime. + /// + public class InprocTestbedTests + { + /// + /// Gets or sets the path to the inproc testbed executable. + /// + private string InprocTestbedPath { get; set; } + + /// + /// Setup done once before all the tests here. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + this.InprocTestbedPath = TestSetup.Parameters.InprocTestbedPath; + + if (string.IsNullOrWhiteSpace(this.InprocTestbedPath)) + { + string assemblyLocation = Assembly.GetExecutingAssembly().Location; + this.InprocTestbedPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), "..\\ComInprocTestbed\\ComInprocTestbed.exe"); + } + } + + /// + /// Executes the testbed as simply as possible to ensure integrations. + /// + [Test] + public void DefaultTest() + { + RunInprocTestbed(new TestbedParameters()); + } + + private enum ActivationType + { + ClassName, + CoCreateInstance + } + + private enum UnloadBehavior + { + Allow, + AtExit, + Never, + }; + + private class TestbedParameters + { + ActivationType? ActivationType { get; init; } = null; + bool? ClearFactories { get; init; } = null; + bool? LeakCOM { get; init; } = null; + UnloadBehavior? UnloadBehavior { get; init; } = null; + string Test { get; init; } = "unload_check"; + int? Iterations { get; init; } = null; + } + + private void RunInprocTestbed(TestbedParameters parameters) + { + + } + } +} diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp index 7673311f5d..41d7d3d8c1 100644 --- a/src/ComInprocTestbed/PackageManager.cpp +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -18,6 +18,16 @@ PackageCatalog Connect(const PackageCatalogReference& reference, std::string_vie return connectResult.PackageCatalog(); } +template +auto WaitForResult(Operation&& operation) +{ + std::promise promise; + auto future = promise.get_future(); + operation.Completed([&](const auto&, const auto&) { promise.set_value(); }); + future.wait(); + return operation.GetResults(); +} + bool UsePackageManager(const TestParameters& testParameters) { PackageManager packageManager = testParameters.CreatePackageManager(); @@ -34,9 +44,34 @@ bool UsePackageManager(const TestParameters& testParameters) CreateCompositePackageCatalogOptions options = testParameters.CreateCreateCompositePackageCatalogOptions(); options.CompositeSearchBehavior(CompositeSearchBehavior::RemotePackagesFromRemoteCatalogs); + auto sourceName = winrt::to_hstring(testParameters.SourceName); + for (PackageCatalogReference catalogRef : packageManager.GetPackageCatalogs()) { - options.Catalogs().Append(catalogRef); + if (sourceName.empty() || catalogRef.Info().Name() == sourceName) + { + options.Catalogs().Append(catalogRef); + } + } + + // Inputs are provided for a source that we did not find; add it. + if (!sourceName.empty() && !testParameters.SourceURL.empty() && options.Catalogs().Size() == 0) + { + AddPackageCatalogOptions addPackageCatalogOptions = testParameters.CreateAddPackageCatalogOptions(); + addPackageCatalogOptions.Name(sourceName); + addPackageCatalogOptions.SourceUri(winrt::to_hstring(testParameters.SourceURL)); + addPackageCatalogOptions.TrustLevel(PackageCatalogTrustLevel::Trusted); + + auto addCatalogResult = WaitForResult(packageManager.AddPackageCatalogAsync(addPackageCatalogOptions)); + + if (addCatalogResult.Status() != AddPackageCatalogStatus::Ok) + { + std::cout << "Adding catalog `" << testParameters.SourceName << "` [`" << testParameters.SourceURL << "`] got: " << static_cast(addCatalogResult.Status()) << " [" << addCatalogResult.ExtendedErrorCode() << "]\n"; + return false; + } + + // Get the new catalog + options.Catalogs().Append(packageManager.GetPackageCatalogByName(sourceName)); } auto compositeCatalog = Connect(packageManager.CreateCompositePackageCatalog(options), "Composite Catalog"); @@ -67,8 +102,7 @@ bool UsePackageManager(const TestParameters& testParameters) } DownloadOptions downloadOptions = testParameters.CreateDownloadOptions(); - auto downloadOperation = packageManager.DownloadPackageAsync(findResult.Matches().GetAt(0).CatalogPackage(), downloadOptions); - auto downloadResult = downloadOperation.get(); + auto downloadResult = WaitForResult(packageManager.DownloadPackageAsync(findResult.Matches().GetAt(0).CatalogPackage(), downloadOptions)); if (downloadResult.Status() != DownloadResultStatus::Ok) { @@ -79,6 +113,13 @@ bool UsePackageManager(const TestParameters& testParameters) return true; } +void InitializePackageManagerGlobals() +{ + PackageManagerSettings settings; + settings.SetCallerIdentifier(L"ComInprocTestbed"); + settings.SetStateIdentifier(L"ComInprocTestbed"); +} + void SetUnloadPreference(bool value) { PackageManagerSettings settings; diff --git a/src/ComInprocTestbed/PackageManager.h b/src/ComInprocTestbed/PackageManager.h index 31c22bef1c..6f0e3ce2d0 100644 --- a/src/ComInprocTestbed/PackageManager.h +++ b/src/ComInprocTestbed/PackageManager.h @@ -7,5 +7,8 @@ // Attempts to instantiate all static objects bool UsePackageManager(const TestParameters& testParameters); +// Sets up the globals for the test caller. +void InitializePackageManagerGlobals(); + // Sets the module to prevent it from unloading. void SetUnloadPreference(bool value); diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index 9af209b45f..b096e2b0a2 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -102,16 +102,24 @@ namespace // Look for the set of well known objects that should be present after we have spun everything up. // Returns true if all well known objects are found in the expected state. - bool SearchForWellKnownObjects(bool expectExist) + bool SearchForWellKnownObjects(bool expectExist, const Snapshot& snapshot) { bool result = true; + // Known modules in snapshot + bool knownModulesLoaded = snapshot.MicrosoftManagementDeploymentInProcLoaded && snapshot.WindowsPackageManagerLoaded; + if (knownModulesLoaded != expectExist) + { + std::cout << "Known modules were not in expected state [" << (expectExist ? "loaded" : "unloaded") << "]\n"; + result = false; + } + auto coreApplicationProperties = winrt::Windows::ApplicationModel::Core::CoreApplication::Properties(); // COM statics for (std::wstring_view item : { L"WindowsPackageManager.CachedInstalledIndex"sv, - L"WindowsPackageManager.TerminationSignalHandler"sv, + L"WindowsPackageManager.TerminationSignalHandler"sv, }) { bool present = coreApplicationProperties.HasKey(item); @@ -208,6 +216,16 @@ TestParameters::TestParameters(int argc, const char** argv) ADVANCE_ARG_PARAMETER PackageName = argv[i]; } + else if ("-src"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + SourceName = argv[i]; + } + else if ("-url"sv == argv[i]) + { + ADVANCE_ARG_PARAMETER + SourceURL = argv[i]; + } else if ("-unload"sv == argv[i]) { ADVANCE_ARG_PARAMETER @@ -230,12 +248,14 @@ void TestParameters::OutputDetails() const std::cout << "Running inproc testbed with:\n" " COM Init : " << ComInit << "\n" " Activate : " << ActivationType << "\n" - " Clear : " << std::boolalpha << !SkipClearFactories << "\n" + " Clear : " << std::boolalpha << !SkipClearFactories << "\n" " Leak COM : " << std::boolalpha << LeakCOM << "\n" " Unload : " << UnloadBehavior << "\n" - " Expect : " << std::boolalpha << UnloadExpected() << "\n" + " Expect : " << std::boolalpha << UnloadExpected() << "\n" " Test : " << TestToRun << "\n" " Package : " << PackageName << "\n" + " Source : " << SourceName << "\n" + " URL : " << SourceURL << "\n" " Passes : " << Iterations << std::endl; } @@ -258,6 +278,8 @@ bool TestParameters::InitializeTestState() const return false; } + InitializePackageManagerGlobals(); + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior) { SetUnloadPreference(false); @@ -325,6 +347,11 @@ DownloadOptions TestParameters::CreateDownloadOptions() const return CreatePackageManagerObject(ActivationType, CLSID_DownloadOptions); } +AddPackageCatalogOptions TestParameters::CreateAddPackageCatalogOptions() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_AddPackageCatalogOptions); +} + Snapshot::Snapshot() { const DWORD processId = GetCurrentProcessId(); @@ -353,6 +380,15 @@ Snapshot::Snapshot() { do { + if (moduleEntry.szModule == L"Microsoft.Management.Deployment.InProc.dll"sv) + { + MicrosoftManagementDeploymentInProcLoaded = true; + } + else if (moduleEntry.szModule == L"WindowsPackageManager.dll"sv) + { + WindowsPackageManagerLoaded = true; + } + ++ModuleCount; } while (Module32Next(snapshot, &moduleEntry)); } @@ -369,18 +405,18 @@ UnloadAndCheckForLeaks::UnloadAndCheckForLeaks(const TestParameters& parameters) bool UnloadAndCheckForLeaks::RunIteration() { - if (!SearchForWellKnownObjects(true)) + Snapshot beforeUnload; + if (!SearchForWellKnownObjects(true, beforeUnload)) { return false; } - Snapshot beforeUnload; - CoFreeUnusedLibrariesEx(0, 0); - m_iterationSnapshots.emplace_back(beforeUnload, Snapshot{}); + Snapshot afterUnload; + m_iterationSnapshots.emplace_back(beforeUnload, afterUnload); - if (!SearchForWellKnownObjects(!m_parameters.UnloadExpected())) + if (!SearchForWellKnownObjects(!m_parameters.UnloadExpected(), afterUnload)) { return false; } @@ -397,7 +433,7 @@ bool UnloadAndCheckForLeaks::RunFinal() std::cout << "--- UnloadAndCheckForLeaks results ---\n"; std::cout << std::setfill(' '); - // Threads + // --- Threads --- std::cout << "Thread Count [Initial: " << m_initialSnapshot.ThreadCount << "]\n"; std::cout << "Iteration "; @@ -421,7 +457,34 @@ bool UnloadAndCheckForLeaks::RunFinal() } std::cout << '\n'; - // Modules + // Look for consistent increase in measured values + if (m_iterationSnapshots.size() > 1) + { + size_t previousValue = m_iterationSnapshots[0].second.ThreadCount; + bool consistentIncrease = true; + + for (size_t i = 1; i < m_iterationSnapshots.size(); ++i) + { + size_t currentValue = m_iterationSnapshots[i].second.ThreadCount; + if (currentValue > previousValue) + { + previousValue = currentValue; + } + else + { + consistentIncrease = false; + break; + } + } + + if (consistentIncrease) + { + std::cout << "Post unload thread count shows consistent increase; failing test.\n"; + result = false; + } + } + + // --- Modules --- std::cout << "Module Count [Initial: " << m_initialSnapshot.ModuleCount << "]\n"; std::cout << "Iteration "; @@ -445,7 +508,27 @@ bool UnloadAndCheckForLeaks::RunFinal() } std::cout << '\n'; - // Memory + // Look for modules not unloading + if (m_parameters.UnloadExpected() && m_iterationSnapshots.size() > 1) + { + bool noUnloadFound = false; + + for (size_t i = 0; i < m_iterationSnapshots.size(); ++i) + { + if (m_iterationSnapshots[i].first.ModuleCount == m_iterationSnapshots[i].second.ModuleCount) + { + noUnloadFound = true; + } + } + + if (noUnloadFound) + { + std::cout << "Module count did not decrease during at least one iteration; failing test.\n"; + result = false; + } + } + + // --- Memory --- std::cout << "Private Usage [Initial: " << GetBytesString(m_initialSnapshot.Memory.PrivateUsage) << "]\n"; std::cout << "Iteration "; diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index cb821fb946..2a5a740daa 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -58,24 +58,29 @@ struct TestParameters winrt::Microsoft::Management::Deployment::PackageMatchFilter CreatePackageMatchFilter() const; winrt::Microsoft::Management::Deployment::FindPackagesOptions CreateFindPackagesOptions() const; winrt::Microsoft::Management::Deployment::DownloadOptions CreateDownloadOptions() const; + winrt::Microsoft::Management::Deployment::AddPackageCatalogOptions CreateAddPackageCatalogOptions() const; std::string TestToRun; ComInitializationType ComInit = ComInitializationType::MTA; bool LeakCOM = false; int Iterations = 1; std::string PackageName = "Microsoft.Edit"; + std::string SourceName = "winget"; + std::string SourceURL; UnloadBehavior UnloadBehavior = UnloadBehavior::Allow; ActivationType ActivationType = ActivationType::ClassName; bool SkipClearFactories = false; }; -// Catpures a snapshot of current resource usage. +// Captures a snapshot of current resource usage. struct Snapshot { Snapshot(); size_t ThreadCount = 0; size_t ModuleCount = 0; + bool MicrosoftManagementDeploymentInProcLoaded = false; + bool WindowsPackageManagerLoaded = false; PROCESS_MEMORY_COUNTERS_EX2 Memory{}; }; diff --git a/src/ComInprocTestbed/pch.h b/src/ComInprocTestbed/pch.h index 3810d4f9cf..e6b4a93660 100644 --- a/src/ComInprocTestbed/pch.h +++ b/src/ComInprocTestbed/pch.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include From ff642195a88eabc93ebced4249bbfb3046686292 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 4 Dec 2025 13:22:06 -0800 Subject: [PATCH 09/13] More tests work, added new test that is not quite done --- .../Helpers/TestCommon.cs | 24 ++-- .../InprocTestbedTests.cs | 134 ++++++++++++++++++ .../Interop/InprocTestbedTests.cs | 77 ---------- src/ComInprocTestbed/PackageManager.cpp | 132 +++++++++++++---- src/ComInprocTestbed/PackageManager.h | 6 + src/ComInprocTestbed/Tests.cpp | 62 ++++++-- src/ComInprocTestbed/Tests.h | 27 +++- src/ComInprocTestbed/main.cpp | 6 +- 8 files changed, 336 insertions(+), 132 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/InprocTestbedTests.cs delete mode 100644 src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 8d1576b47b..2b99865b68 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -126,15 +126,6 @@ public static RunCommandResult RunAICLICommand(string command, string parameters } } - string inputMsg = - "AICLI path: " + TestSetup.Parameters.AICLIPath + - " Command: " + command + - " Parameters: " + parameters + correlationParameter + - (string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) + - " Timeout: " + timeOut; - - TestContext.Out.WriteLine($"Starting command run. {inputMsg}"); - return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout); } @@ -1171,14 +1162,23 @@ public static string CopyInstallerFileToARPInstallSourceDirectory(string install /// The result of the command. public static RunCommandResult RunProcess(string executablePath, string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout) { - RunCommandResult result = new(); + string inputMsg = + "Exe path: " + executablePath + + " Command: " + command + + " Parameters: " + parameters + + (string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) + + " Timeout: " + timeOut; + + TestContext.Out.WriteLine($"Starting command run. {inputMsg}"); + + RunCommandResult result = new (); Process p = new Process(); p.StartInfo = new ProcessStartInfo(executablePath, command + ' ' + parameters); p.StartInfo.UseShellExecute = false; p.StartInfo.StandardOutputEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardOutput = true; - StringBuilder outputData = new(); + StringBuilder outputData = new (); p.OutputDataReceived += (sender, args) => { if (args.Data != null) @@ -1189,7 +1189,7 @@ public static RunCommandResult RunProcess(string executablePath, string command, p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardError = true; - StringBuilder errorData = new(); + StringBuilder errorData = new (); p.ErrorDataReceived += (sender, args) => { if (args.Data != null) diff --git a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs new file mode 100644 index 0000000000..882bd825ba --- /dev/null +++ b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using System.Reflection; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// + /// Tests that run the inproc testbed targeting COM lifetime. + /// + public class InprocTestbedTests + { + /// + /// The activation type to use when creating objects. + /// + private enum ActivationType + { + ClassName, + CoCreateInstance, + } + + /// + /// Control when the module will allow signal that it can be unloaded if all objects are released. + /// + private enum UnloadBehavior + { + Allow, + AtExit, + Never, + } + + /// + /// Gets or sets the path to the inproc testbed executable. + /// + private string InprocTestbedPath { get; set; } + + /// + /// Gets or sets the string that contains the package identity to use for the tests. + /// + private string TargetPackageInformation { get; set; } + + /// + /// Setup done once before all the tests here. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + this.InprocTestbedPath = TestSetup.Parameters.InprocTestbedPath; + + if (string.IsNullOrWhiteSpace(this.InprocTestbedPath)) + { + string assemblyLocation = Assembly.GetExecutingAssembly().Location; + this.InprocTestbedPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), "..\\ComInprocTestbed\\ComInprocTestbed.exe"); + } + + // If we are using the test source, target a package in it + if (!TestSetup.Parameters.SkipTestSource) + { + this.TargetPackageInformation = $"-pkg {Constants.ExeInstallerPackageId} -src {Constants.TestSourceName} -url {Constants.TestSourceUrl}"; + } + } + + /// + /// Executes the testbed as simply as possible to ensure integrations. + /// + [Test] + public void DefaultTest() + { + this.RunInprocTestbed(new TestbedParameters()); + } + + private void RunInprocTestbed(TestbedParameters parameters, int timeout = 300000) + { + string builtParameters = string.Empty; + + if (parameters.ActivationType != null) + { + builtParameters += $"-activation {parameters.ActivationType} "; + } + + if (!parameters.ClearFactories) + { + builtParameters += "-keep-factories "; + } + + if (parameters.LeakCOM) + { + builtParameters += "-leak-com "; + } + + if (parameters.UnloadBehavior != null) + { + builtParameters += $"-unload {parameters.ActivationType} "; + } + + if (parameters.Test != null) + { + builtParameters += $"-test {parameters.Test} "; + } + + if (parameters.Iterations != null) + { + builtParameters += $"-itr {parameters.Iterations} "; + } + + var result = TestCommon.RunProcess(this.InprocTestbedPath, this.TargetPackageInformation, builtParameters, null, timeout, true); + Assert.AreEqual(0, result.ExitCode); + } + + /// + /// The parameters to provide for running tests. + /// + private class TestbedParameters + { + internal ActivationType? ActivationType { get; init; } = null; + + internal bool ClearFactories { get; init; } = true; + + internal bool LeakCOM { get; init; } = false; + + internal UnloadBehavior? UnloadBehavior { get; init; } = null; + + internal string Test { get; init; } = "unload_check"; + + internal int? Iterations { get; init; } = null; + } + } +} diff --git a/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs deleted file mode 100644 index 80ec5fd230..0000000000 --- a/src/AppInstallerCLIE2ETests/Interop/InprocTestbedTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ----------------------------------------------------------------------------- -// -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// -// ----------------------------------------------------------------------------- - -namespace AppInstallerCLIE2ETests -{ - using System; - using System.IO; - using System.Reflection; - using AppInstallerCLIE2ETests.Helpers; - using NUnit.Framework; - - /// - /// Tests that run the inproc testbed targeting COM lifetime. - /// - public class InprocTestbedTests - { - /// - /// Gets or sets the path to the inproc testbed executable. - /// - private string InprocTestbedPath { get; set; } - - /// - /// Setup done once before all the tests here. - /// - [OneTimeSetUp] - public void OneTimeSetup() - { - this.InprocTestbedPath = TestSetup.Parameters.InprocTestbedPath; - - if (string.IsNullOrWhiteSpace(this.InprocTestbedPath)) - { - string assemblyLocation = Assembly.GetExecutingAssembly().Location; - this.InprocTestbedPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), "..\\ComInprocTestbed\\ComInprocTestbed.exe"); - } - } - - /// - /// Executes the testbed as simply as possible to ensure integrations. - /// - [Test] - public void DefaultTest() - { - RunInprocTestbed(new TestbedParameters()); - } - - private enum ActivationType - { - ClassName, - CoCreateInstance - } - - private enum UnloadBehavior - { - Allow, - AtExit, - Never, - }; - - private class TestbedParameters - { - ActivationType? ActivationType { get; init; } = null; - bool? ClearFactories { get; init; } = null; - bool? LeakCOM { get; init; } = null; - UnloadBehavior? UnloadBehavior { get; init; } = null; - string Test { get; init; } = "unload_check"; - int? Iterations { get; init; } = null; - } - - private void RunInprocTestbed(TestbedParameters parameters) - { - - } - } -} diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp index 41d7d3d8c1..e1911f38f2 100644 --- a/src/ComInprocTestbed/PackageManager.cpp +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -5,19 +5,7 @@ using namespace winrt::Microsoft::Management::Deployment; -PackageCatalog Connect(const PackageCatalogReference& reference, std::string_view name) -{ - auto connectResult = reference.Connect(); - - if (connectResult.Status() != ConnectResultStatus::Ok) - { - std::cout << "Connecting to " << name << " got: " << static_cast(connectResult.Status()) << " [" << connectResult.ExtendedErrorCode() << "]\n"; - return nullptr; - } - - return connectResult.PackageCatalog(); -} - +// Work around the assertions about waiting on an STA thread from C++/WinRT template auto WaitForResult(Operation&& operation) { @@ -28,21 +16,24 @@ auto WaitForResult(Operation&& operation) return operation.GetResults(); } -bool UsePackageManager(const TestParameters& testParameters) +PackageCatalog Connect(const PackageCatalogReference& reference, std::string_view name) { - PackageManager packageManager = testParameters.CreatePackageManager(); + auto connectResult = reference.Connect(); - // Force installed cache to be created - auto installedCatalogRef = packageManager.GetLocalPackageCatalog(LocalPackageCatalog::InstalledPackages); - auto installedCatalog = Connect(installedCatalogRef, "Installed Catalog"); - if (!installedCatalog) + if (connectResult.Status() != ConnectResultStatus::Ok) { - return false; + std::cout << "Connecting to " << name << " got: " << static_cast(connectResult.Status()) << " [" << connectResult.ExtendedErrorCode() << "]\n"; + return nullptr; } - // Force TerminationSignalHandler to be created + return connectResult.PackageCatalog(); +} + +PackageCatalog ConnectComposite(const PackageManager& packageManager, const TestParameters& testParameters, CompositeSearchBehavior searchBehavior, PackageInstallScope scope = PackageInstallScope::Any) +{ CreateCompositePackageCatalogOptions options = testParameters.CreateCreateCompositePackageCatalogOptions(); - options.CompositeSearchBehavior(CompositeSearchBehavior::RemotePackagesFromRemoteCatalogs); + options.InstalledScope(scope); + options.CompositeSearchBehavior(searchBehavior); auto sourceName = winrt::to_hstring(testParameters.SourceName); @@ -67,19 +58,18 @@ bool UsePackageManager(const TestParameters& testParameters) if (addCatalogResult.Status() != AddPackageCatalogStatus::Ok) { std::cout << "Adding catalog `" << testParameters.SourceName << "` [`" << testParameters.SourceURL << "`] got: " << static_cast(addCatalogResult.Status()) << " [" << addCatalogResult.ExtendedErrorCode() << "]\n"; - return false; + return nullptr; } // Get the new catalog options.Catalogs().Append(packageManager.GetPackageCatalogByName(sourceName)); } - auto compositeCatalog = Connect(packageManager.CreateCompositePackageCatalog(options), "Composite Catalog"); - if (!compositeCatalog) - { - return false; - } + return Connect(packageManager.CreateCompositePackageCatalog(options), "Composite Catalog"); +} +CatalogPackage FindPackage(const PackageCatalog& compositeCatalog, const TestParameters& testParameters) +{ PackageMatchFilter filter = testParameters.CreatePackageMatchFilter(); filter.Field(PackageMatchField::Id); filter.Option(PackageFieldMatchOption::EqualsCaseInsensitive); @@ -92,17 +82,45 @@ bool UsePackageManager(const TestParameters& testParameters) if (findResult.Status() != FindPackagesResultStatus::Ok) { std::cout << "Finding package " << testParameters.PackageName << " got: " << static_cast(findResult.Status()) << " [" << findResult.ExtendedErrorCode() << "]\n"; - return false; + return nullptr; } if (findResult.Matches().Size() != 1) { std::cout << "Finding package " << testParameters.PackageName << " got " << findResult.Matches().Size() << " results.\n"; + return nullptr; + } + + return findResult.Matches().GetAt(0).CatalogPackage(); +} + +bool UsePackageManager(const TestParameters& testParameters) +{ + PackageManager packageManager = testParameters.CreatePackageManager(); + + // Force installed cache to be created + auto installedCatalogRef = packageManager.GetLocalPackageCatalog(LocalPackageCatalog::InstalledPackages); + auto installedCatalog = Connect(installedCatalogRef, "Installed Catalog"); + if (!installedCatalog) + { + return false; + } + + // Force TerminationSignalHandler to be created + auto compositeCatalog = ConnectComposite(packageManager, testParameters, CompositeSearchBehavior::RemotePackagesFromRemoteCatalogs); + if (!compositeCatalog) + { + return false; + } + + auto package = FindPackage(compositeCatalog, testParameters); + if (!package) + { return false; } DownloadOptions downloadOptions = testParameters.CreateDownloadOptions(); - auto downloadResult = WaitForResult(packageManager.DownloadPackageAsync(findResult.Matches().GetAt(0).CatalogPackage(), downloadOptions)); + auto downloadResult = WaitForResult(packageManager.DownloadPackageAsync(package, downloadOptions)); if (downloadResult.Status() != DownloadResultStatus::Ok) { @@ -125,3 +143,57 @@ void SetUnloadPreference(bool value) PackageManagerSettings settings; settings.CanUnloadPreference(value); } + +bool DetectForSystem(const TestParameters& testParameters) +{ + PackageManager packageManager = testParameters.CreatePackageManager(); + + auto compositeCatalog = ConnectComposite(packageManager, testParameters, CompositeSearchBehavior::RemotePackagesFromAllCatalogs, PackageInstallScope::SystemOrUnknown); + if (!compositeCatalog) + { + winrt::throw_hresult(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + auto package = FindPackage(compositeCatalog, testParameters); + if (!package) + { + winrt::throw_hresult(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + auto installStatus = package.CheckInstalledStatus(); + // ??? Determine detection mechanism + + return true; +} + +bool InstallForSystem(const TestParameters& testParameters) +{ + PackageManager packageManager = testParameters.CreatePackageManager(); + + auto compositeCatalog = ConnectComposite(packageManager, testParameters, CompositeSearchBehavior::RemotePackagesFromAllCatalogs, PackageInstallScope::SystemOrUnknown); + if (!compositeCatalog) + { + winrt::throw_hresult(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + auto package = FindPackage(compositeCatalog, testParameters); + if (!package) + { + winrt::throw_hresult(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + InstallOptions options = testParameters.CreateInstallOptions(); + options.AcceptPackageAgreements(true); + options.BypassIsStoreClientBlockedPolicyCheck(true); + options.Force(true); + options.PackageInstallScope(PackageInstallScope::SystemOrUnknown); + auto installResult = WaitForResult(packageManager.InstallPackageAsync(package, options)); + + if (installResult.Status() != InstallResultStatus::Ok) + { + std::cout << "Installing package " << testParameters.PackageName << " got: " << static_cast(installResult.Status()) << " [" << installResult.ExtendedErrorCode() << "] [" << installResult.InstallerErrorCode() << "]\n"; + return false; + } + + return true; +} diff --git a/src/ComInprocTestbed/PackageManager.h b/src/ComInprocTestbed/PackageManager.h index 6f0e3ce2d0..f589d21371 100644 --- a/src/ComInprocTestbed/PackageManager.h +++ b/src/ComInprocTestbed/PackageManager.h @@ -12,3 +12,9 @@ void InitializePackageManagerGlobals(); // Sets the module to prevent it from unloading. void SetUnloadPreference(bool value); + +// Attempts to detect the target package as installed for the system. +bool DetectForSystem(const TestParameters& testParameters); + +// Installs the target package for the system. +bool InstallForSystem(const TestParameters& testParameters); diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index b096e2b0a2..82ad80b418 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -86,15 +86,21 @@ namespace { bool* result = reinterpret_cast(param); - int textLength = GetWindowTextLengthW(hwnd) + 1; - std::wstring windowText(textLength, '\0'); - textLength = GetWindowTextW(hwnd, &windowText[0], textLength); - windowText.resize(textLength); + DWORD windowProcessId = 0; + GetWindowThreadProcessId(hwnd, &windowProcessId); - if (L"WingetMessageOnlyWindow"sv == windowText) + if (GetCurrentProcessId() == windowProcessId) { - *result = true; - return FALSE; + int textLength = GetWindowTextLengthW(hwnd) + 1; + std::wstring windowText(textLength, '\0'); + textLength = GetWindowTextW(hwnd, &windowText[0], textLength); + windowText.resize(textLength); + + if (L"WingetMessageOnlyWindow"sv == windowText) + { + *result = true; + return FALSE; + } } return TRUE; @@ -294,6 +300,10 @@ std::unique_ptr TestParameters::CreateTest() const { return std::make_unique(*this); } + else if ("install_detect"sv == TestToRun) + { + return std::make_unique(*this); + } return {}; } @@ -352,6 +362,11 @@ AddPackageCatalogOptions TestParameters::CreateAddPackageCatalogOptions() const return CreatePackageManagerObject(ActivationType, CLSID_AddPackageCatalogOptions); } +InstallOptions TestParameters::CreateInstallOptions() const +{ + return CreatePackageManagerObject(ActivationType, CLSID_InstallOptions); +} + Snapshot::Snapshot() { const DWORD processId = GetCurrentProcessId(); @@ -403,8 +418,16 @@ UnloadAndCheckForLeaks::UnloadAndCheckForLeaks(const TestParameters& parameters) { } -bool UnloadAndCheckForLeaks::RunIteration() +bool UnloadAndCheckForLeaks::RunIterationWork() +{ + std::cout << "UnloadAndCheckForLeaks::RunIterationWork\n"; + return UsePackageManager(m_parameters); +} + +bool UnloadAndCheckForLeaks::RunIterationTest() { + std::cout << "UnloadAndCheckForLeaks::RunIterationTest\n"; + Snapshot beforeUnload; if (!SearchForWellKnownObjects(true, beforeUnload)) { @@ -554,3 +577,26 @@ bool UnloadAndCheckForLeaks::RunFinal() return result; } + +InstallForSystem_DetectPresence::InstallForSystem_DetectPresence(const TestParameters& parameters) : m_parameters(parameters) +{ +} + +bool InstallForSystem_DetectPresence::RunIterationWork() +{ + std::cout << "Before installing, the detection state was: " << std::boolalpha << DetectForSystem(m_parameters) << '\n'; + + return InstallForSystem(m_parameters); +} + +bool InstallForSystem_DetectPresence::RunIterationTest() +{ + bool result = DetectForSystem(m_parameters); + std::cout << "After installing, the detection state was: " << std::boolalpha << result << '\n'; + return result; +} + +bool InstallForSystem_DetectPresence::RunFinal() +{ + return true; +} diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index 2a5a740daa..20224afe92 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -11,8 +11,11 @@ struct ITest { virtual ~ITest() = default; - // Runs an iteration of the test. - virtual bool RunIteration() = 0; + // Runs an iteration of the work. Performed at the beginning of the iteration. + virtual bool RunIterationWork() = 0; + + // Runs an iteration of the test. Performed at the end of the iteration. + virtual bool RunIterationTest() = 0; // Performs the final test validation. virtual bool RunFinal() = 0; @@ -59,6 +62,7 @@ struct TestParameters winrt::Microsoft::Management::Deployment::FindPackagesOptions CreateFindPackagesOptions() const; winrt::Microsoft::Management::Deployment::DownloadOptions CreateDownloadOptions() const; winrt::Microsoft::Management::Deployment::AddPackageCatalogOptions CreateAddPackageCatalogOptions() const; + winrt::Microsoft::Management::Deployment::InstallOptions CreateInstallOptions() const; std::string TestToRun; ComInitializationType ComInit = ComInitializationType::MTA; @@ -89,7 +93,9 @@ struct UnloadAndCheckForLeaks : public ITest { UnloadAndCheckForLeaks(const TestParameters& parameters); - bool RunIteration() override; + bool RunIterationWork() override; + + bool RunIterationTest() override; bool RunFinal() override; @@ -98,3 +104,18 @@ struct UnloadAndCheckForLeaks : public ITest Snapshot m_initialSnapshot; std::vector> m_iterationSnapshots; }; + +// A test that installs the package machine wide and then attempts to detect that it is installed. +struct InstallForSystem_DetectPresence : public ITest +{ + InstallForSystem_DetectPresence(const TestParameters& parameters); + + bool RunIterationWork() override; + + bool RunIterationTest() override; + + bool RunFinal() override; + +private: + const TestParameters& m_parameters; +}; diff --git a/src/ComInprocTestbed/main.cpp b/src/ComInprocTestbed/main.cpp index b4c0ea8de4..84cee642d8 100644 --- a/src/ComInprocTestbed/main.cpp +++ b/src/ComInprocTestbed/main.cpp @@ -19,7 +19,9 @@ int main(int argc, const char** argv) try for (int i = 0; i < testParameters.Iterations; ++i) { - if (!UsePackageManager(testParameters)) + std::cout << "Begin iteration " << (i + 1) << std::endl; + + if (test && !test->RunIterationWork()) { return 3; } @@ -29,7 +31,7 @@ int main(int argc, const char** argv) try winrt::clear_factory_cache(); } - if (test && !test->RunIteration()) + if (test && !test->RunIterationTest()) { return 4; } From 32109124df5f4b414015aa159cfc99ed9fc346e4 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 5 Dec 2025 11:06:19 -0800 Subject: [PATCH 10/13] Add test invocations and fix some test things --- .../InprocTestbedTests.cs | 75 ++++++++++++++++++- src/ComInprocTestbed/PackageManager.cpp | 5 +- src/ComInprocTestbed/Tests.cpp | 16 ++-- src/ComInprocTestbed/Tests.h | 2 +- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs index 882bd825ba..f7e6276b3c 100644 --- a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs +++ b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs @@ -19,19 +19,38 @@ public class InprocTestbedTests /// /// The activation type to use when creating objects. /// - private enum ActivationType + public enum ActivationType { + /// + /// Use the WinRT type name for activation via C++/WinRT object construction. + /// ClassName, + + /// + /// Use the CLSID for activatino via C++/WinRT `create_instance`. + /// CoCreateInstance, } /// /// Control when the module will allow signal that it can be unloaded if all objects are released. + /// This does not affect the loader by taking additional references to the module. /// - private enum UnloadBehavior + public enum UnloadBehavior { + /// + /// Allows the unload check function to proceed with object count checks and unload when possible. + /// Allow, - AtExit, + + /// + /// Prevents the unload check until just before COM is uninitialized. + /// + AtUninitialize, + + /// + /// Prevents the unload check at all times. + /// Never, } @@ -75,6 +94,54 @@ public void DefaultTest() this.RunInprocTestbed(new TestbedParameters()); } + /// + /// Tests using the CLSID with CoCreateInstance. + /// + /// Control whether COM should be uninitialized at the end of the process. + /// Set the unload behavior for the test. + [Test] + [TestCase(false, UnloadBehavior.AtUninitialize)] + [TestCase(false, UnloadBehavior.Never)] + [TestCase(true, UnloadBehavior.Allow)] + [TestCase(true, UnloadBehavior.Never)] + public void CLSID_Tests(bool leakCOM, UnloadBehavior unloadBehavior) + { + this.RunInprocTestbed(new TestbedParameters() + { + ActivationType = ActivationType.CoCreateInstance, + LeakCOM = leakCOM, + UnloadBehavior = unloadBehavior, + Iterations = 10, + }); + } + + /// + /// Tests using the C++/WinRT ideomatic activation through the type name. + /// + /// Control whether the C++/WinRT factory cache will be cleared between iterations. + /// Control whether COM should be uninitialized at the end of the process. + /// Set the unload behavior for the test. + [Test] + [TestCase(false, false, UnloadBehavior.AtUninitialize)] + [TestCase(false, false, UnloadBehavior.Never)] + [TestCase(false, true, UnloadBehavior.Allow)] + [TestCase(false, true, UnloadBehavior.Never)] + [TestCase(true, false, UnloadBehavior.AtUninitialize)] + [TestCase(true, false, UnloadBehavior.Never)] + [TestCase(true, true, UnloadBehavior.Allow)] + [TestCase(true, true, UnloadBehavior.Never)] + public void TypeName_Tests(bool freeCachedFactories, bool leakCOM, UnloadBehavior unloadBehavior) + { + this.RunInprocTestbed(new TestbedParameters() + { + ActivationType = ActivationType.ClassName, + ClearFactories = freeCachedFactories, + LeakCOM = leakCOM, + UnloadBehavior = unloadBehavior, + Iterations = 10, + }); + } + private void RunInprocTestbed(TestbedParameters parameters, int timeout = 300000) { string builtParameters = string.Empty; @@ -96,7 +163,7 @@ private void RunInprocTestbed(TestbedParameters parameters, int timeout = 300000 if (parameters.UnloadBehavior != null) { - builtParameters += $"-unload {parameters.ActivationType} "; + builtParameters += $"-unload {parameters.UnloadBehavior} "; } if (parameters.Test != null) diff --git a/src/ComInprocTestbed/PackageManager.cpp b/src/ComInprocTestbed/PackageManager.cpp index e1911f38f2..bb4e0dc98a 100644 --- a/src/ComInprocTestbed/PackageManager.cpp +++ b/src/ComInprocTestbed/PackageManager.cpp @@ -160,10 +160,7 @@ bool DetectForSystem(const TestParameters& testParameters) winrt::throw_hresult(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } - auto installStatus = package.CheckInstalledStatus(); - // ??? Determine detection mechanism - - return true; + return package.DefaultInstallVersion() && package.InstalledVersion(); } bool InstallForSystem(const TestParameters& testParameters) diff --git a/src/ComInprocTestbed/Tests.cpp b/src/ComInprocTestbed/Tests.cpp index 82ad80b418..0291b2a127 100644 --- a/src/ComInprocTestbed/Tests.cpp +++ b/src/ComInprocTestbed/Tests.cpp @@ -62,13 +62,13 @@ namespace BEGIN_ENUM_PARSE_FUNC(UnloadBehavior) ITEM_ENUM_PARSE_FUNC(UnloadBehavior, Allow) - ITEM_ENUM_PARSE_FUNC(UnloadBehavior, AtExit) + ITEM_ENUM_PARSE_FUNC(UnloadBehavior, AtUninitialize) ITEM_ENUM_PARSE_FUNC(UnloadBehavior, Never) END_ENUM_PARSE_FUNC BEGIN_ENUM_NAME_FUNC(UnloadBehavior) ITEM_ENUM_NAME_FUNC(UnloadBehavior, Allow) - ITEM_ENUM_NAME_FUNC(UnloadBehavior, AtExit) + ITEM_ENUM_NAME_FUNC(UnloadBehavior, AtUninitialize) ITEM_ENUM_NAME_FUNC(UnloadBehavior, Never) END_ENUM_NAME_FUNC @@ -286,7 +286,7 @@ bool TestParameters::InitializeTestState() const InitializePackageManagerGlobals(); - if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior) + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtUninitialize == UnloadBehavior) { SetUnloadPreference(false); } @@ -310,21 +310,21 @@ std::unique_ptr TestParameters::CreateTest() const void TestParameters::UninitializeTestState() const { - if (!LeakCOM) + if (UnloadBehavior::AtUninitialize == UnloadBehavior) { - RoUninitialize(); + SetUnloadPreference(true); } - if (UnloadBehavior::AtExit == UnloadBehavior) + if (!LeakCOM) { - SetUnloadPreference(true); + RoUninitialize(); } } bool TestParameters::UnloadExpected() const { bool shouldUnload = true; - if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtExit == UnloadBehavior || + if (UnloadBehavior::Never == UnloadBehavior || UnloadBehavior::AtUninitialize == UnloadBehavior || (ActivationType::ClassName == ActivationType && SkipClearFactories)) { shouldUnload = false; diff --git a/src/ComInprocTestbed/Tests.h b/src/ComInprocTestbed/Tests.h index 20224afe92..1443887202 100644 --- a/src/ComInprocTestbed/Tests.h +++ b/src/ComInprocTestbed/Tests.h @@ -30,7 +30,7 @@ enum class ComInitializationType enum class UnloadBehavior { Allow, - AtExit, + AtUninitialize, Never, }; From 1882f6c0d2ec6ebd545b856871143c5e66668d99 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 5 Dec 2025 12:11:16 -0800 Subject: [PATCH 11/13] Spelling and proj update --- src/AppInstallerCLIE2ETests/InprocTestbedTests.cs | 4 ++-- src/ComInprocTestbed/ComInprocTestbed.vcxproj | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs index f7e6276b3c..db0a95d7ed 100644 --- a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs +++ b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs @@ -27,7 +27,7 @@ public enum ActivationType ClassName, /// - /// Use the CLSID for activatino via C++/WinRT `create_instance`. + /// Use the CLSID for activation via C++/WinRT `create_instance`. /// CoCreateInstance, } @@ -116,7 +116,7 @@ public void CLSID_Tests(bool leakCOM, UnloadBehavior unloadBehavior) } /// - /// Tests using the C++/WinRT ideomatic activation through the type name. + /// Tests using C++/WinRT object activation through the type name. /// /// Control whether the C++/WinRT factory cache will be cleared between iterations. /// Control whether COM should be uninitialized at the end of the process. diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj b/src/ComInprocTestbed/ComInprocTestbed.vcxproj index 6a36e82768..013223c486 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj @@ -39,11 +39,6 @@ Application - v140 - v141 - v142 - v143 - Unicode true From 41fdf502dfd45494436a96446142fb9e8647529f Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 8 Dec 2025 16:48:12 -0800 Subject: [PATCH 12/13] Copy testbed in pipeline; initialize signal handler in server process; use explicit test package parameter in tests --- azure-pipelines.yml | 1 + src/AppInstallerCLICore/Public/ShutdownMonitoring.h | 2 +- src/AppInstallerCLICore/ShutdownMonitoring.cpp | 9 ++++++++- src/AppInstallerCLIE2ETests/Constants.cs | 1 + src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs | 6 ++++++ src/AppInstallerCLIE2ETests/InprocTestbedTests.cs | 5 ++--- templates/e2e-test.template.yml | 2 ++ 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 94ccd14251..c62c5dc734 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -213,6 +213,7 @@ jobs: Contents: | AppInstallerCLIE2ETests\** AppInstallerCLITests\** + ComInprocTestbed\** Microsoft.Management.Configuration\** Microsoft.Management.Configuration.UnitTests\** Microsoft.Management.Configuration.OutOfProc\** diff --git a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h index dfaca8fa82..c74b90ef49 100644 --- a/src/AppInstallerCLICore/Public/ShutdownMonitoring.h +++ b/src/AppInstallerCLICore/Public/ShutdownMonitoring.h @@ -71,7 +71,7 @@ namespace AppInstaller::ShutdownMonitoring using ShutdownCompleteCallback = void (*)(); // Initializes the monitoring system and sets up a callback to be invoked when shutdown is completed. - static void Initialize(ShutdownCompleteCallback callback); + static void Initialize(ShutdownCompleteCallback callback, bool createTerminationSignalHandler = true); // "Interface" for a single component to synchronize with. struct ComponentSystem diff --git a/src/AppInstallerCLICore/ShutdownMonitoring.cpp b/src/AppInstallerCLICore/ShutdownMonitoring.cpp index bcdb3769f7..8972c154c2 100644 --- a/src/AppInstallerCLICore/ShutdownMonitoring.cpp +++ b/src/AppInstallerCLICore/ShutdownMonitoring.cpp @@ -286,9 +286,16 @@ namespace AppInstaller::ShutdownMonitoring } } - void ServerShutdownSynchronization::Initialize(ShutdownCompleteCallback callback) + void ServerShutdownSynchronization::Initialize(ShutdownCompleteCallback callback, bool createTerminationSignalHandler) { Instance().m_callback = callback; + + // Force the creation of the TerminationSignalHandler singleton so that the process can listen for termination signals even if + // it never attempts to run anything that explicitly registers for cancellation callbacks. + if (createTerminationSignalHandler) + { + TerminationSignalHandler::Instance(); + } } void ServerShutdownSynchronization::AddComponent(const ComponentSystem& component) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 70de8b4e19..bdc9f3bf29 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -33,6 +33,7 @@ public class Constants public const string SkipTestSourceParameter = "SkipTestSource"; public const string ForcedExperimentalFeaturesParameter = "ForcedExperimentalFeatures"; public const string InprocTestbedPathParameter = "InprocTestbedPath"; + public const string InprocTestbedUseTestPackageParameter = "InprocTestbedUseTestPackage"; // Test Sources public const string DefaultWingetSourceName = @"winget"; diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs index cf549b761c..7cf71fa7ba 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs @@ -31,6 +31,7 @@ private TestSetup() this.VerboseLogging = this.InitializeBoolParam(Constants.VerboseLoggingParameter, true); this.LooseFileRegistration = this.InitializeBoolParam(Constants.LooseFileRegistrationParameter); this.SkipTestSource = this.InitializeBoolParam(Constants.SkipTestSourceParameter, this.IsDefault); + this.InprocTestbedUseTestPackage = this.InitializeBoolParam(Constants.InprocTestbedUseTestPackageParameter); // For packaged context, default to AppExecutionAlias this.AICLIPath = this.InitializeStringParam(Constants.AICLIPathParameter, this.PackagedContext ? "WinGetDev.exe" : TestCommon.GetTestFile("winget.exe")); @@ -136,6 +137,11 @@ public static TestSetup Parameters /// public string InprocTestbedPath { get; } + /// + /// Gets a value indicating whether to use the test package or not. + /// + public bool InprocTestbedUseTestPackage { get; } + /// /// Gets a value indicating whether to skip creating test source. /// diff --git a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs index db0a95d7ed..75320de0a2 100644 --- a/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs +++ b/src/AppInstallerCLIE2ETests/InprocTestbedTests.cs @@ -75,11 +75,10 @@ public void OneTimeSetup() if (string.IsNullOrWhiteSpace(this.InprocTestbedPath)) { string assemblyLocation = Assembly.GetExecutingAssembly().Location; - this.InprocTestbedPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), "..\\ComInprocTestbed\\ComInprocTestbed.exe"); + this.InprocTestbedPath = Path.Join(Path.GetDirectoryName(assemblyLocation), "..\\ComInprocTestbed\\ComInprocTestbed.exe"); } - // If we are using the test source, target a package in it - if (!TestSetup.Parameters.SkipTestSource) + if (TestSetup.Parameters.InprocTestbedUseTestPackage) { this.TargetPackageInformation = $"-pkg {Constants.ExeInstallerPackageId} -src {Constants.TestSourceName} -url {Constants.TestSourceUrl}"; } diff --git a/templates/e2e-test.template.yml b/templates/e2e-test.template.yml index 4e0d3d6fe5..9231e26b04 100644 --- a/templates/e2e-test.template.yml +++ b/templates/e2e-test.template.yml @@ -33,6 +33,7 @@ steps: -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1 -LocalServerCertPath $(Agent.TempDirectory)\servercert.cer -SkipTestSource true + -InprocTestbedUseTestPackage true -ForcedExperimentalFeatures ${{ parameters.experimentalFeatures }}' ${{ else }}: overrideTestrunParameters: '-PackagedContext false @@ -41,6 +42,7 @@ steps: -PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1 -LocalServerCertPath $(Agent.TempDirectory)\servercert.cer -SkipTestSource true + -InprocTestbedUseTestPackage true -ForcedExperimentalFeatures ${{ parameters.experimentalFeatures }}' - task: CmdLine@2 From 26d978597cfb74c31e810a233fc13a59754832c4 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 9 Dec 2025 10:58:04 -0800 Subject: [PATCH 13/13] Improve project reference copying to hopefully work in pipeline --- src/ComInprocTestbed/ComInprocTestbed.vcxproj | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ComInprocTestbed/ComInprocTestbed.vcxproj b/src/ComInprocTestbed/ComInprocTestbed.vcxproj index 013223c486..116906cf7b 100644 --- a/src/ComInprocTestbed/ComInprocTestbed.vcxproj +++ b/src/ComInprocTestbed/ComInprocTestbed.vcxproj @@ -44,31 +44,37 @@ true $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ false + true true $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ false + true true $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ false + true false $(SolutionDir)x86\$(Configuration)\$(ProjectName)\ false + true false $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ false + true false $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ false + true @@ -172,8 +178,6 @@ false false true - Content - Always {1cc41a9a-ae66-459d-9210-1e572dd7be69} @@ -183,15 +187,20 @@ false false true - Content - Always - true + true + + + + + true + +