0
votes

I'm having a hard time understanding how to properly install and uninstall custom actions, and what the purpose of rollback is. I have a custom action called CreateFSRegistryLink that creates a REG_LINK registry entry (which cannot be created by MSI/InstallShield directly AFAIK). I think I have this running properly for the most part because if the link is already there, it just returns ERROR_FUNCTION_NOT_CALLED, which MSI seems to handle gracefully, proceeding with the rest of the install. This ensures that multiple instances of the product can be installed cleanly (we have a multi-instance product).

The problem comes during un-install. CreateFSRegistryLink appears to be running again in non-rollback mode. From the MSI log I can see that it's running as it should during an install and but it also runs during an uninstall:

I'm checking the mode with:

if (!MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK))

When the condition is true I log a message, "CreateFSRegistryLink is running in non-rollback mode." When it is false, I log a message, "CreateFSRegistryLink is running in rollback mode, so was skipped." I have never seen the second message show up in the log.

I have CreateFSRegistryLink set up with In-Script execution of "Deferred Execution in System Context." I also have another custom action DeleteFSRegistryLink set up with In-Script execution of "Rollback Execution in System Context". I see it getting skipped during an install, but not during an un-install (I suspect it's running normally during an un-install, but have not added logging to confirm this).

I also have a custom action CountOtherFSSystems that sets FS_SystemCount to the number of systems (instances) besides the current instance. I set DeleteFSRegisryLink to have a condition to only run when FS_SystemCount<1 in the Exec sequence. This is how I can tell that it is being skipped during an install because MSI reports that the condition wasn't met and so DeleteFSRegistryLink was skipped. I expect this to help ensure that it only runs when the last instance is being un-installed. I think this condition is working based on log output, but I don't know how to get this DeleteFSRegistryLink custom action to run properly during un-install without the CreateFSRegistryLink action re-installing the link. The last reference to DeleteFSRegistryLink I see in the log is:

MSI (s) (08:CC) [09:42:23:708]: Executing op: CustomActionSchedule(Action=DeleteFSRegistryLink,ActionType=3329,Source=BinaryData,Target=DeleteFSRegistryLink,)

I haven't added logging to this function yet, so I don't know if it ran, but when the un-install is done, the link in the registry is still there. This is not entirely surprising because immediately after that I see that CreateFSRegistryLink ran again:

MSI (s) (08:CC) [09:42:23:708]: Executing op: ActionStart(Name=CreateFSRegistryLink,,)
Action 9:42:23: CreateFSRegistryLink. 
MSI (s) (08:CC) [09:42:23:708]: Executing op: CustomActionSchedule(Action=CreateFSRegistryLink,ActionType=3073,Source=BinaryData,Target=CreateFSRegistryLink,)
MSI (s) (08:0C) [09:42:23:739]: Invoking remote custom action. DLL: C:\windows\Installer\MSI37E1.tmp, Entrypoint: CreateFSRegistryLink
MSI (s) (08:70) [09:42:23:739]: Generating random cookie.
MSI (s) (08:70) [09:42:23:739]: Created Custom Action Server with PID 7640 (0x1DD8).
MSI (s) (08:18) [09:42:23:786]: Running as a service.
MSI (s) (08:18) [09:42:23:786]: Hello, I'm your 32bit Elevated custom action server.
CreateFSRegistryLink is running in non-rollback mode.

I followed the rule at https://msdn.microsoft.com/en-us/library/aa371369(v=vs.85).aspx of "A rollback custom action must always precede the deferred custom action it rolls back in the action sequence" which is still really not making sense to me seeing this log output and results. I think I'm missing a few key points here.

2
A rollback custom action is 'scheduled' (written to the installation script) prior to the deferred custom action. Later in InstallFinalize the installation script is executed and if a failure occurs MSI walks it backwards executing the rollback actions that were scheduled.Christopher Painter
Your question seems to be based on an assumption that rollback is related to uninstall. It is not. It occurs after a failure during a transaction. It is meant to allow you to undo what your previous action began, and there are transactions for each of install and uninstall.Michael Urman
@MichaelUrman Exactly. I was trying to figure out, if Rollback isn't the mechanism used to determine what happens during un-install, then what is the mechanism that determines what happens during uninstall. Given the answers I have come to the conclusion that conditions can be applied to custom actions to ensure they execute only during install or only during un-install or at other times too.BlueMonkMN
@BlueMonkMN: Many people use conditions to only execute some of their actions during an "install" or "uninstall", etc. This puts the complexity in the MSI authoring. My current preference for reusable actions is to make them unconditional in the MSI and use MsiGetComponentState, MsiGetMode, and table data to determine what they need to do (immediate) and do it (pass CustomActionData to some combination of deferred, commit, rollback). In your case, tracking the states of a shared component might avoid the manual refcount antics.Michael Urman

2 Answers

1
votes

To throw in my 2 cents: The issue seems to be related to custom action conditions. Regarding your call to MsiGetMode to see if it's a rollback, why bother? You sequence a rollback custom action before your actual deferred CA, and by definition it will be called only if the original custom action was called, and it's defined as a rollback CA and it needs no conditions. It may be that your uninstall CA can be the same as the rollback CA, but strictly speaking an uninstall CA can assume that its counterpart install CA worked correctly if it is coded correctly and failure causes the install to fail, whereas a rollback CA may need to assume that the install CA may have only partially worked and needs to check more system state.

If CreateRegistryFSLink is being called on uninstall then your condfition on that CA is incorrect.

If your code did or did not do something then it's up to you to remember what it did and the rollback CA undoes it.

The rest of it appears to be about the conditions on your custom actions. If you want one to be called only on uninstall of the product, then use REMOVE="ALL". If you have CAs related specifically to feature or component uninstall then (as Chris says) use a component or feature condition, they are here:

https://msdn.microsoft.com/en-us/library/aa368012(v=vs.85).aspx

If you want an uninstall CA to run as long as the product is not being upgraded, then REMOVE="ALL" and NOT UPGRADINGPRODUCTCODE will work.

So if you're still stuck, it may help to post your definitions of the CAs, in particular the conditions and types.

1
votes

This is on my 'required reading' list for MSI and a good place to start:

Installation Phases and In-Script Execution Options for Custom Actions in Windows Installer

The idea is that every change made by MSI should be transactional. You should be able to rollback the state change on failure during an install, upgrade, repair or uninstall.

Occasionally you'll come across an API where this is not possible. An example would be deleting a user account or interacting with the old IIS metabase API. If the API doesn't support a .commit() .rollback() ability then you have to just make the change in the commit phase execution. Considering that commit phase can be disabled by disabling rollback you have to do it early in those scenarios.

Read the white paper a few times, digest it a bit and then follow up with any other questions that you still have.

Edit: This is how I ended up setting up my custom actions:

  1. CountOtherFSSystems runs with Immediate Execution after InstallInitialize under all circumstances to set FS_SystemCount to the number of other instances that are installed.
  2. RollbackFSRegistryLink runs with Rollback Execution in System Context after CountOtherFSSystems under the condition FS_SystemCount<1 And $FSRegistry = 3 (When the FSRegistry component was being installed local). It calls the function to delete the registry link.
  3. CreateFSRegistryLink runs with Deferred Execution in System Context after RollbackFSRegistryLink under the condition $FSRegistry=3. It calls the function to create the registry link.
  4. A bunch of other functions in the sequence execute, and then we get to the standard action WriteRegistryValues.
  5. RollbackDeleteFSRegistryLink runs with Rollback Execution in System Context after WriteRegistryValues under the condition $FSRegistry<>3 (when the FSRegistry component was being removed, but the rollback needs to put it back). It calls the function to create the registry link.
  6. DeleteFSRegistryLink runs with Deferred Execution in System Context after RollbackDeleteFSRegistryLink under the condition FS_SystemCount<1 AND $FSRegistry <> 3. It calls the function to delete the registry link.
  7. TestError runs with Deferred Execution in System Context after DeleteFSRegistryLink. It calls a test function that just returns an error condition if the user says it's OK (via MSIProcessMessage) to introduce an error for test purposes here. (This function will be disabled for the production builds.)

I tested the following cases:

  • Error during installation of first instance - no registry entries or links are created.
  • Error during installation of second instance - only the first instance's registry entries remain, and the link remains also.
  • Error during uninstall of second instance - both instances and the link remain in the registry.
  • Error during un-install of last remaining instance - While the error is still displayed (before rollback occurs) I can see that the registry entries and link are all gone, and after proceeding, I see the rollback has restored the registry entries and the link.
  • Successful un-install of second instance - link and first instance's registry entries remain.
  • Successful un-install of last remaining instance - link and all registry entries are removed.

Please comment if you see anything I missed here. It seems pretty comprehensive to me.