SignalR
Add real-time, bidirectional communication to your server for live events, progress, and notifications using SignalR.
Full source
File: pwsh/tutorial/examples/10.5-SignalR.ps1
<#
Create a SignalR demo server with Kestrun in PowerShell.
FileName: 10.5-SignalR.ps1
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# Initialize Kestrun root directory
Initialize-KrRoot -Path $PSScriptRoot
## 1. Logging
New-KrLogger | Add-KrSinkConsole | Register-KrLogger -Name 'SignalRDemo' -SetAsDefault
## 2. Create Server
New-KrServer -Name 'Kestrun SignalR Demo'
## 3. Configure Listener
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
## 4. Add SignalR with KestrunHub
Add-KrSignalRHubMiddleware -Path '/hubs/kestrun'
## 5. Enable Scheduler (must be added before configuration)
Add-KrScheduling
# Register the ad-hoc Tasks feature (PowerShell, C#, VB.NET)
Add-KrTasksService
## 6. Enable Configuration
Enable-KrConfiguration
## 7. Add Routes
# Home page with SignalR client
Add-KrHtmlTemplateRoute -Pattern '/' -HtmlTemplatePath 'Assets/wwwroot/signal-r.html'
# Route to broadcast logs via PowerShell
Add-KrMapRoute -Verbs Get -Pattern '/api/ps/log/{level}' {
$level = Get-KrRequestRouteParam -Name 'level'
Write-KrLog -Level $level -Message "Test $level message from PowerShell at $(Get-Date -Format 'HH:mm:ss')"
Send-KrSignalRLog -Level $level -Message "Test $level message from PowerShell at $(Get-Date -Format 'HH:mm:ss')"
Write-KrTextResponse -InputObject "Broadcasted $level log message from PowerShell" -StatusCode 200
}
# Route to broadcast custom events via PowerShell
Add-KrMapRoute -Verbs Get -Pattern '/api/ps/event' {
Send-KrSignalREvent -EventName 'PowerShellEvent' -Data @{
Message = 'Hello from PowerShell'
Timestamp = (Get-Date)
Random = Get-Random -Minimum 1 -Maximum 100
}
Write-KrTextResponse -InputObject 'Broadcasted custom event from PowerShell' -StatusCode 200
}
# Route to join a SignalR group
Add-KrMapRoute -Verbs Post -Pattern '/api/group/join/{groupName}' {
$groupName = Get-KrRequestRouteParam -Name 'groupName'
# This would normally be handled by the hub itself, but we can broadcast a notification
Send-KrSignalREvent -EventName 'GroupJoinRequest' -Data @{
GroupName = $groupName
Message = "Request to join group: $groupName"
Timestamp = (Get-Date)
}
Write-KrJsonResponse -InputObject @{
Success = $true
Message = "Join request sent for group: $groupName"
GroupName = $groupName
} -StatusCode 200
}
# Route to leave a SignalR group
Add-KrMapRoute -Verbs Post -Pattern '/api/group/leave/{groupName}' {
$groupName = Get-KrRequestRouteParam -Name 'groupName'
# This would normally be handled by the hub itself, but we can broadcast a notification
Send-KrSignalREvent -EventName 'GroupLeaveRequest' -Data @{
GroupName = $groupName
Message = "Request to leave group: $groupName"
Timestamp = (Get-Date)
}
Write-KrJsonResponse -InputObject @{
Success = $true
Message = "Leave request sent for group: $groupName"
GroupName = $groupName
} -StatusCode 200
}
# Route to broadcast to a specific group
Add-KrMapRoute -Verbs Post -Pattern '/api/group/broadcast/{groupName}' {
$groupName = Get-KrRequestRouteParam -Name 'groupName'
Send-KrSignalRGroupMessage -GroupName $groupName -Method 'ReceiveGroupMessage' -Message @{
Message = "Hello from PowerShell to group: $groupName"
Timestamp = (Get-Date)
Sender = 'PowerShell Route'
}
Write-KrJsonResponse -InputObject @{
Success = $true
Message = "Broadcasted message to group: $groupName"
GroupName = $groupName
} -StatusCode 200
}
# Route to start a long-running operation with progress updates
Add-KrMapRoute -Verbs Post -Pattern '/api/operation/start' {
$seconds = Get-KrRequestQuery -Name 'seconds' -AsInt
if ($seconds -le 0) { $seconds = 2 }
Write-KrLog -Level Information -Message $message
$id = New-KrTask -ScriptBlock {
for ($i = 1; $i -le $seconds; $i++) {
Start-Sleep -Milliseconds 1000
$TaskProgress.StatusMessage = "Sleeping ($i/$seconds)"
$TaskProgress.PercentComplete = ($i * 100/$seconds)
Write-KrLog -Level Information -Message 'Operation {TaskId} progress: {i}/{seconds} {PercentComplete}%' -Values $TaskId, $i,$seconds,$TaskProgress.PercentComplete
$message = @{
TaskId = $TaskId
Progress = $TaskProgress.PercentComplete
Step = $i
Message = "Processing step $i of $seconds..."
Timestamp = (Get-Date)
}
# Broadcast progress to all clients using the generic event channel the HTML listens to (ReceiveEvent)
try {
Send-KrSignalREvent -EventName 'OperationProgress' -Data $message
# Alternatively, to use the dedicated handler: Send-KrSignalRGroupMessage -GroupName 'OperationProgress' -Method 'ReceiveOperationProgress' -Message $message
} catch {
Write-Warning "Failed to broadcast progress: $_"
}
}
# Send completion message
$completionMessage = @{
TaskId = $TaskId
Progress = $TaskProgress.PercentComplete
Message = $TaskProgress.StatusMessage
Timestamp = (Get-Date)
}
try {
# Broadcast completion on the same event channel the HTML expects
Send-KrSignalREvent -EventName 'OperationComplete' -Data $completionMessage
# Alternatively, for the dedicated handler: Send-KrSignalRGroupMessage -GroupName 'OperationProgress' -Method 'ReceiveOperationProgress' -Message $completionMessage
} catch {
Write-Warning "Failed to broadcast completion: $_"
}
} -Arguments @{ seconds = $seconds } -AutoStart
$message = "Starting long operation with ID: $id"
Set-KrTaskName -Id $id -Name "LongOperation-$id" -Description $message
Write-KrLog -Level Information -Message 'Long operation started: {id}' -Values $id
Write-KrLog -Level Information -Message 'Long operation task created: {id}' -Values $id
Write-KrJsonResponse -InputObject @{
Success = $true
TaskId = $id
Message = $message
} -StatusCode 200
}
# Route to get operation status
Add-KrMapRoute -Verbs Get -Pattern '/api/operation/status/{taskId}' {
$taskId = Get-KrRequestRouteParam -Name 'taskId'
Write-KrLog -Level Information -Message 'Status requested for operation: {taskId}' -Values $taskId
$task = Get-KrTask -Id $taskId
if ( $null -ne $task ) {
Write-KrLog -Level Information -Message 'Found task for operation {taskId} with status {Status}' -Values $taskId, $task.Status
} else {
Write-KrLog -Level Warning -Message 'No task found for operation {taskId}' -Values $taskId
Write-KrJsonResponse -InputObject @{
taskId = $taskId
Message = "No task found for operation '$taskId'"
} -StatusCode 404
return
}
$progress = $null
$status = $null
if ($task.Progress) {
$progress = $task.Progress.PercentComplete
$status = $task.Progress.StatusMessage
}
Write-KrJsonResponse -InputObject @{
OperationId = $operationId
TaskId = $taskId
State = $task.StateText
StartedAt = $task.StartedAt
CompletedAt = $task.CompletedAt
Progress = $progress
Status = $status
Message = 'Operation status retrieved'
} -StatusCode 200
}
# Register a scheduled task that broadcasts to all clients every 30 seconds
Register-KrSchedule -Name 'HeartbeatBroadcast' -Cron '*/30 * * * * *' -ScriptBlock {
$heartbeatMessage = @{
Type = 'Heartbeat'
ServerTime = (Get-Date)
Uptime = Get-KrServer -Uptime
ConnectedClients = Get-KrSignalRConnectedClient
Message = 'Server heartbeat from scheduled task'
}
Write-KrLog -Level Information -Message 'Broadcasting heartbeat:{heartbeatMessage}' -Values $heartbeatMessage
# Broadcast heartbeat to all connected clients
Send-KrSignalREvent -EventName 'ServerHeartbeat' -Data $heartbeatMessage
}
# Register a scheduled task that broadcasts to the "Admins" group every minute
Register-KrSchedule -Name 'AdminStatusUpdate' -Cron '0 * * * * *' -ScriptBlock {
$statusMessage = @{
Type = 'AdminUpdate'
SystemInfo = @{
ProcessorCount = $env:NUMBER_OF_PROCESSORS
MachineName = $env:COMPUTERNAME
UserName = $env:USERNAME
}
Timestamp = (Get-Date)
Message = 'Scheduled admin status update'
}
Write-KrLog -Level Information -Message 'Broadcasting admin status update :{statusMessage}' -Values $statusMessage
# Broadcast to admin group only
Send-KrSignalRGroupMessage -GroupName 'Admins' -Method 'ReceiveAdminUpdate' -Message $statusMessage
}
## 8. Start Server
Write-Host '🟢 Kestrun SignalR Demo Server Started' -ForegroundColor Green
Write-Host '📍 Navigate to http://localhost:5000 to see the demo' -ForegroundColor Cyan
Write-Host '🔌 SignalR Hub available at: http://localhost:5000/hubs/kestrun' -ForegroundColor Cyan
Start-KrServer -CloseLogsOnExit
Step-by-step
- Logging: Register a console logger as the default.
- Server: Create a server named ‘Kestrun SignalR Demo’.
- Listener: Listen on 127.0.0.1:5000.
- SignalR: Add the SignalR hub middleware at
/hubs/kestrun. - Services: Add the Scheduler and ad-hoc Tasks service for background work.
- Configuration: Enable configuration to materialize middleware and services.
- Routes: Map routes to broadcast logs/events, join/leave/broadcast groups, start a long operation, and poll operation status; serve an HTML demo at
/. - Run: Start the server and connect a client to the hub at
/hubs/kestrun.
Try it
Start the sample script (runs until stopped):
pwsh .\docs\_includes\examples\pwsh\10.5-SignalR.ps1
Broadcast a log and a custom event:
# Broadcast an Information log
curl -i http://127.0.0.1:5000/api/ps/log/Information
# Broadcast a custom event
curl -i http://127.0.0.1:5000/api/ps/event
Group operations and group broadcast:
# Request join/leave of a group (demo events)
curl -i -X POST http://127.0.0.1:5000/api/group/join/Admins
curl -i -X POST http://127.0.0.1:5000/api/group/leave/Admins
# Broadcast a message to a group
curl -i -X POST http://127.0.0.1:5000/api/group/broadcast/Admins
Long operation with progress and completion:
# Start a 4-second operation
$start = Invoke-RestMethod -Uri 'http://127.0.0.1:5000/api/operation/start?seconds=4' -Method Post
$taskId = $start.TaskId
# Poll status until completed
do {
Start-Sleep -Milliseconds 500
$status = Invoke-RestMethod -Uri "http://127.0.0.1:5000/api/operation/status/$taskId"
"Progress=$($status.Progress)% State=$($status.State) Status=$($status.Status)"
} while ($status.State -notin @('Completed','Faulted','Canceled'))
Open the HTML client demo to see live updates:
Start-Process http://127.0.0.1:5000/
Key Points
- The default hub path in this sample is
/hubs/kestrun. - Use
Send-KrSignalRLogandSend-KrSignalREventto broadcast to all clients; useSend-KrSignalRGroupMessageto target groups. - For long operations, pair
New-KrTaskwith periodicSend-KrSignalREventmessages likeOperationProgressandOperationComplete. - The HTML client handles both TaskId-first IDs and robust parsing of non-JSON responses.
References
- Initialize-KrRoot
- New-KrLogger
- Add-KrSinkConsole
- Register-KrLogger
- New-KrServer
- Add-KrEndpoint
- Add-KrSignalRHubMiddleware
- Add-KrScheduling
- Add-KrTasksService
- Enable-KrConfiguration
- Add-KrHtmlTemplateRoute
- Add-KrMapRoute
- Get-KrRequestRouteParam
- Get-KrRequestQuery
- Write-KrTextResponse
- Write-KrJsonResponse
- Write-KrLog
- Send-KrSignalRLog
- Send-KrSignalREvent
- Send-KrSignalRGroupMessage
- New-KrTask
- Set-KrTaskName
- Get-KrTask
- Register-KrSchedule
- Start-KrServer
- SignalR Topic
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Hub fails to connect | Wrong path | Use /hubs/kestrun in clients to match the sample |
| No messages received | Client not subscribed | Ensure handlers are registered for ReceiveLog and ReceiveEvent |
| Progress never reaches 100% | Seconds not an integer | Pass seconds as an integer; sample scales percent by i * 100 / seconds |
| 404 on status endpoint | Wrong TaskId | Use the TaskId returned by /api/operation/start |
Previous / Next
Previous: HTTPS and HSTS Security Next: None