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



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 80x80, 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).