Blank entry in hash table or somewhere?

This topic contains 7 replies, has 4 voices, and was last updated by  Stephen Owen 3 years, 8 months ago.

  • Author
    Posts
  • #13858

    www.b0n3z.com
    Participant

    Hello Don and the greats of PowerShell.

    I'm trying to figure out what is wrong with one of my first full PowerShell functions. I want to gather several WMI properties from remote workstations. As usual, most workstations in an environment are never perfect 100%. So with this we may have some workstations which we can ping. However due to services being disabled or rights, we get the dreaded RPC error.

    What is strange about my function is that before each RPC error – it adds a space or entry before. I can see this if I output only to the shell or Export-Csv. I'm trying to stop this behavior so I don't have gaps (rows) within my Csv.

    Thanks for all your help and what you do for the community. It really makes a difference!

    -Ken


    Function Test-WMI {
    < # .SYNOPSIS Gather WMI from remote workstation(s) .DESCRIPTION This function can accept workstation(s) directly on the command or via pipeline. It will then ping (Test-Connection) an gather WMI data. This WMI data will be temporary put into a hash table and output to an object. This can also be piped to Export-Csv. .EXAMPLE Test-WMI ZW650DTS Description ----------- Particular workstation times out. This workstations breaks the script, but works fine normally. .EXAMPLE Test-WMI -ComputerName ZW650DTS, DW65521H, WIN7ENTX86 -Verbose Description ----------- Multiple ComputerName .EXAMPLE Get-Content Computers.txt | Test-WMI -Verbose Description ----------- Using pipeline from file .EXAMPLE Test-WMI -Host DW65521H, WIN7ENTX86 Description ----------- Using alias .EXAMPLE Get-ADComputer -Filter {Name -like "DW6500*"} | Select-Object @{Name='ComputerName';Expression={$_.name}} | Test-WMI -Verbose | Export-Csv -Path .\Output.csv -Encoding ASCII -NoTypeInformation Description ----------- Using pipeline with property name change #>
    [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='Low')]
    Param (
    [Parameter(Mandatory=$False,
    ValueFromPipeline=$True,
    ValueFromPipelineByPropertyName=$True)]
    [Alias('HostName','Workstation')]
    [ValidateLength(5,20)]
    [string[]]$ComputerName = $env:COMPUTERNAME
    )
    BEGIN {
    }
    PROCESS {
    foreach ($Computer in $ComputerName) {
    Write-Verbose "$Computer gathering data"
    $Properties = @{'ComputerName'=$Computer}
    If (Test-Connection -ComputerName $Computer -Count 1 -Quiet -BufferSize 16) {
    $Properties.Add('PS_Ping',"Passed")

    $Win32_OperatingSystem = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computer -ErrorVariable myError -ErrorAction SilentlyContinue
    $Win32_ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -ErrorVariable myError -ErrorAction SilentlyContinue
    $Win32_NetworkAdapterConfiguration = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer -ErrorVariable myError -ErrorAction SilentlyContinue `
    | Where-Object {$_.IPEnabled -eq "True"}

    If ($myError -ne $null) {
    Write-Verbose $myError.Exception.Message | Out-String
    $Properties.Add('WMI_Connection',$myError.Exception.Message)
    }
    Else {
    $Properties.Add('WMI_Connection',"Passed")
    $Properties.Add('WMI_OS',$Win32_OperatingSystem.Caption)
    $Properties.Add('WMI_ServicePack',$Win32_OperatingSystem.CSDVersion)
    $Properties.Add('WMI_Manufacturer',$Win32_ComputerSystem.Manufacturer)
    $Properties.Add('WMI_Model',$Win32_ComputerSystem.Model)
    $Properties.Add('WMI_UserName',$Win32_ComputerSystem.UserName) #null for RDP sessions
    $Properties.Add('WMI_IPAddress',$Win32_NetworkAdapterConfiguration.IPAddress[0])
    }
    }
    Else {
    Write-Verbose "$Computer failed Test-Connection"
    $Properties.Add('PS_Ping',"Failed")
    }
    $Obj = New-Object -TypeName PSObject -Property $Properties | Select-Object -Property `
    ComputerName,
    PS_Ping,
    WMI_Connection,
    WMI_OS,
    WMI_ServicePack,
    WMI_Manufacturer,
    WMI_Model,
    WMI_UserName,
    WMI_IPAddress
    Write-Output $Obj
    }
    }
    END {
    }
    }

  • #13859

    Don Jones
    Keymaster

    You should really look into the Try/Catch construct for error handling; what you're doing is nonstandard and a bit harder to follow.

    But you're always going to be outputting an object with a full set of properties, because that's what your Select-Object is selecting. With a failed computer, you're asking it to select properties that don't exist (WMI_OS and the rest), and so it creates empty properties with those names.

    That's not what you want? Or am I not understanding the question?

  • #13860

    Dave Wyatt
    Moderator

    Some of the Win32 exception messages have newlines before the text, for some reason. If you just call Trim() on the Exception.Message property, it'll strip those out. As Don mentioned, using ErrorVariable in this way is a bit odd; try/catch and -ErrorAction Stop would be more appropriate in this situation. Something like this:

                    $Properties.Add('PS_Ping',"Passed")
    
                    try
                    {
                        $Win32_OperatingSystem = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computer -ErrorAction Stop
                        $Win32_ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -ErrorAction Stop
                        $Win32_NetworkAdapterConfiguration = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer -Filter 'IPEnabled=True' -ErrorAction Stop
                    
                        $Properties.Add('WMI_Connection',"Passed")
                        $Properties.Add('WMI_OS',$Win32_OperatingSystem.Caption)
                        $Properties.Add('WMI_ServicePack',$Win32_OperatingSystem.CSDVersion)
                        $Properties.Add('WMI_Manufacturer',$Win32_ComputerSystem.Manufacturer)
                        $Properties.Add('WMI_Model',$Win32_ComputerSystem.Model)
                        $Properties.Add('WMI_UserName',$Win32_ComputerSystem.UserName) #null for RDP sessions
                        $Properties.Add('WMI_IPAddress',$Win32_NetworkAdapterConfiguration.IPAddress[0])
                    }
                    catch
                    {
                        $message = $_.Exception.Message.Trim()
    
                        Write-Verbose $message
                        $Properties.Add('WMI_Connection', $message)
                    }
    
  • #13861

    www.b0n3z.com
    Participant

    Thanks Don for your help.

    I originally tried to use a Try/Catch, but it seems rather difficult when I would have to do a nested Try/Catch. One for Test-Connection and another for the (3) Get-WMIObject. Maybe I can learn from an example when Try needs to test multiple lines.

    That aside, I don't mind having $null or blank values for the individual properties (for example WMI_OS). Those don't seem to output an entire blank row. I have attached an image to show the highlighted (in yellow) blank rows that are produced above when an error is trapped from "The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)".

    -Ken

  • #13863

    www.b0n3z.com
    Participant

    Thanks Dave – I'll give it a shot!

    -Ken

  • #13864

    www.b0n3z.com
    Participant

    That was it! Thank you Don & Dave. I have been struggling with this for a few hours and getting frustrated. I also learned how to better use Try/Catch.

    -Ken

  • #13865

    Don Jones
    Keymaster

    BTW, you don't need a Try/Catch for Test-Connection. If a ping fails, it won't error when you're using -Quiet, it'll just return $False. You use Try/Catch when you're expecting an error.

  • #13866

    Stephen Owen
    Participant

    If you want to try catch over multiple commands, try this. We're using the Invoke character to run a number of commands through a single try/catch block because frankly we can't be bothered to type that much!

    
    #Invoke-Expression errors are considered non terminating, which means Try/Catch won't natively work.  You need to set
    # errorActionPreference = 'Stop' in order to force this behavior, to allow try/catch to do its job
    $ErrorActionPreference = 'Stop'
    $com =@"
    Get-ChildItem c:\drivers,
    Get-ChildItem hklo:,
    Get-ChildItem c:\temp
    "@
    
    $com.Split(',') | ForEach-Object {
    
        try {
            Invoke-Expression $_ -ErrorAction Stop
            }
    
        catch 
            {
            "Something went wrong"
            }
            
        }
    
    

    First off, we set $ErrorActionPreference = 'Stop', as Invoke-Expression errors are considered non-terminating for the interests of robust script execution. This means that if an error is encountered, it will be written out to screen and the show will attempt to go on, the script will continue running, etc. A terminating error, by comparison, will stop the show and display its output to the screen. In order to coerce PowerShell into exploring our Catch scripting block, we need to set the $ErrorActionPreference to Stop, so PowerShell will treat this seriously. If you want to see this principle in action, try running the above from the console with $ErrorActionPreference = "Continue". You'll notice that the error is displayed on screen and the Catch is never initiated! For this reason, we're setting the value to 'Stop'. Don't worry though, if you run this in a script, the default ErrorActionPreference setting of Continue will be reinstated after the script runs. Speaking of which, be careful in the ISE, as variables set there tend to stick through your whole session.

    So, $com contains all of our commands we want to run. We're splitting the array at the comma to get a number of commands, then ForEach invoking the contents of the variable (ForEach stores the current object in a group of objects within the $_ special variable). IF there is an error, we're catching it. This is not ideal, as sometimes we want to only catch certain types of errors. You could create special catch commands for each type of error, as shown.

    
    
    $com.Split(',') | ForEach-Object {
    
        try {
            Invoke-Expression $_ -ErrorAction Stop
            }
    
      
        catch [System.Management.Automation.DriveNotFoundException]
            {"Double check that drive letter..."
            }
      
        catch {
            "Something else went wrong"
            }      
        }
    

    In this case, we specifically catch for errors of the type 'System.Management.Automation.DriveNotFoundException', and then redirect all other output to a generic catch block.

    If you'd like more information about catching specific errors, check Ed's post here : http://blogs.technet.com/b/heyscriptingguy/archive/2010/03/11/hey-scripting-guy-march-11-2010.aspx

You must be logged in to reply to this topic.