Skip to content
Archive of posts tagged PowerShell

The PowerShell Bug That Wasn’t, and More Package Management

Have you ever tracked down a bug, been confident that you had found the root of your problems, only to realize shortly afterwords that you missed it completely?

What I posted yesterday as a bug in PowerShell (having to do with recursive functions, dot-sourcing, and parameters) seemed during my debugging session to clearly be a bug. After all, I watched the parameter value change from b to a, didn’t I? Sure did. And in almost every language I’ve ever used, that would be a bug. On the other hand, PowerShell is the only language that I know of that has dot-sourcing. Here’s a much simpler code example which shows my faulty thinking:

function f($x){
   if ($x -eq 1){
      write-host $x
      . f ($x+1)
      write-host $x
   }
}

f 1

Here, we have a simple “recursive function” which uses dot-sourcing to call itself. In my mind, how this would have worked is as follows:

  • We call the function, passing 1 for $x
  • The if condition is true, so it prints 1 and calls the function, passing 2 for $x
  • In the inner call, the if condition is false, so nothing happens
  • We pop back to the calling frame, where $x is 1 and print it

If it weren’t for that pesky dot operator, that would have been accurate.

The problem is, the dot operator changes the scoping of the inner call.  Here’s what the about_operators help topic, has to say about the dot sourcing operator:

        Description: Runs a script so that the items in the script are part of the calling scope.

Which is not a surprise…really.  The reason I was using the dot operator in my package management code was to make sure that functions defined in the scripts it was calling would be included in the existing scope, rather than their script scope.  The problem was one of nearsightedness.  I was so focused on the fact that the dot sourcing was making the functions part of the caller’s scope that I didn’t consider that variable declarations (including parameters) would also be in the caller’s scope.

So, the correct interpretation of the above script is:

  • We call the function, passing 1 for $x
  • The if condition is true, so it prints 1 and calls the function, passing 2 for $x
  • The parameter is named $x, so $x in is set to 2 (overwriting the $x that was set to 1)
  • In the inner call, the if condition is false, so nothing happens
  • We pop back to the calling frame, where $x is 2 and print 2.

The trick here is that the function f dot-sourced something that set $x to 2.  The fact that it was f is incidental.  It didn’t have to be.

Maybe this example will make it more clear:

function f($x){
    write-host $x
    . g
    write-host $x
 }

function g{
   $x = "Hello, World!"
}
f 1

If we were doing this without dot-sourcing, we would expect to see the number 1 printed out twice. However, since we dot-sourced g, the assignment in the function body of g happens in the scope of f. In other words, it’s as if the $x=”Hello, World!” were executed inside f. Thus, the output of this script is 1, followed by “Hello, World!”.

So, it wasn’t a bug, it was just me not being thorough in applying my understanding of dot-sourcing.

Now, on with Package Management.
First, to fix the problem caused by the parameter being overwritten (which it is, it’s just that it’s expected to be). I hadn’t worked out a way to fix the problem before I went to bed last night, but as I was rolling this stuff around in my head (which is when I figured out that it wasn’t really a bug), I thought of a simple solution. Since we can expect that sometimes the $filename parameter in the require (and reload) function will be overwritten by the a value in the dot-sourced script, we just need to make sure we’re done using it at that point. So, I simply made the assignment to the dictionary before dot-sourcing.  Here’s the updated code:

$global:loaded_scripts=@{pkg_utils='INITIAL'}

function require($filename){
	if (!$global:loaded_scripts[$filename]){
	   $global:loaded_scripts[$filename]=get-date
	   . scripts:\$filename.ps1
	}
}
function reload($filename){
	$global:loaded_scripts[$filename]=get-date
	. scripts:\$filename.ps1
}

