From fb54576f1d05e82ba47cd718c4c4f8b3bad624c9 Mon Sep 17 00:00:00 2001
From: Sean Hall <r.sean.hall@gmail.com>
Date: Fri, 18 Mar 2022 20:15:33 -0500
Subject: Give BA process id and option to wait for cancelled process to exit.

---
 .../inc/BootstrapperApplication.h                  |  27 +++
 .../WixToolset.Mba.Core/BootstrapperApplication.cs |  25 +++
 src/api/burn/WixToolset.Mba.Core/EventArgs.cs      |  36 ++++
 .../IBootstrapperApplication.cs                    |  32 ++++
 .../IDefaultBootstrapperApplication.cs             |   5 +
 src/api/burn/balutil/inc/BAFunctions.h             |   1 +
 src/api/burn/balutil/inc/BalBaseBAFunctions.h      |  10 ++
 src/api/burn/balutil/inc/BalBaseBAFunctionsProc.h  |   1 +
 .../balutil/inc/BalBaseBootstrapperApplication.h   |  10 ++
 .../inc/BalBaseBootstrapperApplicationProc.h       |  12 ++
 .../burn/balutil/inc/IBootstrapperApplication.h    |   9 +
 src/burn/engine/apply.cpp                          |   8 +
 src/burn/engine/apply.h                            |   5 +
 src/burn/engine/bundlepackageengine.cpp            |  28 +--
 src/burn/engine/elevation.cpp                      |  17 +-
 src/burn/engine/engine.mc                          |   7 +
 src/burn/engine/exeengine.cpp                      | 132 ++++++++++----
 src/burn/engine/exeengine.h                        |   9 +
 src/burn/engine/msuengine.cpp                      |  32 +---
 src/burn/engine/userexperience.cpp                 |  30 +++-
 src/burn/engine/userexperience.h                   |   6 +
 src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj    |   2 +
 src/burn/test/BurnUnitTest/PlanTest.cpp            | 200 +++++++++++++++++++++
 .../TestData/PlanTest/Failure_BundleD_manifest.xml |   1 +
 .../PlanTest/MsuPackageFixture_manifest.xml        |   1 +
 .../WixStandardBootstrapperApplication.cpp         |   8 +
 src/test/burn/TestBA/TestBA.cs                     |  91 ++++++----
 .../TestData/FailureTests/BundleD/BundleD.wixproj  |  19 ++
 .../burn/TestData/FailureTests/BundleD/BundleD.wxs |  19 ++
 .../burn/WixToolsetTest.BurnE2E/FailureTests.cs    |  42 +++++
 .../WixToolsetTest.BurnE2E/TestBAController.cs     |  15 ++
 31 files changed, 716 insertions(+), 124 deletions(-)
 create mode 100644 src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml
 create mode 100644 src/burn/test/BurnUnitTest/TestData/PlanTest/MsuPackageFixture_manifest.xml
 create mode 100644 src/test/burn/TestData/FailureTests/BundleD/BundleD.wixproj
 create mode 100644 src/test/burn/TestData/FailureTests/BundleD/BundleD.wxs

(limited to 'src')

diff --git a/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperApplication.h b/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperApplication.h
index 0b81b35a..df8cac76 100644
--- a/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperApplication.h
+++ b/src/api/burn/WixToolset.BootstrapperCore.Native/inc/BootstrapperApplication.h
@@ -224,6 +224,7 @@ enum BOOTSTRAPPER_APPLICATION_MESSAGE
     BOOTSTRAPPER_APPLICATION_MESSAGE_ONPLANRESTORERELATEDBUNDLE,
     BOOTSTRAPPER_APPLICATION_MESSAGE_ONPLANRELATEDBUNDLETYPE,
     BOOTSTRAPPER_APPLICATION_MESSAGE_ONAPPLYDOWNGRADE,
+    BOOTSTRAPPER_APPLICATION_MESSAGE_ONEXECUTEPROCESSCANCEL,
 };
 
 enum BOOTSTRAPPER_APPLYCOMPLETE_ACTION
@@ -282,6 +283,18 @@ enum BOOTSTRAPPER_EXECUTEPACKAGECOMPLETE_ACTION
     BOOTSTRAPPER_EXECUTEPACKAGECOMPLETE_ACTION_SUSPEND,
 };
 
+enum BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION
+{
+    // Instructs the engine to stop waiting for the process to exit.
+    // The package is immediately considered to have failed with ERROR_INSTALL_USEREXIT.
+    // The engine will never rollback the package.
+    BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION_ABANDON,
+    // Instructs the engine to wait for the process to exit.
+    // Once the process has exited, the package is considered to have failed with ERROR_INSTALL_USEREXIT.
+    // This allows the engine to rollback the package if necessary.
+    BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION_WAIT,
+};
+
 enum BOOTSTRAPPER_SHUTDOWN_ACTION
 {
     BOOTSTRAPPER_SHUTDOWN_ACTION_NONE,
@@ -997,6 +1010,20 @@ struct BA_ONEXECUTEPATCHTARGET_RESULTS
     BOOL fCancel;
 };
 
+struct BA_ONEXECUTEPROCESSCANCEL_ARGS
+{
+    DWORD cbSize;
+    LPCWSTR wzPackageId;
+    DWORD dwProcessId;
+    BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION recommendation;
+};
+
+struct BA_ONEXECUTEPROCESSCANCEL_RESULTS
+{
+    DWORD cbSize;
+    BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action;
+};
+
 struct BA_ONEXECUTEPROGRESS_ARGS
 {
     DWORD cbSize;
diff --git a/src/api/burn/WixToolset.Mba.Core/BootstrapperApplication.cs b/src/api/burn/WixToolset.Mba.Core/BootstrapperApplication.cs
index 8a2e0e93..5ed064fa 100644
--- a/src/api/burn/WixToolset.Mba.Core/BootstrapperApplication.cs
+++ b/src/api/burn/WixToolset.Mba.Core/BootstrapperApplication.cs
@@ -280,6 +280,9 @@ namespace WixToolset.Mba.Core
         /// <inheritdoc/>
         public event EventHandler<PlanRestoreRelatedBundleEventArgs> PlanRestoreRelatedBundle;
 
+        /// <inheritdoc/>
+        public event EventHandler<ExecuteProcessCancelEventArgs> ExecuteProcessCancel;
+
         /// <summary>
         /// Entry point that is called when the bootstrapper application is ready to run.
         /// </summary>
@@ -1369,6 +1372,19 @@ namespace WixToolset.Mba.Core
             }
         }
 
+        /// <summary>
+        /// Called by the engine, raises the <see cref="ExecuteProcessCancel"/> event.
+        /// </summary>
+        /// <param name="args">Additional arguments for this event.</param>
+        protected virtual void OnExecuteProcessCancel(ExecuteProcessCancelEventArgs args)
+        {
+            EventHandler<ExecuteProcessCancelEventArgs> handler = this.ExecuteProcessCancel;
+            if (null != handler)
+            {
+                handler(this, args);
+            }
+        }
+
         #region IBootstrapperApplication Members
 
         int IBootstrapperApplication.BAProc(int message, IntPtr pvArgs, IntPtr pvResults, IntPtr pvContext)
@@ -2119,6 +2135,15 @@ namespace WixToolset.Mba.Core
             return args.HResult;
         }
 
+        int IBootstrapperApplication.OnExecuteProcessCancel(string wzPackageId, int processId, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION recommendation, ref BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION pAction)
+        {
+            ExecuteProcessCancelEventArgs args = new ExecuteProcessCancelEventArgs(wzPackageId, processId, recommendation, pAction);
+            this.OnExecuteProcessCancel(args);
+
+            pAction = args.Action;
+            return args.HResult;
+        }
+
         #endregion
     }
 }
diff --git a/src/api/burn/WixToolset.Mba.Core/EventArgs.cs b/src/api/burn/WixToolset.Mba.Core/EventArgs.cs
index c93c2885..c2c73067 100644
--- a/src/api/burn/WixToolset.Mba.Core/EventArgs.cs
+++ b/src/api/burn/WixToolset.Mba.Core/EventArgs.cs
@@ -2488,4 +2488,40 @@ namespace WixToolset.Mba.Core
         /// </summary>
         public RequestState State { get; set; }
     }
+
+    /// <summary>
+    /// Event arguments for <see cref="IDefaultBootstrapperApplication.ExecuteProcessCancel"/>
+    /// </summary>
+    [Serializable]
+    public class ExecuteProcessCancelEventArgs : HResultEventArgs
+    {
+        /// <summary />
+        public ExecuteProcessCancelEventArgs(string packageId, int processId, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION recommendation, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action)
+        {
+            this.PackageId = packageId;
+            this.ProcessId = processId;
+            this.Recommendation = recommendation;
+            this.Action = action;
+        }
+
+        /// <summary>
+        /// Gets the identity of the package.
+        /// </summary>
+        public string PackageId { get; private set; }
+
+        /// <summary>
+        /// Gets the process id.
+        /// </summary>
+        public int ProcessId { get; private set; }
+
+        /// <summary>
+        /// Gets the recommended action from the engine.
+        /// </summary>
+        public BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION Recommendation { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the action to be performed. This is passed back to the engine.
+        /// </summary>
+        public BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION Action { get; set; }
+    }
 }
diff --git a/src/api/burn/WixToolset.Mba.Core/IBootstrapperApplication.cs b/src/api/burn/WixToolset.Mba.Core/IBootstrapperApplication.cs
index d4fe8320..1786eecd 100644
--- a/src/api/burn/WixToolset.Mba.Core/IBootstrapperApplication.cs
+++ b/src/api/burn/WixToolset.Mba.Core/IBootstrapperApplication.cs
@@ -1170,6 +1170,18 @@ namespace WixToolset.Mba.Core
             [MarshalAs(UnmanagedType.I4)] int hrRecommended,
             [MarshalAs(UnmanagedType.I4)] ref int hrStatus
             );
+
+        /// <summary>
+        /// See <see cref="IDefaultBootstrapperApplication.ExecuteProcessCancel"/>.
+        /// </summary>
+        [PreserveSig]
+        [return: MarshalAs(UnmanagedType.I4)]
+        int OnExecuteProcessCancel(
+            [MarshalAs(UnmanagedType.LPWStr)] string wzPackageId,
+            int processId,
+            [MarshalAs(UnmanagedType.I4)] BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION recommendation,
+            [MarshalAs(UnmanagedType.I4)] ref BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION pAction
+            );
     }
 
     /// <summary>
