Powershell


A co-worker asked the other day about a communication between different powershell scripts. First solution was using Mutex but it showed pretty fast that Mutex and Semaphore does only work in Powershell if you start with the –STA switch. By default Powershell uses a Multithreaded Apartment with each command running in its own thread. So when the simples method failed I looked for other means

Named Pipes to the Rescue

Named Pipes are not supported natively by Powershell (as far as I know). But as you can use any .Net Classes in Powershell thats not a Problem. You only need to load the System.Core Assembly (from .Net 3.5) via either

add-Type -assembly "System.Core" (in PS V2)

or old fashioned via

[reflection.Assembly]::LoadWithPartialName("system.core")

Some are telling you to load with explicit version but IMHO this only
ties you to a certain combination of Powershell and .Net. Usualy newer versions of .Net ensures backwards compability anyway.

Server Side

$pipe=new-object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\Wulf");
'Created server side of "\\.\pipe\Wulf"'
$pipe.WaitForConnection(); 

$sr = new-object System.IO.StreamReader($pipe); 
while (($cmd= $sr.ReadLine()) -ne 'exit') 
{
 $cmd 
}; 

$sr.Dispose();
$pipe.Dispose();

In this case the server only emits the given command but feel free to parse it and even extract parameters or just ignore the cmd and always do the same.

Client Side

$pipe = new-object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\Wulf");
 $pipe.Connect(); 

$sw = new-object System.IO.StreamWriter($pipe);
$sw.WriteLine("Go"); 
$sw.WriteLine("start abc 123"); 
$sw.WriteLine('exit'); 

$sw.Dispose(); 
$pipe.Dispose():

On the client side you are writing to the pipe. As long as you don’t send the exit command you can connect from different clientscripts.

Naming the Pipe

Named Pipes must have uniq names per System so you better use something like \\.\pipe\company.tld.app. While the server side is bound to use localhost ‘.’ the client can use a remote connection with the \\servername\pipe\… syntax. (If the policies and firewall settings permit it).

 

I might revisit it and bring it to a proper module form but for the moment it is enough. Stay alert as this is not tested in production qualitiy scripts there might be a lot of caveats. Don’t say I didn’t told you.

It’s again that time of the year, its MIX in Las Vegas. Lots and lots of sessions and every session is recorded on video. So even if you can’t be in Vegas get the whole thing. But I don’t like the idea of downloading all of those via the browser. So I wrote a script to download them via BitsTransfer from PowerShell loosely based on a script from poshcode I used to get videos from PDC. I can’t find the original author and his script again but thanks a lot.

Update: I polished the script a little bit like warnings if a file is already on disk, listing all missing sessions, sorting index, removing the dummy, some minor bugfixing with naming, etc. Still not perfect but better.

Mix10VideoDownload.zip.docx Rename to Mix10VideoDownload.zip to extract.

The script takes a session code as parameter. It then grabs the RSS-Feed to extract title, speaker, tags etc. These values will inserted into an index.xml file (maybe I write a corresponding XSLT for viewing but you can already view it in the browser). After that a BitsTransfer Job is started to download the video.

If the script is called without parameter it will list all missing sessions.

I use some small functions in my profile to control downloads:

# Downloads with BitsTransfer
function Get-Downloads {
  Import-Module BitsTransfer
  Get-BitsTransfer | % { "{0}: {2} {3:0.00} MB/{4:0.00} MB ( {1:0.00}% )" -f $_.DisplayName, $(100*$_.BytesTransferred/$_.BytesTotal), $_.JobState, ($_.BytesTransferred/1MB), ($_.BytesTotal/1MB) }
}
Set-Alias "dl" "Get-Downloads"

# Get a list of all downloads
function Complete-Downloads 
{ 
  Import-Module BitsTransfer
  Get-BitsTransfer | ? { $_.JobState -eq "Transferred" } | Complete-BitsTransfer 
}

# suspend all Downloads (optional all below a certain threshhold
function Suspend-Downloads( [int] $maxPercent = 100 )
{
  Complete-Downloads
  Get-BitsTransfer | ? { $_.JobState -eq "Transferring"  -and ($_.BytesTransferred/$_.BytesTotal) -lt ($maxPercent/100) } | Suspend-BitsTransfer 
}

#resume all Downloads (optional all above a certain threshhold
function Resume-Downloads( [int] $minPercent = 0 )
{
  Complete-Downloads
  Get-BitsTransfer | ? { $_.JobState -eq "Suspended" -and ($_.BytesTransferred/$_.BytesTotal) -gt ($minPercent/100) } | Resume-BitsTransfer -Async
}

# download Mix Session Videos. Call it like mix "EX21" "CL01" "FT05"
function Mix()
{
  complete-downloads; $args | % { & $scripthome\get-Mix10Video.ps1 $_ } ; sleep -Seconds 5; get-downloads
}