To add modules, we need to do a few extra things:

  • We need to detect if we’re running in 2.0 or not
  • We need to see if there is a module with the given name
  • We need to see if the module is already loaded or not (in the case of require…it won’t matter for reload

Fortunately, none of those are very difficult.  Here’s the updated code (including modules). I even added some comments to make the flow more clear:

$global:loaded_scripts=@{pkg_utils='INITIAL'}

function require($filename){
    if ($global:loaded_scripts[$filename]){
          # this function has already loaded this (script or module)
          return
    }
    if ($psversiontable){
       # we're in 2.0
       if (get-module $filename -listavailable){
               #the module exists in the module path
         	   $global:loaded_scripts[$filename]=get-date
               import-module $filename
               return
       }
    }
    #it wasn't a module...so dot-source the script
    $global:loaded_scripts[$filename]=get-date
    . scripts:\$filename.ps1

}
function reload($filename){
    if ($psversiontable){
        # we're in 2.0
        if (get-module $filename -listavailable){
           #the module exists in the module path
           $global:loaded_scripts[$filename]=get-date
           import-module $filename
           return
        }
    }
    # it wasn't a module...so dot-source the script.
  	$global:loaded_scripts[$filename]=get-date
	. scripts:\$filename.ps1
}

That’s it for today. Let me know what you think.

-Mike

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Package Management and a PowerShell Bug

UPDATE: I have worked out how the behavior described at the end of this post is not a bug, but in fact just PowerShell doing what it’s told. Don’t have time to explain right now, but I’ll write something up later today. I also worked out how to “fix” the behavior.

For a long time now, I’ve been dissatisfied with what I call “package management” in PowerShell.  Those of you who know me will be shocked that anything in PowerShell is less than perfect in my eyes, but this is one place that I feel let down.  Modules in 2.0 remedy the situation somewhat, but it still isn’t quite what I want or am used to in other languages.

Let me give an example.  In VB.NET, if you need to use the functions in an assembly, you put “Imports AssemblyName” at the top of your script.  In C#, you would have “Using AssemblyName”.  In Python, there would be “Import Something”.

In PowerShell 1.0, you had nothing.  In 2.0, you could create a module manifest which would specify either RequiredModules or ScriptsToProcess (or several other things to do upon loading the module).  The problems I see  with using the module manifest are:

  • What if I’m not writing a module?  There’s no such thing as a “script manifest”
  • What if the script or module that is required performs some initialization that should only be done once per session?
  • What if the script or module that is required performs expensive initialization?

Because of these reasons (and because I only started using 2.0 when it went RTM) I wrote a couple of quick functions to do what I thought made sense.

$global:loaded_scripts=@{pkg_utils='INITIAL'}

function require($filename){
	if (!$global:loaded_scripts[$filename]){
	   . scripts:\$filename.ps1
	   $global:loaded_scripts[$filename]=get-date
	}
}
function reload($filename){
	. scripts:\$filename.ps1
	$global:loaded_scripts[$filename]=get-date
}

To use these you need to create a psdrive called scripts: with code like this (probably in your profile):

New-PSdrive -name scripts -PSprovider filesystem -root \\PathToYourLibraries | Out-Null

Then, also in your profile, you’ll want to dot-source the file you put these functions in (for example, package_tools.ps1):

. scripts:\package_tools.ps1

Once you have those set up, you can dot-source the require function to make sure that a script has been loaded as such:

. require somelibrary

I have the functions I use divided by “subject” into several library scripts, and make sure that at the top of each script, I use “. require” to ensure that any prerequisites are already loaded.

Now for the PowerShell bug (which took me a long time to track down).
Create 2 files, a.ps1 and b.ps1 in your scripts: directory.

# a.ps1
write-host "this is script a"
#b.ps1
write-host "this is script b"
write-host "this script loads a"
. require a

After dot-sourcing package_tools, run the following commands:

. require b

You should get output that looks something like this:

this is script b
this script loads a
this is script a

Everything looks good until you inspect the $global:loaded_scripts variable:

ps> $loaded_scripts

Name                           Value
----                           -----
a                              1/19/2010 11:23:09 PM
package_tools                  INITIAL

Although b.ps1 was indeed dot-sourced (you can see the output), and the only code-path through the require function that would dot-source it would also add an entry to $loaded_scripts, there is no such entry. The problem is that when b.ps1 called the require function (to load a.ps1), the $filename variable in the calling context (where it should have been “b”) was overwritten by the call with “a” as a parameter. Walking through the code in a debugger confirms the problem.

Have you ever seen problems with recursion and dot-sourcing in PowerShell? Can you see any way around the problem I’ve described? For instance, saving the $filename in a variable and restoring it after the dot-source call (line 5 above) doesn’t help, because the same code-path is followed in the recursive call, and that variable is overwritten as well.

Even with this bug, I find the require function (and reload, which I didn’t discuss, but always loads the script in question) to be very helpful. I also have extended them to include importing modules, if they exist. I’ll discuss them in my next post, coming soon.

-Mike

P.S. Here‘s a question I posted to StackOverflow.com about these functions back in November of 2008.

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (the epilog)

As I mentioned before, I have created a CodePlex project to track the development of a WPF PowerShell host using AvalonDock and AvalonEdit.

It’s still in the very beginning stages, but it’s comparable to the code I used in this tutorial series (except that it’s using different technologies, all of which I’m new to).

PowerShellWorkBench will eventually include:

  • Treeview controls
  • Node/Edge Graphs (using the GraphXL library)
  • Context-menus based on powershell ETS
  • Whatever you think of and submit

If you’re interested in contributing to PowerShellWorkBench, drop me a line (mike).

-Mike

[EDIT]: The windows forms-based powershell workbench project can be downloaded here.

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (part 6…the final episode)

Before we proceed with putting powershell objects in a treeview (which I promised last time), I need to explain some changes I have made to the code.

  • Refactoring the InvokeString functionality ouf of the menu item event
  • Merging the error stream into the output stream
  • Replacing the clear-host function with a custom cmdlet

First, we had been calling the invoke method in the OnClick event of the menu item. While that works fine as a proof-of-concept, we’re going to need that functionality elsewhere, so it’s a simple matter to extract the logic into a function as follows:

Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)
  	InvokeString(txtScript.Text)
End Sub

Private Sub InvokeString(strScript As String)
	dim ps As powershell=PowerShell.Create()
        ps.Runspace=r
        ps.AddScript(strScript)
        ps.AddCommand("out-default")
        ps.Commands.Commands.Item(ps.Commands.Commands.Count-1).MergeUnclaimedPreviousCommandResults = PipelineResultTypes.Error + PipelineResultTypes.Output
        Dim output As Collection(Of psobject)
        output=ps.Invoke()

End Sub

In this new InvokeString method you see highlighted (if you allow javascript :-) ) the line of code that merges the error stream into the output stream (so that errors we throw with our new cmdlets will show up in the console).  We’ll still need to update the our PSHostUserInterface class to handle the WriteError method, but that’s pretty easy (as are the debug, verbose, and warning methods):

Public Overloads Overrides Sub WriteErrorLine(value As String)
	MainForm.PowerShellOutput.AppendText("ERROR:"+value +vbcrlf)
End Sub

Public Overloads Overrides Sub WriteDebugLine(message As String)
	MainForm.PowerShellOutput.AppendText("DEBUG:"+message +vbcrlf)