@@ -1906,6 +1918,26 @@ namespace WixToolset.Mba.Core
         Suspend,
     }
 
+    /// <summary>
+    /// The available actions for <see cref="IDefaultBootstrapperApplication.ExecuteProcessCancel"/>.
+    /// </summary>
+    public enum BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION
+    {
+        /// <summary>
+        /// Instructs the engine to stop waiting for the process to exit.
+        /// The package is immediately considered to have failed with ERROR_INSTALL_USEREXIT.
+        /// The engine will never rollback the package.
+        /// </summary>
+        Abandon,
+
+        /// <summary>
+        /// Instructs the engine to wait for the process to exit.
+        /// Once the process has exited, the package is considered to have failed with ERROR_INSTALL_USEREXIT.
+        /// This allows the engine to rollback the package if necessary.
+        /// </summary>
+        Wait,
+    }
+
     /// <summary>
     /// The result of evaluating a condition from a package.
     /// </summary>
diff --git a/src/api/burn/WixToolset.Mba.Core/IDefaultBootstrapperApplication.cs b/src/api/burn/WixToolset.Mba.Core/IDefaultBootstrapperApplication.cs
index c9284b69..21d99b32 100644
--- a/src/api/burn/WixToolset.Mba.Core/IDefaultBootstrapperApplication.cs
+++ b/src/api/burn/WixToolset.Mba.Core/IDefaultBootstrapperApplication.cs
@@ -243,6 +243,11 @@ namespace WixToolset.Mba.Core
         /// </summary>
         event EventHandler<ExecutePackageCompleteEventArgs> ExecutePackageComplete;
 
+        /// <summary>
+        /// Fired when a package that spawned a process is cancelled.
+        /// </summary>
+        event EventHandler<ExecuteProcessCancelEventArgs> ExecuteProcessCancel;
+
         /// <summary>
         /// Fired when the engine executes one or more patches targeting a product.
         /// </summary>
diff --git a/src/api/burn/balutil/inc/BAFunctions.h b/src/api/burn/balutil/inc/BAFunctions.h
index 58c26166..158e65b5 100644
--- a/src/api/burn/balutil/inc/BAFunctions.h
+++ b/src/api/burn/balutil/inc/BAFunctions.h
@@ -91,6 +91,7 @@ enum BA_FUNCTIONS_MESSAGE
     BA_FUNCTIONS_MESSAGE_ONPLANRESTORERELATEDBUNDLE = BOOTSTRAPPER_APPLICATION_MESSAGE_ONPLANRESTORERELATEDBUNDLE,
     BA_FUNCTIONS_MESSAGE_ONPLANRELATEDBUNDLETYPE = BOOTSTRAPPER_APPLICATION_MESSAGE_ONPLANRELATEDBUNDLETYPE,
     BA_FUNCTIONS_MESSAGE_ONAPPLYDOWNGRADE = BOOTSTRAPPER_APPLICATION_MESSAGE_ONAPPLYDOWNGRADE,
+    BA_FUNCTIONS_MESSAGE_ONEXECUTEPROCESSCANCEL = BOOTSTRAPPER_APPLICATION_MESSAGE_ONEXECUTEPROCESSCANCEL,
 
     BA_FUNCTIONS_MESSAGE_ONTHEMELOADED = 1024,
     BA_FUNCTIONS_MESSAGE_WNDPROC,
diff --git a/src/api/burn/balutil/inc/BalBaseBAFunctions.h b/src/api/burn/balutil/inc/BalBaseBAFunctions.h
index fe5c99ba..614d4bcf 100644
--- a/src/api/burn/balutil/inc/BalBaseBAFunctions.h
+++ b/src/api/burn/balutil/inc/BalBaseBAFunctions.h
@@ -877,6 +877,16 @@ public: // IBootstrapperApplication
         return S_OK;
     }
 
+    virtual STDMETHODIMP OnExecuteProcessCancel(
+        __in_z LPCWSTR /*wzPackageId*/,
+        __in DWORD /*dwProcessId*/,
+        __in BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION /*recommendation*/,
+        __inout BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION* /*pAction*/
+        )
+    {
+        return S_OK;
+    }
+
 public: // IBAFunctions
     virtual STDMETHODIMP OnPlan(
         )
diff --git a/src/api/burn/balutil/inc/BalBaseBAFunctionsProc.h b/src/api/burn/balutil/inc/BalBaseBAFunctionsProc.h
index 100e5c30..b96a180c 100644
--- a/src/api/burn/balutil/inc/BalBaseBAFunctionsProc.h
+++ b/src/api/burn/balutil/inc/BalBaseBAFunctionsProc.h
@@ -162,6 +162,7 @@ static HRESULT WINAPI BalBaseBAFunctionsProc(
         case BA_FUNCTIONS_MESSAGE_ONPLANRESTORERELATEDBUNDLE:
         case BA_FUNCTIONS_MESSAGE_ONPLANRELATEDBUNDLETYPE:
         case BA_FUNCTIONS_MESSAGE_ONAPPLYDOWNGRADE:
+        case BA_FUNCTIONS_MESSAGE_ONEXECUTEPROCESSCANCEL:
             hr = BalBaseBootstrapperApplicationProc((BOOTSTRAPPER_APPLICATION_MESSAGE)message, pvArgs, pvResults, pvContext);
             break;
         case BA_FUNCTIONS_MESSAGE_ONTHEMELOADED:
diff --git a/src/api/burn/balutil/inc/BalBaseBootstrapperApplication.h b/src/api/burn/balutil/inc/BalBaseBootstrapperApplication.h
index fd06a83f..25570ffd 100644
--- a/src/api/burn/balutil/inc/BalBaseBootstrapperApplication.h
+++ b/src/api/burn/balutil/inc/BalBaseBootstrapperApplication.h
@@ -1077,6 +1077,16 @@ public: // IBootstrapperApplication
         return S_OK;
     }
 
+    virtual STDMETHODIMP OnExecuteProcessCancel(
+        __in_z LPCWSTR /*wzPackageId*/,
+        __in DWORD /*dwProcessId*/,
+        __in BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION /*recommendation*/,
+        __inout BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION* /*pAction*/
+        )
+    {
+        return S_OK;
+    }
+
 public: //CBalBaseBootstrapperApplication
     virtual STDMETHODIMP Initialize(
         __in const BOOTSTRAPPER_CREATE_ARGS* pCreateArgs
diff --git a/src/api/burn/balutil/inc/BalBaseBootstrapperApplicationProc.h b/src/api/burn/balutil/inc/BalBaseBootstrapperApplicationProc.h
index 4e413e4e..b196d183 100644
--- a/src/api/burn/balutil/inc/BalBaseBootstrapperApplicationProc.h
+++ b/src/api/burn/balutil/inc/BalBaseBootstrapperApplicationProc.h
@@ -486,6 +486,15 @@ static HRESULT BalBaseBAProcOnExecutePackageComplete(
     return pBA->OnExecutePackageComplete(pArgs->wzPackageId, pArgs->hrStatus, pArgs->restart, pArgs->recommendation, &pResults->action);
 }
 
+static HRESULT BalBaseBAProcOnExecuteProcessCancel(
+    __in IBootstrapperApplication* pBA,
+    __in BA_ONEXECUTEPROCESSCANCEL_ARGS* pArgs,
+    __inout BA_ONEXECUTEPROCESSCANCEL_RESULTS* pResults
+    )
+{
+    return pBA->OnExecuteProcessCancel(pArgs->wzPackageId, pArgs->dwProcessId, pArgs->recommendation, &pResults->action);
+}
+
 static HRESULT BalBaseBAProcOnExecuteComplete(
     __in IBootstrapperApplication* pBA,
     __in BA_ONEXECUTECOMPLETE_ARGS* pArgs,
@@ -1012,6 +1021,9 @@ static HRESULT WINAPI BalBaseBootstrapperApplicationProc(
         case BOOTSTRAPPER_APPLICATION_MESSAGE_ONAPPLYDOWNGRADE:
             hr = BalBaseBAProcOnApplyDowngrade(pBA, reinterpret_cast<BA_ONAPPLYDOWNGRADE_ARGS*>(pvArgs), reinterpret_cast<BA_ONAPPLYDOWNGRADE_RESULTS*>(pvResults));
             break;
+        case BOOTSTRAPPER_APPLICATION_MESSAGE_ONEXECUTEPROCESSCANCEL:
+            hr = BalBaseBAProcOnExecuteProcessCancel(pBA, reinterpret_cast<BA_ONEXECUTEPROCESSCANCEL_ARGS*>(pvArgs), reinterpret_cast<BA_ONEXECUTEPROCESSCANCEL_RESULTS*>(pvResults));
+            break;
         }
     }
 
diff --git a/src/api/burn/balutil/inc/IBootstrapperApplication.h b/src/api/burn/balutil/inc/IBootstrapperApplication.h
index c9cf3126..6174c290 100644
--- a/src/api/burn/balutil/inc/IBootstrapperApplication.h
+++ b/src/api/burn/balutil/inc/IBootstrapperApplication.h
@@ -714,4 +714,13 @@ DECLARE_INTERFACE_IID_(IBootstrapperApplication, IUnknown, "53C31D56-49C0-426B-A
         __in HRESULT hrRecommended,
         __inout HRESULT* phrStatus
         ) = 0;
+
+    // OnExecuteProcessCancel - called when a package that spawned a process is cancelled.
+    //
+    STDMETHOD(OnExecuteProcessCancel)(
+        __in_z LPCWSTR wzPackageId,
+        __in DWORD dwProcessId,
+        __in BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION recommendation,
+        __inout BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION* pAction
+        ) = 0;
 };
diff --git a/src/burn/engine/apply.cpp b/src/burn/engine/apply.cpp
index 3ad22e9b..73b5b396 100644
--- a/src/burn/engine/apply.cpp
+++ b/src/burn/engine/apply.cpp
@@ -3326,6 +3326,14 @@ static int GenericExecuteMessageHandler(
         }
         break;
 
