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