[note: all my scripts are now zip'd up together on this page]

This particular script probably isn't useful for very many people (it's useful for me for some test scenarios we have).  However, it's a good example of some various constructs that get repeated a good bit in my scripts, so I figured I would provide it here.

What's it do?  You point it at a TFS server, it asks TFS where to find WSS (specifically, the admin.asmx of WSS) and then asks WSS what languages it supports, then converts those to readable strings.

# get-tfs-wss-languages tkbgitvstfat01
1033 = English (United States)

Simple input (TFS location), simple output (locale info).

What's going on, though?  Since I'm a fan of write-debug, we can set $DebugPreference = 'Continue' and see some of what's going on under the covers.

# get-tfs-wss-languages tkbgitvstfat01
DEBUG: Normalized TFS server to http://tkbgitvstfat01:8080
DEBUG: Found admin.asmx URL for server of http://TKBGITVSTFAT01:17012/_vti_adm/admin.asmx
DEBUG: Setting SOAPAction header to http://schemas.microsoft.com/sharepoint/soap/GetLanguages
DEBUG: Sending to http://tkbgitvstfat01:17012/_vti_adm/admin.asmx request of <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetLanguages xmlns="
http://schemas.microsoft.com/sharepoint/soap/" />
</soap:Body>
</soap:Envelope>
DEBUG: Got back 435 bytes
DEBUG: Got back response string of <?xml version="1.0" encoding="utf-8"?><soap:Envelope
xmlns:soap="
http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><GetLanguagesResponse
xmlns="
http://schemas.microsoft.com/sharepoint/soap/"><GetLanguagesResult><Languages
xmlns=""><LCID>1033</LCID></Languages></GetLanguagesResult></GetLanguagesResponse></soap:Body></soap:Envelope>
DEBUG: Got GetLanguages response of <?xml version="1.0" encoding="utf-8"?><soap:Envelope
xmlns:soap="
http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><GetLanguagesResponse
xmlns="
http://schemas.microsoft.com/sharepoint/soap/"><GetLanguagesResult><Languages
xmlns=""><LCID>1033</LCID></Languages></GetLanguagesResult></GetLanguagesResponse></soap:Body></soap:Envelope>
DEBUG: Running regex <LCID>(.*?)</LCID> against content of <?xml version="1.0" encoding="utf-8"?><soap:Envelope
xmlns:soap="
http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><GetLanguagesResponse
xmlns="
http://schemas.microsoft.com/sharepoint/soap/"><GetLanguagesResult><Languages
xmlns=""><LCID>1033</LCID></Languages></GetLanguagesResult></GetLanguagesResponse></soap:Body></soap:Envelope>
DEBUG: Found match <LCID>1033</LCID>
DEBUG: Got languages response of 1033
1033 = English (United States)

How does it do it?  Lots of leveraging of other scripts. 

  • get-tfs-wss-languages.ps1 is the outer script
    • normalize-server-url.ps1 is the first script to get called.  It justs uses the framework.  It isn't really needed, but I thought it was a nice touch for easier calling in many situations.
    • get-tfs-wss-adminAsmxUrl.ps1 is the next to get called.  It finds the admin.asmx location via the TFS registration data
      • get-tfs-registrationdata.ps1 is used to fetch the registration data for the Wss toolId via the object model off the TeamFoundationServer object
        • get-tfs.ps1 is used to get the reference to the TeamFoundationServer object and easy access to the IRegistration service via a new ScriptProperty on it called Reg
    • Next get-wss-languages.ps1 is called since we have the admin.asmx location and want to call the GetLanguages service
      • In turn, it uses the call-webservice.ps1 script to actually call the GetLanguages web method via the admin.asmx service.  It uses the System.Net.WebClient for that work.

Wow - a total of 7 scripts get invoked, with a total of 4 "levels" of scripts getting invoked.  That probably seems like a lot, so let's go through them.  Each of them is pretty small and targeted (which is a good thing, we *like* the unix philsophy of small, composable entities), so it shouldn't be too bad.  I thought about explaining them in bottom-up order (so you already understand the lower level scripts as we tackle each one), but decided instead of a top-down drill-into-it approach.  If you find one way better than the other, please let me know - I'm not sure which is actually "better" / more grok-able for most people.

get-tfs-wss-languages.ps1

As the outer script, it's largely just calling other scripts.  The only "real work" it does is leveraging the CultureInfo class to convert the locale id we get back from WSS (like 1033 for en-us) to a more readable string.  This is pretty simple since CultureInfo has a constructor taking the locale id as an int and then has a property called DisplayName - perfect!

param ([string] $tfsServer = $(throw 'tfs server is required'))

$tfsServer = normalize-server-url $tfsServer
write-debug "Normalized TFS server to $tfsServer"
$adminAsmxUrl = get-tfs-wss-adminAsmxUrl $tfsServer
write-debug "Found admin.asmx URL for server of $adminAsmxUrl"
$languages = get-wss-languages $adminAsmxUrl
write-debug "Got languages response of $languages"

# convert to a more meaningful string with CultureInfo
$languages |
%{ '{0} = {1}' -f $_, (new-object 'globalization.cultureinfo' ([int]$_)).DisplayName }

normalize-server-url.ps1

As I said before, this script is actually optional, but it keeps me from having to type http:// and :8080 more often than otherwise, so I keep it around :)  It's a good example of using the client-side object model to check for the registered servers (persisted in the windows registry).  If we don't find it there, and it's not already a URL, we just guess it's the hostname.  I could definitely do more checking, but this is Good Enough For Me (tm). :)