End Sub

Public Overloads Overrides Sub WriteProgress(sourceId As Long, record As System.Management.Automation.ProgressRecord)
	Throw New NotImplementedException()
End Sub

Public Overloads Overrides Sub WriteVerboseLine(message As String)
	MainForm.PowerShellOutput.AppendText("VERBOSE:"+message +vbcrlf)
End Sub

Public Overloads Overrides Sub WriteWarningLine(message As String)
	MainForm.PowerShellOutput.AppendText("WARNING:"+message +vbcrlf)
End Sub

With that, we can see that the built-in clear-host isn’t going to work:

ERROR:Exception setting "CursorPosition": "The method or operation is not implemented
ERROR:."
ERROR:At line:8 char:16
ERROR:+ $Host.UI.RawUI. <<<< CursorPosition = $origin
ERROR:    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
ERROR:    + FullyQualifiedErrorId : PropertyAssignmentException
ERROR:
ERROR:Exception calling "SetBufferContents" with "2" argument(s): "The method or oper
ERROR:ation is not implemented."
ERROR:At line:9 char:33
ERROR:+ $Host.UI.RawUI.SetBufferContents <<<< ($rect, $space)
ERROR:    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
ERROR:    + FullyQualifiedErrorId : DotNetMethodException
ERROR:

You can see that, by default, “clear-host” is a function that relies on the RawUI class in the host (using rectangles, and filling with spaces, it looks like).  We really don’t want that kind of access in our interface, so we’re going to replace this function with a cmdlet that simply clears the textbox.

That brings up another “benefit” of writing your own GUI host…the ability to implement cmdlets without writing SnapIns.  With 2.0, you can write advanced functions (and I encourage you to do that), but with 1.0 you didn’t have that option.  With your own host, you get to add cmdlets without the pain of a SnapIn installer.  The two things we need to do are:

  1. Create a cmdlet class to do the work
  2. Add the cmdlet to the runspace configuration

When we replace the clear-host function, we’re going to also want to remove the existing function, but that’s not typical.  Here’s the code:

First, the cmdlet class (I usually put all of the cmdlets in the same file, rather than having a single file for each class, but that’s just a preference):

Imports System.Management.Automation
Imports System.ComponentModel

 _
Public Class ClearHost
    Inherits Cmdlet

    Protected Overrides Sub EndProcessing()
        MainForm.PowerShellOutput.Clear
    End Sub
End Class

To add the cmdlet to the runspace (and remove the function), I added these lines after the r.Open() call:

InvokeString("remove-item function:clear-host")
r.RunspaceConfiguration.Cmdlets.Prepend(New CmdletConfigurationEntry("clear-host",GetType(ClearHost),Nothing))
r.RunspaceConfiguration.Cmdlets.Update()

Now, finally, on to the promised treeview manipulation.  I want the cmdlet to be fairly simple, allowing you to specify the name of the label of the new node, and optionally the label of the parent node and an object to attach to the node (we’ll put it in the tag property of the treenode).  We’ll also need to expose the treeview control in a shared member of the form (since the cmdlet doesn’t have a reference to the specific window we instantiate).

First, here’s the cmdlet.  I’ve tried to make the code as simple as possible, so there are no tricks involved.

 _
Public Class NewTreeNode
	Inherits Cmdlet

	private _nodename as String=""
	private _parentnodename as String=""
	private _object as PSObject=nothing
 _
    Public Property NodeName() As String
        Get
            Return _nodename
        End Get

        Set(ByVal value As String)
            _nodename = value
        End Set

    End Property
 _
    Public Property ParentNodeName() As String
        Get
            Return _parentnodename
        End Get

        Set(ByVal value As String)
            _parentnodename = value
        End Set

    End Property
 _
    Public Property PSObject() As PSObject
        Get
            Return _object
        End Get

        Set(ByVal value As PSObject)
            _object = value
        End Set

    End Property

    Protected Overloads Overrides Sub EndProcessing()
		MyBase.EndProcessing()
		Dim _node As TreeNode
		Dim _parent As TreeNode
		_parent=PWBUIHandling.FindNodeInTree(_parentnodename,mainform.Tree.Nodes)
		If _parent is nothing then
			_node=MainForm.Tree.Nodes.Add(_nodename,_nodename)
		Else
			_node=_parent.Nodes.Add(_nodename,_nodename)
		End If
		_node.Tag=_object
    End Sub
End Class

In the form, we’ll need to add a treeview (I also added a second splitter to help organize the UI, but that’s obviously not necessary).  Adding the shared property,setting it, and adding the cmdlet to the runspace complete the changes:

Public Partial Class MainForm

Public Shared PowerShellOutput As textbox
public shared Tree as TreeView
private host as new PowerShellWorkBenchHost
private r As Runspace=RunspaceFactory.CreateRunspace(host)
	Public Sub New()
		' The Me.InitializeComponent call is required for Windows Forms designer support.
		Me.InitializeComponent()

		PowerShellOutput=txtOutput
		Tree=treeView1
       	r.ThreadOptions=PSThreadOptions.UseCurrentThread
        r.Open()
        InvokeString("remove-item function:clear-host")
      	r.RunspaceConfiguration.Cmdlets.Prepend(New CmdletConfigurationEntry("clear-host",GetType(ClearHost),Nothing))
      	r.RunspaceConfiguration.Cmdlets.Append(New CmdletConfigurationEntry("new-treenode",GetType(NewTreeNode),Nothing))
      	r.RunspaceConfiguration.Cmdlets.Update()

   End Sub

