Scripting Games 2013: Event 4 Notes

It is all downhill from here folks! Event 4 is in the books and we only have 2 more to go! Everyone has been doing an outstanding job with their submissions and it is becoming clear that people are learning new things and showing some great techniques with their code.

Of course, this doesn’t mean that there isn’t room for improvement with some submissions to make them even better or just some simple mistakes that can be cleaned up to make average submissions into amazing submissions. With that, its time to dive into my notes…

Send-MailMessage, not Net.Mail.MailMessage

I’ve seen several submissions where people have an optional email message that can be sent out. I had no issues with this and in fact applaud those that did! There is nothing wrong with added functionality to a script to make it better. What I did have issues with was those that chose the hard road of building out the email system using .NET instead of the readily available Send-Mailmessage cmdlet.

Compare these examples:

.NET Way

#SMTP server name
$smtpServer = $smtpServer
 
#Creating a Mail object
$msg = new-object Net.Mail.MailMessage
 
#Creating SMTP server object
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
 
#Email structure
$msg.From = $EmailFrom
$msg.ReplyTo = $EmailFrom
$msg.To.Add($EmailTo)
$msg.subject = $EmailSubject
$msg.attachments.add($Attachment)
$msg.body = $EmailBody
 
#Sending email
$smtp.Send($msg)

PowerShell Cmdlet

$email = @{
    To = $EmailTo
    From = $EmailFrom
    Subject = $EmailSubject
    Body = $EmailBody
    SMTPServer = $smtpServer
}
Send-MailMessage @Email

The proof is not only in the lines of code, but also in the readability of the code.

Again, don’t use .NET types unless absolutely necessary as it creates more work for you and really goes against the best practice of using PowerShell cmdlets in PowerShell scripts. Please try to force yourself away from this habit.

A better [ValidatePattern()] with [ValidateScript()]

I didn’t count this against anyone, but I have seen a lot of people using [ValidatePattern()] to do some regular expression (RegEx) matching. While it does work in preventing incorrect data from being supplied to a parameter, the error message is something that is just terrible for a user to try and interpret. Take this example:

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter()]
        [ValidatePattern("^(\d{1,3}\.){3}\d{1,3}$")]
        [string]$IPAddress
    )
    $IPAddress
}

If a wrong value is given…

image

The false IP fails the test, but unless you know RegEx, the error message pretty much means nothing at all. Enter [ValidateScript()] to make this better. The one thing to watch out for with this is that you provide more to the scriptblock than this:

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter()]
        [ValidateScript({($_ -match "^(\d{1,3}\.){3}\d{1,3}$")})]
        [string]$IPAddress
    )
    $IPAddress
}

image

While the idea is there, it still doesn’t help the user to know what is wrong. Instead, add and If/Else block to handle the matching and use Throw to put a more descriptive error message.

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter()]
        [ValidateScript({
            If ($_ -match "^(\d{1,3}\.){3}\d{1,3}$") {
                $True
            } Else {
                Throw "$_ is not a valid IPV4 address!"
            }
        })]
        [string]$IPAddress
    )
    $IPAddress
}

image

Now the error message has more meaning to it!

User the –Properties Parameter for Get-ADUser

As with Get-WMObject, you need to use the parameters available in the cmdlets to have the most efficient submission. If  you specify –Properties * and then try to pull data, you are bringing back more data than needed. Instead use –Properties and specify each property that you want.

LastLogon or LastLogonTimeStamp property?

This one would take some research to determine the best course of action. While either one will get you the last logon date required for the submission. You have to consider which is the best approach. You can use the LastLogonTimeTimestamp but under the knowledge that it is only replicated every 9-14 days. The other option is that you can use LastLogon instead which is not replicated to all Domain Controllers meaning that in order to accurately determine the accurate timestamp of the user account, you must query each of the domain controllers in the environment and take the highest value found. Some people did this, others took a different route. Here are my points on each one:

  1. LastLogonTimeStamp
    1. 6-14 days to replicate
    2. May not be the most accurate
    3. Best performance as only one Domain controller needs to be queried
    4. Ok if accuracy isn’t the top priority and timeline is small to get this done
  2. LastLogon
    1. Does not replicate at all
    2. Most accurate
    3. Requires querying every domain controller and finding the largest value
      1. Performance hit
    4. Ok if accuracy is important and time isn’t an issue

