Scripting Games 2012 Tip: Don’t Use Aliases

Continuing on from me last article, now we are looking at why using an alias in your script is not a good idea.

There are a lot of aliases out there in PowerShell. Some even use the same alias as *nix commands (ls,ps) and also as the command.exe commands you are used to (mkdir,dir). While this is perfectly fine when working directly in the console running commands that do not require much effort or something that isn’t complicated.

Even if you have been using PowerShell for a while, odds are there are aliases that you haven’t seen and wouldn’t even know what cmdlet it maps to.

Take a look at this:

gps -id 3720 | % {
    $_.Name
}

Unless you use this often, then you might not know that gps is actually Get-Process and the “%” (modulus) is actually the alias for “ForEach-Object”. All this does is make it more difficult to read the code that was written and creates more work for the person trying to understand what the code is actually doing.

Use this instead:

Get-Process -Id 3720 | ForEach {
    $_.Name
}

While this example is not that impressive, imagine going through 100+ lines of code having to translate aliased cmdlets. Not much fun and it won’t help you score any more points in the Scripting Games either.

The same goes for parameters for each cmdlet. While it is tempting to shorten the parameter in an effort to save space, go ahead and expand the parameter out to what it should be. This will not only help you out if you have to go back to the script for some reason and also help others out who might be looking at your code to learn or troubleshoot.

Don’t do this:

Get-WmiObject -co dc1 -cl win32_service -f "name='wmi'"

Do this:

Get-WmiObject -Computer dc1 -Class win32_service -Filter "name='wmi'"

Makes the code look much cleaner and more easier to read.

 

Remember, use the full cmdlet name, no aliases at all in your script. Regardless of how cool it might look to use nothing but aliases. Same goes for the parameters for each cmdlet. Make it as readable as you can because not only could you be the one to come back to the code later on, but others might also be looking at your code and you want to have the best presentation possible!

Posted in Scripting Games 2012 | Tagged , , | 1 Comment

Scripting Games 2012 Tip: Variable Names That Make Sense

This is something that I am going to do from now until the start of the Scripting Games. Basically I am going to discuss some areas to watch out for when writing scripts for the upcoming games. These are intended to be a short example and not a drawn out thing. I want to also add that these are my own opinions and do not reflect the opinions of the other judges.

Variables

One thing that I have seen during the games and also in everyday scripting are the wide use of variable names. When using variables in PowerShell, it is best to keep it to something that makes so the people reading the scripts know exactly what it is you are using the variable for. Take this example:

$Computername = 'server1','server2'
ForEach ($Computer in $Computername) {$Computer}

We know exactly what is in this variable and do not have to read back up the code to try and determine what might be in it.

Something like this would lead into confusion to figure out what it means.

$Servers = 'server1','server2'
ForEach ($C in $Servers) {$c}

Not a lot of consistency here and worse than that, we need to figure out what exactly $c is.

Another example:

$p = Get-WMIObject Win32_Process
$dt = Get-Date

There are no prizes for the shortest variable name. Take the extra time to write it out so it is more readable.

$processes = Get-WmiObject Win32_Process
$Date = Get-Date

Keep the variables to something that is clear and concise with what they are meant to be used for.

Hungarian notation

Gone are the days where you need to name a variable by its type.

$strComputer=  'server'
$arrComputer = 'server','server1','server2'
$collWmi = Get-WmiObject Win32_Processor

Its just not as important as it used to be in tracking the type of object in a variable. Clarity is the important thing, especially when the code can get long where the person reading the code would just prefer to know what it is being used for rather then knowing what type of object it is.

 

Remember, clarity in variable names are important to your script and also to the person who comes in after you to read your script!

Posted in Scripting Games 2012 | Tagged , | Leave a comment

Time For Scripting Games 2012

image

You heard it! The 2012 Scripting Games are almost upon us and this is your chance to test your skills! Being a judge this year, I will be one of the people that will look at the scripts being submitted and help rate each one. I am very excited to be a part of this alongside so many superstars in PowerShell.

What It Is

The scripting games are a scripting ‘competition’ using PowerShell that goes from April 2nd through April 13th. Each day (Monday-Friday) a new scenario is given that must be solved via a PowerShell solution. Once a script is submitted, it will be judged by the judges and given a score of either 1-5 stars. I say ‘competition’ because it is also about having fun and expanding on your current knowledge of using PowerShell as well as competing in one of two categories (Beginner or Advanced) against others around the world for the top place finisher.

What You Should Do