+    case GENERIC_EXECUTE_MESSAGE_PROCESS_CANCEL:
+        {
+            BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action = BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION_ABANDON;
+            UserExperienceOnExecuteProcessCancel(pContext->pUX, pContext->wzExecutingPackageId, pMessage->processCancel.dwProcessId, &action); // ignore return value.
+            nResult = BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION_WAIT == action ? IDRETRY : IDIGNORE;
+        }
+        break;
+
     case GENERIC_EXECUTE_MESSAGE_ERROR:
         UserExperienceOnError(pContext->pUX, BOOTSTRAPPER_ERROR_TYPE_EXE_PACKAGE, pContext->wzExecutingPackageId, pMessage->error.dwErrorCode, pMessage->error.wzMessage, pMessage->dwUIHint, 0, NULL, &nResult); // ignore return value.
         break;
diff --git a/src/burn/engine/apply.h b/src/burn/engine/apply.h
index 1717a71a..47f0ece6 100644
--- a/src/burn/engine/apply.h
+++ b/src/burn/engine/apply.h
@@ -13,6 +13,7 @@ enum GENERIC_EXECUTE_MESSAGE_TYPE
     GENERIC_EXECUTE_MESSAGE_ERROR,
     GENERIC_EXECUTE_MESSAGE_PROGRESS,
     GENERIC_EXECUTE_MESSAGE_NETFX_FILES_IN_USE,
+    GENERIC_EXECUTE_MESSAGE_PROCESS_CANCEL,
 };
 
 typedef struct _APPLY_AUTHENTICATION_REQUIRED_DATA
@@ -43,6 +44,10 @@ typedef struct _GENERIC_EXECUTE_MESSAGE
             DWORD cFiles;
             LPCWSTR* rgwzFiles;
         } filesInUse;
+        struct
+        {
+            DWORD dwProcessId;
+        } processCancel;
     };
 } GENERIC_EXECUTE_MESSAGE;
 
diff --git a/src/burn/engine/bundlepackageengine.cpp b/src/burn/engine/bundlepackageengine.cpp
index 88a00f5e..0bee054f 100644
--- a/src/burn/engine/bundlepackageengine.cpp
+++ b/src/burn/engine/bundlepackageengine.cpp
@@ -251,7 +251,6 @@ extern "C" HRESULT BundlePackageEngineExecuteRelatedBundle(
     )
 {
     HRESULT hr = S_OK;
-    int nResult = IDNOACTION;
     LPCWSTR wzArguments = NULL;
     LPWSTR sczArguments = NULL;
     LPWSTR sczArgumentsFormatted = NULL;
@@ -420,31 +419,10 @@ extern "C" HRESULT BundlePackageEngineExecuteRelatedBundle(
         hr = EmbeddedRunBundle(sczExecutablePath, sczCommand, pfnGenericMessageHandler, pvContext, &dwExitCode);
         ExitOnFailure(hr, "Failed to run bundle as embedded from path: %ls", sczExecutablePath);
     }
-    else // create and wait for the executable process while sending fake progress to allow cancel.
+    else
     {
-        // Make the cache location of the executable the current directory to help those executables
-        // that expect stuff to be relative to them.
-        si.cb = sizeof(si);
-        if (!::CreateProcessW(sczExecutablePath, sczCommand, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, sczCachedDirectory, &si, &pi))
-        {
-            ExitWithLastError(hr, "Failed to CreateProcess on path: %ls", sczExecutablePath);
-        }
-
-        do
-        {
-            message.type = GENERIC_EXECUTE_MESSAGE_PROGRESS;
-            message.dwUIHint = MB_OKCANCEL;
-            message.progress.dwPercentage = 50;
-            nResult = pfnGenericMessageHandler(&message, pvContext);
-            hr = (IDOK == nResult || IDNOACTION == nResult) ? S_OK : IDCANCEL == nResult ? HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) : HRESULT_FROM_WIN32(ERROR_INSTALL_FAILURE);
-            ExitOnRootFailure(hr, "Bootstrapper application aborted during BUNDLE progress.");
-
-            hr = ProcWaitForCompletion(pi.hProcess, 500, &dwExitCode);
-            if (HRESULT_FROM_WIN32(WAIT_TIMEOUT) != hr)
-            {
-                ExitOnFailure(hr, "Failed to wait for executable to complete: %ls", sczExecutablePath);
-            }
-        } while (HRESULT_FROM_WIN32(WAIT_TIMEOUT) == hr);
+        hr = ExeEngineRunProcess(pfnGenericMessageHandler, pvContext, pPackage, sczExecutablePath, sczCommand, sczCachedDirectory, &dwExitCode);
+        ExitOnFailure(hr, "Failed to run BUNDLE process");
     }
 
     hr = ExeEngineHandleExitCode(pPackage->Bundle.rgExitCodes, pPackage->Bundle.cExitCodes, dwExitCode, pRestart);
diff --git a/src/burn/engine/elevation.cpp b/src/burn/engine/elevation.cpp
index 636d67ce..3c2872f1 100644
--- a/src/burn/engine/elevation.cpp
+++ b/src/burn/engine/elevation.cpp
@@ -43,6 +43,7 @@ typedef enum _BURN_ELEVATION_MESSAGE_TYPE
     BURN_ELEVATION_MESSAGE_TYPE_BURN_CACHE_COMPLETE,
     BURN_ELEVATION_MESSAGE_TYPE_BURN_CACHE_SUCCESS,
     BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_PROGRESS,
+    BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_PROCESS_CANCEL,
     BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_ERROR,
     BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_MSI_MESSAGE,
     BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_MSI_FILES_IN_USE,