So what does this mean? Well, it really depends on the scenario. If someone did go for the lastlogon attribute, I hope that they knew to hit all of the domain controllers, otherwise the report will be suspect as the data will not be that accurate.

Use of Here-Strings for HTML code

I’ve seen more people using Here-Strings instead of concatenating strings when building out their html /CSS styles for use with ConvertTo-HTML. And for that I say Good Job! I still see some people trying to concatenate strings and hope that those habits change for the last 2 events (if applicable). I did see somebody use a stringbuilder class to construct the html code. Very interesting and I can’t say that I really have much of an opinion on it as it is something different and the person does use it beyond the initial creation of the styles.

All in all, great job with this event! I am looking forward to seeing what everyone brings to the table with Event 5!

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , , , | 2 Comments

Scripting Games 2013: Event 3 Notes

Wow, it is hard to believe that we are now halfway through the Scripting Games! As the events have progressed, I have seen a lot of improvement with the techniques as well as seeing new techniques that continue to impress me. On the flip side, I have seen some mistakes or assumptions when coding that cause a potential 5 star script to be a 2 or 3 star script. The best part about all of this is that we are all (yes, even the judges) learning new things that can only help to improve everyone’s scripting knowledge.

With that, it is time to take a look at the things that could be improved upon or should be avoided altogether.

-Filter vs. Where-Object

This was probably the biggest issue I have seen with the submissions for Event 3. Too many submissions were taking the output of Get-WMIObject and piping the results into Where-Object to get all of the local hard drives. The sooner you get used to working with –Filter, the better off you will be working with scripts and one liners to query WMI on both local and remote systems. When you use the Filter parameter, especially on remote systems, the filtering is performed on the remote system and only the objects that you filtered for will be passed back to the local system.

Some people claim that it is an ugly approach and I disagree. If I am looking for only hard drives, which one looks worse:

Filter

drivetype=3

Where-Object

$_.drivetype –eq 3

To me, the obvious answer is using Filter and supplying the WQL statement. Sure the statements may get complicated, but so could using the Where-Object. And if you might not be sure as to what type of WQL statement to use, simply look it up. There are plenty of resources available that you can use to figure out the proper query.

I’ve also heard that performance was the reason for using Where-Object locally vs. –Filter and that it isn’t much better to consider using the –Filter parameter. Lets compare performance just to see.

Filter

(Measure-Command {
    Get-WmiObject -Class Win32_LogicalDisk -Filter 'drivetype=3'
}).TotalMilliseconds

image

 

Where-Object

(Measure-Command {
    Get-WmiObject -Class Win32_LogicalDisk | Where {
        $_.driveType -eq 3
    }
}).TotalMilliseconds

image

About a 50 millisecond difference. Pretty good difference to me. Some might disagree, but the fact is that using –Filter has better performance. So why is that the case? Let’s take a closer look at each command. If you know PowerShell, you know that Where-Object takes the input from whatever command is before it in the pipeline and starts performing a comparison against the object in the pipeline based on the data you provide.

Get-WmiObject -Class Win32_LogicalDisk | Where {
    Write-Verbose $_ -Verbose
    $_.driveType -eq 3
}
 

image

As you can see, each object is being looked at when just using Where-Object. I wonder what happens when I use the –Filter parameter…

Get-WmiObject -Class Win32_LogicalDisk -Filter 'drivetype=3' | Where {
    Write-Verbose $_ -Verbose
}

image

Instead of going through 4 items, I only see the 3 that are actual hard drives. This is the efficiency of using a parameter on ANY cmdlet vs. using Where-Object.

