As OP beg for that, here is simple and very basic full PowerShell implementation of a named pipe client / server that will not hang. Launch the server script in one CLI. Then launch the client script in another CLI, you can launch that last one many times and see the effect on the server CLI.
The PowerShell code is not very clean, but it's just an example demonstration. And i can't afford hours on that.
The server script is a basic "host list" maintainer, just note that, due to the use of a StreamWriter
, the output pipe requires to be re-instantiated after each write.
This server understand theses commands :
hello
, it replies with current date and time
add [host]
, it adds [host] to its internal list and reply with the current list size.
del [host]
, it removes [host] from its internal list and reply with the remaining number of hosts.
list
, it reply with the list of [host] in its internal list.
Server script
# NamedPipe Server script
$status = 0;
$npipeServerInput = new-object System.IO.Pipes.NamedPipeServerStream('my_pipe_server_input', [System.IO.Pipes.PipeDirection]::In, 1, [System.IO.Pipes.PipeTransmissionMode]::Message, [System.IO.Pipes.PipeOptions]::None, 256, 256);
if (!$npipeServerInput) {
Write-Host("Failed to open server input pipe...");
exit(1);
}
$npipeServerOutput = new-object System.IO.Pipes.NamedPipeServerStream('my_pipe_server_output', [System.IO.Pipes.PipeDirection]::Out, 1, [System.IO.Pipes.PipeTransmissionMode]::Message, [System.IO.Pipes.PipeOptions]::None, 256, 256);
if (!$npipeServerOutput) {
Write-Host("Failed to open server output pipe...");
$npipeServerInput.Dispose();
exit(1);
}
$server_list = new-object System.Collections.ArrayList;
function ProcessMessage([String]$ParMessage) {
$reply = ""
switch($ParMessage) {
"hello" {
$curdte = Get-Date -Format 'HH:mm:ss';
$reply='Reply to '+$ParMessage+', im here, time is '+$curdte;
}
"list" {
$server_list | %{ $reply += [Environment]::NewLine + $_; };
}
default {
if ($ParMessage.StartsWith("add ")) {
$reply = $server_list.Add($ParMessage.Split(" ")[1]);
$reply = "Added, " + [String]$server_list.Count + " items";
} else {
if ($ParMessage.StartsWith("del ")) {
$reply = $server_list.Remove($ParMessage.Split(" ")[1]);
$reply = "Removed, " + [String]$server_list.Count + " items left";
} else {
$reply = "Unknown command " + $ParMessage;
}
}
}
}
return($reply);
}
Try {
$npipeServerInput.WaitForConnection();
$pipeReader = new-object System.IO.StreamReader($npipeServerInput);
if (!$pipeReader) {
Write-Host("Failed to read server input pipe...");
$status = 2;
} else {
Try {
While(($msg = $pipeReader.ReadLine()) -notmatch 'QUIT') {
Try {
If ($msg.Length -gt 0) {
$disp='Server got :'+$msg;
Write-Host($disp);
$reply = ProcessMessage($msg);
$npipeServerOutput.WaitForConnection();
$pipeWriter = new-object System.IO.StreamWriter($npipeServerOutput);
if (!$pipeWriter) {
Write-Host("Failed to write server output pipe...");
$status = 2;
} else {
Try {
$reply_flag=0;
While($reply_flag -eq 0) {
Try {
# Write-Output($reply) ^>5;
$pipeWriter.WriteLine($reply);
$reply_flag=1;
} Catch [System.IO.IOException] {
# Deal as you whish with potential errors
Write-Host("Failed to write to server output pipe...");
exit(3);
};
};
$pipeWriter.Flush();
Write-Host('Reply sent. '+$reply);
} Finally {
# The streamwriter disposal will mess up our output pipe, so we recrerate it...
$pipeWriter.Dispose();
$npipeServerOutput.Dispose();
$npipeServerOutput = new-object System.IO.Pipes.NamedPipeServerStream('my_pipe_server_output', [System.IO.Pipes.PipeDirection]::Out, 1, [System.IO.Pipes.PipeTransmissionMode]::Message, [System.IO.Pipes.PipeOptions]::None, 256, 256);
}
}
}
} Finally {
$npipeServerInput.Disconnect();
$npipeServerInput.WaitForConnection();
}
};
} Catch [System.IO.IOException] {
# Deal as you whish with potential errors, you will have InvalidOperation here when the streamReader is empty
Write-Host("Failed to read server input pipe...");
exit(4);
}
}
} Catch [System.IO.IOException] {
Write-Host("I/O failure...");
} Finally {
If ($npipeServerInput) {
$npipeServerInput.Dispose();
}
If ($npipeServerOutput) {
$npipeServerInput.Dispose();
}
}
exit($status)
The client script :
- Sends
hello
to the server and display its reply.
- Sends
add localhost
to the server and display its reply.
- Sends
add google.com
to the server and display its reply.
- Sends
add stackoverflow.com
to the server and display its reply.
- Sends
del google.com
to the server and display its reply.
- Sends
list
to the server and display its reply.
The client script wait 5 seconds for a reply for each command sent, if the server hasn't replied in the delay the client script aborts. Each time the client script run, localhost
and stackoverflow.com
are added to the server list.
For commodity, i shamelessly take the New-ScriptBlockCallback
from Oisin Grehan, very convenient to make your callback directly in PowerShell.
As the server script re-instantiated its output pipe after each write, the client script do then the same for its connected input pipe. This can be avoided by using something else in the server script than the StreamWriter
(i keep it to show that with PowerShell, you have to care about underlying disposal of objects). The pipe methods are static
methods in a class for convenience, as i prefer a return
that doesn't take everything that happens in the functions. There is also a basic stack for incoming messages from the server, to show a basic way to handle incoming messages with a callback
.
Client script
# NamedPipe Client script
#
# Client return status
#
$status = 0;
#
# Input and output named pipes to exchange with the server
#
$npipeClientInput = new-object System.IO.Pipes.NamedPipeClientStream('.', 'my_pipe_server_output', [System.IO.Pipes.PipeDirection]::In, [System.IO.Pipes.PipeOptions]::None);
if (!$npipeClientInput) {
Write-Host("Main: Failed to open client input pipe...");
exit(1);
}
$npipeClientOutput = new-object System.IO.Pipes.NamedPipeClientStream('.', 'my_pipe_server_input', [System.IO.Pipes.PipeDirection]::Out, [System.IO.Pipes.PipeOptions]::None);
if (!$npipeClientOutput) {
Write-Host("Main: Failed to open client output pipe...");
$npipeClientInput.Dispose();
exit(1);
}
# Thanks to Oisin Grehan, https://stackoverflow.com/a/41155285/3641635
function New-ScriptBlockCallback {
param(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[scriptblock]$Callback
)
<#
.SYNOPSIS
Allows running ScriptBlocks via .NET async callbacks.
.DESCRIPTION
Allows running ScriptBlocks via .NET async callbacks. Internally this is
managed by converting .NET async callbacks into .NET events. This enables
PowerShell 2.0 to run ScriptBlocks indirectly through Register-ObjectEvent.
.PARAMETER Callback
Specify a ScriptBlock to be executed in response to the callback.
Because the ScriptBlock is executed by the eventing subsystem, it only has
access to global scope. Any additional arguments to this function will be
passed as event MessageData.
.EXAMPLE
You wish to run a scriptblock in reponse to a callback. Here is the .NET
method signature:
void Bar(AsyncCallback handler, int blah)
ps> [foo]::bar((New-ScriptBlockCallback { ... }), 42)
.OUTPUTS
A System.AsyncCallback delegate.
#>
# is this type already defined?
if (-not ("CallbackEventBridge" -as [type])) {
Add-Type @"
using System;
public sealed class CallbackEventBridge
{
public event AsyncCallback CallbackComplete = delegate { };
private CallbackEventBridge() {}
private void CallbackInternal(IAsyncResult result)
{
CallbackComplete(result);
}
public AsyncCallback Callback
{
get { return new AsyncCallback(CallbackInternal); }
}
public static CallbackEventBridge Create()
{
return new CallbackEventBridge();
}
}
"@
}
$bridge = [callbackeventbridge]::create()
Register-ObjectEvent -input $bridge -EventName callbackcomplete -action $callback -messagedata $args > $null
$bridge.callback
}
class ClientPipes {
#
# Internal FIFO stack for incoming messages from the server
#
static [System.Collections.Queue]$ReadStack;
#
# Function to send a message to the server
#
# Parameters :
# ParOutputPipe : Ref to the output pipe
# $Message : Message to send
# Returns :
# 0 : Success
# 1 : Wrong parameters
# 2 : I/O failure
static [int32]SendServer([ref]$ParOutputPipe, [String]$Message) {
$OutputPipe = [System.IO.Pipes.NamedPipeClientStream]$ParOutputPipe.value;
# Connect the client output pipe
if (!$OutputPipe.IsConnected) {
$OutputPipe.Connect(2000);
Write-Host("SendServer: output pipe connected.");
}
$pipeWriter = new-object System.IO.StreamWriter($OutputPipe);
if (!$pipeWriter) {
Write-Host("SendServer: Failed to set a writer to client output pipe...");
$status = 2;
} else {
Try {
if (!$pipeWriter) {
Write-Host("SendServer: Wrong parameters..");
return(1);
}
Try {
# We send to the server
$pipeWriter.WriteLine($Message);
$pipeWriter.Flush();
$OutputPipe.WaitForPipeDrain();
} Catch [System.IO.IOException] {
Write-Host("SendServer: Failed to write to server...");
return(2);
} Finally {
# The streamwriter disposal will mess up our output pipe, so we recrerate it...
$pipeWriter.Dispose();
$OutputPipe.Dispose();
$ParOutputPipe.value = new-object System.IO.Pipes.NamedPipeClientStream('.', 'my_pipe_server_input', [System.IO.Pipes.PipeDirection]::Out, [System.IO.Pipes.PipeOptions]::None);
}
} Catch [System.IO.IOException] {
# Deal as you whish with potential errors, you will have InvalidOperation here when the streamReader is empty
Write-Host("SendServer: Failed to write client output pipe...");
return(2);
}
}
return(0);
}
#
# Synchonized function to push on the FIFO stack
#
# Parameters :
# $Message : Message to push
# Returns :
# 0 : Success
# 1 : Synchronization failure
static [int32]PushOnStack([System.Collections.Queue]$Stack, [string]$ParMessage) {
$SyncStack = [System.Collections.Queue]::Synchronized($Stack);
if (!$SyncStack) {
Write-Host("PushOnStack: Failed to synchronize...");
return(1);
}
$SyncStack.Enqueue($ParMessage);
return(0);
}
#
# Synchonized function to pop from the FIFO stack
#
# Parameters :
# $Message : Buffer to store message
# Returns :
# 0 : Success, there was a message
# 1 : Success, there was no message
# 2 : Synchronization failure
static [int32]PopFromStack([System.Collections.Queue]$Stack,[ref]$ParMessage) {
$SyncStack = [System.Collections.Queue]::Synchronized($Stack);
if (!$SyncStack) {
Write-Host("PopFromStack: Failed to synchronize...");
return(2);
}
if ($SyncStack.Count -ne 0) {
$ParMessage.value = [String]$SyncStack.Dequeue();
return(0);
} else {
return(1);
}
}
#
# Callback function for incoming messages from the server
#
# Parameters :
# $Result : Object issued
# Returns :
# 0 : Success
# 1 : Failure
# 2 : Failure, stack
# 3 : Failure, pipe
static [int32]ProcessRead([IAsyncResult]$Result) {
$status = 0;
# Check if that comes from our ReadServer function
$RefPipe = [ref]$Result.AsyncState[0];
$Pipe = [System.IO.Pipes.NamedPipeClientStream]$RefPipe.value;
if (!$Pipe) {
Write-Host("ProcessRead: Unexpected event...");
$status = 1;
} else {
Write-Host("ProcessRead: Callback event");
$Msg = [System.Text.Encoding]::ASCII.GetString($Result.AsyncState[1]);
$Stack = [System.Collections.Queue]$Result.AsyncState[2];
Write-Host("ProcessRead: read " + $Msg);
if ([ClientPipes]::PushOnStack($Stack, $Msg) -ne 0) {
Write-Host("ProcessRead: Failed to push message...");
$status = 2;
}
Try {
$Pipe.EndRead($Result);
#$Pipe.Close();
} Catch [System.IO.IOException] {
Write-Host("Failed to terminate read client input pipe...");
$status = 3;
} finally {
# As the server have probably closed the pipe, we recreate it
$RefPipe.value = new-object System.IO.Pipes.NamedPipeClientStream('.', 'my_pipe_server_output', [System.IO.Pipes.PipeDirection]::In, [System.IO.Pipes.PipeOptions]::None);
}
}
return($status);
}
#
# Function to set a handler to read incoming messages from the server
#
# Parameters :
# $ParInputPipe : Ref to input pipe
# Returns :
# 0 : Success
# 1 : Wrong parameters
# 2 : I/O failure
# 3 : Internal failure
static [int32]ReadServer([ref]$ParInputPipe) {
$InputPipe = [System.IO.Pipes.NamedPipeClientStream]$ParInputPipe.value;
if (!$InputPipe) {
Write-Host("ReadServer: Wrong parameters..");
return(1);
}
Try {
# We create the Async callback to receive messages
$Buffer = New-Object Byte[] 256
$CallBackParams = @($ParInputPipe, $Buffer, [ClientPipes]::ReadStack)
$CallBack = New-ScriptBlockCallback { param([IAsyncResult] $Result) Write-Host("Callback event"); [ClientPipes]::ProcessRead($Result); };
if (!$CallBack) {
Write-Host("ReadServer: Failed to create a callback...");
return(3);
}
# Connect the Pipe
if (!$InputPipe.IsConnected) {
$InputPipe.Connect(2000);
Write-Host("ReadServer: Pipe connected.");
}
# We add a signal handler to get incoming Message
$AsyncHandler = $InputPipe.BeginRead($Buffer, 0, 256, $CallBack, $CallBackParams)
} Catch [System.IO.IOException] {
Write-Host("ReadServer: Failed to set a callback...");
return(2);
}
return(0);
}
#
# Get last message from the msg stack
#
# Parameters :
# $LastMsg : Buffer to store last message
# $ParDelay : Timeout in second
# Returns :
# 0 : Success, there was a message
# 1 : Timeout, no last message
# 2 : Failure
static [int32]GetLastMessage([ref]$LastMsg, [int32]$ParDelay) {
$StackStatus = 1;
$Timer = [Diagnostics.Stopwatch]::StartNew();
While($StackStatus -eq 1 -and $Timer.Elapsed.TotalSeconds -lt $ParDelay) {
Write-Host("GetLastMessage: Waiting for a reply...");
$StackStatus = [ClientPipes]::PopFromStack([ClientPipes]::ReadStack, $LastMsg);
if ($StackStatus -eq 1) {
sleep -m 1000
}
};
if ($StackStatus -eq 2) {
Write-Host("GetLastMessage: Error on incoming reply...");
return(2);
}
return($StackStatus);
}
#
# Exchnage request / rsponse with the server
#
# Parameters :
# $InputPipe : Ref to client input pipe
# $OutputPipe : Ref to client output pipe
# $Request : Requets to send to the server
# $Response : Buffer to store server response
# $ParDelay : Timeout in second
# Returns :
# 0 : Success, we have have a response
# 1 : Timeout, no last message
# 2 : Failure
static [int32]Request([ref]$InputPipe, [ref]$OutputPipe, [String]$Request,[ref]$Response, [int32]$ParDelay) {
# We send to the server and we expect to get a reply
Write-Host("Request: Sending " + $Request);
if ([ClientPipes]::SendServer($OutputPipe, $Request) -ne 0) {
Write-Host("Request: Failed to send a message through client output pipe...");
return(2);
} else {
# We poll the incoming stack for messages
if ([ClientPipes]::ReadServer($InputPipe) -ne 0) {
Write-Host("Request: Failed to set a callback...");
return(2);
}
if ([ClientPipes]::GetLastMessage($Response, $ParDelay) -eq 0) {
return(0);
}
return(1);
}
}
}
#
# Main
#
[ClientPipes]::ReadStack = new-object System.Collections.Queue;
if (![ClientPipes]::ReadStack) {
Write-Host("Main: Can't get a stack...");
exit(1);
}
Try {
Write-Host("Main: Setting up client");
# We send to the server "Hello" and we expect to get a reply in less than 5 seconds
Write-Host("Main: Sending hello");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"hello",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
# We send to the server a request to add "localhsot" to its internal stack and we expect to get a reply in less than 5 seconds
Write-Host("Main: Server, store localhost please.");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"add localhost",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
# We send to the server a request to add "google.com" to its internal stack and we expect to get a reply in less than 5 seconds
Write-Host("Main: Server, store google.com please.");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"add google.com",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
# We send to the server a request to add "stackoverflow.com" to its internal stack and we expect to get a reply in less than 5 seconds
Write-Host("Main: Server, store stackoverflow.com please.");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"add stackoverflow.com",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
# We send to the server a request to remove "google.com" from its internal stack and we expect to get a reply in less than 5 seconds
Write-Host("Main: Server, remove google.com please.");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"del google.com",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
# We send to the server an output of its internal stack and we expect to get a reply in less than 5 seconds
Write-Host("Main: Server, your stack please.");
$LastMsg=""
$status = [ClientPipes]::Request([ref]$npipeClientInput,[ref]$npipeClientOutput,"list",[ref]$LastMsg, 5);
if ($status -eq 0) {
Write-Host("Main: Get a reply : " + $LastMsg);
} else {
exit(1);
}
} Catch [System.IO.IOException] {
Write-Host("Main: I/O failure...");
} Finally {
If ($npipeClientInput) {
$npipeClientInput.Dispose();
}
If ($npipeClientOutput) {
$npipeClientOutput.Dispose();
}
}
exit($status)
On a final note, i guess that your troubles with pipes and sockets come from common things like :
- "Non class" functions in PowerShell will returns everything from their stdout. As an example, the
ArrayList.Add
method in the server script output the resulting number of items on stdout, and then make the reply as an object instead of a String
. A simple way to avoid that, assign its output to a variable.
- Redirection like
>NUL
in 'read/write pipe' functions will mess up your pipes. Do not do that.
- Some stream helper like
StreamReader
or StreamWriter
will close the underlying pipe at their disposal, so take care of that or do not use them.
Surely i forgot plenty of things.
For the demo, the output of the server script :
Server got :hello
Reply sent. Reply to hello, im here, time is 23:23:13
Server got :add localhost
Reply sent. Added, 1 items
Server got :add google.com
Reply sent. Added, 2 items
Server got :add stackoverflow.com
Reply sent. Added, 3 items
Server got :del google.com
Reply sent. Removed, 2 items left
Server got :list
Reply sent.
localhost
stackoverflow.com
The output of the client script :
Main: Setting up client
Main: Sending hello
Request: Sending hello
SendServer: output pipe connected.
ReadServer: Pipe connected.
Callback event
ProcessRead: Callback event
ProcessRead: read Reply to hello, im here, time is 23:23:13
GetLastMessage: Waiting for a reply...
Main: Get a reply : Reply to hello, im here, time is 23:23:13
Main: Server, store localhost please.
Request: Sending add localhost
SendServer: output pipe connected.
ReadServer: Pipe connected.
GetLastMessage: Waiting for a reply...
Callback event
ProcessRead: Callback event
ProcessRead: read Added, 1 items
GetLastMessage: Waiting for a reply...
Main: Get a reply : Added, 1 items
Main: Server, store google.com please.
Request: Sending add google.com
SendServer: output pipe connected.
ReadServer: Pipe connected.
Callback event
ProcessRead: Callback event
ProcessRead: read Added, 2 items
GetLastMessage: Waiting for a reply...
Main: Get a reply : Added, 2 items
Main: Server, store stackoverflow.com please.
Request: Sending add stackoverflow.com
SendServer: output pipe connected.
ReadServer: Pipe connected.
Callback event
ProcessRead: Callback event
ProcessRead: read Added, 3 items
GetLastMessage: Waiting for a reply...
Main: Get a reply : Added, 3 items
Main: Server, remove google.com please.
Request: Sending del google.com
SendServer: output pipe connected.
ReadServer: Pipe connected.
GetLastMessage: Waiting for a reply...
Callback event
ProcessRead: Callback event
ProcessRead: read Removed, 2 items left
GetLastMessage: Waiting for a reply...
Main: Get a reply : Removed, 2 items left
Main: Server, your stack please.
Request: Sending list
SendServer: output pipe connected.
ReadServer: Pipe connected.
GetLastMessage: Waiting for a reply...
Callback event
ProcessRead: Callback event
ProcessRead: read
localhost
stackoverflow.com
GetLastMessage: Waiting for a reply...
Main: Get a reply :
localhost
stackoverflow.com