Use Get-Downloads to view the progress of the download and Complete-Downloads to finish it (The file will not show up until you complete it)

Use the mix function to call the script with multiple sessions e.g. mix “EX14” “EX06” will download the excellent talks of Laurent Bugnion about MVVM Pattern and Robby Ingebretsen about Design Principles and other things.

Extract the zip. Pay attention to not overwrite your index.xml if you already have it. Put the script at any place appropriate to you and the index.xml into the destination directory. I suggest putting your destination path into the script instead of my default value. Call it either with ? to get the list of available sessions or with a session code to download these video.

An Example:

.\Get-Mix10Video.ps1 CL01

will put the file CL01-Introduction to Windows Phone 7 Series.wmv into your destination directory.

#requires -version 2.0
PARAM ( 
   [Parameter(Position=1, Mandatory=$false)]
   [String]$Video="?",

   [Parameter(Position=2, Mandatory=$false)]
   [ValidateSet("wmv","wmv-hq","pptx", "mp4")]
   [String]$MediaType ="wmv-hq",
   
   [string]$Destination = "F:\Videos\Mix10",
   
   [string]$rss = "http://live.visitmix.com/Sessions/RSS"
)

Import-Module BitsTransfer

#$illegalChars = "[{0}]" -f ([Regex]::Escape([String] [System.IO.Path]::GetInvalidFileNameChars()))
$illegalChars = '[:*?\\\/\t\n<>|"]'

# Get Session RSS, parse Titel, Speaker, Tags, Description, ...
$wc = new-object System.Net.WebClient
$rssdata = [xml]$wc.DownloadString($rss)
$item = $rssdata.rss.channel.item | ? { $_.link.EndsWith($Video) }

Push-Location $Destination
$Extension = $(switch -wildcard($MediaType){"wmv*"{"wmv"} "mp4"{"mp4"} "pptx"{"pptx"}})
$SrcUrl = "http://ecn.channel9.msdn.com/o9/mix/10/{0}/{2}.{1}" -f  $MediaType, $Extension, $Video
$Destfile = ( "{0}-{1}.{2}" -f $Video, $Title, $Extension ) -replace $illegalChars, ""
$Destpath = $("{0}\{1}" -f $Destination, $Destfile)
$indexFile =  $("{0}\{1}" -f $Destination, "index.xml")

$Title = $item.title


# get index.xml
$index = new-object XML
$index.Load($indexFile)

# returns the list of the missing sessions if code is ? or item not found
# use filter like | ? { $_.tags -contains "WindowsPhone" } | % { .\Get-Mix10Video.ps $_.code }
if ( $Video -eq "?" -or $item -eq $null )
{
    $rssdata.rss.channel.item |  % {
         New-Object PsObject -Property @{ 
            code    = $_.link.Substring($_.link.LastIndexOf("/") +1 )
            title   = $_.title
            speaker = $_.author
            tags    = @( $_.category ) 
        }
    } | ? {
        $xpath = "//session"
       $index.SelectSingleNode($xpath) -eq $null
    } | sort -Property "code"
    return
}


# does index already contains session?
$session = $index.SelectSingleNode("//session")
if ($session -eq $null) {
  $firstSession = @( $index.sessions.session )[0]
  $session = $firstSession.Clone()
  $session.code = $Video
  [void]$index.sessions.AppendChild($session)
}

$session.title = $Title
$session.speaker = $item.author
$session.tags = $item.category  -join ","
$session.sessionref = $item.link
$session.local = $Destpath
$session.href = $SrcUrl
$session.description = $item.description

# remove dummy as it is no longer needed
$dummy = $session = $index.SelectSingleNode("//session")
if ($dummy -ne $null) { $index.sessions.RemoveChild( $dummy ) }

# sort sessions by code
[void] ( $index.sessions.session | sort -Property "code" | % { $index.sessions.RemoveChild($_); $index.sessions.AppendChild($_) } )

$index.Save($indexFile)

# file downloaded in former session?
if (Test-Path "$Video*.$Extension") {
  $title = "File $Video*.$Extension already exists!"
  $message = "Do you want to delete the existing file and download it new?"

  $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", `
      "Delete $Destfile and start download"

  $continue = New-Object System.Management.Automation.Host.ChoiceDescription "&Continue", `
      "Continue without deleting file"
      
  $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", `
      "Hold file and skip download"

  $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $continue, $no)

  $result = $host.ui.PromptForChoice($title, $message, $options, 2) 

  switch ($result)
    {
        0 { delete $Destpath }
        1 { "Continuing with download" }
        2 {             
            Pop-Location
            return
          }
    }

}

#start download
[void] ( Start-BitsTransfer -Source $SrcUrl -Destination $Destpath -DisplayName ($Video +": "+$Title)  -Description $item.description -Async )

Pop-Location

Write-Host "You may now use Get-BitsTransfer to check on the status of the downloads. By default, failed transfers will be retried every 10 minutes for two weeks."