So bottom line: –Filter is not only a best practice for Get-WMIObject, but it is also more efficient and provides better performance.

Hard Coded Paths

Something that was driving me crazy during this event, especially on the beginner side was the use of hard coded paths to output the html file. When you use a path such as C:\smith\awesomefolder as the directory, you cannot expect it to work for me. I shouldn’t have to modify your code to make it work for me. It should work without any issues. The solution for this would be to use something like $pwd, or also in some scripts I saw (good job, by the way!), $Env:TEMP  or $Env:USERPROFILE. There are other possibilities, but the main thing is that you must make sure that it is universal to the everyone who uses it.

.Net Types

This is the PowerShell scripting games, not the .Net games. Don’t use [DateTime]::now, when there is a perfectly good cmdlet called Get-Date that will accomplish the same thing. I even saw this: [environment]::MachineName being used. Really? You can get the same thing out of $Env:Computername.

Another person took the time to create their own network checker using System.Net.NetworkInformation.Ping. The same thing can be done by using Test-Connection.

Unless there is compelling need to use this (haven’t seen it yet) to use a .Net type to accomplish your goal, stick with the cmdlets and you will be better off.

Over Commenting

This is pretty self explanatory. We don’t need a 2-3 line comment talking about how you are querying WMI on a local system and then continuing this trend for each and every line of code. Not every line of code requires a comment, let along multiple lines of commenting. Keep it simple and clean and everything will be Ok.

Some Final Points

  • Consider use of [pscustomobject] vs. Net-Object PSObject for V3 code
  • Stop using back ticks as line breaks.
  • If the event says to do a one line, then you should shoot for a one liner and avoid using semicolons because it is no longer a one liner.
  • No more concatenating (Ex: “this” + $something + “is great!”). We are not in the vbscripting games.
  • Also avoid using += to append lines to an existing variable. Use a Here-String instead.

Keep up the great submissions and I am looking forward to seeing what you do with Event 4!

Posted in powershell, Scripting Games 2013 | Tagged , , , | 3 Comments

Scripting Games 2013: Event 2 Notes

I spent some time last week and this weekend to compile a list of notes of what I have seen with the Event 2 submissions that should show improvement. I touched up on some items with my previous article where I picked out some submissions that I liked and didn’t quite like but wanted to touch on a few more things. Some of this feels like a repeat of last week and even last years games, but that is Ok. This is all about learning and as long as everyone takes what all of the judges have been writing about, then there will be nothing but great improvements during the course of the games.

When is a one-liner not a one-liner?

The answer is when someone uses a semicolon “;” and assumes that counts as a one-liner. Sorry folks, but this doesn’t work. A one-liner is something that is a complete continuation of a string of commands. In PowerShell, a semicolon is a line break which means that the one-liner that was being put together has now became a multi-one-liner.

Here is what not to do:

$OS = Get-WMIObject -Class Win32_OperatingSystem ;$ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem; New-Object PSObject -Property @{Computername=$ComputerSystem.Name;OS=$Os.Caption}

It seems like a one-liner, but with the semicolons, it is nothing more than a one-liner lie. In fact, the only time here that the semicolon is legal is with the New-Object cmdlet that takes a hash table as input.

A one liner is not required for the Scripting Games in the beginner event. It is a nice to have, but it is also a risk because if you try to force it as such, it could come back to bite you when it comes to voting. My advice, split the code up on lines because not only will it prevent possible issues in the code, it is going to look tons better!

Formatting your code

Moving from one-liners to proper code formatting so it can easily be read by not only the voting community, but also applies to production scripts that may have to be read 6 months from by someone else or even the future you. Think to yourself, “Will I know exactly what is going on here in 6 months?”. If you cannot answer this question or answer No, then stop, go back and take the time to re-format the code so it is more readable.

