powershellasynchronousnetwork-programmingconsole.readline

Powershell Peer to Peer chat in a console window


I am working on a peer to peer messaging app in powershell. Unfortunately I have to use powershell since my organization does not give me access to any better options.

I've set up the connection and the two clients can communicate over the network but my issue now is I can't take in data from the console without pausing execution for a response, which is of course not sufficient here.

Here is my sending and receiving functions. Pretty typical stuff (except I'm not the best with scripting/programming since it's not exactly my trade)


Function Send-MessageToClient(){

    param (

    [Parameter(Mandatory=$true)]$client,
    [switch]$PromptUser,
    [string]$TextToSend

    )

    if ([bool]$TextToSend -and $PromptUser) { Write-Error -Message "TextToSend and PromptUser cannot be used in the same function call." -ErrorId 1 -Category InvalidArgument ; return }
    if ((-not $PromptUser) -and (-not $TextToSend)) { Write-Error -Message "You must use either PromptUser or TextToSend to call this function." -ErrorId 2 -Category InvalidArgument ; return }

    if ($PromptUser) {
    
    $TextToSend = Read-Host "$env:USERNAME@$env:COMPUTERNAME"
    if ($TextToSend.Length -gt 1020) { Write-Host "Max Character limit exceeded." -ForegroundColor Red ; Continue }

    }

    [System.Byte[]]$bytes = [System.Text.Encoding]::ASCII.GetBytes($TextToSend)

    $null = $client.Client.Send($bytes)

}

Function Receive-MessageFromClient(){

    param (

    [Parameter(Mandatory=$true)]$client

    )

    $stream = $client.GetStream()
    $buffer = New-Object System.Byte[] 1024
    $i = $stream.Read($buffer, 0, $buffer.Length)

    if ($i -ne 0) { 

        $EncodedText = New-Object System.Text.ASCIIEncoding
        $data = $EncodedText.GetString($buffer, 0, $i)

        return "$data"
    }

}

A possible workaround I considered was using a windows form instead of the console, which I am very comfortable with and from my experience it seems to be able to take in user data asynchronously. Thank you!


Solution

  • In order to implement a non-blocking open-ended text prompt that allows you to continue to perform operations in parallel, you can use a thread job as follows, but note that the concurrent foreground activity must not interfere with the console's (terminal's) state. The following self-contained sample code demonstrates this approach:

    Write-Host -NoNewline "Enter a line of text: "
    $job = Start-ThreadJob { [Console]::ReadLine() }
    $iterationCount = 0
    while ($job.State -ne 'Completed') { # Wait for the user to finish submitting input.
      # Here you can perform any operation that doesn't interfere
      # with the console's (terminal's) state.
      ++$iterationCount
      Start-Sleep -Milliseconds 100 # Sleep a little to avoid a tight loop.
    }
    $enteredText = $job | Receive-Job -Wait -AutoRemoveJob
    "You entered: [$enteredText]
    The foreground loop was processed $iterationCount time(s)."
    

    Note:


    Alternatively, if:

    and/or

    Implement a polling loop based on [Console]::KeyAvailable in order to emulate the functionality of Read-Host / [Console]::ReadLine() and manage updating the different parts of the console window via other members of the [Console] .NET API, as the following self-contained sample demonstrates:

    $textEntered = ''
    Write-Host -NoNewline 'Enter a line of text: '
    while ($true) {
      if ([Console]::KeyAvailable) {
        $key = [Console]::ReadKey($true)
        if ($key.Key -eq 'Esc') { $textEntered = $null; break } 
        elseif ($key.Key -eq 'Enter') { $textEntered = $textEntered.Trim(); break }
        elseif ($key.Key -eq 'Backspace') { if ($textEntered) { $textEntered = $textEntered.Substring(0, $textEntered.Length - 1); Write-Host -NoNewline "`b `b" } }
        elseif ($key.KeyChar) { 
          Write-Host -NoNewline $key.KeyChar
          $textEntered += $key.KeyChar
        }
      }
      # Perform other, short-lived tasks here.
      # In this example, print the current time of day at the end of the line.
      $x, $y = [Console]::CursorLeft, [Console]::CursorTop
      [Console]::CursorVisible = $false
      [Console]::SetCursorPosition([Console]::WindowWidth - 9, $y)
      Write-Host -NoNewline ([datetime]::Now.Tostring('HH:mm:ss'))
      [Console]::SetCursorPosition($x, $y)
      [Console]::CursorVisible = $true
      # Sleep a little to avoid polling too frequently.
      Start-Sleep -Milliseconds 50
    }
    
    if ($null -eq $textEntered) {
      Write-Host "`nYou pressed Esc to cancel."
    }
    else {
      Write-Host "`nYou entered: [$textEntered]"
    }
    

    Note: