Pipes/NamedPipes.ps1


$ErrorActionPreference = "Stop"


if (-not ("CallbackEventBridge" -as [type])) {
    Add-Type @"
        using System;
        public sealed class CallbackEventBridge
        {
            public event AsyncCallback CallbackComplete = delegate {};
            private void CallbackInternal(IAsyncResult result)
            {
                CallbackComplete(result);
            }
            public AsyncCallback Callback
            {
                get { return new AsyncCallback(CallbackInternal); }
            }
        }
"@
}

class Pipe {

    Pipe() {
        
    }

    static [string] convertToCliXML ([pscustomobject]$obj){
        $tempFilePath = [System.IO.Path]::GetTempFileName()
        $obj | Export-Clixml -Path $tempFilePath -Force
        [string]$cliXML = Get-Content $tempFilePath -Raw -Encoding UTF8 #-ErrorAction Stop
        Remove-Item -Path $tempFilePath | Out-Null
        return $cliXML
    }

    static [pscustomobject] convertFromCliXML ([string]$cliXML){

        [pscustomobject]$obj = $null
        
        if( $cliXML.Contains("http://schemas.microsoft.com/powershell/2004/04")) {            
            $tempFilePath = [System.IO.Path]::GetTempFileName()
            $cliXML | Set-Content $tempFilePath -Force -Encoding UTF8 -NoNewline #-ErrorAction Stop
            $obj = Import-Clixml $tempFilePath
            Remove-Item -Path $tempFilePath | Out-Null                
        }
        else {
            if($cliXML -ne "NULL") { 
                #Write-Host "convertFromCliXML - skipped"
                $obj = [pscustomobject]$cliXML 
            }
            else {
                #Write-Host "convertFromCliXML - NULL"
                $obj = $null
            }
        }
        return $obj
    }
}
class NamedPipeServer : Pipe {

    hidden $_pipeName = "";
    hidden $_pipeClosedByClient = $false

    hidden [System.IO.Pipes.NamedPipeServerStream]$_serverPipe = $null;
    hidden $_readBuffer = $null;

    hidden $_OnConnectedCallbackBridge = $null; # this is our internal async event bridge
    hidden [System.Management.Automation.ScriptBlock]$_OnConnectedScriptBlock = {}; # this is the scriptblock the user wants to call

    hidden $_OnDataAvailableCallbackBridge = $null; # this is our internal async event bridge
    hidden [System.Management.Automation.ScriptBlock]$_OnDataAvailableScriptBlock = {}; # this is the scriptblock the user wants to call


    hidden $_OnDataWrittenCallbackBridge = $null; # this is our internal async event bridge


