1

I'm attempting to add a wallpaper, along with certain parameters, to each user on a computer. It's been hit and miss with this working/not working on computers. The ones that fail I get the error "Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'."

The variables $WallpaperPath and $Style are coming from another source within Automation Manager (using N-Central).

# Get each user profile SID and Path to the profile
$UserProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$" } | Select-Object @{Name="SID"; Expression={$_.PSChildName}}, @{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}}

# Add in the .DEFAULT User Profile
$DefaultProfile = "" | Select-Object SID, UserHive
$DefaultProfile.SID = ".DEFAULT"
$DefaultProfile.Userhive = "C:\Users\Public\NTuser.dat"
$UserProfiles += $DefaultProfile

# Loop through each profile on the machine</p>
Foreach ($UserProfile in $UserProfiles) {
    # Load User ntuser.dat if it's not already loaded
    If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) {
        Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE LOAD HKU\$($UserProfile.SID) $($UserProfile.UserHive)" -Wait -WindowStyle Hidden
    }

# Write to the registry
$key = "Registry::HKEY_USERS\$($UserProfile.SID)\Control Panel\Desktop"
Set-ItemProperty -Path $key -name Wallpaper -value "$WallpaperPath"
Set-ItemProperty -Path $key -name TileWallpaper -value "0" 
Set-ItemProperty -Path $key -name WallpaperStyle -value "$Style" -Force

# Unload NTuser.dat        
If ($ProfileWasLoaded -eq $false) {
    [gc]::Collect()
    Start-Sleep 1
    Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden| Out-Null
    }
}

I'm looking for this to load a temporary HKU hive for each user that's not currently logged in, and has an NTuser.dat file, and write the registry entries specified. It should then unload any hive for users it added.

Adam Kohne
  • 11
  • 2

1 Answers1

0

Instead of $UserProfiles = ..., use either [array] $UserProfiles = ... or $UserProfiles = @(...) in order to ensure that $UserProfiles always contains an array, even if the command happens to return just one object.

That way, your += operation is guaranteed to work as intended, namely to (loosely speaking) append an element to the array.[1]

Note that PowerShell's pipeline has no concept of an array, just a stream of objects. When such a stream is collected, a single object is captured as itself; only two or more objects are captured in an array ([object[]]) - see this answer for more information.

A simple demonstration:

2, 1 | ForEach-Object {
  $result = Get-ChildItem / | Select-Object Name -First $_
  try {
    $result += [pscustomobject] @{ Name = 'another name' }
    "`$result has $($result.Count) elements."
  } catch {
    Write-Warning "+= operation failed: $_"
  }
}

In the first iteration, 2 objects are returned, which are stored in an array. += is then used to "append" another element.

In the second iteration, only 1 object is returned and stored as such. Since [pscustomobject], which is the type of object returned by Select-Object, doesn't define a + operation (which would have to be implemented via an op_Addition() method at the .NET level), the error you saw occurs.

Using an [array] type constraint or @(...), the array-subexpression operator operator, avoids this problem:

2, 1 | ForEach-Object {
  # Note the use of @(...)
  # Alternatively:
  #   [array] $result = Get-ChildItem \ | Select-Object Name -First $_
  $result = @(Get-ChildItem / | Select-Object Name -First $_)
  $result += [pscustomobject] @{ Name = 'another name' }
  "`$result has $($result.Count) elements."
}

As noted, [array] $results = Get-ChildItem \ | Select-Object Name -First $_ works too, though there are subtle differences between the two approaches - see this answer.


As an aside:

  • To synchronously execute console applications or batch files and capture their output, call them directly (c:\path\to\some.exe ... or & $exePath ...), do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer. GitHub docs issue #6239 provides guidance on when use of Start-Process is and isn't appropriate.

  • That is, you can just make calls such as the following:

    REG.EXE LOAD "HKU\$($UserProfile.SID)" "$($UserProfile.UserHive)"
    
  • Also, it's easier and more efficient to construct [pscustomobject] instances with their literal syntax (v3+; see the conceptual about_PSCustomObject help topic):

      $UserProfiles += [pscustomobject] @{
        SID = ".DEFAULT"
        Userhive = "C:\Users\Public\NTuser.dat"
      }
    

[1] Technically, a new array must be created behind the scenes, given that arrays are fixed-size data structures. While += is convenient, it is therefore inefficient, which matters in loops - see this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you! I added [array] to the $UserProfiles like you suggested and it worked without giving that error. Another note, however, is it didn't run this against the currently logged in user. It ran it against HKU\Default and the users that were not currently logged in, however. – Adam Kohne Feb 09 '23 at 17:43
  • Glad to hear it, @AdamKohne; I've added some more information to the answer. What ran against `HKU\Default`? Anyway, this is an unrelated problem, right? – mklement0 Feb 09 '23 at 17:56
  • 1
    It is a separate issue. The reason it didn't grab the currently logged in user is due to the name of the profile, as it is an Azure profile that starts with "S-1-12-1" vs starting with "S-1-5-21". – Adam Kohne Feb 09 '23 at 19:21