powershelljobscomobject

"You cannot call a method on a null-valued expression." when trying to receive Result of update Searcher running in Background Job


I need to List all updates on Windows that are not installed and write them to a file. But if it takes too long there must be a timeout.

I tried to run a search with a Update Searcher Object as a Job and then continue either when it's completed or the timeout takes place. Then I check if the job has been completed or not. If it did I pass the job to Receive-Job to get its result and write it to the file.

$session = New-Object -ComObject "Microsoft.Update.Session"
$searcher = $session.CreateUpdateSearcher()

$j = Start-Job -ScriptBlock {
    $searcher.Search("IsInstalled=0").Updates | Select-Object Type, Title, IsHidden
} | Wait-Job -Timeout 120

if ($j.State -eq 'Completed') {
    Receive-Job $j -Keep | Out-File @out_options
} else {
    echo "[TIMEOUT] Script took too long to fetch all missing updates." |
        Out-File @out_options
}

@out_options are defined, if you should wonder.

The only thing I receive is following error:

You cannot call a method on a null-valued expression.
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull
    + PSComputerName        : localhost

By now I figured out that the error stems from calling Receive-Job. It seems the job is completing before there is a result. How do I receive Result from my background job?


Solution

  • You've run into a scope issue. The variable $searcher inside your scriptblock is in a different scope (and is thus a different variable) than the variable $searcher in the script scope. Because it wasn't initialized properly the variable in the scriptblock is empty, thus causing the error you observed (you're trying to invoke Search() on an empty value).

    The usual fix for this would be either using the using scope modifier

    $j = Start-Job -ScriptBlock {
        $using:searcher.Search("IsInstalled=0").Updates | ...
    } | ...

    or passing the variable as an argument

    $j = Start-Job -ScriptBlock {
        Param($s)
        $s.Search("IsInstalled=0").Updates | ...
    } -ArgumentList $searcher | ...

    However, in your case both approaches don't work, which to my knowledge is because of how COM objects are serialized/deserialized when being passed between different contexts.

    To avoid this pitfall create the Update session and searcher inside the scriptblock:

    $j = Start-Job -ScriptBlock {
        $session = New-Object -ComObject 'Microsoft.Update.Session'
        $searcher = $session.CreateUpdateSearcher()
        $searcher.Search('IsInstalled=0').Updates | Select-Object Type, Title, IsHidden
    } | Wait-Job -Timeout 120