You should compete! It is as simple as that! As I mentioned earlier, there are two categories that you can sign up. You can only sign up for one of the categories, so make sure you choose the right one that matches your skills. If you are a seasoned scripter and want more of a challenge, then the Advanced category is for you. However, if you are just learning about PowerShell or haven’t been able to use it as much as you would like then you can look at doing the Beginner’s category. Each scenario that is delivered is a real-world type of situation where you must solve a given task using PowerShell.

A new thing to this year’s games is the inclusion of PowerShell V3 beta. This means that those of you who are using PowerShell V3 beta will be allowed to submit solutions using this instead of V2. V2 is still allowed in the games, but you now have a different option if desired.

Why Should You Compete?

PowerShell is the tool in automation and scripting in the Windows and Non-Windows environment and is only becoming more and more prevalent in the field with Windows Server 8 (over 2000 cmdlets available!) coming online. The community for PowerShell is huge with members blogging, posting scripts and helping each other out in the forums to cover many facets of technology. Even Non-Microsoft technologies are capable of being automated and scripted against using PowerShell, such as VMware and EMC.

This is a great chance to dive into PowerShell and learn more about what it can do for you and your organization. Not only will you expand on your current skill-set, but you can also win stuff as well! The big prize for finishing first in the Beginner and Advanced categories are a pass to Tech-Ed 2012. There are other prizes available as well that will be announced as the games draw closer.

Where Do You Sign Up?

As of right now, signups are not open yet for the games, but they will be happening soon enough! Keep an eye out on the twitter front and also by checking the 2012 Scripting Games page. The page hosting the script submission repository will be at PoshCode.org again this year.

Extra Items

Scripting Games 2012 All In One Page

Judging Criteria has been announced for the Scripting Games.

Scripting Games 2012 Judges

Ed Wilson is hosting a 5 day set of Live Meetings call Windows PowerShell For The Busy Admin that will be vital for folks competing this year in the games. I suggest you check them all out! Link listing each Live Meeting is here.

Twitter HashTag #2012sg

Hey, Scripting Guy Facebook Page

Good luck to everyone participating this year! It is going to be a great time!

Posted in News, powershell, Scripting Games 2012 | Tagged , , | Leave a comment

PoshChat 2 of 2: Building a Chat Client Using PowerShell

Continuing from yesterday’s article where I talked about how I wrote the code to run the chat server portion of PoshChat, this article will now go into what I did to create the client interface that connects to the server and allows you to send messages to others connected to the server.

About half of the code is setting the UI of the client while another chunk of code sets up some of the controls. The rest is where I set up the connection to the server and send/receive messages that have been relayed from the server from other clients.

As with the server, there were some requirements that I wanted to lay out before starting on this client.

Requirements:

  • Make a connection to the server
  • Allow a username and server name to be defined for connection
  • Able to actively listen for messages while still being able to send messages
  • Cleanly close connections when exiting client

With the requirements out of the way, lets take a look at some code.