param ([string] $serverName = $(throw 'serverName is required'))

# if it's already a valid URI, just return it back
if ([uri]::trycreate($serverName, [urikind]::absolute, [ref]$null))
{
return $serverName
}

# can we find it as a registered server?
[void][reflection.assembly]::loadwithpartialname('Microsoft.TeamFoundation.Client')
$uri = [Microsoft.TeamFoundation.Client.RegisteredServers]::GetUriForServer($serverName)
if ($uri -ne $null)
{
return $uri.AbsoluteUri
}

# guess it by way of assuming http and port 8080
# TODO: make sure $servername resolves
$guessedPath = "http://$($serverName):8080/"
if ([uri]::trycreate($guessedPath, [urikind]::absolute, [ref]$null))
{
return $guessedPath
}

throw "Failed to translate $serverName into a valid TFS server URI"

get-tfs-wss-adminAsmxUrl.ps1

This is another script that's largely just using another - we're just getting the TFS registration data for the registered service interfaces for "Wss" and looking for the URL associated with "WssAdminService".

param ([string] $tfsServer = $(throw 'tfs server is required'))

$wssTools = get-tfs-registrationdata $tfsServer 'Wss'

$wssTools |
select -expand ServiceInterfaces | %{ if ($_.Name -eq 'WssAdminService') { $_.Url } }

get-tfs-registrationdata.ps1

This script leverages the TeamFoundationServer object model, in particular the IRegistration service.  It makes a single method call to get the registration data.  If you don't provide a particular tool id, the empty-string default means "get all registration data".

param (
[string] $serverUrl = $(throw 'serverUrl parameter is required'),
[string] $toolId = ''
)

$tfs = get-tfs $serverUrl
$tfs.reg.GetRegistrationEntries($toolId)

get-tfs.ps1

This only changed by 1 line from the original version, since I wanted to use IRegistration.  Here's the unified diff showing the line that I added (the + isn't in the file, it indicates that it was an add).  I won't bother repeating the entire script here since I already have that original post to talk about it.  However, I think it's "neat" and shows the "power" of PowerShell that I only had to add one entry into one data structure to get a new property that does what I want :)

# ..\diff.exe -u1 get-tfs.ps1.orig get-tfs.ps1
--- get-tfs.ps1.orig Wed Feb 14 06:53:19 2007
+++ get-tfs.ps1 Mon Feb 12 12:06:38 2007
@@ -14,2 +14,3 @@
('GSS', 'Microsoft.TeamFoundation', 'Microsoft.TeamFoundation.Server.IGroupSecurityService'),
+ ('REG', 'Microsoft.TeamFoundation', 'Microsoft.TeamFoundation.Server.IRegistration')
)

get-wss-languages.ps1

This is the "real worker" among these scripts - he needs to make a web service call to WSS (using "raw" XML in my case, other approaches are available) and then parses the XML response to pick out the particular data we care about (in this case, the LCID values, the locale id's).  Note that I didn't have any parameters to pass in for this case, but I could have formatted some into the XML if needed (there are other scripts where I do that).  It leverages call-webservice to do the actual network calls.

param (
[uri] $wssAdminAsmxUrl = $(throw 'wssAdminAsmxUrl is required')
)

$request = '<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetLanguages xmlns="http://schemas.microsoft.com/sharepoint/soap/" />
</soap:Body>
</soap:Envelope>
'

$soapAction = 'http://schemas.microsoft.com/sharepoint/soap/GetLanguages';

$response = call-webservice $wssAdminAsmxUrl $request $soapAction
write-debug "Got GetLanguages response of $($response)"

get-matches $response '<LCID>(.*?)</LCID>'

call-webservice.ps1

Last, but hopefully not least, is the script that uses System.Net.WebClient to actually make a web method call.  It expects the caller to have already formed the request and any necessary SOAPAction header, so it's just making a POST call and doing conversion of strings to and from byte arrays (via UTF8 encoding).

param (
[string] $webServiceUrl = $(throw 'webServiceUrl is required'),
[string] $request = $(throw 'request is required'),
[string] $soapaction = $null
)

$webClient = new-object 'system.net.webclient'
$webClient.UseDefaultCredentials = $true
$webClient.Headers['Content-Type'] = 'text/xml; charset=utf-8'
if ($soapaction)
{
$webClient.Headers['SOAPAction'] = $soapaction
write-debug "Setting SOAPAction header to $soapaction"
}

write-debug "Sending to $webServiceUrl request of $request"
$requestBytes = [text.encoding]::utf8.getbytes($request)
trap {
write-error "Could not access $webServiceUrl"
break
}
$respBytes = $webClient.UploadData($webServiceUrl, 'POST', $requestBytes)
write-debug "Got back $($respBytes.Count) bytes"

$webClient.Dispose()
$respString = [text.encoding]::utf8.getstring($respBytes)
write-debug "Got back response string of $($respString)"

$respString

That's it - hopefully that provides some good examples of both how to "compose your scripts", another route to make web service calls ("manually" with the xml), some more examples of using the TFS object model, etc.

Also, please let me know what parts you do and don't like about a post like this (for example "Don't like how long it got - break it up already!").

This post brought to you a little faster thanks to Wes' WLW plugin for codehtmler.