With that, let’s see how it works:

screenshot00052009113022_11_42_thumb.jpg

Obviously, I haven’t built an application that’s ready for use, but I think it is a good example of how you can use the PowerShell APIs to create a scriptable environment that you can customize.  And the fact that the code written to make it happen is less than 200 lines is a testament to the useful nature of the API (actual hand-coded lines, that is, there are about 400 lines in the whole project).

What’s next?  I think I’ll stop on the tutorial and segue into the codeplex project I’m starting (it should be live in the next week or 2).  In it, you should find things like

  • Syntax Highlighting (thanks to AvalonEdit)
  • Advanced docking interface (thanks to AvalonDock)
  • Tab Expansion
  • Custom pop-up menus for UI objects (like the nodes in the tree, for example)
  • Whatever else I (or anyone who wants to contribute) think of

-Mike

P.S.  I just realized that I forgot to include the FindNodeInTree function that the cmdlet called.  I hate that the treeview class doesn’t include a Find method.  Here’s the code:

	Function FindNodeInTree(nodename As String, nodes As TreeNodeCollection) as TreeNode
		dim rtn as TreeNode =nothing
		If nodes.ContainsKey(nodename) Then
			Return nodes(nodename)
		Else
			For Each node As treenode In nodes
				rtn=FindNodeInTree(nodename,node.Nodes)
				If rtn IsNot Nothing Then
					return rtn
				End If
			Next

		End If
		return rtn
	End Function
  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (part 5)

In the last post, we got to the point that we were actually using the new host objects that we implemented, but we still hadn’t provided anything more than trivial implementations (throwing an exception) for the methods that make a custom host useful, e.g. the write-* functions.

Before we do that, we need to discuss interaction between PowerShell (the engine) and windows forms (though we would have had the same issue with WPF).  In PowerShell 1.0, the engine creates its own thread to run the Invoke() method, and doesn’t provide a way to change that thread’s apartment model, which is MTA.  The reason that is important is that to interact safely with Windows Forms (or WPF), you need to be in the same thread.  The bottom line is that when using the 1.0 object interface, you can’t directly interact with the window environment.  Which means that any hopes you had of writing some simple code to append text to the textbox in the WriteHost method are going to be dashed.  Unless, of course, you use the 2.0 object model.  The designers realized the shortcoming, and in 2.o they allow you to change the child thread to STA.

So now we have a couple of choices.  As I mentioned in part 3, I was purposely using the 1.0 object model, since 2.0 wasn’t final, and the 1.0 methods would work fine in a 2.0 install.   One thing we could easily do is switch the code to 2.0, set the threading model to STA, and go on our merry way.  Another approach would be to have the Host objects interact with the user interface indirectly.  One way to do that would be to simply have the host methods package their arguments into an object, and add the object into a queue that is consumed in a timer event handler on the form.  This works quite nicely, and provides an easy separation between the host and the interface.

For now, though, for the sake of simplicity (and to keep the code from getting longer than anyone would care to read), we’ll just use the 2.0 object model.  As I mentioned in part 4, I plan to create a project on Codeplex for a more complete host than I can really create in a tutorial.  It will include code to keep the host and interface separate (which I think I like better).

Here is the revised code in the form to use the 2.0 model (I’ve moved some of the declarations out of the Click method because the objects don’t need to be recreated each time):

Public Partial Class MainForm

public shared PowerShellOutput as textbox
private host as new PowerShellWorkBenchHost
private r As Runspace=RunspaceFactory.CreateRunspace(host)
	Public Sub New()
		' The Me.InitializeComponent call is required for Windows Forms designer support.
		Me.InitializeComponent()

		PowerShellOutput=txtOutput
       	r.ThreadOptions=PSThreadOptions.UseCurrentThread
        r.Open()
     End Sub

    Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)

 		dim ps As powershell=PowerShell.Create()
        ps.Runspace=r
        ps.AddScript(txtScript.Text)
        ps.AddCommand("out-default")
        Dim output As Collection(Of psobject)
        output=ps.Invoke()

    End Sub

End Class

The line of code that will allow the host methods to interact with the form is:

r.ThreadOptions=PSThreadOptions.UseCurrentThread

A few other changes that should be noted are:

  • Adding a shared member PowerShellOutput to use in the host to update the textbox
  • Switching from out-string to out-default (now that we’re handling host output, we can let the default behavior send the objects in the pipeline to the host)
  • Removing the loop through the output (because of the previous point)

With that being said, I’ll make another comment.  If you’re trying to follow along with this series (as in, you have an editor open and are copying code in and trying it as you go), you’ll want to make sure you set a breakpoint on each of the throw statements in the host classes.  If you don’t do this, you won’t know what methods you need to implement (except by trial and error).  I have spent several hours debugging when the breakpoints would have showed me the problem immediately.  Please learn from my mistakes.

Now, we can finally get to coding the output routines.  We obviously need to implement some write* method, but there are several.   To figure out which one, I tried to run write-host “hello” and dir (fairly simple commands) and it turns out that we need to implement these methods for those to work:

  • WriteLine
  • Write  –both versions
  • PSHostRawUserInterface.ForegroundColor
  • PSHostRawUserInterface.BackgroundColor

