PowerShell Patch Utility PoshPAIG Updated to 2.1.5

I have just released version 2.1.5 of PoshPAIG, my patching utility, to the public. It is available for download at http://poshpaig.codeplex.com.

image

I spent quite a bit of time ripping out the PSJobs infrastructure and replacing it with background PowerShell runspaces to speed up the UI as well as replacing the data table object used to update the Listview with an Observable Collection which will make scaling out new additions to the UI easier.

Other items added were some extra reporting for installed patches as well as some small UI changes with the Listview to include a different font color for the headers and the use of alternating colors for each row in the Listview.

Other items worked on were fixing some code with how the CSV reports are written by the vbscript after installing updates. It was using a comma delimited which caused some chaos with titles that had commas in it.

Other than that, no other major additions are included with this release as I want to stomp out any bugs reported with this version. It still behaves exactly like the previous version so no curve balls are being thrown.

Once I feel that this is stable enough, I will begin work on version 2.2 and some newer features that have been asked for. Smile

Go ahead and check it out and let me know what you think!

Posted in GUI, powershell, scripts, WPF | Tagged , , , | 3 Comments

Building an Xbox Live Widget using PowerShell

Something a little fun that I had the opportunity to work on was building a Xbox Live widget using PowerShell that does a nice job of capturing all of the relevant information that a Xbox live person would like to see along such as online friends, gamer score and the most recent activity.

elgatorabioso

What got me started down this road was a few blog posts from fellow PowerSheller Jonathan Tyler (Twitter | Blog) that described how he found an API for performing queries to gather information on Xbox Live.

The API site is: https://xboxapi.com/

It is also important to note that this is an unofficial API and the current API limit is 250 calls per hour. If you go over that amount by too great a deal that you IP will get blocked automatically for Spam and you will need to email the owner of the site to request it to be unblocked (as I found out while building this widget). If the API site goes down or is unavailable for whatever reason, this this widget will fail. I do know of another API site, but some of the features available on this widget do not line up properly with that site.

Just supply a gamertag to the script via the –Gamertag parameter. In this case, I will look at Major Nelson’s information.

.\xboxlivewidget.ps1 -Gamertag "Major Nelson"

image

You can access the context menu be right clicking anywhere on the widget and either select Exit to close the widget out or viewing Online Friends.

image

Once you select Show Online Friends, a new window opens up displaying all of the online friends. Even better is that this is an auto-updating window, so as the main window updates the number of friends, this window will update as well!

image

I am still working on a better way to refresh the online friends window to just remove the offline friends instead of clearing the window and re-building the list. That will be hopefully ready for the next release.

The basics of this is using Invoke-WebRequest (PowerShell V3 only) to query the API while providing the Gamertag in the url request, using my gamertag: El Gato Rabioso.

