A little while ago I started writing a telnet client in Microsoft's PowerShell. After it sat unfinished for a time I finally got around to improving it with multiple threads and cleaning it up. It now works well so I decided to post about it.
Why would I write a telnet client in PowerShell? Just for fun mainly. It has been a good project to learn some more about PowerShell and may be of use for automating the configuration of Cisco routers and switches or running scripts on other servers.
The most interesting thing I learned as I worked through the project was about how to get PowerShell to support multiple threads. Using the low level .NET Framework System.Net.Sockets.Socket class added to the complexity.
To start off with I created the telnet client using a "while" loop that ran continuously and caused the script to use up 20% of the CPU while doing nothing. I couldn't fix this with a sleep timer because it made the client unresponsive. The problem was I needed to respond asynchronously to reception of data through the TCP connection, and respond to user input at the console. Very easy to do with C#, but in PowerShell?
To implement the reception and transmission of data at the same time asynchronously I started by trying to use the Asynchronous Programming Model used in the .NET Framework. This is a little tricky because the tread pool used in PowerShell is different to the thread pool used in .NET. I did find a way of using the async callback methods with PowerShell from a blog post by Oisin Greham. I still had issues trying to get this to work though.
I gave up on using the async methods of the Socket class and started looking for alternatives. It would have been nice to use the Register-ObjectEvent cmdlet and other event cmdlets but the Socket class does not have any publicly visible events to consume.
I briefly looked at the PowerShell Jobs cmdlets, but they didn't work well for this application because they use the remoting subsystem which serializes objects when they are passed between jobs. This means passing an object by reference is not possible and I need a reference to the connected Socket. That's when I came across the concept of creating a new PowerShell object using [PowerShell]::Create().
When [PowerShell]::Create() is called from a PowerShell script or console, a new instance of PowerShell with an empty pipeline is returned for you to make dance and sing any way you like. The beauty of this new PowerShell object is you can pass objects by reference meaning I could pass the connected Socket.
So now I have two threads to use in my PowerShell telnet client. The main PowerShell process creates a child PowerShell process and initiates it with a script to receive data from the socket. After initiating the child a "while" loop is used with a blocking call to the $Host.UI.RawUI.ReadKey() method to wait for user input.
Rather than explain the code in any more detail, I will let the code do the talking. If you want to use this code use the Gist link: https://gist.github.com/grantcarthew/6985142
Hi Grant!
ReplyDeleteYour post was very useful for me as a first step - thank you so much for it! I also would to note that even "Socket class does not have any publicly visible events to consume", events might be leveraged using "xxxAsync" methods - the "SocketAsyncEventArgs" class provides "Completed" event and I found it really easy and convenient to use in conjunction with PowerShell events handling model.
Thanks for the comment Stanvy.
DeleteI don't remember why I didn't use the Completed event. I did see it and try to use it but it didn't achieve what I needed in the telnet client. I think it was the ability to transmit and receive at the same time.
Can you post a Gist of your solution?
Thanks again.
What do you mean under "at the same time"? Be able to perform both operations within the same thread or use dedicated thread for each operation? Based on your code, I guess you are talking about the same thread, however both options are available if it necessary.
DeleteHere a simplified code I'm using - "Echo" clients ("Async" and "Sync" versions) and "Async" server I have written for you as example.
However, I have faced with issue - at some point "Async" version gets stuck until you press "Enter" or "Ctrl+C" in the console. It seems like some internal buffer/queue/pipe gets filled and this workaround makes it reset. Unfortunately, as I'm not a developer and this is my first multithreading application, it's a bit challenging for me to find out the root cause of this issue and probably, it's the same thing you have faced with previously. Isn't it :) ?
I would suggest to try "Async" server and "Sync" client for easier reproducing of this issue.
Actually, this issue doesn't affect my current needs, but I would like to eliminate it completely as "tomorrow" it could bring some troubles :) ...
Do you have any ideas on that?
Ah yes, that was the issue I had. If you look at line 202 in the code you will see that I am instantiating a new PowerShell process giving me the ability to receive while the main PowerShell process is blocking.
ReplyDeleteIn fact, if you read the article above you would have seen that. I had forgotten the article.
Thanks again.
Thanks for posting this. I was able to tweak it to meet my needs with some old network equipment, wouldn't have even gotten close without it!
ReplyDeleteGood stuff Jared. I'm glad someone can use it. I am still not using it for anything.
Delete