diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 42fb16ffcb..f85234da1b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -310,6 +310,7 @@ liwpx localizationpriority localsource Logon +logon LONGLONG LOWORD LPARAM @@ -587,7 +588,9 @@ sqlite Srinivasan SRL srs +STARTF STARTUPINFOEX +STARTUPINFOW STDMETHODCALLTYPE storeapps storeorigin @@ -658,6 +661,7 @@ upgradeable upgradecode URLZONE USEDEFAULT +USESHOWWINDOW userfilesetting userprofile uswgp diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp index 5c24bfc3d6..4a520a60c7 100644 --- a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -430,6 +430,24 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED); } + // In non-interactive sessions (Session 0, SSH, WinRM, CI), the AppX deployment broker + // requires an interactive logon session when invoked from a packaged context, returning + // ERROR_NO_SUCH_LOGON_SESSION (0x80070520). If the package is not registered for the user + // (e.g. removed or absent), avoid reporting a false failure. + if (re.GetErrorCode() == HRESULT_FROM_WIN32(ERROR_NO_SUCH_LOGON_SESSION)) + { + if (!packageFamilyName.empty() && !Deployment::IsRegistered(packageFamilyName)) + { + AICLI_LOG(CLI, Info, << "Package " << packageFamilyName << " is not registered for user; ignoring ERROR_NO_SUCH_LOGON_SESSION (0x80070520)"); + continue; + } + + if (!Runtime::IsRunningAsInteractiveUser() || Runtime::IsRunningInPackagedContext()) + { + AICLI_LOG(CLI, Warning, << "Non-interactive MSIX uninstall encountered ERROR_NO_SUCH_LOGON_SESSION (0x80070520) in packaged context for " << packageFamilyName); + } + } + context.Add(re.GetErrorCode()); context << ReportUninstallerResult("MSIXUninstall"sv, re.GetErrorCode(), /* isHResult */ true); return; diff --git a/src/AppInstallerCLITests/UninstallFlow.cpp b/src/AppInstallerCLITests/UninstallFlow.cpp index 52ce44b812..41fdf59d88 100644 --- a/src/AppInstallerCLITests/UninstallFlow.cpp +++ b/src/AppInstallerCLITests/UninstallFlow.cpp @@ -254,3 +254,21 @@ TEST_CASE("UninstallFlow_AdminContextWithUserScopeInstall", "[UninstallFlow][wor REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED); } + +TEST_CASE("UninstallFlow_MSIXUninstall_NonInteractiveSessionNoError", "[UninstallFlow][workflow]") +{ + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + + OverrideForCompositeInstalledSource(context, CreateTestSource({ TSR::TestInstaller_Msix })); + OverrideForMSIXUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Msix.Query); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + REQUIRE(!context.IsTerminated()); +} + diff --git a/src/AppInstallerCommonCore/Deployment.cpp b/src/AppInstallerCommonCore/Deployment.cpp index 4b054ff742..01e842b22f 100644 --- a/src/AppInstallerCommonCore/Deployment.cpp +++ b/src/AppInstallerCommonCore/Deployment.cpp @@ -264,6 +264,43 @@ namespace AppInstaller::Deployment return registrationDeferred; } + bool RemovePackageUnpackaged(std::string_view packageFullName) + { + AICLI_LOG(Core, Info, << "Attempting unpackaged Remove-AppxPackage fallback for: " << packageFullName); + + std::wstring commandLine = L"powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -Command \"Remove-AppxPackage -Package '" + Utility::ConvertToUTF16(packageFullName) + L"'\""; + + STARTUPINFOW startupInfo = { sizeof(startupInfo) }; + startupInfo.dwFlags = STARTF_USESHOWWINDOW; + startupInfo.wShowWindow = SW_HIDE; + wil::unique_process_information processInfo; + + if (!CreateProcessW(nullptr, &commandLine[0], nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startupInfo, &processInfo)) + { + AICLI_LOG(Core, Warning, << "Failed to create PowerShell process for Remove-AppxPackage fallback: " << GetLastError()); + return false; + } + + DWORD waitResult = WaitForSingleObject(processInfo.hProcess, 60000); + if (waitResult == WAIT_OBJECT_0) + { + DWORD exitCode = 0; + if (GetExitCodeProcess(processInfo.hProcess, &exitCode) && exitCode == 0) + { + AICLI_LOG(Core, Info, << "Unpackaged Remove-AppxPackage succeeded for: " << packageFullName); + return true; + } + AICLI_LOG(Core, Warning, << "Unpackaged Remove-AppxPackage returned non-zero exit code: " << exitCode); + } + else + { + AICLI_LOG(Core, Warning, << "Unpackaged Remove-AppxPackage wait failed or timed out: " << waitResult); + TerminateProcess(processInfo.hProcess, 1); + } + + return false; + } + void RemovePackage( std::string_view packageFullName, RemovalOptions options, @@ -276,7 +313,39 @@ namespace AppInstaller::Deployment winrt::hstring fullName = Utility::ConvertToUTF16(packageFullName).c_str(); auto deployOperation = packageManager.RemovePackageAsync(fullName, options); - WaitForDeployment(deployOperation, id, callback); + HRESULT hr = WaitForDeployment(deployOperation, id, callback, false); + if (!SUCCEEDED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_NO_SUCH_LOGON_SESSION)) + { + AICLI_LOG(Core, Warning, << "RemovePackage operation #" << id << " received ERROR_NO_SUCH_LOGON_SESSION (0x80070520) in non-interactive session for " << packageFullName); + + std::string packageFamilyName = Msix::GetPackageFamilyNameFromFullName(std::string{ packageFullName }); + if (!packageFamilyName.empty() && !IsRegistered(packageFamilyName)) + { + AICLI_LOG(Core, Info, << "Package " << packageFamilyName << " is not registered for current user, treating as successful removal"); + return; + } + + // If running in packaged context or non-interactive session, fall back to unpackaged removal + if (!Runtime::IsRunningAsInteractiveUser() || Runtime::IsRunningInPackagedContext()) + { + if (RemovePackageUnpackaged(packageFullName)) + { + return; + } + + // Check again if the package registration was removed + if (!packageFamilyName.empty() && !IsRegistered(packageFamilyName)) + { + AICLI_LOG(Core, Info, << "Package " << packageFamilyName << " is no longer registered after fallback attempt"); + return; + } + } + } + + THROW_IF_FAILED(hr); + } } bool AddPackageMachineScope(