@@ -1812,7 +1813,14 @@ static HRESULT ProcessGenericExecuteMessages(
 
         // read message parameters
         hr = BuffReadNumber((BYTE*)pMsg->pvData, pMsg->cbData, &iData, &message.progress.dwPercentage);
-        ExitOnFailure(hr, "Failed to progress.");
+        ExitOnFailure(hr, "Failed to read progress.");
+        break;
+
+    case BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_PROCESS_CANCEL:
+        message.type = GENERIC_EXECUTE_MESSAGE_PROCESS_CANCEL;
+
+        hr = BuffReadNumber((BYTE*)pMsg->pvData, pMsg->cbData, &iData, &message.processCancel.dwProcessId);
+        ExitOnFailure(hr, "Failed to read processId.");
         break;
 
     case BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_ERROR:
@@ -3450,6 +3458,13 @@ static int GenericExecuteMessageHandler(
         dwMessage = BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_PROGRESS;
         break;
 
+    case GENERIC_EXECUTE_MESSAGE_PROCESS_CANCEL:
+        hr = BuffWriteNumber(&pbData, &cbData, pMessage->processCancel.dwProcessId);
+        ExitOnFailure(hr, "Failed to write progress percentage to message buffer.");
+
+        dwMessage = BURN_ELEVATION_MESSAGE_TYPE_EXECUTE_PROCESS_CANCEL;
+        break;
+
     case GENERIC_EXECUTE_MESSAGE_ERROR:
         // serialize message data
         hr = BuffWriteNumber(&pbData, &cbData, pMessage->error.dwErrorCode);
diff --git a/src/burn/engine/engine.mc b/src/burn/engine/engine.mc
index 53e6b256..9e139661 100644
--- a/src/burn/engine/engine.mc
+++ b/src/burn/engine/engine.mc
@@ -933,6 +933,13 @@ Language=English
 Could not create system restore point, error: 0x%1!x!. Continuing...
 .
 
+MessageId=364
+Severity=Success
+SymbolicName=MSG_EXECUTE_PROCESS_DELAYED_CANCEL_REQUESTED
+Language=English
+Bootstrapper application requested delayed cancel during package process progress, id: %1!ls!. Waiting...
+.
+
 MessageId=370
 Severity=Success
 SymbolicName=MSG_SESSION_BEGIN
diff --git a/src/burn/engine/exeengine.cpp b/src/burn/engine/exeengine.cpp
index c984f5a7..4c3c6fb0 100644
--- a/src/burn/engine/exeengine.cpp
+++ b/src/burn/engine/exeengine.cpp
@@ -317,7 +317,6 @@ extern "C" HRESULT ExeEngineExecutePackage(
     )
 {
     HRESULT hr = S_OK;
-    int nResult = IDNOACTION;
     LPCWSTR wzArguments = NULL;
     LPWSTR sczArguments = NULL;
     LPWSTR sczArgumentsFormatted = NULL;
@@ -327,10 +326,7 @@ extern "C" HRESULT ExeEngineExecutePackage(
     LPWSTR sczCommand = NULL;
     LPWSTR sczCommandObfuscated = NULL;
     HANDLE hExecutableFile = INVALID_HANDLE_VALUE;
-    STARTUPINFOW si = { };
-    PROCESS_INFORMATION pi = { };
     DWORD dwExitCode = 0;
-    GENERIC_EXECUTE_MESSAGE message = { };
     BURN_PACKAGE* pPackage = pExecuteAction->exePackage.pPackage;
     BURN_PAYLOAD* pPackagePayload = pPackage->payloads.rgItems[0].pPayload;
 
@@ -442,37 +438,10 @@ extern "C" HRESULT ExeEngineExecutePackage(
         hr = NetFxRunChainer(sczExecutablePath, sczCommand, pfnGenericMessageHandler, pvContext, &dwExitCode);
         ExitOnFailure(hr, "Failed to run netfx chainer: %ls", sczExecutablePath);
     }
-    else // create and wait for the executable process while sending fake progress to allow cancel.
+    else
     {
-        // Make the cache location of the executable the current directory to help those executables
-        // that expect stuff to be relative to them.
-        si.cb = sizeof(si);
-        if (!::CreateProcessW(sczExecutablePath, sczCommand, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, sczCachedDirectory, &si, &pi))
-        {
-            ExitWithLastError(hr, "Failed to CreateProcess on path: %ls", sczExecutablePath);
-        }
-
-        if (pPackage->Exe.fFireAndForget)
-        {
-            ::WaitForInputIdle(pi.hProcess, 5000);
-            ExitFunction();
-        }
-
-        do
-        {
-            message.type = GENERIC_EXECUTE_MESSAGE_PROGRESS;
-            message.dwUIHint = MB_OKCANCEL;
-            message.progress.dwPercentage = 50;
-            nResult = pfnGenericMessageHandler(&message, pvContext);
-            hr = (IDOK == nResult || IDNOACTION == nResult) ? S_OK : IDCANCEL == nResult ? HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) : HRESULT_FROM_WIN32(ERROR_INSTALL_FAILURE);
-            ExitOnRootFailure(hr, "Bootstrapper application aborted during EXE progress.");
-
-            hr = ProcWaitForCompletion(pi.hProcess, 500, &dwExitCode);
-            if (HRESULT_FROM_WIN32(WAIT_TIMEOUT) != hr)
-            {
-                ExitOnFailure(hr, "Failed to wait for executable to complete: %ls", sczExecutablePath);
-            }
-        } while (HRESULT_FROM_WIN32(WAIT_TIMEOUT) == hr);
+        hr = ExeEngineRunProcess(pfnGenericMessageHandler, pvContext, pPackage, sczExecutablePath, sczCommand, sczCachedDirectory, &dwExitCode);
+        ExitOnFailure(hr, "Failed to run EXE process");
     }
 
     hr = ExeEngineHandleExitCode(pPackage->Exe.rgExitCodes, pPackage->Exe.cExitCodes, dwExitCode, pRestart);
@@ -487,8 +456,6 @@ LExit:
     StrSecureZeroFreeString(sczCommand);
     ReleaseStr(sczCommandObfuscated);
 
-    ReleaseHandle(pi.hThread);
-    ReleaseHandle(pi.hProcess);
     ReleaseFileHandle(hExecutableFile);
 
     // Best effort to clear the execute package cache folder and action variables.
@@ -498,6 +465,99 @@ LExit:
     return hr;
 }
 
+extern "C" HRESULT ExeEngineRunProcess(
+    __in PFN_GENERICMESSAGEHANDLER pfnGenericMessageHandler,
+    __in LPVOID pvContext,
+    __in BURN_PACKAGE* pPackage,
+    __in_z LPCWSTR wzExecutablePath,
+    __in_z LPWSTR wzCommand,
+    __in_z_opt LPCWSTR wzCachedDirectory,
+    __inout DWORD* pdwExitCode
+    )
+{
+    HRESULT hr = S_OK;
+    STARTUPINFOW si = { };
+    PROCESS_INFORMATION pi = { };
+    GENERIC_EXECUTE_MESSAGE message = { };
+    int nResult = IDNOACTION;
+    DWORD dwProcessId = 0;
+    BOOL fDelayedCancel = FALSE;
+    BOOL fFireAndForget = BURN_PACKAGE_TYPE_EXE == pPackage->type && pPackage->Exe.fFireAndForget;
+    BOOL fInheritHandles = BURN_PACKAGE_TYPE_BUNDLE == pPackage->type;
+
+    // Make the cache location of the executable the current directory to help those executables
+    // that expect stuff to be relative to them.
+    si.cb = sizeof(si);
+    if (!::CreateProcessW(wzExecutablePath, wzCommand, NULL, NULL, fInheritHandles, CREATE_NO_WINDOW, NULL, wzCachedDirectory, &si, &pi))
+    {
+        ExitWithLastError(hr, "Failed to CreateProcess on path: %ls", wzExecutablePath);
+    }
+
+    if (fFireAndForget)
+    {
+        ::WaitForInputIdle(pi.hProcess, 5000);
+        ExitFunction();
+    }
+
+    dwProcessId = ::GetProcessId(pi.hProcess);
+
+    // Wait for the executable process while sending fake progress to allow cancel.
+    do
+    {
+        message.type = GENERIC_EXECUTE_MESSAGE_PROGRESS;
+        message.dwUIHint = MB_OKCANCEL;
+        message.progress.dwPercentage = 50;
+        nResult = pfnGenericMessageHandler(&message, pvContext);
+
+        if (IDCANCEL == nResult)
+        {
+            memset(&message, 0, sizeof(message));
+            message.type = GENERIC_EXECUTE_MESSAGE_PROCESS_CANCEL;
+            message.dwUIHint = MB_ABORTRETRYIGNORE;
+            message.processCancel.dwProcessId = dwProcessId;
+            nResult = pfnGenericMessageHandler(&message, pvContext);
+
+            if (IDIGNORE == nResult) // abandon
+            {
+                nResult = IDCANCEL;
+                fDelayedCancel = FALSE;
+            }
+            //else if (IDABORT == nResult) // kill
+            else // wait
+            {
+                if (!fDelayedCancel)
+                {
+                    fDelayedCancel = TRUE;
+
+                    LogId(REPORT_STANDARD, MSG_EXECUTE_PROCESS_DELAYED_CANCEL_REQUESTED, pPackage->sczId);
+                }
+
+                nResult = IDNOACTION;
+            }
+        }
+
+        hr = (IDOK == nResult || IDNOACTION == nResult) ? S_OK : IDCANCEL == nResult ? HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) : HRESULT_FROM_WIN32(ERROR_INSTALL_FAILURE);
+        ExitOnRootFailure(hr, "Bootstrapper application aborted during package process progress.");
+
+        hr = ProcWaitForCompletion(pi.hProcess, 500, pdwExitCode);
+        if (HRESULT_FROM_WIN32(WAIT_TIMEOUT) != hr)
+        {
+            ExitOnFailure(hr, "Failed to wait for executable to complete: %ls", wzExecutablePath);
+        }
+    } while (HRESULT_FROM_WIN32(WAIT_TIMEOUT) == hr);
+
+    if (fDelayedCancel)
+    {
+        ExitWithRootFailure(hr, HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT), "Bootstrapper application cancelled during package process progress, exit code: 0x%x", *pdwExitCode);
+    }
+
+LExit:
+    ReleaseHandle(pi.hThread);
+    ReleaseHandle(pi.hProcess);
+
+    return hr;
+}
+
 extern "C" void ExeEngineUpdateInstallRegistrationState(
     __in BURN_EXECUTE_ACTION* pAction,
     __in HRESULT hrExecute
diff --git a/src/burn/engine/exeengine.h b/src/burn/engine/exeengine.h
index 743621b7..636988f1 100644
--- a/src/burn/engine/exeengine.h
+++ b/src/burn/engine/exeengine.h
@@ -42,6 +42,15 @@ HRESULT ExeEngineExecutePackage(
     __in LPVOID pvContext,
     __out BOOTSTRAPPER_APPLY_RESTART* pRestart
     );
+HRESULT ExeEngineRunProcess(
+    __in PFN_GENERICMESSAGEHANDLER pfnGenericMessageHandler,
+    __in LPVOID pvContext,
+    __in BURN_PACKAGE* pPackage,
+    __in_z LPCWSTR wzExecutablePath,
+    __in_z LPWSTR wzCommand,
+    __in_z_opt LPCWSTR wzCachedDirectory,
+    __inout DWORD* pdwExitCode
+    );
 void ExeEngineUpdateInstallRegistrationState(
     __in BURN_EXECUTE_ACTION* pAction,
     __in HRESULT hrExecute
diff --git a/src/burn/engine/msuengine.cpp b/src/burn/engine/msuengine.cpp
index 091bbe62..2f1fb61c 100644
--- a/src/burn/engine/msuengine.cpp
+++ b/src/burn/engine/msuengine.cpp
@@ -264,7 +264,6 @@ extern "C" HRESULT MsuEngineExecutePackage(
     )
 {
     HRESULT hr = S_OK;
-    int nResult = IDNOACTION;
     LPWSTR sczCachedDirectory = NULL;
     LPWSTR sczMsuPath = NULL;
     LPWSTR sczWindowsPath = NULL;
@@ -350,35 +349,8 @@ extern "C" HRESULT MsuEngineExecutePackage(
     hr = EnsureWUServiceEnabled(fStopWusaService, &schWu, &fWuWasDisabled);
     ExitOnFailure(hr, "Failed to ensure WU service was enabled to install MSU package.");
 
-    // create process
-    si.cb = sizeof(si);
-    if (!::CreateProcessW(sczWusaPath, sczCommand, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi))
-    {
-        ExitWithLastError(hr, "Failed to CreateProcess on path: %ls", sczWusaPath);
-    }
-
-    do
-    {
-        message.type = GENERIC_EXECUTE_MESSAGE_PROGRESS;
-        message.dwUIHint = MB_OKCANCEL;
-        message.progress.dwPercentage = 50;
-        nResult = pfnGenericMessageHandler(&message, pvContext);
-        hr = (IDOK == nResult || IDNOACTION == nResult) ? S_OK : IDCANCEL == nResult ? HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) : HRESULT_FROM_WIN32(ERROR_INSTALL_FAILURE);
-        ExitOnRootFailure(hr, "Bootstrapper application aborted during MSU progress.");
-
-        // wait for process to terminate
-        hr = ProcWaitForCompletion(pi.hProcess, 500, &dwExitCode);
-        if (HRESULT_FROM_WIN32(WAIT_TIMEOUT) != hr)
-        {
-            ExitOnFailure(hr, "Failed to wait for executable to complete: %ls", sczWusaPath);
-        }
-    } while (HRESULT_FROM_WIN32(WAIT_TIMEOUT) == hr);
-
-    // get process exit code
-    if (!::GetExitCodeProcess(pi.hProcess, &dwExitCode))
-    {
-        ExitWithLastError(hr, "Failed to get process exit code.");
-    }
+    hr = ExeEngineRunProcess(pfnGenericMessageHandler, pvContext, pPackage, sczWusaPath, sczCommand, NULL, &dwExitCode);
+    ExitOnFailure(hr, "Failed to run MSU process");
 
     // We'll normalize the restart required error code from wusa.exe just in case. Most likely
     // that on reboot we'll actually get WU_S_REBOOT_REQUIRED.
diff --git a/src/burn/engine/userexperience.cpp b/src/burn/engine/userexperience.cpp
index 81ce8bb9..06f87363 100644
--- a/src/burn/engine/userexperience.cpp
+++ b/src/burn/engine/userexperience.cpp
@@ -104,7 +104,7 @@ extern "C" HRESULT UserExperienceLoad(
     args.pCommand = pCommand;
     args.pfnBootstrapperEngineProc = EngineForApplicationProc;
     args.pvBootstrapperEngineProcContext = pEngineContext;
-    args.qwEngineAPIVersion = MAKEQWORDVERSION(2022, 3, 14, 0);
+    args.qwEngineAPIVersion = MAKEQWORDVERSION(2022, 3, 17, 0);
 
     results.cbSize = sizeof(BOOTSTRAPPER_CREATE_RESULTS);
 
@@ -1701,6 +1701,34 @@ LExit:
     return hr;
 }
 
+BAAPI UserExperienceOnExecuteProcessCancel(
+    __in BURN_USER_EXPERIENCE* pUserExperience,
+    __in_z LPCWSTR wzPackageId,
+    __in DWORD dwProcessId,
+    __inout BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION* pAction
+    )
+{
+    HRESULT hr = S_OK;
+    BA_ONEXECUTEPROCESSCANCEL_ARGS args = { };
+    BA_ONEXECUTEPROCESSCANCEL_RESULTS results = { };
+
+    args.cbSize = sizeof(args);
+    args.wzPackageId = wzPackageId;
+    args.dwProcessId = dwProcessId;
+    args.recommendation = *pAction;
+
+    results.cbSize = sizeof(results);
+    results.action = *pAction;
+
+    hr = SendBAMessage(pUserExperience, BOOTSTRAPPER_APPLICATION_MESSAGE_ONEXECUTEPROCESSCANCEL, &args, &results);
+    ExitOnFailure(hr, "BA OnExecuteProcessCancel failed.");
+
+    *pAction = results.action;
+
+LExit:
+    return hr;
+}
+
 EXTERN_C BAAPI UserExperienceOnExecuteProgress(
     __in BURN_USER_EXPERIENCE* pUserExperience,
     __in_z LPCWSTR wzPackageId,
diff --git a/src/burn/engine/userexperience.h b/src/burn/engine/userexperience.h
index 2f18acdd..de558ad5 100644
--- a/src/burn/engine/userexperience.h
+++ b/src/burn/engine/userexperience.h
@@ -398,6 +398,12 @@ BAAPI UserExperienceOnExecutePatchTarget(
     __in_z LPCWSTR wzPackageId,
     __in_z LPCWSTR wzTargetProductCode
     );
+BAAPI UserExperienceOnExecuteProcessCancel(
+    __in BURN_USER_EXPERIENCE* pUserExperience,
+    __in_z LPCWSTR wzPackageId,
+    __in DWORD dwProcessId,
+    __inout BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION* pAction
+    );
 BAAPI UserExperienceOnExecuteProgress(
     __in BURN_USER_EXPERIENCE* pUserExperience,
     __in_z LPCWSTR wzPackageId,
diff --git a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
index 7375af86..35415dc3 100644
--- a/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
+++ b/src/burn/test/BurnUnitTest/BurnUnitTest.vcxproj
@@ -80,7 +80,9 @@
   <ItemGroup>
     <None Include="TestData\CacheTest\CacheSignatureTest.File" CopyToOutputDirectory="PreserveNewest" />
     <None Include="TestData\PlanTest\BasicFunctionality_BundleA_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
+    <None Include="TestData\PlanTest\Failure_BundleD_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
     <None Include="TestData\PlanTest\MsiTransaction_BundleAv1_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
+    <None Include="TestData\PlanTest\MsuPackageFixture_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
     <None Include="TestData\PlanTest\Slipstream_BundleA_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
     <None Include="TestData\PlanTest\Slipstream_BundleA_modified_manifest.xml" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
diff --git a/src/burn/test/BurnUnitTest/PlanTest.cpp b/src/burn/test/BurnUnitTest/PlanTest.cpp
index 4d726fb4..770922b4 100644
--- a/src/burn/test/BurnUnitTest/PlanTest.cpp
+++ b/src/burn/test/BurnUnitTest/PlanTest.cpp
@@ -10,7 +10,9 @@ static HRESULT WINAPI PlanTestBAProc(
     );
 
 static LPCWSTR wzMsiTransactionManifestFileName = L"MsiTransaction_BundleAv1_manifest.xml";
+static LPCWSTR wzSingleExeManifestFileName = L"Failure_BundleD_manifest.xml";
 static LPCWSTR wzSingleMsiManifestFileName = L"BasicFunctionality_BundleA_manifest.xml";
+static LPCWSTR wzSingleMsuManifestFileName = L"MsuPackageFixture_manifest.xml";
 static LPCWSTR wzSlipstreamManifestFileName = L"Slipstream_BundleA_manifest.xml";
 static LPCWSTR wzSlipstreamModifiedManifestFileName = L"Slipstream_BundleA_modified_manifest.xml";
 
@@ -658,6 +660,97 @@ namespace Bootstrapper
             ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[0], L"PackageA", BURN_PACKAGE_REGISTRATION_STATE_PRESENT, BURN_PACKAGE_REGISTRATION_STATE_PRESENT);
         }
 
+        [Fact]
+        void SingleExeInstallTest()
+        {
+            HRESULT hr = S_OK;
+            BURN_ENGINE_STATE engineState = { };
+            BURN_ENGINE_STATE* pEngineState = &engineState;
+            BURN_PLAN* pPlan = &engineState.plan;
+
+            InitializeEngineStateForCorePlan(wzSingleExeManifestFileName, pEngineState);
+            DetectAttachedContainerAsAttached(pEngineState);
+            DetectPermanentPackagesAsPresentAndCached(pEngineState);
+
+            hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_INSTALL);
+            NativeAssert::Succeeded(hr, "CorePlan failed");
+
+            Assert::Equal<DWORD>(BOOTSTRAPPER_ACTION_INSTALL, pPlan->action);
+            NativeAssert::StringEqual(L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}", pPlan->wzBundleId);
+            NativeAssert::StringEqual(L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}", pPlan->wzBundleProviderKey);
+            Assert::Equal<BOOL>(FALSE, pPlan->fEnabledForwardCompatibleBundle);
+            Assert::Equal<BOOL>(TRUE, pPlan->fPerMachine);
+            Assert::Equal<BOOL>(TRUE, pPlan->fCanAffectMachineState);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDisableRollback);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDisallowRemoval);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDowngrade);
+
+            BOOL fRollback = FALSE;
+            DWORD dwIndex = 0;
+            ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, TRUE, L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}", L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}");
+            Assert::Equal(dwIndex, pPlan->cRegistrationActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, FALSE, L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}", L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}");
+            Assert::Equal(dwIndex, pPlan->cRollbackRegistrationActions);
+
+            fRollback = FALSE;
+            dwIndex = 0;
+            ValidateCacheCheckpoint(pPlan, fRollback, dwIndex++, 1);
+            ValidateCachePackage(pPlan, fRollback, dwIndex++, L"ExeA");
+            ValidateCacheSignalSyncpoint(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cCacheActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            Assert::Equal(dwIndex, pPlan->cRollbackCacheActions);
+
+            Assert::Equal(1463267ull, pPlan->qwEstimatedSize);
+            Assert::Equal(119695ull, pPlan->qwCacheSizeTotal);
+
+            fRollback = FALSE;
+            dwIndex = 0;
+            DWORD dwExecuteCheckpointId = 2;
+            ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteWaitCachePackage(pPlan, fRollback, dwIndex++, L"ExeA");
+            ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"ExeA", BOOTSTRAPPER_ACTION_STATE_INSTALL);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cExecuteActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            dwExecuteCheckpointId = 2;
+            ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteExePackage(pPlan, fRollback, dwIndex++, L"ExeA", BOOTSTRAPPER_ACTION_STATE_UNINSTALL);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cRollbackActions);
+
+            Assert::Equal(1ul, pPlan->cExecutePackagesTotal);
+            Assert::Equal(2ul, pPlan->cOverallProgressTicksTotal);
+
+            dwIndex = 0;
+            Assert::Equal(dwIndex, pPlan->cRestoreRelatedBundleActions);
+
+            dwIndex = 0;
+            ValidateCleanAction(pPlan, dwIndex++, L"NetFx48Web");
+            ValidateCleanAction(pPlan, dwIndex++, L"ExeA");
+            Assert::Equal(dwIndex, pPlan->cCleanActions);
+
+            UINT uIndex = 0;
+            ValidatePlannedProvider(pPlan, uIndex++, L"{9C184683-04FB-49AD-9D79-65101BDC3EE3}", NULL);
+            Assert::Equal(uIndex, pPlan->cPlannedProviders);
+
+            Assert::Equal(2ul, pEngineState->packages.cPackages);
+            ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[1], L"ExeA", BURN_PACKAGE_REGISTRATION_STATE_ABSENT, BURN_PACKAGE_REGISTRATION_STATE_PRESENT);
+        }
+
         [Fact]
         void SingleMsiCacheTest()
         {
@@ -1532,6 +1625,98 @@ namespace Bootstrapper
             ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[0], L"PackageA", BURN_PACKAGE_REGISTRATION_STATE_ABSENT, BURN_PACKAGE_REGISTRATION_STATE_ABSENT);
         }
 
