Windows PowerShell and Named Pipes

A named pipe is a stream-based mechanism for inter-process communication (IPC).  The .NET Framework has two types for allow you to use named pipes:

MSDN describes named pipes like so:

Named pipes provide one-way or duplex pipes for communication between a pipe server and one or more pipe clients. Named pipes can be used for interprocess communication locally or over a network. A single pipe name can be shared by multiple NamedPipeClientStream objects.

Being a .NET feature, named pipes are easily usable from PowerShell giving you a mechanism to communicate between separate PowerShell processes on the same machine or between different machines.  Note: another way of communicating between separate PowerShell processes in a very decoupled way is to use the Microsoft Message Queue (MSMQ) but that’s a topic for another blog post.

For this demonstration, I’ve chosen to implement BlackJack.  First a disclaimer, I’m no expert in BlackJack.  The implementation is from memory and is just basic BlackJack. There’s no support for doubling down, splitting, insurance, etc.  The point is to show how you can use named pipes to communicate between two PowerShell processes even from two different machines.

Before we get into the code, here is what a game session looks like:

BlackJackDealer:

PS C:\> .\BlackJackDealer.ps1
BlackJack dealer started
Waiting for client connection
Connection established
Connected to Keith.
Starting new game -----------------------------------------
Dealer's hand is Queen of Diamonds, hole card
Keith hand is King of Diamonds, 5 of Spades
Keith drew a 8 of Clubs, updated hand King of Diamonds, 5 of Spades, 8 of Clubs
DEALER's hand is Queen of Diamonds, King of Hearts
Keith busts with King of Diamonds, 5 of Spades, 8 of Clubs
DEALER wins with Queen of Diamonds, King of Hearts

BlackJackPlayer:

PS C:\> .\BlackJackPlayer.ps1 Keith-PC
Enter your name: Keith
BlackJack player connecting to dealer
Connected to dealer
Connected to Keith.
Starting new game -----------------------------------------
Deck empty, reshuffling deck
Dealer's hand is Queen of Diamonds, hole card
Keith hand is King of Diamonds, 5 of Spades
Enter H (hit me) or S (stand): h
Keith drew a 8 of Clubs, updated hand King of Diamonds, 5 of Spades, 8 of Clubs
DEALER's hand is Queen of Diamonds, King of Hearts
Keith busts with King of Diamonds, 5 of Spades, 8 of Clubs
DEALER wins with Queen of Diamonds, King of Hearts
Deal again? Y (yes) N (no):

And that’s why I don’t gamble.  Smile

Let’s get to the implementation which you can download in whole from by OneDrive via the two PowerShell scripts BlackJackDealer.ps1 and BlackJackPlayer.ps1.

First up is the dealer script:

 
$suits = 'Clubs','Diamonds','Hearts','Spades'
$ranks = 'Ace','2','3','4','5','6','7','8','9','10','Jack','Queen','King'

function GetShuffledDeck {
    $deck = 0..3 | Foreach {$suit = $_; 0..12 | Foreach { 
                      $num = if ($_ -eq 0) {11} elseif ($_ -ge 10) {10} else {$_ + 1}
                      [pscustomobject]@{Suit=$suits[$suit];Rank=$ranks[$_];Value=$num}}
                   }
    for($i = $deck.Length - 1; $i -gt 0; --$i) {
        $rndNdx = Get-Random -Maximum ($i+1)
        $temp = $deck[$i]
        $deck[$i] = $deck[$rndNdx]
        $deck[$rndNdx] = $temp
    }
    $deck
}

function GetValueOfHand($hand) {
    $sum = ($hand | Measure-Object Value -Sum).Sum
    if ($sum -gt 21) {
        $sum = ($hand | Foreach {if ($_.Value -eq 11) {1} else {$_.Value}} | Measure-Object -Sum).Sum
    }
    $sum
}

function IsHandBust($hand) {
    (GetValueOfHand $hand) -gt 21
}

function IsHandBlackJack($hand) {
    if ($hand.Length -ne 2) { return $false }
    (GetValueOfHand $hand) -eq 21
}

function DumpHand($hand) {
    $cards = $hand | Foreach {DumpCard $_}
    $OFS = ', '
    "$cards"
}

function DumpCard($card) {
    "$($card.Rank) of $($card.Suit)"
}

