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!
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:
The Start-ThreadJob
cmdlet offers a lightweight, much faster thread-based alternative to the child-process-based regular background jobs created with Start-Job
.
It ships with PowerShell (Core) 7; in Windows PowerShell, it can be installed on demand, using, e.g., Install-Module ThreadJob -Scope CurrentUser
.
The user pressing Ctrl+C aborts the entire script, not just the background interactive prompt; handling this would require extra effort.
Alternatively, if:
ThreadJob
module that contains Start-ThreadJob
is not an optionand/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:
The above demonstrates updating a different part of the console (terminal) window by showing the time of day on the right edge of the current line.
Frequently toggling the [Console]::CursorVisible
property results in noticeable flickering of the cursor.