Skip to content
Archive of posts tagged VB.NET

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