+        [Fact]
+        void SingleMsuInstallTest()
+        {
+            HRESULT hr = S_OK;
+            BURN_ENGINE_STATE engineState = { };
+            BURN_ENGINE_STATE* pEngineState = &engineState;
+            BURN_PLAN* pPlan = &engineState.plan;
+
+            InitializeEngineStateForCorePlan(wzSingleMsuManifestFileName, pEngineState);
+            DetectAttachedContainerAsAttached(pEngineState);
+            DetectPackagesAsAbsent(pEngineState);
+
+            hr = CorePlan(pEngineState, BOOTSTRAPPER_ACTION_INSTALL);
+            NativeAssert::Succeeded(hr, "CorePlan failed");
+
+            Assert::Equal<DWORD>(BOOTSTRAPPER_ACTION_INSTALL, pPlan->action);
+            NativeAssert::StringEqual(L"{06077C60-DC46-4F4A-8D3C-05F869187191}", pPlan->wzBundleId);
+            NativeAssert::StringEqual(L"{06077C60-DC46-4F4A-8D3C-05F869187191}", pPlan->wzBundleProviderKey);
+            Assert::Equal<BOOL>(FALSE, pPlan->fEnabledForwardCompatibleBundle);
+            Assert::Equal<BOOL>(TRUE, pPlan->fPerMachine);
+            Assert::Equal<BOOL>(TRUE, pPlan->fCanAffectMachineState);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDisableRollback);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDisallowRemoval);
+            Assert::Equal<BOOL>(FALSE, pPlan->fDowngrade);
+
+            BOOL fRollback = FALSE;
+            DWORD dwIndex = 0;
+            ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, TRUE, L"{06077C60-DC46-4F4A-8D3C-05F869187191}", L"{06077C60-DC46-4F4A-8D3C-05F869187191}");
+            Assert::Equal(dwIndex, pPlan->cRegistrationActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            ValidateDependentRegistrationAction(pPlan, fRollback, dwIndex++, FALSE, L"{06077C60-DC46-4F4A-8D3C-05F869187191}", L"{06077C60-DC46-4F4A-8D3C-05F869187191}");
+            Assert::Equal(dwIndex, pPlan->cRollbackRegistrationActions);
+
+            fRollback = FALSE;
+            dwIndex = 0;
+            ValidateCacheCheckpoint(pPlan, fRollback, dwIndex++, 1);
+            ValidateCachePackage(pPlan, fRollback, dwIndex++, L"test.msu");
+            ValidateCacheSignalSyncpoint(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cCacheActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            ValidateCacheRollbackPackage(pPlan, fRollback, dwIndex++, L"test.msu");
+            ValidateCacheCheckpoint(pPlan, fRollback, dwIndex++, 1);
+            Assert::Equal(dwIndex, pPlan->cRollbackCacheActions);
+
+            Assert::Equal(56ull, pPlan->qwEstimatedSize);
+            Assert::Equal(140ull, pPlan->qwCacheSizeTotal);
+
+            fRollback = FALSE;
+            dwIndex = 0;
+            DWORD dwExecuteCheckpointId = 2;
+            ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteWaitCachePackage(pPlan, fRollback, dwIndex++, L"test.msu");
+            ValidateExecuteMsuPackage(pPlan, fRollback, dwIndex++, L"test.msu", BOOTSTRAPPER_ACTION_STATE_INSTALL);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cExecuteActions);
+
+            fRollback = TRUE;
+            dwIndex = 0;
+            dwExecuteCheckpointId = 2;
+            ValidateExecuteRollbackBoundaryStart(pPlan, fRollback, dwIndex++, L"WixDefaultBoundary", TRUE, FALSE);
+            ValidateExecuteUncachePackage(pPlan, fRollback, dwIndex++, L"test.msu");
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteMsuPackage(pPlan, fRollback, dwIndex++, L"test.msu", BOOTSTRAPPER_ACTION_STATE_UNINSTALL);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteCheckpoint(pPlan, fRollback, dwIndex++, dwExecuteCheckpointId++);
+            ValidateExecuteRollbackBoundaryEnd(pPlan, fRollback, dwIndex++);
+            Assert::Equal(dwIndex, pPlan->cRollbackActions);
+
+            Assert::Equal(1ul, pPlan->cExecutePackagesTotal);
+            Assert::Equal(2ul, pPlan->cOverallProgressTicksTotal);
+
+            dwIndex = 0;
+            Assert::Equal(dwIndex, pPlan->cRestoreRelatedBundleActions);
+
+            dwIndex = 0;
+            Assert::Equal(dwIndex, pPlan->cCleanActions);
+
+            UINT uIndex = 0;
+            ValidatePlannedProvider(pPlan, uIndex++, L"{06077C60-DC46-4F4A-8D3C-05F869187191}", NULL);
+            Assert::Equal(uIndex, pPlan->cPlannedProviders);
+
+            Assert::Equal(1ul, pEngineState->packages.cPackages);
+            ValidateNonPermanentPackageExpectedStates(&pEngineState->packages.rgPackages[0], L"test.msu", BURN_PACKAGE_REGISTRATION_STATE_PRESENT, BURN_PACKAGE_REGISTRATION_STATE_PRESENT);
+        }
+
         [Fact]
         void SlipstreamInstallTest()
         {
@@ -2571,6 +2756,21 @@ namespace Bootstrapper
             NativeAssert::StringEqual(wzPackageId, pOrderedPatch->pPackage->sczId);
         }
 
