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
		}
"@
}

“No Response Ping” during network discovery on Server Core

No Response Ping

When running a discovery for network devices in SCOM 2012 R2 from a server running Server 2012 R2 Core, the discovery may fail with the above error message. This doesn’t seem to occur with a full installation of Server 2012 R2. The solution, as per this guide is to enable the pre-defined firewall rules. To do this in PowerShell:

Get-NetFirewallRule | ? { $_.DisplayName -match "operations" -and $_.Enabled -ne "True" } | Set-NetFirewallRule -Enabled "True"

I know that querying and setting the Enabled property in this way seems odd, but this property is an enum rather than a Boolean.

After setting the above rules, run the discovery again and the devices should be detected.

Bug in SCOM’s DayTimeExpression operator of System.ExpressionFilter

Matthew Long’s post on the useful DayTimeExpression module of the System.ExpressionFilter data filter is the only useful information I could find on its use, save for the MSDN documentation. However, in using the module, I’ve noticed what appears to be a bug in its implementation.

The problem

The module works by checking whether a date-time is bound (or not bound, as defined by <InRange />) by a window that’s defined by (a) a days of the week mask and (b) a time window, the start and end of which are specified by the number of seconds since midnight. The problem appears to be that the date-time is interpreted as local time, despite being defined in ISO 8601 format, e.g. 2015-05-29T17:15:00Z, rather than being correctly interpreted as UTC and converted to local time before being checked against the defined window. This problem occurs whether a date-time is manually specified, e.g. <Value Type="DateTime">2015-05-29T17:15:00Z</Value>, or is obtained at runtime, e.g. <XPathQuery Type="DateTime">./@time</XPathQuery>.

The workaround

It may be possible to solve this problem using XQuery/XPath, but my solution is to return the current local date-time from the PowerShell script called by the data source, albeit presented as UTC in ISO 8601 format. For example, if the script were executed at 17:15 (local time), the script would return, in its property bag, a property with a value of 2015-05-29T17:15:00Z. This is a hack, as, because I’m on BST (UTC+1), this value should be 2015-05-29T16:15:00Z, but this triggers the bug.

In my PowerShell script:

$Bag.AddValue("Date-time executed (local time)", (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ"))

In my monitor type:

<Expression>
	<DayTimeExpression>
		<ValueExpression>
			<XPathQuery Type="DateTime">Property[@Name='Date-time executed (local time)']</XPathQuery>
		</ValueExpression>
		<StartTime>$Config/SecondsFromMidnightCheckWindowStart$</StartTime>
		<EndTime>$Config/SecondsFromMidnightCheckWindowEnd$</EndTime>
		<Days>$Config/DaysOfWeekMask$</Days>
		<InRange>true</InRange>
	</DayTimeExpression>
</Expression>

I am now able to define the window using the number of seconds from midnight, local time, and System.ExpressionFilter correctly checks this window against the execution time.