Search code examples
powershellterminalwindowwindows-terminal

Is there a way to send character strings between 2 powershell terminal windows?


In unix world we have always been able to send character strings between terminal windows using the /dev/ttyS serial devices or the /dev/pty pseudo-terminals. To do that is trivial once you know your tty/pty number, and could use:

# On /dev/pty1 write:
echo -e "\nHello! \n" >/dev/pty0

# On /dev/pty0, receive:

Hello! 

(There is a bug in the Cygwin/Bash version that interferes with the character \n and whatever is in front, unless its a space.)

This even works in MSYS/Cygwin.

Is there a way to accomplish a way to do the same (in Windows) between powershell terminal windows?


I have seen that you can send strings using the COM serial devices, using something like:

echo "Hello!" > \\.\COM3\

But AFAIK, the Windows terminal shell doesn't have an associated COM port, nor anything else obvious or accessible.

Apparently the COM ports are set in the registry at:
HKLM:\SYSTEM\CurrentControlSet\Control\COM Name Arbiter\Devices

References:


Solution

  • The closest I could think is to use NamedPipeServerStream and NamedPipeClientStream as suggested in this answer, however the difference here is that your server is started on a different thread and writes to your console using PSConsoleReadLine.Insert, this provides similar functionality as what you have in the Linux word.

    Here you can see how to use these functions:

    enter image description here

    You can also pipe to the writing function, so something like this can work:

    PS \> Get-Content foo.txt | pipemsg theDestinationPipe
    

    You should note using PSConsoleReadLine.Insert like this is a bit wonky and outside the supported realm.

    Lastly, the Start-NamedPipeServer function outputs an object that can be used as argument to Remove-NamedPipeServer. Once you're done using your server you can dispose it like this:

    # create
    $server = Start-NamedPipeServer myServer
    # dispose
    $server | Remove-NamedPipeServer
    

    Here is the function definitions:

    function Start-NamedPipeServer {
        [Alias('pipeserver')]
        param(
            [Parameter(Mandatory)]
            [string] $Name
        )
    
        if ([System.IO.File]::Exists("\\.\pipe\$Name")) {
            $err = [System.Management.Automation.ErrorRecord]::new(
                [System.IO.IOException]::new("A pipe already exists with name '$Name'."),
                'PipeExists',
                [System.Management.Automation.ErrorCategory]::OpenError,
                $null)
    
            $PSCmdlet.ThrowTerminatingError($err)
        }
    
        $script = {
            while (-not $token.IsCancellationRequested) {
                try {
                    $pipe = [System.IO.Pipes.NamedPipeServerStream]::new(
                        $Name, [System.IO.Pipes.PipeDirection]::InOut, 1,
                        [System.IO.Pipes.PipeTransmissionMode]::Byte,
                        [System.IO.Pipes.PipeOptions]::Asynchronous)
    
                    $task = $pipe.WaitForConnectionAsync($token)
                    while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
                    $null = $task.GetAwaiter().GetResult()
                    $sr = [System.IO.StreamReader]::new($pipe)
                    while ($null -ne ($msg = $sr.ReadLine())) {
                        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($msg + "`n")
                    }
                }
                catch [System.OperationCanceledException] {
                    # ignore
                }
                finally {
                    if ($pipe) { $pipe.Dispose() }
                    if ($sr) { $sr.Dispose() }
                }
            }
        }
    
        $src = [System.Threading.CancellationTokenSource]::new()
        $ps = [powershell]::Create().AddScript($script)
        $ps.Runspace.SessionStateProxy.SetVariable('Name', $Name)
        $ps.Runspace.SessionStateProxy.SetVariable('token', $src.Token)
    
        [pscustomobject]@{
            TokenSource = $src
            Instance    = $ps
            Async       = $ps.BeginInvoke()
        }
    }
    
    function Write-PipeMessage {
        [Alias('pipemsg')]
        param(
            [Parameter(Mandatory)]
            [string] $Name,
    
            [Parameter(ValueFromPipeline, ValueFromRemainingArguments)]
            [string[]] $Message
        )
    
        begin {
            try {
                $pipe = [System.IO.Pipes.NamedPipeClientStream]::new($Name)
                $task = $pipe.ConnectAsync()
                while (-not $task.Wait(200)) { }
                $sw = [System.IO.StreamWriter]::new($pipe)
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
        process {
            foreach ($msg in $Message) {
                $sw.WriteLine($msg)
            }
        }
        end {
            $sw.Dispose()
            $pipe.Dispose()
        }
    }
    
    function Remove-NamedPipeServer {
        param([Parameter(Mandatory, ValueFromPipeline)] $Server)
    
        process {
            try {
                $Server.TokenSource.Cancel()
                $Server.TokenSource.Dispose()
                $Server.Instance.EndInvoke($Server.Async)
                if ($Server.Instance.HadErrors) {
                    foreach ($err in $Server.Instance.Streams.Error) {
                        $PSCmdlet.WriteError($err)
                    }
                }
                $Server.Instance.Dispose()
            }
            catch {
                $PSCmdlet.WriteError($_)
            }
        }
    }