PowerShell and IronPython 1
The (relatively) new kid on the block for system administrators is PowerShell. PowerShell extends the concept of shell scripting to allow you to pipe objects between commands instead ofjust data. It's essentially a programming language (cleverly disguised as a scripting environment) specialized for Windows system administration. We know what you're thinking; you have Python—why would you need another language?
NOTE There's an open source implementation of PowerShell for Mono called Pash (PowerShell + bash). See http://pash.sourceforge.net/ for more details. It aims to be a faithful implementation of PowerShell, with the project page proclaiming the user experience should be seamless for people who are used to Windows' version of PowerShell. The scripts, cmdlets and providers should runs AS-IS (except where they use Windows-specific functionality).
In this section, you'll see that IronPython and PowerShell can interact in two different ways. We use PowerShell commands and APIs directly from IronPython, and we also use IronPython in PowerShell as a way of overcoming some of PowerShell's limitations.
10.3.1 Using PowerShell from IronPython
The normal way to use PowerShell is as a replacement command line. Running PowerShell opens a console window that looks much like the normal Windows command prompt, cmd.exe, but is in fact much more like the Python interactive interpreter. You execute PowerShell commands that return objects, which you can store or pipe to other commands. You can see the PowerShell command prompt in figure 10.4.
Windows PouferShell
Copyright (C) 2906 Microsoft Corporation. All rights reserved.
PS C:MJsersNAdminlstrator> iipy = Get-Process -Name ipy PS C:\Users\Administ rator> tipy
Handles NPM(K) PM(K) USCK) VM(M) CPU(s) Id ProcessName
273 11 21608 26260 "l46 "¿"21 3603 ipy"
PS C:\Users\Admini5trator> iipy.Path e:\Dev\ironpythonl\i py.exe PS C: Misers\Admi nis t rator> ^
Figure 10.4 The PowerShell interactive environment
PowerShell processes the output of its commands as .NET objects. The commands themselves (cmdlets) are usually thin wrappers around .NET classes. The PowerShell infrastructure provides argument parsing and binding, a runtime, and utilities for formatting and displaying results. This infrastructure is provided through a set of .NET assemblies installed when you install PowerShell. The top-level namespace for this infrastructure and its accompanying APIs is System.Management.Automation.14 This is an apposite name. Automation is at the heart of systems administration. Humans are unreliable and the more we can automate, and keep humans out of the process, the better. Naturally, these namespace are available to use from IronPython.
NOTE To follow these examples, you'll need PowerShell 1.0 installed.15 This section isn't a comprehensive introduction to PowerShell. If you want to learn more about PowerShell, then Windows PowerShell in Action by Bruce Payette (Manning, 2007) is a great resource.
The simplest way to access PowerShell functionality from IronPython is by creating a runspace, which is a kind of execution scope for PowerShell commands. The PowerShell commands live in a different namespace, Microsoft.Powershell.Commands. You can use a runspace to execute commands by name, and don't need to directly reference this namespace. THE POWERSHELL RUNSPACE
Listing 10.13 invokes a PowerShell command in a runspace and uses the object that the command returns.
Listing 10.13 Executing PowerShell commands from IronPython import clr clr.AddReference('System.Management.Automation') from System.Management.Automation import RunspaceInvoke
14 See http://msdn2.microsoft.com/en-us/library/system.management.automation.aspx.
15 PowerShell can be obtained from http://www.microsoft.com/powershell.
runspace = RunspaceInvoke()
psobjects = runspace.Invoke("Get-Process -Name ipy") process = psobjects [0] <1-
print 'Path = ', process.Properties['Path'].Value for prop in process.Properties: name = prop.Name if name in ('ExitCode', 'ExitTime', 'StandardIn' , 'StandardOut', ' StandardInput', 'StandardOutput', 'StandardError'):
# Can't fetch these on a process
# that hasn't exited or redirected
# the in/out/error streams continue <— Skips properties that raise errors print prop.Name, prop.Value
IronPython, PowerShell, and COM
Automation with IronPython and COM is a big topic that we don't have the space to cover.16 As well as using PowerShell for easy access to WMI, you can use it to work with COM. The following snippet shows how to use COM from PowerShell to sync an iPod with the iTunes application:
PS > $app = Get-Object -ComObject iTunes.application PS > $app.UpdateIPod()
The call to Invoke returns a collection of PSObject objects, which you can interact with. One use case is to take advantage of the WMI/PowerShell integration, which can make it easier to work with certain aspects of WMI. Listing 10.14 uses the Get-WmiObject command to examine the video controller and the CPU and to find a running process.
Listing 10.14 WMI from PowerShell inside IronPython!
import clr clr.AddReference('System.Management.Automation') from System.Management.Automation import ( PSMethod, RunspaceInvoke
runspace = RunspaceInvoke()
cmd = "Get-WmiObject Win32_VideoController" psobjects = runspace.Invoke(cmd) video = psobjects [0]
print print 'Video controller properties' for prop in video.Properties: print prop.Name, prop.Value
16 There are several good examples of using COM from IronPython on the IronPython Cookbook, including a good introduction, at http://www.ironpython.info/index.php/Interop_introduction.
<— Executes command Pulls out first result psobjects = runspace.Invoke("Get-WmiObject Win32_Processor") cpu = psobjects [0]
print print 'CPU properties' A WMI query for prop in cpu. Properties : from PowerShell print prop.Name, prop.Value cmd = ' Get-WmiObject Win32_Process -filter \ 'Name="ipy.exe"\' ' <-
psobjects = runspace.Invoke(cmd) ipy = psobjects [0]
print print 'WMI process methods' I Loop through c , ■ ■ v, _i all members for member in ipy. Members: <-
continue | Find the methods print member
You'll notice that the last command uses the filter keyword. This is a WMI query that uses PowerShell rather than WQL syntax. Like the WMI objects we've already worked with, PowerShell objects have a Properties collection that you can iterate over. They also have Methods and Members collections. Unfortunately, I got null reference exceptions when accessing the Methods collection; but you can find methods by iterating over all members and checking for instances of the PSMethod type.
The IronPython PowerShell sample
The IronPython team has provided a wrapper around a lot of this functionality in the PowerShell sample.17 You can directly invoke PowerShell commands on the shell object they provide, by calling methods with lowercase command names and underscores instead of dashes.
>>> from powershell import shell
>>> shell.get_process('notepad').stop_process()
MULTIPLE COMMANDS AND THE PIPELINE
The RunspaceInvoke instances are great for executing individual commands, but you can achieve more by creating a pipeline. This gets you, in effect, a PowerShell environment embedded into IronPython. Listing 10.15 creates a pipeline, adds commands to it, and then invokes the whole pipeline.
Listing 10.15 The PowerShell pipeline import clr clr.AddReference('System.Management.Automation') from System.Management.Automation.Runspaces import ( RunspaceFactory
17 You can download the samples from the IronPython 2.0 release page on CodePlex.
runspace = RunspaceFactory.CreateRunspace() runspace.Open()
runspace.SessionStateProxy.SetVariable("processName", 'ipy') pipeline = runspace.CreatePipeline()
pipeline.Commands.AddScript('Get-Process -Name $processName') pipeline.Commands.Add('Out-String')
results = pipeline.Invoke() for result in results: print result
This code uses a different technique to create the runspace—from a factory that returns a Runspace18 instance, which you must Open before using it. The runspace also has an OpenAsync method, which opens it in another thread.
The code also sets the processName variable in the execution environment via the SessionStateProxy. These APIs are analogous to the IronPython hosting API, and could be useful if you want to expose a PowerShell scripting environment to your users!
The last command added to the pipeline command collection is the Out-String command. This formats the results using the PowerShell pretty printer so that, when you print the results, you get nicely formatted output like the one in figure 10.5.
QQSdtclAdnniniitukic C: .V nik-v...^-iiJ.f = . _ □ *
Z : Wo 1 umesVSecond Dr i ve\I ronPyt honBock'vsourcecode\chapterl0\10.3 . l>e : V | DevNironpyt hon IMpy . exe powers he ll^pipel ine.py
Handles NPM(K> PM(K> WSIK} VM(M) CPU(s) Id ProcessName
Z: WolumesSSecond Dri ve\IronPythonBock\soureecode\chapterl0\10. 3. Figure 10.5 The formatted output from a PowerShell pipeline
We've looked at one side of the coin: embedding PowerShell in IronPython. Let's move into the flip side.
10.3.2 Using IronPython from PowerShell
Because PowerShell is a .NET scripting environment, it can use .NET assemblies and objects. The IronPython interpreter is an ordinary (for some value of ordinary) .NET object and can easily be used from other .NET applications, which includes PowerShell.
So why on earth would you want to do this? Well, it turns out that you can use Iron-Python to overcome certain limitations with PowerShell. These limitations include operations that would block the console or actions that should only be done from an STA thread and don't work directly from PowerShell, which runs in a Multi-Threaded
18 See http://msdn2.microsoft.com/en-us/library/system.management.automation.runspaces.runspace.aspx.
263 45S
11 21556 16 46260
26252 144 2.21 3600 ipy 45744 190 3.42 3620 ipy
Apartment (MTA).19 You can also use IronPython from within PowerShell to work with Python libraries.
EMBEDDING IRONPYTHON IN POWERSHELL
You embed IronPython via its hosting API—which is something we'll explore in more detail when we look at providing a scripting API to a .NET application with Iron-Python. IronPython 1 and 2 have different hosting APIs, so how you access IronPython from inside PowerShell depends on which version of IronPython you have.
Executing the examples
To execute the example scripts,20 you'll need to set the execution policy to allow unsigned scripts. The PowerShell command to do this is as follows:
Set-ExecutionPolicy Unrestricted
For more information about script signing, you can execute the following command:
Get-Help About_Signing
Listing 10.16 shows the PowerShell code necessary for executing code with Iron-Python 1. It assumes you have the IronPython assemblies in the current working directory.
Listing 10.16 IronPython 1 in PowerShell
$full_path = Resolve-Path $cur_dir 'IronPython.dll'
[reflection.assembly] : :LoadFrom($full_path) <— Loads IronPython assembly $engine = New-Object IronPython.Hosting.PythonEngine
$engine .Execute ("print 'Hello World! from IP1' ") <- Executes code from string
The call to load assemblies requires an absolute path, which you construct with a call to Resolve-Path (which resolves paths relative to current working directory). Having constructed an IronPython engine, Python code is executed with the Execute method.
The IronPython 2 hosting API
The code here is written against the hosting API of IronPython 2.0.
These examples only use a small part of the IronPython hosting API. Chapter 15 has a much more in-depth look at embedding IronPython in other .NET environments, and many of the techniques shown there could also be used from PowerShell.
19 PowerShell 2 will support an -sta command-line switch. Even then this solution could be useful because it will allow you to access STA functionality without having to start PowerShell with particular command-line arguments.
20 Downloaded from http://www.ironpythoninaction.com/, of course.
Listing 10.17 shows the equivalent for code for IronPython 2. The code is more complicated because IronPython 2 is built on the DLR and the hosting API is more generic.
Listing 10.17 IronPython 2 in PoweiShell
$base_dir_env = Get-Item env:IP2ASSEMBLIES <-
$base_dir = $base_dir_env.Value
$first_path = Join-Path $base_dir 'Microsoft.Scripting.dll $second_path = Join-Path $base_dir 'IronPython.dll' [reflection.assembly]::LoadFrom($first_path) [reflection.assembly]::LoadFrom($second_path)
$engine = [ironpython.hosting.python]::CreateEngine() $st = [microsoft.scripting.sourcecodekind]::Statements $code = 'print "Hello World from IP2 ! " '
$source = $engine.CreateScriptSourceFromString($code, $st) $scope = $engine.CreateScope() $source.Execute($scope)
This snippet uses a different technique to load the assemblies. It assumes you've set an environment variable IP2ASSEMBLIES with the path to a directory containing the Iron-Python 2 assemblies.
To execute code you have to create a script source from the code string and the SourceCodeKind.Statements enumeration member. The syntax to do this in PowerShell is somewhat ugly. The obvious thing to do is to abstract this little dance out into a function like listing 10.18.
Listing 10.18 Executing Python code from a function in PoweiShell
$base_dir_env = Get-Item env:IP2ASSEMBLIES $base_dir = $base_dir_env.Value
$first_path = Join-Path $base_dir 'Microsoft.Scripting.dll' $second_path = Join-Path $base_dir 'IronPython.dll'
[reflection.assembly]::LoadFrom($first_path) [reflection.assembly]::LoadFrom($second_path)
$global:engine = [ironpython.hosting.python]::CreateEngine() $global:st = [microsoft.scripting.sourcecodekind]::Statements
Function global:Execute-Python ($code) {
$source = $engine.CreateScriptSourceFromString($code, $st) $scope = $engine.CreateScope() $source.Execute($scope)
This listing creates a function, which executes code that you pass in as a string. Power-Shell's scoping rules are very different from Python's.21 The global keyword makes Execute-Python available to the interactive environment when this code is executed from a script. Because PowerShell is dynamically scoped, all the variables the function uses also have to be global because they'll be looked up in the scope that calls the function.
21 And not at all better in our opinion. Dynamic scoping is designed with interactive use in mind, and is the same as the scoping rules used by Bash.
Fetches
IP2ASSEMBLIES
environment variable
Turns Python code into ScriptSource
Execute-Python is called, as follows: Execute-Python 'print "Hello world from PowerShell"'
You can build on this general technique, whether working with IronPython 1 or 2, to do various things useful from within the PowerShell environment. CREATING STA THREADS
PowerShell runs in an MTA thread, which causes problems for code that has to be called from an STA. This prevents you using Windows Forms objects, such as calling Clip-board.SetText to put text on the clipboard. You can get around this by spinning up an STA thread from IronPython and setting the clipboard from there22 (listing 10.19).
WARNING Unhandled exceptions inside threads will cause PowerShell to bomb out and die! You will get the exception traceback when it happens. Running PowerShell from cmd.exe rather than launching it from the start menu will give you a chance to read the traceback.
Listing 10.19 Setting the clipboard from PowerShell with IronPython 1
$global:ClipCode = @' import clr clr.AddReference("System.Windows.Forms") from System.Windows.Forms import Clipboard from System.Threading import ( ApartmentState, Thread, ThreadStart
def thread_proc():
Clipboard.SetText(text)
t = Thread(ThreadStart(thread_proc)) t.ApartmentState = ApartmentState.STA
Function global:Set-Clipboard ($Text){ $engine.Globals["text"] = $Text $engine.Execute($ClipCode)
This code works with IronPython 1 and assumes you've already created the IronPy-thon engine as the $engine variable (and made it global). The reason this code is specific to IronPython 1 is that it sets the text variable in the Python engine Globals so that the IronPython code can use it to set the text on the clipboard. To make this code work with IronPython 2, you need to create an explicit execution scope and set the variable in there. You then need to pass the scope in when you call Execute on $ClipCode, and this is where the fun starts.
When you call Execute with one argument (a ScriptScope), it becomes a generic method. Calling generic methods from PowerShell is non-trivial. Luckily, Lee
22 Many thanks to Marc, The PowerShell Guy, who provided the original code for this example.
Holmes has solved this problem, so you'll use his Invoke-GenericMethod23 script to invoke Execute.
Again assuming that you've already created an IronPython engine, listing 10.20 creates a Set-Clipboard function that sets text on the clipboard using IronPython 2.
Listing 10.20 Setting clipboard from PowerShell with IronPython 2
$global:scope = $engine.CreateScope()
$global:ClipCode = $engine.CreateScriptSourceFromString(@' import clr clr.AddReference("System") clr.AddReference("mscorlib") clr.AddReference("System.Windows.Forms") from System.Windows.Forms import Clipboard import System from System.Threading import Thread, ThreadStart def thread_proc():
Clipboard.SetText(text)
t = Thread(ThreadStart(thread_proc))
t.ApartmentState = System.Threading.ApartmentState.STA t.Start() '@, $st)
Sets text in scope
Function global:Set-Clipboard ($Text){ $scope.SetVariable('text', $Text) <— $params = @('microsoft.scripting.hosting.scriptscope') ./Invoke-GenericMethod $ClipCode 'Execute' $params $scope
Type of parameter for Execute
Invokes Execute on $ClipCode with $scope
Another difference between this code and the code for IronPython 1 is that, for Iron-Python 2, you need to explicitly add references to the system assemblies, both Sys-tem.dll and mscorlib.dll. In IronPython 1, the PythonEngine does this, but not in IronPython 2.
The code that finds the right generic overload of Execute isn't pretty, but it's abstracted away in the Invoke-GenericMethod script. The call parameters are as follows:
./Invoke-GenericMethod instance MethodName params arguments params should be an array of strings with the type names of the arguments. The arguments parameter is the set of arguments that Execute is to be called with, passed in as an array of objects. If you pass in an individual string and an individual object for params and arguments, then PowerShell will cast them into arrays. ASYNCHRONOUS EVENTS WITHOUT BLOCKING
The next use case for IronPython from PowerShell is for handling events. In .NET, asynchronous events are raised on another thread, preventing you from using PowerShell script blocks as event handlers. The usual solution is to wait for the event to be raised on the main execution thread, which blocks the console. You can get around this by subscribing to the event from IronPython.
See http://www.leeholmes.com/blog/InvokingGenericMethodsOnNonGenericClassesInPowerShell.aspx.
Listing 10.21 uses the EventLog class,24 and its EntryWritten event, to print the details of any messages written to the Windows event logs.
Listing 10.21 Handling asynchronous events from PowerShell with IronPython
$source = $engine.CreateScriptSourceFromString(@' import clr clr.AddReference('System')
from System.Diagnostics import EventLog def handler(sender, event):
print 'Entry from', sender.Log entry = event.Entry print entry.Message logs = EventLog.GetEventLogs() for log in logs: try:
log.EnableRaisingEvents = True log.EntryWritten += handler print 'Added handler to', log.Log except:
$scope = $engine.CreateScope() $source.Execute($scope)
After running this code, control returns immediately to the console. To see your event handlers in action, start a new program, or perform any action that causes writes to event logs, and you'll see the log messages appear at the console. You can see the start of one of these messages in figure 10.6.
So far we've been using IronPython to access .NET features from PowerShell. Because PowerShell has native access to most of .NET, bar a few limitations, a more compelling reason to use IronPython is to access Python itself. In particular, you can use IronPython to take advantage of Python libraries.
j Select LVindmv; PöiverShelL
Failed to add handler to HardwareEvents Failed to add handler to Internet Explorer Failed to add handler to Key Management Service Failed to add handler to Media Center Failed to add handler to ODiag Added handler to OSession Added handler to Security Added handler to System Added handler to Windows PowerShell
PS Zi Wo 1 urnes\Second Drive\IronPythonBook\5ourcecodeVchapterl0Nl0. 3. 2>
Entry from Windows PowerShell Provider "Alias" is Started.
Details :
ProviderName=Alias Neu»ProviderState=Started
SequenceNumber=l
Figure 10.6 Listening to the Windows event logs
See http://msdn2.microsoft.com/en-us/library/system.diagnostics.eventlog.aspx.
CALLING PYTHON CODE AND RETURNING RESULTS
Using the same pattern as the previous examples, you can create a PowerShell function that calls into Python code and returns the result. In theory, you could do this with a single expression, creating the ScriptSource with SourceCodeKind.Expression rather than SourceCodeKind.Statements.25 Calling generic methods that return values becomes an even bigger world of pain, but there's a simple way around this: you can assign the return value to a variable and fetch that back out of the scope. The basic pattern is as follows:
$script = $engine.CreateScriptSourceFromString($src, $st) $scope.SetVariable('value', $value)
./Invoke-GenericMethod ...
$scope.TryGetVariable('result', $result) $result.Value
Fetching the result out of the scope is done with TryGetVariable, which takes an out parameter. You do this from PowerShell by creating a [Ref] type. You fetch the resulting value by accessing the Value property after the call to TryGetVariable.
Listing 10.22 pulls all this together. It provides two functions, B64Encode and B64Decode, that can encode and decode strings with the base64 encoding, using the base6426 library from the Python standard library.
Listing 10.22 Calling Python functions and returning values
$setupSrc = @' import sys sys.path.append(r'c:\Python2 5\lib')
impo r t ba se 64<j_| Setup code that imports base64
$init_code = $engine.CreateScriptSourceFromString($setupSrc, $st) $src = 'result = base64.b64encode(value)'
$global:encode = $engine.CreateScriptSourceFromString($src, $st) $src = 'result = base64.b64decode(value)'
$global:decode = $engine.CreateScriptSourceFromString($src, $st) ./Invoke-GenericMethod $init_code 'Execute' $params $scope <-, Executes
Function global:B64Encode ($value) { I setup code
$scope.SetVariable('value', $value)
./Invoke-GenericMethod $encode 'Execute' $params $scope | out-null [Ref] $result = $null
$scope.TryGetVariable('result', $result) | out-null
} I Returns result
Function global:B64Decode ($value){ $scope.SetVariable('value', $value)
25 Assumin that you're working with the IronPython 2 API.
26 See http://docs.python.org/lib/module-base64.html.
< I Fetches result
./Invoke-GenericMethod $decode 'Execute' $params $scope | out-null [Ref] $result = $null
$scope.TryGetVariable('result', $result) | out-null $result.Value
PowerShell functions return all unhandled output. Inside B64Encode and B64Decode, unneeded values are suppressed by piping them to out-null. The real result is returned by $result.Value, and it does in fact work!
PS C:\> $a = B64Encode 'This really works! ' PS C:\> $a
VGhpcyByZWFsbHkgd2 9ya3M= PS C:\> B64Decode $a This really works!
Extending this example to call Python functions that take or return multiple values would be simple—just set and fetch more variables in the scope.
One interesting, if slightly insane, way of using this would be when embedding PowerShell into IronPython. You could pass in a scope populated with Python callback functions, and call into them from PowerShell as a way of communicating between the environments.
PowerShell is an interesting new programming environment. We're not about to give up IronPython for PowerShell, but it's great to see that these two systems can work well together. After summarizing this chapter, we'll move on to using IronPython with a completely different system.
Post a comment