Custom Fix Scripts

ChangeBASE Custom Fixes

This document will take you through the steps necessary to create a PowerShell Custom Fix Script.

Overview of most used objects

A PowerShell Custom Fix Script will be passed two parameters from ChangeBASE:

  • FixInstance – this is an instance of a CustomFixScriptExecutor class. For more information on the methods this object provides look at the FixScriptBase class in the Fix API Documentation
  • Controller - this class inherits from FixScriptBase which provides helper methods to more complex MSI fixing commands

The Controller is an instance of the FixScriptController class. This class provides access to the following classes, method and property:

  • DataSupplier – Provides access to the underlying data relating to the issue which is to be fixed
  • FixBagItemHeader – Provides access to the actual issue which was identified by ChangeBASE. This object contains details about the Check and which Rule discovered the issue.
  • Commands – Provides access to commands which allow MSI database manipulation
  • ReadValidationTable – Method which reads in the validation table of an MSI and populates the Constraints dictionary
  • Constraints – Dictionary containing constraints read in from the MSI via the ReadValidationTable method

 

Custom Fix Script example

In this example we have a Custom Check defined. The Custom Check makes sure a Package has the ALLUSERS Property defined with a value of “1”. The ALLUSERS Property is case sensitive and therefore any variations on “ALLUSERS” must be corrected.

The Custom Check has two Rules defined:

1)      “RULE.1”: Identifies Packages where the ALLUSERS Property is NOT set to the value “1”

2)      “RULE.2”: Identifies Packages where the ALLUSERS Property is NOT present

The Custom Check requires a Custom Fix Script to be able to fix these two cases.

 

Writing a Custom Fix PowerShell script

The PowerShell script must start with the following line:

param($FixInstance, $Controller)

This allows ChangeBASE to pass in the two objects on which most actions will take place.

The following lines are not required but generally considered good practice just so you know that the code relying on these objects will be able to access them:

# do some initial checking to make sure that FixInstance and Controller are set

if($FixInstance -eq $null)

{

       throw "FixInstance is null"

}

 

if($Controller -eq $null)

{

       throw "Controller is null"

}

 

# make sure the FixBagItemHeader is set

if($Controller.FixBagItemHeader -eq $null)

{

       throw "FixBagItemHeader is null"

}

 

For any of the fixes we will require a .Net Dictionary object to store the properties we wish to insert/update in the MSI. Set up a variable as a new Dictionary as follows:

 

$newProperty = New-Object "System.Collections.Generic.Dictionary[[string],[object]]"

 

The DataSupplier initially does not contain any information about the package this Custom Fix Script will be fixing. To retrieve data relating to the package you must use the Fetch method.

In our example we are fixing something in the Property table of the MSI. We need to Fetch this table into the DataSupplier so that we can use any existing data in that table to complete our fix.

 

$Controller.DataSupplier.Fetch([CB.Data.Shape.CBDIdCollection]::EID_PROPERTY_EIDID)

 

At this point you can now start introducing the logic required to make the required fixes.

The Custom Check has two rules; “RULE.1” and “RULE.2”. We need to separate our fix into two distinct sections as each rule has different logic. The Shared property on FixBagItemHeader contains the CheckRule property. The CheckRule property is an object which represents the Rule which flagged up the issue in ChangeBASE. Using the Id property on the CheckRule we can then determine what fix we need to apply.

 

if($Controller.FixBagItemHeader.Shared.CheckRule.Id -eq "RULE.1")

 

The first Rule identifies Packages where the ALLUSERS property is found but its value is not set to “1” or the ALLUSERS property is not all in upper case (and that the Value is not necessarily set to “1”).

As we know the property exists we can use the FixBagItemHeader.TargetItem to get the actual property which is incorrectly set. The following code will cast the TargetItem to a PropertyInstance object and make sure that it is set.

 

[CB.Data.Shape.CBDIdCollection+PropertyInstance] $property = [CB.Data.Shape.CBDIdCollection+PropertyInstance]$Controller.FixBagItemHeader.TargetItem

if($property -eq $null)

{

       throw "Target must be a PropertyInstance row"

}

 

When issuing an update command we need the primary key(s) of the item(s) which need updating. The ChangeBASE Fixing API requires that an array of primary keys is defined, even if there is only one row which needs updating. The easiest way to create this array is as follows:

 

[object[]] $propNameArr = $property.Name

Using the Dictionary that was created earlier we can now set up the Property row as intended.

 

$newProperty.Add("Property", "ALLUSERS")

$newProperty.Add("Value", "1");

 

Everything is now set up correctly and we can now decide whether we need to Update the Property row (if the Property Name = “ALLUSERS” – all upper case) or whether we need to replace that row with a correctly formed row – when ALLUSERS isn’t all in upper case.

 

if($property.Name -ceq "ALLUSERS")

{

       $newProperty.Remove("Property")

 

       $Controller.Commands.InstallerCommands.Update("Property", $newProperty, $propNameArr);

}

 

The -ceq operator does a case sensitive equality check. As it is only necessary to update the Value for the Property row we remove the “Property” entry from the Dictionary.

The Update method updates the “Property” table with the values in the $newProperty Dictionary and uses the existing $propNameArr array for the keys of the rows to update. In this case it is just the one row which will be updated.

If the ALLUSERS Property isn’t all in upper case then the easiest way to “update” the invalid Property row is to first delete the row and then insert a new row.

 

else

{

$Controller.Commands.InstallerCommands.Delete("Property", $propNameArr);

$Controller.Commands.InstallerCommands.Insert("Property", $newProperty);

}

 

The first statement Deletes row(s) from the “Property” table with the Property key(s) specified in the $propNameArr array. The second statement then Inserts a new row into the “Property” table with the $newProperty Dictionary defining the column properties to set in the row.

That’s all that is required for the first Rule.

The second Rule needs to be executed when the CheckRule.Id is “RULE.2”. This Rule handles the case where the ALLUSERS Property is not set at all.

As this Property hasn’t been set, we simply need to populate the $newProperty Dictionary  with the required values and call the Insert method.

 

elseif($Controller.FixBagItemHeader.Shared.CheckRule.Id -eq "RULE.2")

{

       $newProperty.Add("Property", "ALLUSERS")

       $newProperty.Add("Value", "1");

 

       $Controller.Commands.InstallerCommands.Insert("Property", $newProperty);

}

 

This completes the custom fix script for this check.

The complete .ps1 file should be placed in %PROGRAMDATA%\ChangeBASE\FixScripts.

You can choose which Custom Fix to use for a particular Custom Check inside ChangeBASE on the Checks tab in Design View. If your Fix Script is not shown in the drop-down list then you can click the refresh button to get ChangeBASE to refresh its list of Custom Fix scripts.