RSS

Embedded Systems

PowerShell for Developers with Admin Tasks


Prompt

The prompt is important in interactive use. You can put pretty much anything you want there. I like to know where I am, so the current directory is a no-brainer. I don't care much about the host because this information can be displayed in the console window title (it doesn't change from one command to the next normally). I also like to know the time a command completed because I often run several lengthy commands in multiple console windows over and over (for example, for deployment after a code change) and I want to be sure of the last time I ran a particular command. There is nothing remarkable about these requirements. The only twist I added to my prompt is that I don't want to display the full path. As a developer, I often work with multiple workspaces with deep directory structures. If I display the full path, it takes a significant part of the line and doesn't leave enough space to type the command itself.

Displaying just the last part of the path doesn't work for me either because I often work on multiple copies of the same project from different branches in parallel. The solution I came up with is to compress most of the long prefix of common trees into a unique mnemonic abbreviation. For example, when I work in WebQA tree, I collapse the path: C:\r\Release\WebQA to [WQA]. A path like c:\r\Release\WebQA\Services\Tools\TfsTools shows up as [WQA]\Services\Tools\TfsTools and is very easy to distinguish from the same project in the GameService_GetGamesPage branch, which has the path C:\r\Branches\Services\GameService_GetGamesPage\Services\Tools\TfsTools but shows up as [GS]\Services\Tools\TfsTools (see Figure 2).


Figure 2: A prompt made easier with PowerShell.

The trick is to always have good abbreviations for the trees and branches I currently work on. To set the prompt, you need to have a Prompt() function in your profile that returns a string. In the code below, I keep a hash of path prefixes to abbreviations and I check whether the current working directory matches any of the prefixes — and add the current time, too.