$tempGamertag = "El Gato Rabioso"
Add-Type System.Web
$Gamertag = [System.Web.HttpUtility]::UrlEncode($Gamertag.Trim())($tempGamertag.Trim())
[xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/profile/{0}" -f $Gamertag) | Select-Object -ExpandProperty Content)

To quickly break this down, I am first supplying a Gamertag that will be used in the request to get the information I need. But, before I can do that, I need to take care of those spaces in the name, otherwise the request will fail miserably. To handle the proper encoding I will enlist the help of [System.Web.HttpUtility] with the UrlEncode method to take those spaces and replace them with a “+” sign instead. If there were other special characters in the name, they would be properly translated as well into their respected character.

With that out of the way, I then perform the request to the API site and download the content which I cast to XML using [xml] so I now have a nice structured object to work with and select only what I need from it.

But this isn’t really about using the API from the command line, if you want more information about that, I highly suggest you check out Jonathan’s articles and play with the APIs yourself. Smile

The Code

I won’t go over each and every line of code as it is fairly large and somewhat redundant in areas. Instead, I will go over the parts of the code that I feel is interesting enough to talk about.

Param (
    $Gamertag = "Major Nelson"
)
#region Gamertag Encoding
Add-Type -AssemblyName System.Web
#Encode the gamertag so it works in the API call
$updatedGamertag = [System.Web.HttpUtility]::UrlEncode($Gamertag.Trim())
#endregion Gamertag Encoding
#region Build Synchronized Collections
$xboxLive = [hashtable]::Synchronized(@{})
$ui = [hashtable]::Synchronized(@{})
$childui = [hashtable]::Synchronized(@{})
$runspace = [hashtable]::Synchronized(@{})
#endregion Build Synchronized Collections
#region Default Settings and Items for Collections
$xboxLive.Gamertag = $updatedGamertag
$xboxLive.apiLimit = 250
$childui.onlineFriendsActiveWindow = $False
$runspace.Flag = $True
$runspace.errors = $Error
$xboxLive.isAtAPILimit = $False
#endregion Default Settings and Items for Collections
#region Main Runspace

Here I set up the parameter to accept a Gamertag and then handle the encoding to make it more http request friendly as well as setting up the synchronized collections to handle the data that is being shared between each runspace thread. I also set up some settings such as the API limit and whether the child window for Online Friends is active.

        #Internal function to update child UI  
        Function New-GridItem {
            [cmdletbinding(DefaultParameterSetName='Body')]
            Param (
                [parameter(ParameterSetName='Object')]
                $InputObject
            )

            #region Build Parent Grid
            $grid = New-Object Windows.Controls.Grid
            $grid.Width = 250
            $grid.tag = $InputObject.Gamertag
            $column1 = New-Object Windows.Controls.ColumnDefinition
            $column1.Width = "Auto"
            $column2 = New-Object Windows.Controls.ColumnDefinition
            $grid.ColumnDefinitions.Add($column1) | Out-Null
            $grid.ColumnDefinitions.Add($column2) | Out-Null
            #endregion Build Parent Grid

            #region Child Grid
            $childGrid = New-Object Windows.Controls.Grid
            $row1 = New-Object Windows.Controls.RowDefinition
            $row1.Height = "Auto"
            $row2 = New-Object Windows.Controls.RowDefinition
            $row2.Height = "Auto"
            $childGrid.RowDefinitions.Add($row1) | Out-Null
            $childGrid.RowDefinitions.Add($row2) | Out-Null
            [Windows.Controls.Grid]::SetColumn($childGrid,1)
            [Windows.Controls.Grid]::SetColumnSpan($childGrid,2)
            $grid.Children.Add($childGrid) | Out-Null
            #endregion Child Grid
    
            #region Add Controls to Child Grid
            $nameTextBlock = New-Object Windows.Controls.TextBlock
            $nameTextBlock.Text = $InputObject.Gamertag
            $nameTextBlock.FontWeight = 'Bold'  
            $nameTextBlock.FontSize = '14'  
            $nameTextBlock.Foreground = 'White'
            [Windows.Controls.Grid]::SetRow($nameTextBlock,0)

            $notesTextBlock = New-Object Windows.Controls.TextBlock
            $notesTextBlock.TextWrapping="Wrap"
            $notesTextBlock.Text = $InputObject.Presence
            $notesTextBlock.Foreground = 'White'
            [Windows.Controls.Grid]::SetRow($notesTextBlock,1)

            $childGrid.Children.Add($nameTextBlock) | Out-Null
            $childGrid.Children.Add($notesTextBlock) | Out-Null
            #endregion Add Controls to Child Grid

            #region Add Controls to Parent Grid
            $image = New-Object Windows.Controls.Image
            $image.Source = $InputObject.GamerTileurl
            $image.Width = 50
            $image.Height = 50
            [Windows.Controls.Grid]::SetRow($image,0)
            [Windows.Controls.Grid]::SetColumn($image,0)
            [Windows.Controls.Grid]::SetRowSpan($image,2)

            $grid.Children.Add($image) | Out-Null
            #endregion Add Controls to Parent Grid

            Write-Output $grid
        }    
    #Internal Function to quickly create tooltip
    Function New-ToolTip {
        Param (
            [parameter()]
            $Header,
            [parameter()]
            $Body,
            [parameter()]
            $HeaderColor = "Black"
        )
        $stackPanel = New-Object Windows.Controls.StackPanel
            If ($Header) {
                $headerText = New-Object Windows.Controls.TextBlock
                $headerText.FontWeight = 'Bold'
                $headerText.Text = $Header
                $headerText.Foreground = $HeaderColor
                $stackPanel.AddChild($headerText) | Out-Null
            }
            If ($Body) {
                $bodyText = New-Object Windows.Controls.TextBlock
                $bodyText.TextWrapping = "Wrap"
                $bodyText.Text = $Body | Out-String   
                $stackPanel.AddChild($bodyText) | Out-Null
            }
        Write-Output $stackPanel
    }

New-ToolTip and New-Grid are my helper functions that make it easier to build dynamic UIs into the Widget. The tooltip function just adds a richer tooltip for the recent activity while the grid function is used on the child window for online friends to output a grid that has a couple textblocks and image controls to a listview to make viewing online friends easier. I originally had this as another tooltip, but quickly realized the disaster that it would have been.

            #Allow API calls if an hour has passed and currently unable to make call due to limit
            If ($xboxLive.isAtAPILimit -AND ((Get-Date).AddHours(-1) -gt $initalTime)) {
                $initalTime = Get-Date
                $xboxLive.isAtAPILimit = $False
            }
            #region API Calls
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxProfile = [xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/profile/{0}" -f $xboxLive.Gamertag) | 
                    Select-Object -ExpandProperty Content)
                $xboxLive.api = ($xboxLive.xboxProfile.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxGames = ([xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/games/{0}" -f $xboxLive.Gamertag)))
                $xboxGames = $xboxLive.xboxGames.Data.Games | Select -First 5
                $xboxLive.api = ($xboxLive.xboxGames.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxFriends = [xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/friends/{0}" -f $xboxLive.Gamertag))   
                $xboxLive.api = ($xboxLive.xboxFriends.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }

This is the API calls to get all of the information needed to properly update the widget. I have checks in place to make sure that I do not go over the API limit within an hour. Once an hour is up, the API limit resets itself. Of course, if you go over the API limit the widget will wait a full hour before it will allow another call. I am still working a better way to handle this other than a full hour wait, but that will be in the next version.

            #region UI Update            
            $ui.gamertagpicture.Dispatcher.Invoke("Normal",[action]{
                #region Xbox Live Profile
                If ((("{0}" -f $ui.gamertagpicture.Source) -ne $xboxLive.xboxProfile.data.Player.Avatar.Gamerpic.Large)) {
                    $ui.gamertagpicture.Source = $xboxLive.xboxProfile.data.Player.Avatar.Gamerpic.Large
                }
            })
            $ui.achievementpoints.Dispatcher.Invoke("Normal",[action]{
                $ui.achievementpoints.Text = $xboxLive.xboxProfile.Data.Player.Gamerscore
            })
            $ui.gamertag_txt.Dispatcher.Invoke("Normal",[action]{
                $ui.gamertag_txt.Text = $xboxLive.xboxProfile.Data.Player.Gamertag
                If ($xboxLive.xboxProfile.Data.Player.Status.Online) {
                    $ui.onlinestate.Text = 'Online'    
                } Else {
                    $ui.onlinestate.Text = 'Offline'
                }
            })
            $ui.status.Dispatcher.Invoke("Normal",[action]{
                $ui.status.Text = $xboxLive.xboxProfile.data.Player.Status.Online_Status
            })
                #endregion
                #region Xbox Live Friends             
                $totalFriends = @($xboxLive.xboxFriends.data.Friends)
                $xboxLive.onlineFriends = @($xboxLive.xboxFriends.data.Friends | Where {$_.IsOnline})
                $online = $onlineFriends | Select -Expand Gamertag
                $ui.friends.Dispatcher.Invoke("Normal",[action]{
                    $ui.friends.Text = ("{0} / {1} Friends Online" -f $xboxLive.onlineFriends.count,$totalFriends.count)                    

                })
                If ($xboxLive.onlineFriends.Count -gt 0 -AND $childui.onlineFriendsActiveWindow) {
                    $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{
                        $childui.onlineFriendsListView.Items.Clear()                       
                    })

                    $xboxLive.onlineFriends | ForEach {
                        $xboxLive.onlineUser = $_
                        $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{                              
                            $childui.onlineFriendsListView.Items.Add((New-GridItem -InputObject $xboxlive.onlineUser)) | Out-Null              
                        })
                    }   
                }
                If ($xboxLive.onlineFriends.Count -gt 0 -AND $ui.onlineFriendsActiveWindow) {
                    $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{
                        $childui.onlineFriendsListView.Items.Clear()                       
                    })                
                }
                #endregion
                #region Recent Games
            $ui.game0_image.Dispatcher.Invoke("Normal",[action]{               
                If ((("{0}" -f $ui.game0_image.Source) -ne $xboxGames[0].boxart.large)) {
                    $ui.game0_image.Source = $xboxGames[0].boxart.large
                }
                $ui.ui.NavigateUri = $xboxGames[0].MarketplaceURL
                $ui.game0_link.ToolTip = New-ToolTip -Header $xboxGames[0].Name -Body @"
Achievements: $($xboxGames[0].Progress.Achievements) / $($xboxGames[0].PossibleAchievements)
Points: $($xboxGames[0].Progress.Score) / $($xboxGames[0].PossibleScore)
"@      
            })
            $ui.game1_image.Dispatcher.Invoke("Normal",[action]{ 
                If ((("{0}" -f $ui.game1_image.Source) -ne $xboxGames[1].boxart.large)) {
                    $ui.game1_image.Source = $xboxGames[1].boxart.large
                }
                $ui.game1_link.NavigateUri = $xboxGames[1].MarketplaceURL
                $ui.game1_link.ToolTip = New-ToolTip -Header $xboxGames[1].Name -Body @"
Achievements: $($xboxGames[1].Progress.Achievements) / $($xboxGames[1].PossibleAchievements)
Points: $($xboxGames[1].Progress.Score) / $($xboxGames[1].PossibleScore)
"@ 
            })
            $ui.game2_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game2_image.Source) -ne $xboxGames[2].boxart.large)) {
                    $ui.game2_image.Source = $xboxGames[2].boxart.large
                }
                $ui.game2_link.NavigateUri = $xboxGames[2].MarketplaceURL
                $ui.game2_link.ToolTip = New-ToolTip -Header $xboxGames[2].Name -Body @"