+        void ValidateExecuteMsuPackage(
+            __in BURN_PLAN* pPlan,
+            __in BOOL fRollback,
+            __in DWORD dwIndex,
+            __in LPCWSTR wzPackageId,
+            __in BOOTSTRAPPER_ACTION_STATE action
+            )
+        {
+            BURN_EXECUTE_ACTION* pAction = ValidateExecuteActionExists(pPlan, fRollback, dwIndex);
+            Assert::Equal<DWORD>(BURN_EXECUTE_ACTION_TYPE_MSU_PACKAGE, pAction->type);
+            NativeAssert::StringEqual(wzPackageId, pAction->msuPackage.pPackage->sczId);
+            Assert::Equal<DWORD>(action, pAction->msuPackage.action);
+            Assert::Equal<BOOL>(FALSE, pAction->fDeleted);
+        }
+
         void ValidateExecutePackageDependency(
             __in BURN_PLAN* pPlan,
             __in BOOL fRollback,
diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml
new file mode 100644
index 00000000..6afb0108
--- /dev/null
+++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/Failure_BundleD_manifest.xml
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><BurnManifest xmlns="http://wixtoolset.org/schemas/v4/2008/Burn"><Log PathVariable="WixBundleLog" Prefix="~FailureTests_BundleD" Extension=".log" /><RelatedBundle Id="{3C1A4842-81AC-4C90-8B35-A5E18F034C8D}" Action="Upgrade" /><Variable Id="TestGroupName" Value="FailureTests" Type="string" Hidden="no" Persisted="no" /><Variable Id="WixBundleInProgressName" Hidden="no" Persisted="yes" /><Variable Id="WixBundleName" Hidden="no" Persisted="yes" /><Variable Id="WixBundleOriginalSource" Hidden="no" Persisted="yes" /><Variable Id="WixBundleOriginalSourceFolder" Hidden="no" Persisted="yes" /><Variable Id="WixBundleLastUsedSource" Hidden="no" Persisted="yes" /><RegistrySearch Id="NETFRAMEWORK45" Variable="NETFRAMEWORK45" Root="HKLM" Key="SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" Value="Release" Type="value" VariableType="string" /><RegistrySearch Id="wrsQ7JTGqvaQuDYjfHJoyjxtkLlR6c" Variable="ExeA_Version" Root="HKLM" Key="Software\WiX\Tests\FailureTests\ExeA" Value="Version" Type="value" VariableType="string" /><UX><Payload Id="WixManagedBootstrapperApplicationHost" FilePath="mbahost.dll" SourcePath="u30" /><Payload Id="payO60IVK4ATGzPpMz3rwVbUWl6DyU" FilePath="WixToolset.Mba.Host.config" SourcePath="u0" /><Payload Id="payxj4zDAKL2NVlz4ohp0GvwFHepyI" FilePath="TestBA.dll" SourcePath="u1" /><Payload Id="pay1hOSAUC8_D633cD2TXpIXCL30OU" FilePath="mbanative.dll" SourcePath="u2" /><Payload Id="payujy6Izl_BlUNfHt2eI.ADfjYAv4" FilePath="WixToolset.Mba.Core.dll" SourcePath="u3" /><Payload Id="payR4EbR4OTDZpPEycWaSSM_gZRBWM" FilePath="mbapreq.thm" SourcePath="u4" /><Payload Id="paylVCy2Ecl8pHPdJTCQZryUG4T9us" FilePath="mbapreq.png" SourcePath="u5" /><Payload Id="payTaG4B_lob1aLcKFaOqSSG3MPMpU" FilePath="mbapreq.wxl" SourcePath="u6" /><Payload Id="payZwIGuiezVTitZOoZKxyh2DdRSGs" FilePath="1028\mbapreq.wxl" SourcePath="u7" /><Payload Id="pay.herBWX.LlOh8jLsx24aWdunV_0" FilePath="1029\mbapreq.wxl" SourcePath="u8" /><Payload Id="pay8DkMszYsoxxdgX14huLDMYXylQg" FilePath="1030\mbapreq.wxl" SourcePath="u9" /><Payload Id="payPaHpoTeOdkW.TK99IDwktNLhTAg" FilePath="1031\mbapreq.wxl" SourcePath="u10" /><Payload Id="pay45AtAzterLTMzZgdxxtuYvaiXwU" FilePath="1032\mbapreq.wxl" SourcePath="u11" /><Payload Id="payA2VEKIqhePyNIEmr14eyH3JoVLc" FilePath="1035\mbapreq.wxl" SourcePath="u12" /><Payload Id="payvre23ObscjzhcaFIifUAkXMdPa8" FilePath="1036\mbapreq.wxl" SourcePath="u13" /><Payload Id="paytxUV3vuBbG2c.a9c.d_sZX2x6wA" FilePath="1038\mbapreq.wxl" SourcePath="u14" /><Payload Id="payYvMWRK9xelo5.sQn7jRkJIaBp9A" FilePath="1040\mbapreq.wxl" SourcePath="u15" /><Payload Id="pay68KKSApyQimbA25t6kSbqhdeH10" FilePath="1041\mbapreq.wxl" SourcePath="u16" /><Payload Id="paypiqxaHpYZqx.9eDVjQrj1igLbRY" FilePath="1042\mbapreq.wxl" SourcePath="u17" /><Payload Id="payTO0YwZzxKpbqdrBVUcVRTu3BFe8" FilePath="1043\mbapreq.wxl" SourcePath="u18" /><Payload Id="payIXg2ldBJukRzhqWolJVOEbTmF34" FilePath="1044\mbapreq.wxl" SourcePath="u19" /><Payload Id="payOHIZbSkIvrpwKkkXI173tv3u3B4" FilePath="1045\mbapreq.wxl" SourcePath="u20" /><Payload Id="payQRQ_UZl_R2UtV0xDXB2yeH2bg3E" FilePath="1046\mbapreq.wxl" SourcePath="u21" /><Payload Id="payhrejLLBfc1i27iN._QPhQ4K337I" FilePath="1049\mbapreq.wxl" SourcePath="u22" /><Payload Id="payqEzaDNzxB68vGp29jgDcCos6dvg" FilePath="1051\mbapreq.wxl" SourcePath="u23" /><Payload Id="paydz8Vk8xSTyYohgGXTSIxWGXL5.Q" FilePath="1053\mbapreq.wxl" SourcePath="u24" /><Payload Id="pay0HRUZTlbC3taSOffJBsEj92Br8Y" FilePath="1055\mbapreq.wxl" SourcePath="u25" /><Payload Id="payIvUOkc_EMH7laMFehefNolV8hZo" FilePath="1060\mbapreq.wxl" SourcePath="u26" /><Payload Id="payLFhOb.rHuk4sW5CYAPMShG0NjGI" FilePath="2052\mbapreq.wxl" SourcePath="u27" /><Payload Id="payqIKCmERK7Nhxx_nNXvRxdKqKDbI" FilePath="2070\mbapreq.wxl" SourcePath="u28" /><Payload Id="payqeWUzIVaEqjuRXN0z8ECC3Y4tCc" FilePath="3082\mbapreq.wxl" SourcePath="u29" /><Payload Id="paylfeHEjJSSTnNzY9QMZM2Ye3Ipy4" FilePath="mbapreq.dll" SourcePath="u31" /><Payload Id="payDPxs6uy8nbky.R7zhir2RRAfc.c" FilePath="WixToolset.Mba.Host.dll" SourcePath="u32" /><Payload Id="uxTxMXPVMXwQrPTMIGa5WGt93w0Ns" FilePath="BootstrapperApplicationData.xml" SourcePath="u33" /><Payload Id="uxYRbgitOs0K878jn5L_z7LdJ21KI" FilePath="BundleExtensionData.xml" SourcePath="u34" /></UX><Container Id="WixAttachedContainer" FileSize="24029" Hash="03F9C95A2ADA5563D3D937C0161F22A76E12F2F0AF2AA6BE567292D0AB122E2C42990E97CA9C1EE9A5F43A571B01C4ED7A3EA5759A6836AC8BFD959D7FFDCB18" FilePath="BundleD.exe" AttachedIndex="1" Attached="yes" Primary="yes" /><Payload Id="NetFx48Web" FilePath="redist\ndp48-web.exe" FileSize="1439328" CertificateRootPublicKeyIdentifier="F49F9B33E25E33CCA0BFB15A62B7C29FFAB3880B" CertificateRootThumbprint="ABDCA79AF9DD48A0EA702AD45260B3C03093FB4B" DownloadUrl="https://go.microsoft.com/fwlink/?LinkId=2085155" Packaging="external" SourcePath="redist\ndp48-web.exe" /><Payload Id="TestExe.exe" FilePath="TestExe.exe" FileSize="23552" Hash="4344604ECBA4DFE5DE7C680CB1AA5BD6FAA29BF95CE07740F02878C2BB1EF6DE6432944A0DB79B034D1C6F68CF80842EEE442EA8A551816E52D3F68901C50AB9" Packaging="embedded" SourcePath="a0" Container="WixAttachedContainer" /><Payload Id="paygJp32KbpyjbVEQFNbl5_izmhdZw" FilePath="TestExe.exe.config" FileSize="387" Hash="8C819A9E835F3921FA80C5C783AB0C42DDAADF0C0F2BEF8630EA122ABCB9DC8EAF0B14E061C46B37C92F55114BB09A8D5B1B613947A76A648953F2C63C0ACA63" Packaging="embedded" SourcePath="a1" Container="WixAttachedContainer" /><RollbackBoundary Id="WixDefaultBoundary" Vital="yes" Transaction="no" /><Registration Id="{9C184683-04FB-49AD-9D79-65101BDC3EE3}" ExecutableName="BundleD.exe" PerMachine="yes" Tag="" Version="1.0.0.0" ProviderKey="{9C184683-04FB-49AD-9D79-65101BDC3EE3}"><Arp Register="yes" DisplayName="~FailureTests - BundleD" DisplayVersion="1.0.0.0" /></Registration><Chain><ExePackage Id="NetFx48Web" Cache="remove" CacheId="642721C60D52051C7F3434D8710FE3406A7CFE10B2B39E90EA847719ED1697D7C614F2DF44AD50412B1DF8C98DD78FDC57CA1D047D28C81AC158092E5FB18040" InstallSize="1439328" Size="1439328" PerMachine="yes" Permanent="yes" Vital="yes" RollbackBoundaryForward="WixDefaultBoundary" LogPathVariable="NetFx48WebLog" RollbackLogPathVariable="WixBundleRollbackLog_NetFx48Web" DetectCondition="NETFRAMEWORK45 &gt;= 528040" InstallArguments="/q /norestart /log &quot;[NetFx48WebLog].html&quot;" UninstallArguments="" Uninstallable="no" RepairArguments="" Repairable="no" Protocol="netfx4"><PayloadRef Id="NetFx48Web" /></ExePackage><ExePackage Id="ExeA" Cache="remove" CacheId="4344604ECBA4DFE5DE7C680CB1AA5BD6FAA29BF95CE07740F02878C2BB1EF6DE6432944A0DB79B034D1C6F68CF80842EEE442EA8A551816E52D3F68901C50AB9" InstallSize="23939" Size="23939" PerMachine="yes" Permanent="no" Vital="yes" RollbackBoundaryBackward="WixDefaultBoundary" LogPathVariable="WixBundleLog_ExeA" RollbackLogPathVariable="WixBundleRollbackLog_ExeA" DetectCondition="ExeA_Version AND ExeA_Version &gt;= v1.0.0.0" InstallArguments="/s 5000 /regw &quot;HKLM\Software\WiX\Tests\FailureTests\ExeA,Version,String,1.0.0.0&quot;" UninstallArguments="/regd &quot;HKLM\Software\WiX\Tests\FailureTests\ExeA,Version&quot;" Uninstallable="yes" RepairArguments="/regw &quot;HKLM\Software\WiX\Tests\FailureTests\ExeA,Version,String,1.0.0.0&quot;" Repairable="yes"><PayloadRef Id="TestExe.exe" /><PayloadRef Id="paygJp32KbpyjbVEQFNbl5_izmhdZw" /></ExePackage></Chain><CommandLine Variables="upperCase" /></BurnManifest>
\ No newline at end of file
diff --git a/src/burn/test/BurnUnitTest/TestData/PlanTest/MsuPackageFixture_manifest.xml b/src/burn/test/BurnUnitTest/TestData/PlanTest/MsuPackageFixture_manifest.xml
new file mode 100644
index 00000000..fb6afa88
--- /dev/null
+++ b/src/burn/test/BurnUnitTest/TestData/PlanTest/MsuPackageFixture_manifest.xml
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><BurnManifest xmlns="http://wixtoolset.org/schemas/v4/2008/Burn"><Log PathVariable="WixBundleLog" Prefix="BurnBundle" Extension="log" /><RelatedBundle Id="{B94478B1-E1F3-4700-9CE8-6AA090854AEC}" Action="Upgrade" /><Variable Id="WixBundleInProgressName" Hidden="no" Persisted="yes" /><Variable Id="WixBundleName" Hidden="no" Persisted="yes" /><Variable Id="WixBundleOriginalSource" Hidden="no" Persisted="yes" /><Variable Id="WixBundleOriginalSourceFolder" Hidden="no" Persisted="yes" /><Variable Id="WixBundleLastUsedSource" Hidden="no" Persisted="yes" /><UX><Payload Id="payaQenPi7_8hq6T._EXtBW0NvR7gA" FilePath="fakeba.dll" SourcePath="u0" /><Payload Id="uxTxMXPVMXwQrPTMIGa5WGt93w0Ns" FilePath="BootstrapperApplicationData.xml" SourcePath="u1" /><Payload Id="uxYRbgitOs0K878jn5L_z7LdJ21KI" FilePath="BundleExtensionData.xml" SourcePath="u2" /></UX><Container Id="WixAttachedContainer" FileSize="119" Hash="06D28293FD57CD231E125EF9C82418A488928A98832A6937A77A3283A17A5C37F8D619C51759319A57E8F8A948FA73E8C5814185A0114130F3213AB268073555" FilePath="test.exe" AttachedIndex="1" Attached="yes" Primary="yes" /><Payload Id="test.msu" FilePath="test.msu" FileSize="28" Hash="B040F02D2F90E04E9AFBDC91C00CEB5DF97D48E205D96DC0A44E10AF8870794DAE62CA70224F12BE9112AA730BBE470CA81FB5617AAC690E832F3F84510E92BA" Packaging="embedded" SourcePath="a0" Container="WixAttachedContainer" /><RollbackBoundary Id="WixDefaultBoundary" Vital="yes" Transaction="no" /><Registration Id="{06077C60-DC46-4F4A-8D3C-05F869187191}" ExecutableName="test.exe" PerMachine="yes" Tag="" Version="1.0.0.0" ProviderKey="{06077C60-DC46-4F4A-8D3C-05F869187191}"><Arp Register="yes" DisplayName="BurnBundle" DisplayVersion="1.0.0.0" Publisher="Example Corporation" /></Registration><Chain><MsuPackage Id="test.msu" Cache="keep" CacheId="B040F02D2F90E04E9AFBDC91C00CEB5DF97D48E205D96DC0A44E10AF8870794DAE62CA70224F12BE9112AA730BBE470CA81FB5617AAC690E832F3F84510E92BA" InstallSize="28" Size="28" PerMachine="yes" Permanent="no" Vital="yes" RollbackBoundaryForward="WixDefaultBoundary" RollbackBoundaryBackward="WixDefaultBoundary" DetectCondition="DetectedTheMsu" KB="xyz"><PayloadRef Id="test.msu" /></MsuPackage></Chain><CommandLine Variables="upperCase" /></BurnManifest>
\ No newline at end of file
diff --git a/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp b/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
index 84e32867..444a917f 100644
--- a/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
+++ b/src/ext/Bal/wixstdba/WixStandardBootstrapperApplication.cpp
@@ -2170,6 +2170,14 @@ private: // privates
         m_pfnBAFunctionsProc(BA_FUNCTIONS_MESSAGE_ONAPPLYDOWNGRADE, pArgs, pResults, m_pvBAFunctionsProcContext);
     }
 