$cardNdx = -1
$deck
function DealCard {
    if ($cardNdx -lt 0) {
        WriteToPipeAndLog 'Deck empty, reshuffling deck' | Out-Null
        $script:deck = GetShuffledDeck
        $script:cardNdx = $deck.Length - 1
    } 
    $deck[$script:cardNdx--]
}

$pipeWriter
function WriteToPipeAndLog($msg) {
    $msg 
    $pipeWriter.WriteLine($msg)
}

$npipeServer = new-object System.IO.Pipes.NamedPipeServerStream('BlackJack', 
                              [System.IO.Pipes.PipeDirection]::InOut)
try {
    'BlackJack dealer started'
    'Waiting for client connection'
    $npipeServer.WaitForConnection()
    'Connection established'

    $pipeReader = new-object System.IO.StreamReader($npipeServer)
    $script:pipeWriter = new-object System.IO.StreamWriter($npipeServer)
    $pipeWriter.AutoFlush = $true

    $playerName = $pipeReader.ReadLine()
    WriteToPipeAndLog "Connected to $playerName."

    # Outer game loop
    while (1)
    {
        WriteToPipeAndLog 'Starting new game -----------------------------------------'
        $playerHand  = @(DealCard)
        $dealerHand  = @(DealCard)
        $playerHand += DealCard
        $dealerHand += DealCard

        WriteToPipeAndLog "Dealer's hand is $(DumpCard $dealerHand[0]), hole card"
        WriteToPipeAndLog "$playerName hand is $(DumpHand $playerHand)"

        $playerDealtBlackJack = IsHandBlackJack $playerHand
        $dealerDealtBlackJack = IsHandBlackJack $dealerHand

        if ($playerDealtBlackJack -and $dealerDealtBlackJack) {
            WriteToPipeAndLog "Both the Dealer and $playerName get BLACKJACK. The game is a push"
        }
        elseif (IsHandBlackJack $playerHand) {
            WriteToPipeAndLog "$playerName gets BLACKJACK and wins!"
        }
        elseif (IsHandBlackJack $dealerHand) {
            WriteToPipeAndLog "Dealer gets BLACKJACK and wins!"
        }
        else {
            # Let's play this hand
            $dealerBusts = $false
            $playerBusts = $false

            # Player's turn
            while (1) {
                $stand = $false
                $invalidKey = $false
                $pipeWriter.WriteLine("YOURMOVE")
                $command = $pipeReader.ReadLine()
                switch ($command) {
                    "H"     { }
                    "S"     { $stand = $true }
                    default { $invalidKey = $true }
                }

                if ($invalidKey) {
                    WriteToPipeAndLog "Sorry $playerName, didn't recognize command: $command"
                    continue
                }
                elseif ($stand) {
                    WriteToPipeAndLog "$playerName stands with hand $(DumpHand $playerHand)"
                    break
                }
                else {
                    $newCard = DealCard
                    $playerHand += $newCard
                    WriteToPipeAndLog "$playerName drew a $(DumpCard $newCard), updated hand $(DumpHand $playerHand)"
                    if (IsHandBust $playerHand) {
                        $playerBusts = $true
                        break
                    }
                }
            }

            # Dealer's turn
            WriteToPipeAndLog "DEALER's hand is $(DumpHand $dealerHand)"
            if (!$playerBusts) {
                do {
                    $dealerSum = GetValueOfHand $dealerHand
                    if ($dealerSum -gt 21) {
                        $dealerBusts = $true
                        break;
                    }
                    elseif ($dealerSum -ge 17) {
                        WriteToPipeAndLog "DEALER stands with $(DumpHand $dealerHand)"
                        break
                    }

                    $newCard = DealCard
                    $dealerHand += $newCard
                    WriteToPipeAndLog "Dealer draws $(DumpCard $newCard), updated hand $(DumpHand $dealerHand)"

                    Start-Sleep -Seconds 1
                } while (1)
            }

            # Determine who won
            if ($playerBusts) {
                WriteToPipeAndLog "$playerName busts with $(DumpHand $playerHand)"
                WriteToPipeAndLog "DEALER wins with $(DumpHand $dealerHand)"
            }
            elseif ($dealerBusts) {
                WriteToPipeAndLog "DEALER busts with $(DumpHand $dealerHand)"
                WriteToPipeAndLog "$playerName wins with $(DumpHand $playerHand)"
            }
            else {
                $dealerSum = GetValueOfHand $dealerHand
                $playerSum = GetValueOfHand $playerHand
                if ($dealerSum -gt $playerSum) {
                    $msg = "DEALER wins with $(DumpHand $dealerHand)"
                }
                elseif ($playerSum -gt $dealerSum) {
                    $msg = "$playerName wins with $(DumpHand $playerHand)"
                }
                else {
                    $msg = 'The game is a push'
                }
                WriteToPipeAndLog $msg
            }
        }

        $pipeWriter.WriteLine('ROUNDOVER')
        $pipeWriter.WriteLine('NEWDEAL')
        $command = $pipeReader.ReadLine()
        if ($command -eq 'EXIT') { break }
    }

    Start-Sleep -Seconds 2
}
finally {
    'Game exiting'
    $npipeServer.Dispose()
}