Achievements: $($xboxGames[2].Progress.Achievements) / $($xboxGames[2].PossibleAchievements)
Points: $($xboxGames[2].Progress.Score) / $($xboxGames[2].PossibleScore)
"@ 
            })
            $ui.game3_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game3_image.Source) -ne $xboxGames[3].boxart.large)) {
                    $ui.game3_image.Source = $xboxGames[3].boxart.large
                }
                $ui.game3_link.NavigateUri = $xboxGames[3].MarketplaceURL
                $ui.game3_link.ToolTip = New-ToolTip -Header $xboxGames[3].Name -Body @"
Achievements: $($xboxGames[3].Progress.Achievements) / $($xboxGames[3].PossibleAchievements)
Points: $($xboxGames[3].Progress.Score) / $($xboxGames[3].PossibleScore)
"@  
            })
            $ui.game4_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game4_image.Source) -ne $xboxGames[4].boxart.large)) {
                    $ui.game4_image.Source = $xboxGames[4].boxart.large
                }
                $ui.game4_link.NavigateUri = $xboxGames[4].MarketplaceURL
                $ui.game4_link.ToolTip = New-ToolTip -Header $xboxGames[4].Name -Body @"
Achievements: $($xboxGames[4].Progress.Achievements) / $($xboxGames[4].PossibleAchievements)
Points: $($xboxGames[4].Progress.Score) / $($xboxGames[4].PossibleScore)
"@               
            })  
                #endregion Recent Games
    #endregion
        #Wait 30 seconds before making next API call
        Start-Sleep -Seconds 30
        }

