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).