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