This is where all of the UI updates take place. If you remember from my previous blog post, you will see some familiar techniques being used with the dispatcher object on each UI control. An important thing to note is that you should avoid performing any sort of data collection or long running commands during the use of the Invoke method of the dispatcher as it will crush your UI and cause it to freeze up. So make sure that you do all of the data gathering prior to calling the dispatcher and just add the data during that time to the proper control.

    $ui.onlinefriendsmenu.Add_Click({
        $rs = [RunspaceFactory]::CreateRunspace()
        $rs.ApartmentState = “STA”
        $rs.ThreadOptions = “ReuseThread”
        $rs.Open() | Out-Null
        $runspace.__PowerShell = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase}.GetPowerShell() 
        $rs.SessionStateProxy.SetVariable('xboxLive',$xboxLive)
        $rs.SessionStateProxy.SetVariable('ui',$ui)
        $rs.SessionStateProxy.SetVariable('runspace',$runspace)
        $rs.SessionStateProxy.SetVariable('childui',$childui)
        $runspace.__PowerShell.Runspace = $rs 
        $runspace.__PowerShell.AddScript({ 
                Function New-GridItem {
                    [cmdletbinding(DefaultParameterSetName='Body')]
                    Param (
                        [parameter(ParameterSetName='Object')]
                        $InputObject
                    )

                    #region Build Parent Grid
                    $grid = New-Object Windows.Controls.Grid
                    $grid.Width = 250
                    $grid.tag = $InputObject.Gamertag
                    $column1 = New-Object Windows.Controls.ColumnDefinition
                    $column1.Width = "Auto"
                    $column2 = New-Object Windows.Controls.ColumnDefinition
                    $grid.ColumnDefinitions.Add($column1) | Out-Null
                    $grid.ColumnDefinitions.Add($column2) | Out-Null
                    #endregion Build Parent Grid

                    #region Child Grid
                    $childGrid = New-Object Windows.Controls.Grid
                    $row1 = New-Object Windows.Controls.RowDefinition
                    $row1.Height = "Auto"
                    $row2 = New-Object Windows.Controls.RowDefinition
                    $row2.Height = "Auto"
                    $childGrid.RowDefinitions.Add($row1) | Out-Null
                    $childGrid.RowDefinitions.Add($row2) | Out-Null
                    [Windows.Controls.Grid]::SetColumn($childGrid,1)
                    [Windows.Controls.Grid]::SetColumnSpan($childGrid,2)
                    $grid.Children.Add($childGrid) | Out-Null
                    #endregion Child Grid
    
                    #region Add Controls to Child Grid
                    $nameTextBlock = New-Object Windows.Controls.TextBlock
                    $nameTextBlock.Text = $InputObject.Gamertag
                    $nameTextBlock.FontWeight = 'Bold'  
                    $nameTextBlock.FontSize = '14'  
                    $nameTextBlock.Foreground = 'White'
                    [Windows.Controls.Grid]::SetRow($nameTextBlock,0)

                    $notesTextBlock = New-Object Windows.Controls.TextBlock
                    $notesTextBlock.TextWrapping="Wrap"
                    $notesTextBlock.Text = $InputObject.Presence
                    $notesTextBlock.Foreground = 'White'
                    [Windows.Controls.Grid]::SetRow($notesTextBlock,1)

                    $childGrid.Children.Add($nameTextBlock) | Out-Null
                    $childGrid.Children.Add($notesTextBlock) | Out-Null
                    #endregion Add Controls to Child Grid

                    #region Add Controls to Parent Grid
                    $image = New-Object Windows.Controls.Image
                    $image.Source = $InputObject.GamerTileurl
                    $image.Width = 50
                    $image.Height = 50
                    [Windows.Controls.Grid]::SetRow($image,0)
                    [Windows.Controls.Grid]::SetColumn($image,0)
                    [Windows.Controls.Grid]::SetRowSpan($image,2)

                    $grid.Children.Add($image) | Out-Null
                    #endregion Add Controls to Parent Grid

                    Write-Output $grid
                }
                #Launch new window showing online friends
                [xml]$onlineFriendsXAML =  @"
                <Window
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    Title="MainWindow" SizeToContent="WidthAndHeight" ResizeMode="NoResize" WindowStyle="None" AllowsTransparency="True" 
                    Background="Transparent" Topmost="True">
                    <Grid Height="240" Width="330">
                        <Rectangle Height="240" HorizontalAlignment="Left" Name="rectangle1" VerticalAlignment="Top" Width="320" RadiusX="15" RadiusY="15">
                            <Rectangle.Fill>
                                <LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Color="Green" Offset='0' />
                                        <GradientStop Color="White" Offset='1' />
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Rectangle.Fill>
                        </Rectangle>
                        <Label Content="Online Friends" Height="28" HorizontalAlignment="Left" Margin="12,8,0,0" VerticalAlignment="Top" Width="276" 
                        Foreground="White" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontWeight="Bold" FontSize="14" >
                            <Label.Effect>
                                <DropShadowEffect Color = 'Black' ShadowDepth = '2' BlurRadius = '8' />
                            </Label.Effect>                
                        </Label>
                        <ListView Height="200" HorizontalAlignment="Left" Margin="9,36,0,0" Name="listview" VerticalAlignment="Top" Width="300">
                            <ListView.Background>
                                <SolidColorBrush />
                            </ListView.Background>
                        </ListView>
                    </Grid>
                </Window>
"@
                $onlinereader=(New-Object System.Xml.XmlNodeReader $onlineFriendsXAML)
                $childui.onlineFriendsWindow = [Windows.Markup.XamlReader]::Load($onlinereader)               
                $childui.onlineFriendsListView = $childui.onlineFriendsWindow.FindName('listview')
                $xboxLive.onlineFriends | ForEach {
                    $childui.onlineFriendsListView.Items.Add((New-GridItem -InputObject $_)) | Out-Null
                }     
                $childui.onlineFriendsWindow.Add_MouseRightButtonUp({$This.Close()})
                $childui.onlineFriendsWindow.Add_MouseLeftButtonDown({$This.DragMove()})
                $childui.onlineFriendsWindow.Add_Closed({
                    $ui.onlineFriendsActiveWindow = $False
                    $runspace.__PowerShell.EndInvoke($runspace.__Handle)
                    $runspace.__PowerShell.Dispose()
                })
                $childui.onlineFriendsActiveWindow = $True
                $childui.onlinefriendsWindow.ShowDialog() | Out-Null
        }) | Out-Null
        $runspace.__Handle = $runspace.__PowerShell.BeginInvoke()

This is notable because when you use the context menu to select to view the Online Friends, this will spin up a new runspace and then open the new window that shows all of the online friends.

Everything else in the code is the usual mix of XAML code and reading the XAML into PowerShell and connecting to the controls and managing various events.

Let me know what you think of this widget as well as anything else you would like to see from it! I have plans to add more features to this as well as squashing any bugs that I can find.

I had a great time putting this together and hope you enjoy it as well! Give it a download at the link below.

Download the Widget

Script Repository

Posted in GUI, powershell, WPF | Tagged , , , , , , | Leave a comment

Find and Report Members of a Local Group

One of the things that a Sys Admin might want to know about is who exactly is in a group, such as Administrators. Getting it from one system is pretty simple, but pulling this data from multiple remote systems can be a little hectic, especially if trying to do this manually.

Luckily PowerShell can once again help us to get this information with little to no effort.

Excluding the help, I will break this function (Get-LocalGroupMember) up and explain what each part is doing to guide you along the way and then finish up with the function in action.

[cmdletbinding()]
    #region Parameters
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias("Computer","__Server","IPAddress","CN","dnshostname")]
        [string[]]$Computername = $env:COMPUTERNAME,
        [parameter()]
        [string]$Group = 'Administrators',
        [parameter()]
        [string[]]$ValidMember,
        [parameter()]
        [Alias("MaxJobs")]
        [int]$Throttle = 10
    )
    #endregion Parameters
    Begin {
        #region Functions
        #Function to perform runspace job cleanup
        Function Get-RunspaceData {
            [cmdletbinding()]
            param(
                [switch]$Wait
            )
            Do {
                $more = $false         
                Foreach($runspace in $runspaces) {
                    If ($runspace.Runspace.isCompleted) {
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null                 
                    } ElseIf ($runspace.Runspace -ne $null) {
                        $more = $true
                    }
                }
                If ($more -AND $PSBoundParameters['Wait']) {
                    Start-Sleep -Milliseconds 100
                }   
                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where {
                    $_.runspace -eq $Null
                } | ForEach {
                    Write-Verbose ("Removing {0}" -f $_.computer)
                    $Runspaces.remove($_)
                }             
            } while ($more -AND $PSBoundParameters['Wait'])
        }
        #endregion Functions

 

Here we are setting up the parameters for the function which range from specifying a collection of computers to what local group you want to look at. A notable parameter is the ValidMember which allow you to specify a collection of members of a group that will result in the IsValid property being True, meaning that we expected the group to have these members. Great for running a baseline report of members of a specific group, such as Administrators.

Also we have the Get-RunspaceData function which will handle the background runspaces that get created to support the multithreading of the local group check.

  #region Splat Tables
        #Define hash table for Get-RunspaceData function
        $runspacehash = @{}

        $testConnectionHash = @{
            Count = 1
            Quiet = $True
        }

        #endregion Splat Tables

        #region ScriptBlock
        $scriptBlock = {
            Param ($Computer,$Group,$ValidMember,$testConnectionHash)
            Write-Verbose ("{0}: Testing if online" -f $Computer)
            $testConnectionHash.Computername = $Computer
            If (Test-Connection @testConnectionHash) {
		        $adsicomputer = [ADSI]("WinNT://$Computer,computer")
    	        $localgroup = $adsicomputer.children.find($Group)
                If ($localGroup) {
    	            $localgroup.psbase.invoke("members") | ForEach {
                        Try {
                            $member = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
                            If ($ValidMember-notcontains $member) {
                                New-Object PSObject -Property @{
                                    Computername = $Computer
                                    Group = $Group
                                    Account = $member
                                    IsValid = $FALSE
                                }
                            } Else {
                                New-Object PSObject -Property @{
                                    Computername = $Computer
                                    Group = $Group
                                    Account = $member
                                    IsValid = $TRUE
                                }
                            }
                        } Catch {
                            Write-Warning ("{0}: {1}" -f $Computer,$_.exception.message)
                        }
                    }
                } Else {
                    Write-Warning ("{0} does not exist on {1}!" -f $Group,$Computer)
                }  
            } Else {
                Write-Warning ("{0}: Unable to connect!" -f $Computer)
            }         
        }
        #endregion ScriptBlock

        #region Runspace Creation
        Write-Verbose ("Creating runspace pool and session states")
        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()  
        
        Write-Verbose ("Creating empty collection to hold runspace jobs")
        $Script:runspaces = New-Object System.Collections.ArrayList        
        #endregion Runspace Creation
    }