I really wasn’t expecting the last color methods to come into play until we started passing them to the write-host cmdlet (which is why I lost so much time debugging).  Here are the implementations I’m using for now.  Note that we’re somewhat limited by the choice of a textbox (rather than a more fully-featured control) for output.
In PSHostUserInterface:

	Public Overloads Overrides Sub Write(value As String)
		If value=vblf Then
			mainform.PowerShellOutput.AppendText(vbcrlf)
		else
			MainForm.PowerShellOutput.AppendText(value	)
		End If
	End Sub

	Public Overloads Overrides Sub Write(foregroundColor As ConsoleColor, backgroundColor As ConsoleColor, value As String)
		MainForm.PowerShellOutput.AppendText(value	)
	End Sub

	Public Overloads Overrides Sub WriteLine(value As String)
		MainForm.PowerShellOutput.AppendText(value+vbcrlf)
	End Sub

and in PSHostRawUserInterface:

	Public Overloads Overrides Property ForegroundColor() As ConsoleColor
		Get
			return ConsoleColor.Black
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property BackgroundColor() As ConsoleColor
		Get
			return ConsoleColor.White
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

With those changes, we are (finally) using the custom host for output. Here’s an obligatory screenshot:

Output at last!

Output at last!

So what’s next? Obviously, we should fill in appropriate implementations for the other write-* functions. Other than write-progress, they shouldn’t prove any challenge. Write-progress, on the other hand, would really look nice as a progressbar (possibly in a status bar?). There are a few other things to consider:

  • clear-host is implemented as a function which uses the rawUI class to perform it’s stuff…that probably won’t work in out GUI app
  • Colors (if you choose to implement them) are going to be specified using the ConsoleColor class (which is different from the Color class used by Windows Forms)
  • Profiles….do you want to load them?  Do you want to have a profile specific to your new host?
  • What about interacting with the GUI in other ways?

The last point is the main thing that drove me to write my own host.  You may be fortunate enough to have a GUI tool to do all of your administration duties, but I suspect that most of us have several tools that we have to switch between to get stuff done.  And those tools are probably not powershell-ready.  Writing your own host allows you to build your “dream environment”, combining the best features of your favorite tools, and adding script-support in the process.

Next time, we’ll see about doing something different…adding data from powershell into a treeview control (in the host, of course).

As usual, please let me know if you’re enjoying this series.

Mike

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (Part 4)

WARNING:  This is a long post with lots of code!  :-)

In the last post, we got to the point that we ran into the limitatoin of simply running scripts through a bare runspace. You can accomplish quite a bit, but to have the full shell experience, you’ll want to actually create a the host objects, so that the PowerShell engine will know how to handle interacting with the environment. The hint that we were at this point was the error message “System.Management.Automation.CmdletInvocationException: Cannot invoke this function because the current host does not implement it.” Creating a host that does implement “it” is not too difficult, but involves a lot of code. Without further ado, here we go.

There are three classes to inherit from to implement a custom host. They are:

  • System.Management.Automation.Host.PSHost
  • System.Management.Automation.Host.PSHostUserInterface
  • System.Management.Automation.Host.PSHostRawUserInterface

These classes are declared as MustInherit (which is the same as Abstract in C#), and each declares several properties and methods as MustOverride.  To easily generate code for these methods and properties (in SharpDevelop…each tool may or may not have a way to do this),  I wrote simple stub classes for these as follows:

Public Class PowerShellWorkBenchHost
    Inherits System.Management.Automation.Host.PSHost

 End Class

Public Class PowerShellWorkBenchHostUI
	Inherits System.Management.Automation.Host.PSHostUserInterface 

  End Sub

End Class

Public Class PowerShellWorkBenchHostRawUI
		Inherits System.Management.Automation.Host.PSHostRawUserInterface 

End Class

Then, I’m put the cursor in the blank line under the inherits clause in the first class,  PowerShellWorkBenchHost, and selected Auto Code Generation from the Tools menu.  This brings up a dialog that lets you indicate what code to generate.  One of the options is “Abstract class overridings”, which is what we want.  Selecting that shows us a checkbox for the Abstract (MustInherit) class that we’re inheriting from (PSHost).  Checking PSHost and clicking OK fills in the member definitions with some default behavior as shown below:

Auto Code Generate Dialog

Auto Code Generate Dialog

Public Class PowerShellWorkBenchHost
    Inherits System.Management.Automation.Host.PSHost
	Public Overloads Overrides ReadOnly Property Name() As String
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property Version() As Version
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property InstanceId() As Guid
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property UI() As System.Management.Automation.Host.PSHostUserInterface
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property CurrentCulture() As System.Globalization.CultureInfo
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property CurrentUICulture() As System.Globalization.CultureInfo
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides Sub SetShouldExit(exitCode As Integer)
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub EnterNestedPrompt()
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub ExitNestedPrompt()
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub NotifyBeginApplication()
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub NotifyEndApplication()
		Throw New NotImplementedException()
	End Sub

 End Class

Repeating that for the other two classes results in the following:

Public Class PowerShellWorkBenchHostRawUI
		Inherits System.Management.Automation.Host.PSHostRawUserInterface 

	Public Overloads Overrides Property ForegroundColor() As ConsoleColor
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property BackgroundColor() As ConsoleColor
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property CursorPosition() As System.Management.Automation.Host.Coordinates
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property WindowPosition() As System.Management.Automation.Host.Coordinates
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property CursorSize() As Integer
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property BufferSize() As System.Management.Automation.Host.Size
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property WindowSize() As System.Management.Automation.Host.Size
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides ReadOnly Property MaxWindowSize() As System.Management.Automation.Host.Size
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property MaxPhysicalWindowSize() As System.Management.Automation.Host.Size
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property KeyAvailable() As Boolean
		Get
			Throw New NotImplementedException()
		End Get
	End Property

	Public Overloads Overrides Property WindowTitle() As String
		Get
			Throw New NotImplementedException()
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Function ReadKey(options As System.Management.Automation.Host.ReadKeyOptions) As System.Management.Automation.Host.KeyInfo
		Throw New NotImplementedException()
	End Function

	Public Overloads Overrides Sub FlushInputBuffer()
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub SetBufferContents(origin As System.Management.Automation.Host.Coordinates, contents As System.Management.Automation.Host.BufferCell(,))
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Sub SetBufferContents(rectangle As System.Management.Automation.Host.Rectangle, fill As System.Management.Automation.Host.BufferCell)
		Throw New NotImplementedException()
	End Sub

	Public Overloads Overrides Function GetBufferContents(rectangle As System.Management.Automation.Host.Rectangle) As System.Management.Automation.Host.BufferCell(,)
		Throw New NotImplementedException()
	End Function

	Public Overloads Overrides Sub ScrollBufferContents(source As System.Management.Automation.Host.Rectangle, destination As System.Management.Automation.Host.Coordinates, clip As System.Management.Automation.Host.Rectangle, fill As System.Management.Automation.Host.BufferCell)
		Throw New NotImplementedException()
	End Sub

End Class

That’s a lot of code, but it’s not so bad, since I didn’t actually have to write it.  Also, since all of the members simply throw NotImplementedException, it doesn’t accomplish anything.

But it should be clear that a big part of what we need to do now is to fill in the method bodies that implement the features we want to have in our host.

To actually use these new classes in conjunction with the runspace and pipeline that we created last week, we’ll need to modify that code (but only slightly) to reference the new host class:

Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)
	Dim host as New PowerShellWorkBenchHost
        Dim r As Runspace=RunspaceFactory.CreateRunspace(host)
        r.Open()
        Dim p As Pipeline=r.CreatePipeline(txtScript.Text)
        p.Commands.add(new Command("out-string"))
        Dim output As Collection(Of psobject)
        output=p.Invoke()
        For Each o As PSObject In output
            txtOutput.AppendText(o.ToString()+vbcrlf)
        Next
