diff --git a/WindowsAppRuntime.sln b/WindowsAppRuntime.sln index 714bda40d2..707d4bb7e0 100644 --- a/WindowsAppRuntime.sln +++ b/WindowsAppRuntime.sln @@ -742,6 +742,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mrmmin", "dev\MRTCore\mrt\m EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MRM", "dev\MRTCore\mrt\Core\src\MRM.vcxproj", "{CF03CC8D-FFF1-4CDC-B773-D219AD4E6F76}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DeploymentUnitTests", "test\DeploymentUnitTests\DeploymentUnitTests.vcxproj", "{F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DeploymentUnitTests", "DeploymentUnitTests", "{2ECD5D10-0811-44D3-A789-B2C330BA8DF6}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsAppSDK.Test.NetCore", "test\WindowsAppSDK.Test.NetCore\WindowsAppSDK.Test.NetCore.csproj", "{BF580B26-B869-3AF1-43EC-D0FD55A49E4D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsAppSDK.Test.SampleTests", "test\WindowsAppSDK.Test.SampleTests\WindowsAppSDK.Test.SampleTests.csproj", "{346E099B-45E4-FF40-E63D-8B34915223C1}" @@ -2592,6 +2596,22 @@ Global {CF03CC8D-FFF1-4CDC-B773-D219AD4E6F76}.Release|x64.Build.0 = Release|x64 {CF03CC8D-FFF1-4CDC-B773-D219AD4E6F76}.Release|x86.ActiveCfg = Release|Win32 {CF03CC8D-FFF1-4CDC-B773-D219AD4E6F76}.Release|x86.Build.0 = Release|Win32 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|Any CPU.ActiveCfg = Debug|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|Any CPU.Build.0 = Debug|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|ARM64.Build.0 = Debug|ARM64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|x64.ActiveCfg = Debug|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|x64.Build.0 = Debug|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|x86.ActiveCfg = Debug|Win32 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Debug|x86.Build.0 = Debug|Win32 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|Any CPU.ActiveCfg = Release|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|Any CPU.Build.0 = Release|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|ARM64.ActiveCfg = Release|ARM64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|ARM64.Build.0 = Release|ARM64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|x64.ActiveCfg = Release|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|x64.Build.0 = Release|x64 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|x86.ActiveCfg = Release|Win32 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34}.Release|x86.Build.0 = Release|Win32 {BF580B26-B869-3AF1-43EC-D0FD55A49E4D}.Debug|Any CPU.ActiveCfg = Debug|x64 {BF580B26-B869-3AF1-43EC-D0FD55A49E4D}.Debug|Any CPU.Build.0 = Debug|x64 {BF580B26-B869-3AF1-43EC-D0FD55A49E4D}.Debug|ARM64.ActiveCfg = Debug|arm64 @@ -2847,6 +2867,8 @@ Global {E9C055BB-6AE4-497A-A354-D07841E68976} = {022E355A-AB24-48EE-9CC0-965BEFDF5E8C} {DC453DE3-18FD-43E7-8103-20763C8B97C8} = {5012149E-F09F-4F18-A03C-FFE597203821} {C40AE1D8-FD5F-472E-86B5-DDA5ABA6FF99} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34} = {2ECD5D10-0811-44D3-A789-B2C330BA8DF6} + {2ECD5D10-0811-44D3-A789-B2C330BA8DF6} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} {BF580B26-B869-3AF1-43EC-D0FD55A49E4D} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} {346E099B-45E4-FF40-E63D-8B34915223C1} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} EndGlobalSection @@ -2901,5 +2923,7 @@ Global dev\VersionInfo\VersionInfo.vcxitems*{e3edec7f-a24e-4766-bb1d-6bdfba157c51}*SharedItemsImports = 9 dev\AppNotifications\AppNotificationBuilder\AppNotificationBuilder.vcxitems*{e49329f3-5196-4bba-b5c4-e11ce7efb07a}*SharedItemsImports = 9 test\inc\inc.vcxitems*{e5659a29-fe68-417b-9bc5-613073dd54df}*SharedItemsImports = 4 + dev\Common\Common.vcxitems*{f2a9b8e7-4c62-4d89-9a4f-829f8e2a7e34}*SharedItemsImports = 4 + test\inc\inc.vcxitems*{f2a9b8e7-4c62-4d89-9a4f-829f8e2a7e34}*SharedItemsImports = 4 EndGlobalSection EndGlobal diff --git a/dev/Deployment/Deployment.vcxitems b/dev/Deployment/Deployment.vcxitems index 9636a5884f..bf4bdd099c 100644 --- a/dev/Deployment/Deployment.vcxitems +++ b/dev/Deployment/Deployment.vcxitems @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -27,6 +27,11 @@ + + + + + @@ -34,6 +39,10 @@ + + + + diff --git a/dev/Deployment/DeploymentManager.cpp b/dev/Deployment/DeploymentManager.cpp index 911ed92660..5941976742 100644 --- a/dev/Deployment/DeploymentManager.cpp +++ b/dev/Deployment/DeploymentManager.cpp @@ -5,11 +5,15 @@ #include #include #include -#include +#include +#include "PackageDefinitions.h" +#include "PackageUtilities.h" #include #include #include #include "WindowsAppRuntime-License.h" +#include "Licensing.h" +#include "PackageDeployment.h" using namespace winrt; using namespace winrt::Windows::Foundation; @@ -54,7 +58,7 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentResult DeploymentManager::GetStatus() { FAIL_FAST_HR_IF(HRESULT_FROM_WIN32(APPMODEL_ERROR_NO_PACKAGE), !AppModel::Identity::IsPackagedProcess()); - return GetStatus(GetCurrentFrameworkPackageFullName()); + return GetStatus(::WindowsAppRuntime::Deployment::Package::GetCurrentFrameworkPackageFullName()); } winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentResult DeploymentManager::Initialize() @@ -66,13 +70,13 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentResult DeploymentManager::Initialize( winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentInitializeOptions const& deploymentInitializeOptions) { - return Initialize(GetCurrentFrameworkPackageFullName(), deploymentInitializeOptions); + return Initialize(::WindowsAppRuntime::Deployment::Package::GetCurrentFrameworkPackageFullName(), deploymentInitializeOptions); } winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentResult DeploymentManager::Repair() { winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::DeploymentInitializeOptions options{}; - return Initialize(GetCurrentFrameworkPackageFullName(), options, true); + return Initialize(::WindowsAppRuntime::Deployment::Package::GetCurrentFrameworkPackageFullName(), options, true); } std::wstring ExtractFormattedVersionTag(const std::wstring& versionTag) @@ -94,7 +98,7 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem { // Get PackageInfo for WinAppSDK framework package std::wstring frameworkPackageFullName{ packageFullName }; - auto frameworkPackageInfo{ GetPackageInfoForPackage(frameworkPackageFullName) }; + auto frameworkPackageInfo{ ::WindowsAppRuntime::Deployment::Package::GetPackageInfoForPackage(frameworkPackageFullName) }; // Should only be called with a framework name that exists. FAIL_FAST_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), frameworkPackageInfo.Count() != 1); @@ -167,7 +171,7 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem // Get target version based on the framework. auto targetPackageVersion{ frameworkPackageInfo.Package(0).packageId.version }; - verifyResult = VerifyPackage(packageFamilyName, targetPackageVersion, package.identifier); + verifyResult = ::WindowsAppRuntime::Deployment::Package::VerifyPackage(packageFamilyName, targetPackageVersion, package.identifier); if (FAILED(verifyResult)) { break; @@ -290,7 +294,9 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem } std::wstring frameworkPackageFullName{ packageFullName }; - auto deployPackagesResult{ Deploy(frameworkPackageFullName, deploymentInitializeOptions.ForceDeployment()) }; + + auto deployPackagesResult{ Deploy(frameworkPackageFullName, initializeActivityContext, deploymentInitializeOptions.ForceDeployment()) }; + DeploymentStatus status{}; if (SUCCEEDED(deployPackagesResult)) { @@ -328,351 +334,58 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem return winrt::make(status, deployPackagesResult); } - MddCore::PackageInfo DeploymentManager::GetPackageInfoForPackage(std::wstring const& packageFullName) - { - wil::unique_package_info_reference packageInfoReference; - THROW_IF_WIN32_ERROR(OpenPackageInfoByFullName(packageFullName.c_str(), 0, &packageInfoReference)); - return MddCore::PackageInfo::FromPackageInfoReference(packageInfoReference.get()); - } - - // Borrowed and repurposed from Dynamic Dependencies - std::vector DeploymentManager::FindPackagesByFamily(std::wstring const& packageFamilyName) - { - UINT32 count{}; - UINT32 bufferLength{}; - const auto rc{ FindPackagesByPackageFamily(packageFamilyName.c_str(), PACKAGE_FILTER_HEAD | PACKAGE_FILTER_DIRECT, &count, nullptr, &bufferLength, nullptr, nullptr) }; - if (rc == ERROR_SUCCESS) - { - // The package family has no packages registered to the user - return std::vector(); - } - else if (rc != ERROR_INSUFFICIENT_BUFFER) - { - THROW_WIN32(rc); - } - - auto packageFullNames{ wil::make_unique_cotaskmem(count) }; - auto buffer{ wil::make_unique_cotaskmem(bufferLength) }; - THROW_IF_WIN32_ERROR(FindPackagesByPackageFamily(packageFamilyName.c_str(), PACKAGE_FILTER_HEAD | PACKAGE_FILTER_DIRECT, &count, packageFullNames.get(), &bufferLength, buffer.get(), nullptr)); - - std::vector packageFullNamesList; - for (UINT32 index=0; index < count; ++index) - { - const auto packageFullName{ packageFullNames[index] }; - packageFullNamesList.push_back(std::wstring(packageFullName)); - } - return packageFullNamesList; - } - - HRESULT DeploymentManager::VerifyPackage(const std::wstring& packageFamilyName, const PACKAGE_VERSION targetVersion, - const std::wstring& packageIdentifier) try + HRESULT DeploymentManager::Deploy( + const std::wstring& frameworkPackageFullName, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, + const bool forceDeployment + ) try { - auto packageFullNames{ FindPackagesByFamily(packageFamilyName) }; - bool match{}; - for (const auto& packageFullName : packageFullNames) - { - auto packagePath{ GetPackagePath(packageFullName) }; - if (packagePath.empty()) - { - continue; - } - - auto packageId{ AppModel::Identity::PackageIdentity::FromPackageFullName(packageFullName.c_str()) }; - if (packageId.Version().Version >= targetVersion.Version) - { - match = true; - if (packageId.Version().Version > targetVersion.Version) - { - g_existingTargetPackagesIfHigherVersion.insert(std::make_pair(packageIdentifier, packageFullName)); - } - break; - } - } - - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !match); + RETURN_IF_FAILED(InstallLicenses(frameworkPackageFullName, initializeActivityContext)); + RETURN_IF_FAILED(DeployPackages(frameworkPackageFullName, initializeActivityContext, forceDeployment)); return S_OK; } CATCH_RETURN() - // Gets the package path, which is a fast and reliable way to check if the package is - // at least staged on the device, even without package query capabilities. - std::wstring DeploymentManager::GetPackagePath(std::wstring const& packageFullName) - { - UINT32 pathLength{}; - const auto rc{ GetPackagePathByFullName(packageFullName.c_str(), &pathLength, nullptr) }; - if (rc == ERROR_NOT_FOUND) - { - return std::wstring(); - } - else if (rc != ERROR_INSUFFICIENT_BUFFER) - { - THROW_WIN32(rc); - } - - auto path{ wil::make_process_heap_string(nullptr, pathLength) }; - THROW_IF_WIN32_ERROR(GetPackagePathByFullName(packageFullName.c_str(), &pathLength, path.get())); - return std::wstring{ path.get() }; - } - - // If useExistingPackageIfHigherVersion == false, Adds the current version package at the passed in path using PackageManager. - // If useExistingPackageIfHigherVersion == true, Registers the higher version package using the passed in path as manifest path and PackageManager. - // This requires the 'packageManagement' or 'runFullTrust' capabilities. - HRESULT DeploymentManager::AddOrRegisterPackage(const std::filesystem::path& path, const bool useExistingPackageIfHigherVersion, const bool forceDeployment) try - { - winrt::Windows::Management::Deployment::PackageManager packageManager; - - const auto options{ forceDeployment ? - winrt::Windows::Management::Deployment::DeploymentOptions::ForceTargetApplicationShutdown : - winrt::Windows::Management::Deployment::DeploymentOptions::None }; - - winrt::Windows::Foundation::IAsyncOperationWithProgress deploymentOperation; - - const auto pathUri { winrt::Windows::Foundation::Uri(path.c_str()) }; - if (useExistingPackageIfHigherVersion) - { - deploymentOperation = packageManager.RegisterPackageAsync(pathUri, nullptr, options); - } - else - { - deploymentOperation = packageManager.AddPackageAsync(pathUri, nullptr, options); - } - deploymentOperation.get(); - const auto deploymentResult{ deploymentOperation.GetResults() }; - HRESULT deploymentOperationHResult{}; - HRESULT deploymentOperationExtendedHResult{}; - - if (deploymentOperation.Status() != winrt::Windows::Foundation::AsyncStatus::Completed) - { - deploymentOperationHResult = static_cast(deploymentOperation.ErrorCode()); - deploymentOperationExtendedHResult = deploymentResult.ExtendedErrorCode(); - - ::WindowsAppRuntime::Deployment::Activity::Context::Get().SetDeploymentErrorInfo( - deploymentOperationExtendedHResult, - deploymentResult.ErrorText().c_str(), - deploymentResult.ActivityId()); - } - - // If deploymentOperationHResult indicates success, take that, ignore deploymentOperationExtendedHResult. - // Otherwise, return deploymentOperationExtendedHResult if there is an error in it, if not, return deploymentOperationHResult. - return SUCCEEDED(deploymentOperationHResult) ? deploymentOperationHResult : - (FAILED(deploymentOperationExtendedHResult) ? deploymentOperationExtendedHResult : deploymentOperationHResult); - } - CATCH_RETURN() - - std::wstring DeploymentManager::GenerateDeploymentAgentPath() - { - // Calculate the path to the restart agent as being in the same directory as the current module. - wil::unique_hmodule module; - THROW_IF_WIN32_BOOL_FALSE(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast(DeploymentManager::GenerateDeploymentAgentPath), &module)); - - std::filesystem::path modulePath{ wil::GetModuleFileNameW(module.get()) }; - return modulePath.parent_path() / c_deploymentAgentFilename; - } - - /// @warning This function is ONLY for processes with package identity. It's the caller's responsibility to ensure this. - HRESULT DeploymentManager::AddOrRegisterPackageInBreakAwayProcess(const std::filesystem::path& path, const bool useExistingPackageIfHigherVersion, const bool forceDeployment) try + HRESULT DeploymentManager::InstallLicenses(const std::wstring& frameworkPackageFullName, ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext) { - auto exePath{ GenerateDeploymentAgentPath() }; - auto activityId{ winrt::to_hstring(*::WindowsAppRuntime::Deployment::Activity::Context::Get().GetActivity().Id()) }; - - // \deploymentagent.exe - auto cmdLine{ wil::str_printf(L"\"%s\" %u \"%s\" %u %s", exePath.c_str(), (useExistingPackageIfHigherVersion ? 1 : 0), path.c_str(), (forceDeployment ? 1 : 0), activityId.c_str()) }; - - SIZE_T attributeListSize{}; - auto attributeCount{ 1 }; - - // attributeCount is always >0 so we need to allocate a buffer. Call InitializeProcThreadAttributeList() - // to determine the size needed so we always expect ERROR_INSUFFICIENT_BUFFER. - THROW_HR_IF(E_UNEXPECTED, !!InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &attributeListSize)); - const auto lastError{ GetLastError() }; - THROW_HR_IF(HRESULT_FROM_WIN32(lastError), lastError != ERROR_INSUFFICIENT_BUFFER); - wistd::unique_ptr attributeListBuffer{ new BYTE[attributeListSize] }; - auto attributeList{ reinterpret_cast(attributeListBuffer.get()) }; - THROW_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(attributeList, attributeCount, 0, &attributeListSize)); - auto freeAttributeList{ wil::scope_exit([&] { DeleteProcThreadAttributeList(attributeList); }) }; - - // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute - // The process being created will create any child processes outside of the desktop app runtime environment. - // This behavior is the default for processes for which no policy has been set - DWORD policy{ PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE }; - THROW_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(attributeList, 0, PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY, &policy, sizeof(policy), nullptr, nullptr)); - - STARTUPINFOEX info{}; - info.StartupInfo.cb = sizeof(info); - info.lpAttributeList = attributeList; - - wil::unique_process_information processInfo; - THROW_IF_WIN32_BOOL_FALSE(CreateProcess(nullptr, cmdLine.get(), nullptr, nullptr, FALSE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &info.StartupInfo, &processInfo)); - - // This API is designed to only return to the caller on failure, otherwise block until process termination. - // Wait for the agent to exit. If the agent succeeds, it will terminate this process. If the agent fails, - // it can exit or crash. This API will be able to detect the failure and return. - wil::handle_wait(processInfo.hProcess); - - DWORD processExitCode{}; - THROW_IF_WIN32_BOOL_FALSE_MSG(GetExitCodeProcess(processInfo.hProcess, &processExitCode), "CmdLine: %ls, processExitCode: %u", cmdLine.get(), processExitCode); - RETURN_IF_FAILED_MSG(HRESULT_FROM_WIN32(processExitCode), "DeploymentAgent exitcode:0x%X", processExitCode); - return S_OK; - } - CATCH_RETURN() + auto licenseInstaller{ ::Microsoft::Windows::ApplicationModel::Licensing::Installer{} }; + auto licenseInstallerProxy{ ::WindowsAppRuntime::Deployment::Licensing::LicenseInstallerProxy{ licenseInstaller } }; - // Deploys all of the packages carried by the specified framework. - HRESULT DeploymentManager::Deploy(const std::wstring& frameworkPackageFullName, const bool forceDeployment) try - { - RETURN_IF_FAILED(InstallLicenses(frameworkPackageFullName)); - RETURN_IF_FAILED(DeployPackages(frameworkPackageFullName, forceDeployment)); - return S_OK; - } - CATCH_RETURN() + initializeActivityContext.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::GetLicensePath); - HRESULT DeploymentManager::InstallLicenses(const std::wstring& frameworkPackageFullName) - { - ::WindowsAppRuntime::Deployment::Activity::Context::Get().Reset(); - ::WindowsAppRuntime::Deployment::Activity::Context::Get().SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::GetLicensePath); + auto packagePath = ::WindowsAppRuntime::Deployment::Package::GetPackagePath(frameworkPackageFullName); // Build path for licenses - auto licensePath{ std::filesystem::path(GetPackagePath(frameworkPackageFullName)) }; + auto licensePath{ std::filesystem::path(packagePath) }; licensePath /= WINDOWSAPPRUNTIME_FRAMEWORK_PACKAGE_FOLDER; auto licenseFilespec{ licensePath }; licenseFilespec /= L"*_license.xml"; - ::WindowsAppRuntime::Deployment::Activity::Context::Get().SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::InstallLicense); - - // Deploy the licenses (if any) - ::Microsoft::Windows::ApplicationModel::Licensing::Installer licenseInstaller; - WIN32_FIND_DATA findFileData{}; - wil::unique_hfind hfind{ FindFirstFileW(licenseFilespec.c_str(), &findFileData) }; - if (!hfind) - { - const auto lastError{ GetLastError() }; - RETURN_HR_IF_MSG(HRESULT_FROM_WIN32(lastError), (lastError != ERROR_FILE_NOT_FOUND) && (lastError != ERROR_PATH_NOT_FOUND), - "FindFirstFile:%ls", licenseFilespec.c_str()); - return S_OK; - } - for (;;) - { - // Install the license file - auto licenseFilename{ licensePath }; - licenseFilename /= findFileData.cFileName; - - ::WindowsAppRuntime::Deployment::Activity::Context::Get().SetCurrentResourceId(licenseFilename); - - RETURN_IF_FAILED_MSG(licenseInstaller.InstallLicenseFile(licenseFilename.c_str()), - "LicenseFile:%ls", licenseFilename.c_str()); - - // Next! (if any) - if (!FindNextFileW(hfind.get(), &findFileData)) - { - const auto lastError{ GetLastError() }; - RETURN_HR_IF(HRESULT_FROM_WIN32(lastError), lastError != ERROR_NO_MORE_FILES); - break; - } - } + std::vector licenseFiles; + RETURN_IF_FAILED(::WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(licenseFilespec, licenseFiles)); + RETURN_IF_FAILED(::WindowsAppRuntime::Deployment::Licensing::InstallLicenses(licenseFiles, licensePath, licenseInstallerProxy, initializeActivityContext)); return S_OK; } - HRESULT DeploymentManager::DeployPackages(const std::wstring& frameworkPackageFullName, const bool forceDeployment) + HRESULT DeploymentManager::DeployPackages(const std::wstring& frameworkPackageFullName, ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, const bool forceDeployment) { - auto initializeActivity{ ::WindowsAppRuntime::Deployment::Activity::Context::Get() }; - initializeActivity.Reset(); + std::function getPackagePathFunc { ::WindowsAppRuntime::Deployment::Package::GetPackagePath }; + const auto deploymentPackageArguments = ::WindowsAppRuntime::Deployment::PackageDeployment::GetDeploymentPackageArguments(frameworkPackageFullName, initializeActivityContext, g_existingTargetPackagesIfHigherVersion, getPackagePathFunc); + RETURN_IF_FAILED(::WindowsAppRuntime::Deployment::PackageDeployment::DeployPackages(deploymentPackageArguments, forceDeployment, initializeActivityContext)); - initializeActivity.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::GetPackagePath); - const auto frameworkPath{ std::filesystem::path(GetPackagePath(frameworkPackageFullName)) }; - - for (auto package : c_targetPackages) + // Always restart Push Notifications Long Running Platform when Singleton package is processed and installed. + for (const auto& package : deploymentPackageArguments) { - auto isSingleton{ CompareStringOrdinal(package.identifier.c_str(), -1, WINDOWSAPPRUNTIME_PACKAGE_SUBTYPENAME_SINGLETON, -1, TRUE) == CSTR_EQUAL }; - initializeActivity.Reset(); - initializeActivity.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::AddPackage); - initializeActivity.SetCurrentResourceId(package.identifier); - - std::filesystem::path packagePath{}; - - // If there is exisiting target package version higher than that of framework current version package, then re-register it. - // Otherwise, deploy the target msix package from the current framework package version. - auto existingPackageIfHigherVersion = g_existingTargetPackagesIfHigherVersion.find(package.identifier); - auto useExistingPackageIfHigherVersion { existingPackageIfHigherVersion != g_existingTargetPackagesIfHigherVersion.end() }; - if (useExistingPackageIfHigherVersion) - { - initializeActivity.SetUseExistingPackageIfHigherVersion(); - packagePath = std::filesystem::path(GetPackagePath(existingPackageIfHigherVersion->second)); - packagePath /= WINDOWSAPPRUNTIME_PACKAGE_MANIFEST_FILE; - } - else - { - packagePath = frameworkPath; - packagePath /= WINDOWSAPPRUNTIME_FRAMEWORK_PACKAGE_FOLDER; - packagePath /= package.identifier + WINDOWSAPPRUNTIME_FRAMEWORK_PACKAGE_FILE_EXTENSION; - } - - // If the current application has runFullTrust capability, then Deploy the target package in a Breakaway process. - // Otherwise, call PackageManager API to deploy the target package. - // The Singleton package will always set true for forceDeployment and the running process will be terminated to update the package. - if (initializeActivity.GetIsFullTrustPackage()) + if (package.isSingleton) { - - RETURN_IF_FAILED(AddOrRegisterPackageInBreakAwayProcess(packagePath, useExistingPackageIfHigherVersion, forceDeployment || isSingleton)); - } - else - { - RETURN_IF_FAILED(AddOrRegisterPackage(packagePath, useExistingPackageIfHigherVersion, forceDeployment || isSingleton)); - } - - // Always restart Push Notifications Long Running Platform when Singleton package is processed and installed. - if (isSingleton) - { - // wil callback is set up to log telemetry events for Push Notifications LRP. - LOG_IF_FAILED_MSG(StartupNotificationsLongRunningPlatform(), "Restarting Notifications LRP failed in all 3 attempts."); + // WIL callback is set up to log telemetry events for Push Notifications LRP. + std::ignore = LOG_IF_FAILED(StartupNotificationsLongRunningPlatform()); + break; } } - return S_OK; } - hstring DeploymentManager::GetCurrentFrameworkPackageFullName() - { - // Get current package identity. - WCHAR packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; - UINT32 packageFullNameLength{ static_cast(ARRAYSIZE(packageFullName)) }; - const auto rc{ ::GetCurrentPackageFullName(&packageFullNameLength, packageFullName) }; - if (rc != ERROR_SUCCESS) - { - THROW_WIN32(rc); - } - - // Get the PackageInfo of current package and it's dependency packages - std::wstring currentPackageFullName{ packageFullName }; - auto currentPackageInfo{ GetPackageInfoForPackage(currentPackageFullName) }; - - // Index starts at 1 since the first package is the current package and we are interested in - // dependency packages only. - for (size_t i = 0; i < currentPackageInfo.Count(); ++i) - { - auto dependencyPackage{ currentPackageInfo.Package(i) }; - - // Verify PublisherId matches. - if (CompareStringOrdinal(dependencyPackage.packageId.publisherId, -1, WINDOWSAPPRUNTIME_PACKAGE_PUBLISHERID, -1, TRUE) != CSTR_EQUAL) - { - continue; - } - - // Verify that the WindowsAppRuntime prefix identifier is in the name. - // This should also be the beginning of the name, so its find position is expected to be 0. - std::wstring dependencyPackageName{ dependencyPackage.packageId.name }; - if (dependencyPackageName.find(WINDOWSAPPRUNTIME_PACKAGE_NAME_PREFIX) != 0) - { - continue; - } - - // On WindowsAppSDK 1.1+, there is no need to check and rule out Main, Singleton and DDLM Package identifiers as their names don't have a overlap with WINDOWSAPPRUNTIME_PACKAGE_NAME_PREFIX. - - return hstring(dependencyPackage.packageFullName); - } - - THROW_WIN32(ERROR_NOT_FOUND); - } - HRESULT Initialize_Log( HRESULT hrInitialize, const AppModel::Identity::PackageIdentity& packageIdentity, diff --git a/dev/Deployment/DeploymentManager.h b/dev/Deployment/DeploymentManager.h index f3310ab286..b27a6b98a8 100644 --- a/dev/Deployment/DeploymentManager.h +++ b/dev/Deployment/DeploymentManager.h @@ -1,7 +1,6 @@ -// Copyright (c) Microsoft Corporation and Contributors. +// Copyright (c) Microsoft Corporation and Contributors. // Licensed under the MIT License. #pragma once -#include #include #include #include "Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManager.g.h" @@ -10,8 +9,6 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implementation { - static PCWSTR c_deploymentAgentFilename{ L"DeploymentAgent.exe" }; - struct DeploymentManager { DeploymentManager() = default; @@ -27,26 +24,19 @@ namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implem WindowsAppRuntime::DeploymentInitializeOptions const& deploymentInitializeOptions, bool isRepair = false); - private: static WindowsAppRuntime::DeploymentResult _Initialize( ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, hstring const& packageFullName, WindowsAppRuntime::DeploymentInitializeOptions const& deploymentInitializeOptions, bool isRepair); - private: - static MddCore::PackageInfo GetPackageInfoForPackage(std::wstring const& packageFullName); - static std::vector FindPackagesByFamily(std::wstring const& packageFamilyName); - static HRESULT VerifyPackage(const std::wstring& packageFamilyName, const PACKAGE_VERSION targetVersion, const std::wstring& matchedPackageFullName); - static std::wstring GetPackagePath(std::wstring const& packageFullName); - static HRESULT AddOrRegisterPackageInBreakAwayProcess(const std::filesystem::path& packagePath, const bool regiterHigherVersionPackage, const bool forceDeployment); - static std::wstring GenerateDeploymentAgentPath(); - static HRESULT AddOrRegisterPackage(const std::filesystem::path& package, const bool regiterHigherVersionPackage, const bool forceDeployment); - static HRESULT DeployPackages(const std::wstring& frameworkPackageFullName, const bool forceDeployment); - static HRESULT Deploy(const std::wstring& frameworkPackageFullName, const bool forceDeployment = false); - static HRESULT InstallLicenses(const std::wstring& frameworkPackageFullName); - static hstring GetCurrentFrameworkPackageFullName(); + static HRESULT Deploy( + const std::wstring& frameworkPackageFullName, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, + const bool forceDeployment); + static HRESULT InstallLicenses(const std::wstring& frameworkPackageFullName, ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext); + static HRESULT DeployPackages(const std::wstring& frameworkPackageFullName, ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, const bool forceDeployment); }; } namespace winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::factory_implementation diff --git a/dev/Deployment/LicenseInstallerProxy.h b/dev/Deployment/LicenseInstallerProxy.h new file mode 100644 index 0000000000..893ab89345 --- /dev/null +++ b/dev/Deployment/LicenseInstallerProxy.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. +#pragma once + +#include "Licensing.h" +#include "WindowsAppRuntime-License.h" + +namespace WindowsAppRuntime::Deployment::Licensing +{ + class LicenseInstallerProxy : public ::WindowsAppRuntime::Deployment::Licensing::ILicenseInstaller + { + ::Microsoft::Windows::ApplicationModel::Licensing::Installer& m_installer; + + public: + LicenseInstallerProxy(::Microsoft::Windows::ApplicationModel::Licensing::Installer& installer) : m_installer(installer) {} + + HRESULT InstallLicenseFile(const std::wstring& licenseFilename) override + { + return m_installer.InstallLicenseFile(licenseFilename.c_str()); + } + }; +} diff --git a/dev/Deployment/Licensing.cpp b/dev/Deployment/Licensing.cpp new file mode 100644 index 0000000000..0e1b88b680 --- /dev/null +++ b/dev/Deployment/Licensing.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include +#include "DeploymentActivityContext.h" +#include "Licensing.h" + +using namespace winrt; + +namespace WindowsAppRuntime::Deployment::Licensing +{ + // licenseFileSpec: This parameter specifies the file specification (e.g., path and pattern) for the license files to be retrieved. + // licenseFiles: This is an output parameter that will be populated with the names of the license files found matching the specified file specification. + HRESULT GetLicenseFiles(const std::wstring& licenseFileSpec, std::vector& licenseFiles) + { + licenseFiles.clear(); + + WIN32_FIND_DATA findFileData{}; + wil::unique_hfind hfind{ FindFirstFileW(licenseFileSpec.c_str(), &findFileData) }; + if (!hfind) + { + const auto lastError{ GetLastError() }; + RETURN_HR_IF_MSG(HRESULT_FROM_WIN32(lastError), (lastError != ERROR_FILE_NOT_FOUND) && (lastError != ERROR_PATH_NOT_FOUND), + "FindFirstFile:%ls", licenseFileSpec.c_str()); + return S_OK; + } + for (;;) + { + // Add the license file + licenseFiles.push_back(findFileData.cFileName); + + // Next! (if any) + if (!FindNextFileW(hfind.get(), &findFileData)) + { + const auto lastError{ GetLastError() }; + RETURN_HR_IF(HRESULT_FROM_WIN32(lastError), lastError != ERROR_NO_MORE_FILES); + break; + } + } + return S_OK; + } + + HRESULT InstallLicenses( + const std::vector& licenseFiles, + const std::filesystem::path& licensePath, + ILicenseInstaller& licenseInstaller, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext) + { + initializeActivityContext.Reset(); + initializeActivityContext.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::InstallLicense); + + // Deploy the licenses (if any) + for (const auto& licenseFileName : licenseFiles) + { + // Install the license file + auto licenseFileFullName{ licensePath }; + licenseFileFullName /= licenseFileName; + + initializeActivityContext.SetCurrentResourceId(licenseFileFullName); + + RETURN_IF_FAILED_MSG(licenseInstaller.InstallLicenseFile(licenseFileFullName.c_str()), + "LicenseFile:%ls", licenseFileFullName.c_str()); + } + return S_OK; + } +} diff --git a/dev/Deployment/Licensing.h b/dev/Deployment/Licensing.h new file mode 100644 index 0000000000..54d076756a --- /dev/null +++ b/dev/Deployment/Licensing.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include "DeploymentActivityContext.h" + +namespace WindowsAppRuntime::Deployment::Licensing +{ + // Proxy/Wrapper for license installer. Open possibility to add more methods if needed. + struct ILicenseInstaller + { + virtual HRESULT InstallLicenseFile(const std::wstring& licenseFilename) = 0; + }; + + // Get license files from the specified path pattern + HRESULT GetLicenseFiles(const std::wstring& licenseFileSpec, std::vector& licenseFiles); + + // Install license files + HRESULT InstallLicenses( + const std::vector& licenseFiles, + const std::filesystem::path& licensePath, + ILicenseInstaller& licenseInstaller, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext); +} diff --git a/dev/Deployment/PackageDeployment.cpp b/dev/Deployment/PackageDeployment.cpp new file mode 100644 index 0000000000..16f53eb77b --- /dev/null +++ b/dev/Deployment/PackageDeployment.cpp @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include +#include +#include "DeploymentActivityContext.h" +#include "PackageDefinitions.h" +#include "PackageDeployment.h" +#include "PackageRegistrar.h" + +namespace WindowsAppRuntime::Deployment::PackageDeployment +{ + std::vector GetDeploymentPackageArguments( + const std::wstring& frameworkPackageFullName, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, + const std::map& existingTargetPackagesIfHigherVersion, + const std::function& getPackagePathFunc) + { + initializeActivityContext.Reset(); + initializeActivityContext.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::GetPackagePath); + + std::vector deploymentPackageArguments; + + const auto frameworkPath{ std::filesystem::path(getPackagePathFunc(frameworkPackageFullName)) }; + for (auto package : winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implementation::c_targetPackages) + { + auto isSingleton{ CompareStringOrdinal(package.identifier.c_str(), -1, WINDOWSAPPRUNTIME_PACKAGE_SUBTYPENAME_SINGLETON, -1, TRUE) == CSTR_EQUAL }; + + std::filesystem::path packagePath{}; + + // If there is exisiting target package version higher than that of framework current version package, then re-register it. + // Otherwise, deploy the target msix package from the current framework package version. + auto existingPackageIfHigherVersion = existingTargetPackagesIfHigherVersion.find(package.identifier); + auto useExistingPackageIfHigherVersion { existingPackageIfHigherVersion != existingTargetPackagesIfHigherVersion.end() }; + if (useExistingPackageIfHigherVersion) + { + packagePath = std::filesystem::path(getPackagePathFunc(existingPackageIfHigherVersion->second)); + packagePath /= WINDOWSAPPRUNTIME_PACKAGE_MANIFEST_FILE; + } + else + { + packagePath = frameworkPath; + packagePath /= WINDOWSAPPRUNTIME_FRAMEWORK_PACKAGE_FOLDER; + packagePath /= package.identifier + WINDOWSAPPRUNTIME_FRAMEWORK_PACKAGE_FILE_EXTENSION; + } + + deploymentPackageArguments.push_back(DeploymentPackageArguments{ package.identifier, packagePath, useExistingPackageIfHigherVersion, isSingleton }); + } + + return deploymentPackageArguments; + } + + HRESULT DeployPackages( + const std::vector& deploymentPackageArguments, + const bool forceDeployment, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivity) + { + for (auto package : deploymentPackageArguments) + { + initializeActivity.Reset(); + initializeActivity.SetInstallStage(::WindowsAppRuntime::Deployment::Activity::DeploymentStage::AddPackage); + initializeActivity.SetCurrentResourceId(package.identifier); + if (package.useExistingPackageIfHigherVersion) + { + initializeActivity.SetUseExistingPackageIfHigherVersion(); + } + + // If the current application has runFullTrust capability, then Deploy the target package in a Breakaway process. + // Otherwise, call PackageManager API to deploy the target package. + // The Singleton package will always set true for forceDeployment and the running process will be terminated to update the package. + if (initializeActivity.GetIsFullTrustPackage()) + { + RETURN_IF_FAILED(::WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackageInBreakAwayProcess( + package.packagePath, + package.useExistingPackageIfHigherVersion, + forceDeployment || package.isSingleton, + initializeActivity, + ::WindowsAppRuntime::Deployment::PackageRegistrar::GenerateDeploymentAgentPath() + )); + } + else + { + auto packageManager = winrt::Windows::Management::Deployment::PackageManager{}; + RETURN_IF_FAILED(::WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackage( + package.packagePath, + package.useExistingPackageIfHigherVersion, + forceDeployment || package.isSingleton, + packageManager, + initializeActivity + )); + } + } + + return S_OK; + } +} diff --git a/dev/Deployment/PackageDeployment.h b/dev/Deployment/PackageDeployment.h new file mode 100644 index 0000000000..4079f1e42c --- /dev/null +++ b/dev/Deployment/PackageDeployment.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include +#include +#include "DeploymentActivityContext.h" + +namespace WindowsAppRuntime::Deployment::PackageDeployment +{ + // Structure to hold deployment package arguments + struct DeploymentPackageArguments + { + std::wstring identifier{}; + std::filesystem::path packagePath{}; + bool useExistingPackageIfHigherVersion{}; + bool isSingleton{}; + }; + + // Get deployment package arguments + std::vector GetDeploymentPackageArguments( + const std::wstring& frameworkPackageFullName, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivityContext, + const std::map& existingTargetPackagesIfHigherVersion, + const std::function& getPackagePathFunc); + + // Package deployment + HRESULT DeployPackages( + const std::vector& deploymentPackageArguments, + const bool forceDeployment, + ::WindowsAppRuntime::Deployment::Activity::Context& initializeActivity); +} diff --git a/dev/Deployment/PackageRegistrar.cpp b/dev/Deployment/PackageRegistrar.cpp new file mode 100644 index 0000000000..3338deda40 --- /dev/null +++ b/dev/Deployment/PackageRegistrar.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace WindowsAppRuntime::Deployment::PackageRegistrar +{ + // If useExistingPackageIfHigherVersion == false, Adds the current version package at the passed in path using PackageManager. + // If useExistingPackageIfHigherVersion == true, Registers the higher version package using the passed in path as manifest path and PackageManager. + // This requires the 'packageManagement' or 'runFullTrust' capabilities. + HRESULT AddOrRegisterPackage( + const std::filesystem::path& path, + const bool useExistingPackageIfHigherVersion, + const bool forceDeployment, + winrt::Windows::Management::Deployment::IPackageManager& packageManager, + ::WindowsAppRuntime::Deployment::Activity::Context& activityContext) try + { + const auto options{ forceDeployment ? + winrt::Windows::Management::Deployment::DeploymentOptions::ForceTargetApplicationShutdown : + winrt::Windows::Management::Deployment::DeploymentOptions::None }; + + winrt::Windows::Foundation::IAsyncOperationWithProgress deploymentOperation; + + const auto pathUri { winrt::Windows::Foundation::Uri(path.c_str()) }; + if (useExistingPackageIfHigherVersion) + { + deploymentOperation = packageManager.RegisterPackageAsync(pathUri, nullptr, options); + } + else + { + deploymentOperation = packageManager.AddPackageAsync(pathUri, nullptr, options); + } + + deploymentOperation.get(); + + const auto deploymentResult{ deploymentOperation.GetResults() }; + HRESULT hrError{}; + HRESULT hrExtendedError{}; + + if (deploymentOperation.Status() != winrt::Windows::Foundation::AsyncStatus::Completed) + { + hrError = static_cast(deploymentOperation.ErrorCode()); + hrExtendedError = deploymentResult.ExtendedErrorCode(); + + activityContext.SetDeploymentErrorInfo( + hrExtendedError, + deploymentResult.ErrorText().c_str(), + deploymentResult.ActivityId()); + } + + // If hrError indicates success, take that, ignore hrExtendedError. + // Otherwise, return hrExtendedError if there is an error in it, if not, return hrError. + return (FAILED(hrError) && FAILED(hrExtendedError) ? hrExtendedError : hrError); + } + CATCH_RETURN() + + std::wstring GenerateDeploymentAgentPath() + { + // Calculate the path to the restart agent as being in the same directory as the current module. + wil::unique_hmodule module; + THROW_IF_WIN32_BOOL_FALSE(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast(PackageRegistrar::GenerateDeploymentAgentPath), &module)); + + std::filesystem::path modulePath{ wil::GetModuleFileNameW(module.get()) }; + return modulePath.parent_path() / c_deploymentAgentFilename; + } + + /// @warning This function is ONLY for processes with package identity. It's the caller's responsibility to ensure this. + HRESULT AddOrRegisterPackageInBreakAwayProcess( + const std::filesystem::path& path, + const bool useExistingPackageIfHigherVersion, + const bool forceDeployment, + ::WindowsAppRuntime::Deployment::Activity::Context& activityContext, + const std::wstring& deploymentAgentPath) try + { + auto exePath{ deploymentAgentPath }; + auto activityId{ winrt::to_hstring(*activityContext.GetActivity().Id()) }; + + // \deploymentagent.exe + auto cmdLine{ wil::str_printf(L"\"%s\" %u \"%s\" %u %s", exePath.c_str(), (useExistingPackageIfHigherVersion ? 1 : 0), path.c_str(), (forceDeployment ? 1 : 0), activityId.c_str()) }; + + SIZE_T attributeListSize{}; + auto attributeCount{ 1 }; + + // attributeCount is always >0 so we need to allocate a buffer. Call InitializeProcThreadAttributeList() + // to determine the size needed so we always expect ERROR_INSUFFICIENT_BUFFER. + THROW_HR_IF(E_UNEXPECTED, !!InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &attributeListSize)); + const auto lastError{ GetLastError() }; + THROW_HR_IF(HRESULT_FROM_WIN32(lastError), lastError != ERROR_INSUFFICIENT_BUFFER); + wistd::unique_ptr attributeListBuffer{ new BYTE[attributeListSize] }; + auto attributeList{ reinterpret_cast(attributeListBuffer.get()) }; + THROW_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(attributeList, attributeCount, 0, &attributeListSize)); + auto freeAttributeList{ wil::scope_exit([&] { DeleteProcThreadAttributeList(attributeList); }) }; + + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute + // The process being created will create any child processes outside of the desktop app runtime environment. + // This behavior is the default for processes for which no policy has been set + DWORD policy{ PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE }; + THROW_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(attributeList, 0, PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY, &policy, sizeof(policy), nullptr, nullptr)); + + STARTUPINFOEX info{}; + info.StartupInfo.cb = sizeof(info); + info.lpAttributeList = attributeList; + + wil::unique_process_information processInfo; + // We would possible like to have a seam here for testing different results from CreateProcess. + // Also, we don't want unit tests to run external processes all the time :) + THROW_IF_WIN32_BOOL_FALSE(CreateProcess(nullptr, cmdLine.get(), nullptr, nullptr, FALSE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &info.StartupInfo, &processInfo)); + + // This API is designed to only return to the caller on failure, otherwise block until process termination. + // Wait for the agent to exit. If the agent succeeds, it will terminate this process. If the agent fails, + // it can exit or crash. This API will be able to detect the failure and return. + wil::handle_wait(processInfo.hProcess); + + DWORD processExitCode{}; + THROW_IF_WIN32_BOOL_FALSE_MSG(GetExitCodeProcess(processInfo.hProcess, &processExitCode), "CmdLine: %ls, processExitCode: %u", cmdLine.get(), processExitCode); + RETURN_IF_FAILED_MSG(HRESULT_FROM_WIN32(processExitCode), "DeploymentAgent exitcode:0x%X", processExitCode); + return S_OK; + } + CATCH_RETURN() +} diff --git a/dev/Deployment/PackageRegistrar.h b/dev/Deployment/PackageRegistrar.h new file mode 100644 index 0000000000..c58ba20b4b --- /dev/null +++ b/dev/Deployment/PackageRegistrar.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include +#include "DeploymentActivityContext.h" + +namespace WindowsAppRuntime::Deployment::PackageRegistrar +{ + static constexpr PCWSTR c_deploymentAgentFilename{ L"DeploymentAgent.exe" }; + + std::wstring GenerateDeploymentAgentPath(); + + HRESULT AddOrRegisterPackage( + const std::filesystem::path& path, + const bool useExistingPackageIfHigherVersion, + const bool forceDeployment, + winrt::Windows::Management::Deployment::IPackageManager& packageManager, + ::WindowsAppRuntime::Deployment::Activity::Context& activityContext); + + HRESULT AddOrRegisterPackageInBreakAwayProcess( + const std::filesystem::path& path, + const bool useExistingPackageIfHigherVersion, + const bool forceDeployment, + ::WindowsAppRuntime::Deployment::Activity::Context& activityContext, + const std::wstring& deploymentAgentPath); +} diff --git a/dev/Deployment/PackageUtilities.cpp b/dev/Deployment/PackageUtilities.cpp new file mode 100644 index 0000000000..3512791e3d --- /dev/null +++ b/dev/Deployment/PackageUtilities.cpp @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include "PackageUtilities.h" +#include "PackageDefinitions.h" +#include + +namespace WindowsAppRuntime::Deployment::Package +{ + std::wstring GetPackagePath(std::wstring const& packageFullName) + { + UINT32 pathLength{}; + const auto rc{ GetPackagePathByFullName(packageFullName.c_str(), &pathLength, nullptr) }; + if (rc == ERROR_NOT_FOUND) + { + return std::wstring(); + } + else if (rc != ERROR_INSUFFICIENT_BUFFER) + { + THROW_WIN32(rc); + } + + auto path{ wil::make_process_heap_string(nullptr, pathLength) }; + THROW_IF_WIN32_ERROR(GetPackagePathByFullName(packageFullName.c_str(), &pathLength, path.get())); + return std::wstring{ path.get() }; + } + + // Borrowed and repurposed from Dynamic Dependencies + std::vector FindPackagesByFamily(std::wstring const& packageFamilyName) + { + UINT32 count{}; + UINT32 bufferLength{}; + const auto rc{ FindPackagesByPackageFamily(packageFamilyName.c_str(), PACKAGE_FILTER_HEAD | PACKAGE_FILTER_DIRECT, &count, nullptr, &bufferLength, nullptr, nullptr) }; + if (rc == ERROR_SUCCESS) + { + // The package family has no packages registered to the user + return std::vector(); + } + else if (rc != ERROR_INSUFFICIENT_BUFFER) + { + THROW_WIN32(rc); + } + + auto packageFullNames{ wil::make_unique_cotaskmem(count) }; + auto buffer{ wil::make_unique_cotaskmem(bufferLength) }; + THROW_IF_WIN32_ERROR(FindPackagesByPackageFamily(packageFamilyName.c_str(), PACKAGE_FILTER_HEAD | PACKAGE_FILTER_DIRECT, &count, packageFullNames.get(), &bufferLength, buffer.get(), nullptr)); + + std::vector packageFullNamesList; + for (UINT32 index=0; index < count; ++index) + { + const auto packageFullName{ packageFullNames[index] }; + packageFullNamesList.push_back(std::wstring(packageFullName)); + } + return packageFullNamesList; + } + + HRESULT VerifyPackage(const std::wstring& packageFamilyName, const PACKAGE_VERSION targetVersion, + const std::wstring& packageIdentifier) try + { + auto packageFullNames{ FindPackagesByFamily(packageFamilyName) }; + bool match{}; + for (const auto& packageFullName : packageFullNames) + { + auto packagePath{ GetPackagePath(packageFullName) }; + if (packagePath.empty()) + { + continue; + } + + auto packageId{ AppModel::Identity::PackageIdentity::FromPackageFullName(packageFullName.c_str()) }; + if (packageId.Version().Version >= targetVersion.Version) + { + match = true; + if (packageId.Version().Version > targetVersion.Version) + { + winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::implementation::g_existingTargetPackagesIfHigherVersion.insert(std::make_pair(packageIdentifier, packageFullName)); + } + break; + } + } + + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !match); + return S_OK; + } + CATCH_RETURN() + + MddCore::PackageInfo GetPackageInfoForPackage(std::wstring const& packageFullName) + { + wil::unique_package_info_reference packageInfoReference; + THROW_IF_WIN32_ERROR(OpenPackageInfoByFullName(packageFullName.c_str(), 0, &packageInfoReference)); + return MddCore::PackageInfo::FromPackageInfoReference(packageInfoReference.get()); + } + + winrt::hstring GetCurrentFrameworkPackageFullName() + { + // Get current package identity. + WCHAR packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; + UINT32 packageFullNameLength{ static_cast(ARRAYSIZE(packageFullName)) }; + const auto rc{ ::GetCurrentPackageFullName(&packageFullNameLength, packageFullName) }; + if (rc != ERROR_SUCCESS) + { + THROW_WIN32(rc); + } + + // Get the PackageInfo of current package and it's dependency packages + std::wstring currentPackageFullName{ packageFullName }; + auto currentPackageInfo{ GetPackageInfoForPackage(currentPackageFullName) }; + + // Index starts at 1 since the first package is the current package and we are interested in + // dependency packages only. + for (size_t i = 0; i < currentPackageInfo.Count(); ++i) + { + auto dependencyPackage{ currentPackageInfo.Package(i) }; + + // Verify PublisherId matches. + if (CompareStringOrdinal(dependencyPackage.packageId.publisherId, -1, WINDOWSAPPRUNTIME_PACKAGE_PUBLISHERID, -1, TRUE) != CSTR_EQUAL) + { + continue; + } + + // Verify that the WindowsAppRuntime prefix identifier is in the name. + // This should also be the beginning of the name, so its find position is expected to be 0. + std::wstring dependencyPackageName{ dependencyPackage.packageId.name }; + if (dependencyPackageName.find(WINDOWSAPPRUNTIME_PACKAGE_NAME_PREFIX) != 0) + { + continue; + } + + // On WindowsAppSDK 1.1+, there is no need to check and rule out Main, Singleton and DDLM Package identifiers as their names don't have a overlap with WINDOWSAPPRUNTIME_PACKAGE_NAME_PREFIX. + + return winrt::hstring(dependencyPackage.packageFullName); + } + + THROW_WIN32(ERROR_NOT_FOUND); + } +} diff --git a/dev/Deployment/PackageUtilities.h b/dev/Deployment/PackageUtilities.h new file mode 100644 index 0000000000..3dd71a9e40 --- /dev/null +++ b/dev/Deployment/PackageUtilities.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace WindowsAppRuntime::Deployment::Package +{ + std::wstring GetPackagePath(std::wstring const& packageFullName); + + std::vector FindPackagesByFamily(std::wstring const& packageFamilyName); + + HRESULT VerifyPackage(const std::wstring& packageFamilyName, const PACKAGE_VERSION targetVersion, const std::wstring& packageIdentifier); + + MddCore::PackageInfo GetPackageInfoForPackage(std::wstring const& packageFullName); + + winrt::hstring GetCurrentFrameworkPackageFullName(); +} diff --git a/test/Deployment/API/pch.h b/test/Deployment/API/pch.h index ece1c28e92..a8b318f6ea 100644 --- a/test/Deployment/API/pch.h +++ b/test/Deployment/API/pch.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation and Contributors. +// Copyright (c) Microsoft Corporation and Contributors. // Licensed under the MIT License. #ifndef PCH_H diff --git a/test/DeploymentUnitTests/DeploymentUnitTests.testdef b/test/DeploymentUnitTests/DeploymentUnitTests.testdef new file mode 100644 index 0000000000..d4c7858ec6 --- /dev/null +++ b/test/DeploymentUnitTests/DeploymentUnitTests.testdef @@ -0,0 +1,11 @@ +{ + "Tests": [ + { + "Description": "Deployment Unit Tests", + "Filename": "DeploymentUnitTests.dll", + "Parameters": "", + "Architectures": ["x64", "x86", "arm64"], + "Status": "Enabled" + } + ] +} diff --git a/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj b/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj new file mode 100644 index 0000000000..65110223c2 --- /dev/null +++ b/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj @@ -0,0 +1,273 @@ + + + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {F2A9B8E7-4C62-4D89-9A4F-829F8E2A7E34} + Win32Proj + Test.DeploymentUnitTests + 10.0 + NativeUnitTestProject + DeploymentUnitTests + + + DynamicLibrary + true + v143 + Unicode + false + + + DynamicLibrary + false + v143 + Unicode + false + + + DynamicLibrary + true + v143 + Unicode + false + + + DynamicLibrary + true + v143 + Unicode + false + + + DynamicLibrary + false + v143 + Unicode + false + + + DynamicLibrary + false + v143 + Unicode + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + WIN32;NDEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + WIN32;_DEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + _DEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + _DEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + NDEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Use + %(AdditionalIncludeDirectories);$(OutDir)\..\WindowsAppRuntime_DLL;$(RepoRoot)\test\inc;$(OutDir)\..\WindowsAppRuntime_BootstrapDLL;$(RepoRoot)\dev\Deployment;$(RepoRoot)\dev\Common + NDEBUG;%(PreprocessorDefinitions);;INLINE_TEST_METHOD_MARKUP + true + pch.h + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories);$(OutDir)\..\WindowsAppRuntime_DLL + onecore.lib;onecoreuap.lib;Microsoft.WindowsAppRuntime.lib;wex.common.lib;wex.logger.lib;te.common.lib;%(AdditionalDependencies) + + + $(RepoRoot)\dev\common + + + + + Create + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + .Debug + + + + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.Windows.ApplicationModel.WindowsAppRuntime.winmd + 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/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj.filters b/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj.filters new file mode 100644 index 0000000000..f9ceced99d --- /dev/null +++ b/test/DeploymentUnitTests/DeploymentUnitTests.vcxproj.filters @@ -0,0 +1,87 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;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 + + + {5F8E8F1A-1B2C-4D3E-9F4A-1B2C3D4E5F6A} + + + {6A9B8C7D-2C3D-4E5F-A6B7-2C3D4E5F6A7B} + + + + + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF} + + + + + Header Files + + + + External\Deployment + + + External\Deployment + + + External\Deployment + + + External\Deployment + + + + + Source Files + + + Source Files + + + Source Files + + + + External\Deployment + + + External\Deployment + + + External\Deployment + + + External\Deployment + + + + + + + + Test Data\MSIX + + + Test Data\MSIX + + + Test Data\MSIX + + + Test Data\MSIX + + + diff --git a/test/DeploymentUnitTests/LicensingTests.cpp b/test/DeploymentUnitTests/LicensingTests.cpp new file mode 100644 index 0000000000..3fe7864cdd --- /dev/null +++ b/test/DeploymentUnitTests/LicensingTests.cpp @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace winrt; + +namespace Test::Deployment::Licensing +{ + // Mock implementation of ILicenseInstaller for testing + struct MockLicenseInstaller : public WindowsAppRuntime::Deployment::Licensing::ILicenseInstaller + { + std::vector m_installedFiles; + std::unordered_map m_expectedFailureMap; + bool m_shouldTrhowException = false; + + MockLicenseInstaller() = default; + + // Set up the mock to fail on a specific file + void SetupFailureOnFile(const std::wstring& filename, HRESULT errorCode) + { + m_expectedFailureMap[filename] = errorCode; + } + + void SetShouldThrowException(bool shouldThrow) + { + m_shouldTrhowException = shouldThrow; + } + + HRESULT InstallLicenseFile(const std::wstring& licenseFilename) override + { + if (m_shouldTrhowException) + { + throw std::runtime_error("Test exception"); + } + + for (auto it : m_expectedFailureMap) + { + auto filename {it.first}; + if(licenseFilename.find(filename) != std::wstring::npos) + { + return it.second; + } + } + + m_installedFiles.push_back(licenseFilename); + return S_OK; + } + + // Test helper methods + const std::vector& GetInstalledFiles() const { return m_installedFiles; } + void Reset() { m_installedFiles.clear(); m_expectedFailureMap.clear(); } + size_t GetInstallCount() const { return m_installedFiles.size(); } + }; + + class LicensingTests + { + public: + BEGIN_TEST_CLASS(LicensingTests) + TEST_CLASS_PROPERTY(L"ThreadingModel", L"MTA") + END_TEST_CLASS() + + TEST_CLASS_SETUP(ClassInit) + { + return true; + } + + TEST_CLASS_CLEANUP(ClassUninit) + { + return true; + } + + // GetLicenseFiles Tests + TEST_METHOD(GetLicenseFiles_NoFilesFound_ReturnsSuccessWithEmptyVector) + { + Log::Comment(L"Test GetLicenseFiles with non-existent path returns success with empty vector"); + + std::vector licenseFiles; + std::wstring nonExistentPath = L"C:\\NonExistent\\Path\\*_license.xml"; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(nonExistentPath, licenseFiles); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(licenseFiles.size(), 0u); + Log::Comment(L"GetLicenseFiles correctly handled non-existent path"); + } + + TEST_METHOD(GetLicenseFiles_EmptyFileSpec_Succeeds) + { + Log::Comment(L"Test GetLicenseFiles with empty file specification"); + + std::vector licenseFiles; + std::wstring emptyPath = L""; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(emptyPath, licenseFiles); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(licenseFiles.size(), 0u); + Log::Comment(L"GetLicenseFiles correctly handled empty file specification"); + } + + TEST_METHOD(GetLicenseFiles_InvalidPath_ReturnsError) + { + Log::Comment(L"Test GetLicenseFiles with invalid path characters"); + + std::vector licenseFiles; + std::wstring invalidPath = L"C:\\Invalid|Path\\*_license.xml"; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(invalidPath, licenseFiles); + + VERIFY_IS_TRUE(FAILED(hr)); + Log::Comment(String().Format(L"GetLicenseFiles correctly failed with invalid path, HR: 0x%08X", hr)); + } + + TEST_METHOD(GetLicenseFiles_ClearsOutputVector) + { + Log::Comment(L"Test GetLicenseFiles clears the output vector"); + + std::vector licenseFiles; + // Pre-populate the vector + licenseFiles.push_back(L"existing_file.xml"); + licenseFiles.push_back(L"another_file.xml"); + VERIFY_ARE_EQUAL(licenseFiles.size(), 2u); + + std::wstring nonExistentPath = L"C:\\NonExistent\\Path\\*_license.xml"; + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(nonExistentPath, licenseFiles); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(licenseFiles.size(), 0u); + Log::Comment(L"GetLicenseFiles correctly cleared the output vector"); + } + + TEST_METHOD(GetLicenseFiles_WithRealFiles_FindsAllLicenseFiles) + { + Log::Comment(L"Test GetLicenseFiles with real mock license files"); + + std::vector licenseFiles; + + // Get the current test directory and construct the MSIX path + wchar_t currentDir[MAX_PATH]; + GetCurrentDirectory(MAX_PATH, currentDir); + std::wstring testPath = std::wstring(currentDir) + L"\\test\\DeploymentUnitTests\\MSIX\\*_license.xml"; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(testPath, licenseFiles); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(licenseFiles.size(), 3u); + + VERIFY_ARE_EQUAL(licenseFiles[0], L"a_license.xml"); + VERIFY_ARE_EQUAL(licenseFiles[1], L"b_license.xml"); + // Note: preserves case of original name + VERIFY_ARE_EQUAL(licenseFiles[2], L"c_License.xml"); + + Log::Comment(L"GetLicenseFiles correctly found all 3 license files"); + } + + // InstallLicenses Tests + TEST_METHOD(InstallLicenses_EmptyLicenseList_ReturnsSuccess) + { + Log::Comment(L"Test InstallLicenses with empty license file list"); + + std::vector licenseFiles; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(mockInstaller.GetInstallCount(), 0u); + Log::Comment(L"InstallLicenses correctly handled empty license list"); + } + + TEST_METHOD(InstallLicenses_SingleLicenseFile_InstallsSuccessfully) + { + Log::Comment(L"Test InstallLicenses with single license file"); + + std::vector licenseFiles = { L"test_license.xml" }; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(mockInstaller.GetInstallCount(), 1u); + + const auto& installedFiles = mockInstaller.GetInstalledFiles(); + VERIFY_IS_TRUE(installedFiles[0].find(L"test_license.xml") != std::wstring::npos); + VERIFY_IS_TRUE(installedFiles[0].find(L"C:\\TestPath") != std::wstring::npos); + + Log::Comment(String().Format(L"Successfully installed: %s", installedFiles[0].c_str())); + } + + TEST_METHOD(InstallLicenses_MultipleLicenseFiles_InstallsAll) + { + Log::Comment(L"Test InstallLicenses with multiple license files"); + + std::vector licenseFiles = { + L"license1.xml", + L"license2.xml", + L"license3.xml" + }; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(mockInstaller.GetInstallCount(), 3u); + + const auto& installedFiles = mockInstaller.GetInstalledFiles(); + for (size_t i = 0; i < licenseFiles.size(); ++i) + { + VERIFY_IS_TRUE(installedFiles[i].find(licenseFiles[i]) != std::wstring::npos); + Log::Comment(String().Format(L"Installed file %zu: %s", i + 1, installedFiles[i].c_str())); + } + } + + TEST_METHOD(InstallLicenses_InstallerFails_ReturnsError) + { + Log::Comment(L"Test InstallLicenses when installer fails"); + + std::vector licenseFiles = { L"failing_license.xml" }; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + mockInstaller.SetupFailureOnFile(L"failing_license.xml", E_ACCESSDENIED); + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_ARE_EQUAL(hr, E_ACCESSDENIED); + Log::Comment(String().Format(L"InstallLicenses correctly returned error: 0x%08X", hr)); + } + + TEST_METHOD(InstallLicenses_PartialFailure_StopsOnFirstError) + { + Log::Comment(L"Test InstallLicenses stops on first error in batch"); + + std::vector licenseFiles = { + L"good_license.xml", + L"failing_license.xml", + L"never_reached.xml" + }; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + mockInstaller.SetupFailureOnFile(L"failing_license.xml", E_FAIL); + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_ARE_EQUAL(hr, E_FAIL); + // Should have installed the first file before failing on the second + VERIFY_ARE_EQUAL(mockInstaller.GetInstallCount(), 1u); + + const auto& installedFiles = mockInstaller.GetInstalledFiles(); + VERIFY_IS_TRUE(installedFiles[0].find(L"good_license.xml") != std::wstring::npos); + + Log::Comment(L"InstallLicenses correctly stopped after first failure"); + } + + TEST_METHOD(InstallLicenses_SetsActivityContext) + { + Log::Comment(L"Test InstallLicenses updates activity context properly"); + + std::vector licenseFiles = { L"test_license.xml" }; + std::filesystem::path licensePath = L"C:\\TestPath"; + MockLicenseInstaller mockInstaller; + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + // Reset context to known state + activityContext.Reset(); + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_SUCCEEDED(hr); + + // The function should have set the install stage + // Note: We can't easily verify the stage without exposing it in the API, + // but we can verify the operation completed successfully + Log::Comment(L"InstallLicenses completed with activity context updates"); + } + + TEST_METHOD(InstallLicenses_CorrectPathCombination) + { + Log::Comment(L"Test InstallLicenses correctly combines path and filename"); + + std::vector licenseFiles = { L"myapp_license.xml" }; + std::filesystem::path licensePath = L"C:\\Program Files\\TestApp\\Licenses"; + MockLicenseInstaller mockInstaller; + WindowsAppRuntime::Deployment::Activity::Context activityContext; + + HRESULT hr = WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + VERIFY_SUCCEEDED(hr); + VERIFY_ARE_EQUAL(mockInstaller.GetInstallCount(), 1u); + + const auto& installedFiles = mockInstaller.GetInstalledFiles(); + std::wstring expectedPath = L"C:\\Program Files\\TestApp\\Licenses\\myapp_license.xml"; + + // Normalize path separators for comparison + std::wstring actualPath = installedFiles[0]; + VERIFY_IS_TRUE(actualPath.find(L"TestApp") != std::wstring::npos); + VERIFY_IS_TRUE(actualPath.find(L"Licenses") != std::wstring::npos); + VERIFY_IS_TRUE(actualPath.find(L"myapp_license.xml") != std::wstring::npos); + + Log::Comment(String().Format(L"Correct path combination: %s", actualPath.c_str())); + } + + TEST_METHOD(InstallLicenses_CorruptedLicenseFile_HandlesException) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Ignore", L"True") // Currently failing + END_TEST_METHOD_PROPERTIES() + + std::vector licenseFiles = { L"corrupted_license.xml" }; + std::filesystem::path licensePath = L"C:\\Program Files\\TestApp\\Licenses"; + MockLicenseInstaller mockInstaller; + ::WindowsAppRuntime::Deployment::Activity::Context activityContext{}; + + // Mock the installer to simulate exception during license processing + mockInstaller.SetShouldThrowException(true); + + auto hr = ::WindowsAppRuntime::Deployment::Licensing::InstallLicenses( + licenseFiles, licensePath, mockInstaller, activityContext); + + // Should handle license processing exceptions gracefully + VERIFY_IS_TRUE(FAILED(hr)); + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); + } + + TEST_METHOD(GetLicenseFiles_FileSystemException_HandlesGracefully) + { + // Test with path that could cause file system exceptions + std::wstring problematicPath = L"\\\\?\\C:\\System Volume Information\\*_license.xml"; // Restricted access + std::vector licenseFiles; + + auto hr = ::WindowsAppRuntime::Deployment::Licensing::GetLicenseFiles(problematicPath, licenseFiles); + + // Should handle file system access issues gracefully + VERIFY_IS_TRUE(SUCCEEDED(hr) || FAILED(hr)); // Either way, shouldn't throw unhandled exception + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); + } + }; +} diff --git a/test/DeploymentUnitTests/MSIX/AppxManifest.xml b/test/DeploymentUnitTests/MSIX/AppxManifest.xml new file mode 100644 index 0000000000..f90e279eb0 --- /dev/null +++ b/test/DeploymentUnitTests/MSIX/AppxManifest.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/test/DeploymentUnitTests/MSIX/a_license.xml b/test/DeploymentUnitTests/MSIX/a_license.xml new file mode 100644 index 0000000000..2cb0693bc7 --- /dev/null +++ b/test/DeploymentUnitTests/MSIX/a_license.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/DeploymentUnitTests/MSIX/b_license.xml b/test/DeploymentUnitTests/MSIX/b_license.xml new file mode 100644 index 0000000000..89ed42eac7 --- /dev/null +++ b/test/DeploymentUnitTests/MSIX/b_license.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/DeploymentUnitTests/MSIX/c_License.xml b/test/DeploymentUnitTests/MSIX/c_License.xml new file mode 100644 index 0000000000..7f7d442561 --- /dev/null +++ b/test/DeploymentUnitTests/MSIX/c_License.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/DeploymentUnitTests/PackageDeploymentTests.cpp b/test/DeploymentUnitTests/PackageDeploymentTests.cpp new file mode 100644 index 0000000000..eea39437aa --- /dev/null +++ b/test/DeploymentUnitTests/PackageDeploymentTests.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include +#include +#include +#include +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace winrt; + +namespace Test::Deployment::PackageDeployment +{ + class PackageDeploymentTests + { + public: + BEGIN_TEST_CLASS(PackageDeploymentTests) + TEST_CLASS_PROPERTY(L"ThreadingModel", L"MTA") + END_TEST_CLASS() + + TEST_CLASS_SETUP(ClassInit) + { + return true; + } + + TEST_CLASS_CLEANUP(ClassUninit) + { + return true; + } + + TEST_METHOD(DeployPackages_PackageManagerException_HandlesGracefully) + { + std::vector args; + ::WindowsAppRuntime::Deployment::Activity::Context activityContext{}; + + // Add invalid package argument that could cause exceptions + args.push_back({ + L"TestPackage", + std::filesystem::path(L"\\\\invalid\\path\\package.msix"), + false, + false + }); + + auto hr = ::WindowsAppRuntime::Deployment::PackageDeployment::DeployPackages( + args, false, activityContext); + + // Should handle package deployment exceptions gracefully + VERIFY_IS_TRUE(FAILED(hr)); + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); + } + }; +} diff --git a/test/DeploymentUnitTests/PackageRegistrarTests.cpp b/test/DeploymentUnitTests/PackageRegistrarTests.cpp new file mode 100644 index 0000000000..bd22914740 --- /dev/null +++ b/test/DeploymentUnitTests/PackageRegistrarTests.cpp @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#include "pch.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace winrt; +using namespace winrt::Windows::Management::Deployment; + +namespace Test::Deployment +{ + class PackageRegistrarTests + { + public: + BEGIN_TEST_CLASS(PackageRegistrarTests) + TEST_CLASS_PROPERTY(L"ThreadingModel", L"MTA") + END_TEST_CLASS() + + TEST_CLASS_SETUP(ClassInit) + { + return true; + } + + TEST_CLASS_CLEANUP(ClassUninit) + { + return true; + } + + // Path validation tests + TEST_METHOD(PackageRegistrar_GenerateDeploymentAgentPath_PathExists) + { + Log::Comment(L"Test that generated path points to an existing location"); + + auto path = WindowsAppRuntime::Deployment::PackageRegistrar::GenerateDeploymentAgentPath(); + std::filesystem::path fsPath(path); + + // The directory should exist (even if the exe doesn't) + auto parentPath = fsPath.parent_path(); + VERIFY_IS_TRUE(std::filesystem::exists(parentPath)); + + Log::Comment(String().Format(L"Parent directory exists: %s", parentPath.c_str())); + } + + // AddOrRegisterPackageInBreakAwayProcess tests + TEST_METHOD(PackageRegistrar_AddOrRegisterPackageInBreakAwayProcess_InvalidPath_ReturnsError) + { + Log::Comment(L"Test AddOrRegisterPackageInBreakAwayProcess with invalid path"); + + WindowsAppRuntime::Deployment::Activity::Context activityContext; + std::filesystem::path invalidPath(L"C:\\NonExistent\\Invalid.msix"); + + HRESULT hr = WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackageInBreakAwayProcess( + invalidPath, + false, // useExistingPackageIfHigherVersion + false, // forceDeployment + activityContext, + WindowsAppRuntime::Deployment::PackageRegistrar::GenerateDeploymentAgentPath() + ); + + VERIFY_IS_TRUE(FAILED(hr)); + } + + TEST_METHOD(PackageRegistrar_AddOrRegisterPackageInBreakAwayProcess_CustomDeploymentAgentPath) + { + Log::Comment(L"Test AddOrRegisterPackageInBreakAwayProcess with custom deployment agent path"); + + WindowsAppRuntime::Deployment::Activity::Context activityContext; + std::filesystem::path testPackagePath(L"C:\\test.msix"); + std::wstring customAgentPath = L"C:\\CustomPath\\DeploymentAgent.exe"; + + // Should fail because neither package nor agent exist, but we're testing parameter handling + HRESULT hr = WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackageInBreakAwayProcess( + testPackagePath, + false, + false, + activityContext, + customAgentPath + ); + + VERIFY_IS_TRUE(FAILED(hr)); + } + + TEST_METHOD(AddOrRegisterPackage_InvalidPath_HandlesException) + { + // Maybe we should mock PackageManager to throw exceptions? + auto packageManager = winrt::Windows::Management::Deployment::PackageManager{}; + ::WindowsAppRuntime::Deployment::Activity::Context activityContext{}; + + // Test with invalid/malformed path that could cause unhandled exceptions + std::filesystem::path invalidPath{ L"\\\\invalid-unc-path\\nonexistent\\path\\package.msix" }; + + auto hr = ::WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackage( + invalidPath, false, false, packageManager, activityContext); + + // Should return a proper HRESULT, not throw unhandled exception + VERIFY_IS_TRUE(FAILED(hr)); + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); // Should not be ERROR_UNHANDLED_EXCEPTION + } + + TEST_METHOD(AddOrRegisterPackage_CorruptedPackage_HandlesException) + { + auto packageManager = winrt::Windows::Management::Deployment::PackageManager{}; + ::WindowsAppRuntime::Deployment::Activity::Context activityContext{}; + + // Create a corrupted/invalid MSIX file + std::filesystem::path tempPath = std::filesystem::temp_directory_path() / L"corrupted.msix"; + std::ofstream corruptedFile(tempPath, std::ios::binary); + corruptedFile << "This is not a valid MSIX file content"; + corruptedFile.close(); + + auto hr = ::WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackage( + tempPath, false, false, packageManager, activityContext); + + // Cleanup + std::filesystem::remove(tempPath); + + // Should handle the corruption gracefully, not throw unhandled exception + VERIFY_IS_TRUE(FAILED(hr)); + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); + } + + TEST_METHOD(AddOrRegisterPackageInBreakAwayProcess_InvalidAgentPath_HandlesException) + { + ::WindowsAppRuntime::Deployment::Activity::Context activityContext{}; + std::filesystem::path validPackagePath = std::filesystem::temp_directory_path() / L"test.msix"; + std::wstring invalidAgentPath = L"C:\\NonExistent\\deploymentagent.exe"; + + auto hr = ::WindowsAppRuntime::Deployment::PackageRegistrar::AddOrRegisterPackageInBreakAwayProcess( + validPackagePath, false, false, activityContext, invalidAgentPath); + + // Should fail gracefully, not throw unhandled exception + VERIFY_IS_TRUE(FAILED(hr)); + VERIFY_ARE_NOT_EQUAL(hr, static_cast(0x8007023E)); + } + + TEST_METHOD(ProcessDeploymentOperationResult_AsyncOperationException_HandlesGracefully) + { + // TODO: Mock scenario where deploymentOperation.get() throws + // This test would require mocking the async operation to throw exceptions + // We'd need to create a mock IAsyncOperationWithProgress that throws + // We want to test the function: + // HRESULT WindowsAppRuntime::Deployment::PackageRegistrar::ProcessDeploymentOperationResult() + + // For now, keep this as document of a test case that should be implemented + Log::Comment(L"Test case: Verify ProcessDeploymentOperationResult handles C++/WinRT exceptions from async operations"); + } + }; +} diff --git a/test/DeploymentUnitTests/packages.config b/test/DeploymentUnitTests/packages.config new file mode 100644 index 0000000000..d5bdb8bcfc --- /dev/null +++ b/test/DeploymentUnitTests/packages.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/DeploymentUnitTests/pch.cpp b/test/DeploymentUnitTests/pch.cpp new file mode 100644 index 0000000000..a77728ba07 --- /dev/null +++ b/test/DeploymentUnitTests/pch.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/test/DeploymentUnitTests/pch.h b/test/DeploymentUnitTests/pch.h new file mode 100644 index 0000000000..1bed8195bc --- /dev/null +++ b/test/DeploymentUnitTests/pch.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +#ifndef PCH_H +#define PCH_H +#endif //PCH_H + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include + +#include +namespace TP = ::Test::Packages;