|
|
October 25 I got the following query in one of my Google hits today: "pipeline.Invoke() "Read-Host" Cannot invoke this function because the current host does not implement it." I believe that this query was in reference to the lack of host functionality in the default settings for a runspace in Windows Powershell. I don't recall ever actually taking much about this, yet I am known for having abstracted the runspace API for use as a multi threading interface in Powershell. Now is a good time to talk some about this. A runspace in Powershell is a big blob of information that pertains to a particular shell within Powershell. The runspace has several components within it, such as the cmdlets, aliases, functions, providers, etc. The host component specificly is ment to be used as an abstract interface to the console. The default host object used by a runspace doesn't have much functionality, so you may encounter error messages like the one above when you are using a generic host object. It is possible to tell Powershell what host object should be used when the runspace is initialized. I do know that version 1.0 of powershell.exe will use a host that writes to the console used by the powershell.exe process. Since most other instances of Powershell don't have a console the host object will often be the default host object, that doesn't provide any functionality on its own, or a very simple host object, using only implementing a few methods in the PSHostUserInterface class. From the query above it looks like this googler was trying to use Read-Host within an asynchronous runspace. The easy solution to this is to just not do so. It's easier, in most cases, to simply pass the needed information into the asynchronous runspace from the parent runspace. However, there are still some cases where you must use Read-Host from an asynchronous runspace. In that case you'll have to code and compile your own host class, and use that when initializing the runspace. October 22 I finally added support for GnuPG to Library-StringCrypto. The previous functionality to encrypt strings in process is still present and continues to work as expected. Not a couple more parameters have been added to support symmetric and asymmetric encryption using GnuPG. The new parameters are 'gnupg', to enable use of gpg.exe, and 'recipient', to identify the person the encrypted message is for. When using GnuPG passwords/passphrases may be supplied in 'password' parameter or via the console to the gpg.exe process. To use symmetric encryption, with GnuPG just add the gnupg switch parameter. Write-EncryptedString message secret -gnupg
You can also omit the password from the command line and enter it interactivly. Write-EncryptedString message -gnupg
To use asymmetric encryption, omit the password and use the recipient parameter. Write-EncryptedString message -gnupg -recipient 'John Doe'
Write-EncryptedString returns the ASCII armored version of the ciphertext. Read-EncryptedString will be able to detect the ASCII armor and automatically process it using gpg.exe. Decryption should just work as expected. The password parameter can be used for both symmetric and asymmetric encrypted messages. Also the passphrase may be interactively entered via the console to gpg.exe. Usage notes: The command 'gpg' is expected to resolve to gpg.exe, so you may need to add a path entry or alias gpg to wherever your copy of gpg.exe is. When the password is given via the password parameter, it is passed to gpg.exe as the first line of its standard input. This means the password will not show up in its command line. This also means that passphrases are limited to one line. No check is performed to ensure the password is not longer than one line. If the passphrase is longer than one line then only the first line will be used as key material, the rest will appear at the start of the decrypted message. Some cryptanalytic stuff: Using GnuPG via this script should be no less secure than using GnuPG by any other method in Powershell. However, because this involves communication with another process, using GnuPG does have a larger attack surface for side channel attacks than simply performing the encryption using the CLR. i.e. Either your installation of the .NET Framework or your installation gpg.exe may be compromised, but gpg.exe alone being compromised does not effect the .NET Framework. Download Library-StringCrypto.ps1 here.May 15 Today, I went back over the scripts that I use in Powershell to perform encryption to give them and update. I wanted to have a simple script that will perform encryption over a string, using a string as a password, and return a string, because strings are easy to manage in a shell. (I had been using a really complex but really awesome object based encryption script.) Before I started I took a look at what was available on the web. As it turned out there isn't much out there and what is out there is garbage. In particular I saw http://poshcode.org/116 with it's hard coded salt and weak IV selection. It's key schedule as a whole is worthless. So I open up PSPad and start typing. About an hour of research and 104 lines of code later, I now have a string encryption script that better adheres to cryptoanalytic recommendations, is easier to use, and even supports compression before encryption. I'm normally used to being the first to release any script of any specific genera and don't have any reason to speak down on any other script in that genera. However, in this case, seeing as how poorly the predecessor handles key information, I have to strongly recommend switching to my script as soon as possible if you have been using the script linked to above. To be serious now, since you're data is important enough to encrypt, I would bet you would be rather upset if someone managed to crack your password when your encrypted data does get leaked. The cryptanalysis of my script is quite simple. The stream cipher is Rijndael, and the key schedule is RFC2898 with a random 256 bit salt that is tacked on to the cipher message(just as it should be). Don't understand all that? Don't worry. The only weakness here will likely be the passwords you use. I'm sure you have heard it a hundred times: make passwords that are hard to predict and change passwords as often as possible. You can make the password as long as you want and use any characters you want. Also don't store passwords anywhere someone else might see them. For further cryptanalysis look up Rijndael and RFC2898. I had to add in compression because the moment just before encryption is the last chance you get to do compression. Not only is it the last chance, but generally also the best time to perform compression. Compression works best on uncompressed data and when there is a great amount of it. Compression also will not work at all on encrypted data. Beware that compression will not work for very small parcels of information. You'll want a string of at least 1500 characters before you can get any benefit out of using compression. Library-StringCrypto.ps1I can't imagine any uncool way to use this so if you find a use for this I would like a comment here or on Twitter @aitsusan so I can hear about it. Important Update:I have already rewritten this script. The changes are wide enough to make the old version and the new version incompatible, but I'm going to use the same script name anyway, because I feel this update to be very important for the security of any person that uses this script. If you have Library-StringCrypto.ps1 version 1.0 please download version 2.0 now. The change is an addition of an HMAC. What this means is that now the script can more effectively detect any corruption of modification to the encrypted data. In version 1.0 no direct method of detecting corruption was implemented. This raises a potential security hole where an attacker could modify the cipher string in a way that would result in garbage being returned in the decrypted string, and not having any exception thrown. The lack of a HMAC before could have caused a script, that did not on it's own perform any authentication, to reveal some information about the encrypted data to an attacker or could have caused the script to otherwise behave in an unexpected manner. Now, with the HMAC in place an exception is thrown before any of the data is decrypted, none of the remaining encrypted data can be leaked and the script should then terminate. March 26 Check out this blog post from the Powershell Team. It shows how you can make variables in Powershell behave as global static properties. Be sure to grab the New-ScriptVariable script. Note that it uses the Add-Type cmdlet available in Powershell 2.0. If you don't have version 2.0 then Add-Type can be replaced with my New-CAssembly script. It's a really easy conversion. In fact I already converted it and is available from my SkyDrive. December 07 I have just released version 2.0 of PSGhost. Internally PSGhost has many changes, but its behavior is otherwise not changed. The biggest change is that commands are no longer invoked within PSGhost. It interprets its command line arguments as being a command for Powershell.exe. PSGhost encodes the command and hands it over to Powershell.exe, and prevents the console window from appearing. A few benefits of this is that the binary is much smaller, down from 16KB to 5KB. Also, the memory requirement has been significantly reduced. I'm not sure why, but, when loading System.Windows.Forms.dll and showing a simple message box, version 1 of psghost.exe used about two to four times as much memory as powershell.exe. Another thing is that since commands are invoked within a hidden powershell.exe process any console applications called by those commands are also hidden. Version 1 was not able to hide the console windows of console applications used by psghost scripts. The one drawback is that since powershell.exe is used as the actual host then there is no direct way to know whether the console window can be used for user input. Any command going into PSGhost must not attempt to use either the console or the powershell host object to get user input, but must know beforehand that the only way to get user input is to create a GUI using WinForms or WPF. There may still be some room for improvement in PSGhost. Version 2 doesn't use the same profile that was used by version 1. PSGhost definitely has the potential to perform some pre processing on the command to do things like use a separate profile. Otherwise I see version 2.0 as being the completion of the project. The original objectives have be completed. PSGhost can be downloaded from SkyDrive or from my website. September 22 I'm now in the middle of another lunatic experiment using Powershell. This time I have entered a few registry values to associate the URI schema "posh" with one of my Powershell scripts. Whenever a "posh" URI is invoked on my machine that script receives the URI as its first argument. At this point I believe that I have the process of getting the URI to the script wrapped up fairly well. There shouldn't be any passibility of code injection without being able to alter my registry or the script that ultimately receives the URI, and if someone did manage to pull that off then simple code injection would be the least of my worries. Getting past the issue of safely transporting the URI to the script I can only think of a single caveat: I don't have any way to know for sure where the URI came from. Since I don't know where a URI comes from then I don't know if the source of the URI is authorized to be able commit any actions that are associated with the URI. My solution to that caveat is simple, it allows me to know what generated the URI, it allows me to keep the identity of the object that created the URI to remain anonymous, and, since I know what generated the URI, I can be fairly certain about where the URI invocation is coming from. The idea is to invoke some script on demand and have it generate a new GUID. The script would then generate a series of URIs using the new GUID. (e.g. posh:4fdcd7f0-7b1c-4c82-979e-7d0fa2b4bb0f:args) Then, the script would store those URIs somewhere they would be of use, like in an html file. Finally, the script would register the GUID, a scriptlet that would handle URI invocations with that GUID, and a DateTime of when the GUID should expire. In the case of generating an html file the script may invoke the html file before completion. When the scriptlet that is associated with the GUID is invoked, it can be certain that the URI in someway come from where ever the original script stored the URIs. What is not certain is whether or not the URIs have been copied away or if the arguments were modified by someone unauthorized. If the arguments need to be protected then they too could be made anonymous by the use of GUIDs. (e.g. posh:4fdcd7f0-7b1c-4c82-979e-7d0fa2b4bb0f:20075295-9b78-47da-8ccf-3320db848ccf) The only thing that remains is that valid posh URIs could still be copied from a location where its use is valid to a location where its use would be invalid. The only prevention against this is the expiration date on the GUIDs and the extreme improbability that a GUID may be invoked in an unauthorized manner before the GUID expires. There may also be some use of permanent named scriptlets. In an intranet setting links could be placed on a company web site that invoke scriptlets that invoke local applications relevant to that page, and administrators would be able to easily add the necessary registration information to all machines on the network. Web sites like the script repository could release scripts that help with the download and resigning of scripts from the repository; the script would be associated with some posh URI and the script repository could place special links near each script displayed. This may not be a big deal to everyone but this makes it easier to do more advanced local system management using a simple web interface.
August 30 I have been using an updated version of New-CAssembly for some time now. To be exact I have been using a Cmdlet version of New-CAssembly for time time now. The cmdlet version improves performance and adds features. I would release the cmdlet version, but I haven't wanted to polish it off yet, partly due to not wanting to document all features, and partly due to there already being an equivalent cmdlet in Powershell V2.0. I might not release the cmdlet, but I will release the updated script. The update includes 1 bug fix, cleaner code, and the ability to specify the compiler version. To maintain compatibility with scripts already written for New-CAssembly it defaults to using the classic compiler, but just set CompilerVersion parameter to v3.5 and suddenly you can use LINQ! Compiler v3.5 should be compatible with v2.0, but I didn't want to risk breaking any mission critical scripts because of some subtle redbit that I didn't know about. You should be able to change the default value yourself from v2.0 to v3.5 on your own without too much fear. I was going to give a few examples of the advantages of being able to use extension methods in post compiled code, show how much easier it is now to use LINQ, and how much fun it is to play with LINQed lists(really they are like magic), but that was going to turn into a very long winded post. A person could go on forever about LINQ and extension methods, but we already know how great that stuff is because we use Powershell and we love the pipe! Link To New-CAssenbly.ps1 May 27 After writing my last post I realized that I didn't have a script that would simplify the process of generating event handlers down to a single line task. I quickly wrote a script that wraps C# method code into a delegate. The script allows delegates of any signature to be created. The result has four parameters, three of witch have default values, and only 17 lines of actual code. This script should be fairly easy to use. The default values are set to use the EventHandler delegate. I chose this type of delegate because Powershell script blocks can be cast to EventHandler. I wanted to be able to easily generate a compiled delegate and be able to combine it with a script block. Get-CDelegate.ps1 Here is an example of its use.
PS> $handler = Get-CDelegate 'Console.WriteLine("Hello world!");'
PS> $handler.Invoke($null,$null)
Hello world!
Simple! Right? Think of the compiled delegate as if it were a script block that runs really fast.
You can also make delegates of different types.
PS> $callback = Get-CDelegate 'Console.WriteLine("Hello world!");' -DelegateType AsyncCallback -Signature void, IAsyncResult
Here's the cool part. Combine a script block with an event handler to make a new event hander.
PS> $eh1 = get-cdelegate 'Console.WriteLine("Compiled EventHandler!");' PS> $eh2 = [eventhandler]{write-host 'ScriptBlock!'}
PS> $eh3 = [delegate]::combine($eh1, $eh2)
PS> $eh3.invoke($null,$null)
Compiled EventHandler!
ScriptBlock!
There is also a using parameter. Add any namespaces that you want to use there. The default is 'System'. Now, take it, have it, play with it, enjoy it, and remember, the key making this useful is to keep in mind that the result of this script is a reusable function that can do something that is hard much faster than an ordinary script block. Now go find an interesting use for this and post about it!
Update
I expanded the script so that the delegate can receive an object from the script that made it and also be able to remember past invocations. Script blocks store all their context information as variables, but that's not so easy in this case. Here there is no guarantee that there is even a Runspace associated with the thread that invokes the delegate. Now you can provide an initialization object, optional initialization code, and define the types for any values that should remain present between invocations.
PS> $eh = get-cdelegate 'context0++; Console.WriteLine(context0);' -contexttypes int -initobject 50 -initcode 'context0 = (int)init.BaseObject;'
PS> $eh.invoke($null,$null)
51
PS> $eh.invoke($null,$null)
52
PS> $eh.invoke($null,$null)
53
PS> $eh.invoke($null,$null)
54
May 26 I have seen this a few times in my Google referrals. People want to know how to how to add events to PresentationFramework controls in Powershell. I'm sorry to tell you this, but, as of Powershell v1.0, you can't use script blocks generated in Powershell as event handlers in any STA style user interface, including WPF. This has to do with how Powershell currently handles threading. That's why I had to make three really long scripts just to show a single window. If you really want to be able to add event handlers to WPF controls have a look at my New-CAssembly script. That will make it easier to include compiled code, such as event handlers, into a Powershell script. Then, look at my Library-PresentationInterface script. That will help you pack a custom user interface into a script as XAML. Alternatively you may look at a few of Team Powershell's recent posts about controlling WPF through Powershell v2.0 CTP 2. They added an STA flag to CTP 2. This helps to resolve some of the threading issues in regards to controlling STA style user interfaces. April 27 As you will see the last WPF and PowerShell example is a little over complicated when used with the currently released version of Library-PresentationInterface, but I will likely update the library at some point and then scripts like this could then be made more concise.
To make the script easier to read I will list the few basic steps taken by the script.
- Generate a special collection class that can inform any WPF window displaying it when it is updated. When a window displays a normal collection any updates will likely not be noticed. This part may eventually be added to the library.
- Create two collections and make the first element of the first collection the string "Loading..." and the second element of the first collection the second collection.
- Display a window to display the content of the collections. The first collection becomes the tag of the window. A TextBlock is bound to the first element of the first collection(i.e. "Loading...") and a ListBox is bound the content of the second collection.
- Add the numbers 1 to 5 to the second collection pausing one second between each number.
- Wait for the user to close the window.
I have used a modified version of this script to monitor the log file of an IRC chat room in real time, effectively making the resulting window another chat window(minus the ability to respond). Any amount of information could be added like other chat rooms, RSS feeds, or any other information you can get your hands on, and any filters can be added to that information.
Requirements:
- New-CAssembly
- Library-PresentationInterface
WPF Example Script 4January 30 It's probably time that I post the third example of how to use Library-PresentationInterface. I am sorry about the delay. I had the code for all the examples compiled before even posting the first example. The only thing keeping me from posting the example scripts is that I have to write the commentary. So I have no valid excuse for the delay.
The purpose of this example is to show that Library-PresentationInterface can be used to load a custom compiled window class using only that class's type name. The script includes a call to New-CAssembly to compile a new window class. A call to Start-PresentationInterface is then made with the new classes name passed to the WindowType parameter.
You may wish to save this code! I have found it very useful for debugging! The result of this script is a GUI representation of any exception or error record passed to the script! Also the Wait switch parameter may be used to halt script execution while the window is open, the dialog buttons then may be used to return a true/false value from the script witch may be used by any error trapping code.
Here it is, all 145 lines of it...
param ($ErrorInfo, [Switch]$Wait)
. Library-PresentationInterface
[void](new-cassembly @'
using System;
using System.Management.Automation;
using System.Windows;
using System.Windows.Controls;
namespace ErrorWindow {
public class ErrorWindowClass : Window {
bool IsErrorRecord;
ErrorRecord Error;
Exception Ex;
protected override void OnInitialized(EventArgs e) {
base.OnInitialized(e);
this.Title = "ErrorWindow";
if ((this.Tag is ErrorRecord) || (this.Tag is Exception)) {
this.Height = 400;
this.Width = 800;
StackPanel Stack = new StackPanel();
if (this.Tag is ErrorRecord) {
IsErrorRecord = true;
Error = (ErrorRecord)(this.Tag);
Ex = Error.Exception;
}
else {
IsErrorRecord = false;
Ex = (Exception)(this.Tag);
}
Label MessageLabel = new Label();
MessageLabel.Content = Ex.Message;
Stack.Children.Add(MessageLabel);
if (IsErrorRecord) {
Label IdLabel = new Label();
IdLabel.Content = String.Format("Error ID: {0}",Error.FullyQualifiedErrorId);
Stack.Children.Add(IdLabel);
Label CategoryLabel = new Label();
CategoryLabel.Content = String.Format("Error Category: {0}",Error.CategoryInfo.Category.ToString());
Stack.Children.Add(CategoryLabel);
if (Error.CategoryInfo.Activity != "") {
Label ActivityLabel = new Label();
ActivityLabel.Content = String.Format("Error Activity: {0}",Error.CategoryInfo.Activity);
Stack.Children.Add(ActivityLabel);
}
if (Error.CategoryInfo.Reason != "") {
Label ReasonLabel = new Label();
ReasonLabel.Content = String.Format("Error Reason: {0}",Error.CategoryInfo.Reason);
Stack.Children.Add(ReasonLabel);
}
if (Error.CategoryInfo.TargetName != "") {
Label TargetNameLabel = new Label();
TargetNameLabel.Content = String.Format("Target Name: {0}",Error.CategoryInfo.TargetName);
Stack.Children.Add(TargetNameLabel);
}
if (Error.CategoryInfo.TargetType != "") {
Label TargetTypeLabel = new Label();
TargetTypeLabel.Content = String.Format("Target Type: {0}",Error.CategoryInfo.TargetType);
Stack.Children.Add(TargetTypeLabel);
}
Label PositionLabel = new Label();
PositionLabel.Content = String.Format("Error Position: {0}",Error.InvocationInfo.PositionMessage);
Stack.Children.Add(PositionLabel);
}
Label TypeLabel = new Label();
TypeLabel.Content = String.Format("Error Type: {0}",Ex.GetType().FullName);
Stack.Children.Add(TypeLabel);
if (Ex.InnerException != null) {
StackPanel InnerStack = new StackPanel();
InnerStack.Orientation = Orientation.Horizontal;
Label InnerExLabel = new Label();
InnerExLabel.Content = String.Format("Inner Exception: {0}",Ex.InnerException.Message);
InnerStack.Children.Add(InnerExLabel);
Button InnerExButton = new Button();
InnerExButton.Content = "More Info";
InnerExButton.Click += this.InnerExceptionInfoClick;
InnerStack.Children.Add(InnerExButton);
Stack.Children.Add(InnerStack);
}
TextBlock StackBlock = new TextBlock();
StackBlock.Text = String.Format("Stack:\n{0}",Ex.StackTrace);
Stack.Children.Add(StackBlock);
if (this.Owner == null) {
StackPanel DialogStack = new StackPanel();
DialogStack.Orientation = Orientation.Horizontal;
DialogStack.HorizontalAlignment = HorizontalAlignment.Center;
Button ContinueButton = new Button();
ContinueButton.Content = "Continue/Retry";
ContinueButton.Click += this.ContinueClick;
DialogStack.Children.Add(ContinueButton);
Button BreakButton = new Button();
BreakButton.Content = "Break/Fail";
BreakButton.Click += this.BreakClick;
DialogStack.Children.Add(BreakButton);
Stack.Children.Add(DialogStack);
}
ScrollViewer ScrollView = new ScrollViewer();
ScrollView.Content = Stack;
ScrollView.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
this.Content = ScrollView;
}
else {
this.Height = 100;
this.Width = 500;
this.Content="Target object is not an ErrorRecord.";
}
}
public void InnerExceptionInfoClick(object sender, RoutedEventArgs e) {
ErrorWindowClass ChildWindow = new ErrorWindowClass();
ChildWindow.Tag = Ex.InnerException;
ChildWindow.Owner = this;
ChildWindow.Show();
}
public void ContinueClick(object sender, RoutedEventArgs e) {
this.DialogResult = true;
}
public void BreakClick(object sender, RoutedEventArgs e) {
this.DialogResult = false;
}
}
}
'@)
$result = start-PresentationInterface -WindowType ([ErrorWindow.ErrorWindowClass]) -WindowTag $ErrorInfo
if ($Wait) {(stop-PresentationInterface $result).DialogResult}
I suggest saving it as Get-ErrorDialog.
To use it simply perform any action that would cause an exception. Then, you may read the exception straight from the $error variable.
Get-ErrorDialog $Error[0]
November 19 Here is another example of how to use WPF with Powershell. This example shows how to use a custom control in WPF. This example is somewhat esoteric and does not pertain directly to Powershell, but custom types can make it easier to create different interfaces. Knowing how to do this is important because many of the base WPF types do not expose much useful functionality(e.g. a Button doesn't do anything on it's own and needs to have code added to it's events).
The first half of the example script compiles a new WPF class derived from the Button class. This new class does not necessarily need to be defined in the script. XAML can use any WPF class from any loaded assembly. This class could be part of a library of custom controls.
The new class, CountButton, is derived from the Button class in the System.Windows.Controls namespace. The button functionality is extended by making the button count the number of times it is clicked by overriding the OnClick method. The number of times the button is clicked is then exposed by the read only dependancy property ClickCount.
The second half of the example script contains XAML that will use the new WPF class. In the XAML the "t" xml namespace corresponds to the Test namespace in the embedded C# code. The element name for our CountButton class is then t:CountButton. The example XAML defines a CountButton with the name "countbutton" and the display text as "Click this!" The example XAML also defines a label with it's content bounded to the ClickCount property of the named button.
When this example script is invoked, the button will keep track of the number of times it is clicked and the label will automatically update it's content.
Combining example one and this example, a person could display some properties of a collection of objects, and have buttons available to invoke methods on those objects.
. Library-PresentationInterface
[void](new-cassembly @'
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
[assembly:System.Windows.Markup.XmlnsDefinition("http://testschema/test","Test")]
namespace Test {
public class CountButton : Button {
int _ClickCount = 0;
public int ClickCount {get {return _ClickCount;}}
internal static readonly DependencyPropertyKey ClickCountKey = DependencyProperty.RegisterReadOnly(
"ClickCount", typeof(int), typeof(CountButton),new PropertyMetadata(0));
public static readonly DependencyProperty ClickCountProperty = ClickCountKey.DependencyProperty;
protected override void OnClick() {
this._ClickCount++;
base.OnClick();
this.SetValue(ClickCountKey, this._ClickCount);
}
}
}
'@)
$result = start-PresentationInterface -xaml ([xml]@'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="http://testschema/test"
Title="Example 2" SizeToContent="WidthAndHeight">
<StackPanel>
<t:CountButton Name="countbutton" Width="150" HorizontalAlignment="Left">Click this!</t:CountButton>
<StackPanel Orientation="Horizontal">
<Label>Button click event count:</Label>
<Label Content="{Binding ElementName=countbutton, Path=ClickCount}" />
</StackPanel>
</StackPanel>
</Window>
'@)
[void](stop-PresentationInterface $result)
November 14 This is the first real example of how WPF could be used by Powershell. It shows how to display a few properties of a collection of objects using XAML and no compiled parts. The XAML in the example script tells WPF how to display the data type we are using(WPF will actually only see the PSObjects that are passed, not the content), and the collection is then handed over to the window.
This is not the only way to do this. It would also be possible to construct the XAML for each object in the collection individually and not need to use a template.
. Library-PresentationInterface
function Get-ProcessDialog { $Xaml = [Xml]( @" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ps="clr-namespace:System.Management.Automation;assembly=System.Management.Automation" Name="ThisWindow" Title="Formated Process List"> <Window.Resources> <DataTemplate DataType="{x:Type ps:PSObject}"> <Border BorderThickness='0,0,0,1' BorderBrush='Gray' Padding='3'> <StackPanel> <TextBlock FontSize='14' FontWeight='Bold' Text="{Binding Properties[ProcessName].Value}" /> <StackPanel Orientation="Horizontal"> <StackPanel.Style> <Style TargetType="StackPanel"> <Style.Triggers> <DataTrigger Binding="{Binding Properties[CPU].Value}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed" /> </DataTrigger> </Style.Triggers> </Style> </StackPanel.Style> <TextBlock FontSize='12' Text="CPU: " /> <TextBlock FontSize='12' Text="{Binding Properties[CPU].Value}" /> </StackPanel> <StackPanel Orientation="Horizontal"> <StackPanel.Style> <Style TargetType="StackPanel"> <Style.Triggers> <DataTrigger Binding="{Binding Properties[WorkingSet].Value}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed" /> </DataTrigger> </Style.Triggers> </Style> </StackPanel.Style> <TextBlock FontSize='12' Text="Working Set: " /> <TextBlock FontSize='12' Text="{Binding Properties[WorkingSet].Value}" /> </StackPanel> <StackPanel Orientation="Horizontal"> <StackPanel.Style> <Style TargetType="StackPanel"> <Style.Triggers> <DataTrigger Binding="{Binding Properties[Description].Value}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed" /> </DataTrigger> </Style.Triggers> </Style> </StackPanel.Style> <TextBlock FontSize='12' Text="Description: " /> <TextBlock FontSize='12' Text="{Binding Properties[Description].Value}" /> </StackPanel> <StackPanel Orientation="Horizontal"> <StackPanel.Style> <Style TargetType="StackPanel"> <Style.Triggers> <DataTrigger Binding="{Binding Properties[Path].Value}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed" /> </DataTrigger> </Style.Triggers> </Style> </StackPanel.Style> <TextBlock FontSize='12' Text="Path: " /> <TextBlock FontSize='12' Text="{Binding Properties[Path].Value}" /> </StackPanel> </StackPanel> </Border> </DataTemplate> </Window.Resources> <ScrollViewer> <ItemsControl ItemsSource="{Binding ElementName=ThisWindow, Path=Tag}" /> </ScrollViewer> </Window> "@ ) $result = Start-PresentationInterface -xaml ($Xaml) -WindowTag ( @($input | select ProcessName, CPU, WorkingSet, Description, Path)) [void](stop-PresentationInterface $result) }
Get-Process | Get-ProcessDialog
November 07 In the past, when it came to generating a GUI using Powershell, the only option was to use System.Windows.Forms.dll to generate the GUI. This method required lots of verbose script, and the result was not always pretty. The PresentationFramework.dll library provided the XAML loader, but the Presentation classes required an STA thread to initialize while the threads that interpreted Powershell scripts are always MTA.
I really wanted to be able to create WPF windows in Powershell so I created a library script that will define several classes that will handle the initialization of an STA thread that can then handle the initialization of a WPF window.
Here are a list of a few of the major features of the library script.
- Windows can be initialized from XAML.
- Windows can be initialized using the default constructor of a class that derives from the window type.
- The Tag property can be set prior to initialization. Arguments for the window can be passed here.
- The value of the Tag property is returned when the window is closed. This can be useful in the case that a bool dialog result is not enough.
- The window threads operate asynchronously to Powershell. Synchronization is done useing an IAsyncResult object.
- The Dispatch object that is associated with the window's UI thread is attached to the IAsyncResult object. External threads cannot interact with user interface elements, but can use the Dispatch object to give instructions to the UI thread.
Later I will be giving four examples of how to use this library.
- The first will give a simple example of the use XAML.
- The second will be an example of using custom types with XAML.
- The third will be an example of using a compiled window type.
- The forth example will show how to use the Dispatch object.
To use this library script you will first need to save a copy of the New-CAssembly script either as a function or as a script file in a path location.
Use of the library is fairly simple. Simply dot source the library. Then, call the Start-PresentationInterface function passing either the XAML as an XML node to the Xaml parameter, or the type object of any compiled window type to the WindowType parameter. You may also initialize the window's Tag property by passing it to the WindowTag parameter. The result of that function will give an IAsyncResult object. It behaves just like any other IAsyncResult object, and has a IExposeDispatch interface giving it the Dispatch property. That object may be disposed of safely or you can pass it to the Result parameter of the Stop-PresentationInterface function. That function will block the calling thread untill the window has closed. The return value will give the DialogResult of the window and the final value of the Tag property of the window.
The script for the library is below, but first here is a really simple example to get you started.
. Library-PresentationInterface
# Initialize a window
$result = Start-PresentationInterface -xaml (
[xml]'<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"><Viewbox><TextBlock>Welcome to the Presentation Framework!</TextBlock></Viewbox></Window>'
)
# Look at the result object
$result
# Wait for the window to close
Stop-PresentationInterface $result
Now the script.
# Library-PresentationInterface.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain/Unrestricted Use
# Provided "as is," with out warranty, and without any assertions of suitability
# for any purpose either express or implied.
if ($args -contains '-?') { @'
Library-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist
USAGE: Library-PresentationInterface
DESCRIPTION: This library contains two functions and four types. The types are
only used by the functions and do not need to used directly. The two
functions, Start-PresentationInterface and Stop-PresentationInterface, provide
access to the Windows Presentation Framework.
CONTAINS: Start-PresentationInterface
Stop-PresentationInterface
'@
return
}
[Void]([Reflection.Assembly]::LoadWithPartialName('PresentationFramework'))
[Void](New-CAssembly @"
using System;
using System.Threading;
using System.Windows;
namespace LibraryPresentationInterface {
// structure returned by the stop method
public struct PresentationResult {
// Contains the value of the DialogResult property of the window
public bool DialogResult;
// Contains the value of the Tag property of the window
// may be used if a bool is insufficient
public object WindowTag;
public PresentationResult(bool dialogResult, object tag) {
DialogResult = dialogResult; WindowTag = tag;
}
}
// public interface can give a reference to the dispatcher assosiated with a window
public interface IExposeDispatcher {
System.Windows.Threading.Dispatcher Dispatcher {get;}
}
// this is an IAsynResult object returned by the start method
// may be passed to the stop method to retrieve the dialog result or
// may be discarded safely
internal class PresentationAsyncResultImpl : IAsyncResult, IExposeDispatcher {
object UserObject;
EventWaitHandle Handle;
bool Completed;
PresentationResult DialogResult;
bool IsError;
System.Exception Error;
System.Windows.Threading.Dispatcher _Dispatcher;
public PresentationAsyncResultImpl(object userObject, EventWaitHandle handle) {
UserObject = userObject; this.Handle = handle; Completed = false;
}
// returns the user defined object that was given at the start method
public object AsyncState {get{return this.UserObject;}}
// returns a wait handle that will be signaled when the window is closed
public WaitHandle AsyncWaitHandle {get{return this.Handle;}}
// returns false, this is an asyncronous method
public bool CompletedSynchronously {get{return false;}}
// returns true if the window is closed, or if initialization has failed
public bool IsCompleted {get{return this.Completed;}}
// returns the dispatcher of the UI thread of the window
public System.Windows.Threading.Dispatcher Dispatcher {get {return _Dispatcher;}}
// used by the stop mehtod to get the result
internal PresentationResult Result {get{
if (IsError)
throw Error;
return DialogResult;
}}
// used to set the result when the window is closed
internal void SetComplete(PresentationResult result) {
this.DialogResult = result; this.Completed = true; this.Handle.Set();
}
// used to set the dispatcher once the window is initialized
internal void SetDispatcher(System.Windows.Threading.Dispatcher dispatcher) {
_Dispatcher = dispatcher;
}
// used to set the result to throw an exception
internal void SetException(System.Exception ex) {
this.IsError = true; this.Error = ex; this.Completed = true;
this.Handle.Set();
}
}
// Args passed to the window thread
internal class WindowArgs {
// [xml]containing the xaml for the window or a [type] for a self describing window type
public object WindowInfo;
// initial value for the window's tag
public object WindowTag;
// an internal reference to the IAsyncResult object so the the result can be set
public PresentationAsyncResultImpl AsyncResult;
// initialization wait handle, signaled once initialization is completed
public EventWaitHandle InitHandle;
// an initialization exception that, if set, will be thown by the start method
public Exception InitException;
}
public class Presentation {
// internal method to start the window thread
private static IAsyncResult InternalStart (object windowInfo, object tag, object userObject) {
WindowArgs Args = new WindowArgs();
Args.WindowInfo = windowInfo; Args.WindowTag = tag;
Args.AsyncResult = new PresentationAsyncResultImpl(userObject, new EventWaitHandle(false, EventResetMode.ManualReset));
Args.InitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
Args.InitException = null;
Thread WindowThread = new Thread(new ParameterizedThreadStart(WindowProc));
WindowThread.Name = "Presentation.Thread";
WindowThread.SetApartmentState(ApartmentState.STA);
WindowThread.Start(Args);
Args.InitHandle.WaitOne();
if (Args.InitException != null)
throw Args.InitException;
return Args.AsyncResult;
}
// public start method to accept xaml
public static IAsyncResult Start(System.Xml.XmlNode xaml, object tag, object userObject) {
if (xaml == null)
throw new ArgumentNullException("xaml");
return InternalStart(xaml, tag, userObject);
}
// public start method to accept a window type
public static IAsyncResult Start(Type windowClass, object tag, object userObject) {
if (windowClass == null)
throw new ArgumentNullException("windowClass");
return InternalStart(windowClass, tag, userObject);
}
// stop method to retrieve the dialog result
public static PresentationResult Stop(IAsyncResult asyncResult) {
if (asyncResult == null)
throw new ArgumentNullException("asyncResult");
if (!(asyncResult is PresentationAsyncResultImpl))
throw new ArgumentException("Unrecognised result type.");
PresentationAsyncResultImpl Result = (PresentationAsyncResultImpl)asyncResult;
Result.AsyncWaitHandle.WaitOne();
return Result.Result;
}
// procedure for the window thread
private static void WindowProc(object obj) {
WindowArgs Args = (WindowArgs)obj;
Window LocalWindow;
PresentationResult FinalResult = new PresentationResult();
// trap initialization errors
try {
if (Args.WindowInfo is System.Xml.XmlNode) {
// initialize from xaml
object XamlObject = System.Windows.Markup.XamlReader.Load(
new System.Xml.XmlNodeReader((System.Xml.XmlNode)Args.WindowInfo));
if (!(XamlObject is Window))
throw new InvalidCastException("The given XAML did not return a Window object.");
LocalWindow = (Window)XamlObject;
}
else {
// initialize from type
Type WindowType = (Type)Args.WindowInfo;
System.Reflection.ConstructorInfo Constructor = WindowType.GetConstructor(Type.EmptyTypes);
if (Constructor != null) {
LocalWindow = (Window)Constructor.Invoke(new Object[0]);
}
else {
throw new ArgumentException(String.Format("The type\"{0}\" does not have an accessable default constructor.",WindowType.FullName));
}
}
LocalWindow.Tag = Args.WindowTag;
}
catch (Exception ex) {
// set initialization exception
Args.InitException = ex;
Args.InitHandle.Set();
return;
}
// initialization complete
// set the dispatcher
Args.AsyncResult.SetDispatcher(LocalWindow.Dispatcher);
// set the initialization wait handle
Args.InitHandle.Set();
// trap runtime exceptions
try {
FinalResult.DialogResult = (bool)LocalWindow.ShowDialog();
FinalResult.WindowTag = LocalWindow.Tag;
}
catch (Exception ex) {
// set the runtime exception to be rethrown by the stop method
Args.AsyncResult.SetException(ex);
return;
}
// set the dialog result
Args.AsyncResult.SetComplete(FinalResult);
}
}
}
"@)
function Start-PresentationInterface([System.Xml.XmlNode]$Xaml, [Type]$WindowType, $WindowTag, $UserObject) {
if (($args -contains '-?') -or ((!$Xaml) -and (!$WindowType)) -or (($Xaml) -and ($WindowType))) { @'
Library-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist
USAGE:
Start-PresentationInterface -Xaml [System.Xml.XmlNode]
[-WindowTag [Object]] [-UserObject [Object]]
Start-PresentationInterface -WindowType [Type] [-WindowTag [Object]]
[-UserObject [Object]]
PARAMETERS:
Xaml: An XML node, such as an XML document, in the format of XAML that
describes the content of a Presentation Framework window. This argument is
mandatory and mutually exclusive to WindowType.
WindowType: May be any type derived from and including the type
System.Windows.Window. The type must have a public default constructor.
The default constuctor of this type is called and the resulting window
object is displayed. This argument is mandatory and mutually exclusive to
Xaml.
WindowTag: The value of this argument is applied to the Tag property of the
window after initialization.
UserObject: This object is not used internally. It is only made available
though the resulting IAsyncResult object.
RETURNS: An IAsyncResult object that can be used to get the dialog result of
the window.
DESCRIPTION: Initializes a new window using the information provided by the
WindowType or Xaml arguments.
SEE ALSO: Stop-PresentationInterface
'@
return
}
if ($Xaml) {
[LibraryPresentationInterface.Presentation]::Start($Xaml,$WindowTag,$UserObject)
}
else {
[LibraryPresentationInterface.Presentation]::Start($WindowType,$WindowTag,$UserObject)
}
}
function Stop-PresentationInterface([IAsyncResult]$Result) {
if (($args -contains '-?') -or (!$Result)) { @'
Stop-PresentationInterface
VERSION: 1.0
AUTHOR: LunaticExperimentalist
USAGE:
Stop-PresentationInterface -Result [IAsyncResult]
PARAMETERS:
Result: An IAsyncResult object obtained from the Start-PresentationInterface
function.
RETURNS: An object of type LibraryPresentationInterface.PresentationResult
containing dialog result information.
DESCRIPTION: Causes the current thread to wait for the window corresponding the
given IAsyncResult object to close.
SEE ALSO: Start-PresentationInterface
'@
return
}
[LibraryPresentationInterface.Presentation]::Stop($Result)
}
September 24 My last post talked about how to embed a .NET assembly that has already been compiled into a POSH script. This time I will talk about how to embed code for a .NET assembly into a script that will be compiled when the script is executed. The .NET Framework comes with compilers for Visual Basic .NET and C# .NET. The .NET Framework also abstracts those compilers in the System.dll assembly through the Microsoft.VisualBasic.VBCodeProvider and Microsoft.CSharp.CSharpCodeProvider classes respectively. Using the CSharpCodeProvider class, I have created two scripts that make it easy to insert C# code into a POSH script. The Invoke-CScript script will compile it's code argument as a single method of a class and execute that method. The New-CAssembly script gives more direct access to the C# compiler allowing multiple, entire classes to be compiled. Once an assembly is compiled, it is loaded into the current application domain and reused whenever the scripts are invoked with an identical code parameter. Precompiled and post compiled assemblies have advantages and disadvantages. - A precompiled assembly is not dependent on availability of compilers, while a post compiled assembly must be written in a language that the end user has a compiler for. This shouldn't be a problem, however, if you program in Visual Basic or C#.
- A precompiled assembly should load faster than the equivalent post compiled assembly since the precompiled assembly does not need to be processed by a compiler. This will only have a performance impact when the assembly is first loaded and will only be noticeable with very large assemblies.
- A post compiled assembly can be inserted into a script as plain text, while a precompiled assembly must be maintained and compiled separately from the script.
Do not over use embedded assemblies. Both methods of embedding an assembly into a script can result in a huge performance boost when hundreds of records must be processed. Both methods can also allow .NET classes to defined and used by the rest of the script. However, when no significant performance boost can be attained the result would only be an over complication due to having to use separate languages for compiled and interpreted code. The act of compiling the assembly will also take time. I have written and executed the following script as an illustration of the potential performance boost and as an example of how to use the two scripts. The script contains three functions that will use different methods to find the sum of the elements of it's input array.
$CompileTime = Measure-Command {New-CAssembly @" namespace Test { public class SpeedTestClass { public static int GetSum(int[] arr) { int sum = 0; foreach (int i in arr) {sum += i;} return sum; } } } "@}
"TestCModule compile time: " + $CompileTime.TotalMilliseconds
function TestCModule([int[]]$in) { [test.speedtestclass]::GetSum($in) }
function TestCMethod([int[]]$in) { Invoke-CScript 'int sum = 0; foreach (Object o in args) {sum += (int)o;} return sum;' $in }
function TestPSMethod([int[]]$in) { [int]$sum = 0 $in | Foreach {$sum += $_} $sum } [int[]]$arr = 1..10 "TestCModule(1..10): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms" "TestCMethod(1..10): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms" "TestPSMethod(1..10): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms" '' [int[]]$arr = 1..100 "TestCModule(1..100): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms" "TestCMethod(1..100): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms" "TestPSMethod(1..100): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms" '' [int[]]$arr = 1..1000 "TestCModule(1..1000): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms" "TestCMethod(1..1000): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms" "TestPSMethod(1..1000): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms" '' [int[]]$arr = 1..10000 "TestCModule(1..10000): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms" "TestCMethod(1..10000): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms" "TestPSMethod(1..10000): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms"
The result: TestCModule compile time: 184.6108 TestCModule(1..10): 4.0505ms TestCMethod(1..10): 146.3526ms TestPSMethod(1..10): 2.8179ms
TestCModule(1..100): 1.1498ms TestCMethod(1..100): 25.9426ms TestPSMethod(1..100): 14.3305ms
TestCModule(1..1000): 1.1065ms TestCMethod(1..1000): 25.8739ms TestPSMethod(1..1000): 187.4232ms
TestCModule(1..10000): 4.1893ms TestCMethod(1..10000): 37.7209ms TestPSMethod(1..10000): 1589.6593ms
The results of that test show that the code that made use of the class defined by the New-CAssembly compleated in just a couple of milliseconds. However, that is only after taking 184 milliseconds to compile that class. The code that used the Invoke-CScript returned in about 30 milliseconds in all but the first test. The large difference between using New-CAssembly and Invoke-CScript is that Invoke-CScript must make assurances that when the code is reused only the correct code is used, while with New-CAssembly we can simply create a unique type name and be fairly sure that when that type name is used we will be using the code that we want. The code that calculated the sum using Powershell syntax quickly began to take more time to process as the number of elements increased. Although it would appear that Invoke-CScript has a large overhead in comparison to New-CAssembly, in reality this overhead is due to having to make a call to New-CAssembly to verify the code that get executed. When you just want to execute fast code on a large number of records then using Invoke-CScript would probably be the best route, if you can suffice with a single invocation.
# New-CAssembly.ps1 # Version: 1.0 # Author: LunaticExperimentalist # End User License: Public Domain
param ([String]$Code = $(throw "C# code required to compile new assembly."))
# get code hash $CodeHash = $Code.GetHashCode() -band 0x7fffffff
# look for an existing assembly $ExistingAsm = [AppDomain]::CurrentDomain.GetAssemblies() | Foreach { $InfoType = $_.GetType("EmitedInfoClass") if (($InfoType) -and ($InfoType.GetProperty("ModuleCode").GetValue($null,$null) -eq $Code )) { return $_ } }
if ($ExistingAsm) { # use existing assembly return $ExistingAsm }
# or compile a new assembly $CSharpCodeProvider = New-Object Microsoft.CSharp.CSharpCodeProvider $Parameters = New-Object System.CodeDom.Compiler.CompilerParameters( @([AppDomain]::CurrentDomain.GetAssemblies() | where {$_.GlobalAssemblyCache -eq $true} | Foreach {$_.Location}), $null,$false) $Parameters.GenerateInMemory = $true $CompileResult=$CSharpCodeProvider.CompileAssemblyFromDom($Parameters, @( New-Object System.CodeDom.CodeSnippetCompileUnit( 'public class EmitedInfoClass { public static string ModuleCode{get{return "' + ($Code -replace '\\','\\' -replace '"','\"' -replace '\r','\r' -replace '\n','\n') + '";} } }' ); New-Object System.CodeDom.CodeSnippetCompileUnit($Code) ) )
# check for errors if ($CompileResult.Errors.Count -eq 0) { # use new assembly $CompileResult.CompiledAssembly } else { # write errors $CompileResult.Errors | Foreach { Write-Error "C Sharp Compile Error: Line $($_.Line) Col $($_.Column) `"$($_.ErrorText)`"" } }
# Invoke-CScript.ps1 # Version: 1.0 # Author: LunaticExperimentalist # End User License: Public Domain
param ([String]$Code = $(throw "C# code required to compile new assembly."), [Object]$Arguments = $null)
$Asm = New-CAssembly @" using System; namespace Emited { public class EmitedClass { public static Object EmitedCMethod( Object[] args, System.Management.Automation.PSObject[] input, System.Management.Automation.EngineIntrinsics context) { $Code } } } "@
if ($Asm) { # use assembly $method = $Asm.GetType("Emited.EmitedClass").GetMethod("EmitedCMethod") $InputList = New-Object System.Collections.ArrayList $Input | Foreach {[Void]$InputList.Add($_)} $method.Invoke($null, @(@($Arguments),[System.Management.Automation.PSObject[]]$InputList.ToArray(),$ExecutionContext)) }
September 21 I may have gone too far this time in my far reaching experiments. The joy about script is that we can simply read them to find out whether they are safe to execute. The script that I am presenting today could make doing that difficult even for people that have advanced debugging techniques up their sleeves. The benefit, however, is that compiled code executes much faster than interpreted code, making this script useful in some cases. The script as you can see below is very simple. There are two parameters to this script the first is a list of paths that should point to valid assemblies, and the second is a switch that indicates that the loading process should not return the reference to the assembly once it finally gets loaded. The processes of the script is simple. For each path in the "AssemblyPath" parameter, the content of the file at that path is loaded as byte data. That byte data is then converted to a string using the Base64 method. that Base64 string is the prepended and appended with the code that will load the assembly. The result of that is then returned as a single string for each path that was given. The process of loading that assembly is also very simple. The Base64 string is converted back to byte data and sent to the "[AppDomain]::CurrentDomain.Load" method. The resulting string(s) are valid Powershell script and can be immediately exported to a .ps1 file or included in an existing script. This script will allow us to include precompiled, fast executing code into scripts, but beware, while this script is safe to use, it is very difficult to determine if a script that was made using this script by someone else is safe to execute. So unless you trust the life of your computer to the person that made the script, I would suggest inspecting any assemblies included in scripts you find before use. Something like the .NET Reflector should be of use if you can get the content of the embedded assembly to a file.
# Get-AssemblyResource.ps1 # Version: 1.0 # Author: LunaticExperimentalist # End User License: Public Domain
param ([String[]]$AssemblyPath, [Switch]$Silent)
$AssemblyPath | Foreach { [String]::Join("`r`n", @(if ($Silent) {'[Void]('} "[AppDomain]::CurrentDomain.Load([Convert]::FromBase64String(@'" [Convert]::ToBase64String((Get-Content $_ -encoding byte),'InsertLineBreaks') "'@) )" if ($Silent) {');'})) } September 01 The problem with Powershell scripts is that you can't run them while they are compressed. You may not be able to run them while they are compressed, but I can. I have written two scripts that can be used to import and export functions, and the exported functions can be compressed.
The export function will read named functions out of the current runspace and save them as xml to a file. Those exported functions can optionally be compressed with GZip. The import function can then read the file and parse the xml to get to the functions. The imported functions can then be written back into the runspace or written to the output buffer.
The GZip functionality uses the GZipStream that comes with the .NET Framework API so there no need to employ an external command for compression. GZip is also a very fast compression format and saving xml function libraries as GZip compressed will not cause significant overhead.
There are two noteworthy limitations on the scripts. The first and more significant limitation is that the libraries are not yet checked for signatures. This means that if the import script can run, then any function stored in an xml function library can be run. The other limitation is that only GZip is supported. With modifications many different compression formats could be added, however, and I do intend to eventually add support for LZMA, the compression format that is used by 7Zip.
Adding signature checking is my first priority for revising these scripts, and there is not a great amount of documentation on how that could be accomplished, so any comments on how to do so would be appreciated. Any general comments would also be appreciated.
# Export-FunctionLibrary.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain
param ([String]$Path, [String[]]$FunctionNames,
[String]$Encoding = 'utf8', [String]$CompressionFormat = 'None')
if (($args -contains "-?") -or (-not $Path) -or (-not $FunctionNames)) {
"Export-FunctionLibrary"
"VERSION: 1.0"
"AUTHOR: LunaticExperimentalist"
""
"USAGE: Export-FunctionLibrary -Path [String] -FunctionNames [String[]]"
" [-Encoding [String]] [-CompressionFormat [String]]"
""
"PARAMETERS:"
" Path: A manditory parameter containing a path string to a file where the"
" selected functions are to be stored."
""
" FunctionNames: A manditory parameter containing a list of function names."
" The use of path style wildcards may be used here."
""
" Encoding: An optional parameter selecting an encoding to be used when"
" exporting the xml text file. Valid encodings are utf8, utf16le,"
" and utf16be."
""
" CompressionFormat: An optional parameter selecting a compression format to"
" be applyed to the resulting library. Only GZip is"
" supported by this version."
""
"DESCRIPTION: This script copys the code from one or more functions and saves"
" them to an xml formated file. The resuling xml can optionally"
" be compressed."
""
"SEE ALSO: Import-FunctionLibrary"
return
}
if (('none','gzip') -notcontains $CompressionFormat) {
throw 'Invalid compression format. Available formats are None and GZip.'
}
if (('utf8','utf16le','utf16be') -notcontains $Encoding) {
throw 'Invalid encoding. Available encodings are utf8, utf16le, and utf16be.'
}
function IsFunctionNamed {
param ([String]$FunctionName)
$FunctionNames | Foreach { if ($FunctionName -like $_) { return $True } }
return $False
}
function XMLEncode {
param ([String]$Text)
return $Text -replace '&','&' -replace '<','<' -replace '>','>'
}
# Generate the xml containg the names and script form the functions indicated by $FunctionNames
$LibraryXML = [String]::Concat($(
''
Get-ChildItem Function: | Foreach { if (IsFunctionNamed($_.Name)) {
"$(XMLEncode($_.Name))$(XMLEncode($_.ScriptBlock.ToString()))"
} }
""
))
# Encode the xml
$Unicode = $( switch ($Encoding) {
utf8 {New-Object System.Text.UTF8Encoding($False); break}
utf16le {New-Object System.Text.UnicodeEncoding($False,$True); break}
utf16be {New-Object System.Text.UnicodeEncoding($True,$True); break}
} )
[Byte[]]$LibraryBytes = @($Unicode.GetPreamble(); $Unicode.GetBytes($LibraryXML))
# Open a writable file at the path provided
[IO.FileStream]$FileStream = $Null
if (Test-Path $Path) {
# File exists
# Overwrive old file
$FileStream = (Get-Item -Path $Path).Create()
}
else {
# File does not exist
# Create new file
$FileStream = (New-Item -Path $Path -Type File).Create()
}
# Write the library byte data to the file
if ($CompressionFormat -eq 'gzip') {
# GZip was requested
$GZip = New-Object System.IO.Compression.GZipStream($FileStream, 'Compress')
$GZip.Write($LibraryBytes, 0, $LibraryBytes.Length)
$GZip.Close()
}
else {$FileStream.Write($LibraryBytes, 0, $LibraryBytes.Length); $FileStream.Close()}
# Import-FunctionLibrary.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain
param ([String]$Path, [String[]]$FunctionNames = '*',
[String]$CompressionFormat = 'None', [Switch]$List)
if (($args -contains "-?") -or (-not $Path)) {
"Import-FunctionLibrary"
"VERSION: 1.0"
"AUTHOR: LunaticExperimentalist"
""
"USAGE: Import-FunctionLibrary -Path [String] -FunctionNames [String[]]"
" [-CompressionFormat [String]]"
""
"PARAMETERS:"
" Path: A manditory parameter containing a path string to a file where the"
" selected functions are stored."
""
" FunctionNames: An optional parameter containing a list of function names."
" The use of path style wildcards may be used here."
""
" CompressionFormat: An optional parameter selecting a compression format to"
" be applyed to the resulting library. Only GZip is"
" supported by this version. The default value is none."
""
" List: A swtch parameter that causes Import-FunctionLibrary to output the"
" content of the library without creating new functions in the current"
" runspace."
""
"DESCRIPTION: This script reads functions form a file and makes them available"
" to the command line."
""
"SEE ALSO: Export-FunctionLibrary"
return
}
if (('none','gzip') -notcontains $CompressionFormat) {
throw 'Invalid compression format. Available formats are None and GZip.'
}
function IsFunctionNamed {
param ([String]$FunctionName)
$FunctionNames | Foreach { if ($FunctionName -like $_) { return $True } }
return $False
}
# Read text form file
if (-not (Test-Path $Path)) {throw "Path not found."}
$LibraryText=$Null
if ($CompressionFormat -eq 'gzip') {
# Read GZiped content
$FileStream = (Get-Item $Path).OpenRead()
$GZipStream = New-Object System.IO.Compression.GZipStream($FileStream,'Decompress')
$FileReader = New-Object System.IO.StreamReader($GZipStream, $True)
$LibraryText = $FileReader.ReadToEnd()
$FileReader.Close()
}
else { $LibraryText = Get-Content $Path }
# Parse text as xml
$(
trap {throw "The library text could not be parsed as xml."}
$LibraryXML = [xml]$LibraryText
)
# Filter and apply functions from xml to the current runspace
$LibraryXML.SelectNodes('/library/function') | Foreach {
if (IsFunctionNamed($_.Name)) {
if ($List) {
# Write functions to output
$Container = New-Object PSObject
Add-Member -MemberType NoteProperty -Name Name -Value ($_.Name) -InputObject $Container
Add-Member -MemberType NoteProperty -Name Script -Value ($_.ScriptBlock) -InputObject $Container
$Container
}
else {
# Make functions available for use
$FunctionName = $_.Name
$(
trap {Write-Error ('Could not import function "'+$FunctionName+'".'); continue}
Set-Content -LiteralPath ("Function:\"+$FunctionName) -Value $_.ScriptBlock
)
}
}
}
February 17 With my experience of controling runspace objects it didn't take much to create a Powershell host that could run as a background process. Since running shell scripts in background is not an unheard of thing I decided to create a project to do just that. PSGhost is a Powershell host that can run scripts in background or scripts that define their own GUI.
It's a really simple application. The binary is only 16KB. It should be easy enough to use if you know how to us Powershell.
I must make a security advisory about PSGhost. Since it runs completely silently in the background, shell scripts can be run with out the user even noticing. Be sure to store the PSGhost binary and all your shell scripts in a location that is writeable only by you or your system administrator. January 31 I have found a few bugs in Library-AsyncRunspace version 1.1 and I have added a couple of new functions so it's time for an update. The current version is 1.3 now. Yes, I skipped an update. Version 1.2 added the Stop-AsyncPipe function that can be used whenever a pipeline gets stuck in an infinite loop. Version 1.3 added the Write-AsyncMessage function and each new runspace also has a Read-AsyncMessage function. Some mild bugs were also fixed.
An object queue has now been associated with each runspace. The Write-AsyncMessage function will enqueue an object into the queue and Read-AsyncMessage will dequeue objects form the object queue in order. The purpose of the queue is to easily allow control messages to be sent into a runspace that is operating on a pipeline that is of indefinite length.
There is no specific use for this new feature. You may use it how you like. An example of its use would be to have a server run in a background runspace, periodically checking for messages in its queue for information on what to do next. All other changes are bug fixes.
Here is the code:
# Library-AsyncRunspace # Version: 1.3
# Include with {. Library-AsyncRunspace}
# Sugjested aliases... # New-Alias eap End-AsyncPipe # New-Alias gar Get-AsyncRunspace # New-Alias wam Write-AsyncMessage # New-Alias r! Invoke-ExpressionInRunspace # New-Alias iar Invoke-ExpressionInRunspace # New-Alias nar New-AsyncRunspace # New-Alias rap Read-AsyncPipe # New-Alias rar Remove-AsyncRunspace # New-Alias sap Start-AsyncPipe
# Don't destroy previous runspace catalog if ( -not $RunspaceCatalog ) {
# Declare the runspace catalog $RunspaceCatalog = New-Object PSObject $RunspaceCatalog | Add-Member NoteProperty RunspaceCountPosition 0 -PassThru | Add-Member NoteProperty RunspaceList @{}
function New-AsyncRunspace { param ([String]$NewName) # Incroment id number counter $RunspaceCatalog.RunspaceCountPosition++ # Generate a runspace and add usefull properties $NewRunspace = [management.automation.runspaces.runspacefactory]::CreateRunspace() | Add-Member NoteProperty Name $NewName -PassThru | Add-Member NoteProperty CurrentPipe $null -PassThru | Add-Member NoteProperty MessageQueue ([Collections.Queue]::Synchronized($(New-Object Collections.Queue))) -PassThru | Add-Member ScriptProperty PipeState { $this.CurrentPipe.PipelineStateInfo.State } -PassThru | Add-Member ScriptProperty PipeStateReason { $this.CurrentPipe.PipelineStateInfo.Reason } -PassThru | Add-Member ScriptProperty CurrentCommands { "{$($this.CurrentPipe.Commands)}" } -PassThru $NewRunspace.Open() # Add a reference to the message queue and a function to read it to the runspace $PipeLine = $NewRunspace.CreatePipeline({ $MessageQueue = $input | Select-Object -first 1 function Read-AsyncMessage { if ($MessageQueue.Count) { return $MessageQueue.Dequeue() } else { return $null } } }) [void]$Pipeline.Input.Write($NewRunspace.MessageQueue) $Pipeline.Input.Close() $PipeLine.Invoke() # Add new runspace to catalog $RunspaceCatalog.RunspaceList[$RunspaceCatalog.RunspaceCountPosition] = $NewRunspace # Report new runspace id number $RunspaceCatalog.RunspaceCountPosition }
function Remove-AsyncRunspace { param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" } #Find selected runspace by number if ($RunspaceID -is [Int32]) { $RunspaceCatalog.RunspaceList[$RunspaceID].Close() $RunspaceCatalog.RunspaceList.Remove($RunspaceID) } #Or find selected runspace by name else { $keys = $RunspaceCatalog.RunspaceList.Keys $keys | Foreach-Object { if ( $RunspaceCatalog.RunspaceList[$_].Name -like $RunspaceID ) { $RunspaceCatalog.RunspaceList[$_].Close() $RunspaceCatalog.RunspaceList.Remove($_) } } } }
function Get-AsyncRunspace { param ($RunspaceID) # Return the entire list of available runspaces by default if ( $RunspaceID -eq $null ) { return $RunspaceCatalog.RunspaceList.Values } # Type check on id if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" } #Find selected runspace by number if ($RunspaceID -is [Int32]) { $SelectedRunspace = $RunspaceCatalog.RunspaceList[$RunspaceID] } #Or find selected runspace by name else { $SelectedRunspace = $RunspaceCatalog.RunspaceList.Values | Where-Object { $_.Name -like $RunspaceID } } # Return result $SelectedRunspace }
function Write-AsyncMessage { param ($RunspaceID = (read-host "Enter a valid runspace ID or name."), [Object]$Message) $SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1 # Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Append $Message to message queue $SelectedRunspace.MessageQueue.Enqueue($Message) }
function Invoke-ExpressionInRunspace { param ($RunspaceID = (read-host "Enter a valid runspace ID or name."), [String]$Command = (read-host "Enter a command to be executed in the runspace.")) $SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1 # Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" } # Insert the command into the runspace. $Pipeline = $SelectedRunspace.CreatePipeline($Command)
# Insert input into the new pipe $input | Foreach-Object { [void]$Pipeline.Input.Write($_) } $Pipeline.Input.Close() # Execute pipe $Pipeline.Invoke() # Read pipeline result if ($Pipeline.PipelineStateInfo.State -eq [management.automation.runspaces.pipelinestate]::failed) { throw $Pipeline.PipelineStateInfo.Reason } $Pipeline.Error.ReadToEnd() | Foreach-Object { Write-Error $_ } $Pipeline.Output.ReadToEnd() }
function Start-AsyncPipe { param ( $RunspaceID = (read-host "Enter a valid runspace ID or name."), [String]$Command = (read-host "Enter a command to executed in the runspace.") )
BEGIN { $SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1 # Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" } # Insert the command into the runspace. $Pipeline = $SelectedRunspace.CreatePipeline($Command) # Start async process $Pipeline.InvokeAsync() # List this pipe as current with the runspace $SelectedRunspace.CurrentPipe = $Pipeline } PROCESS { # Insert input into the new pipe [void]$Pipeline.Input.Write($_) } END { # Close input $Pipeline.Input.Close() } }
function Read-AsyncPipe { param ($RunspaceID = (read-host "Enter a valid runspace ID or name.")) $SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
# Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
if ($SelectedRunspace.CurrentPipe) { # Read pipeline result if ($SelectedRunspace.CurrentPipe.pipelineStateInfo.state -eq [management.automation.runspaces.pipelinestate]::failed) { throw $SelectedRunspace.CurrentPipe.PipelineStateInfo.Reason } $SelectedRunspace.CurrentPipe.Error.NonBlockingRead() | Foreach-Object { Write-Error $_ } $SelectedRunspace.CurrentPipe.Output.NonBlockingRead() } }
function Stop-AsyncPipe { param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1 # Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" } if ( $SelectedRunspace.CurrentPipe ) { $SelectedRunspace.CurrentPipe.StopAsync()
# Pipeline is no longer current $SelectedRunspace.CurrentPipe = $null } }
function End-AsyncPipe { param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1 # Throw error if no runspace was found with the given id if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Check current pipeline if ($SelectedRunspace.CurrentPipe) { # Pipeline is no longer current $Pipeline = $SelectedRunspace.CurrentPipe $SelectedRunspace.CurrentPipe = $null # Read pipeline result if ($Pipeline.PipelineStateInfo.State -eq [management.automation.runspaces.pipelinestate]::failed) { throw $Pipeline.PipelineStateInfo.Reason } while ( -not $Pipeline.Output.EndOfPipeline -or -not $Pipeline.Error.EndOfPipeline ) { $Pipeline.Error.NonBlockingRead() | Foreach-Object { Write-Error $_ } $Pipeline.Output.NonBlockingRead() sleep -m 100 } } }
} # End if ( -not $RunspaceCatalog )
December 09 First I must say I think it's awesome to get a comment from someone on the PoSH team. Thanks. Now, about the script, I must criticize/comment on the inspiration for this script. The original inspiration is Karl Prosser's *-backgroundpipeline snap in. That snap in, for some reason, didn't work for me when it was new, and now it's out of date. Due to the lack of function of it I tried to write a script to fill the hole in my heart. That script worked, but it sucked. So I had to re script the whole thing, but before I did that I looked for one that works and is easier to use. I did a very short search and found Jim Truher's New-Job script. The New-Job script is an attempt to copy the background job feature of bash. The New-Job script got a vague appearance of that feature, however bash allows as many background jobs as you want, the script did not. My script is a library script with seven functions. Three are used to control the runspace objects that are necessary for multi threading in Powershell, and the other four are for controlling pipeline execution(commands). The runspaces can be referenced by a number returned when it is created, or by a name given when it is created. Wild card characters can be given to the Get-AsyncRunspace function to references to several runspaces. The Invoke-ExpressionInRunspace function is simplified function of the other pipeline functions, and executes the given command syncronusly with the current thread, making it more efficient with small commands. The Read-AsyncPipe function is optional and can be used to get the results of a command as it executes. I hope you find it easy to use if you do use it. I'll sugjest using three letter aliases, in place of the long function names, when you're using the shell in interactive mode. Of course, each runspace keeps track of their own variables and you could pass reference objects between them, and overall do most of what you could do with any other .NET, multi threaded programming language. Now the script.
file: Library-AsyncRunspace.ps1
# Library-AsyncRunspace
# Version: 1.1
# Include with {. Library-AsyncRunspace}
# Sugjested aliases...
##New-Alias eap End-AsyncPipe
##New-Alias gar Get-AsyncRunspace
##New-Alias r! Invoke-ExpressionInRunspace
##New-Alias iar Invoke-ExpressionInRunspace
##New-Alias nar New-AsyncRunspace
##New-Alias rap Read-AsyncPipe
##New-Alias rar Remove-AsyncRunspace
##New-Alias sap Start-AsyncPipe
# Don't destroy previous runspace catalog
if ( -not $RunspaceCatalog ) {
$RunspaceCatalog = New-Object PSObject
$RunspaceCatalog | Add-Member NoteProperty RunspaceCountPosition 0 -PassThru |
Add-Member NoteProperty RunspaceList @{}
}
function New-AsyncRunspace {
param ([String]$NewName)
# Incroment id number counter
$RunspaceCatalog.RunspaceCountPosition++
# Generate a runspace and add usefull properties
$NewRunspace = [management.automation.runspaces.runspacefactory]::CreateRunspace() |
Add-Member NoteProperty Name $NewName -PassThru |
Add-Member NoteProperty CurrentPipe $null -PassThru |
Add-Member ScriptProperty PipeState { $this.CurrentPipe.PipelineStateInfo.State } -PassThru |
Add-Member ScriptProperty PipeStateReason { $this.CurrentPipe.PipelineStateInfo.Reason } -PassThru |
Add-Member ScriptProperty CurrentCommands { "{$($this.CurrentPipe.Commands)}" } -PassThru
$NewRunspace.Open()
# Add new runspace to catalog
$RunspaceCatalog.RunspaceList[$RunspaceCatalog.RunspaceCountPosition] = $NewRunspace
# Report new runspace id number
$RunspaceCatalog.RunspaceCountPosition
}
function Remove-AsyncRunspace {
param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" }
#Find selected runspace by number
if ($RunspaceID -is [Int32]) {
$RunspaceCatalog.RunspaceList[$RunspaceID].Close()
$RunspaceCatalog.RunspaceList.Remove($RunspaceID)
}
#Or find selected runspace by name
else {
$keys = $RunspaceCatalog.RunspaceList.Keys
$keys | Foreach-Object {
if ( $RunspaceCatalog.RunspaceList[$_].Name -like $RunspaceID ) {
$RunspaceCatalog.RunspaceList[$_].Close()
$RunspaceCatalog.RunspaceList.Remove($_)
}
}
}
}
function Get-AsyncRunspace {
param ($RunspaceID)
# Return the entire list of available runspaces by default
if ( $RunspaceID -eq $null ) {
return $RunspaceCatalog.RunspaceList.Values
}
# Type check on id
if ( -not ($RunspaceID -is [Int32] -or $RunspaceID -is [String])) { throw "A runspace id must be supplied by integer or name!" }
#Find selected runspace by number
if ($RunspaceID -is [Int32]) {
$SelectedRunspace = $RunspaceCatalog.RunspaceList[$RunspaceID]
}
#Or find selected runspace by name
else {
$SelectedRunspace = $RunspaceCatalog.RunspaceList.Values | Where-Object { $_.Name -like $RunspaceID }
}
# Return result
$SelectedRunspace
}
function Invoke-ExpressionInRunspace {
param ($RunspaceID = (read-host "Enter a valid runspace ID or name."), [String]$Command = (read-host "Enter a command to be executed in the runspace."))
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
# Throw error if no runspace was found with the given id
if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Insert the command into the runspace.
$Pipeline = $SelectedRunspace.CreatePipeline($Command)
# Insert input into the new pipe
$input | Foreach-Object { [void]$Pipeline.Input.Write($_) }
$Pipeline.Input.Close()
# Execute pipe
$Pipeline.Invoke()
# Return output from pipe
$Pipeline.Output.ReadToEnd()
}
function Start-AsyncPipe {
param (
$RunspaceID = (read-host "Enter a valid runspace ID or name."),
[String]$Command = (read-host "Enter a command to executed in the runspace.")
)
BEGIN {
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
# Throw error if no runspace was found with the given id
if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Insert the command into the runspace.
$Pipeline = $SelectedRunspace.CreatePipeline($Command)
# Start async process
$Pipeline.InvokeAsync()
# List this pipe as current with the runspace
$SelectedRunspace.CurrentPipe = $Pipeline
}
PROCESS {
# Insert input into the new pipe
[void]$Pipeline.Input.Write($_)
}
END {
# Close input
$Pipeline.Input.Close()
}
}
function Read-AsyncPipe {
param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
# Throw error if no runspace was found with the given id
if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Read pipeline result
if ($SelectedRunspace.CurrentPipe.pipelineStateInfo.state -eq [management.automation.runspaces.pipelinestate]::failed) {
throw $SelectedRunspace.CurrentPipe.PipelineStateInfo.Reason
}
$SelectedRunspace.CurrentPipe.Output.nonblockingread()
}
function End-AsyncPipe {
param ($RunspaceID = (read-host "Enter a valid runspace ID or name."))
$SelectedRunspace = Get-AsyncRunspace $RunspaceID | Select-Object -first 1
# Throw error if no runspace was found with the given id
if ( ($SelectedRunspace | Measure-Object).Count -eq 0 ) { throw "Runspace not found!" }
# Pipeline is no longer current
$Pipeline = $SelectedRunspace.CurrentPipe
$SelectedRunspace.CurrentPipe = $null
# Read pipeline result
if ($Pipeline.PipelineStateInfo.State -eq [management.automation.runspaces.pipelinestate]::failed) {
throw $Pipeline.PipelineStateInfo.Reason
}
else {
while (-not $Pipeline.Output.endofpipeline) {
$Pipeline.Output.nonblockingread()
start-sleep -m 50 #don't overload the cpu
}
$Pipeline.output.Close()
}
}
That's it. 183 lines of code and it can't do a thing with out your inspiration so take it and build some network applications. I'll have to make a note of those three letter aliases again, because learning the dynamics of the whole thing using the full names is less than fun. I did enjoy the debug procedure using those aliases and funny runspace names. Now I'll close with a simple example of the use of the script. The following example should be valid is you're using the aliases. PS>nar foo 1 PS>iar foo { $h = "Hello"; $w = "world!" } PS>"$(iar foo {$h}) $(iar foo {$w})" Hello world! Try it. Have fun, and all that will make sence in about 5 minutes.
|