$rs=[RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = "STA"
$rs.ThreadOptions = "ReuseThread"
$rs.Open()
$ps = [PowerShell]::Create()
$ps.Runspace = $rs

First thing that I am doing is setting up the runspace that will run in the background to free up the console while the UI is running. The key thing here is that I am setting the apartment state to ‘STA’ so the UI will work normally. This means that you can run the console in ‘MTA’ without worry about the UI not starting up.

$handle = $ps.AddScript({               
Add-Type –assemblyName PresentationFramework
Add-Type –assemblyName PresentationCore
Add-Type –assemblyName WindowsBase               
[xml]$xaml = @"
<Window
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    x:Name='Window' Title='PoshChat' Height = '600' Width = '800' ResizeMode = 'NoResize' WindowStartupLocation = 'CenterScreen' ShowInTaskbar = 'True'>    
    <Window.Background>
        <LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
            <LinearGradientBrush.GradientStops> <GradientStop Color='#C4CBD8' Offset='0' /> <GradientStop Color='#E6EAF5' Offset='0.2' /> 
            <GradientStop Color='#CFD7E2' Offset='0.9' /> <GradientStop Color='#C4CBD8' Offset='1' /> </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
    </Window.Background>    
    <Grid ShowGridLines = 'false'>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width ='175*'> </ColumnDefinition>
            <ColumnDefinition Width ='Auto'> </ColumnDefinition>
            <ColumnDefinition Width ='75*'> </ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height = '*'/>
            <RowDefinition Height = '10'/>
            <RowDefinition Height = '80'/>
        </Grid.RowDefinitions>     
        <TextBox x:Name = 'MainMessage_txt' Grid.Column = '0' Grid.Row = '0' IsReadOnly = 'True' VerticalScrollBarVisibility='Visible'
        TextWrapping = 'Wrap'/>   
        <Label Grid.Column='1' Grid.Row = '0' Width='8' Grid.RowSpan = '3' HorizontalAlignment = 'Center' VerticalAlignment = 'Stretch'
        Background = 'LightGray'/>
        <Label Grid.Column = '0' Grid.Row = '1' Grid.ColumnSpan = '3' Background = 'LightGray'/>
        <ListView x:Name = 'OnlineUsers' Grid.Column = '2' Grid.Row = '0' />
        <StackPanel Grid.Column = '0' Grid.Row = '2' Orientation="Horizontal">
            <TextBox x:Name = 'Input_txt' Width = '500' AcceptsReturn = 'True' VerticalScrollBarVisibility='Visible' TextWrapping = 'Wrap'/>
            <Button x:Name = 'Send_btn' Width = '50' Height = '25' Content = 'Send' />
        </StackPanel>        
        <StackPanel Grid.Column = '2' Grid.Row = '2'>
            <StackPanel Orientation="Horizontal">
                <Label Content = 'UserName'/>
                <TextBox x:Name = 'username_txt' Width = '150' />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Label Content = 'Server' Width = '66' />
                <TextBox x:Name = 'servername_txt' Width = '150'/>
            </StackPanel>          
            <Label Height = '3' />
            <Button x:Name = 'Connect_btn' Width = '75' Height = '20' Content = 'Connect'/>
        </StackPanel>
    </Grid>
</Window>

"@
#Load XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

##Controls
$Global:OnlineUsers = $Window.FindName('OnlineUsers')
$SendButton = $Window.FindName('Send_btn')
$Global:ConnectButton = $Window.FindName('Connect_btn')
$Username_txt = $Window.FindName('username_txt')
$Server_txt = $Window.FindName('servername_txt')
$Inputbox_txt = $Window.FindName('Input_txt')
$Global:MainMessage = $Window.FindName('MainMessage_txt')

Here is the beginning of the scriptblock that I will supply to the runspace. The first thing I do is load of the assemblies required to run the WPF client and display the UI. I am also setting up the UI using XAML code. the first line here is where I begin adding the code to the runspace via a scriptblock. The scriptblock will not be closed up until the end of the script. The XAML code is casted to XML using the [XML] type accelerator that then gets loaded into the System.XML.XMLNodeReader. Next, I connect to the controls that I need access to later on in the code to perform a variety of things such as handling button events or reading from a text box.

##Events
#Connect
$ConnectButton.Add_Click({
    $ConnectButton.IsEnabled = $False
    
    #Get Server IP
    $Server = $Server_txt.text
    
    #Get Username
    $Global:Username = $Username_txt.text
    
    If ($Server -AND $Username) {        
        $MainMessage.text = "{0} >> Connecting to {1} as {2}`n" -f (Get-Date).ToString(),$Server,$username
        
        #Connect to server
        $Endpoint = new-object System.Net.IPEndpoint ([ipaddress]::any,$SourcePort)
        $TcpClient = [Net.Sockets.TCPClient]$endpoint    
        Try {
            $TcpClient.Connect($Server,15600)
            $Global:ServerStream = $TcpClient.GetStream()
            $data = [text.Encoding]::Ascii.GetBytes($Username)
            $ServerStream.Write($data,0,$data.length)
            $ServerStream.Flush()   
            If ($TcpClient.Connected) {       
                $Window.Title = ("{0}: Connected as {1}" -f $Window.Title,$Username)
                #Kick off a job to watch for messages from clients
                $newRunspace = [RunSpaceFactory]::CreateRunspace()
                $newRunspace.Open()
                $newRunspace.SessionStateProxy.setVariable("TcpClient", $TcpClient)
                $newRunspace.SessionStateProxy.setVariable("MessageQueue", $MessageQueue)                
                $newPowerShell = [PowerShell]::Create()
                $newPowerShell.Runspace = $newRunspace   
                $sb = {
                    #Code to kick off client connection monitor and look for incoming messages.
                    $client = $TCPClient
                    $serverstream = $Client.GetStream()
                    #While client is connected to server, check for incoming traffic
                    While ($client.Connected) {                        
                        [byte[]]$inStream = New-Object byte[] 10025
                        $buffSize = $client.ReceiveBufferSize
                        $return = $serverstream.Read($inStream, 0, $buffSize)
                        If ($return -gt 0) {
                            $Messagequeue.Enqueue([System.Text.Encoding]::ASCII.GetString($inStream[0..($return - 1)]))
                        }
                    }
                    #Shutdown the connection as connection has ended
                    $client.Client.Disconnect($True)
                    $client.Client.Close()
                    $client.Close()                      
                }
                $job = "" | Select Job, PowerShell
                $job.PowerShell = $newPowerShell
                $Job.job = $newPowerShell.AddScript($sb).BeginInvoke()
                $ClientConnection.$Username = $job             
            }
        } Catch {
            #Errors Connecting to server
            $MainMessage.text = ("Unable to connect to {0}!'nPlease try again later!" -f $RemoteServer)
            $ConnectButton.IsEnabled = $True
            $TcpClient.Close()  
            $ClientConnections.user.PowerShell.EndInvoke($ClientConnections.user.Job)
            $ClientConnections.user.PowerShell.Runspace.Close()
            $ClientConnections.user.PowerShell.Dispose()
        }
    }
})

Now we are getting into the first control event being handled. This is what controls the connect button and is actually the biggest chunk of code as it has to handle the initial connection attempt to the chat server. A validation is made to be sure that a username and server name is supplied before proceeding. Once that is done a local endpoint is created using a random port number using System.Net.IPEndpoint class. Once that has been completed, a TCP object is created and begins an attempted connection to the chat server over port 15600 (currently hard-coded, but will changed in the next release) and if a connection is successful, then spawns a new runspace that will continue to handle the connection and listen for messages from the server.

The created runspace is saved to a synced hashtable ($ClientConnections) that will be used later if the client is closed so it can gracefully close out all of the runspaces and connections.

Another key piece of code is here:

While ($client.Connected) {                        
    [byte[]]$inStream = New-Object byte[] 10025
    $buffSize = $client.ReceiveBufferSize
    $return = $serverstream.Read($inStream, 0, $buffSize)
    If ($return -gt 0) {
        $Messagequeue.Enqueue([System.Text.Encoding]::ASCII.GetString($inStream[0..($return - 1)]))
    }
}

This ensures that while the connection is active, it will constantly look for messages from the server. The Read() method will block any more action in the runspace until a message has been received on the client. If the message contains data, it will be translated from bytes and sent to the message queue.

#Send message
$SendButton.Add_Click({
    #Send message to server
    $Message = "~M{0}{1}{2}" -f $username,"~~",$Inputbox_txt.Text
    $data = [text.Encoding]::Ascii.GetBytes($Message)
    $ServerStream.Write($data,0,$data.length)
    $ServerStream.Flush()  
    $Inputbox_txt.Clear()  
})

This piece of code handles the send button event when clicked. The text from the inputbox is collected with the “~M” appended to it so can be interpreted as a message, then it gets converted into a byte[] array before being sent to the chat server.

#Load Window
$Window.Add_Loaded({
    #Used for managing the queue of messages in an orderly fashion
    $Global:MessageQueue =  [System.Collections.Queue]::Synchronized((New-Object System.collections.queue))      
    #Used for managing client connection
    $Global:ClientConnection = [hashtable]::Synchronized(@{}) 
    #Create Timer object
    $Global:timer = new-object System.Windows.Threading.DispatcherTimer 
    #Fire off every 1 seconds
    $timer.Interval = [TimeSpan]"0:0:1.00"
    #Add event per tick
    $timer.Add_Tick({    
        Write-Host ('Message Count: {0}' -f $Messagequeue.count)
        [Windows.Input.InputEventHandler]{ $Global:Window.UpdateLayout() }
        If ($Messagequeue.Count -gt 0) {
            $Message = $Messagequeue.Dequeue()
            Switch ($Message) {
                {$_.Startswith("~M")} {
                    #Message
                    $data = ($_).SubString(2)
                    $split = $data -split ("{0}" -f "~~")
                    $MainMessage.text += ("{0} >> {1}: {2}`n" -f (Get-Date).ToString(),$split[0],$split[1])
                }
                {$_.Startswith("~D")} {
                    #Disconnect
                    $MainMessage.text += ("{0} >> {1} has disconnected from the server`n" -f (Get-Date).ToString(),$_.SubString(2))
                    #Remove user from online list
                    $OnlineUsers.Items.Remove($_.SubString(2))
                }
                {$_.StartsWith("~C")} {
                    #Connect
                    $MainMessage.text += ("{0} >> {1} has connected to the server`n" -f (Get-Date).ToString(),$_.SubString(2))  
                    ##Add user to online list       
                    If ($Username -ne $_.SubString(2)) {
                        $OnlineUsers.Items.Add($_.SubString(2))   
                    }
                }
                {$_.StartsWith("~S")} {
                    #Server Shutdown
                    $MainMessage.text += ("{0} >> SERVER HAS DISCONNECTED.`n" -f (Get-Date).ToString())  
                    $TcpClient.Close()  
                    $ClientConnection.user.PowerShell.EndInvoke($ClientConnections.user.Job)
                    $ClientConnection.user.PowerShell.Runspace.Close()
                    $ClientConnection.user.PowerShell.Dispose()  
                    $ConnectButton.IsEnabled = $True   
                    $DisconnectButton.IsEnabled = $False  
                    $OnlineUsers.Items.Clear()                                       
                }                 
                {$_.StartsWith("~Z")} {
                    #List of connected users
                    $online = (($_).SubString(2) -split "~~")
                    #Add online users to window
                    $Online | ForEach {
                        $OnlineUsers.Items.Add($_)
                    }
                }
                Default {
                    $MainMessage.text += ("{0} >> {1}`n" -f (Get-Date).ToString(),$_)
                }
            }            
        } 
    })

 

This really is a two-part chunk of code as it not only handles the initial loading of the form, but also sets up a form timer that performs an action with every tick. The ‘tick’ is set for every second. Two synchronized objects are created to handle the client/server connection and to handle all of the messages from the server.

  1. MessageQueue
    1. Handles all of the message traffic from the server
  2. ClientConnection
    1. Handles the client connection with the server

Next up is creating and configuring the form timer (System.Windows.Threading.DispatcherTimer) to check the message queue and print out messages to the client based on what type of message is received from the server. The following types of messages that are accepted are:

  1. ~M
    1. Standard messages from other connected clients
  2. ~D
    1. Handles messages when other clients are disconnected from server
  3. ~C
    1. Handles messages when new clients are connected to server
  4. ~S
    1. Handles the message when the server is shutdown or closes client connection unexpectedly
  5. ~Z
    1. Handles the initial connection message sent from the server listing all of the currently connected clients.
    #Start timer
    $timer.Start()
    If (-NOT $timer.IsEnabled) {
        $Window.Close()
    }

 

This starts up the timer and if it is not enabled, then the form will close on its own to avoid any errors.

#Disconnect from server
$DisconnectButton.Add_Click({    
    $MainMessage.text += ("{0} >> Disconnecting from server: {1}`n" -f (Get-Date).ToString(),$Server)
    #Shutdown client runspace and socket
    $TcpClient.Close()  
    $ClientConnection.user.PowerShell.EndInvoke($ClientConnection.user.Job)
    $ClientConnection.user.PowerShell.Runspace.Close()
    $ClientConnection.user.PowerShell.Dispose()
    $ConnectButton.IsEnabled = $True   
    $DisconnectButton.IsEnabled = $False
    $OnlineUsers.Items.Clear()
})

Here we are handling the disconnect button event. A message is displayed on the window stating the client is disconnecting and then the socket connection is closed. Afterwards, the rest of the runspace starts shutting down and getting disposed. Finally the list of online clients is cleared and the Connect and Disconnect buttons swap availability to be enabled and disabled.

#Close Window
$Window.Add_Closed({
    $TcpClient.Close()  
    $ClientConnection.user.PowerShell.EndInvoke($ClientConnection.user.Job)
    $ClientConnection.user.PowerShell.Runspace.Close()
    $ClientConnection.user.PowerShell.Dispose()
})

Because there is a chance that the window could be closed instead of using the Disconnect button, I want to be sure to handle that event and shut everything down gracefully. The main pieces are here to shutdown the socket and the runspace.

[void]$Window.showDialog()

This is the last piece of code in the runspace scriptblock and it is very vital as it is the code that starts up the UI for the client. I use ShowDialog() because it will bring up the window in the runspaces thread instead of using Show() which will cause the UI to lockup and become un-usable.

}).BeginInvoke()

And here is the last piece of code in the script! I close out the scriptblock and then at the same begin running the runspace using BeginInvoke(). Using BeginInvoke() calls the runspace asynchronously instead of synchronously which allows the console that it is called from to be free from waiting for the runspace to finish, which would have happened had I used Invoke().

When the code is run, you see this:

. .\Start-PoshChatClient.ps1

image

A nice clean chat client that you can now use to connect to the chat server that was shown in the previous article.

image

Remember, you can download PoshChat here and if you have any feature request or find bugs, to report those here.

Posted in powershell, scripts | Tagged , , , | Leave a comment

PoshChat 1 of 2: Building a Chat Server Using PowerShell

As you have seen in my previous article, I wrote and published version 0.9 of PoshChat, my implementation of a PowerShell client/server chat room. As promised, this is part 1 of  a 2 part article series detailing how I wrote each piece of PoshChat. For this article, I will be talking about what I considered to be the more complex and challenging piece: the chat server that will host all of the connections and act as the chat room for everyone. But before I dive into the code, I want to break down what I thought were the key things that the server needed to do before I could consider it to be a usable product.

Requirements for server:

  • Ensure a constant listener is active to handle new client connections
  • Spawn a new runspace for each new client connection for message listening and relaying
  • Set up a message queue to handle incoming messages in a first in, first out basis
  • Handle disconnects from clients

So with that out of the way, time to dive into some code and see where we end up!

#No Prompt
Function Global:Prompt {[char]8}
Clear-Host
##Create Globally synchronized hash tables and queue to share across runspaces
#Used for initial connections
$Global:sharedData = [HashTable]::Synchronized(@{})
#Used for managing client connections
$Global:ClientConnections = [hashtable]::Synchronized(@{})
#Used for managing the queue of messages in an orderly fashion
$Global:MessageQueue =  [System.Collections.Queue]::Synchronized((New-Object System.collections.queue))
#Used to manage incoming client messages
$Global:ClientHash = [HashTable]::Synchronized(@{})
#Removal Queue
$Global:RemoveQueue =  [System.Collections.Queue]::Synchronized((New-Object System.collections.queue))

#Set up timer
$Timer = New-Object Timers.Timer
$timer.Enabled = $true
$Timer.Interval = 1000 

First I am setting up all of the items I need to start up the chat server. I configure a global prompt that basically is no prompt at all. Once that is set up, the next piece of code is really what I consider to be the ‘backbone’ of the whole server. That is I am using synchronized hashtables and synchronized queue objects to handle all of my data and traffic on the server. Unfortunately, I will not be diving deep into using these as there are many more things to discuss on the server code. But I will try to drum up an article later on that shows both of these objects in use.

These are very exciting because when synchronized, these objects can be shared between runspaces and keep a live object between the runspaces as well. By runspace, I mean a manually created runspace, not a ‘constrained runspace’ that gets created when you use Start-Job or Invoke-Command as they will not work. Each of these synced objects have a special purpose in the server operations.

  1. SharedData (Synced Hashtable)
    1. Used for initial connections and also helps in making sure that duplicate usernames are not being used.
  2. ClientHash (Synced Hashtable)
    1. Used to handle each client connection that is made with the server. Listens for incoming messages from each client and also used to relay messages to each client from the server. This is also used to close out the socket connection when the client exits the server.
  3. ClientConnections (Synced Hashtable)
    1. Used to handle each runspace that is created for each client and its handle that is created when invoking the runspace. This makes it easier to close out the runspace once a client exits the server.
  4. MessageQueue (Synced Queue)
    1. This is a first in, first out queue object that helps manage message traffic by making sure that each message it processed as it comes in cleanly.
  5. RemoveQueue (Synced Queue)
    1. This queue object is what processes each client that disconnects from the server. It uses a first in, first out method to ensure that each client is processed cleanly.

Up next in this chunk of code is a timer object that I have set up to perform a tick every 1 second. This will be useful in the next chunk of code that sets up some events to monitor this object and each tick to perform a set of actions that will give the server more flexibility to perform most of its tasks.

#Timer event to track client connections and remove disconnected clients
$NewConnectionTimer = Register-ObjectEvent -SourceIdentifier MonitorClientConnection `
-InputObject $Timer -EventName Elapsed -Action { 
    While ($RemoveQueue.count -ne 0) {    
        $user = $RemoveQueue.Dequeue()
        ##Close down the runspace
        $ClientConnections.$user.PowerShell.EndInvoke($ClientConnections.$user.Job)
        $ClientConnections.$user.PowerShell.Runspace.Close()
        $ClientConnections.$user.PowerShell.Dispose()          
        $ClientConnections.Remove($User)                          
        $Messagequeue.Enqueue("~D{0}" -f $user)   
    }   
}

The first event that uses the timer object I created earlier to track disconnected clients. You can see that I use my synced queue ($RemoveQueue) object to process each client being removed from the server. The Dequeue() method not only outputs the client, but also removes it from the queue itself, which is very handy. In this case, it handles the username of the client connection.  I also use my synced hashtable ($ClientConnections) which works with the outputted item from the queue to close out runspace running the client connection. After which another synced hashtable ($MessageQueue) then takes the username and queues it up to be broadcasted out to each client.

#Timer event to track for new incoming connections and to kick off seperate jobs to track messages 
$NewConnectionTimer = Register-ObjectEvent -SourceIdentifier NewConnectionTimer `
-InputObject $Timer -EventName Elapsed -Action {
    If ($ClientHash.count -lt $SharedData.count) {
        $sharedData.GetEnumerator() | ForEach {
            If (-Not ($ClientHash.Contains($_.Name))) {
                #Spin off new job and add to ClientHash
                $ClientHash[$_.Name]=$_.Value               
                $User = $_.Name
                $Messagequeue.Enqueue(("~C{0}" -f $User))
                
                #Kick off a job to watch for messages from clients
                $newRunspace = [RunSpaceFactory]::CreateRunspace()
                $newRunspace.Open()
                $newRunspace.SessionStateProxy.setVariable("shareddata", $shareddata)
                $newRunspace.SessionStateProxy.setVariable("ClientHash", $ClientHash)
                $newRunspace.SessionStateProxy.setVariable("User", $user)
                $newRunspace.SessionStateProxy.setVariable("MessageQueue", $MessageQueue)               
                $newRunspace.SessionStateProxy.setVariable("RemoveQueue", $RemoveQueue)
                $newPowerShell = [PowerShell]::Create()
                $newPowerShell.Runspace = $newRunspace   
                $sb = {
                    #Code to kick off client connection monitor and look for incoming messages.
                    $client = $ClientHash.$user
                    $serverstream = $Client.GetStream()
                    #While client is connected to server, check for incoming traffic
                    While ($True) {                        
                        [byte[]]$inStream = New-Object byte[] 10025
                        $buffSize = $client.ReceiveBufferSize
                        $return = $serverstream.Read($inStream, 0, $buffSize)
                        If ($return -gt 0) {
                            $Messagequeue.Enqueue([System.Text.Encoding]::ASCII.GetString($inStream[0..($return - 1)]))
                        } Else {
                            $shareddata.Remove($User)
                            $clienthash.Remove($User)                   
                            $RemoveQueue.Enqueue($User)
                            Break
                        }
                    }
                }
                $job = "" | Select Job, PowerShell
                $job.PowerShell = $newPowerShell
                $Job.job = $newPowerShell.AddScript($sb).BeginInvoke()
                $ClientConnections.$User = $job                                             
            }
        }
    }
}

This is a very meaty piece of code that handles each client connection and is also based on the timer event. The synced hashtable ($SharedData) that has data added to it in later code first compares itself to another synced hashtable ($Clienthash) to make sure that the given username is not a duplicate to prevent confusion on the clients and also in processing disconnecting sessions.

A message is then queued up that will be broadcasted out to all other clients alerting them of a new client connection. Next up is the creation of a new background runspace that will then handle that specific client connection. I also make sure that all of my synced objects are available in that new runspace by using the SessionState.SetVariable() method.

The scriptblock that I create and is later supplied to the runspace contains all of the code needed to listen on that connection for messages being sent to it from the client. I use a byte[] buffer to handle the bits coming in from the client. To translate that message into something human readable, I make use of the System.Text.Encoding  class to handle the translation. Once the message has been translated, it is then queued up to be broadcasted to the other clients. The $return variable is used to determine if there is an actual message (greater than 0) otherwise if a 0 is returned, it means that the client has disconnected from the server and will then be processed by closing out the local connection and then the username is added to the $RemoveQueue to be processed.

Lastly, the runspace itself gets invoked using BeginInvoke() and that output is saved to a variable and finally that and the runspace itself is saved to the $ClientConnections hashtable. This will be used to handle and remove the runspace if the client connection is closed.

#Timer event to track for new incoming messages and broadcast message to all connected clients
$IncomingMessageTimer = Register-ObjectEvent -SourceIdentifier IncomingMessageTimer `
-InputObject $Timer -EventName Elapsed -Action {
    While ($MessageQueue.Count -ne 0) {
        $Message = $MessageQueue.dequeue() 
        Switch ($Message) {
            {$_.Startswith("~M")} {
                #Message
                $data = ($_).SubString(2)
                $split = $data -split ("{0}" -f "~~")
                Write-Host ("{0} >> {1}: {2}" -f (Get-Date).ToString(),$split[0],$split[1])
            }
            {$_.Startswith("~D")} {
                #Disconnect
                Write-Host ("{0} >> {1} has disconnected from the server" -f (Get-Date).ToString(),$_.SubString(2))
            }
            {$_.StartsWith("~C")} {
                #Connect
                Write-Host ("{0} >> {1} has connected to the server" -f (Get-Date).ToString(),$_.SubString(2))            
            }
            Default {
                Write-Host ("{0} >> {1}" -f (Get-Date).ToString(),$_)
            }
        }        
        #Broadcast message
        $Clienthash.GetEnumerator() | ForEach {
            $Broadcast = $Clienthash[$_.Name]
            $broadcastStream = $broadcast.GetStream()
            $string = $Message
            $broadcastbyte = ([text.encoding]::ASCII).GetBytes($String)
            $broadcastStream.Write($broadcastbyte,0,$broadcastbyte.Length)
            $broadcastStream.Flush()            
        }
    }
}

This is the last timer event set up on the server and this one handles all of the message traffic and broadcasting of messages to all connected clients as well as displaying each message on the console running the server script. A check is performed each second to see if there are messages queued up and ready for sending to other clients. Once a message is queued up, it then begins the process of first dequeueing the message and then deciding what type of message has been sent. I decided to use the “~” followed by a letter which makes it easier to determine what type of message it is via using Substring() method.

  1. ~D
    1. Used for Disconnected clients
  2. ~C
    1. Used for incoming client connections
  3. ~M
    1. Used for messages being sent from clients to other clients

Once the type of message is determined, then some parsing is used to first remove the first 2 characters that were used to determine the type of message. Then it determines what the username and message is by using the –Split operator. I chose “~~” as the likelihood of it ever being in a message was a risk that I could accept. Although looking back now, I will probably change it to use LastIndexOf() instead to better ensure that nothing crazy will happen. Smile

Once the message has been split up, I then broadcast the message to all clients by using the $ClientHash synced hashtable and created an object to send the stream of bytes to each client and flushing the stream afterwards. Before the data can be streamed, it must first be converted to bytes, which is what the  Write() method will accept. This continues until all clients have received the message.

$Timer.Start()

#Initial runspace creation to set up server listener 
$newRunspace = [RunSpaceFactory]::CreateRunspace()
$newRunspace.Open()
$newRunspace.SessionStateProxy.setVariable("sharedData", $sharedData)
$newPowerShell = [PowerShell]::Create()
$newPowerShell.Runspace = $newRunspace
$sb = {
 $Listener = [System.Net.Sockets.TcpListener]15600
 $listener.Start()
 [console]::WriteLine("{0} >> Server Started" -f (Get-Date).ToString())
 while($true) {
    [byte[]]$byte = New-Object byte[] 1024
    $client = $listener.AcceptTcpClient()
    $stream = $client.GetStream()
    Do {
        #Write-Host 'Processing Data'
        Write-Verbose ("Bytes Left: {0}" -f $Client.Available)
        $Return = $stream.Read($byte, 0, $byte.Length)
        $String += [text.Encoding]::Ascii.GetString($byte[0..($Return-1)])
       
    } While ($stream.DataAvailable)
        If ($SharedData.Count -lt 30) { 
            $SharedData[$String] = $client           
            #Send list of online users to client
            $users = ("~Z{0}" -f ($shareddata.Keys -join "~~"))
            $broadcastStream = $client.GetStream()
            $broadcastbyte = ([text.encoding]::ASCII).GetBytes($users)
            $broadcastStream.Write($broadcastbyte,0,$broadcastbyte.Length)
            $broadcastStream.Flush()             
            $String = $Null
        } Else {
            #Too many clients, refuse connection
        }
 }#End While
}
$handle = $newPowerShell.AddScript($sb).BeginInvoke()

Now we are at the final piece of code in this script! Here I start my timer object using the Start() method. This is the piece where the actual listener for the server is being used in a background runspace. I only need to supply my $SharedData hashtable for this runspace and then in my scriptblock, create the TCPListener object with a specified port (15600 in this case) I then start up the listener which you can verify that it is in fact listening by running the following command:

netstat -ano | Select-String 15600

image

Yep, it’s listening.

So now we set up another byte[] buffer to handle incoming clients. Using the GetStream() method blocks the connection until data is received (client connecting to server) and then proceeds to process the client connection. Each new client connection appends a ~Z which tells the server to first collect a list of connected usernames as an array and then join each item using the –Join operator and joining them using “~~”. That data is then sent back to the client prior after it has been added to the $SharedData hashtable that will then be processed and spawned as a new runspace that will handle that connection. Before the message can be send to the client, it has to be converted from an ASCII string to a byte[] array. Once that is done, it is then streamed to the client via the open connection. The runspace itself is invoked like the other runspaces by using BeginInvoke() with the return handle saved to $handle (this will be used in a later update to allow the server to be stopped gracefully).

So that is all to the server portion of the chat program. While I didn’t go into great detail with everything that is used here, I hope that I hit on enough items to give you a general idea about how everything is being used for this server. As mentioned earlier, I will look to talk some more about the synced hashtable and queue objects in a later article and provide some examples for each.

The next article will talk about the client side of the PoshChat program and I hope to have that ready in a few days. See you then!

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