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