End Sub

If you run the app at this point, it will blow up when you try to run anything.  That’s because there are certain things that must be implemented for the custom host to function.  Other things only need to be implemented if you want to use those features in your host.   The not-so-nice thing is that I haven’t ever found a list that tells you what you actually need to do, so it’s a trial and error kind of thing.  What I did was to put breakpoints on all of the throw statements that were generated, and run the app over and over, trying to run a simple “dir”, and implementing the methods that got hit.  Doing that showed me that the following are pretty much essential to implement:

  • PSHost.UI
  • PSHost.Name
  • PSHost.InstanceID
  • PSHost.CurrentCulture
  • PSHost.CurrentUICulture
  • PSHostUserInterface.RawUI
  • PSHostRawUserInterface.BufferSize

Fortunately, these are all pretty easy to implement.  The Name and InstanceID can be constants, the UI and RawUI properties need to return instances of the classes we inherited from PSHostUserInterface and PSHostRawUserInterface.  The CurrentCulture and CurrentUICulture I just pulled from the Threading.Thread.CurrentThread object (which has CurrentCulture and CurrentUICulture properties).  The BufferSize property refers to the size of the “window” that the console will be writing output to, measured in characters.  I made it 80×80, just to have something to work with.
Here’s what those methods look like (I omitted all of the methods that still throw exceptions to make the listing smaller, but you still need the definitions in your code)

Public Class PowerShellWorkBenchHost
    Inherits System.Management.Automation.Host.PSHost
    Private _instanceID As New Guid("eb30b404-18c2-455d-8271-423039280b9b" )
    private _UI as New PowerShellWorkBenchHostUI
	Public Overloads Overrides ReadOnly Property Name() As String
		Get
			return "PowerShellWorkBenchHost"
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property Version() As Version
		Get
			return new Version(1,0,0)
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property InstanceId() As Guid
		Get
			return _instanceID
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property UI() As System.Management.Automation.Host.PSHostUserInterface
		Get
			return _UI
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property CurrentCulture() As System.Globalization.CultureInfo
		Get
		  Return Threading.Thread.CurrentThread.CurrentCulture
		End Get
	End Property

	Public Overloads Overrides ReadOnly Property CurrentUICulture() As System.Globalization.CultureInfo
		Get
			  Return Threading.Thread.CurrentThread.CurrentUICulture
		End Get
	End Property

 ' LOTS OF OMITTED CODE

 End Class

Public Class PowerShellWorkBenchHostUI
	Inherits System.Management.Automation.Host.PSHostUserInterface
	private _RawUI as New PowerShellWorkBenchHostRawUI
	Public Overloads Overrides ReadOnly Property RawUI() As System.Management.Automation.Host.PSHostRawUserInterface
		Get
			return _RawUI
		End Get
	End Property

'LOTS OF OMITTED CODE

End Class

Public Class PowerShellWorkBenchHostRawUI
		Inherits System.Management.Automation.Host.PSHostRawUserInterface 

	Public Overloads Overrides Property BufferSize() As System.Management.Automation.Host.Size
		Get
			return new system.management.automation.host.size(80,80)
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

'LOTS OF OMITTED CODE

End Class

Now we’re using the host, and actually getting output.  We’re not getting any more output than we were before using the host, but since we haven’t implemented any real host functionality, that’s to be expected.

