Null-valued expression after starting RemoteRegistry service

So, I have a function that retrieves information from the registry about all of the installed software on a given machine. Because I’ve run into several machines with the RemoteRegistry service stopped, I wrote another function that will try to open the registry, if it gets a “The network path was not found” error, it tries to start the RemoteRegistry service and then tries to open the registry again.

function OpenRemoteRegistryKey {
	Param(
		[Parameter(ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$key,
		[Parameter(ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$ComputerName,
		[int]$Iteration = 1
	)
	
	$reg = $null
	try {
		#Create an instance of the Registry Object and open the HKLM base key
		$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$ComputerName)
	}
	catch [Exception] {
		if ($_.Exception.Message -like "*The network path was not found*")
		{
			if ($Iteration -ge 3) {
				Write-Error "Unable to start RemoteRegistry service on $ComputerName"
				$reg = $null
			}
			else {
				# Try to start the RemoteRegistry service on the remote machine
				# http://www.sqlservercentral.com/blogs/timradney/2011/07/18/starting-and-stopping-a-remote-service-with-powershell/
				$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StartService()
				$Iteration++
				switch ($response.ReturnValue) {
					# Service started successfully
					0 {
						Start-Sleep -Seconds 10
						OpenRemoteRegistryKey -ComputerName $ComputerName -key $key -Iteration $Iteration
					}
					# Service was already running
					10 {
						# Stop the RemoteRegistry service, wait 30 seconds, and retry
						$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StopService()
						Start-Sleep -Seconds 10
						OpenRemoteRegistryKey -ComputerName $ComputerName -key $key -Iteration $Iteration
					}
					default {
						Write-Error "Unable to start RemoteRegistry service on $ComputerName. ReturnValue: $($response.ReturnValue)"
						$reg = $null
					}
				}
			}
		}
		else {
			Write-Error "$ComputerName : $_"
			$reg = $null
		}
	}
	finally {
		$reg
	}
}

function Get-InstalledSoftware {
	[CmdletBinding()]
	Param (
		[Parameter(ValueFromPipelineByPropertyName=$true,Position=0,Mandatory=$true)][string[]]$ComputerName
	)

	BEGIN {
	}
	PROCESS {
		$installedApps = @()
		foreach($comp in $ComputerName){
			#Define the variable to hold the location of Currently Installed Programs
			$UninstallKey="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" 
			$reg = OpenRemoteRegistryKey -ComputerName $comp -key $UninstallKey
			if($reg) {
				#Drill down into the Uninstall key using the OpenSubKey Method and retrieve an array of strings that contain all the subkey names
				$subkeys = $reg.OpenSubKey($UninstallKey).GetSubKeyNames()
				#Open each Subkey and use GetValue Method to return the required values for each
				foreach($key in $subkeys){
					$thisSubKey = $reg.OpenSubKey($UninstallKey+"\\"+$key)
					$InstallDate = $($thisSubKey.GetValue("InstallDate"))
					if (($InstallDate -ne $null) -and ($InstallDate.Length -gt 0)) {
						$InstallDate = [datetime]::ParseExact($($thisSubKey.GetValue("InstallDate")), "yyyyMMdd", $null)
					}
					$obj = New-Object -TypeName PSObject -Property @{
						ComputerName=$comp
						DisplayName=$($thisSubKey.GetValue("DisplayName"))
						DisplayVersion=$($thisSubKey.GetValue("DisplayVersion"))
						InstallLocation=$($thisSubKey.GetValue("InstallLocation"))
						Publisher=$($thisSubKey.GetValue("Publisher"))
						InstallDate=$InstallDate
					}
					$installedApps += $obj
				}
			}			
		}
		$installedApps | Where-Object DisplayName | Select-Object ComputerName, DisplayName, DisplayVersion, InstallLocation, Publisher, InstallDate
	}
	END {
	}
}

For testing, I’ll intentionally stop the RemoteRegistry service on a machine and then run Get-InstalledSoftware with that computername. It looks like everything works as expected, but I always get a “You cannot call a method on a null-valued expression.” exception when it tries to open the subkeys. If I immediately run the Get-InstalledSoftware function again, the service is started and I get a good response.

By adding in some Write-Host lines in various places in the OpenRemoteRegistryKey function, I can see that the call works: it tries to open the registry key, catches the exception when it can’t, starts the service, sees a ReturnValue of 0, sleeps for 10 secs, tries to open the registry key again, gets a good response, and returns an object to the pipeline. In the Get-InstalledSoftware function, I can add some debug code before the “if($reg)” line and see that $reg is not null. I even added the “$reg.OpenSubKey($UninstallKey).GetSubKeyNames()” line, (with the proper substitutions), to the OpenRemoteRegistryKey function and got a good response there, but it failed in the Get-InstalledSoftware function.

Any ideas?

Output with error and subsequent call:

PS F:\Storage\Scripts\Windows\Tools> get-date

Tuesday, August 20, 2013 12:44:44 PM


PS F:\Storage\Scripts\Windows\Tools> $response = (gwmi -ComputerName adm701209 -Class win32_service -filter "Name='Remot
eRegistry'").stopservice()
PS F:\Storage\Scripts\Windows\Tools> Get-InstalledSoftware "adm701209"
You cannot call a method on a null-valued expression.
At F:\Storage\Scripts\Windows\Modules\AVReport\AVReport.psm1:163 char:5
+                 $subkeys = $reg.OpenSubKey($UninstallKey).GetSubKeyNames()
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

PS F:\Storage\Scripts\Windows\Tools> Get-InstalledSoftware "adm701209"


ComputerName    : adm701209
DisplayName     : Adobe AIR
DisplayVersion  : 2.7.1.19610
InstallLocation : c:\Program Files\Common Files\Adobe AIR\
Publisher       : Adobe Systems Incorporated
InstallDate     :

<clip>

PS F:\Storage\Scripts\Windows\Tools> get-date

Tuesday, August 20, 2013 12:45:19 PM


PS F:\Storage\Scripts\Windows\Tools>

Got a workaround in place, but still no idea why it wasn’t working originally…

function OpenRemoteRegistryKey {
	Param(
		[Parameter(ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$ComputerName,
		[int]$Iteration = 1
	)
	
	$reg = $null
	try {
		#Create an instance of the Registry Object and open the HKLM base key
		$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$ComputerName)
	}
	catch [Exception] {
		if ($_.Exception.Message -like "*The network path was not found*")
		{
			if ($Iteration -ge 3) {
				Write-Error "Unable to start RemoteRegistry service on $ComputerName"
				$reg = $null
			}
			else {
				# Try to start the RemoteRegistry service on the remote machine
				# http://www.sqlservercentral.com/blogs/timradney/2011/07/18/starting-and-stopping-a-remote-service-with-powershell/
				$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StartService()
				$Iteration++
				switch ($response.ReturnValue) {
					# Service started successfully
					0 {
						Start-Sleep -Seconds 10
						OpenRemoteRegistryKey -ComputerName $ComputerName -Iteration $Iteration
					}
					# Service was already running
					10 {
						# Stop the RemoteRegistry service, wait 30 seconds, and retry
						$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StopService()
						Start-Sleep -Seconds 10
						OpenRemoteRegistryKey -ComputerName $ComputerName -Iteration $Iteration
					}
					default {
						Write-Error "Unable to start RemoteRegistry service on $ComputerName. ReturnValue: $($response.ReturnValue)"
						$reg = $null
					}
				}
			}
		}
		else {
			Write-Error "$ComputerName : $_"
			$reg = $null
		}
	}
	finally {
		$reg
	}
}

function Get-InstalledSoftware {
	[CmdletBinding()]
	Param (
		[Parameter(ValueFromPipelineByPropertyName=$true,Position=0,Mandatory=$true)][string[]]$ComputerName
	)

	BEGIN {
	}
	PROCESS {
		$installedApps = @()
		foreach($comp in $ComputerName){
			#Define the variable to hold the location of Currently Installed Programs
			$UninstallKey="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" 
			$reg = OpenRemoteRegistryKey -ComputerName $comp
			$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$ComputerName)
			if($reg) {
				#Drill down into the Uninstall key using the OpenSubKey Method and retrieve an array of strings that contain all the subkey names
				$subkeys = $reg.OpenSubKey($UninstallKey).GetSubKeyNames()
				#Open each Subkey and use GetValue Method to return the required values for each
				foreach($key in $subkeys){
					$thisSubKey = $reg.OpenSubKey($UninstallKey+"\\"+$key)
					$InstallDate = $($thisSubKey.GetValue("InstallDate"))
					if (($InstallDate -ne $null) -and ($InstallDate.Length -gt 0)) {
						$InstallDate = [datetime]::ParseExact($($thisSubKey.GetValue("InstallDate")), "yyyyMMdd", $null)
					}
					$obj = New-Object -TypeName PSObject -Property @{
						ComputerName=$comp
						DisplayName=$($thisSubKey.GetValue("DisplayName"))
						DisplayVersion=$($thisSubKey.GetValue("DisplayVersion"))
						InstallLocation=$($thisSubKey.GetValue("InstallLocation"))
						Publisher=$($thisSubKey.GetValue("Publisher"))
						InstallDate=$InstallDate
					}
					$installedApps += $obj
				}
			}			
		}
		$installedApps | Where-Object DisplayName | Select-Object ComputerName, DisplayName, DisplayVersion, InstallLocation, Publisher, InstallDate
	}
	END {
	}
}

Your OpenRemoteRegistryKey function has a bug. In both places where you’re calling the function recursively, you should assign the result to $reg; as written, you’re just throwing the result away (and returning $null as a result, even if the recursive calls were successful).

function OpenRemoteRegistryKey {
	Param(
		[Parameter(ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$ComputerName,
		[int]$Iteration = 1
	)
	
	$reg = $null
	try {
		#Create an instance of the Registry Object and open the HKLM base key
		$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$ComputerName)
	}
	catch [Exception] {
		if ($_.Exception.Message -like "*The network path was not found*")
		{
			if ($Iteration -ge 3) {
				Write-Error "Unable to start RemoteRegistry service on $ComputerName"
				$reg = $null
			}
			else {
				# Try to start the RemoteRegistry service on the remote machine
				# http://www.sqlservercentral.com/blogs/timradney/2011/07/18/starting-and-stopping-a-remote-service-with-powershell/
				$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StartService()
				$Iteration++
				switch ($response.ReturnValue) {
					# Service started successfully
					0 {
						Start-Sleep -Seconds 10
						$reg = OpenRemoteRegistryKey -ComputerName $ComputerName -Iteration $Iteration
					}
					# Service was already running
					10 {
						# Stop the RemoteRegistry service, wait 30 seconds, and retry
						$response = (Get-WmiObject -ComputerName $ComputerName -Class win32_service -filter "Name='RemoteRegistry'").StopService()
						Start-Sleep -Seconds 10
						$reg = OpenRemoteRegistryKey -ComputerName $ComputerName -Iteration $Iteration
					}
					default {
						Write-Error "Unable to start RemoteRegistry service on $ComputerName. ReturnValue: $($response.ReturnValue)"
						$reg = $null
					}
				}
			}
		}
		else {
			Write-Error "$ComputerName : $_"
			$reg = $null
		}
	}
	finally {
		$reg
	}
}

Actually, now that I think about it, my description of the bug probably wasn’t accurate. It wasn’t that you were throwing away the results of the recursive call (because PowerShell should still write those return values to the output stream), but that you were also writing $null to the output stream afterward (in your finally block, when the value of $reg hasn’t changed). So you probably were getting a collection back from OpenRemoteRegistryKey in some cases, and some elements of the collection were null.

Either way, assigning the results of those recursive calls to $reg should fix the problem (and ensure that your function only returns a single value, in all cases).

Thanks, Dave! That seems to work, but I’m still not sure I understand why. I’ll have to look at this one for a while… I appreciate the help, though!

Here’s a stripped-down version of your original code which will hopefully make the problem more clear:

function ExampleFunction
{
    $reg = $null
	
    try
    {
	    $reg = SomeMethodWhichMayThrowAnException
    }
    catch [Exception]
    {
        if ($_.Exception.Message -like "*The network path was not found*")
        {
            # If the method was successful the second time, it will return an object.
            # Because you're not assigning this object to a variable, it will be written to the
            # output stream (a common PowerShell "gotcha"; sometimes unexpected things show up
            # in function output because of this).

            ExampleFunction
        }
        else
        {
            # Not technically necessary; $reg is already $null, since you set it before the try block.
            $reg = $null
        }
    }
    finally
    {
        # This line will send the value of $reg to the output stream.  Because we didn't assign the value
        # of the recursive call to ExampleFunction to a variable, this may still be $null (even if the recursive
        # call was successful).

        $reg
    }
}

By assigning the value of ExampleFunction to $reg, you avoid both problems. In the finally block, it will contain an object if the function was successful (whether on the first try or in a recursive call), and will be $null otherwise. You also won’t be sending output down the pipeline from any unexpected places.

Yeah, I finally figured out how my logic was wrong; I was thinking of the recursion as a loop instead of as recursion. Basically, in my head, I was thinking of the flow like this:

  1. Enter OpenRemoteRegistryKey
  2. Try to set $reg and fail
  3. Restart RemoteRegistry service
  4. Dump everything in the call stack, close the OpenRemoteRegistryKey function scope and enter OpenRemoteRegistryKey again
But! The actual flow is more like this:
  1. Enter OpenRemoteRegistryKey
  2. Try to set $reg and fail
  3. Restart RemoteRegistry service
  4. Leave the call stack intact, leave the OpenRemoteRegistryKey function scope open, create a new OpenRemoteRegistryKey scope, and start over
  5. Repeat as necessary
  6. Return a response from the last scope opened and close that scope
  7. Finish execution in the parent scope and then return a response from that scope to its parent
  8. Rinse and repeat

When I followed the actual flow, I realized that $reg was being set before the recursive call and was never being set again! So now the assignment changes that you suggested actually make sense to me! With those change, what is essentially happening is that the deepest recursive call is setting $reg in that try block and then returning that value of $reg up the call stack to the original call.

Thanks for making me think, David!

No problem! If you wanted to, you could have implemented this without recursion (personal preference there. Sometimes one is better than the other, sometimes it just doesn’t matter). Here’s an example of what the code structure with a loop might look like:

function ExampleFunction
{
    $reg = $null
	
    # Assuming that you still want to limit this to 3 attempts:

    for ($i = 0; $i -lt 3; $i++)
    {
        try
        {
            $reg = SomeMethodWhichMayThrowAnException
            
            # This statement will not execute if the last line threw an Exception
            break
        }
        catch [Exception]
        {
            # Not technically necessary; $reg is already $null, since you set it before the try block.
            $reg = $null
        }
    }

    # Write whatever $reg's value is (might be null, if all attempts failed) to the output stream.

    $reg
}