The index.xml uses the following format:

<?xml version="1.0" encoding="utf-8"?>
<!-- Sessionlist unter http://live.visitmix.com/Sessions/RSS brauchbar? -->
<sessions>
  <session>
    <code>CL01</code>
    <title>Changing our Game – an Introduction to Windows Phone 7 Series</title>
    <speaker>Joe Belfiore</speaker>
    <tags>Mobile, Windows Phone</tags>
    <href>http://ecn.channel9.msdn.com/o9/mix/10/wmv-hq/CL01.wmv</href>
    <sessionref>http://live.visitmix.com/MIX10/Sessions/CL01</sessionref>
    <local>CL01-Introduction to Windows Phone 7 Series.wmv</local>
    <description>
      Major changes are coming to Windows Phone! This session goes in-depth on the design and 
      features of Windows Phone and gives a comprehensive picture of what’s 
      coming in this exciting new release.
    </description>
    <filesize>
    </filesize>
    <length>
    </length>
  </session>
</sessions>

I wanted a good visible Prompt with a shortend Path for common working directories. It consists now on the parts Historynumber of the current command, End of last command Time, the shortend path and a > Sign. Every Part is colored differently. The Full Path is shown in Title. Global foreground color is reset to white.

Feel free to use and modifiy it:

# prompt
function prompt {
 $host.ui.rawui.windowtitle = "PowerShell - "+ (Get-Location).ToString()
 $host.ui.rawui.foregroundcolor = "White"
 $private:history = @(get-history)
 if ($private:history.count -eq 0) {
  $private:intCommandCount = 1
 } else {
  $private:intCommandCount = $private:history[$private:history.count - 1].ID +1
 }
 $private:location = @((Get-Location).ToString().replace($home, "~").replace("C:\entwicklung\","~"))

Write-Host -NoNewline -ForeGroundColor Yellow ("[$private:intCommandCount] ")
 Write-Host -NoNewline -ForeGroundColor Gray @(Get-Date -format "HH:mm:ss " )
 Write-Host -NoNewline -ForeGroundColor Yellow $private:location
" > "
}

Subversion speichert seine Metadaten in .svn Verzeichnissen, die einen Bug in Webapplikationen bei Visual Studio 2003 triggern. Man muss einmal das Projekt ohne die .svn Verzeichnisse laden, danach erhält man bei jedem weiteren Öffnen zwar eine Fehlermeldung, das macht aber nichts, alle wesentlichen Daten liegen dann bereits unter C:\Dokumente und Einstellungen\username\VSWebCache\. Insbesondere beim neueinrichten eines Rechners vom Backup eines anderen Rechners kann das extrem lästig werden.

Wie immer ist der Retter in der Not die Powershell.

get-childitem -recurse -force -ErrorAction SilentlyContinue | where { $_.Name -eq ".svn" } | foreach { Rename-Item $_.FullName "_svn" }

get-childitem holt alle Dateien im aktuellen Verzeichnis und, dank -recurse, auch aus allen unterhalb. .svn wird standardmäßig als versteckt betrachtet, deshalb ist -force notwendig. Nach dem Umbenennen ist es natürlich nicht mehr möglich das Verzeichnis rekursiv zu durchsuchen, dies führt zu einer Fehlermeldung. Da .svn aber keiene .svn Ordner enthalten, schalten wir die Fehlerbehandlung für get-childitem einfach mit dem Standardparameter ErrorAction ab.

where filtert die Pipeline auf .svn Ordner.

Foreach führt dann das Rename auf dem absoluten Pfad der .svn Ordner aus. 

Für die Rückrichtung müssen wir dann nur .svn und _svn vertauschen. (hier mal in Kurzschreibweise):

gci -r -fo -ErrorAction SilentlyContinue | ? { $_.Name -eq "_svn" } | %{ Rename-Item $_.FullName ".svn" }

Immer wieder klasse wie flexibel man Alltagsaufgaben mit der Powershell lösen kann. Kürzlich mußte ich einige hundert Files unbenennen deren Namen auf -x86 endeten (mit verschiedenen Extensions). Eigentlich hätte das Installationsprogramm diesen String entfernen sollen, irgendwas war da aber schief gegangen. Lösung:
dir | % { rename $_.Name $_Name.replace("-x86", "") }

Erklärung: dir (oder get-childitems) legt alle dateien in die Pipeline, % ( oder For-Each) führt den Block für alle Elemente der Pipleine aus, $_ ist das aktuelle FileInfo oder DirInfo object. Der Name steht dann als ganz normaler String über die Eigenschaft Name zur Verfügung. Auf diesem Namen kann ich alle Methoden der .Net String Klasse verwenden, in diesem Fall replace. Ersetzen von “-x86” durch den leeren String führt genau zu dem gewünschten Namen. Ein rename (oder Rename-Item) eledigt dann die eigentliche Arbeit.