    # Constructor
    NamedPipeServer ([string]$PipeName) : base () {

        $this._pipeName = $PipeName
        $this._pipeClosedByClient = $false
        $this._readBuffer = New-Object byte[] 65536
        #$this._serverPipe = New-Object System.IO.Pipes.NamedPipeServerStream $PipeName, InOut, 1, Byte, Asynchronous, 65536, 65536
        #$this.Start()
        # Create a bridge for OnConnected async events
        $this._OnConnectedCallbackBridge = New-Object CallbackEventBridge
        Register-ObjectEvent -InputObject $this._OnConnectedCallbackBridge -EventName CallbackComplete `
            -Action {
                param($asyncResult)
                $asyncResult.AsyncState.OnClientConnected.Invoke($asyncResult);
            } > $null


        # Create a bridge for OnDataAvailable async events
        $this._OnDataAvailableCallbackBridge = New-Object CallbackEventBridge
        Register-ObjectEvent -InputObject $this._OnDataAvailableCallbackBridge -EventName CallbackComplete `
            -Action {
                param($asyncResult)
                $asyncResult.AsyncState.OnDataAvailable.Invoke($asyncResult);
            } > $null


        # Create a bridge for OnDataWritten async events
        $this._OnDataWrittenCallbackBridge = New-Object CallbackEventBridge
        Register-ObjectEvent -InputObject $this._OnDataWrittenCallbackBridge -EventName CallbackComplete `
            -Action {
                param($asyncResult)
                $asyncResult.AsyncState.OnDataWritten.Invoke($asyncResult);
            } > $null
    }
 

    
    [void]WaitForConnection(){
        #Write-Host "WaitForConnection"
        $this._serverPipe.WaitForConnection();
    }
    [void]BeginWaitForConnection([System.Management.Automation.ScriptBlock]$Callback = {}){
        #Write-Host "BeginWaitForConnection"
        $this._OnConnectedScriptBlock = $Callback; # Save the script the user wants to call on connect
        $this._serverPipe.BeginWaitForConnection($this._OnConnectedCallbackBridge.Callback, $this);
    }
    hidden [void]OnClientConnected($asyncResult){
        #Write-Host "OnClientConnected"
        $this._serverPipe.EndWaitForConnection($asyncResult)
        $this._OnConnectedScriptBlock.Invoke($this);
    }



    [void]BeginRead([System.Management.Automation.ScriptBlock]$Callback){
        #Write-Host "BeginRead"
        $this._OnDataAvailableScriptBlock = $Callback; # Save the script the user wants to call when data is available
        $this._serverPipe.BeginRead($this._readBuffer, 0, $this._readBuffer.Length, $this._OnDataAvailableCallbackBridge.Callback, $this)
    }
    hidden [void]OnDataAvailable($asyncResult){
        #Write-Host "OnDataAvailable"
        $MessageLength = $this._serverPipe.EndRead($asyncResult)

        if ($MessageLength -gt 0){
            $PipeText = [System.Text.Encoding]::UTF8.GetString($this._readBuffer, 0, $MessageLength)
            #$this._OnDataAvailableScriptBlock.Invoke($PipeText, $this);
            [pscustomobject]$obj = [Pipe]::convertFromCliXML( $PipeText )            
            $this._OnDataAvailableScriptBlock.Invoke($obj, $this);
            $this.BeginRead($this._OnDataAvailableScriptBlock);
        } else {
            # Pipe was closed
            # Write-Host "Pipe closed" -ForegroundColor Red
            $this._pipeClosedByClient = $true
            $PipeText = ""
            $this._OnDataAvailableScriptBlock.Invoke($PipeText, $this);
        }
    }


    [void]BeginWrite([string]$Message){
        $_writeBufferLocal = [System.Text.Encoding]::UTF8.GetBytes($Message)
        $this._serverPipe.BeginWrite($_writeBufferLocal, 0, $_writeBufferLocal.Length, $this._OnDataWrittenCallbackBridge.Callback, $this)
    }
    hidden [void]OnDataWritten($asyncResult){
        $asyncResult._serverPipe.EndWrite($asyncResult)
    }
    [void]Write([string]$Message){
        $_writeBufferLocal = [System.Text.Encoding]::UTF8.GetBytes($Message)
        $this._serverPipe.Write($_writeBufferLocal, 0, $_writeBufferLocal.Length)    
    }

    [Boolean] IsClosedByClient(){
        return $this._pipeClosedByClient
    }

    [Boolean] IsConnected(){
        return $this._serverPipe.IsConnected
    }
    
    [string] GetPipeName(){
        return $this._pipeName
    }
    [void] Start(){
        $this._serverPipe = New-Object System.IO.Pipes.NamedPipeServerStream $($this._pipeName), InOut, 1, Byte, Asynchronous, 65536, 65536
    }
    [void] Stop (){
        $this._serverPipe.Close();
    }
}
class NamedPipeClient : Pipe {
    hidden $_pipeName = "";
    hidden [System.IO.Pipes.NamedPipeClientStream]$_clientPipe = $null;
    hidden $_readBuffer = $null;
    
    hidden $_OnDataAvailableCallbackBridge = $null; # this is our internal async event bridge
    hidden [System.Management.Automation.ScriptBlock]$_OnDataAvailableScriptBlock = {}; # this is the scriptblock the user wants to call
    
    hidden $_OnDataWrittenCallbackBridge = $null; # this is our internal async event bridge
    
    
    NamedPipeClient ([string]$PipeName) : base () {
        $this._pipeName = $PipeName
        [string]$PipeServer = "."
        $this._readBuffer = New-Object byte[] 65536
        $this._clientPipe = New-Object System.IO.Pipes.NamedPipeClientStream $PipeServer, $PipeName, InOut, Asynchronous
        
        # Create a bridge for OnDataAvailable async events
        $this._OnDataAvailableCallbackBridge = New-Object CallbackEventBridge
        Register-ObjectEvent -InputObject $this._OnDataAvailableCallbackBridge -EventName CallbackComplete `
            -Action {
                param($asyncResult)
                $asyncResult.AsyncState.OnDataAvailable.Invoke($asyncResult);
            } > $null
        
        # Create a bridge for OnDataWritten async events
        $this._OnDataWrittenCallbackBridge = New-Object CallbackEventBridge
        Register-ObjectEvent -InputObject $this._OnDataWrittenCallbackBridge -EventName CallbackComplete `
            -Action {
                param($asyncResult)
                $asyncResult.AsyncState.OnDataWritten.Invoke($asyncResult);
            } > $null
    }
    
    [void]BeginRead([System.Management.Automation.ScriptBlock]$Callback){
        $this._OnDataAvailableScriptBlock = $Callback; # Save the script the user wants to call when data is available
        $this._clientPipe.BeginRead($this._readBuffer, 0, $this._readBuffer.Length, $this._OnDataAvailableCallbackBridge.Callback, $this)
    }
    hidden [void]OnDataAvailable($asyncResult){
        $MessageLength = $this._clientPipe.EndRead($asyncResult)

        if ($MessageLength -gt 0){
            $PipeText = [System.Text.Encoding]::UTF8.GetString($this._readBuffer, 0, $MessageLength)
            $this._OnDataAvailableScriptBlock.Invoke($PipeText, $this);
            $this.BeginRead($this._OnDataAvailableScriptBlock);
        } else {
            # Pipe was closed
            #Write-Verbose "Pipe closed" -ForegroundColor Red
        }
    }
    
    [void]BeginWrite([string]$Message){
        $_writeBufferLocal = [System.Text.Encoding]::UTF8.GetBytes($Message)
        $this._clientPipe.BeginWrite($_writeBufferLocal, 0, $_writeBufferLocal.Length, $this._OnDataWrittenCallbackBridge.Callback, $this)
    }
    hidden [void]OnDataWritten($asyncResult){
        $asyncResult._clientPipe.EndWrite($asyncResult)
    }
    [void] Write([string]$Message){
        $_writeBufferLocal = [System.Text.Encoding]::UTF8.GetBytes($Message)
        $this._clientPipe.Write($_writeBufferLocal, 0, $_writeBufferLocal.Length)    
    }
    [string] Read() {
        $PipeText = ""
        $MessageLength = $this._clientPipe.Read($this._readBuffer, 0, $this._readBuffer.Length)
        if ($MessageLength -gt 0){
            $PipeText = [System.Text.Encoding]::UTF8.GetString($this._readBuffer, 0, $MessageLength)
        } else {
            # Pipe was closed
            #Write-Host "Pipe closed" -ForegroundColor Red
            $this._clientPipe.Close();
        }
        return $PipeText
    }
    Connect([int]$Timeout = 2000){
        $this._clientPipe.Connect($Timeout);
    }
    
    Connect(){
        $this._clientPipe.Connect(2000);
    }

    [Boolean] IsConnected(){
        return $this._clientPipe.IsConnected
    }
    
    [string] GetPipeName(){
        return $this._pipeName
    }
    
    Close (){
        $this._clientPipe.Close();
    }
}
class PipeClient : NamedPipeClient {

    $Caller                     = $null

    PipeClient ([string]$PipeName) : base ($PipeName) {
    }

    [pscustomobject] ReadObject() {
        $obj = $null
        try {
            $PipeText = ([NamedPipeClient]$this).Read()
            [pscustomobject]$obj = [Pipe]::convertFromCliXML( $PipeText )

            if( $($obj.getType().Name) -eq "String") {
                if($obj -eq "invalid") { $obj = $null}
            }
        } catch {}
        return $obj
    }

    [void] Write( [pscustomobject]$obj ) {
        [string]$Message = [Pipe]::convertToCliXML( $obj )
        ([NamedPipeClient]$this).BeginWrite($Message)
    }


}
class PipeServer : NamedPipeServer {

    $Caller                     = $null

    hidden $_timer                 = $null #
    hidden $_doEvents             = $false
    hidden $_messageCallback     = $null
    hidden $_closeCallback         = $null

    # Constructor
    PipeServer ([string]$PipeName, [boolean]$DoEvents ) : base ($PipeName){
        $this._doEvents = $DoEvents
        $this._messageCallback = $null
        $this._closeCallback = $null

        $this.Open()
    }
    hidden [void] StartTimer() {
        if($this._doEvents) {
            if( -not $this._timer) {
                $this._timer = [System.Windows.Forms.Timer]::new()
                $this._timer.Interval = 1000
                $this._timer.add_tick( 
                {
                    Write-Host '' -NoNewline  # DoEvents doesn't works without this
                    [System.Windows.Forms.Application]::DoEvents() 
                }) #$this._timerCallback )
            }
            #Write-Host "starting DoEvents timer ..."
            $this._timer.start() 
        }
    }
    hidden [void] StopTimer() {
        if($this._doEvents) { 
            $this._timer.stop()
        }
        #Write-Host "DoEvents timer stopped"
    }
    hidden [void] StartServer() {
        $this.Start()
        $this.BeginWaitForConnection({            
            $_this = $($args[0])

            $pipename = $_this.GetPipeName()
            Write-Host "[server] $pipename opened by client";

            $_this.BeginRead({    
                $client_msg = $($args[0]) 
                $_this = $($args[1])

                if ( $client_msg -eq "" ) {

                    $pipename = $_this.GetPipeName()
                    Write-Host "[server] $pipename closed by client"

                    $closed = $_this.IsClosedByClient()
                    if( $closed ) {
                        $_this.Restart()
                    }
                }
                else {
                    if( $_this._messageCallback) { $_this._messageCallback.Invoke($_this, $client_msg) }
                }
            })
        })
    }
    hidden [void] StopServer() {
        $this.Stop()
    }
    [void] OnClose( [System.Management.Automation.ScriptBlock]$onCloseCallback ) {
        $this._closeCallback = $onCloseCallback
    }    
    [void] OnMessage( [System.Management.Automation.ScriptBlock]$onMessageCallback ) {
        $this._messageCallback = $onMessageCallback
    }    
    [void] Write([string]$Message){
        $this.BeginWrite($Message)
    }
    [void] Write( [pscustomobject]$obj ) {
        [string]$Message = [Pipe]::convertToCliXML( $obj )
        $this.BeginWrite($Message)
    }
    [void] Open (){
        $this.StartTimer()
        $this.StartServer()
    }
    [void] Close (){
        $this.StopServer()
        $this.StopTimer()
        if( $this._closeCallback) { $this._closeCallback.Invoke($this) }
    }
    [void] Restart() {
        $pipename = $this.GetPipeName()
        Write-Host "[server] $pipename restarted"
        $this.Close()
        $this.Open()
    }
    [void] Dispose (){
        $this.StopServer()
        $this.StopTimer()
        #$this._timer.Dispose($true)
    }

}