Problem with variable names starting with “Data”

There is a known bug in SCOM, whereby PowerShell scripts that are embedded in rules or monitors (i.e. where the script is visible in the data source of the rule/monitor) can’t contain references to variables whose names start with Data, e.g. Database. More specifically, such variables can’t be accessed using the dollar notation, e.g. $Database. VSAE will throw an exception when attempting to build a project containing such references:

The configuration specified for Module DS is not valid.
: Incorrect expression specified: $Database
Unable to resolve this expression. Check the expression for errors.

There are two workarounds. The easiest is to simply rename all the affected variables; e.g.:

$Database

to

$_Database

A more complex solution, although it allows the existing variable names to be used, is to change the method of access: using the Set-Variable and Get-Variable cmdlets. For example:

Set-Variable DataSet (New-Object System.Data.DataSet)
Get-Variable DataSet -ValueOnly

Care must be taken, however, to ensure that the value is unboxed; e.g. this will always return a value of 1:

Set-Variable SomeValues (1..5)
(Get-Variable SomeValues -ValueOnly | measure).Count

Instead, to obtain the count, use:

((Get-Variable SomeValues -ValueOnly) | measure).Count

Escape special characters with -match and -like

Let’s try comparing two identical strings.

$String = "Get-BlogPost [is super]."

$String -match $String
False

$String -like $String
False

The first result is expected, because match implements the regex function, and brackets are special characters in regex. The second result is, perhaps, more surprising. Why does like not return True in this instance? It appears that like is underpinned by regex, meaning that regex special characters must be escaped. So, let’s try escaping them:

$String -match [Regex]::Escape($String)
True

$String -like [Regex]::Escape($String)
False

The first works because [Regex]::Escape() applies escape characters that are applicable to regex:

[Regex]::Escape($String)
Get-BlogPost\ \[is\ super]\.

However, this doesn’t work with the like operator, because, although regex characters must be escaped, they must be escaped with the standard PowerShell escape character: backtick (`). To do this, use [System.Management.Automation.WildcardPattern]::Escape():

[System.Management.Automation.WildcardPattern]::Escape($String)
Get-BlogPost `[is super`].

To summarise:

$String -match [Regex]::Escape($String)
True

$String -like [System.Management.Automation.WildcardPattern]::Escape($String)
True

Garbage collection in PowerShell scripts in SCOM

SCOM executes PowerShell scripts in a single AppDomain, i.e. it doesn’t launch a new instance for every script, as is often the case when running scripts locally or testing. This means that each script must clean-up after itself and not leave, for example, connections open or variables with references, as PowerShell will not run garbage collection on these objects, which results in handle and memory leaks.

The general principles:

  1. At the beginning of the script, determine those variables already in memory, which will primarily be system variables.
  2. At the end of the script, de-reference those variables created by the script, i.e. all those currently in memory, excluding those in memory when the script started.
  3. In the script, call the Close() method on objects when appropriate.
  4. At the end of the script, call the Close() and Dispose() methods on objects that support them.
  5. Do not create variables of type Constant, as these can’t be de-referenced.
  6. Wrap the main code body in a Try..Catch and include the clean-up code in the Finally section to ensure it’s always executed.
  7. It isn’t necessary to call [System.GC]::Collect().

Example:

Try
{
	# Store the variables in memory at start-up, excluding parameters supplied to the script.
	Set-Variable VariablesStartup -Option ReadOnly -Value (Get-Variable -Scope Global | ? { $_.Attributes.TypeId.Name -notcontains "ParameterAttribute" } | Select -ExpandProperty Name)

	Function Invoke-VariableCleanup
	{
	    $VariablesToBeRemoved = (Get-Variable -Scope Global | Select -ExpandProperty Name) | ? { $VariablesStartup -notcontains $_ }
	    ForEach ($Item in $VariablesToBeRemoved)
	    {
	        Remove-Variable -Name $Item -Scope Global -Force -ErrorAction SilentlyContinue
	    }
	}

	# Do something useful.
	$SQLConnection = New-Object System.Data.SqlClient.SqlConnection
	$SQLConnection.ConnectionString = "Server=ServerA;Integrated Security=True;Connection Timeout=600"
	$SQLConnection.Open()
	$SQLCommand = New-Object System.Data.SqlClient.SqlCommand
	$SQLCommand.CommandTimeout = 600
	$SQLCommand.CommandText = "SELECT 1"
	$SQLCommand.Connection = $SQLConnection
	
	$Adapter = New-Object System.Data.SqlClient.SqlDataAdapter $SQLCommand
	$DataSet = New-Object System.Data.DataSet
	$Adapter.Fill($DataSet) | Out-Null
	$DataTable = $DataSet.Tables[0]
	$SQLConnection.Close()
}
Catch
{
	# Log the error somewhere.
}
Finally
{
	If ($DataSet)
	{
		$DataSet.Dispose()
	}
	If ($Adapter)
	{
		$Adapter.Dispose()
	}
	If ($SQLCommand)
	{
		$SQLCommand.Dispose()
	}
	If ($SQLConnection)
	{
		$SQLConnection.Dispose()
	}

	Invoke-VariableCleanup
}

Type definitions

Type definitions can’t be unloaded from an AppDomain, so if one is defined in a script and is changed by an updated script, the type definition won’t change on the SCOM server where the script is executed. In order to allow for type definition updates, the monitoring agent, HealthService, must be restarted.

Example:

If (!([Management.Automation.PSTypeName]'SCOMResolutionState').Type)
{
	Add-Type -TypeDefinition @"
		public enum SCOMResolutionState
		{
			New = 0,
			Acknowledged = 249,
			Resolved = 254,
			Closed = 255
		}
"@
}