I was hoping to get something cool (like write-host) working in this post, but I’m afraid it’s already way too long.  I’ll try to bang out another entry tomorrow to follow up.

I’m also thinking of creating a project on CodePlex for the code that I’m writing.  Obviously the limited functionality that I’m implementing in the tutorial is not something that everyone would want, but as a community driven project, it could eventually become considerably better (and since the source would be available, you could take it and do what you want with it).  Just a thought at this point…let me know what you think.

Mike

P.S.  I really would like to hear what you think about this series (and the blog in general).

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (Part 3)

In the last post we started building the app, but ran into a problem with output.   We were able to get output from some scripts (dir, for example, gave incomplete output), but others didn’t give us anything useful at all (get-service, returned “System.ServiceProcess.ServiceController” over and over).

The reason for this is simple.  PowerShell cmdlets (and by extension, scripts) return objects,  not strings.  To get string output, we need to tell the script to output strings rather than ask each object that is output to give us its string representation by calling ToString() on them.

To do this, we could try to do something like surround the script that’s passed in with parentheses, and add “| out-string”, but there’s an easier solution.  The object we’re using to run our scripts is called a Pipeline.  As such, it has a method to append commands.  The “corrected” code is this:

Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)
        Dim r As Runspace=RunspaceFactory.CreateRunspace
        r.Open
        Dim p As Pipeline=r.CreatePipeline(txtScript.Text)
        p.Commands.add(new Command("out-string"))
        Dim output As Collection(Of psobject)
        output=p.Invoke()
        For Each o As PSObject In output
            txtOutput.AppendText(o.ToString()+vbcrlf)
        Next
End Sub

The only new line is the one that contains the “out-string”.  We can even leave the ToString() calls, because we know that string objects’ ToString() will just output the string itself, or at least we hope it would.

With that, here’s the output for “get-service” (note: I changed the font to a fixed-width font):

Fixed Output

That’s much nicer and even has column headers like we’d expect.  With that change, cmdlets that output objects directly to the pipeline will work fine.  But what about cmdlets that output text to the host (like the write-* cmdlets other than write-output)?  Simply trying “Write-host ‘Hello, World.’” gives us a big fat error, but one that gives us and idea what we need to do to fix it: “System.Management.Automation.CmdletInvocationException: Cannot invoke this function because the current host does not implement it.”

That seems like a pretty good breaking point.  Implementing the host (which pretty much involves inheriting from a couple of classes and implementing some basic methods) will take some time, but most of it’s pretty easy.

One thing that I should mention.  I haven’t been specific about what version of PowerShell this series is using.  The reason is that the code so far will work on either 1.0 or 2.0 (and I anticipate that the rest of the code will as well, but I haven’t written the rest yet).  In fact, the custom host that I use at work has no problems running on either 1.0 or 2.0.  I’ve been very impressed with the PowerShell team and their commitment to making PowerShell 2.0 backwards compatible as far as possible.  I expected that this effort would end as soon as I got into the object model, but I have yet to find anything that I’ve written for 1.0 that hasn’t worked in 2.0.  Now there’s a lot of stuff that can be written for 2.0 that won’t work in 1.0, but that’s to be expected.

Speaking of 2.0, the final release of 2.0 (for XP and Win2k3) showed up today, much to my surprise.  Kudos again to the PowerShell team for a very quick release schedule following last week’s Windows 7 release.  If you haven’t already, I definitely recommend getting 2.0 downloaded and installed so you can try out all of the neat stuff that’s included.  I especially recommend trying the out-gridview cmdlet!

Mike

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (Part 2)

In the last post, I discussed some of the reasons why you might want to write your own PowerShell hosting app.  I realized later that I didn’t define what that meant.

In general, there are 2 ways to include PowerShell technology in an application.

  1. Use the PowerShell objects (in the System.Management.Automation.* namespaces) to execute scripts, and use the objects that are returned in your code.
  2. Create a custom “host” for PowerShell, providing the PowerShell engine with the ability to interact with the environment.

With the first option, you have access to the input, output, and error streams of the PowerShell pipeline (which is how PowerShell represents a piece of running code).  With the second option, you also have the ability to handle other output like debug, verbose, and warning, as well as handling prompts for things like read-host and get-credential.

In general, you can get quite a lot done with the first approach, and that’s how we’re going to start.  Adding the custom host won’t involve rewriting much code, so it makes more sense to start out easy.

A few more things before we start coding:  First,I’m going to use VB.NET rather than C#.  I know this is probably a turn off for some of you (sorry), but there are a some good (I think) reasons to do this.

  1. Almost all example .Net code dealing with PowerShell is C#
  2. Administrators are more likely to be familiar with vbscript, so VB.NET may be more approachable.
  3. Most of the actual code for dealing with PowerShell is pretty simple, so it won’t be hard for C# folks to modify it.
  4. (the real reason)  I don’t have a history of writing C#, and I don’t really want to start my efforts in that direction in a blog post.  :-)

And now, on to the code.  I’m going to use SharpDevelop, because it’s possible that you want to do something like this, but don’t have the budget (as an admin) to have development tools.  SharpDevelop is a free, open-source IDE for .NET languages.  It is very similar to Visual Studio, and includes a lot of features.  Did I mention that it’s free?

Now, on to coding.  I’m envisioning a simple screen with an area to enter PowerShell code, and an area to view the output.  I started by creating a new VB.NET Windows Application.  I then added a menustrip, a splitter, and two textboxes (one above the splitter, and one below).  I set both textboxes to multiline and set their dock property to fill.  I also right-clicked on the menustrip and selected “Insert Standard Items”  Clicking the Run button should give you something that looks like this:

