Getting an Assembly’s Strong name with PowerShell
Quick tip, should get me out of blogging hibernation.
It’s a common issue to find an assembly’s strong name, but this can be done very easily with a simple PowerShell function:
function Get-AssemblyStrongName($assemblyPath) { [System.Reflection.AssemblyName]::GetAssemblyName($assemblyPath).FullName }
Simply give the assembly’s full path and you’ll get the strong name in return.
Personnaly, I have that in my $profile file and have an alias gan for it.
Web UI Automation and Test using PowerShell
Lately, I had to do lots of repetitive tasks at work in order to fix defects in an application. As I grew tired of filling numerous forms with the mandatory data needed for them to validate, I thought that I could maybe use PowerShell to automate the form-filling process.
I very quickly stumbled upon an MSDN article titled Web UI Automation with Windows PowerShell. It was definitely the way to go, but I was not really satisfied with everything I found over there.
Getting a Brower object
This part is similar to the one used in the MSDN article.
$global:ie = New-Object -com "InternetExplorer.Application" $global:ie.Navigate("about:blank") $global:ie.visible = $true
Please note that I will use $global:ie and $global:doc all the time, as global variables. I could have used $script: scope variables, but global are better for this task. Here, we are building a bunch of reusable function; declaring these variables as $global: ensures that the variables will be available in scripts that include this utility script. See here for more information on PowerShell variable scopes.
So this, sets $global:ie to a COM Internet Explorer object, navigates to “about:blank” and makes the window visible. Now we need some helper functions to navigate, fill forms and click elements easily.
Helper Functions to Navigate, Click, etc…
First of all, we need a method that yields the script while the current page is loading. So, what it should do is simple: wait for the current page to be loaded then set the $global:doc variable to the loaded document.
Function WaitForPage([int] $delayTime = 100) { $loaded = $false while ($loaded -eq $false) { [System.Threading.Thread]::Sleep($delayTime) #If the browser is not busy, the page is loaded if (-not $global:ie.Busy) { $loaded = $true } } $global:doc = $global:ie.Document }
I used an optional parameter here for the delay between checks of the browser’s status. It’s pretty straight forward, while the browser ($global:ie) is busy, wait. Once it’s not busy anymore, assign the document to the $global:doc variable. Let’s now define a function to navigate to a given url.
Function NavigateTo([string] $url, [int] $delayTime = 100) { Write-Verbose "Navigating to $url"; $global:ie.Navigate($url) WaitForPage $delayTime }
Note that I used Write-Verbose commands around in functions to output some useful information in my script, making it easier to spot mistakes while running it.
Now, as this was made to fill web forms, let’s define a function that will fill an input text field with a given value.
Function SetElementValueByName($name, $value, [int] $position = 0) { if ($global:doc -eq $null) { Write-Error "Document is null"; break } $elements = @($global:doc.getElementsByName($name)) if ($elements.Count -ne 0) { $elements[$position].Value = $value } else { Write-Warning "Couldn't find any element with name ""$name"""; } }
This is heavily used in my scripts. An HTML form always has lots of input elements that have unique names that need to be filled. So, if you need to fill the username input text with Philippe value, just call this function:
SetElementValueByName “username” “Philippe”
Note that there is also a option parameter that is used as the element’s position in the array returned by $global:doc.getElementByName. By default, the used position is 0 because most of the forms will only have one element with a given name. However, it can be that in (badly designed?) forms, two elements have the same name. In this case, you can specify which one you want to fill.
Now, I won’t explain all the functions, but here are the ones I wrote:
Function ClickElementByTagName($tagName, [int] $position = 0) { if ($global:doc -eq $null) { Write-Error "Document is null" break } $elements = @($global:doc.getElementsByTagName($tagName)) if ($elements.Count -ne 0) { $elements[$position].Click() WaitForPage } else { Write-Error "Couldn't find element ""$tagName"" at position ""$position"""; break } } Function ClickElementById($id) { $element = $global:doc.getElementById($id) if ($element -ne $null) { $element.Click() WaitForPage } else { Write-Error "Couldn't find element with id ""$id""" break } } Function ClickElementByName($name, [int] $position = 0) { if ($global:doc -eq $null) { Write-Error "Document is null" break } $elements = @($global:doc.getElementsByName($name)) if ($elements.Count -ne 0) { $elements[$position].Click() WaitForPage } else { Write-Error "Couldn't find element with name ""$name"" at position ""$position""" break } }
These are used to click on objects of the DOM in order to submit the form. These functions are not error proof, but as far as I used this stuff, I didn’t have issues.
A Little Example
Let’s write something very simple to test these functions. I will do a advanced search on google using these functions:
NavigateTo "http://www.google.com/advanced_search" SetElementValueByName "as_oq" "Unisys Fenix PLDA" SetElementValueByName "num" "30" SetElementValueByName "lr" "lang_en" ClickElementByName "btnG"
This gives you a little example of the kind of things you can do. It’s rather simple, but very powerful if you have repetitive tasks to on some web sites.
Download the full script here.
Get-Content and –encoding Switch
Today I ran into a funny issue. I was playing around with PowerShell in order to automate some repetitive task I have to do at work (it is Web UI automation, I’ll post on that later), and when reading XML file using Get-Content I had encoding issues. When the XML contained a ü character, it was replaced in the output file by some junk that immediately tells you that there is something wrong with encoding somewhere.
Let’s see this with a little example.
Here is a sample XML file:
<Test> <EvilStringOfDeath><![CDATA[ a'b<'>",!"/%$?$&?%*(()%/"!"/&?%$/"*&$/"?%&?-f¯Ñ112üêù ]]></EvilStringOfDeath> </Test>
Ok, the content is ugly, but you get the point. Let’s read this in PowerShell and see what happens
$xmlData = [xml] (gc .\Input.xml)
And now, let’s see the content of that variable
$xmlData.Test.EvilStringOfDeath.get_innertext()
a’b<’>",!"/%$?$&?%*(()%/"!"/&?%$/"*&$/"?%&?-f¯Ò112üêù
Something was lost in translation. You might think that it’s only a display issue, as help on Get-Content says that the –encoding switch but it is not. If you write the content of the $xmlData variable using .Save() or simply by out-putting it to a file (using > operator), the content of the file will not be valid.
To make this work, there is a switch in Get-Content: -encoding. You can specify which encoding format. So, using this command:
$xmlData = [xml] (gc .\Input.xml -enc UTF8)
The data should be read properly. Now if I display the content of $xmlData once more, here is what I get:
a’b<’>",!"/%$?$&?%*(()%/"!"/&?%$/"*&$/"?%&?-f¯Ñ112üêù
Perfect, I got what I want. I can now save or output it properly!
PS: if you go a gm (Get-Member) on $xmlData.Test.EvilStringOfDeath you won’t see the get_innertext() member. I don’t know why, but still, it works. I found it over here.
PS2: I had that issues at work, when I’m running a XP laptop. Now that I’m home on my Windows 7 RC box, the –encoding switch of Get-Content doesn’t even show in the help… There sure is a good explanation, but I don’t have it.
Encrypt App.config section using PowerShell as a Post-build event
It is very easy to encrypt a section of the Web.config file using Aspnet_regiis.exe tool, but there is no equivalent tool to encrypt a section of an application configuration file (App.config). It can be done very easily in code, as explained in this post, but there is now way to do that automatically. So I decided to write a PowerShell script that would encode a section of the given application’s configuration file.
Here is what it looks like:
param( [String] $appPath = $(throw "Application exe file path is mandatory"), [String] $sectionName = $(throw "Configuration section is mandatory"), [String] $dataProtectionProvider = "DataProtectionConfigurationProvider" ) #The System.Configuration assembly must be loaded $configurationAssembly = "System.Configuration, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a" [void] [Reflection.Assembly]::Load($configurationAssembly) Write-Host "Encrypting configuration section..." $configuration = [System.Configuration.ConfigurationManager]::OpenExeConfiguration($appPath) $section = $configuration.GetSection($sectionName) if (-not $section.SectionInformation.IsProtected) { $section.SectionInformation.ProtectSection($dataProtectionProvider); $section.SectionInformation.ForceSave = [System.Boolean]::True; $configuration.Save([System.Configuration.ConfigurationSaveMode]::Modified); } Write-Host "Succeeded!"
Some explanation on the script:
- System.Configuration assembly must be loaded. To do this, I use a technique described in a post from Lee Holmes.
- The default data protection provider is DataProtectionConfigurationProvider. Still, you can specify another provider (for example, RSAProtectedConfigurationProvider) as a third argument.
Believe it or not, that was the easy part.
The next step is to run this script automatically as a Post-build event in Visual Studio, so the .config file that is "built" is encrypted. Sounds very easy, but is actually tricky, I had to try many times to get it right.
I will give it straight away, here is the command to put in the Post-build event:
powershell "& ""C:\Documents and Settings\VlericP\My Documents\WindowsPowerShell\EncryptAppConfigSection.ps1""" '$(TargetPath)' 'connectionStrings'
There are lot of quotes, but this is the only way to get it working. For detailed explanations on why, see this post: Invoking a PowerShell script from cmd.exe (or Start | Run).
Using this, you can have your App.config file unencrypted in your solution, and when you build the output .config file is encrypted.
PowerShell Script to Download a List of Files
Lately I have been playing around with PowerShell. I decided that I will write scripts in order to perform some simple actions, actions that could be scripted generally are not due to the fact that writing the script takes longer than manually doing it.
So here is the first task I would like to automate: sequentially download each file in a list, the list being provided as a text file.
Download a file using PowerShell
For this part, I simply "stole" the code from somewhere else: http://community.bartdesmet.net/blogs/bart/archive/2006/11/25/PowerShell-_2D00_-How-to-download-a-file_3F00_.aspx, plain simple, I don’t think I’ll be able to do better.
Read a text file in an array
I was expecting this to be a bit more complicated, but it turns out to be piece of cake:
$array = Get-Content TextFile.txt
Not too much trouble.
Putting it all together
So now we need to do make these things work together. First, read the content of the file (given as parameter) in an array, then for each item in the array get the client to download it.
Now, there is one little trick here. The WebClient.DownloadFile method’s second argument is the local file. In this case, we want it to have the same file name as the source file name. Leveraging the .NET framework, System.IO.Path is the way to go.
$list = Get-Content $args $clnt = New-Object System.Net.WebClient foreach($url in $list) { #Get the filename $filename = [System.IO.Path]::GetFileName($url) #Create the output path $file = [System.IO.Path]::Combine($pwd.Path, $filename) Write-Host -NoNewline "Getting ""$url""... " #Download the file using the WebClient $clnt.DownloadFile($url, $file) Write-Host "done." }
Two remarks on of the Path class:
- To call static members, we have to use :: instead of ., unlike in C#.
- Surprisingly (and what a nice surprise), Path.GetFileName works on URLs too. So Path.GetFileName(http://www.google.com/hello.txt) returns "hello.txt".
The script is not error proof, but it good enough for me now.
Edit: I added some Host-Write lines in order to display something in the shell. When downloading a long list of files, it is nice to see the progress.