The next section sets up some collections that will house the runspace jobs and supply a splatting hash table for the Test-Connection cmdlet that is used later on to verify if a system is online or not.

The $scriptblock is just that, a scriptblock that we will pass to the runspace to run in the background to get the members of a local group. To get the group, I use the [adsi] type accelerator to connect to the local or remote system. From there I find the group that is specified on the $Group parameter and then pull all of the members of the group using GetType().InvokeMember(). More information about this interface can be found at http://msdn.microsoft.com/en-us/library/aa772237(VS.85).aspx.

Process {
        ForEach ($Computer in $Computername) {
            #Create the powershell instance and supply the scriptblock with the other parameters 
            $powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($Group).AddArgument($ValidMember).AddArgument($testConnectionHash)
           
            #Add the runspace into the powershell instance
            $powershell.RunspacePool = $runspacepool
           
            #Create a temporary collection for each runspace
            $temp = "" | Select-Object PowerShell,Runspace,Computer
            $Temp.Computer = $Computer
            $temp.PowerShell = $powershell
           
            #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
            $temp.Runspace = $powershell.BeginInvoke()
            Write-Verbose ("Adding {0} collection" -f $temp.Computer)
            $runspaces.Add($temp) | Out-Null
           
            Write-Verbose ("Checking status of runspace jobs")
            Get-RunspaceData @runspacehash        
        }
    }
    End {
        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))
        $runspacehash.Wait = $true
        Get-RunspaceData @runspacehash
    
        #region Cleanup Runspace
        Write-Verbose ("Closing the runspace pool")
        $runspacepool.close()  
        $runspacepool.Dispose() 
        #endregion Cleanup Runspace
    } 
}

 

