We are looking for a way to communicate from our event hub-connected agents to an app. I’m aware that we can use Send-PSUEvent to run scripts on those machines, but that method is blocking and will not return unless the script itself returns (see GH#4354 for more information) from the agent, which works fine for small jobs but for our particular implementation isn’t going to cut it.
We have designed a short event listener that we are looking to instantiate in the agents whose action can tee the app to update in a particular way. I’ve had success using System.Net.Sockets.TcpListener within a New-UDDynamic region that we can trigger with an API to update an element, but as soon as the page is refreshed the TCP listener tries to open itself back up but the port is already consumed by the previous socket. So, how can I use this event listener on the agents to trigger immediate (broadcasted ideally) updates to the UI?
Update: The API → [TcpListener] method is the most efficient i can find as of right now. @adam is there a way I can start the TcpListener underneath the app as a sort of management layer shared by all the sessions of the app? Would a PSU cache variable be able to hold the object in this way?
If you’re looking to engage with the platform in this way I’d highly recommend a solid understanding of the TcpClient/TcpListener .NET objects. It is possible, though it requires a lot of moving parts. Currently, we have our event hub agents configured with an event listener that PUTs to a custom API that calls a TcpClient.Connect() to our app which is running a TcpListener in a New-UDDynamic region saved into a PSU cache variable. If the variable is not set, it recreates the connection. Highly recommend using a database for a backup in the event the TcpListener goes down for an unforeseen reason.
This is fascinating as I am facing a similar challenge for our IT folks who I need to have apps launched from the App on their local machines and talk back to the App. I will be bookmarking this as I get closer to having to work on it. I would be seriously curious to look at a bare boned version of this if you ever felt like sharing.
Thanks for posting your overall solution to the issue though! Much appreciated.
# URL - "/api/v1/updateApp" accepts PUT
switch ( $method) {
"PUT" {
# Your request body will contain the things you want to send
# For this example, we will assume it is just to update a text box on the app
$Address = "127.0.0.1" # Localhost - relative to the individual server
$Port = 8988 # Obviously up to you, 8988 should be free
# Create a new TcpClient
$Client = [System.Net.Sockets.TcpClient]::new()
try {
$Client.Connect($Address, $Port)
$Stream = $Client.GetStream()
$MessageBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) # the PUT data
$Stream.Write($MessageBytes, 0, $MessageBytes.Length)
} catch {
New-PSUApiResponse -StatusCode 500 -Body "An error occurred when attempting to connect to the specified TCP server."
} finally {
$Client.Close()
$Client.Dispose()
}
continue;
}
}
App
New-UDElement -Tag "div" -Id "responseTarget" -Content {
"Send a PUT to ""/api/v1/updateApp"" to update this app!"
}
New-UDDynamic -Id "tcpListener" -Content {
try {
# Build listener on port 8988
# Hold the TcpListener in the cache - this prevents duplicate listeners
# Feel free to change this part into ternary - this method is PS5 compatible
if ($Cache:TcpListener) { $TcpListener = $Cache:TcpListener } else { $Cache:TcpListener = [System.Net.Sockets.TcpListener]::new("127.0.0.1", 8988); $TcpListener = $Cache:TcpListener }
Write-Verbose "Starting server"
# Start the listening server
$TcpListener.Start()
Write-Verbose "Server started"
# Buffer for reading data
$Bytes = [Byte[]]::new(2048)
$ReceivedData = $null
# Listening loop - blocking synchronous
while (1) {
Show-UDToast "Waiting for connection..." -Duration 5000
[System.Net.Sockets.TcpClient]$Client = $TcpListener.AcceptTcpClient()
Show-UDToast "Connected!" -Duration 5000
$ReceivedData = $null
# Get the stream object for processing
$Stream = $Client.GetStream()
$i = 0
# Start receiving bytes
while (($i = $Stream.Read($Bytes, 0, $Bytes.Length)) -ne 0) {
# Translate received bytes to UTF8 encoding
$ReceivedData = [System.Text.Encoding]::UTF8.GetString($Bytes, 0, $i)
Write-Information "Received bytes: $Bytes"
Show-UDToast ("Received: {0}" -f $ReceivedData) -Duration 5000
# Data handling - for now, just update the target element
Set-UDElement -Id "responseTarget" -Content { $ReceivedData } -Broadcast
# Optional: Write data back to the host
# $Stream.Write($Response, 0, $Response.Length)
# Write-Information ("Sent: {0}" -f $Data)
}
}
}
catch {
Write-Error "SocketException: $_"
}
finally {
$TcpListener.Stop()
$TcpListener.Dispose()
$Cache:TcpListener = $null
# Sync-UDElement -Id "tcpListener"
}
}
From whatever you wish, you can send an API to your configured URL (authenticate it!) and it will pass the data over via the socket connection and update the app as soon as that data is received.
Let me know if you have any questions about the specific application here - I commented lightly but I can provide more info to help if needed.