1

I am trying to pass a hashtable of calculated properties in to a query, for use with Select-Object. It works when run in the console. I can confirm that the job is reading the hashtable as it lists the selected properties in the result, yet all of their values are null.

Note: I understand that I do not need to type cast these properties. I just demonstrating my issue.

If I run the following code (it looks weird, but there's actually a use case for this) the output contains my selected properties (from $globalConfig.SystemState.Processors.SelectProperties) but the calculated properties have a value of null, the only property that returns the correct value is name:

$globalConfig = @{
    PingAddress = '8.8.8.8';
    SystemState = @{
        Processors = @{
            Namespace = 'root\cimv2';
            ClassName = 'Win32_Processor';
            SelectProperties = 'name', @{ n = 'CpuStatus'; e = { [int]$_.CpuStatus }}, @{ n = 'CurrentVoltage'; e = { [int]$_.CurrentVoltage }};
        }
    }
}

$job = Start-Job -Name Processors -ArgumentList $globalConfig.SystemState.Processors -ScriptBlock {
    Try{
        $Response = @{
            State   = @();
            Error   = $Null
        }
        $Response.State = Get-CimInstance -ClassName $Args[0].ClassName | Select-Object $Args[0].SelectProperties -ErrorAction Stop
    }Catch{
        $Response.Error = @{Id = 2; Message = "$($Args[0].Target) query failed: $($_.Exception.Message)"}
    }

    Return $Response
}

$job | Wait-Job
$job | Receive-Job | ConvertTo-Json -Depth 3

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
119    Processors      BackgroundJob   Completed     True            localhost            ...

{
    "Error":  null,
    "State":  {
                  "name":  "Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz",
                  "CpuStatus":  null,
                  "CurrentVoltage":  null
              }
}

Yet if I run the same job but with the same calculated properties hard coded (not passed to Select-Object using a PSObject), it works as expected (the values 1 and 12 are returned in the output):

$job = Start-Job -Name Processors -ArgumentList $globalConfig.SystemState.Processors -ScriptBlock {
    Try{
        $Response = @{
            State   = @();
            Error   = $Null
        }
        $Response.State = Get-CimInstance -ClassName $Args[0].ClassName | Select-Object Name, @{ n = 'CpuStatus'; e = { [int]$_.CpuStatus }},@{ n = 'CurrentVoltage'; e = { [int]$_.CurrentVoltage }}
    }Catch{
        $Response.Error = @{Id = 2; Message = "$($Args[0].Target) query failed: $($_.Exception.Message)"}
    }

    Return $Response
}

$job | Wait-Job
$job | Receive-Job | ConvertTo-Json -Depth 3

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
121    Processors      BackgroundJob   Completed     True            localhost            ...

{
    "Error":  null,
    "State":  {
                  "Name":  "Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz",
                  "CpuStatus":  1,
                  "CurrentVoltage":  12
              }
}

How can I pass an object of calculated properties in-line to Select-Object while inside of a job?

Arbiter
  • 450
  • 5
  • 26

2 Answers2

2

It's a hashtable, not a psobject. It looks like you can't pass scriptblocks into jobs. They get turned into strings.

$globalConfig = @{
  PingAddress = '8.8.8.8'
  SystemState = @{
    Processors = @{
      Namespace = 'root\cimv2'
      ClassName = 'Win32_Processor'
      SelectProperties = 'name', 
        @{ n = 'CpuStatus'; e = { [int]$_.CpuStatus }},
        @{ n = 'CurrentVoltage'; e = { [int]$_.CurrentVoltage }}
    }
  }
}

start-job -args $globalconfig.systemstate.processors {
  $list = $args[0].selectproperties
  $list[1].e.gettype()
  $list[2].e.gettype()
} | receive-job -wait -auto



IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
True     True     String                                   System.Object

It works with threadjobs. Threadjobs don't serialize their objects. In ps 5 you can download it from the powershell gallery. https://www.powershellgallery.com/packages/ThreadJob In ps 6 & 7, it comes with it. It doesn't make a new process, and is faster anyway. (It doesn't have the "-args" alias.)

start-threadjob -argumentlist $globalconfig.systemstate.processors {
  $list = $args[0].selectproperties
  $list[1].e.gettype()
  $list[2].e.gettype()
} | receive-job -wait -auto


IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     ScriptBlock                              System.Object
True     True     ScriptBlock                              System.Object

Or use the static method [scriptblock]::create() like here: How do I pass a scriptblock as one of the parameters in start-job

js2010
  • 23,033
  • 6
  • 64
  • 66
  • Thanks for this, you put me on the right track. I've added a workaround to this limitation as an answer and will post any new findings back here. The workaround requires enabling WinRM which I cannot do in my production environment. +1 – Arbiter Mar 21 '20 at 11:54
0

I wouldn't have found my answer without @js2010 pointing me in the right direction, thank you!

He/She is correct - for security reasons you cannot pass a scriptblock to a job, but as always with PowerShell there is a workaround.

I can get this to work by switching from Start-Job to Invoke-Command -asjob, but this requires WinRM to be runnning and configured, which isn't an option in production.

$globalConfig = @{
    PingAddress = '8.8.8.8';
    SystemState = @{
        Processors = @{
            Namespace = 'root\cimv2';
            ClassName = 'Win32_Processor';
            SelectProperties = 'Name',  @{ n = 'CpuStatus'; e = { [int]$_.CpuStatus }};
        }
    }
}

$ScriptBlock = {

    param(
        [Parameter(Mandatory=$True, Position=1)]
        [hashtable]$hashtable
    )

    Try{
        $Response = @{
            State = @(); 
            Error = $Null
        }
        $Response.State = Get-CimInstance -ClassName $hashtable.ClassName | Select-Object -Property $hashtable.SelectProperties -ErrorAction Stop
    }Catch{
        $Response.Error = @{Id = 2; Message = "$($data.Target) query failed: $($_.Exception.Message)"}
    }

    Return $Response
}

Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $globalConfig.SystemState.Processors -ComputerName . -AsJob


Name                           Value
----                           -----
Error
State                          @{Name=Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz; CpuStatus=1}

This must be possible... so I'll keep testing solutions and will post anything I find here.

Update

I have this working, its a little dirty, but it meets the needs of the project. I simply construct the command using the hashtable outside of the job (as a string), pass the command string to the job as an argument and then run the command inside the job using Invoke-Expression:

$globalConfig = @{
    PingAddress = '8.8.8.8';
    SystemState = @{
        Processors = @{
            Target = 'Processors'
            Namespace = 'root\cimv2';
            ClassName = 'Win32_Processor';
            SelectProperties = [string]('"Name", @{ n = "CpuStatus"; e = { [int]$_.CpuStatus }}');
        }
    }
}

$Args = [PSCustomObject]@{
    Target = $globalConfig.SystemState.Processors.Target;
    Command = "Get-CimInstance -ClassName $($globalConfig.SystemState.Processors.ClassName) -ErrorAction Stop | Select-Object $($globalConfig.SystemState.Processors.SelectProperties) -ErrorAction Stop";
}

$job = Start-Job -Name Processors -ArgumentList $Args -ScriptBlock {

    Try{
        $Response = @{
            State = @(); 
            Error = $Null
        }
        $Response.State = Invoke-Expression -Command $args.Command
    }Catch{
        $Response.Error = @{Id = 2; Message = "$($Args.Target) query failed: $($_.Exception.Message)"}
    }

    Return $Response
}

$job | Wait-Job
$job | Receive-Job | ConvertTo-Json -Depth 3


{
    "Error":  null,
    "State":  {
                  "Name":  "Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz",
                  "CpuStatus":  1
              },
    "PSComputerName":  "localhost",
    "RunspaceId":  "89f17de1-98b6-4746-a0ba-3e7c47294c61",
    "PSShowComputerName":  false
}
Arbiter
  • 450
  • 5
  • 26