Note lines 62 – 71 of the dealer script is where I setup the server side of the named pipe.  It sits and waits at the WaitForConnection() call for a player to join the game.  The named pipe is a low-level, byte-oriented stream.  To make it simple to pass string messages and commands back and forth, I decorate the PipeStream with a StreamReader and StreamWriter.  I use those to write and read write strings to and from the client.  Once a player has joined, the game loop proceeds to send instructions by using the StreamWriter’s WriteLine() method to the player and it uses the StreamReader’s ReadLine() to receive the player’s instructions as a string e.g. “H” for hit and “S” for stand.

And here is the simpler, player script:

param ($ComputerName = '.')

$npipeClient = new-object System.IO.Pipes.NamedPipeClientStream($ComputerName, 'BlackJack', [System.IO.Pipes.PipeDirection]::InOut,
                                                                [System.IO.Pipes.PipeOptions]::None, 
                                                                [System.Security.Principal.TokenImpersonationLevel]::Impersonation)
$pipeReader = $pipeWriter = $null
try {
    $playerName = Read-Host 'Enter your name'
    'BlackJack player connecting to dealer'
    $npipeClient.Connect()
    'Connected to dealer'

    $pipeReader = new-object System.IO.StreamReader($npipeClient)
    $pipeWriter = new-object System.IO.StreamWriter($npipeClient)
    $pipeWriter.AutoFlush = $true
 
    $pipeWriter.WriteLine($playerName)
    $pipeReader.ReadLine()
    
    # Game loop
    while (1) {
        # Hand loop
        while (1) {      
            while (($msg = $pipeReader.ReadLine()) -notmatch 'YOURMOVE|ROUNDOVER') {
                $msg
            }
            if ($msg -match 'ROUNDOVER') { break }
            $command = Read-Host 'Enter H (hit me) or S (stand)'
            $pipeWriter.WriteLine($command)
        }

        while (($msg = $pipeReader.ReadLine()) -notmatch 'NEWDEAL') {
            $msg
        }
        $res = Read-Host 'Deal again? Y (yes) N (no)'
        if ($res -eq 'N') { 
            $pipeWriter.WriteLine('EXIT')
            break 
        }
        else {
            $pipeWriter.WriteLine('NEWDEAL')
        }
    }
}
finally {
    'Game exiting'
    $npipeClient.Dispose()
}

Lines 3 – 15 in the player script is where I set up the client side of the named pipe and connect to the server. Note the [System.Security.Principal.TokenImpersonationLevel]::Impersonation is required to make the connection work between two different machines.

There is a fair amount to the logic of the game that has nothing to do with named pipes but the above should give you a quick primer on how to use named pipes in your application. In summary, it is pretty straightforward:

  1. Create NamedPipeServerStream
  2. Wrap the server’s PipeStream in StreamReader/StreamWriter objects if you want to read/write string messages.
  3. Have the server pipe WaitForConnection
  4. Create NamedPipeClientStream in client script
  5. Wrap the client’s PipeStream in StreamReader/StreamWriter objects if you want to read/write string messages.
  6. Call Connect() to connect to the server.
  7. Start using StreamWriter.WriteLine() and StreamReader.ReadLine() to pass messages back and forth between the server and the client.

Stay tuned.  My next goal is to show you what this looks like using preview version of  PowerShell V5 classes.

Advertisements
This entry was posted in .NET, PowerShell. Bookmark the permalink.

One Response to Windows PowerShell and Named Pipes

  1. Pingback: BlackJack, NamedPipes and PowerShell Classes – Oh My! | Keith Hill's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s