Here are some bullet points to think about when writing code:

  • Space out your code; separate by code blocks so it is not just one line after another of code
  • Use the proper line continuation characters: commas“,”, opening square bracket “{“ and pipe symbol “|”; please for the love of everything good, do not use a backtick “`” as a line continuation. This is also a lie and must be stopped!
  • Indent your code where needed. Examples would be when you use a natural line break (first bullet point) or during a ForEach/Where block and If/Else block. Doing so will make it easier to read and organize your code.

Splatting instead of back ticks for parameters

Splatting is not only great for working with various cmdlets and making input a lot easier when in a loop, but it is also a smart idea to make your code easier to read and avoiding the use of back ticks when working with multiple parameters.

Send-MailMessage -To user@domain.com `
                 -From user1@domain.com `
                 -Subject Test `
                 -SMTPServer server@domain.com `
                 -Body "This is a test"

This is not a good way of handling the parameters in a script. Sure, it is better than having them on one line and navigating that mess, but this still doesn’t fit a good practice. This is where splatting comes into play.

$email = @{
    To = 'user@domain.com'
    From = 'user1@domain.com'
    Subject = 'Test'
    SMTPServer = 'server.domain.com'
}
...
#Some code
...
#Add the output into the body of the email
$email.Body = $outputText
Send-MailMessage @email

I can define most of my parameters for Send-MailMessage up front and once I have the data available for my Body, I can add that into my $email hash table before calling Send-MailMessage and supplying the hash table into the cmdlet (using “@” to splat) and the command will run just like usual.

Enough with $ErrorActionPreference

I have seen a lot of people using $ErrorActionPreference up front in their submissions as a catch all for error handling. Sure, some people are using Stop or Continue (this is already the default). This should never be touched at all in a script unless there is an excellent reason to do so (so far there hasn’t been a reason to do so). Each cmdlet has a common parameter called –ErrorAction that accepts Continue, Inquire, Stop and SilentlyContinue that you can use. Unless you need to suppress error messages for some reason, then I would recommend using Stop and put this command in a Try/Catch statement to handle any errors that come up.

Format* fail

The last thing that I want to go over is the use of Format* cmdlets as a final step to output data to the user. Bad, bad idea! What you are doing is ruining the object for the user in a way that they cannot manipulate the output either through Sort or Where-Object and cannot even output this to a file ( it can be outputted to a .txt file, but it will be just like what is on screen, truncated and all).

Get-Service | Format-Table | Sort Name

image

Doesn’t work out so good, does it? If only the Format-Table wasn’t there, then I could have sorted this out.

Maybe this will work for a CSV file instead?

Get-Service | Format-Table | Export-Csv -NoTypeInformation Report.csv

image

No errors, so it had to have worked correctly, right? Wrong!

image

If you can read this, then you are a better person than me. Smile The same goes with Format-List as well. Format* cmdlets should never be used in a script and should be left for a user to make the decision themselves. It will save you the hassle of being asked why the output stinks and also save you from losing points in a submission.

That’s it for now. Hopefully you can use these tips not only for the Scripting Games but also for “real world” scripts in your environment as well!

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , | 2 Comments

Scripting Games 2013: Event 2 ‘Favorite’ and ‘Not So Favorite’

Event 2 is in the books and with that, it is time to take a look at all of the scripts submitted and make the difficult decisions as to which ones I liked and which ones I didn’t quite like. Remember, just because a script landed on my ‘Not so Favorite’ list doesn’t mean it was terrible. It was just that I felt that there were some things here and there that could have been looked at a little differently.

So without further ado, lets dig into the submissions!

Advanced Category – Not So Favorite Submission

<#
.SYNOPSIS
    This script runs an inventory of OS, CPU cores and installed RAM on a list of computers
.DESCRIPTION
    .
.PARAMETER FilePath
    The path to the computer list text file
 
 
.EXAMPLE
        .\inventory.ps1 C:\Computernames.txt
.NOTES
    Author: Posh_London
    Date:   May 2013    
#>
 
# == Define params == #
 
param([Parameter(Mandatory=$true)][string]$filepath)
 
$output = @()
$Servers = gc $filepath
$Servers | % {
	$processor = gwmi win32_processor -computername $_
	$os = gwmi win32_operatingsystem -computername $_
	$m = gwmi win32_computersystem -computername $_
	$memGB = $m.totalphysicalmemory/1gb
	$memory = [math]::round($memGB)
	$osout = $os.caption
	$cores = $processor.numberofcores
	$servername = $m.name
	$output +="$ServerName   $osout    $cores CPU Cores       $memory GB RAM"
	$output +="`n"
 
	}
 