+    void OnExecuteProcessCancelFallback(
+        __in BA_ONEXECUTEPROCESSCANCEL_ARGS* pArgs,
+        __inout BA_ONEXECUTEPROCESSCANCEL_RESULTS* pResults
+        )
+    {
+        m_pfnBAFunctionsProc(BA_FUNCTIONS_MESSAGE_ONEXECUTEPROCESSCANCEL, pArgs, pResults, m_pvBAFunctionsProcContext);
+    }
+
 
 public: //CBalBaseBootstrapperApplication
     virtual STDMETHODIMP Initialize(
diff --git a/src/test/burn/TestBA/TestBA.cs b/src/test/burn/TestBA/TestBA.cs
index 3688e028..c219ce9c 100644
--- a/src/test/burn/TestBA/TestBA.cs
+++ b/src/test/burn/TestBA/TestBA.cs
@@ -21,7 +21,7 @@ namespace WixToolset.Test.BA
         private Form dummyWindow;
         private IntPtr windowHandle;
         private LaunchAction action;
-        private ManualResetEvent wait;
+        private readonly ManualResetEvent wait;
         private int result;
 
         private string updateBundlePath;
@@ -397,6 +397,35 @@ namespace WixToolset.Test.BA
             }
         }
 
+        protected override void OnExecutePackageComplete(ExecutePackageCompleteEventArgs args)
+        {
+            bool logTestRegistryValue;
+            string recordTestRegistryValue = this.ReadPackageAction(args.PackageId, "RecordTestRegistryValue");
+            if (!String.IsNullOrEmpty(recordTestRegistryValue) && Boolean.TryParse(recordTestRegistryValue, out logTestRegistryValue) && logTestRegistryValue)
+            {
+                var value = this.ReadTestRegistryValue(args.PackageId);
+                this.Log("TestRegistryValue: {0}, Version, '{1}'", args.PackageId, value);
+            }
+        }
+
+        protected override void OnExecuteProcessCancel(ExecuteProcessCancelEventArgs args)
+        {
+            BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action;
+            string actionValue = this.ReadPackageAction(args.PackageId, "ProcessCancelAction");
+            if (actionValue != null && TryParseEnum<BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION>(actionValue, out action))
+            {
+                args.Action = action;
+            }
+
+            if (args.Action == BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION.Abandon)
+            {
+                // Give time to the process to start before its files are deleted.
+                Thread.Sleep(2000);
+            }
+
+            this.Log("OnExecuteProcessCancel({0})", args.Action);
+        }
+
         protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args)
         {
             this.Log("OnExecuteFilesInUse() - package: {0}, source: {1}, retries remaining: {2}, data: {3}", args.PackageId, args.Source, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray()));
