VBScript Custom Actions and MSI properties

Helllo, my fellow MSI packagers (and long-time reader Duncan Paul)!  Long time, no post!  I thought I'd revisit something that I haven't had to do in a long time.  So here's an article about using VBScript to read and make use of MSI properties and some gotchas.

In general, A good packager NEVER hard codes with the exception of perhaps some specific configuration which should be stored in the Property table. 

If you have to create a VBScript which runs in the Deferred (System) Context to make system changes, then you'll need to make use of the CustomActionData property.  In a nutshell, there are very few native properties that you can use in VBScript custom actions in the deferred context - and they're not particularly useful.

Therefore, if you want to access a property, then you would do so using a special property called CustomActionData is a property that can hold the value of other properties (and also be available in the Deferred context).

For example, if you wanted to know the path to a file that's installed by your package, and also reference that file location in a VBScript, there are a few things you need to do.

Firstly, create a Type 51 custom action (Set Property) to ensure the property that you use in your script is persisted.

In this particular case, we have a file that's installed by our package, and we want our VBScript to know where that file is without terrible hard coding.  We know that if we were able to reference the path to that file in the MSI, we would normally use the [#FileKey] or [!FileKey] depending on whether you want the long ("C:\Program Files\Application Name\File.txt")  or short path ("C:\PROGRA~1\APPLIC~1\FILE.TXT") returned.

Ultimately, I want to run a VBScript custom action to find out the where my package is installing DisableProcessIsolation.vbs, so I can create a scheduled task to run that script periodically.

So create a custom action and name it whatever you wish.  The Source field is important though, and this will have to match the name of your VBScript Custom Action (which we will come to later).  For the target field, you would specify the property value that you are after. This field accepts accepts a Formatted string type, so that's why we can use the file key here.  Essentially, we are saying that we need to resolve the short path to the file.

 

 Next, we have to create our VBScript custom action to make use of that:

Here I'm using a VBScript custom action embedded in the CustomAction table.  Note that Windows Installer uses a different VBScript engine compared to Windows Script Host.  So the VBScript for access the Wscript.Shell object needs to be instantiated slightly differently and certain wscript methods may not work.

   Set objWSHShell = WScript.CreateObject("WScript.Shell")    '// Will fail
   Set objWSHShell = CreateObject("WScript.Shell")    '// Will work


 Note that the VBScript custom action MUST be named the same as the custom property we defined  earlier.  i.e. CreateCRMScheduledTask.

To read that property, you just simply need to add

strCustomActionData = Session.Property("CustomActionData")

Note that you can store multiple properties in the earlier step.  You just need to use a delimiter such as ";".  And then your VBScript would need to split the string with the delimiter so you can work with an array of properties.

E.g. Set Property Custom Action

Source:  CreateCRMScheduledTask
Target: [Property1];[Property2];[Property3]

Followed by your VBScript code which would look something like this:

strCustomActionData = Session.Property("CustomActionData")
arrMSIProperties = Split(strCustomActionData, ";")

strProperty0 = arrMSIProperties(0)
strProperty1 = arrMSIProperties(1)
strProperty2 = arrMSIProperties(2)

 

Finally, we need to sequence the custom actions we've created. The Set Property custom action must occur after InstallInitialize as per below:

And finally, our VBScript custom action must occur after when the MSI starts to make changes to the system. In this case, we need to ensure the file is installed by the package, so logically it needs to be sequenced after InstallFiles.

The creation of the scheduled task which is ultimately what I'm trying to do, requires elevation.  So this is the crux of why we need the custom action to be run in the System (Deferred context) whilst having the ability to reference the file path within the MSI.  We condition these custom actions to only run on installation, which is why "NOT Installed" as specified in the Condition field.

What was the problem I was trying to solve?

Of course, there are multiple ways to skin a cat.  Some options are not possible due to the constraints of the environment you're working in.  In our particular case, Powershell script execution by standard users are blocked.

We had an application that needed to integrate with Outlook, and for complete integration an additional user registry key needed to be set.  The vendor included a separate utility to perform this integration which modifies the Outlook profile for the logged in user.  However, there is an unintended side effect that it also deletes any registry values in its own registry key that may contain additional settings. 

We had to ensure that we set an additional registry value AFTER the initial integration had been run for that user (in their context), and only AFTER Outlook had been launched by the user.

Therefore, we can't use Active Setup to populate the key, because the registry value will just get deleted after the integration.  We can't force the closure of Outlook as that may result in a poor user experience and potential lost work.  We can't use SCCM configuration baselines to evaluate compliance and run a remediation script, as they're run in the SYSTEM context and has no visibility of the user's HKCU registry hive.  We can't use the PowerShell App Deploy Toolkit Invoke-HKCURegistrySettingsForAllUsers function, as we can't set this at install-time as they value would just get deleted during the per-user integration.  And we can't just brute force deploy a registry key to all affected users via Group Policy Preferences as the application deployments are targeting devices.  We also can't rely on creating a prelauncher shortcut as it's an Outlook add-in.

So I figured creating a scheduled task which runs every X minutes (as the user) would be the best way forward (in my opinion).  The scheduled task is configured to run only when the user is logged in, and has a scheduled expiration (configurable to be X days from the date of installation).  The scheduled task points points to a VBScript installed by the package, that simply checks for evidence that the integration has already run (through file and/or registry checks), and if so, it writes the necessary HKCU registry value to complete the integration.

Easy isn't it?  The end result we have looks like this:

#NoHardCoding

Comments

Popular posts from this blog

AppUserModelID & Disappearing Shortcuts in Windows 8

Sideloading Universal Windows Apps on Windows 10 (Deep Dive)

An alternative method of installing device drivers