Screenshot 1

Screenshot 1

It’s nothing spectacular, but this isn’t a post about writing a spectacular interface.  This is about PowerShell in a GUI.  Now to add the PowerShell.

You’re going to need to reference to System.Management.Automation (right-click on the References node in the Projects window, select Add Reference, and select System.Management.Automation from the list on the GAC tab).   You will probably want to add the following to the top of the .vb file:

Imports System.Management.Automation
Imports System.Management.Automation.Runspaces
imports System.Collections.ObjectModel

Now, we’re going to need to add a menu item to run the code.  I added a “Run” menu with an item under it called “Run Script” that has a shortcut key of F5.  Double-clicking on the “Run Script” menu item brings up the code editor in the handler for the menu item.  Here’s the code I put in it.

    Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)
        Dim r As Runspace=RunspaceFactory.CreateRunspace
        r.Open
        Dim p As Pipeline=r.CreatePipeline(txtScript.Text)

        Dim output As Collection(Of PSObject)
        output=p.Invoke()
        For Each o As PSObject In output
            txtOutput.AppendText(o.ToString()+vbcrlf)
        Next
    End Sub

With that, I clicked the run button in SharpDevelop, and behold:

The Finished(?) App

The Finished(?) App

Typing the simplest PowerShell script I could think of (dir) and selecting Run (or hitting F5) provides the desired output.  That’s a good start, but to see a shortcoming, look what happens when I try something more adventurous (After shrinking the size of the app so the screenshots aren’t so obnoxious):

Not So Useful Output

Not So Useful Output

Since the System.Process.ServiceController class didn’t have a very friendly ToString() method, we’re kind of out of luck. And hey, the output from the “dir” example wasn’t anywhere close to what you would normally see in a PowerShell prompt. What’s up with that?

Tune in next time for the reason. I’ll try to get it written Saturday, but no promises.

In the meantime, play around with the code and see what you can accomplish. And let me know what you think of my choice of VB.NET and SharpDevelop.

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Writing your own PowerShell Hosting App (Part 1. Introduction)

I’ve mentioned before that I use a homegrown PowerShell host in my work.  I have been more than pleasantly surprised at how easy and how rewarding this is.  In the last few weeks, I’ve seen a few articles that have gotten me thinking about writing a series of blog posts about how to get started.

Before actually writing anything, it’s good to ask yourself…why in the world would I write a host when there are so many out there already (ISE and PowerGUI are notable free examples)?  This is a really important question and one that will stop most projects in their tracks.  Most people can get what they need using an existing host.  Here are some of the reasons I chose to write  a host:

  • I wanted complete control over the environment, as I knew (hoped) that I would be spending a lot of time using it
  • I wanted to be able to interact with the environment in ways that the existing tools didn’t allow
  • I was constrained to use PowerShell 1.0 (which eliminates the ISE)

But probably the most pressing reason in reality was:

  • I had a book (link) that explained the technology and I wanted to play  :-)

Unlike most (some?) administrators, I have a development background and even have Visual Studio installed on my machine, so testing the waters of writing a host wasn’t a big investment of time, and the pleasure of seeing something like this come together was well worth it.

Here are the posts that got my mind going again:

Create your own IDE in 10 minutes

How to Host PowerShell in a WPF Application

In the next post, I’ll start the project and give you something to look at.

Let me know if there’s anything specific you’d like to see (or have experience implementing).

Mike

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark

Is it just me? (Or does PowerShell remind you of SQL?)

When preparing a PowerShell training class for a group of DBAs, I realized that there were some parallels between basic SQL and basic PowerShell commands.

A (very) basic SQL statement has the form:

SELECT <COLUMNS>
FROM <TABLE>
WHERE <CONDITION>
ORDER BY <EXPRESSION>

I noticed that a very common idiom for PowerShell pipelines* was:

<data source cmdlet> | select-object <properties> | where-object <CONDITION> |  sort-object <EXPRESSION>
 

By “<data source cmdlet>”, I mean some cmdlet that puts a bunch of objects in the pipeline, like get-childitem, get-process, get-task, etc.

Part of the power of SQL is that it doesn’t matter what kind of data is in the tables, the same form of SQL statement works the same way (predictability).  This is one of the things I love about PowerShell.  It doesn’t matter what kinds of data is returned by a cmdlet.  The same form of PowerShell pipeline* will perform the same kind of predictable operations on it.  I know that this is often mentioned in tutorials and videos about PowerShell, but this was when it really struck me. 

A few other SQL/PowerShell comparisons might be:

SQL

PowerShell

GROUP BY group-object
SUM(), AVG(), etc. measure-object
Cursors foreach-object loops
SELECT DISTINCT select-object –unique
SELECT TOP n select-object –first n

 

Obviously, this comparison breaks down pretty quickly.  There isn’t really a parallel that I can find to JOIN statements, which make SQL so powerful, and clearly there’s a lot of powershell scripts that don’t fit the pattern I’m describing.  I think, though, that it’s a useful comparison and can help get people “over the hump” in their quest to master PowerShell.

Let me know what you think.

Mike

* A Pipeline in PowerShell is a sequence of cmdlets where each takes the output of the previous cmdlet as its input.

  • Digg
  • Slashdot
  • Reddit
  • Tumblr
  • Delicious
  • Twitter
  • Google Bookmarks
  • StumbleUpon
  • Technorati Favorites
  • Google Reader
  • Share/Bookmark