write-host $output -foregroundcolor "yellow"
  1. Line 1: Help text is lacking good examples and a Description
  2. Line 19: FilePath is not really a good choice for Parameters, especially when you might want to pass Computernames through to it. Computername would be the better choice in this case.
  3. Line 19: Lack of pipeline support for this really hurts the submissions ability to pass content from a text file through to the script.
  4. Really should be a function if in the Advanced category; makes it more of a usable tool
  5. Line 19: Instead of using Mandatory for the parameter, consider setting a default value such as $Env:Computername
  6. Line 22,23: Alias are bad news! Don’t use them
  7. Line 23-37: No object at all is being outputted at all! In fact, you can’t even output this to a file because Write-Host is being used or do things such as sorting or manipulating of the objects. Recommend that the user read up outputting objects using New-Object to create custom objects that can then be exported to files or manipulated.

Advanced Category – Favorite Submission

<#
.SYNOPSIS
   Get inventory data for specified computer system.
.DESCRIPTION
   Get inventory data for provided host using wmi.
   Data proccessing use multithreading and support using timeouts in case of wmi problems.
   Target computer system must be reacheble using ICMP Echo.
   Provide ComputerName specified by user and HostName used by OS. Also provide OS version, CPU and memory info.
.PARAMETER ComputerName
   Specifies the target computer for data query.
.PARAMETER ThrottleLimit
   Specifies the maximum number of WMI operations that can be executed simultaneously
.PARAMETER Timeout
   Specifies the maximum time in second command can run in background before terminating this thread.
.PARAMETER ShowProgress
   Show progress bar information
.EXAMPLE
   PS > Get-AssetInfo -ComputerName test1
 
   ComputerName : hp-test1
   OSCaption    : Microsoft Windows 8 Enterprise
   Memory       : 5,93 GB
   Cores        : 2
   Sockets      : 1
 
   Description
   -----------
   Query information ablout computer test1
.EXAMPLE
   PS > Get-AssetInfo -ComputerName test1 -Credential (get-credential) | fromat-list * -force
 
   ComputerName   : hp-test1
   OSCaption      : Microsoft Windows 8 Enterprise
   OSVersion      : 6.2.9200
   Cores          : 2
   OSServicePack  : 0
   Memory         : 5,93 GB
   Sockets        : 1
   PSComputerName : test1
   Description
   -----------
   Query information ablout computer test1 using alternate credentials
.EXAMPLE
   PS > get-content C:\complist.txt | Get-AssetInfo -ThrottleLimit 100 -Timeout 60 -ShowProgress
 
   Description
   -----------
   Query information about computers in file C:\complist.txt using 100 thread at time with 60 sec timeout and showing progressbar
.NOTES
   Required: Powershell 2.0
   Info: WMI prefered over CIM as there no speed advantage using cimsessions in multitheating against old systems.