The final section covers adding each computer to the runspace pool if it has a good network connection from wherever it is at. Each runspace is added to the runspace jobs collection and the Get-RunspaceData function is called after each computer is added to make sure that no runspaces have finished and if they have finished, then it will properly dispose of the object.

At the very end we wait for the rest of the runspaces to finish up and dispose of the rest of the runspaces along with the runspace pool.

Now lets put this function into action! First we need to dot source the script to actually load the function into the current PowerShell session.

. .\Get-LocalGroupMember

If we do not do this, then the function will simply not work. In fact, running the script like you normally would will result in nothing happening.

Get-LocalGroupMember -ValidMember "Administrator"

image

As you can see, all members of the Administrators group are presented in a report and because I specified Administrator as a ValidMember so it can be filtered out if needed by only looking for False entries under the IsValid property.

Get-LocalGroupMember -ValidMember "Administrator" `
-Computername dc1.rivendell.com,boe-pc

 

image

Using this, you could set the function up as scheduled job to report on groups that do not meet a baseline of users or whatever else you can think of.

Download the Function

Script Repository

Posted in powershell, scripts | Tagged , | 5 Comments

Quick Hits: Speed Up Some of your Commands by Avoiding the Pipeline

Sometimes running a command using the pipeline can take a while depending on the amount of data that is being processed. For the easiest example, I will make use of Get-Random to demonstrate the differences in speed and how much memory is used.

("Memory Before: {0:#,#}KB" -f ((Get-Process -Id $PID).PeakWorkingSet /1kb))
(Measure-Command {
    1..1E6 | Get-Random
}).TotalSeconds
("Memory After: {0:#,#}KB" -f ((Get-Process -Id $PID).PeakWorkingSet /1kb))

image

Here you see that it takes a little over a minute to run, but the amount of memory used isn’t a whole lot; about 6KB.

Instead, you should take a look at using the –InputObject parameter as it accepts a collection of objects to use instead for much faster performance.

("Memory Before: {0:#,#}KB" -f ((Get-Process -Id $PID).PeakWorkingSet /1kb))
(Measure-Command {
    Get-Random -InputObject (1..1E6)
}).TotalSeconds
("Memory After: {0:#,#}KB" -f ((Get-Process -Id $PID).PeakWorkingSet /1kb))

image

This didn’t event take a second to run using the InputObject parameter. However, you will see that the amount of memory required to perform this operation jumped up by about 20KB, so caution should be used if trying to run this against a lot of data to avoid running into memory issues.

Of course, there is a catch to this performance increase in that it will consume memory more than using the pipeline. So be sure to keep this in mind when using the InputObject parameter.

This is especially true when working with large logs. While cmdlets such as Get-Content and Import-CSV do not have InputObject parameters, it is better to use the pipeline to handle the amount of data that is being returned without throwing an Out of Memory exception. Assume that we have a 600MB+ CSV file and we want to know something simple such as how many lines are in this CSV, the following has the potential to throw the OOM error (this actually happened to me at work).

(Import-Csv -File log.csv).Count

In order to avoid this, I made use of the Measure-Object cmdlet by piping the output of Export-CSV into the Measure-Object to get the count.

Export-Csv -File test.csv | Measure-Object 

Of course, these are very generic examples, but hopefully you can see the benefit of using each of these methods in your scripts. So the bottom line is: use the pipeline to avoid issues with memory at the cost of slower performance and avoid using the pipeline if you want better performance from your code at the expense of using more memory.

Posted in powershell | Tagged , , , , | 1 Comment

Name Change to PoshWSUS Commands with PoshWSUS 2.1

If you remember, I posted a poll regarding the renaming of the PoshWSUS commands to avoid a naming collision with the UpdateServices module now available with Windows 2012/8 which use the *-WSUS* format. Well, after letting the poll sit for a month, the winner is… (VERB)-PSWSUS(NOUN)!

So with that, I updated all of the commands to use this format and have since released PoshWSUS V.2.1 which has this change. There are no other changes with this version, so if you don’t feel like downloading it just yet, then you are not missing anything important just yet.

Just for some fun, how did I manage to rename the files and the contents of each file? Well, I did this with one line of code for each action: Renaming the files and renaming the contents of the files.

Rename the files

Get-ChildItem | rename-item -NewName {$_.name -replace "-WSUS","-PoshWSUS"}

Rename the content of the files

Get-ChildItem -Recurse | ForEach {
    ((Get-Content $_.fullname) -replace "-WSUS","-PoshWSUS") | 
        Set-Content $_.FullName
}

Yep, it was that simple.

You can download the latest version of PoshWSUS at the link below.

http://poshwsus.codeplex.com/

Posted in Modules, WSUS | Tagged , , , | Leave a comment