@@ -488,43 +517,34 @@ namespace WixToolset.Test.BA
         private void TestVariables()
         {
             // First make sure we can check and get standard variables of each type.
+            if (this.Engine.ContainsVariable("WindowsFolder"))
             {
-                string value = null;
-                if (this.Engine.ContainsVariable("WindowsFolder"))
-                {
-                    value = this.Engine.GetVariableString("WindowsFolder");
-                    this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder");
-                }
-                else
-                {
-                    throw new Exception("Engine did not define a standard variable: WindowsFolder");
-                }
+                string value = this.Engine.GetVariableString("WindowsFolder");
+                this.Engine.Log(LogLevel.Verbose, String.Format("TEST: Successfully retrieved a string variable: WindowsFolder '{0}'", value));
+            }
+            else
+            {
+                throw new Exception("Engine did not define a standard variable: WindowsFolder");
             }
 
+            if (this.Engine.ContainsVariable("NTProductType"))
             {
-                long value = 0;
-                if (this.Engine.ContainsVariable("NTProductType"))
-                {
-                    value = this.Engine.GetVariableNumeric("NTProductType");
-                    this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType");
-                }
-                else
-                {
-                    throw new Exception("Engine did not define a standard variable: NTProductType");
-                }
+                long value = this.Engine.GetVariableNumeric("NTProductType");
+                this.Engine.Log(LogLevel.Verbose, String.Format("TEST: Successfully retrieved a numeric variable: NTProductType '{0}'", value));
+            }
+            else
+            {
+                throw new Exception("Engine did not define a standard variable: NTProductType");
             }
 
+            if (this.Engine.ContainsVariable("VersionMsi"))
             {
-                string value = null;
-                if (this.Engine.ContainsVariable("VersionMsi"))
-                {
-                    value = this.Engine.GetVariableVersion("VersionMsi");
-                    this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi");
-                }
-                else
-                {
-                    throw new Exception("Engine did not define a standard variable: VersionMsi");
-                }
+                string value = this.Engine.GetVariableVersion("VersionMsi");
+                this.Engine.Log(LogLevel.Verbose, String.Format("TEST: Successfully retrieved a version variable: VersionMsi '{0}'", value));
+            }
+            else
+            {
+                throw new Exception("Engine did not define a standard variable: VersionMsi");
             }
 
             // Now validate that Contians returns false for non-existant variables of each type.
@@ -596,6 +616,15 @@ namespace WixToolset.Test.BA
             }
         }
 
+        private string ReadTestRegistryValue(string name)
+        {
+            string testName = this.Engine.GetVariableString("TestGroupName");
+            using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\{0}\{1}", testName, name)))
+            {
+                return testKey == null ? null : testKey.GetValue("Version") as string;
+            }
+        }
+
         private static bool TryParseEnum<T>(string value, out T t)
         {
             try
diff --git a/src/test/burn/TestData/FailureTests/BundleD/BundleD.wixproj b/src/test/burn/TestData/FailureTests/BundleD/BundleD.wixproj
new file mode 100644
index 00000000..7b7408c6
--- /dev/null
+++ b/src/test/burn/TestData/FailureTests/BundleD/BundleD.wixproj
@@ -0,0 +1,19 @@
+<!-- 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. -->
+<Project Sdk="WixToolset.Sdk">
+  <PropertyGroup>
+    <OutputType>Bundle</OutputType>
+    <UpgradeCode>{3C1A4842-81AC-4C90-8B35-A5E18F034C8D}</UpgradeCode>
+    <Version>1.0.0.0</Version>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="..\..\Templates\Bundle.wxs" Link="Bundle.wxs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="WixToolset.Bal.wixext" />
+    <PackageReference Include="WixToolset.NetFx.wixext" />
+    <PackageReference Include="WixToolset.Util.wixext" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/test/burn/TestData/FailureTests/BundleD/BundleD.wxs b/src/test/burn/TestData/FailureTests/BundleD/BundleD.wxs
new file mode 100644
index 00000000..ca70236d
--- /dev/null
+++ b/src/test/burn/TestData/FailureTests/BundleD/BundleD.wxs
@@ -0,0 +1,19 @@
+<!-- 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. -->
+
+<?define TestExeRegistryKey = Software\WiX\Tests\$(var.TestGroupName)\ExeA?>
+
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
+  <Fragment>
+    <util:RegistrySearch Root="HKLM" Key="$(var.TestExeRegistryKey)" Value="Version" Variable="ExeA_Version" />
+
+    <PackageGroup Id="BundlePackages">
+      <ExePackage Id="ExeA" Cache="remove" PerMachine="yes"
+                  DetectCondition="ExeA_Version AND ExeA_Version &gt;= v$(var.Version)"
+                  InstallArguments="/s 5000 /regw &quot;HKLM\$(var.TestExeRegistryKey),Version,String,$(var.Version)&quot;"
+                  RepairArguments="/regw &quot;HKLM\$(var.TestExeRegistryKey),Version,String,$(var.Version)&quot;"
+                  UninstallArguments="/regd &quot;HKLM\$(var.TestExeRegistryKey),Version&quot;">
+        <PayloadGroupRef Id="TestExePayloads" />
+      </ExePackage>
+    </PackageGroup>
+  </Fragment>
+</Wix>
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/FailureTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/FailureTests.cs
index d8428a54..bbc0b387 100644
--- a/src/test/burn/WixToolsetTest.BurnE2E/FailureTests.cs
+++ b/src/test/burn/WixToolsetTest.BurnE2E/FailureTests.cs
@@ -2,7 +2,9 @@
 
 namespace WixToolsetTest.BurnE2E
 {
+    using System.Threading;
     using WixTestTools;
+    using WixToolset.Mba.Core;
     using Xunit;
     using Xunit.Abstractions;
 
@@ -10,6 +12,46 @@ namespace WixToolsetTest.BurnE2E
     {
         public FailureTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { }
 
+        [Fact]
+        public void CanCancelExePackageAndAbandonIt()
+        {
+            var bundleD = this.CreateBundleInstaller("BundleD");
+            var testBAController = this.CreateTestBAController();
+
+            // Cancel package ExeA after it starts.
+            testBAController.SetPackageCancelExecuteAtProgress("ExeA", 1);
+            testBAController.SetPackageRecordTestRegistryValue("ExeA");
+
+            var logPath = bundleD.Install((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_USEREXIT);
+            bundleD.VerifyUnregisteredAndRemovedFromPackageCache();
+
+            Assert.True(LogVerifier.MessageInLogFile(logPath, "TestRegistryValue: ExeA, Version, ''"));
+
+            // Make sure ExeA finishes running.
+            Thread.Sleep(3000);
+
+            bundleD.VerifyExeTestRegistryValue("ExeA", "1.0.0.0");
+        }
+
+        [Fact]
+        public void CanCancelExePackageAndWaitUntilItCompletes()
+        {
+            var bundleD = this.CreateBundleInstaller("BundleD");
+            var testBAController = this.CreateTestBAController();
+
+            // Cancel package ExeA after it starts.
+            testBAController.SetPackageCancelExecuteAtProgress("ExeA", 1);
+            testBAController.SetPackageProcessCancelAction("ExeA", BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION.Wait);
+            testBAController.SetPackageRecordTestRegistryValue("ExeA");
+
+            var logPath = bundleD.Install((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_USEREXIT);
+            bundleD.VerifyUnregisteredAndRemovedFromPackageCache();
+
+            Assert.True(LogVerifier.MessageInLogFile(logPath, "TestRegistryValue: ExeA, Version, '1.0.0.0'"));
+
+            bundleD.VerifyExeTestRegistryValue("ExeA", "1.0.0.0");
+        }
+
         [Fact]
         public void CanCancelMsiPackageVeryEarly()
         {
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs b/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs
index d2e8a1ca..fa553919 100644
--- a/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs
+++ b/src/test/burn/WixToolsetTest.BurnE2E/TestBAController.cs
@@ -147,6 +147,21 @@ namespace WixToolsetTest.BurnE2E
             this.SetPackageState(packageId, String.Concat(featureId, "Requested"), state.ToString());
         }
 
+        /// <summary>
+        /// Requests the BA to log the test registry value for the specified package.
+        /// </summary>
+        /// <param name="packageId"></param>
+        /// <param name="value"></param>
+        public void SetPackageRecordTestRegistryValue(string packageId, string value = "true")
+        {
+            this.SetPackageState(packageId, "RecordTestRegistryValue", value);
+        }
+
+        public void SetPackageProcessCancelAction(string packageId, BOOTSTRAPPER_EXECUTEPROCESSCANCEL_ACTION action)
+        {
+            this.SetPackageState(packageId, "ProcessCancelAction", action.ToString());
+        }
+
         /// <summary>
         /// Sets the number of times to re-run the Detect phase.
         /// </summary>
-- 
cgit v1.2.3-55-g6feb