#>
function Get-AssetInfo
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias('DNSHostName','PSComputerName')]
        [string[]]
        $ComputerName,
 
        [Parameter(Position=1)]
        [ValidateRange(1,65535)]
        [int32]
        $ThrottleLimit = 32,
 
        [Parameter(Position=2)]
        [ValidateRange(1,65535)]
        [int32]
        $Timeout = 120,
 
        [Parameter(Position=3)]
        [switch]
        $ShowProgress,
 
        [Parameter(Position=4)]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )
 
    Begin
    {
 
        Write-Verbose -Message 'Creating local hostname list'
        $IPAddresses = [net.dns]::GetHostAddresses($env:COMPUTERNAME) | Select-Object -ExpandProperty IpAddressToString
        $HostNames = $IPAddresses | ForEach-Object {
            try {
                [net.dns]::GetHostByAddress($_)
            } catch {
                # We do not care about errors here...
            }
        } | Select-Object -ExpandProperty HostName -Unique
        $LocalHost = @('', '.', 'localhost', $env:COMPUTERNAME, '::1', '127.0.0.1') + $IPAddresses + $HostNames
 
        Write-Verbose -Message 'Creating initial variables'
        $runspacetimers = [HashTable]::Synchronized(@{})
        $runspaces = New-Object -TypeName System.Collections.ArrayList
        $bgRunspaceCounter = 0
 
        Write-Verbose -Message 'Creating Initial Session State'
        $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        foreach ($ExternalVariable in ('runspacetimers', 'Credential', 'LocalHost'))
        {
            Write-Verbose -Message "Adding variable $ExternalVariable to initial session state"
            $iss.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $ExternalVariable, (Get-Variable -Name $ExternalVariable -ValueOnly), ''))
        }
 
        Write-Verbose -Message 'Creating runspace pool'
        $rp = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $ThrottleLimit, $iss, $Host)
        $rp.Open()
 
        Write-Verbose -Message 'Defining background runspaces scriptblock'
        $ScriptBlock = {
            [CmdletBinding()]
            Param
            (
                [Parameter(Position=0)]
                [string]
                $ComputerName,
 
                [Parameter(Position=1)]
                [int]
                $bgRunspaceID
            )
            $runspacetimers.$bgRunspaceID = Get-Date
 
            if (Test-Connection -ComputerName $ComputerName -Quiet -Count 1 -ErrorAction SilentlyContinue)
            {
                try
                {
                    Write-Verbose -Message "WMI Query: $ComputerName"
                    $WMIHast = @{
                        ComputerName = $ComputerName
                        ErrorAction = 'Stop'
                    }
                    if ($LocalHost -notcontains $ComputerName)
                    {
                        $WMIHast.Credential = $Credential
                    }
 
                    $WMICompSystem = Get-WmiObject @WMIHast -Class Win32_ComputerSystem
                    $WMIOS = Get-WmiObject @WMIHast -Class Win32_OperatingSystem
                    $WMIProc = Get-WmiObject @WMIHast -Class Win32_Processor
 
                    if (@($WMIProc)[0].NumberOfCores) #Modern OS
                    {
                        $Sockets = @($WMIProc).Count
                        $Cores = ($WMIProc | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
                    }
                    else #Legacy OS
                    {
                        $Sockets = @($WMIProc | Select-Object -Property SocketDesignation -Unique).Count
                        $Cores = @($WMIProc).Count
                    }
 
                    #region Create custom output object
                    #Due to some bug setting scriptblock directly as value can cause 'NullReferenceException' in v3 host
                    $MethodOptions = @{
                        Name = 'ToString'
                        MemberType = 'ScriptMethod'
                        PassThru = $true
                        Force = $true
                        Value = [ScriptBlock]::Create(@"
                            "{0:N1} {1}" -f @(
                                switch -Regex ([math]::Log(`$this,1024)) {
                                    ^0 {
                                        (`$this / 1), ' B'
                                    }
                                    ^1 {
                                        (`$this / 1KB), 'KB'
                                    }
                                    ^2 {
                                        (`$this / 1MB), 'MB'
                                    }
                                    ^3 {
                                        (`$this / 1GB), 'GB'
                                    }
                                    ^4 {
                                        (`$this / 1TB), 'TB'
                                    }
                                    default {
                                        (`$this / 1PB), 'PB'
                                    }
                                }
                            )
"@
                        )
                    }
 
                    $myObject = New-Object -TypeName PSObject -Property @{
                        'PSComputerName' = $ComputerName
                        'ComputerName' = $WMICompSystem.DNSHostName
                        'OSCaption' = $WMIOS.Caption
                        'OSServicePack' = $WMIOS.ServicePackMajorVersion
                        'OSVersion' = $WMIOS.Version
                        'Memory' = $WMICompSystem.TotalPhysicalMemory | Add-Member @MethodOptions
                        'Cores' = $Cores
                        'Sockets' = $Sockets
                    }
 
                    $myObject.PSObject.TypeNames.Insert(0,'My.Asset.Info')
                    $defaultProperties = @('ComputerName','OSCaption', 'Memory', 'Cores', 'Sockets')
                    $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultProperties)
                    $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
                    $myObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    #endregion
 
                    Write-Output -InputObject $myObject
                }
                catch
                {
                    Write-Warning -Message ('{0}: {1}' -f $ComputerName, $_.Exception.Message)
                }
            }
            else
            {
                Write-Warning -Message ("{0}: Unavailable" -f $ComputerName)
            }
        }
 
        function Get-Result
        {
            [CmdletBinding()]
            Param
            (
                [switch]$Wait
            )
            do
            {
                $More = $false
                foreach ($runspace in $runspaces)
                {
                    $StartTime = $runspacetimers.($runspace.ID)
                    if ($runspace.Handle.isCompleted)
                    {
                        Write-Verbose -Message ('Thread done for {0}' -f $runspace.IObject)
                        $runspace.PowerShell.EndInvoke($runspace.Handle)
                        $runspace.PowerShell.Dispose()
                        $runspace.PowerShell = $null
                        $runspace.Handle = $null
                    }
                    elseif ($runspace.Handle -ne $null)
                    {
                        $More = $true
                    }
                    if ($Timeout -and $StartTime)
                    {
                        if ((New-TimeSpan -Start $StartTime).TotalSeconds -ge $Timeout -and $runspace.PowerShell)
                        {
                            Write-Warning -Message ('Timeout {0}' -f $runspace.IObject)
                            $runspace.PowerShell.Dispose()
                            $runspace.PowerShell = $null
                            $runspace.Handle = $null
                        }
                    }
                }
                if ($More -and $PSBoundParameters['Wait'])
                {
                    Start-Sleep -Milliseconds 100
                }
                foreach ($threat in $runspaces.Clone())
                {
                    if ( -not $threat.handle)
                    {
                        Write-Verbose -Message ('Removing {0} from runspaces' -f $threat.IObject)
                        $runspaces.Remove($threat)
                    }
                }
                if ($ShowProgress)
                {
                    $ProgressSplatting = @{
                        Activity = 'Getting asset info'
                        Status = '{0} of {1} total threads done' -f ($bgRunspaceCounter - $runspaces.Count), $bgRunspaceCounter
                        PercentComplete = ($bgRunspaceCounter - $runspaces.Count) / $bgRunspaceCounter * 100
                    }
                    Write-Progress @ProgressSplatting
                }
            }
            while ($More -and $PSBoundParameters['Wait'])
        }
    }
    Process
    {
        foreach ($Computer in $ComputerName)
        {
            $bgRunspaceCounter++
            $psCMD = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddParameter('bgRunspaceID',$bgRunspaceCounter).AddParameter('ComputerName',$Computer)
            $psCMD.RunspacePool = $rp
 
            Write-Verbose -Message ('Starting {0}' -f $Computer)
            [void]$runspaces.Add(@{
                Handle = $psCMD.BeginInvoke()
                PowerShell = $psCMD
                IObject = $Computer
                ID = $bgRunspaceCounter
           })
           Get-Result
        }
    }
 
    End
    {
        Get-Result -Wait
        if ($ShowProgress)
        {
            Write-Progress -Activity 'Getting asset info' -Status 'Done' -Completed
        }
        Write-Verbose -Message "Closing runspace pool"
        $rp.Close()
        $rp.Dispose()
    }
}
  1. Excellent use of help. Shows multiple examples of using this function and clearly defines parameters
  2. Line 58-69: Great use of parameter attributes to handle various situations. I like the use of [Alias()] with the ByPropertyName attribute for the pipeline. This allows you to pipe a call from Get-ADComputer directly into this function and it will chug right along to perform the query.
  3. Line 82,83: Great use of handling the Credential parameter. By doing it this way, you have a few ways to handle the Credential input (domain\username, username, pscredential object or just use the –Credential param and allow it to prompt for input.
  4. Use of  [net.dns]::GetHostByAddress() was unnecessary as the WMI call would return a usable hostname for the object output.
  5. I will give kudos to the use of custom runspaces to make a more efficient query. Great job at handling the runspaces and the implementation of a timer to handle runspaces that might be hung up.
  6. Handling of the credential against local system issue was nicely done to avoid issues.
  7. Use of splat tables to pass into cmdlet is great.
  8. Line 206-210: Smart use of enhancing the output of the object. Haven’t seen most of this done before.

 

Beginner Category – Not So Favorite Submission

#Pulls IPs from txt file.  Gets Machine Name, Number of Physical CPUs, Number of cores per CPU, RAM in GBs, and Windows OS install
ipcsv C:\ScriptingGames\Event2\IPList.txt | gwmi Win32_ComputerSystem | 
    Format-List Name,NumberOfProcessors, NumberOfLogicalProcessors, 
    @{Name="RAM"; Expression={[math]::round($($_.TotalPhysicalMemory/1GB), 2)}} ;
    gwmi win32_OperatingSystem |Format-list name

Code formatted so it can be viewed better

  1. This isn’t a CSV file, so I am unsure as to why Import-Csv is being used for the list of IPs.
  2. Get-WMIObject doesn’t support pipeline input at all. Check Get-Help Get-Wmiobject –Parameter Computername. You can also check out my article on using Trace-Command to troubleshoot pipeline issues here. This causes the line to fail instantly. Instead, use a ForEach loop and use the –Computername parameter with the current item in the loop.
  3. Format-List is a no-no. Once you use a Format* cmdlet, you have lost all ability to use this object for anything such as sorting, outputting to a file, etc… See this article for more information.
  4. A better approach would have been to save the output of each WMI call to variables and then use New-Object to output a custom object that lists all of the information as well as allowing the use to pipe the object to another cmdlet or export to a file.
  5. The second pipe to Get-WMIObject will fail pretty much like the first; again this should have been used with the –Computername parameter and saved to a variable for future object outputting.

 

Beginner Category – Favorite Submission

Get-Content -LiteralPath 'C:\IPLIST.txt' | ForEach-Object {
    (Get-WmiObject -Class "Win32_ComputerSystem" -ComputerName $_ | 
    Select-Object -Property Name,@{n="Total Memory (GB)";e={$_.TotalPhysicalMemory / 1GB}},
    @{n="Cores";e={$_.NumberOfLogicalProcessors}},@{n="Processors";e={$_.NumberOfProcessors}} | 
    Add-Member -Name "OS Version" -Value $(Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_ | 
    Select-Object -ExpandProperty Caption) -MemberType NoteProperty -PassThru)
}

Code formatted so it can be viewed better

  1. Great use of a one-liner approach to solve this issue
  2. Like the use of ForEach-Object to iterate through each of the items in the text file.
  3. Custom objects with the Select-Object @{n=’name’;e={$_}} allows for object output that can be sorted, exported to a file, etc…
  4. Clever use of Add-Member to handle the Caption property in the current object

 

That’s it for Event 2! I will say that it was very tough deciding on which submissions I wanted as ‘Favorite’ as there were some great submissions, which meant that I really had to be picky about what I liked. Same for the ‘Not So Favorites’, it is never easy to find one that I just don’t like as much as another, but it goes with the territory.

I always appreciate feedback on how you think I did with picking the submissions. Disagree with my decisions? Let me know! I am just one person out of many and as we all know, not everyone has the same opinions on what they think is a better or not so good solution. As long as we can provide a learning experience, then we all win in this competition!

Posted in 2013 Scripting Games Judges Notes, powershell, Scripting Games 2013 | Tagged , , | 3 Comments