function Prompt
{ 
    $hash = @{
      "c:\r\branches\services\gameservice_getgamespage" = "[GS]";
      "c:\r\release\webqa" = "[WQA]"; 
      "c:\r\web" = "[WEB]"; 
      "c:\users\gigi" = "~"; }

    $result = $PWD.Path
    $fullPath = $PWD.Path.ToLower()
    foreach ($name in $hash.Keys)
    {
        if ($fullPath.StartsWith($name))
        {            
            $path = $PWD.Path.Substring($name.Length);
            if ($path[0] -eq "\")
            {
                $path = $path.Substring(1)
            }
            $result = $hash[$name] + " " + $path;
        }
    }
    (get-date -f HH:mm:ss) + " " + $result +"> "
}

Text Searching (grepping)

PowerShell is all about objects, but it doesn't neglect text processing. The equivalent of the UNIX/Linux workhorse grep is Select-String. For some reason, the PowerShell designers didn't alias it to grep, so it is not as discoverable. I defined my own alias to it: set-alias grep Select-String.

When working with objects, the PowerShell equivalent to Select-String/grep is Select-Object. This is normally more powerful and robust. I defined a couple of functions that help me grep over a bunch of objects by converting each object to its text representation, then using Select-String on the result. I aliased this concoction as sgrep:

# Convert a collection object to a collection of their textual representation
function Objects2Strings
{
    $input | ForEach-Object $_ { $_.ToString() }
}

# Convert input to strings (good for searching in filenames without digging into file contents)
function GrepFromPipe
{
    $input | Objects2Strings | select-string -Pattern $args[0] 
}

set-alias sgrep GrepFromPipe

Source Control

At my company, we use Microsoft TFS for source control. I'm not very happy with this choice. It is a very sophisticated and mature product with lots of features and great GUI, but it feels like it's over-engineered and crumbles under its own complexity. Very fundamental stuff sometimes doesn't work. For example, under some circumstances, TFS gets confused and its list of pending changes is sometimes a little off. I created a command-line tool in C# called "TfsHelper" that uses the TFS API to smooth things out, and I employ PowerShell to run it:

function tfh
{
    pushd .
    cd "C:\r\Release\WebQA\Services\Tools\TfsTools\TfsHelper\bin\Debug"
    .\TfsHelper.exe $args[0] $args[1] $args[2] $args[3] $args[4] 
         $args[5] $args[6] $args[7] $args[8] $args[9] $args[10]
         $args[11] popd
}

# Merge from WebQA to target branch
function merge-to
{
    tfh mu $/Release/WebQA ("$/Branches/" + $args[0]) $args[1] $args[2]
        $args[3]  $args[4]  $args[5] $args[6] $args[7] $args[8] $args[9] 
        $args[10] $args[11]
}

# Merge from target branch to WebQA
function merge-from
{
    tfh mu ("$/Branches/" + $args[0]) $/Release/WebQA $args[1] $args[2]   
        $args[3] $args[4] $args[5] $args[6] $args[7] $args[8] $args[9] 
        $args[10] $args[11]
}

Deployment to Test Environment

Another very common task when developing distributed services is to deploy a service or a set of services to a test environment. Ideally, you can do most of your development and testing on your local machine; but when you get to the integration testing phase, you must test how your code interacts with databases, services, and other programs. I currently work on a complicated service called the "game service" (responsible for managing the state of all the games and players in the Roblox universe) that consists of a distributed ASP.NET MVC front-end Web service and a back-end WCF service that need to interact with several other services and with a set of game servers that actually run the games. To test the operation of the whole service, I need to deploy to several test environments. Doing this manually consists of 20 to 30 steps that include building the latest versions of each component, setting the configuration database properly, connecting to remote machines, copying the latest code, uninstalling and reinstalling the latest services, cleaning up Windows event log files, pointing IIS to the latest code, and more.

After doing this manually for a while (and getting at least one step wrong more often than not), I automated the process and created two PowerShell scripts called DeployApiService.ps1 and DeployWcrfService.ps1. These scripts are pretty specific to the Roblox environment, so I will not present them here; but here is PowerShell code to deploy the game service components in a safe and repeatable manner. It accepts the host names of the API box (running the ASP.NET MVC service), and the WCF box and the parent directory that contains the latest code. And it can deploy to different environments by providing the proper host names to each environment:

function DeployGameService($apiBox, $wcfBox, $localTargetParentDir)
{
    $branch = "GameService_GetGamesPage"

    # Clear API service event log
    ClearEventLog RGS2.Roblox.Games.Service $apiBox

    # Deploy the API Service
    $cmd = Join-Path $roblox_dir 
           "Release\WebQA\Services\Tools\ServiceDroppr\DeployApiService.ps1"
    & $cmd "c:\r\publish\Roblox.Games.Service" 
           "\\$apiBox\c$\Roblox\Roblox.Games.Service" "\\$apiBox"    

    # Clear WCF service event log
    ClearEventLog RGS2.Roblox.Games.Service $wcfBox
    
    # Deploy the WCF Service
    $cmd = Join-Path $roblox_dir 
           "Release\WebQA\Services\Tools\ServiceDroppr\DeployWcfService.ps1"    
    $sourceDir = "C:\r\Branches\Services\" + $branch + 
           "\Services\Roblox.Games\Roblox.Games.WcfService\bin\Debug"
    #$sourceDir = 
 "C:\r\Release\WebQA\Services\Roblox.Games\Roblox.Games.WcfService\bin\Debug"
    $targetParentDir = "\\$wcfBox\" + $localTargetParentDir
    $hostName = "\\$wcfBox"
    echo "sourceDir: $sourceDir", 
         "targetParentDir: $targetParentDir", "hostName: $hostName"  
    
    $serviceName = (Split-Path -Leaf $targetParentDir).Split('.')[0..1] 
       -join '.' echo "serviceName: $serviceName"
        & $cmd $sourceDir $targetParentDir $hostName  
}

The source branch I'm working with is embedded inside the function itself because it is common to all invocations. Here is the ClearEventLog() function used by DeployGameService() that remotely clears the event log of the remote machines:

# If the log exists clear it, otherwise create a new one
function ClearEventLog($log, $computerName)
{
    if ((Get-Eventlog -ComputerName $computerName -List | 
         Select-Object -ExpandProperty Log) -Contains "$log")
    {
        Clear-EventLog -LogName $log -ComputerName $computerName
    }
    else
    {
        New-EventLog  -LogName $log -Source $log -ComputerName $computerName
    }
}

Another helper function is ResetGameService(). It takes a log file name and a list of computers (host names). On each computer, it clears the event log, stops the WCF component of the game service, and restarts it. I use it a lot when I want to rerun some experiment from scratch, but don't need to deploy new code.

 function ResetGameService
{
    $log = $args[0]
    $computers = $args[1..$args.Count]

    # Stop target place
    #https://games.api.sitetest4.roblox.com/StopPlace/?apiKey=cef349f4-98b1-416b-bacc-4cd26d798987&placeId=1741

    # Stop WCF service
    $service = Get-Service -Name Roblox.Games -ComputerName $computers[0]
    $service.Stop()

    # Clean all event logs
    $computers | ForEach-Object { ClearEventLog $log $_ }    
    
    # Start WCF service
    $service.Start()
}

Finally, here is how I deploy or reset the game service on a particular test environment. Instead of remembering the host names of each environment and invoking DeployGameService() or ResetGameService(), I created a little function to do it for me.

dgs3 deploys to test environment #3, dgs4 deploys to test environment #4, cl3 cleans up test environment #3, and cl4 cleans up test environment #4:

function cl4
{
    ResetGameService RGS2.Roblox.Games.Service sm-app3.roblox.local sm-web2.roblox.local
    ClearEventLog RobloxGames sm-web1.roblox.local
}

function cl3
{
    ResetGameService RGS2.Roblox.Games.Service 10.10.3.218 10.10.3.10
    ClearEventLog RobloxGames 10.10.3.190
}

function dgs3 
{ 
    cl3; DeployGameService 10.10.3.10 10.10.3.218   
    c$\Roblox\DistributedServices\Roblox.Games.Service 
}

function dgs4 
{ 
     DeployGameService sm-web2.roblox.local sm-app3.roblox.local 
     c$\Roblox\Roblox.Games
}

Conclusion

PowerShell was designed with the administrator in mind, but it can be amazingly useful in the hands of developers, too. If you find yourself frequently doing tedious and error-prone tasks, consider using PowerShell to make your life easier. PowerShell is powerful, well-designed, and has a good ecosystem and community. It will also be around for a long time given its adoption, ongoing development, and integration with Microsoft products.


Gigi Sayfan specializes in cross-platform object-oriented programming in C/C++/ C#/Python/Java with emphasis on large-scale distributed systems, and is a long-time contributor to Dr. Dobb's.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Best of the Web

First C Compiler Now on Github

The earliest known C compiler by the legendary Dennis Ritchie has been published on the repository.

Quick Read

HTML5 Mobile Development: Seven Good Ideas (and Three Bad Ones)

HTML5 Mobile Development: Seven Good Ideas (and Three Bad Ones)

Quick Read

Building Bare Metal ARM Systems with GNU

All you need to know to get up and running... and programming on ARM

Quick Read

Amazon's Vogels Challenges IT: Rethink App Dev

Amazon Web Services CTO says promised land of cloud computing requires a new generation of applications that follow different principles.

Quick Read

How to Select a PaaS Partner

Eventually, the vast majority of Web applications will run on a platform-as-a-service, or PaaS, vendor's infrastructure. To help sort out the options, we sent out a matrix with more than 70 decision points to a variety of PaaS providers.

Quick Read


More "Best of the Web" >>

Video