aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Hall <r.sean.hall@gmail.com>2022-09-02 16:12:42 -0500
committerSean Hall <r.sean.hall@gmail.com>2022-09-03 15:29:26 -0500
commit237bdc6a97c0dd455da99c16e6b3b7cac4c79e86 (patch)
tree1f2c789a7aa752e068fac8a4ca08815d13bf527a
parent7728e34e48a4fdb710ecc92dd8dca833bff3993f (diff)
downloadwix-237bdc6a97c0dd455da99c16e6b3b7cac4c79e86.tar.gz
wix-237bdc6a97c0dd455da99c16e6b3b7cac4c79e86.tar.bz2
wix-237bdc6a97c0dd455da99c16e6b3b7cac4c79e86.zip
Add WixCanRestart and make wixstdba elevate for restart when required.
Fixes 5499
-rw-r--r--src/burn/engine/EngineForApplication.h16
-rw-r--r--src/burn/engine/core.cpp7
-rw-r--r--src/burn/engine/core.h1
-rw-r--r--src/burn/engine/elevation.cpp7
-rw-r--r--src/burn/engine/elevation.h1
-rw-r--r--src/burn/engine/engine.cpp23
-rw-r--r--src/burn/engine/platform.h16
-rw-r--r--src/burn/engine/variable.cpp25
-rw-r--r--src/burn/test/BurnUnitTest/ElevationTest.cpp2
-rw-r--r--src/burn/test/BurnUnitTest/ExitCodeTest.cpp2
-rw-r--r--src/burn/test/BurnUnitTest/VariableTest.cpp1
-rw-r--r--src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp63
-rw-r--r--src/libs/dutil/WixToolset.DUtil/inc/procutil.h16
-rw-r--r--src/libs/dutil/WixToolset.DUtil/procutil.cpp106
-rw-r--r--src/libs/dutil/test/DUtilUnitTest/ProcUtilTest.cpp28
-rw-r--r--src/test/burn/TestData/Manual/BafThmutilTesting/BafThmUtilTesting.cpp2
-rw-r--r--src/test/burn/TestData/Manual/BundleA/ManualTests.txt54
-rw-r--r--src/test/burn/TestData/Manual/BundleC/BundleC.wixproj18
-rw-r--r--src/test/burn/TestData/Manual/BundleC/BundleC.wxs15
-rw-r--r--src/test/burn/TestData/Manual/PackagePerUser/PackagePerUser.wixproj9
20 files changed, 336 insertions, 76 deletions
diff --git a/src/burn/engine/EngineForApplication.h b/src/burn/engine/EngineForApplication.h
index e5e8f6d7..d25a7e51 100644
--- a/src/burn/engine/EngineForApplication.h
+++ b/src/burn/engine/EngineForApplication.h
@@ -6,22 +6,6 @@
6extern "C" { 6extern "C" {
7#endif 7#endif
8 8
9// constants
10
11enum WM_BURN
12{
13 WM_BURN_FIRST = WM_APP + 0xFFF, // this enum value must always be first.
14
15 WM_BURN_DETECT,
16 WM_BURN_PLAN,
17 WM_BURN_ELEVATE,
18 WM_BURN_APPLY,
19 WM_BURN_LAUNCH_APPROVED_EXE,
20 WM_BURN_QUIT,
21
22 WM_BURN_LAST, // this enum value must always be last.
23};
24
25// structs 9// structs
26 10
27typedef struct _BOOTSTRAPPER_ENGINE_CONTEXT 11typedef struct _BOOTSTRAPPER_ENGINE_CONTEXT
diff --git a/src/burn/engine/core.cpp b/src/burn/engine/core.cpp
index c8dce17b..93b9c002 100644
--- a/src/burn/engine/core.cpp
+++ b/src/burn/engine/core.cpp
@@ -593,6 +593,7 @@ LExit:
593 593
594extern "C" HRESULT CoreElevate( 594extern "C" HRESULT CoreElevate(
595 __in BURN_ENGINE_STATE* pEngineState, 595 __in BURN_ENGINE_STATE* pEngineState,
596 __in WM_BURN reason,
596 __in_opt HWND hwndParent 597 __in_opt HWND hwndParent
597 ) 598 )
598{ 599{
@@ -608,7 +609,7 @@ extern "C" HRESULT CoreElevate(
608 ExitOnFailure(hr, "Failed to cache engine to working directory."); 609 ExitOnFailure(hr, "Failed to cache engine to working directory.");
609 } 610 }
610 611
611 hr = ElevationElevate(pEngineState, hwndParent); 612 hr = ElevationElevate(pEngineState, reason, hwndParent);
612 if (E_SUSPECTED_AV_INTERFERENCE == hr && 1 > cAVRetryAttempts) 613 if (E_SUSPECTED_AV_INTERFERENCE == hr && 1 > cAVRetryAttempts)
613 { 614 {
614 ++cAVRetryAttempts; 615 ++cAVRetryAttempts;
@@ -720,7 +721,7 @@ extern "C" HRESULT CoreApply(
720 // Elevate. 721 // Elevate.
721 if (pEngineState->plan.fPerMachine) 722 if (pEngineState->plan.fPerMachine)
722 { 723 {
723 hr = CoreElevate(pEngineState, pEngineState->userExperience.hwndApply); 724 hr = CoreElevate(pEngineState, WM_BURN_APPLY, pEngineState->userExperience.hwndApply);
724 ExitOnFailure(hr, "Failed to elevate."); 725 ExitOnFailure(hr, "Failed to elevate.");
725 726
726 hr = ElevationApplyInitialize(pEngineState->companionConnection.hPipe, &pEngineState->userExperience, &pEngineState->variables, &pEngineState->plan); 727 hr = ElevationApplyInitialize(pEngineState->companionConnection.hPipe, &pEngineState->userExperience, &pEngineState->variables, &pEngineState->plan);
@@ -872,7 +873,7 @@ extern "C" HRESULT CoreLaunchApprovedExe(
872 ExitOnRootFailure(hr, "BA aborted LaunchApprovedExe begin."); 873 ExitOnRootFailure(hr, "BA aborted LaunchApprovedExe begin.");
873 874
874 // Elevate. 875 // Elevate.
875 hr = CoreElevate(pEngineState, pLaunchApprovedExe->hwndParent); 876 hr = CoreElevate(pEngineState, WM_BURN_LAUNCH_APPROVED_EXE, pLaunchApprovedExe->hwndParent);
876 ExitOnFailure(hr, "Failed to elevate."); 877 ExitOnFailure(hr, "Failed to elevate.");
877 878
878 // Launch. 879 // Launch.
diff --git a/src/burn/engine/core.h b/src/burn/engine/core.h
index 812b40b1..3e636640 100644
--- a/src/burn/engine/core.h
+++ b/src/burn/engine/core.h
@@ -251,6 +251,7 @@ HRESULT CorePlan(
251 ); 251 );
252HRESULT CoreElevate( 252HRESULT CoreElevate(
253 __in BURN_ENGINE_STATE* pEngineState, 253 __in BURN_ENGINE_STATE* pEngineState,
254 __in WM_BURN reason,
254 __in_opt HWND hwndParent 255 __in_opt HWND hwndParent
255 ); 256 );
256HRESULT CoreApply( 257HRESULT CoreApply(
diff --git a/src/burn/engine/elevation.cpp b/src/burn/engine/elevation.cpp
index 4d2e8544..7fd372b0 100644
--- a/src/burn/engine/elevation.cpp
+++ b/src/burn/engine/elevation.cpp
@@ -402,6 +402,7 @@ static HRESULT ElevatedOnExecuteActionComplete(
402 402
403extern "C" HRESULT ElevationElevate( 403extern "C" HRESULT ElevationElevate(
404 __in BURN_ENGINE_STATE* pEngineState, 404 __in BURN_ENGINE_STATE* pEngineState,
405 __in WM_BURN reason,
405 __in_opt HWND hwndParent 406 __in_opt HWND hwndParent
406 ) 407 )
407{ 408{
@@ -450,7 +451,11 @@ extern "C" HRESULT ElevationElevate(
450 else if (HRESULT_FROM_WIN32(ERROR_CANCELLED) == hr) 451 else if (HRESULT_FROM_WIN32(ERROR_CANCELLED) == hr)
451 { 452 {
452 // The user clicked "Cancel" on the elevation prompt or the elevation prompt timed out, provide the notification with the option to retry. 453 // The user clicked "Cancel" on the elevation prompt or the elevation prompt timed out, provide the notification with the option to retry.
453 hr = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT); 454 if (WM_BURN_APPLY == reason)
455 {
456 hr = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT);
457 }
458
454 nResult = UserExperienceSendError(&pEngineState->userExperience, BOOTSTRAPPER_ERROR_TYPE_ELEVATE, NULL, hr, NULL, MB_ICONERROR | MB_RETRYCANCEL, IDNOACTION); 459 nResult = UserExperienceSendError(&pEngineState->userExperience, BOOTSTRAPPER_ERROR_TYPE_ELEVATE, NULL, hr, NULL, MB_ICONERROR | MB_RETRYCANCEL, IDNOACTION);
455 } 460 }
456 } while (IDRETRY == nResult); 461 } while (IDRETRY == nResult);
diff --git a/src/burn/engine/elevation.h b/src/burn/engine/elevation.h
index 0d15b470..810287a3 100644
--- a/src/burn/engine/elevation.h
+++ b/src/burn/engine/elevation.h
@@ -10,6 +10,7 @@ extern "C" {
10// Parent (per-user process) side functions. 10// Parent (per-user process) side functions.
11HRESULT ElevationElevate( 11HRESULT ElevationElevate(
12 __in BURN_ENGINE_STATE* pEngineState, 12 __in BURN_ENGINE_STATE* pEngineState,
13 __in WM_BURN reason,
13 __in_opt HWND hwndParent 14 __in_opt HWND hwndParent
14 ); 15 );
15HRESULT ElevationApplyInitialize( 16HRESULT ElevationApplyInitialize(
diff --git a/src/burn/engine/engine.cpp b/src/burn/engine/engine.cpp
index 89082a88..48196655 100644
--- a/src/burn/engine/engine.cpp
+++ b/src/burn/engine/engine.cpp
@@ -893,7 +893,7 @@ static HRESULT ProcessMessage(
893 break; 893 break;
894 894
895 case WM_BURN_ELEVATE: 895 case WM_BURN_ELEVATE:
896 hr = CoreElevate(pEngineState, reinterpret_cast<HWND>(pmsg->lParam)); 896 hr = CoreElevate(pEngineState, WM_BURN_ELEVATE, reinterpret_cast<HWND>(pmsg->lParam));
897 break; 897 break;
898 898
899 case WM_BURN_APPLY: 899 case WM_BURN_APPLY:
@@ -1069,26 +1069,10 @@ static HRESULT Restart(
1069 ) 1069 )
1070{ 1070{
1071 HRESULT hr = S_OK; 1071 HRESULT hr = S_OK;
1072 HANDLE hProcessToken = NULL;
1073 TOKEN_PRIVILEGES priv = { };
1074 DWORD dwRetries = 0; 1072 DWORD dwRetries = 0;
1075 1073
1076 if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken)) 1074 hr = ProcEnablePrivilege(::GetCurrentProcess(), SE_SHUTDOWN_NAME);
1077 { 1075 ExitOnFailure(hr, "Failed to enable shutdown privilege in process token.");
1078 ExitWithLastError(hr, "Failed to get process token.");
1079 }
1080
1081 priv.PrivilegeCount = 1;
1082 priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
1083 if (!::LookupPrivilegeValueW(NULL, L"SeShutdownPrivilege", &priv.Privileges[0].Luid))
1084 {
1085 ExitWithLastError(hr, "Failed to get shutdown privilege LUID.");
1086 }
1087
1088 if (!::AdjustTokenPrivileges(hProcessToken, FALSE, &priv, sizeof(TOKEN_PRIVILEGES), NULL, 0))
1089 {
1090 ExitWithLastError(hr, "Failed to adjust token to add shutdown privileges.");
1091 }
1092 1076
1093 pEngineState->fRestarting = TRUE; 1077 pEngineState->fRestarting = TRUE;
1094 CoreUpdateRestartState(pEngineState, BURN_RESTART_STATE_REQUESTING); 1078 CoreUpdateRestartState(pEngineState, BURN_RESTART_STATE_REQUESTING);
@@ -1147,7 +1131,6 @@ static HRESULT Restart(
1147 } 1131 }
1148 1132
1149LExit: 1133LExit:
1150 ReleaseHandle(hProcessToken);
1151 return hr; 1134 return hr;
1152} 1135}
1153 1136
diff --git a/src/burn/engine/platform.h b/src/burn/engine/platform.h
index 49d8f3e9..60184c23 100644
--- a/src/burn/engine/platform.h
+++ b/src/burn/engine/platform.h
@@ -8,6 +8,22 @@
8extern "C" { 8extern "C" {
9#endif 9#endif
10 10
11// constants
12
13enum WM_BURN
14{
15 WM_BURN_FIRST = WM_APP + 0xFFF, // this enum value must always be first.
16
17 WM_BURN_DETECT,
18 WM_BURN_PLAN,
19 WM_BURN_ELEVATE,
20 WM_BURN_APPLY,
21 WM_BURN_LAUNCH_APPROVED_EXE,
22 WM_BURN_QUIT,
23
24 WM_BURN_LAST, // this enum value must always be last.
25};
26
11// forward declare 27// forward declare
12 28
13enum BURN_MODE; 29enum BURN_MODE;
diff --git a/src/burn/engine/variable.cpp b/src/burn/engine/variable.cpp
index 81fa31b7..3947e29e 100644
--- a/src/burn/engine/variable.cpp
+++ b/src/burn/engine/variable.cpp
@@ -153,6 +153,10 @@ static HRESULT InitializeVariablePrivileged(
153 __in DWORD_PTR dwpData, 153 __in DWORD_PTR dwpData,
154 __inout BURN_VARIANT* pValue 154 __inout BURN_VARIANT* pValue
155 ); 155 );
156static HRESULT InitializeVariableProcessTokenPrivilege(
157 __in DWORD_PTR dwpData,
158 __inout BURN_VARIANT* pValue
159 );
156static HRESULT InitializeSystemLanguageID( 160static HRESULT InitializeSystemLanguageID(
157 __in DWORD_PTR dwpData, 161 __in DWORD_PTR dwpData,
158 __inout BURN_VARIANT* pValue 162 __inout BURN_VARIANT* pValue
@@ -284,6 +288,7 @@ extern "C" HRESULT VariableInitialize(
284 {L"WindowsFolder", InitializeVariableCsidlFolder, CSIDL_WINDOWS}, 288 {L"WindowsFolder", InitializeVariableCsidlFolder, CSIDL_WINDOWS},
285 {L"WindowsVolume", InitializeVariableWindowsVolumeFolder, 0}, 289 {L"WindowsVolume", InitializeVariableWindowsVolumeFolder, 0},
286 {BURN_BUNDLE_ACTION, InitializeVariableNumeric, 0, FALSE, TRUE}, 290 {BURN_BUNDLE_ACTION, InitializeVariableNumeric, 0, FALSE, TRUE},
291 {L"WixCanRestart", InitializeVariableProcessTokenPrivilege, (DWORD_PTR)SE_SHUTDOWN_NAME},
287 {BURN_BUNDLE_COMMAND_LINE_ACTION, InitializeVariableNumeric, 0, FALSE, TRUE}, 292 {BURN_BUNDLE_COMMAND_LINE_ACTION, InitializeVariableNumeric, 0, FALSE, TRUE},
288 {BURN_BUNDLE_EXECUTE_PACKAGE_CACHE_FOLDER, InitializeVariableString, NULL, FALSE, TRUE}, 293 {BURN_BUNDLE_EXECUTE_PACKAGE_CACHE_FOLDER, InitializeVariableString, NULL, FALSE, TRUE},
289 {BURN_BUNDLE_EXECUTE_PACKAGE_ACTION, InitializeVariableString, NULL, FALSE, TRUE}, 294 {BURN_BUNDLE_EXECUTE_PACKAGE_ACTION, InitializeVariableString, NULL, FALSE, TRUE},
@@ -2165,6 +2170,26 @@ LExit:
2165 return hr; 2170 return hr;
2166} 2171}
2167 2172
2173static HRESULT InitializeVariableProcessTokenPrivilege(
2174 __in DWORD_PTR dwpData,
2175 __inout BURN_VARIANT* pValue
2176 )
2177{
2178 HRESULT hr = S_OK;
2179 BOOL fHasPrivilege = FALSE;
2180 LPCWSTR wzPrivilegeName = (LPCWSTR)dwpData;
2181
2182 hr = ProcHasPrivilege(::GetCurrentProcess(), wzPrivilegeName, &fHasPrivilege);
2183 ExitOnFailure(hr, "Failed to check if process token has privilege: %ls.", wzPrivilegeName);
2184
2185 // set value
2186 hr = BVariantSetNumeric(pValue, fHasPrivilege);
2187 ExitOnFailure(hr, "Failed to set variant value.");
2188
2189LExit:
2190 return hr;
2191}
2192
2168static HRESULT InitializeSystemLanguageID( 2193static HRESULT InitializeSystemLanguageID(
2169 __in DWORD_PTR dwpData, 2194 __in DWORD_PTR dwpData,
2170 __inout BURN_VARIANT* pValue 2195 __inout BURN_VARIANT* pValue
diff --git a/src/burn/test/BurnUnitTest/ElevationTest.cpp b/src/burn/test/BurnUnitTest/ElevationTest.cpp
index 8d4cc7ff..713d9e07 100644
--- a/src/burn/test/BurnUnitTest/ElevationTest.cpp
+++ b/src/burn/test/BurnUnitTest/ElevationTest.cpp
@@ -68,7 +68,7 @@ namespace Bootstrapper
68 // 68 //
69 // per-user side setup 69 // per-user side setup
70 // 70 //
71 hr = ElevationElevate(&engineState, NULL); 71 hr = ElevationElevate(&engineState, WM_BURN_ELEVATE, NULL);
72 TestThrowOnFailure(hr, L"Failed to elevate."); 72 TestThrowOnFailure(hr, L"Failed to elevate.");
73 73
74 // post execute message 74 // post execute message
diff --git a/src/burn/test/BurnUnitTest/ExitCodeTest.cpp b/src/burn/test/BurnUnitTest/ExitCodeTest.cpp
index d7d91d06..465ee04b 100644
--- a/src/burn/test/BurnUnitTest/ExitCodeTest.cpp
+++ b/src/burn/test/BurnUnitTest/ExitCodeTest.cpp
@@ -118,7 +118,7 @@ static void LoadEngineState(
118 // 118 //
119 LoadEngineState(&engineState); 119 LoadEngineState(&engineState);
120 120
121 hr = ElevationElevate(&engineState, NULL); 121 hr = ElevationElevate(&engineState, WM_BURN_ELEVATE, NULL);
122 TestThrowOnFailure(hr, L"Failed to elevate."); 122 TestThrowOnFailure(hr, L"Failed to elevate.");
123 123
124 for (DWORD i = 0; i < countof(rgExitCodeItems); ++i) 124 for (DWORD i = 0; i < countof(rgExitCodeItems); ++i)
diff --git a/src/burn/test/BurnUnitTest/VariableTest.cpp b/src/burn/test/BurnUnitTest/VariableTest.cpp
index 8ee6e179..b373ae8e 100644
--- a/src/burn/test/BurnUnitTest/VariableTest.cpp
+++ b/src/burn/test/BurnUnitTest/VariableTest.cpp
@@ -564,6 +564,7 @@ namespace Bootstrapper
564 VariableGetNumericHelper(&variables, L"TerminalServer"); 564 VariableGetNumericHelper(&variables, L"TerminalServer");
565 VariableGetNumericHelper(&variables, L"UserUILanguageID"); 565 VariableGetNumericHelper(&variables, L"UserUILanguageID");
566 VariableGetNumericHelper(&variables, L"UserLanguageID"); 566 VariableGetNumericHelper(&variables, L"UserLanguageID");
567 VariableGetNumericHelper(&variables, L"WixCanRestart");
567 568
568 // known folders 569 // known folders
569 Assert::Equal<String^>(Environment::GetFolderPath(Environment::SpecialFolder::ApplicationData) + "\\", VariableGetStringHelper(&variables, L"AppDataFolder")); 570 Assert::Equal<String^>(Environment::GetFolderPath(Environment::SpecialFolder::ApplicationData) + "\\", VariableGetStringHelper(&variables, L"AppDataFolder"));
diff --git a/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp b/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
index 8c4b0b35..50de2c7f 100644
--- a/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
+++ b/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
@@ -4,6 +4,7 @@
4#include "BalBaseBootstrapperApplicationProc.h" 4#include "BalBaseBootstrapperApplicationProc.h"
5#include "BalBaseBootstrapperApplication.h" 5#include "BalBaseBootstrapperApplication.h"
6 6
7static const LPCWSTR WIXBUNDLE_VARIABLE_CANRESTART = L"WixCanRestart";
7static const LPCWSTR WIXBUNDLE_VARIABLE_ELEVATED = L"WixBundleElevated"; 8static const LPCWSTR WIXBUNDLE_VARIABLE_ELEVATED = L"WixBundleElevated";
8 9
9static const LPCWSTR WIXSTDBA_WINDOW_CLASS = L"WixStdBA"; 10static const LPCWSTR WIXSTDBA_WINDOW_CLASS = L"WixStdBA";
@@ -1289,6 +1290,28 @@ public: // IBootstrapperApplication
1289 return S_OK; 1290 return S_OK;
1290 } 1291 }
1291 1292
1293
1294 virtual STDMETHODIMP OnElevateComplete(
1295 __in HRESULT hrStatus
1296 )
1297 {
1298 if (m_fElevatingForRestart)
1299 {
1300 m_fElevatingForRestart = FALSE;
1301
1302 if (SUCCEEDED(hrStatus))
1303 {
1304 m_fAllowRestart = TRUE;
1305
1306 ::SendMessageW(m_hWnd, WM_CLOSE, 0, 0);
1307 }
1308 // else if failed then OnError showed the user an error message box
1309 }
1310
1311 return __super::OnElevateComplete(hrStatus);
1312 }
1313
1314
1292 virtual STDMETHODIMP_(void) BAProcFallback( 1315 virtual STDMETHODIMP_(void) BAProcFallback(
1293 __in BOOTSTRAPPER_APPLICATION_MESSAGE message, 1316 __in BOOTSTRAPPER_APPLICATION_MESSAGE message,
1294 __in const LPVOID pvArgs, 1317 __in const LPVOID pvArgs,
@@ -3734,14 +3757,16 @@ private:
3734 3757
3735 if (dwOldPageId != dwNewPageId) 3758 if (dwOldPageId != dwNewPageId)
3736 { 3759 {
3760 LONGLONG llCanRestart = 0;
3761 LONGLONG llElevated = 0;
3762
3763 BalGetNumericVariable(WIXBUNDLE_VARIABLE_CANRESTART, &llCanRestart);
3764 BalGetNumericVariable(WIXBUNDLE_VARIABLE_ELEVATED, &llElevated);
3765 m_fRestartRequiresElevation = !llCanRestart && !llElevated;
3766
3737 // Enable disable controls per-page. 3767 // Enable disable controls per-page.
3738 if (m_rgdwPageIds[WIXSTDBA_PAGE_INSTALL] == dwNewPageId) // on the "Install" page, ensure the install button is enabled/disabled correctly. 3768 if (m_rgdwPageIds[WIXSTDBA_PAGE_INSTALL] == dwNewPageId) // on the "Install" page, ensure the install button is enabled/disabled correctly.
3739 { 3769 {
3740 LONGLONG llElevated = 0;
3741 if (m_Bundle.fPerMachine)
3742 {
3743 BalGetNumericVariable(WIXBUNDLE_VARIABLE_ELEVATED, &llElevated);
3744 }
3745 ThemeControlElevates(m_pControlInstallButton, (m_Bundle.fPerMachine && !llElevated)); 3770 ThemeControlElevates(m_pControlInstallButton, (m_Bundle.fPerMachine && !llElevated));
3746 3771
3747 // If the EULA control exists, show it only if a license URL is provided as well. 3772 // If the EULA control exists, show it only if a license URL is provided as well.
@@ -3757,12 +3782,18 @@ private:
3757 } 3782 }
3758 else if (m_rgdwPageIds[WIXSTDBA_PAGE_MODIFY] == dwNewPageId) 3783 else if (m_rgdwPageIds[WIXSTDBA_PAGE_MODIFY] == dwNewPageId)
3759 { 3784 {
3785 ThemeControlElevates(m_pControlRepairButton, (m_Bundle.fPerMachine && !llElevated));
3786 ThemeControlElevates(m_pControlUninstallButton, (m_Bundle.fPerMachine && !llElevated));
3787
3760 ThemeControlEnable(m_pControlRepairButton, !m_fSuppressRepair); 3788 ThemeControlEnable(m_pControlRepairButton, !m_fSuppressRepair);
3761 } 3789 }
3762 else if (m_rgdwPageIds[WIXSTDBA_PAGE_SUCCESS] == dwNewPageId) // on the "Success" page, check if the restart or launch button should be enabled. 3790 else if (m_rgdwPageIds[WIXSTDBA_PAGE_SUCCESS] == dwNewPageId) // on the "Success" page, check if the restart or launch button should be enabled.
3763 { 3791 {
3764 BOOL fEnableRestartButton = FALSE; 3792 BOOL fEnableRestartButton = FALSE;
3765 BOOL fLaunchTargetExists = FALSE; 3793 BOOL fLaunchTargetExists = FALSE;
3794
3795 ThemeControlElevates(m_pControlSuccessRestartButton, m_fRestartRequiresElevation);
3796
3766 if (m_fShouldRestart) 3797 if (m_fShouldRestart)
3767 { 3798 {
3768 if (BAL_INFO_RESTART_PROMPT == m_BalInfoCommand.restart) 3799 if (BAL_INFO_RESTART_PROMPT == m_BalInfoCommand.restart)
@@ -3784,6 +3815,8 @@ private:
3784 BOOL fShowErrorMessage = FALSE; 3815 BOOL fShowErrorMessage = FALSE;
3785 BOOL fEnableRestartButton = FALSE; 3816 BOOL fEnableRestartButton = FALSE;
3786 3817
3818 ThemeControlElevates(m_pControlFailureRestartButton, m_fRestartRequiresElevation);
3819
3787 if (FAILED(m_hrFinal)) 3820 if (FAILED(m_hrFinal))
3788 { 3821 {
3789 // If we know the failure message, use that. 3822 // If we know the failure message, use that.
@@ -4156,8 +4189,20 @@ private:
4156 { 4189 {
4157 AssertSz(m_fRestartRequired, "Restart must be requested to be able to click on the restart button."); 4190 AssertSz(m_fRestartRequired, "Restart must be requested to be able to click on the restart button.");
4158 4191
4159 m_fAllowRestart = TRUE; 4192 if (m_fRestartRequiresElevation)
4160 ::SendMessageW(m_hWnd, WM_CLOSE, 0, 0); 4193 {
4194 m_fElevatingForRestart = TRUE;
4195 ThemeControlEnable(m_pControlFailureRestartButton, FALSE);
4196 ThemeControlEnable(m_pControlSuccessRestartButton, FALSE);
4197
4198 m_pEngine->Elevate(m_hWnd);
4199 }
4200 else
4201 {
4202 m_fAllowRestart = TRUE;
4203
4204 ::SendMessageW(m_hWnd, WM_CLOSE, 0, 0);
4205 }
4161 } 4206 }
4162 4207
4163 4208
@@ -4649,6 +4694,8 @@ public:
4649 m_fRestartRequired = FALSE; 4694 m_fRestartRequired = FALSE;
4650 m_fShouldRestart = FALSE; 4695 m_fShouldRestart = FALSE;
4651 m_fAllowRestart = FALSE; 4696 m_fAllowRestart = FALSE;
4697 m_fRestartRequiresElevation = FALSE;
4698 m_fElevatingForRestart = FALSE;
4652 4699
4653 m_sczLicenseFile = NULL; 4700 m_sczLicenseFile = NULL;
4654 m_sczLicenseUrl = NULL; 4701 m_sczLicenseUrl = NULL;
@@ -4956,6 +5003,8 @@ private:
4956 BOOL m_fRestartRequired; 5003 BOOL m_fRestartRequired;
4957 BOOL m_fShouldRestart; 5004 BOOL m_fShouldRestart;
4958 BOOL m_fAllowRestart; 5005 BOOL m_fAllowRestart;
5006 BOOL m_fRestartRequiresElevation;
5007 BOOL m_fElevatingForRestart;
4959 5008
4960 LPWSTR m_sczLicenseFile; 5009 LPWSTR m_sczLicenseFile;
4961 LPWSTR m_sczLicenseUrl; 5010 LPWSTR m_sczLicenseUrl;
diff --git a/src/libs/dutil/WixToolset.DUtil/inc/procutil.h b/src/libs/dutil/WixToolset.DUtil/inc/procutil.h
index d61d91b5..6a641a5b 100644
--- a/src/libs/dutil/WixToolset.DUtil/inc/procutil.h
+++ b/src/libs/dutil/WixToolset.DUtil/inc/procutil.h
@@ -23,9 +23,21 @@ HRESULT DAPI ProcSystem(
23 __out BOOL* pfSystem 23 __out BOOL* pfSystem
24 ); 24 );
25 25
26HRESULT DAPI ProcTokenUser( 26HRESULT DAPI ProcGetTokenInformation(
27 __in HANDLE hProcess, 27 __in HANDLE hProcess,
28 __out TOKEN_USER** ppTokenUser 28 __in TOKEN_INFORMATION_CLASS tokenInformationClass,
29 __out LPVOID* ppvTokenInformation
30 );
31
32HRESULT DAPI ProcHasPrivilege(
33 __in HANDLE hProcess,
34 __in LPCWSTR wzPrivilegeName,
35 __out BOOL* pfHasPrivilege
36 );
37
38HRESULT DAPI ProcEnablePrivilege(
39 __in HANDLE hProcess,
40 __in LPCWSTR wzPrivilegeName
29 ); 41 );
30 42
31HRESULT DAPI ProcWow64( 43HRESULT DAPI ProcWow64(
diff --git a/src/libs/dutil/WixToolset.DUtil/procutil.cpp b/src/libs/dutil/WixToolset.DUtil/procutil.cpp
index 29f575ae..376aec6d 100644
--- a/src/libs/dutil/WixToolset.DUtil/procutil.cpp
+++ b/src/libs/dutil/WixToolset.DUtil/procutil.cpp
@@ -85,7 +85,7 @@ extern "C" HRESULT DAPI ProcSystem(
85 HRESULT hr = S_OK; 85 HRESULT hr = S_OK;
86 TOKEN_USER* pTokenUser = NULL; 86 TOKEN_USER* pTokenUser = NULL;
87 87
88 hr = ProcTokenUser(hProcess, &pTokenUser); 88 hr = ProcGetTokenInformation(hProcess, TokenUser, reinterpret_cast<LPVOID*>(&pTokenUser));
89 ProcExitOnFailure(hr, "Failed to get TokenUser from process token."); 89 ProcExitOnFailure(hr, "Failed to get TokenUser from process token.");
90 90
91 *pfSystem = ::IsWellKnownSid(pTokenUser->User.Sid, WinLocalSystemSid); 91 *pfSystem = ::IsWellKnownSid(pTokenUser->User.Sid, WinLocalSystemSid);
@@ -96,15 +96,16 @@ LExit:
96 return hr; 96 return hr;
97} 97}
98 98
99extern "C" HRESULT DAPI ProcTokenUser( 99extern "C" HRESULT DAPI ProcGetTokenInformation(
100 __in HANDLE hProcess, 100 __in HANDLE hProcess,
101 __out TOKEN_USER** ppTokenUser 101 __in TOKEN_INFORMATION_CLASS tokenInformationClass,
102 __out LPVOID* ppvTokenInformation
102 ) 103 )
103{ 104{
104 HRESULT hr = S_OK; 105 HRESULT hr = S_OK;
105 DWORD er = ERROR_SUCCESS; 106 DWORD er = ERROR_SUCCESS;
106 HANDLE hToken = NULL; 107 HANDLE hToken = NULL;
107 TOKEN_USER* pTokenUser = NULL; 108 LPVOID pvTokenInformation = NULL;
108 DWORD cbToken = 0; 109 DWORD cbToken = 0;
109 110
110 if (!::OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) 111 if (!::OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
@@ -112,33 +113,104 @@ extern "C" HRESULT DAPI ProcTokenUser(
112 ProcExitWithLastError(hr, "Failed to open process token."); 113 ProcExitWithLastError(hr, "Failed to open process token.");
113 } 114 }
114 115
115 if (::GetTokenInformation(hToken, TokenUser, pTokenUser, 0, &cbToken)) 116 if (!::GetTokenInformation(hToken, tokenInformationClass, pvTokenInformation, 0, &cbToken))
116 {
117 er = ERROR_SUCCESS;
118 }
119 else
120 { 117 {
121 er = ::GetLastError(); 118 er = ::GetLastError();
122 } 119 }
123 120
124 if (er != ERROR_INSUFFICIENT_BUFFER) 121 if (er != ERROR_INSUFFICIENT_BUFFER)
125 { 122 {
126 ProcExitOnWin32Error(er, hr, "Failed to get user from process token size."); 123 ProcExitOnWin32Error(er, hr, "Failed to get information from process token size.");
127 } 124 }
128 125
129 pTokenUser = reinterpret_cast<TOKEN_USER*>(MemAlloc(cbToken, TRUE)); 126 pvTokenInformation = MemAlloc(cbToken, TRUE);
130 ProcExitOnNull(pTokenUser, hr, E_OUTOFMEMORY, "Failed to allocate token information."); 127 ProcExitOnNull(pvTokenInformation, hr, E_OUTOFMEMORY, "Failed to allocate token information.");
131 128
132 if (!::GetTokenInformation(hToken, TokenUser, pTokenUser, cbToken, &cbToken)) 129 if (!::GetTokenInformation(hToken, tokenInformationClass, pvTokenInformation, cbToken, &cbToken))
133 { 130 {
134 ProcExitWithLastError(hr, "Failed to get user from process token."); 131 ProcExitWithLastError(hr, "Failed to get information from process token.");
135 } 132 }
136 133
137 *ppTokenUser = pTokenUser; 134 *ppvTokenInformation = pvTokenInformation;
138 pTokenUser = NULL; 135 pvTokenInformation = NULL;
136
137LExit:
138 ReleaseMem(pvTokenInformation);
139 ReleaseHandle(hToken);
140
141 return hr;
142}
143
144extern "C" HRESULT DAPI ProcHasPrivilege(
145 __in HANDLE hProcess,
146 __in LPCWSTR wzPrivilegeName,
147 __out BOOL* pfHasPrivilege
148 )
149{
150 HRESULT hr = S_OK;
151 TOKEN_PRIVILEGES* pTokenPrivileges = NULL;
152 LUID luidPrivilege = { };
153
154 *pfHasPrivilege = FALSE;
155
156 if (!::LookupPrivilegeValueW(NULL, wzPrivilegeName, &luidPrivilege))
157 {
158 ProcExitWithLastError(hr, "Failed to get privilege LUID: %ls", wzPrivilegeName);
159 }
160
161 hr = ProcGetTokenInformation(hProcess, TokenPrivileges, reinterpret_cast<LPVOID*>(&pTokenPrivileges));
162 ProcExitOnFailure(hr, "Failed to get token privilege information.");
163
164 for (DWORD i = 0; i < pTokenPrivileges->PrivilegeCount; ++i)
165 {
166 LUID* pTokenLuid = &pTokenPrivileges->Privileges[i].Luid;
167
168 if (luidPrivilege.LowPart == pTokenLuid->LowPart && luidPrivilege.HighPart == pTokenLuid->HighPart)
169 {
170 *pfHasPrivilege = TRUE;
171 break;
172 }
173 }
174
175LExit:
176 ReleaseMem(pTokenPrivileges);
177
178 return hr;
179}
180
181extern "C" HRESULT DAPI ProcEnablePrivilege(
182 __in HANDLE hProcess,
183 __in LPCWSTR wzPrivilegeName
184 )
185{
186 HRESULT hr = S_OK;
187 HANDLE hToken = NULL;
188 TOKEN_PRIVILEGES priv = { };
189
190 priv.PrivilegeCount = 1;
191 priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
192
193 if (!::LookupPrivilegeValueW(NULL, wzPrivilegeName, &priv.Privileges[0].Luid))
194 {
195 ProcExitWithLastError(hr, "Failed to get privilege LUID: %ls", wzPrivilegeName);
196 }
197
198 if (!::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken))
199 {
200 ProcExitWithLastError(hr, "Failed to get process token to adjust privileges.");
201 }
202
203 if (!::AdjustTokenPrivileges(hToken, FALSE, &priv, sizeof(TOKEN_PRIVILEGES), NULL, 0))
204 {
205 ProcExitWithLastError(hr, "Failed to adjust token to add privilege: %ls", wzPrivilegeName);
206 }
207
208 if (ERROR_NOT_ALL_ASSIGNED == ::GetLastError())
209 {
210 hr = S_FALSE;
211 }
139 212
140LExit: 213LExit:
141 ReleaseMem(pTokenUser);
142 ReleaseHandle(hToken); 214 ReleaseHandle(hToken);
143 215
144 return hr; 216 return hr;
diff --git a/src/libs/dutil/test/DUtilUnitTest/ProcUtilTest.cpp b/src/libs/dutil/test/DUtilUnitTest/ProcUtilTest.cpp
index 297d90f4..8672ed38 100644
--- a/src/libs/dutil/test/DUtilUnitTest/ProcUtilTest.cpp
+++ b/src/libs/dutil/test/DUtilUnitTest/ProcUtilTest.cpp
@@ -6,6 +6,7 @@ using namespace System;
6using namespace System::Security::Principal; 6using namespace System::Security::Principal;
7using namespace Xunit; 7using namespace Xunit;
8using namespace WixBuildTools::TestSupport; 8using namespace WixBuildTools::TestSupport;
9using namespace WixBuildTools::TestSupport::XunitExtensions;
9 10
10namespace DutilTests 11namespace DutilTests
11{ 12{
@@ -13,7 +14,7 @@ namespace DutilTests
13 { 14 {
14 public: 15 public:
15 [Fact] 16 [Fact]
16 void ProcTokenUserTest() 17 void ProcGetTokenInformationTest()
17 { 18 {
18 HRESULT hr = S_OK; 19 HRESULT hr = S_OK;
19 TOKEN_USER* pTokenUser = NULL; 20 TOKEN_USER* pTokenUser = NULL;
@@ -21,7 +22,7 @@ namespace DutilTests
21 22
22 try 23 try
23 { 24 {
24 hr = ProcTokenUser(::GetCurrentProcess(), &pTokenUser); 25 hr = ProcGetTokenInformation(::GetCurrentProcess(), TokenUser, reinterpret_cast<LPVOID*>(&pTokenUser));
25 NativeAssert::Succeeded(hr, "Failed to get TokenUser for current process."); 26 NativeAssert::Succeeded(hr, "Failed to get TokenUser for current process.");
26 27
27 if (!::ConvertSidToStringSidW(pTokenUser->User.Sid, &sczSid)) 28 if (!::ConvertSidToStringSidW(pTokenUser->User.Sid, &sczSid))
@@ -38,5 +39,28 @@ namespace DutilTests
38 ReleaseStr(sczSid); 39 ReleaseStr(sczSid);
39 } 40 }
40 } 41 }
42
43 [SkippableFact]
44 void ProcHasPrivilegeTest()
45 {
46 HRESULT hr = S_OK;
47 BOOL fHasPrivilege = FALSE;
48
49 hr = ProcHasPrivilege(::GetCurrentProcess(), SE_CREATE_TOKEN_NAME, &fHasPrivilege);
50 NativeAssert::Succeeded(hr, "Failed to check privilege for current process.");
51
52 if (fHasPrivilege)
53 {
54 WixAssert::Skip("Didn't expect process to have SE_CREATE_TOKEN_NAME privilege");
55 }
56
57 hr = ProcHasPrivilege(::GetCurrentProcess(), SE_INC_WORKING_SET_NAME, &fHasPrivilege);
58 NativeAssert::Succeeded(hr, "Failed to check privilege for current process.");
59
60 if (!fHasPrivilege)
61 {
62 WixAssert::Skip("Expected process to have SE_INC_WORKING_SET_NAME privilege");
63 }
64 }
41 }; 65 };
42} 66}
diff --git a/src/test/burn/TestData/Manual/BafThmutilTesting/BafThmUtilTesting.cpp b/src/test/burn/TestData/Manual/BafThmutilTesting/BafThmUtilTesting.cpp
index c619dbd6..a2b8e041 100644
--- a/src/test/burn/TestData/Manual/BafThmutilTesting/BafThmUtilTesting.cpp
+++ b/src/test/burn/TestData/Manual/BafThmutilTesting/BafThmUtilTesting.cpp
@@ -477,7 +477,7 @@ static HRESULT LogUserSid()
477 TOKEN_USER* pTokenUser = NULL; 477 TOKEN_USER* pTokenUser = NULL;
478 LPWSTR sczSid = NULL; 478 LPWSTR sczSid = NULL;
479 479
480 hr = ProcTokenUser(::GetCurrentProcess(), &pTokenUser); 480 hr = ProcGetTokenInformation(::GetCurrentProcess(), TokenUser, reinterpret_cast<LPVOID*>(&pTokenUser));
481 BalExitOnFailure(hr, "Failed to get user from process token."); 481 BalExitOnFailure(hr, "Failed to get user from process token.");
482 482
483 if (!::ConvertSidToStringSidW(pTokenUser->User.Sid, &sczSid)) 483 if (!::ConvertSidToStringSidW(pTokenUser->User.Sid, &sczSid))
diff --git a/src/test/burn/TestData/Manual/BundleA/ManualTests.txt b/src/test/burn/TestData/Manual/BundleA/ManualTests.txt
index d432f94a..b8c6e74d 100644
--- a/src/test/burn/TestData/Manual/BundleA/ManualTests.txt
+++ b/src/test/burn/TestData/Manual/BundleA/ManualTests.txt
@@ -94,11 +94,12 @@ CanRestartFromUnelevatedPerMachineBundleWithoutShutdownPrivilege
942. Click Install. 942. Click Install.
953. Verify that the UAC prompt came up and accept elevation. 953. Verify that the UAC prompt came up and accept elevation.
964. Click OK on the OnExecuteBegin message box. 964. Click OK on the OnExecuteBegin message box.
975. Click Restart. 975. Verify the Restart button doesn't have the UAC shield and then click it.
986. The machine should restart. 986. Verify that there was no UAC prompt.
997. Login to the machine. 997. The machine should restart.
1008. Verify that the bundle did not automatically start running. 1008. Login to the machine.
1019. Look in the bundle log and verify that the restart request didn't cause any errors, and that it logged messages that look like (the process id for w005 must match the elevated i400 and i401): 1019. Verify that the bundle did not automatically start running.
10210. Look in the bundle log and verify that the restart request didn't cause any errors, and that it logged messages that look like (the process id for w005 must match the elevated i400 and i401):
102 103
103[0DDC:0448]w005: Restarting computer... 104[0DDC:0448]w005: Restarting computer...
104[1228:18CC]i400: Received system request to shut down the process: allowed: Yes, elevated: No, critical: No, logoff: No, close app: No 105[1228:18CC]i400: Received system request to shut down the process: allowed: Yes, elevated: No, critical: No, logoff: No, close app: No
@@ -107,4 +108,47 @@ CanRestartFromUnelevatedPerMachineBundleWithoutShutdownPrivilege
107[0DDC:0954]i401: Received result of system request to shut down the process: closing: Yes, elevated: Yes, critical: No, logoff: No, close app: No 108[0DDC:0954]i401: Received result of system request to shut down the process: closing: Yes, elevated: Yes, critical: No, logoff: No, close app: No
108======================================= 109=======================================
109 110
111(11. Uninstall the bundle)
112
113CanRestartFromUnelevatedPerUserBundleWithoutShutdownPrivilege
114
115 Note: Requires different User Rights Assignment settings from CanLogRestartFailure - Only Administrators should have the shutdown privilege. Users should not have it.
116
1171. Run BundleC.exe unelevated with the command line EXEEXITCODE=3010.
1182. Click Install.
1193. Verify the Restart button has the UAC shield and then click it.
1204. Verify that the UAC prompt came up and accept elevation.
1215. The machine should restart.
1226. Login to the machine.
1237. Verify that the bundle did not automatically start running.
1248. Look in the bundle log and verify that the restart request didn't cause any errors, and that it logged messages that look like (the process id for w005 must match the elevated i400 and i401):
125
126[0DDC:0448]w005: Restarting computer...
127[1228:18CC]i400: Received system request to shut down the process: allowed: Yes, elevated: No, critical: No, logoff: No, close app: No
128[1228:18CC]i401: Received result of system request to shut down the process: closing: Yes, elevated: No, critical: No, logoff: No, close app: No
129[0DDC:0954]i400: Received system request to shut down the process: allowed: Yes, elevated: Yes, critical: No, logoff: No, close app: No
130[0DDC:0954]i401: Received result of system request to shut down the process: closing: Yes, elevated: Yes, critical: No, logoff: No, close app: No
131=======================================
132
133(9. Uninstall the bundle)
134
135CanRestartFromUnelevatedPerUserBundleWithShutdownPrivilege
136
137 Note: Requires different User Rights Assignment settings from CanLogRestartFailure - Administrators and Users should have the shutdown privilege.
138
1391. Run BundleC.exe unelevated with the command line EXEEXITCODE=3010.
1402. Click Install.
1413. Verify that there was no UAC prompt.
1424. Verify the Restart button doesn't have the UAC shield and then click it.
1435. Verify that there was no UAC prompt.
1446. The machine should restart.
1457. Login to the machine.
1468. Verify that the bundle did not automatically start running.
1479. Look in the bundle log and verify that the restart request didn't cause any errors, and that it logged messages that look like (there should be no elevated i400 and i401 and the process id for w005 must match):
148
149[1228:0448]w005: Restarting computer...
150[1228:18CC]i400: Received system request to shut down the process: allowed: Yes, elevated: No, critical: No, logoff: No, close app: No
151[1228:18CC]i401: Received result of system request to shut down the process: closing: Yes, elevated: No, critical: No, logoff: No, close app: No
152=======================================
153
110(10. Uninstall the bundle) 154(10. Uninstall the bundle)
diff --git a/src/test/burn/TestData/Manual/BundleC/BundleC.wixproj b/src/test/burn/TestData/Manual/BundleC/BundleC.wixproj
new file mode 100644
index 00000000..eabd529c
--- /dev/null
+++ b/src/test/burn/TestData/Manual/BundleC/BundleC.wixproj
@@ -0,0 +1,18 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 <BA>hyperlinkLicense</BA>
6 <UpgradeCode>{5CAE82BB-A2F3-4994-9BA8-4ACEAFAE7738}</UpgradeCode>
7 </PropertyGroup>
8 <ItemGroup>
9 <Compile Include="..\..\Templates\Bundle.wxs" Link="Bundle.wxs" />
10 </ItemGroup>
11 <ItemGroup>
12 <ProjectReference Include="..\PackagePerUser\PackagePerUser.wixproj" />
13 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
14 </ItemGroup>
15 <ItemGroup>
16 <PackageReference Include="WixToolset.Bal.wixext" />
17 </ItemGroup>
18</Project> \ No newline at end of file
diff --git a/src/test/burn/TestData/Manual/BundleC/BundleC.wxs b/src/test/burn/TestData/Manual/BundleC/BundleC.wxs
new file mode 100644
index 00000000..aadcd8b2
--- /dev/null
+++ b/src/test/burn/TestData/Manual/BundleC/BundleC.wxs
@@ -0,0 +1,15 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2
3
4<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
5 <Fragment>
6 <PackageGroup Id="BundlePackages">
7 <MsiPackage Id="PackagePerUser" SourceFile="$(var.PackagePerUser.TargetPath)" />
8 <ExePackage Id="ExeA" Cache="remove" PerMachine="no"
9 DetectCondition="" Permanent="yes" InstallArguments="/ec [EXEEXITCODE]">
10 <PayloadGroupRef Id="TestExePayloads" />
11 </ExePackage>
12 </PackageGroup>
13 <Variable Name="EXEEXITCODE" bal:Overridable="yes" Value="0" />
14 </Fragment>
15</Wix>
diff --git a/src/test/burn/TestData/Manual/PackagePerUser/PackagePerUser.wixproj b/src/test/burn/TestData/Manual/PackagePerUser/PackagePerUser.wixproj
new file mode 100644
index 00000000..0efa4d63
--- /dev/null
+++ b/src/test/burn/TestData/Manual/PackagePerUser/PackagePerUser.wixproj
@@ -0,0 +1,9 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <UpgradeCode>{F7FBAC90-07A6-4DEB-A9C5-E267EDDA28EF}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\PackagePerUser.wxs" Link="PackagePerUser.wxs" />
8 </ItemGroup>
9</Project> \ No newline at end of file