diff --git a/.github/workflows/BuildWebSocket.yml b/.github/workflows/BuildWebSocket.yml
index 2f67fd1..dd87ef2 100644
--- a/.github/workflows/BuildWebSocket.yml
+++ b/.github/workflows/BuildWebSocket.yml
@@ -103,7 +103,7 @@ jobs:
}
} @Parameters
- name: PublishTestResults
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@main
with:
name: PesterResults
path: '**.TestResults.xml'
diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1
new file mode 100644
index 0000000..e8111e8
--- /dev/null
+++ b/Build/GitHub/Steps/PublishTestResults.psd1
@@ -0,0 +1,10 @@
+@{
+ name = 'PublishTestResults'
+ uses = 'actions/upload-artifact@main'
+ with = @{
+ name = 'PesterResults'
+ path = '**.TestResults.xml'
+ }
+ if = '${{always()}}'
+}
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0eacffa..5403c61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,41 @@
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
+## WebSocket 0.1.3
+
+WebSocket server support!
+
+### Server Features
+
+For consistency, capabilities, and aesthetics,
+this is a fairly fully features HTTP server that happens to support websockets
+
+* `Get-WebSocket` `-RootURL/-HostHeader` ( #47 )
+* `-StyleSheet` lets you include stylesheets ( #64 )
+* `-JavaScript` lets you include javascript ( #65 )
+* `-Timeout/-LifeSpan` server support ( #85 )
+
+### Client Improvements
+
+* `Get-WebSocket -QueryParameter` lets you specify query parameters ( #41 )
+* `Get-WebSocket -Debug` lets you debug the websocketjob. ( #45 )
+* `Get-WebSocket -SubProtocol` lets you specify a subprotocol (defaults to JSON) ( #46 )
+* `Get-WebSocket -Authenticate` allows sends pre-connection authentication ( #69 )
+* `Get-WebSocket -Handshake` allows post-connection authentication ( #81 )
+* `Get-WebSocket -Force` allows the creation of multiple clients ( #58 )
+
+
+### General Improvements
+
+* `Get-WebSocket -SupportsPaging` ( #55 )
+* `Get-WebSocket -BufferSize 64kb` ( #52 )
+* `Get-WebSocket -Force` ( #58 )
+* `Get-WebSocket -Filter` ( #42 )
+* `Get-WebSocket -ForwardEvent` ( #56 )
+* `Get-WebSocket` Parameter Sets ( #73, #74 )
+* `-Broadcast` lets you broadcast to clients and servers ( #39 )
+* `Get-WebSocket` quieting previous job check ( #43 )
+
## WebSocket 0.1.2
* WebSocket now decorates (#34)
diff --git a/Commands/Get-WebSocket.ps1 b/Commands/Get-WebSocket.ps1
index a2635c8..8216fd9 100644
--- a/Commands/Get-WebSocket.ps1
+++ b/Commands/Get-WebSocket.ps1
@@ -8,17 +8,26 @@ function Get-WebSocket {
This will create a job that connects to a WebSocket and outputs the results.
If the `-Watch` parameter is provided, will output a continous stream of objects.
+ .LINK
+ https://websocket.powershellweb.com/Get-WebSocket/
+ .LINK
+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket?wt.mc_id=MVP_321542
+ .LINK
+ https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?wt.mc_id=MVP_321542
.EXAMPLE
# Create a WebSocket job that connects to a WebSocket and outputs the results.
- Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+ $socketServer = Get-WebSocket -RootUrl "http://localhost:8387/" -HTML "
WebSocket Server
"
+ $socketClient = Get-WebSocket -SocketUrl "ws://localhost:8387/"
+ foreach ($n in 1..10) { $socketServer.Send(@{n=Get-Random}) }
+ $socketClient | Receive-Job -Keep
.EXAMPLE
# Get is the default verb, so we can just say WebSocket.
# `-Watch` will output a continous stream of objects from the websocket.
- # For example, let's Watch BlueSky, but just the text.
- websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ # For example, let's Watch BlueSky, but just the text
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch -Maximum 1kb |
% {
$_.commit.record.text
- }
+ }
.EXAMPLE
# Watch BlueSky, but just the text and spacing
$blueSkySocketUrl = "wss://jetstream2.us-$(
@@ -27,12 +36,19 @@ function Get-WebSocket {
"wantedCollections=app.bsky.feed.post"
) -join '&')"
websocket $blueSkySocketUrl -Watch |
- % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
- .EXAMPLE
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"} -Max 1kb
+ .EXAMPLE
+ # Watch continuously in a background job.
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
+ .EXAMPLE
+ # Watch the first message in -Debug mode.
+ # This allows you to literally debug the WebSocket messages as they are encountered.
+ websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+ } -Max 1 -Debug
.EXAMPLE
# Watch BlueSky, but just the emoji
- websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail -Max 1kb |
Foreach-Object {
$in = $_
if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
@@ -60,11 +76,13 @@ function Get-WebSocket {
}
.EXAMPLE
# BlueSky, but just the hashtags
- websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+ } -WatchFor @{
{$webSocketoutput.commit.record.text -match "\#\w+"}={
$matches.0
}
- }
+ } -Maximum 1kb
.EXAMPLE
# BlueSky, but just the hashtags (as links)
websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
@@ -104,24 +122,112 @@ function Get-WebSocket {
Sort Count -Descending |
Select -First 10
#>
- [CmdletBinding(PositionalBinding=$false)]
- [Alias('WebSocket')]
+ [CmdletBinding(
+ PositionalBinding=$false,
+ SupportsPaging,
+ DefaultParameterSetName='WebSocketClient'
+ )]
+ [Alias('WebSocket','ws','wss')]
param(
- # The Uri of the WebSocket to connect to.
- [Parameter(Position=0,ValueFromPipelineByPropertyName)]
- [Alias('Url','Uri')]
- [uri]$WebSocketUri,
+ # The WebSocket Uri.
+ [Parameter(Position=0,ParameterSetName='WebSocketClient',ValueFromPipelineByPropertyName)]
+ [Alias('Url','Uri','WebSocketUri','WebSocketUrl')]
+ [uri]
+ $SocketUrl,
+
+ # One or more root urls.
+ # If these are provided, a WebSocket server will be created with these listener prefixes.
+ [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('HostHeader','Host','CNAME','ListenerPrefix','ListenerPrefixes','ListenerUrl')]
+ [string[]]
+ $RootUrl,
+
+ # A route table for all requests.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('Routes','RouteTable','WebHook','WebHooks')]
+ [Collections.IDictionary]
+ $Route,
+
+ # The Default HTML.
+ # This will be displayed when visiting the root url.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('DefaultHTML','Home','Index','IndexHTML','DefaultPage')]
+ [string]
+ $HTML,
+
+ # The name of the palette to use. This will include the [4bitcss](https://4bitcss.com) stylesheet.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('Palette','ColorScheme','ColorPalette')]
+ [ArgumentCompleter({
+ param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters )
+ if (-not $script:4bitcssPaletteList) {
+ $script:4bitcssPaletteList = Invoke-RestMethod -Uri https://cdn.jsdelivr.net/gh/2bitdesigns/4bitcss@latest/docs/Palette-List.json
+ }
+ if ($wordToComplete) {
+ $script:4bitcssPaletteList -match "$([Regex]::Escape($wordToComplete) -replace '\\\*', '.{0,}')"
+ } else {
+ $script:4bitcssPaletteList
+ }
+ })]
+ [string]
+ $PaletteName,
- # A ScriptBlock that will handle the output of the WebSocket.
+ # The [Google Font](https://fonts.google.com/) name.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('FontName')]
+ [string]
+ $GoogleFont,
+
+ # The Google Font name to use for code blocks.
+ # (this should be a [monospace font](https://fonts.google.com/?classification=Monospace))
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('PreFont','CodeFontName','PreFontName')]
+ [string]
+ $CodeFont,
+
+ # A list of javascript files or urls to include in the content.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [string[]]
+ $JavaScript,
+
+ # A javascript import map. This allows you to import javascript modules.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketServer')]
+ [Alias('ImportsJavaScript','JavaScriptImports','JavaScriptImportMap')]
+ [Collections.IDictionary]
+ $ImportMap,
+
+ # A collection of query parameters.
+ # These will be appended onto the `-SocketUrl`.
+ # Multiple values for a single parameter will be passed as multiple parameters.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
+ [Alias('QueryParameters','Query')]
+ [Collections.IDictionary]
+ $QueryParameter,
+
+ # A ScriptBlock that can handle the output of the WebSocket or the Http Request.
+ # This may be run in a separate `-Runspace` or `-RunspacePool`.
+ # The output of the WebSocket or the Context will be passed as an object.
[ScriptBlock]
$Handler,
+ # If set, will forward websocket messages as events.
+ # Only events that match -Filter will be forwarded.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
+ [Alias('Forward')]
+ [switch]
+ $ForwardEvent,
+
# Any variables to declare in the WebSocket job.
# These variables will also be added to the job as properties.
- [Collections.IDictionary]
+ [Collections.IDictionary]
$Variable = @{},
- # The name of the WebSocket job.
+ # Any Http Headers to include in the WebSocket request or server response.
+ [Collections.IDictionary]
+ [Alias('Headers')]
+ $Header,
+
+ # The name of the WebSocket job.
[string]
$Name,
@@ -130,51 +236,109 @@ function Get-WebSocket {
$InitializationScript = {},
# The buffer size. Defaults to 16kb.
+ [Parameter(ValueFromPipelineByPropertyName)]
[int]
- $BufferSize = 16kb,
+ $BufferSize = 64kb,
+
+ # If provided, will send an object.
+ # If this is a scriptblock, it will be run and the output will be sent.
+ [Alias('Send')]
+ [PSObject]
+ $Broadcast,
# The ScriptBlock to run after connection to a websocket.
# This can be useful for making any initial requests.
+ [Parameter(ParameterSetName='WebSocketClient')]
[ScriptBlock]
$OnConnect,
# The ScriptBlock to run when an error occurs.
+ [Parameter(ParameterSetName='WebSocketClient')]
[ScriptBlock]
$OnError,
# The ScriptBlock to run when the WebSocket job outputs an object.
+ [Parameter(ParameterSetName='WebSocketClient')]
[ScriptBlock]
$OnOutput,
# The Scriptblock to run when the WebSocket job produces a warning.
+ [Parameter(ParameterSetName='WebSocketClient')]
[ScriptBlock]
$OnWarning,
+ # If provided, will authenticate the WebSocket.
+ # Many websockets require an initial authentication handshake
+ # after an initial message is received.
+ # This parameter can be either a ScriptBlock or any other object.
+ # If it is a ScriptBlock, it will be run with the output of the WebSocket passed as the first argument.
+ # This will run after the socket is connected but before any messages are received.
+ [Parameter(ParameterSetName='WebSocketClient')]
+ [Alias('Authorize','HelloMessage')]
+ [PSObject]
+ $Authenticate,
+
+ # If provided, will shake hands after the first websocket message is received.
+ # This parameter can be either a ScriptBlock or any other object.
+ # If it is a ScriptBlock, it will be run with the output of the WebSocket passed as the first argument.
+ # This will run after the socket is connected and the first message is received.
+ [Parameter(ParameterSetName='WebSocketClient')]
+ [Alias('Identify')]
+ [PSObject]
+ $Handshake,
+
# If set, will watch the output of the WebSocket job, outputting results continuously instead of outputting a websocket job.
+ [Parameter(ParameterSetName='WebSocketClient')]
[Alias('Tail')]
[switch]
$Watch,
# If set, will output the raw text that comes out of the WebSocket.
+ [Parameter(ParameterSetName='WebSocketClient')]
[Alias('Raw')]
[switch]
$RawText,
# If set, will output the raw bytes that come out of the WebSocket.
+ [Parameter(ParameterSetName='WebSocketClient')]
[Alias('RawByte','RawBytes','Bytes','Byte')]
[switch]
- $Binary,
+ $Binary,
+
+ # If set, will force a new job to be created, rather than reusing an existing job.
+ [switch]
+ $Force,
+
+ # The subprotocol used by the websocket. If not provided, this will default to `json`.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
+ [string]
+ $SubProtocol,
+
+ # If set, will not set a subprotocol. This will only work with certain websocket servers, but will not work with an HTTP Listener WebSocket.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
+ [switch]
+ $NoSubProtocol,
+
+ # One or more filters to apply to the output of the WebSocket.
+ # These can be strings, regexes, scriptblocks, or commands.
+ # If they are strings or regexes, they will be applied to the raw text.
+ # If they are scriptblocks, they will be applied to the deserialized JSON.
+ # These filters will be run within the WebSocket job.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
+ [PSObject[]]
+ $Filter,
# If set, will watch the output of a WebSocket job for one or more conditions.
# The conditions are the keys of the dictionary, and can be a regex, a string, or a scriptblock.
# The values of the dictionary are what will happen when a match is found.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
[ValidateScript({
$keys = $_.Keys
$values = $_.values
foreach ($key in $keys) {
if ($key -isnot [scriptblock]) {
- throw "Keys '$key' must be a scriptblock"
- }
+ throw "Key '$key' must be a scriptblock"
+ }
}
foreach ($value in $values) {
if ($value -isnot [scriptblock] -and $value -isnot [string]) {
@@ -187,145 +351,935 @@ function Get-WebSocket {
[Collections.IDictionary]
$WatchFor,
- # The timeout for the WebSocket connection. If this is provided, after the timeout elapsed, the WebSocket will be closed.
+ # The timeout for the WebSocket connection.
+ # If this is provided, after the timeout elapsed, the WebSocket will be closed.
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias('Lifespan')]
[TimeSpan]
$TimeOut,
# If provided, will decorate the objects outputted from a websocket job.
# This will only decorate objects converted from JSON.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
[Alias('PSTypeNames','Decorate','Decoration')]
[string[]]
$PSTypeName,
# The maximum number of messages to receive before closing the WebSocket.
+ [Parameter(ValueFromPipelineByPropertyName)]
[long]
$Maximum,
+ # The throttle limit used when creating background jobs.
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [int]
+ $ThrottleLimit = 64,
+
# The maximum time to wait for a connection to be established.
- # By default, this is 7 seconds.
+ # By default, this is 7 seconds.
+ [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WebSocketClient')]
[TimeSpan]
$ConnectionTimeout = '00:00:07',
# The Runspace where the handler should run.
# Runspaces allow you to limit the scope of the handler.
+ [Parameter(ValueFromPipelineByPropertyName)]
[Runspace]
$Runspace,
# The RunspacePool where the handler should run.
# RunspacePools allow you to limit the scope of the handler to a pool of runspaces.
+ [Parameter(ValueFromPipelineByPropertyName)]
[Management.Automation.Runspaces.RunspacePool]
[Alias('Pool')]
$RunspacePool
)
begin {
- $SocketJob = {
- param([Collections.IDictionary]$Variable)
+ $SocketClientJob = {
+ param(
+ # By accepting a single parameter containing variables,
+ # we can avoid the need to pass in a large number of parameters.
+ # we can also modify this dictionary, to provide a way to pass information back.
+ [Collections.IDictionary]$Variable
+ )
+ $Variable.JobRunspace = [Runspace]::DefaultRunspace
+
+ # Take every every `-Variable` passed in and define it within the job
foreach ($keyValue in $variable.GetEnumerator()) {
$ExecutionContext.SessionState.PSVariable.Set($keyValue.Key, $keyValue.Value)
- }
+ }
- if ((-not $WebSocketUri) -or $webSocket) {
- throw "No WebSocketUri"
+ # If we have no socket url,
+ if ((-not $SocketUrl)) {
+ # throw up an error.
+ throw "No SocketUrl"
}
- if (-not $WebSocketUri.Scheme) {
- $WebSocketUri = [uri]"wss://$WebSocketUri"
- }
+ # If the socket url does not have a scheme
+ if (-not $SocketUrl.Scheme) {
+ # assume `wss`
+ $SocketUrl = [uri]"wss://$SocketUrl"
+ } elseif (
+ # otherwise, if the scheme is http or https
+ $SocketUrl.Scheme -match '^https?'
+ ) {
+ # replace it with `ws` or `wss`
+ $SocketUrl = $SocketUrl -replace '^http', 'ws'
+ }
+
+ # If any query parameters were provided
+ if ($QueryParameter) {
+ # add them to the socket url
+ $SocketUrl = [uri]"$($SocketUrl)$($SocketUrl.Query ? '&' : '?')$(@(
+ foreach ($keyValuePair in $QueryParameter.GetEnumerator()) {
+ # cannocially, each key value pair should be url encoded,
+ # and multiple values should be passed multiple times.
+ foreach ($value in $keyValuePair.Value) {
+ $valueString =
+ # If the value is a boolean or a switch,
+ if ($value -is [bool] -or $value -is [switch]) {
+ # convert it to a string and make it lowercase.
+ ($value -as [bool] -as [string]).ToLower()
+ } else {
+ # Otherwise, just stringify.
+ "$value"
+ }
+ "$($keyValuePair.Key)=$([Web.HttpUtility]::UrlEncode($valueString).Replace('+', '%20'))"
+ }
+ }) -join '&')"
+ }
+ # If we had not set a -BufferSize,
if (-not $BufferSize) {
- $BufferSize = 16kb
+ $BufferSize = 64kb # default to 64kb.
}
+ # Create a cancellation token, as this will save syntax space
$CT = [Threading.CancellationToken]::None
- if (-not $webSocket) {
+ # If `$WebSocket `is not already a websocket
+ if ($webSocket -isnot [Net.WebSockets.ClientWebSocket]) {
+ # create a new socket
$ws = [Net.WebSockets.ClientWebSocket]::new()
- $null = $ws.ConnectAsync($WebSocketUri, $CT).Wait()
+ if ($SubProtocol) {
+ # and add the subprotocol
+ $ws.Options.AddSubProtocol($SubProtocol)
+ } elseif (-not $NoSubProtocol) {
+ $ws.Options.AddSubProtocol('json')
+ }
+ # If there are headers
+ if ($Header) {
+ # add them to the initial socket request.
+ foreach ($headerKeyValue in $header.GetEnumerator()) {
+ $ws.Options.SetRequestHeader($headerKeyValue.Key, $headerKeyValue.Value)
+ }
+ }
+ # Now, let's try to connect to the WebSocket.
+ $null = $ws.ConnectAsync($SocketUrl, $CT).Wait()
} else {
$ws = $WebSocket
- }
+ }
+ # Keep track of the time
$webSocketStartTime = $Variable.WebSocketStartTime = [DateTime]::Now
+ # and add the WebSocket to the variable dictionary, so we can access it later.
$Variable.WebSocket = $ws
- $MessageCount = [long]0
-
- while ($true) {
- if ($ws.State -ne 'Open') {break }
+ # Initialize some counters:
+ $MessageCount = [long]0 # * The number of messages received
+ $FilteredCount = [long]0 # * The number of messages filtered out
+ $SkipCount = [long]0 # * The number of messages skipped
+
+ # Initialize variables related to handshaking
+ $saidHello = $null # * Whether we have said hello
+ $shookHands = $null # * Whether we have shaken hands
+
+ # This loop will run as long as the websocket is open.
+ :WebSocketMessageLoop while ($ws.State -eq 'Open') {
+ # If we've given a timeout for the websocket,
+ # and the websocket has been open for longer than the timeout,
if ($TimeOut -and ([DateTime]::Now - $webSocketStartTime) -gt $TimeOut) {
+ # then it's closing time (you don't have to go home but you can't stay here).
$ws.CloseAsync([Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Timeout', $CT).Wait()
break
}
- if ($Maximum -and $MessageCount -ge $Maximum) {
+ # If we've gotten the maximum number of messages,
+ if ($Maximum -and (
+ ($MessageCount - $FilteredCount) -ge $Maximum
+ )) {
+ # then I can't even take any more responses.
$ws.CloseAsync([Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Maximum messages reached', $CT).Wait()
break
}
+ # If we're authenticating, and haven't yet said hello
+ if ($Authenticate -and -not $SaidHello) {
+ # then we should say hello.
+ # Determine the authentication message
+ $authenticationMessage =
+ # If the authentication message is a scriptblock,
+ if ($Authenticate -is [ScriptBlock]) {
+ & $Authenticate # run it
+ } else {
+ $authenticate # otherwise, use it as-is.
+ }
+
+ # If we have an authentication message
+ if ($authenticationMessage) {
+ # and it's not a string
+ if ($authenticationMessage -isnot [string]) {
+ # then we should send it as JSON and mark that we've said hello.
+ $saidHello = $ws.SendAsync([ArraySegment[byte]]::new(
+ $OutputEncoding.GetBytes((ConvertTo-Json -InputObject $authenticationMessage -Depth 10))
+ ), 'Text', $true, $CT)
+ }
+ }
+ }
+
+ # Ok, let's get the next message.
$Buf = [byte[]]::new($BufferSize)
$Seg = [ArraySegment[byte]]::new($Buf)
- $null = $ws.ReceiveAsync($Seg, $CT).Wait()
+ $receivingWebSocket = $ws.ReceiveAsync($Seg, $CT)
+ # use this tight loop to let us cancel the await if we need to.
+ while (-not ($receivingWebSocket.IsCompleted -or $receivingWebSocket.IsFaulted -or $receivingWebSocket.IsCanceled)) {
+
+ }
+ # If we had a problem, write an error.
+ if ($receivingWebSocket.Exception) {
+ Write-Error -Exception $receivingWebSocket.Exception -Category ProtocolError
+ continue
+ }
$MessageCount++
try {
+ # If we have a handshake and we haven't yet shaken hands
+ if ($Handshake -and -not $shookHands) {
+ # then we should shake hands.
+ # Get the message string
+ $messageString = $OutputEncoding.GetString($Buf, 0, $Buf.Count)
+ # and try to convert it from JSON.
+ $messageObject = ConvertFrom-Json -InputObject $messageString *>&1
+ # Determine the handshake message
+ $handShakeMessage =
+ # If the handshake message is a scriptblock,
+ if ($Handshake -is [ScriptBlock]) {
+ & $Handshake $MessageObject # run it and pass the message
+ } else {
+ $Handshake # otherwise, use it as-is.
+ }
+
+ # If we have a handshake message
+ if ($handShakeMessage) {
+ # and it's not a string
+ if ($handShakeMessage -isnot [string]) {
+ # then we should send it as JSON and mark that we've shaken hands.
+ $saidHello = $ws.SendAsync([ArraySegment[byte]]::new(
+ $OutputEncoding.GetBytes((ConvertTo-Json -InputObject $handShakeMessage -Depth 10))
+ ), 'Text', $true, $CT)
+ }
+ }
+ }
+
+ # Get the message from the websocket
$webSocketMessage =
- if ($Binary) {
- $Buf -gt 0
- } elseif ($RawText) {
- $OutputEncoding.GetString($Buf, 0, $Buf.Count)
+ if ($Binary) { # If we wanted binary
+ $Buf -gt 0 -as [byte[]] # then return non-null bytes
} else {
- $JS = $OutputEncoding.GetString($Buf, 0, $Buf.Count)
- if ([string]::IsNullOrWhitespace($JS)) { continue }
- ConvertFrom-Json $JS
+ # otherwise, get the message as a string
+ $messageString = $OutputEncoding.GetString($Buf, 0, $Buf.Count)
+ # if we have any filters
+ if ($Filter) {
+ # then we see if we can apply them now.
+ foreach ($fil in $filter) {
+ # Wilcard filters can be applied to the raw text
+ if ($fil -is [string] -and $messageString -like "*$fil*") {
+ $FilteredCount++
+ continue WebSocketMessageLoop
+ }
+ # and so can regex filters.
+ if ($fil -is [regex] -and $fil.IsMatch($messageString)) {
+ $FilteredCount++
+ continue WebSocketMessageLoop
+ }
+ }
+ }
+ # If we have asked for -RawText
+ if ($RawText) {
+ $messageString # then return the raw text
+ } else {
+ # Otherwise, try to convert the message from JSON.
+ $MessageObject = ConvertFrom-Json -InputObject $messageString
+
+ # Now we can run any filters that are scriptblocks or commands.
+ if ($filter) {
+ foreach ($fil in $Filter) {
+ if ($fil -is [ScriptBlock] -or
+ $fil -is [Management.Automation.CommandInfo]
+ ) {
+ # Capture the output of the filter
+ $filterOutput = $MessageObject | & $fil $MessageObject
+ # if the output was falsy,
+ if (-not $filterOutput) {
+ $FilteredCount++ # filter out the message.
+ continue WebSocketMessageLoop
+ }
+ }
+ }
+ }
+
+ # If -Skip was provided and we haven't skipped enough messages
+ if ($Skip -and ($SkipCount -le $Skip)) {
+ # then skip this message.
+ $SkipCount++
+ continue WebSocketMessageLoop
+ }
+
+ # Now, emit the message object.
+ # (expressions that are not assigned will be outputted)
+ $MessageObject
+
+ # If we have a -First parameter, and we have not yet reached the maximum
+ # (after accounting for skips and filters)
+ if ($First -and ($MessageCount - $FilteredCount - $SkipCount) -ge $First) {
+ # then set the maximum to first (which will cancel this after the next loop)
+ $Maximum = $first
+ }
+ }
}
+
+ # If we want to decorate the output
if ($PSTypeName) {
+ # clear it's typenames
$webSocketMessage.pstypenames.clear()
- [Array]::Reverse($PSTypeName)
- foreach ($psType in $psTypeName) {
- $webSocketMessage.pstypenames.add($psType)
- }
+ for ($typeNameIndex = $PSTypeName.Length - 1; $typeNameIndex -ge 0; $typeNameIndex--) {
+ # and add each type name in reverse order
+ $webSocketMessage.pstypenames.add($PSTypeName[$typeNameIndex])
+ }
}
- if ($handler) {
- $psCmd =
+
+ # If we are forwarding events
+ if ($ForwardEvent -and $MainRunspace.Events.GenerateEvent) {
+ # generate an event in the main runspace
+ $null = $MainRunspace.Events.GenerateEvent(
+ "$SocketUrl",
+ $ws,
+ @($webSocketMessage),
+ $webSocketMessage
+ )
+ }
+
+ # If we have an output handler, try to run it and get the output
+ $handledResponse = if ($handler) {
+ # We may need to run the handler in a `[PowerShell]` command.
+ $psCmd =
+ # This is true if we want `NoLanguage` mode.
if ($runspace.LanguageMode -eq 'NoLanguage' -or
$runspacePool.InitialSessionState.LanguageMode -eq 'NoLanguage') {
+ # (in which case we'll call .GetPowerShell())
$handler.GetPowerShell()
- } elseif ($Runspace -or $RunspacePool) {
- [PowerShell]::Create().AddScript($handler)
+ } elseif (
+ # or if we have a runspace or runspace pool
+ $Runspace -or $RunspacePool
+ ) {
+ # (in which case we'll `.Create()` and `.AddScript()`)
+ [PowerShell]::Create().AddScript($handler, $true)
}
if ($psCmd) {
+ # If we have a runspace, we'll use that.
if ($Runspace) {
$psCmd.Runspace = $Runspace
} elseif ($RunspacePool) {
+ # or, alternatively, we can use a runspace pool.
$psCmd.RunspacePool = $RunspacePool
}
+ # Now, we can invoke the command.
+ $psCmd.Invoke(@($webSocketMessage))
} else {
+ # Otherwise, we'll just run the handler.
$webSocketMessage | . $handler
}
-
+ }
+
+ # If we have a response from the handler,
+ if ($handledResponse) {
+ $handledResponse # emit that response.
} else {
- $webSocketMessage
+ $webSocketMessage # otherwise, emit the message.
}
} catch {
Write-Error $_
}
}
- }
+
+ # Now that the socket is closed,
+ # check for a status description.
+ # If there is one,
+ if ($ws.CloseStatusDescription) {
+ # write an error.
+ Write-Error $ws.CloseStatusDescription -TargetObject $ws
+ }
+ }
+ $SocketServerJob = {
+ <#
+ .SYNOPSIS
+ A fairly simple WebSocket server
+ .DESCRIPTION
+ A fairly simple WebSocket server
+ #>
+ param(
+ # By accepting a single parameter containing variables,
+ # we can avoid the need to pass in a large number of parameters.
+ # we can also modify this dictionary, to provide a way to pass information back.
+ [Collections.IDictionary]$Variable
+ )
+
+ # Take every every `-Variable` passed in and define it within the job
+ foreach ($keyValue in $variable.GetEnumerator()) {
+ $ExecutionContext.SessionState.PSVariable.Set($keyValue.Key, $keyValue.Value)
+ }
+
+ $Variable['JobRunspace'] = [Runspace]::DefaultRunspace
+
+ # If we have routes, we will cache all of their possible parameters now
+ if ($route.Count) {
+ # We want to keep the parameter sets
+ $routeParameterSets = [Ordered]@{}
+ # and the metadata about parameters.
+ $routeParameters = [Ordered]@{}
+
+ # For each key and value in the route table, we will try to get the command info for the value.
+ foreach ($routePair in $route.GetEnumerator()) {
+ $routeToCmd =
+ # If the value is a scriptblock
+ if ($routePair.Value -is [ScriptBlock]) {
+ # we have to create a temporary function
+ $function:TempFunction = $routePair.Value
+ # and get that function.
+ $ExecutionContext.SessionState.InvokeCommand.GetCommand('TempFunction', 'Function')
+ } elseif ($routePair.Value -is [Management.Automation.CommandInfo]) {
+ $routePair.Value
+ }
+ if ($routeToCmd) {
+ $routeParameterSets[$routePair.Name] = $routeToCmd.ParametersSets
+ $routeParameters[$routePair.Name] = $routeToCmd.Parameters
+ }
+ }
+ }
+
+ # If there's no listener, create one.
+ if (-not $httpListener) {
+ $httpListener = $variable['HttpListener'] = [Net.HttpListener]::new()
+ }
+
+ # If the listener doesn't have a lookup table for SocketRequests, create one.
+ if (-not $httpListener.SocketRequests) {
+ $httpListener.psobject.properties.add(
+ [psnoteproperty]::new('SocketRequests', [Ordered]@{}), $true)
+ }
+
+ # If the listener isn't listening, start it.
+ if (-not $httpListener.IsListening) { $httpListener.Start() }
+
+ $variable['SiteHeader'] = $siteHeader = @(
+
+ if ($Javascript) {
+ # as well as any javascript files provided.
+ foreach ($js in $Javascript) {
+ if ($js -match '.js$') {
+ ""
+ } else {
+ ""
+ }
+ }
+ }
+
+ # If an import map was provided, we will include it.
+ if ($ImportMap) {
+ $variable['ImportMap'] = @(
+ ""
+ ) -join [Environment]::NewLine
+ }
+
+ # If a palette name was provided, we will include the 4bitcss stylesheet.
+ if ($PaletteName) {
+ if ($PaletteName -match '/.+?\.css$') {
+ ""
+
+ } else {
+ '' -replace '\.css', "$PaletteName.css"
+ }
+ }
+
+ # If a font name was provided, we will include the font stylesheet.
+ if ($GoogleFont) {
+ ""
+ ""
+ }
+
+ # If a code font was provided, we will include the code font stylesheet.
+ if ($CodeFont) {
+ ""
+ ""
+ }
+
+ # and if any stylesheets were provided, we will include them.
+ foreach ($css in $variable.StyleSheet) {
+ if ($css -match '.css$') {
+ ""
+ } else {
+ ""
+ }
+ }
+ )
+
+ $httpListener.psobject.properties.add([psnoteproperty]::new('JobVariable',$Variable), $true)
+ $listenerStartTime = [DateTime]::Now
+
+ # While the listener is listening,
+ while ($httpListener.IsListening) {
+ # If we've given a timeout for the listener,
+ # and the listener has been open for longer than the timeout,
+ if ($Timeout -and ([DateTime]::Now - $listenerStartTime) -gt $TimeOut) {
+ # then it's closing time (you don't have to go home but you can't stay here).
+ $httpListener.Stop()
+ break
+ }
+ # get the context asynchronously.
+ $contextAsync = $httpListener.GetContextAsync()
+ # and wait for it to complete.
+ while (-not ($contextAsync.IsCompleted -or $contextAsync.IsFaulted -or $contextAsync.IsCanceled)) {
+ # while this is going on, other events can be processed, and CTRL-C can exit.
+ # also, we can go ahead and check for any socket requests, and get ready for the next one if we find one.
+ foreach ($socketRequest in @($httpListener.SocketRequests.GetEnumerator())) {
+ if ($socketRequest.Value.Receiving.IsCompleted) {
+ $socketRequest.Value.MessageCount++
+ $jsonMessage = ConvertFrom-Json -InputObject ($OutputEncoding.GetString($socketRequest.Value.ClientBuffer -gt 0))
+ $socketRequest.Value.ClientBuffer.Clear()
+ if ($MainRunspace.Events.GenerateEvent) {
+ $MainRunspace.Events.GenerateEvent.Invoke(@(
+ "$($request.Url.Scheme -replace '^http', 'ws')://",
+ $httpListener,
+ @($socketRequest.Value.Context, $socketRequest.Value.WebSocketContet, $socketRequest.Key, $socketRequest.Value),
+ $jsonMessage
+ ))
+ }
+ $socketRequest.Value.Receiving =
+ $socketRequest.Value.WebSocket.ReceiveAsync($socketRequest.Value.ClientBuffer, [Threading.CancellationToken]::None)
+ }
+ }
+ }
+ # If async method fails,
+ if ($contextAsync.IsFaulted) {
+ # write an error and continue.
+ Write-Error -Exception $contextAsync.Exception -Category ProtocolError
+ continue
+ }
+ # Get the context async result.
+ # The context is basically the next request and response in the queue.
+ $context = $(try { $contextAsync.Result } catch { $_ })
+
+ # yield the context immediately, in case anything is watching the output of this job
+ $context
+
+ $Request, $response = $context.Request, $context.Response
+ $RequestedUrl = $Request.Url
+ # Favicons are literally outdated, but they're still requested.
+ if ($RequestedUrl -match '/favicon.ico$') {
+ # by returning a 404 for them, we can make the browser stop asking.
+ $context.Response.StatusCode = 404
+ $context.Response.Close()
+ continue
+ }
+ # Now, for the fun part.
+ # We turn request into a PowerShell events.
+ # The protocol is the scheme of the request url.
+ $Protocol = $RequestedUrl.Scheme
+ # Each event will have the source identifier of the protocol, followed by ://
+ $eventIdentifier = "$($Protocol)://"
+ # and by default it will pass a message containing the context.
+ $messageData = [Ordered]@{Protocol = $protocol; Url = $context.Request.Url;Context = $context}
+
+ if ($Header -and $response) {
+ foreach ($headerKeyValue in $Header.GetEnumerator()) {
+ try {
+ $response.Headers.Add($headerKeyValue.Key, $headerKeyValue.Value)
+ } catch {
+ Write-Warning "Cannot add header '$($headerKeyValue.Key)': $_"
+ }
+ }
+ }
+
+ # HttpListeners are quite nice, especially when it comes to websocket upgrades.
+ # If the request is a websocket request
+ if ($Request.IsWebSocketRequest) {
+ # we will change the event identifier to a websocket scheme.
+ $eventIdentifier = $eventIdentifier -replace '^http', 'ws'
+ # and call the `AcceptWebSocketAsync` method to upgrade the connection.
+ $acceptWebSocket = $context.AcceptWebSocketAsync('json')
+ # Once again, we'll use a tight loop to wait for the upgrade to complete or fail.
+ while (-not ($acceptWebSocket.IsCompleted -or $acceptWebSocket.IsFaulted -or $acceptWebSocket.IsCanceled)) { }
+ # and if it fails,
+ if ($acceptWebSocket.IsFaulted) {
+ # we will write an error and continue.
+ Write-Error -Exception $acceptWebSocket.Exception -Category ProtocolError
+ continue
+ }
+ # If it succeeds, capture the result.
+ $webSocketResult = try { $acceptWebSocket.Result } catch { $_ }
+
+ # If the websocket is open
+ if ($webSocketResult.WebSocket.State -eq 'open') {
+ # we have switched protocols!
+ $Protocol = $requestedUrl.Scheme -replace '^http', 'ws'
+
+ # Now add the result it to the SocketRequests lookup table, using the request trace identifier as the key.
+ $clientBuffer = $webSocketResult.WebSocket::CreateClientBuffer($BufferSize, $BufferSize)
+ $socketObject = [PSCustomObject][Ordered]@{
+ Context = $context
+ WebSocketContext = $webSocketResult
+ WebSocket = $webSocketResult.WebSocket
+ ClientBuffer = $clientBuffer
+ Created = [DateTime]::UtcNow
+ LastMessageTime = $null
+ Receiving = $webSocketResult.WebSocket.ReceiveAsync($clientBuffer, [Threading.CancellationToken]::None)
+ MessageQueue = [Collections.Queue]::new()
+ MessageCount = [long]0
+ }
+
+ if (-not $httpListener.SocketRequests["$($webSocketResult.RequestUri)"]) {
+ $httpListener.SocketRequests["$($webSocketResult.RequestUri)"] = [Collections.Queue]::new()
+ }
+ $httpListener.SocketRequests["$($webSocketResult.RequestUri)"].Enqueue($socketObject)
+ # and add the websocketcontext result to the message data.
+ $messageData["WebSocketContext"] = $webSocketResult
+ # also add the websocket result to the message data,
+ # since many might not exactly know what a "WebSocketContext" is.
+ $messageData["WebSocket"] = $webSocketResult.WebSocket
+ }
+ }
+
+ # Now, we generate the event.
+ $generateEventArguments = @(
+ $eventIdentifier,
+ $httpListener,
+ @($context)
+ $messageData
+ )
+ # Get a pointer to the GenerateEvent method (we'll want this later)
+ if ($MainRunspace.Events.GenerateEvent) {
+ $MainRunspace.Events.GenerateEvent.Invoke($generateEventArguments)
+ }
+
+ # Everything below this point is for HTTP requests.
+ if ($protocol -notmatch '^http') {
+ continue # so if we're already a websocket, we will skip the rest of this code.
+ }
+
+ $routedTo = $null
+ $routeKey = $null
+ # If we have routes, we will try to find a route that matches the request.
+ if ($route.Count) {
+ $routeTable = $route
+ $potentialRouteKeys = @(
+ $request.Url.AbsolutePath,
+ ($request.Url.AbsolutePath -replace '/$'),
+ "$($request.HttpMethod) $($request.Url.AbsolutePath)",
+ "$($request.HttpMethod) $($request.Url.AbsolutePath -replace '/$')"
+ "$($request.HttpMethod) $($request.Url.LocalPath)",
+ "$($request.HttpMethod) $($request.Url.LocalPath -replace '/$')"
+ )
+ $routedTo = foreach ($potentialKey in $potentialRouteKeys) {
+ if ($routeTable[$potentialKey]) {
+ $routeTable[$potentialKey]
+ $routeKey = $potentialKey
+ break
+ }
+ }
+ }
+
+ if (-not $routedTo -and $handler) {
+ # If we have an output handler, try to run it and get the output
+ $routedTo = if ($handler) {
+ # We may need to run the handler in a `[PowerShell]` command.
+ $psCmd =
+ # This is true if we want `NoLanguage` mode.
+ if ($runspace.LanguageMode -eq 'NoLanguage' -or
+ $runspacePool.InitialSessionState.LanguageMode -eq 'NoLanguage') {
+ # (in which case we'll call .GetPowerShell())
+ $handler.GetPowerShell()
+ } elseif (
+ # or if we have a runspace or runspace pool
+ $Runspace -or $RunspacePool
+ ) {
+ # (in which case we'll `.Create()` and `.AddScript()`)
+ [PowerShell]::Create().AddScript($handler, $true)
+ }
+ if ($psCmd) {
+ # If we have a runspace, we'll use that.
+ if ($Runspace) {
+ $psCmd.Runspace = $Runspace
+ } elseif ($RunspacePool) {
+ # or, alternatively, we can use a runspace pool.
+ $psCmd.RunspacePool = $RunspacePool
+ }
+ # Now, we can invoke the command.
+ $psCmd.Invoke(@($context))
+ } else {
+ # Otherwise, we'll just run the handler.
+ $context | . $handler
+ }
+ }
+ }
+
+ if (-not $routedTo -and $html) {
+ $routedTo =
+ # If the content is already html, we will use it as is.
+ if ($html -match '\"
+ ""
+ # and apply the site header.
+ $SiteHeader -join [Environment]::NewLine
+ ""
+ ""
+ $html
+ ""
+ ""
+ ) -join [Environment]::NewLine
+ }
+ }
+
+ # If we routed to a string, we will close the response with the string.
+ if ($routedTo -is [string]) {
+ $response.Close($OutputEncoding.GetBytes($routedTo), $true)
+ continue
+ }
+
+ # If we've routed to is a byte array, we will close the response with the byte array.
+ if ($routedTo -is [byte[]]) {
+ $response.Close($routedTo, $true)
+ continue
+ }
+
+ # If we routed to a script block or command, we will try to execute it.
+ if ($routedTo -is [ScriptBlock] -or
+ $routedTo -is [Management.Automation.CommandInfo]) {
+ $routeSplat = [Ordered]@{}
+
+ # If the command had a `-Request` parameter, we will pass the request object.
+ if ($routeParameters -and $routeParameters[$routeKey].Request) {
+ $routeSplat['Request'] = $request
+ }
+ # If the command had a `-Response` parameter, we will pass the response object.
+ if ($routeParameters -and $routeParameters[$routeKey].Response) {
+ $routeSplat['Response'] = $response
+ }
+
+ # If the request has a query string, we will parse it and pass the values to the command.
+ if ($request.Url.QueryString) {
+ $parsedQuery = [Web.HttpUtility]::ParseQueryString($request.Url.QueryString)
+ foreach ($parsedQueryKey in $parsedQuery.Keys) {
+ if ($routeParameters[$routeKey][$parsedQueryKey]) {
+ $routeSplat[$parsedQueryKey] = $parsedQuery[$parsedQueryKey]
+ }
+ }
+ }
+ # If the request has a content type of json, we will parse the json and pass the values to the command.
+ if ($request.ContentType -match '^(?>application|text)/json') {
+ $streamReader = [IO.StreamReader]::new($request.InputStream)
+ $json = $streamReader.ReadToEnd()
+ $jsonHashtable = ConvertFrom-Json -InputObject $json -AsHashtable
+ foreach ($keyValuePair in $jsonHashtable.GetEnumerator()) {
+ if ($routeParameters[$routeKey][$keyValuePair.Key]) {
+ $routeSplat[$keyValuePair.Key] = $keyValuePair.Value
+ }
+ }
+ $streamReader.Close()
+ $streamReader.Dispose()
+ }
+
+ # If the request has a content type of form-urlencoded, we will parse the form and pass the values to the command.
+ if ($request.ContentType -eq 'application/x-www-form-urlencoded') {
+ $streamReader = [IO.StreamReader]::new($request.InputStream)
+ $formData = [Web.HttpUtility]::ParseQueryString($streamReader.ReadToEnd())
+ foreach ($formKey in $formData.Keys) {
+ if ($routeParameters[$routeKey][$formKey]) {
+ $routeSplat[$formKey] = $form[$formKey]
+ }
+ }
+ $streamReader.Close()
+ $streamReader.Dispose()
+ }
+
+ # We will execute the command and get the output.
+ $routeOutput = . $routedTo @routeSplat
+
+ # If the output is a string, we will close the response with the string.
+ if ($routeOutput -is [string])
+ {
+ $response.Close($OutputEncoding.GetBytes($routeOutput), $true)
+ continue
+ }
+ # If the output is a byte array, we will close the response with the byte array.
+ elseif ($routeOutput -is [byte[]])
+ {
+ $response.Close($routeOutput, $true)
+ continue
+ }
+ # If the response is an array, write the responses out one at a time.
+ # (note: this will likely be changed in the future)
+ elseif ($routeOutput -is [object[]]) {
+ foreach ($routeOut in $routeOutput) {
+ if ($routeOut -is [string]) {
+ $routeOut = $OutputEncoding.GetBytes($routeOut)
+ }
+ if ($routeOut -is [byte[]]) {
+ $response.OutputStream.Write($routeOut, 0, $routeOut.Length)
+ }
+ }
+ $response.Close()
+ }
+ else {
+ # If the response was an object, we will convert it to json and close the response with the json.
+ $responseJson = ConvertTo-Json -InputObject $routeOutput -Depth 3
+ $response.ContentType = 'application/json'
+ $response.Close($OutputEncoding.GetBytes($responseJson), $true)
+ }
+ }
+ }
+ }
}
process {
+ # Sometimes we want to customize the behavior of a command based off of the input object
+ # So, start off by capturing $_
+ $inputObject = $_
+ # If the input was a job, we might remap a parameter
+ if ($inputObject -is 'Management.Automation.Job') {
+ if ($inputObject.WebSocket -is [Net.WebSockets.ClientWebSocket] -and
+ $inputObject.SocketUrl) {
+ $SocketUrl = $inputObject.SocketUrl
+ }
+ if ($inputObject.HttpListener -is [Net.HttpListener] -and
+ $inputObject.RootUrl) {
+ $RootUrl = $inputObject.RootUrl
+ }
+ }
+ if ((-not $SocketUrl) -and (-not $RootUrl)) {
+ $socketAndListenerJobs =
+ foreach ($job in Get-Job) {
+ if (
+ $Job.WebSocket -is [Net.WebSockets.ClientWebSocket] -or
+ $Job.HttpListener -is [Net.HttpListener]
+ ) {
+ $job
+ }
+ }
+ $socketAndListenerJobs
+ }
+ # First, let's pack all of the parameters into a dictionary of variables.
foreach ($keyValuePair in $PSBoundParameters.GetEnumerator()) {
$Variable[$keyValuePair.Key] = $keyValuePair.Value
}
- $webSocketJob =
- if ($WebSocketUri) {
+
+ $Variable['MainRunspace'] = [Runspace]::DefaultRunspace
+ if (-not $variable['BufferSize']) {
+ $variable['BufferSize'] = $BufferSize
+ }
+ $StartThreadJobSplat = [Ordered]@{
+ InitializationScript = $InitializationScript
+ ThrottleLimit = $ThrottleLimit
+ }
+
+ # If we're going to be listening for HTTP requests, run a thread job for the server.
+ if ($RootUrl) {
+
+ if (-not $Name) {
+ $Name = "$($RootUrl -join '|')"
+ }
+
+ $existingJob = foreach ($jobWithThisName in (Get-Job -Name $Name -ErrorAction Ignore)) {
+ if (
+ $jobWithThisName.State -in 'Running','NotStarted' -and
+ $jobWithThisName.HttpListener -is [Net.HttpListener]
+ ) {
+ $jobWithThisName
+ break
+ }
+ }
+
+ if ((-not $existingJob) -or $Force) {
+ $variable['HttpListener'] = $httpListener = [Net.HttpListener]::new()
+ foreach ($potentialPrefix in $RootUrl) {
+ if ($potentialPrefix -match '^https?://') {
+ $httpListener.Prefixes.Add($potentialPrefix)
+ } else {
+ $httpListener.Prefixes.Add("http://$potentialPrefix/")
+ $httpListener.Prefixes.Add("https://$potentialPrefix/")
+ }
+ }
+ $httpListener.Start()
+ }
+
+ if ($DebugPreference -notin 'SilentlyContinue','Ignore') {
+ . $SocketServerJob -Variable $Variable
+ } else {
+ if ($existingJob -and -not $Force) {
+ $httpListenerJob = $existingJob
+ $httpListener = $existingJob.HttpListener
+ } else {
+ $httpListenerJob = Start-ThreadJob -ScriptBlock $SocketServerJob -Name "$RootUrl" -ArgumentList $Variable @StartThreadJobSplat
+ $httpListenerJob.pstypenames.insert(0, 'WebSocket.ThreadJob')
+ $httpListenerJob.pstypenames.insert(0, 'WebSocket.Server.ThreadJob')
+ }
+ }
+
+ # If we have a listener job
+ if ($httpListenerJob) {
+ # and the job has not started
+ if ($httpListenerJob.JobStateInfo.State -eq 'NotStarted') {
+ # sleep for no time (this will allow the job to start)
+ Start-Sleep -Milliseconds 0
+ }
+ foreach ($keyValuePair in $Variable.GetEnumerator()) {
+ $httpListenerJob.psobject.properties.add(
+ [psnoteproperty]::new($keyValuePair.Key, $keyValuePair.Value), $true
+ )
+ }
+
+ if (-not $Broadcast) {
+ $httpListenerJob
+ }
+ }
+ }
+
+ # If `-Debug` was passed,
+ if ($DebugPreference -notin 'SilentlyContinue','Ignore') {
+ # run the job in the current scope (so we can debug it).
+ . $SocketClientJob -Variable $Variable
+ return
+ }
+
+ # If -Debug was not passed, we're running in a background thread job.
+ $webSocketJob =
+ if ($SocketUrl) {
+ # If we had no name, we will use the SocketUrl as the name.
if (-not $name) {
- $Name = $WebSocketUri
+ # and we will ensure that it starts with `ws://` or `wss://`
+ $Name = $SocketUrl -replace '^http', 'ws'
}
- $existingJob = foreach ($jobWithThisName in (Get-Job -Name $Name)) {
+ $existingJob = foreach ($jobWithThisName in (Get-Job -Name $Name -ErrorAction Ignore)) {
if (
$jobWithThisName.State -in 'Running','NotStarted' -and
$jobWithThisName.WebSocket -is [Net.WebSockets.ClientWebSocket]
@@ -335,16 +1289,11 @@ function Get-WebSocket {
}
}
- if ($existingJob) {
+ if ($existingJob -and -not $Force) {
$existingJob
} else {
- Start-ThreadJob -ScriptBlock $SocketJob -Name $Name -InitializationScript $InitializationScript -ArgumentList $Variable
- }
- } elseif ($WebSocket) {
- if (-not $name) {
- $name = "websocket"
+ Start-ThreadJob -ScriptBlock $SocketClientJob -Name $Name -ArgumentList $Variable @StartThreadJobSplat
}
- Start-ThreadJob -ScriptBlock $SocketJob -Name $Name -InitializationScript $InitializationScript -ArgumentList $Variable
}
$subscriptionSplat = @{
@@ -353,33 +1302,90 @@ function Get-WebSocket {
SupportEvent = $true
}
$eventSubscriptions = @(
- if ($OnOutput) {
- Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Output -Action $OnOutput
- }
- if ($OnError) {
- Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Error -Action $OnError
- }
- if ($OnWarning) {
- Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Warning -Action $OnWarning
- }
+ if ($webSocketJob) {
+ if ($OnOutput) {
+ Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Output -Action $OnOutput
+ }
+ if ($OnError) {
+ Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Error -Action $OnError
+ }
+ if ($OnWarning) {
+ Register-ObjectEvent @subscriptionSplat -InputObject $webSocketJob.Warning -Action $OnWarning
+ }
+ }
)
if ($eventSubscriptions) {
$variable['EventSubscriptions'] = $eventSubscriptions
}
- $webSocketConnectTimeout = [DateTime]::Now + $ConnectionTimeout
- while (-not $variable['WebSocket'] -and
- ([DateTime]::Now -lt $webSocketConnectTimeout)) {
- Start-Sleep -Milliseconds 0
+ if ($webSocketJob -and -not $webSocketJob.WebSocket) {
+ $webSocketConnectTimeout = [DateTime]::Now + $ConnectionTimeout
+ while (-not $variable['WebSocket'] -and
+ ([DateTime]::Now -lt $webSocketConnectTimeout)) {
+ Start-Sleep -Milliseconds 0
+ }
+
+ foreach ($keyValuePair in $Variable.GetEnumerator()) {
+ $webSocketJob.psobject.properties.add(
+ [psnoteproperty]::new($keyValuePair.Key, $keyValuePair.Value), $true
+ )
+ }
+ $webSocketJob.pstypenames.insert(0, 'WebSocket.ThreadJob')
+ $webSocketJob.pstypenames.insert(0, 'WebSocket.Client.ThreadJob')
}
-
- foreach ($keyValuePair in $Variable.GetEnumerator()) {
- $webSocketJob.psobject.properties.add(
- [psnoteproperty]::new($keyValuePair.Key, $keyValuePair.Value), $true
+
+ # If we're broadcasting a message
+ if ($Broadcast) {
+ # find out who is listening.
+ $socketList = @(
+ if ($httpListener.SocketRequests) {
+ @(foreach ($queue in $httpListener.SocketRequests.Values) {
+ foreach ($socket in $queue) {
+ if ($socket.WebSocket.State -eq 'Open') {
+ $socket.WebSocket
+ }
+ }
+ })
+ }
+ if ($webSocketJob.WebSocket) {
+ $webSocketJob.WebSocket
+ }
)
+
+ # If no one is listening, write a warning.
+ if (-not $socketList) {
+ Write-Warning "No one is listening"
+ }
+
+ # If the broadcast is a scriptblock or command, run it.
+ if ($Broadcast -is [ScriptBlock] -or
+ $Broadcast -is [Management.Automation.CommandInfo]) {
+ $Broadcast = & $Broadcast
+ }
+ # If the broadcast is a byte array, convert it to an array segment.
+ if ($broadcast -is [byte[]]) {
+ $broadcast = [ArraySegment[byte]]::new($broadcast)
+ }
+
+ # If the broadcast is an array segment, send it as binary.
+ if ($broadcast -is [ArraySegment[byte]]) {
+ foreach ($socket in $socketList) {
+ $null = $socket.SendAsync($broadcast, 'Binary', 'EndOfMessage', [Threading.CancellationToken]::None)
+ }
+ }
+ else {
+ # Otherwise, convert the broadcast to JSON.
+ $broadcastJson = ConvertTo-Json -InputObject $Broadcast
+ $broadcastJsonBytes = $OutputEncoding.GetBytes($broadcastJson)
+ $broadcastSegment = [ArraySegment[byte]]::new($broadcastJsonBytes)
+ foreach ($socket in $socketList) {
+ $null = $socket.SendAsync($broadcastSegment, 'Text', 'EndOfMessage', [Threading.CancellationToken]::None)
+ }
+ }
+ $Broadcast # emit the broadcast.
}
- $webSocketJob.pstypenames.insert(0, 'WebSocketJob')
- if ($Watch) {
+
+ if ($Watch -and $webSocketJob) {
do {
$webSocketJob | Receive-Job
Start-Sleep -Milliseconds (
@@ -387,7 +1393,7 @@ function Get-WebSocket {
)
} while ($webSocketJob.State -in 'Running','NotStarted')
}
- elseif ($WatchFor) {
+ elseif ($WatchFor -and $webSocketJob) {
. {
do {
$webSocketJob | Receive-Job
@@ -413,8 +1419,8 @@ function Get-WebSocket {
}
}
}
- }
- else {
+ }
+ elseif ($webSocketJob -and -not $broadcast) {
$webSocketJob
}
}
diff --git a/Container.init.ps1 b/Container.init.ps1
index 352fe81..6d0c8cf 100644
--- a/Container.init.ps1
+++ b/Container.init.ps1
@@ -10,19 +10,16 @@
# Thank you Microsoft! Thank you PowerShell! Thank you Docker!
FROM mcr.microsoft.com/powershell
# Set the shell to PowerShell (thanks again, Docker!)
- SHELL ["/bin/pwsh", "-nologo", "-command"]
- # Run the initialization script. This will do all remaining initialization in a single layer.
- RUN --mount=type=bind,src=./,target=/Initialize ./Initialize/Container.init.ps1
+ # Copy the module into the container
+ RUN --mount=type=bind,src=./,target=/Initialize /bin/pwsh -nologo -command /Initialize/Container.init.ps1
+ # Set the entrypoint to the script we just created.
+ ENTRYPOINT [ "/bin/pwsh","-nologo","-noexit","-file","/Container.start.ps1" ]
~~~
The scripts arguments can be provided with either an `ARG` or `ENV` instruction in the Dockerfile.
-.NOTES
- Did you know that in PowerShell you can 'use' namespaces that do not really exist?
- This seems like a nice way to describe a relationship to a container image.
- That is why this file is using the namespace 'mcr.microsoft.com/powershell'.
- (this does nothing, but most likely will be used in the future)
#>
-using namespace 'mcr.microsoft.com/powershell AS powerShell'
+
+#use container mcr.microsoft.com/powershell
param(
# The name of the module to be installed.
diff --git a/Container.start.ps1 b/Container.start.ps1
index 1f83e5c..bcde545 100644
--- a/Container.start.ps1
+++ b/Container.start.ps1
@@ -24,7 +24,8 @@
That is why this file is using the namespace 'mcr.microsoft.com/powershell'.
(this does nothing, but most likely will be used in the future)
#>
-using namespace 'ghcr.io/powershellweb/websocket'
+
+#use container ghcr.io/powershellweb/websocket
param()
@@ -62,7 +63,7 @@ if ($args) {
# If a single drive is mounted, start the socket files.
$webSocketFiles = $mountedFolders | Get-ChildItem -Filter *.WebSocket.ps1
foreach ($webSocketFile in $webSocketFiles) {
- Start-ThreadJob -Name $webSocketFile.Name -ScriptBlock {param($webSocketFile) . $using:webSocketFile.FullName } -ArgumentList $webSocketFile
+ Start-ThreadJob -Name $webSocketFile.Name -ScriptBlock { . $using:webSocketFile.FullName }
. $webSocketFile.FullName
}
}
diff --git a/Container.stop.ps1 b/Container.stop.ps1
index edb66db..bb6dfe6 100644
--- a/Container.stop.ps1
+++ b/Container.stop.ps1
@@ -6,4 +6,4 @@
It can be used to perform any necessary cleanup before the container is stopped.
#>
-"Container now exiting, thank you for using WebSocket!" | Out-Host
+"Container now exiting, thank you for using $env:ModuleName!" | Out-Host
diff --git a/Dockerfile b/Dockerfile
index 6df5630..be769c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Thank you Microsoft! Thank you PowerShell! Thank you Docker!
-FROM mcr.microsoft.com/powershell AS powershell
+FROM mcr.microsoft.com/powershell
# Set the module name to the name of the module we are building
ENV ModuleName=WebSocket
diff --git a/README.md b/README.md
index 3270710..84f64b3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# WebSocket
@@ -46,15 +50,18 @@ To stop watching a websocket, simply stop the background job.
~~~powershell
# Create a WebSocket job that connects to a WebSocket and outputs the results.
-Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+$socketServer = Get-WebSocket -RootUrl "http://localhost:8387/" -HTML "
WebSocket Server
"
+$socketClient = Get-WebSocket -SocketUrl "ws://localhost:8387/"
+foreach ($n in 1..10) { $socketServer.Send(@{n=Get-Random}) }
+$socketClient | Receive-Job -Keep
~~~
#### Get-WebSocket Example 2
~~~powershell
# Get is the default verb, so we can just say WebSocket.
# `-Watch` will output a continous stream of objects from the websocket.
-# For example, let's Watch BlueSky, but just the text.
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+# For example, let's Watch BlueSky, but just the text
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch -Maximum 1kb |
% {
$_.commit.record.text
}
@@ -69,18 +76,28 @@ $blueSkySocketUrl = "wss://jetstream2.us-$(
"wantedCollections=app.bsky.feed.post"
) -join '&')"
websocket $blueSkySocketUrl -Watch |
- % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"} -Max 1kb
~~~
#### Get-WebSocket Example 4
~~~powershell
+# Watch continuously in a background job.
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
~~~
#### Get-WebSocket Example 5
+~~~powershell
+# Watch the first message in -Debug mode.
+# This allows you to literally debug the WebSocket messages as they are encountered.
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -Max 1 -Debug
+~~~
+ #### Get-WebSocket Example 6
+
~~~powershell
# Watch BlueSky, but just the emoji
-websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail -Max 1kb |
Foreach-Object {
$in = $_
if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
@@ -88,7 +105,7 @@ websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.f
}
}
~~~
- #### Get-WebSocket Example 6
+ #### Get-WebSocket Example 7
~~~powershell
$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
@@ -102,7 +119,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 7
+ #### Get-WebSocket Example 8
~~~powershell
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
@@ -113,17 +130,19 @@ websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.
$_.commit.record.embed.external.uri
}
~~~
- #### Get-WebSocket Example 8
+ #### Get-WebSocket Example 9
~~~powershell
# BlueSky, but just the hashtags
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -WatchFor @{
{$webSocketoutput.commit.record.text -match "\#\w+"}={
$matches.0
}
-}
+} -Maximum 1kb
~~~
- #### Get-WebSocket Example 9
+ #### Get-WebSocket Example 10
~~~powershell
# BlueSky, but just the hashtags (as links)
@@ -137,7 +156,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 10
+ #### Get-WebSocket Example 11
~~~powershell
websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
@@ -149,7 +168,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 11
+ #### Get-WebSocket Example 12
~~~powershell
# We can decorate a type returned from a WebSocket, allowing us to add additional properties.
diff --git a/README.ps.md b/README.ps.md
index c3a7d5f..397739e 100644
--- a/README.ps.md
+++ b/README.ps.md
@@ -1,5 +1,9 @@
# WebSocket
diff --git a/Types/WebSocket.ThreadJob/Clear.ps1 b/Types/WebSocket.ThreadJob/Clear.ps1
new file mode 100644
index 0000000..ed0c6e2
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/Clear.ps1
@@ -0,0 +1 @@
+$this.Output.Clear()
\ No newline at end of file
diff --git a/Types/WebSocket.ThreadJob/Pop.ps1 b/Types/WebSocket.ThreadJob/Pop.ps1
new file mode 100644
index 0000000..5b64908
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/Pop.ps1
@@ -0,0 +1,6 @@
+param()
+
+if ($this.Output.Count -gt 0) {
+ $this.Output[0]
+ $this.Output.RemoveAt(0)
+}
\ No newline at end of file
diff --git a/Types/WebSocket.ThreadJob/Receive.ps1 b/Types/WebSocket.ThreadJob/Receive.ps1
new file mode 100644
index 0000000..36d40e4
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/Receive.ps1
@@ -0,0 +1 @@
+$this | Receive-Job -Keep -ErrorAction Ignore
\ No newline at end of file
diff --git a/Types/WebSocket.ThreadJob/WebSocket.Client.ThreadJob/Send.ps1 b/Types/WebSocket.ThreadJob/WebSocket.Client.ThreadJob/Send.ps1
new file mode 100644
index 0000000..facfe86
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/WebSocket.Client.ThreadJob/Send.ps1
@@ -0,0 +1,38 @@
+<#
+.SYNOPSIS
+ Sends a WebSocket message.
+.DESCRIPTION
+ Sends a message to a WebSocket server.
+#>
+param(
+[PSObject]
+$Message
+)
+
+function sendMessage {
+ param([Parameter(ValueFromPipeline)]$msg)
+ process {
+ if ($msg -is [byte[]]) {
+ [ArraySegment[byte]]$messageSegment = [ArraySegment[byte]]::new($msg)
+ if ($null -ne $messageSegment -and $this.WebSocket.SendAsync) {
+ $this.WebSocket.SendAsync($messageSegment, 'Binary', 'EndOfMessage',[Threading.Cancellationtoken]::None)
+ }
+ } else {
+ $jsonMessage = ConvertTo-Json -InputObject $msg
+ $messageSegment = [ArraySegment[byte]]::new($OutputEncoding.GetBytes($jsonMessage))
+ if ($null -ne $jsonMessage -and $this.WebSocket.SendAsync) {
+ $this.WebSocket.SendAsync($messageSegment, 'Text', 'EndOfMessage', [Threading.Cancellationtoken]::None)
+ }
+ }
+ }
+}
+
+if ($message -is [Collections.IList] -and $message -isnot [byte[]]) {
+ $Message | sendMessage
+} else {
+ sendMessage -msg $Message
+}
+
+
+
+
diff --git a/Types/WebSocket.ThreadJob/WebSocket.Server.ThreadJob/Send.ps1 b/Types/WebSocket.ThreadJob/WebSocket.Server.ThreadJob/Send.ps1
new file mode 100644
index 0000000..9782265
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/WebSocket.Server.ThreadJob/Send.ps1
@@ -0,0 +1,69 @@
+<#
+.SYNOPSIS
+ Sends a WebSocket message.
+.DESCRIPTION
+ Sends a message from a WebSocket server.
+#>
+param(
+[PSObject]
+$Message,
+
+[string]
+$Pattern
+)
+
+function sendMessage {
+ param([Parameter(ValueFromPipeline)]$msg, [PSObject[]]$Sockets)
+ process {
+ if ($msg -is [byte[]]) {
+ $messageSegment = [ArraySegment[byte]]::new($msg)
+ foreach ($socket in $sockets) {
+ if ($null -ne $messageSegment -and $socket.SendAsync) {
+ $null = $socket.SendAsync($messageSegment, 'Binary', 'EndOfMessage',[Threading.Cancellationtoken]::None)
+ }
+ }
+
+ } else {
+ $jsonMessage = ConvertTo-Json -InputObject $msg
+ $messageSegment = [ArraySegment[byte]]::new($OutputEncoding.GetBytes($jsonMessage))
+ foreach ($socket in $sockets) {
+ if ($null -ne $messageSegment -and $socket.SendAsync) {
+ $null = $socket.SendAsync($messageSegment, 'Binary', 'EndOfMessage',[Threading.Cancellationtoken]::None)
+ }
+ }
+ }
+ $msg
+ }
+}
+
+$patternAsRegex = $pattern -as [regex]
+$socketList = @(
+ foreach ($socketConnection in $this.HttpListener.SocketRequests.Values) {
+ if (
+ $patternAsRegex -and
+ $socketConnection.WebSocketContext.RequestUri -match $pattern
+ ) {
+ $socketConnection.WebSocket
+ }
+ elseif (
+ $pattern -and
+ $socketConnection.WebSocketContext.RequestUri -like $pattern
+ ) {
+ $socketConnection.WebSocket
+ }
+ else {
+ $socketConnection.WebSocket
+ }
+ }
+)
+
+
+if ($message -is [Collections.IList] -and $message -isnot [byte[]]) {
+ $Message | sendMessage -Sockets $socketList
+} else {
+ sendMessage -msg $Message -Sockets $socketList
+}
+
+
+
+
diff --git a/Types/WebSocket.ThreadJob/WebSocket.ThreadJob.format.ps1 b/Types/WebSocket.ThreadJob/WebSocket.ThreadJob.format.ps1
new file mode 100644
index 0000000..22e7f8e
--- /dev/null
+++ b/Types/WebSocket.ThreadJob/WebSocket.ThreadJob.format.ps1
@@ -0,0 +1 @@
+Write-FormatView -TypeName WebSocket.ThreadJob -Property Id, Name, State -AutoSize
diff --git a/WebSocket.format.ps1xml b/WebSocket.format.ps1xml
new file mode 100644
index 0000000..9c8c52b
--- /dev/null
+++ b/WebSocket.format.ps1xml
@@ -0,0 +1,37 @@
+
+
+
+
+ WebSocket.ThreadJob
+
+ WebSocket.ThreadJob
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Id
+
+
+ Name
+
+
+ State
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WebSocket.psd1 b/WebSocket.psd1
index 4eb77b6..8b6598c 100644
--- a/WebSocket.psd1
+++ b/WebSocket.psd1
@@ -1,5 +1,5 @@
@{
- ModuleVersion = '0.1.2'
+ ModuleVersion = '0.1.3'
RootModule = 'WebSocket.psm1'
Guid = '75c70c8b-e5eb-4a60-982e-a19110a1185d'
Author = 'James Brundage'
@@ -7,7 +7,9 @@
Copyright = '2024 StartAutomating'
Description = 'Work with WebSockets in PowerShell'
FunctionsToExport = @('Get-WebSocket')
- AliasesToExport = @('WebSocket')
+ AliasesToExport = @('WebSocket','ws','wss')
+ FormatsToProcess = @('WebSocket.format.ps1xml')
+ TypesToProcess = @('WebSocket.types.ps1xml')
PrivateData = @{
PSData = @{
Tags = @('WebSocket', 'WebSockets', 'Networking', 'Web')
@@ -17,18 +19,44 @@
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
-## WebSocket 0.1.2
+## WebSocket 0.1.3
-* WebSocket now decorates (#34)
- * Added a -PSTypeName(s) parameter to Get-WebSocket, so we can extend the output.
-* Reusing WebSockets (#35)
- * If a WebSocketUri is already open, we will reuse it.
-* Explicitly exporting commands (#38)
- * This should enable automatic import and enable Find-Command
+WebSocket server support!
+
+### Server Features
+
+For consistency, capabilities, and aesthetics,
+this is a fairly fully features HTTP server that happens to support websockets
+
+* `Get-WebSocket` `-RootURL/-HostHeader` ( #47 )
+* `-StyleSheet` lets you include stylesheets ( #64 )
+* `-JavaScript` lets you include javascript ( #65 )
+* `-Timeout/-LifeSpan` server support ( #85 )
+
+### Client Improvements
+
+* `Get-WebSocket -QueryParameter` lets you specify query parameters ( #41 )
+* `Get-WebSocket -Debug` lets you debug the websocketjob. ( #45 )
+* `Get-WebSocket -SubProtocol` lets you specify a subprotocol (defaults to JSON) ( #46 )
+* `Get-WebSocket -Authenticate` allows sends pre-connection authentication ( #69 )
+* `Get-WebSocket -Handshake` allows post-connection authentication ( #81 )
+* `Get-WebSocket -Force` allows the creation of multiple clients ( #58 )
+
+
+### General Improvements
+
+* `Get-WebSocket -SupportsPaging` ( #55 )
+* `Get-WebSocket -BufferSize 64kb` ( #52 )
+* `Get-WebSocket -Force` ( #58 )
+* `Get-WebSocket -Filter` ( #42 )
+* `Get-WebSocket -ForwardEvent` ( #56 )
+* `Get-WebSocket` Parameter Sets ( #73, #74 )
+* `-Broadcast` lets you broadcast to clients and servers ( #39 )
+* `Get-WebSocket` quieting previous job check ( #43 )
---
-Additional details available in the [CHANGELOG](CHANGELOG.md)
+Additional details available in the [CHANGELOG](https://github.com/PowerShellWeb/WebSocket/blob/main/CHANGELOG.md)
'@
}
}
diff --git a/WebSocket.types.ps1xml b/WebSocket.types.ps1xml
new file mode 100644
index 0000000..7d1f114
--- /dev/null
+++ b/WebSocket.types.ps1xml
@@ -0,0 +1,206 @@
+
+
+
+ WebSocket.Client.ThreadJob
+
+
+ Clear
+
+
+
+ Pop
+
+
+
+ Receive
+
+
+
+ Send
+
+
+
+
+
+ WebSocket.Server.ThreadJob
+
+
+ Clear
+
+
+
+ Pop
+
+
+
+ Receive
+
+
+
+ Send
+
+
+
+
+
+ WebSocket.ThreadJob
+
+
+ Clear
+
+
+
+ Pop
+
+
+
+ Receive
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index a2ac339..3314467 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,41 @@
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
+## WebSocket 0.1.3
+
+WebSocket server support!
+
+### Server Features
+
+For consistency, capabilities, and aesthetics,
+this is a fairly fully features HTTP server that happens to support websockets
+
+* `Get-WebSocket` `-RootURL/-HostHeader` ( #47 )
+* `-StyleSheet` lets you include stylesheets ( #64 )
+* `-JavaScript` lets you include javascript ( #65 )
+* `-Timeout/-LifeSpan` server support ( #85 )
+
+### Client Improvements
+
+* `Get-WebSocket -QueryParameter` lets you specify query parameters ( #41 )
+* `Get-WebSocket -Debug` lets you debug the websocketjob. ( #45 )
+* `Get-WebSocket -SubProtocol` lets you specify a subprotocol (defaults to JSON) ( #46 )
+* `Get-WebSocket -Authenticate` allows sends pre-connection authentication ( #69 )
+* `Get-WebSocket -Handshake` allows post-connection authentication ( #81 )
+* `Get-WebSocket -Force` allows the creation of multiple clients ( #58 )
+
+
+### General Improvements
+
+* `Get-WebSocket -SupportsPaging` ( #55 )
+* `Get-WebSocket -BufferSize 64kb` ( #52 )
+* `Get-WebSocket -Force` ( #58 )
+* `Get-WebSocket -Filter` ( #42 )
+* `Get-WebSocket -ForwardEvent` ( #56 )
+* `Get-WebSocket` Parameter Sets ( #73, #74 )
+* `-Broadcast` lets you broadcast to clients and servers ( #39 )
+* `Get-WebSocket` quieting previous job check ( #43 )
+
## WebSocket 0.1.2
* WebSocket now decorates (#34)
diff --git a/docs/Get-WebSocket.md b/docs/Get-WebSocket.md
index f252bef..0e76005 100644
--- a/docs/Get-WebSocket.md
+++ b/docs/Get-WebSocket.md
@@ -16,18 +16,30 @@ If the `-Watch` parameter is provided, will output a continous stream of objects
---
+### Related Links
+* [https://websocket.powershellweb.com/Get-WebSocket/](https://websocket.powershellweb.com/Get-WebSocket/)
+
+* [https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket?wt.mc_id=MVP_321542](https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket?wt.mc_id=MVP_321542)
+
+* [https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?wt.mc_id=MVP_321542](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?wt.mc_id=MVP_321542)
+
+---
+
### Examples
Create a WebSocket job that connects to a WebSocket and outputs the results.
```PowerShell
-Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+$socketServer = Get-WebSocket -RootUrl "http://localhost:8387/" -HTML "
WebSocket Server
"
+$socketClient = Get-WebSocket -SocketUrl "ws://localhost:8387/"
+foreach ($n in 1..10) { $socketServer.Send(@{n=Get-Random}) }
+$socketClient | Receive-Job -Keep
```
Get is the default verb, so we can just say WebSocket.
`-Watch` will output a continous stream of objects from the websocket.
-For example, let's Watch BlueSky, but just the text.
+For example, let's Watch BlueSky, but just the text
```PowerShell
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch -Maximum 1kb |
% {
$_.commit.record.text
}
@@ -41,17 +53,25 @@ $blueSkySocketUrl = "wss://jetstream2.us-$(
"wantedCollections=app.bsky.feed.post"
) -join '&')"
websocket $blueSkySocketUrl -Watch |
- % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"} -Max 1kb
```
-> EXAMPLE 4
+Watch continuously in a background job.
```PowerShell
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
```
+Watch the first message in -Debug mode.
+This allows you to literally debug the WebSocket messages as they are encountered.
+
+```PowerShell
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -Max 1 -Debug
+```
Watch BlueSky, but just the emoji
```PowerShell
-websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail -Max 1kb |
Foreach-Object {
$in = $_
if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
@@ -59,7 +79,7 @@ websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.f
}
}
```
-> EXAMPLE 6
+> EXAMPLE 7
```PowerShell
$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
@@ -73,7 +93,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
```
-> EXAMPLE 7
+> EXAMPLE 8
```PowerShell
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
@@ -87,11 +107,13 @@ websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.
BlueSky, but just the hashtags
```PowerShell
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -WatchFor @{
{$webSocketoutput.commit.record.text -match "\#\w+"}={
$matches.0
}
-}
+} -Maximum 1kb
```
BlueSky, but just the hashtags (as links)
@@ -106,7 +128,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
```
-> EXAMPLE 10
+> EXAMPLE 11
```PowerShell
websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
@@ -141,20 +163,98 @@ $somePosts |
---
### Parameters
-#### **WebSocketUri**
-The Uri of the WebSocket to connect to.
+#### **SocketUrl**
+The WebSocket Uri.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|-------|--------|--------|---------------------|---------------------------------------------|
+|`[Uri]`|false |1 |true (ByPropertyName)|Url Uri WebSocketUri WebSocketUrl|
+
+#### **RootUrl**
+One or more root urls.
+If these are provided, a WebSocket server will be created with these listener prefixes.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|------------|--------|--------|---------------------|-------------------------------------------------------------------------------------|
+|`[String[]]`|true |named |true (ByPropertyName)|HostHeader Host CNAME ListenerPrefix ListenerPrefixes ListenerUrl|
+
+#### **Route**
+A route table for all requests.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|---------------|--------|--------|---------------------|----------------------------------------------|
+|`[IDictionary]`|false |named |true (ByPropertyName)|Routes RouteTable WebHook WebHooks|
-|Type |Required|Position|PipelineInput |Aliases |
-|-------|--------|--------|---------------------|-----------|
-|`[Uri]`|false |1 |true (ByPropertyName)|Url Uri|
+#### **HTML**
+The Default HTML.
+This will be displayed when visiting the root url.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------|--------|--------|---------------------|------------------------------------------------------------|
+|`[String]`|false |named |true (ByPropertyName)|DefaultHTML Home Index IndexHTML DefaultPage|
+
+#### **PaletteName**
+The name of the palette to use. This will include the [4bitcss](https://4bitcss.com) stylesheet.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------|--------|--------|---------------------|----------------------------------------|
+|`[String]`|false |named |true (ByPropertyName)|Palette ColorScheme ColorPalette|
+
+#### **GoogleFont**
+The [Google Font](https://fonts.google.com/) name.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------|--------|--------|---------------------|--------|
+|`[String]`|false |named |true (ByPropertyName)|FontName|
+
+#### **CodeFont**
+The Google Font name to use for code blocks.
+(this should be a [monospace font](https://fonts.google.com/?classification=Monospace))
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------|--------|--------|---------------------|----------------------------------------|
+|`[String]`|false |named |true (ByPropertyName)|PreFont CodeFontName PreFontName|
+
+#### **JavaScript**
+A list of javascript files or urls to include in the content.
+
+|Type |Required|Position|PipelineInput |
+|------------|--------|--------|---------------------|
+|`[String[]]`|false |named |true (ByPropertyName)|
+
+#### **ImportMap**
+A javascript import map. This allows you to import javascript modules.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|---------------|--------|--------|---------------------|---------------------------------------------------------------|
+|`[IDictionary]`|false |named |true (ByPropertyName)|ImportsJavaScript JavaScriptImports JavaScriptImportMap|
+
+#### **QueryParameter**
+A collection of query parameters.
+These will be appended onto the `-SocketUrl`.
+Multiple values for a single parameter will be passed as multiple parameters.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|---------------|--------|--------|---------------------|-------------------------|
+|`[IDictionary]`|false |named |true (ByPropertyName)|QueryParameters Query|
#### **Handler**
-A ScriptBlock that will handle the output of the WebSocket.
+A ScriptBlock that can handle the output of the WebSocket or the Http Request.
+This may be run in a separate `-Runspace` or `-RunspacePool`.
+The output of the WebSocket or the Context will be passed as an object.
|Type |Required|Position|PipelineInput|
|---------------|--------|--------|-------------|
|`[ScriptBlock]`|false |named |false |
+#### **ForwardEvent**
+If set, will forward websocket messages as events.
+Only events that match -Filter will be forwarded.
+
+|Type |Required|Position|PipelineInput |Aliases|
+|----------|--------|--------|---------------------|-------|
+|`[Switch]`|false |named |true (ByPropertyName)|Forward|
+
#### **Variable**
Any variables to declare in the WebSocket job.
These variables will also be added to the job as properties.
@@ -163,6 +263,13 @@ These variables will also be added to the job as properties.
|---------------|--------|--------|-------------|
|`[IDictionary]`|false |named |false |
+#### **Header**
+Any Http Headers to include in the WebSocket request or server response.
+
+|Type |Required|Position|PipelineInput|Aliases|
+|---------------|--------|--------|-------------|-------|
+|`[IDictionary]`|false |named |false |Headers|
+
#### **Name**
The name of the WebSocket job.
@@ -180,9 +287,17 @@ The script to run when the WebSocket job starts.
#### **BufferSize**
The buffer size. Defaults to 16kb.
-|Type |Required|Position|PipelineInput|
-|---------|--------|--------|-------------|
-|`[Int32]`|false |named |false |
+|Type |Required|Position|PipelineInput |
+|---------|--------|--------|---------------------|
+|`[Int32]`|false |named |true (ByPropertyName)|
+
+#### **Broadcast**
+If provided, will send an object.
+If this is a scriptblock, it will be run and the output will be sent.
+
+|Type |Required|Position|PipelineInput|Aliases|
+|------------|--------|--------|-------------|-------|
+|`[PSObject]`|false |named |false |Send |
#### **OnConnect**
The ScriptBlock to run after connection to a websocket.
@@ -213,6 +328,28 @@ The Scriptblock to run when the WebSocket job produces a warning.
|---------------|--------|--------|-------------|
|`[ScriptBlock]`|false |named |false |
+#### **Authenticate**
+If provided, will authenticate the WebSocket.
+Many websockets require an initial authentication handshake
+after an initial message is received.
+This parameter can be either a ScriptBlock or any other object.
+If it is a ScriptBlock, it will be run with the output of the WebSocket passed as the first argument.
+This will run after the socket is connected but before any messages are received.
+
+|Type |Required|Position|PipelineInput|Aliases |
+|------------|--------|--------|-------------|--------------------------|
+|`[PSObject]`|false |named |false |Authorize HelloMessage|
+
+#### **Handshake**
+If provided, will shake hands after the first websocket message is received.
+This parameter can be either a ScriptBlock or any other object.
+If it is a ScriptBlock, it will be run with the output of the WebSocket passed as the first argument.
+This will run after the socket is connected and the first message is received.
+
+|Type |Required|Position|PipelineInput|Aliases |
+|------------|--------|--------|-------------|--------|
+|`[PSObject]`|false |named |false |Identify|
+
#### **Watch**
If set, will watch the output of the WebSocket job, outputting results continuously instead of outputting a websocket job.
@@ -234,64 +371,125 @@ If set, will output the raw bytes that come out of the WebSocket.
|----------|--------|--------|-------------|---------------------------------------|
|`[Switch]`|false |named |false |RawByte RawBytes Bytes Byte|
+#### **Force**
+If set, will force a new job to be created, rather than reusing an existing job.
+
+|Type |Required|Position|PipelineInput|
+|----------|--------|--------|-------------|
+|`[Switch]`|false |named |false |
+
+#### **SubProtocol**
+The subprotocol used by the websocket. If not provided, this will default to `json`.
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[String]`|false |named |true (ByPropertyName)|
+
+#### **NoSubProtocol**
+If set, will not set a subprotocol. This will only work with certain websocket servers, but will not work with an HTTP Listener WebSocket.
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[Switch]`|false |named |true (ByPropertyName)|
+
+#### **Filter**
+One or more filters to apply to the output of the WebSocket.
+These can be strings, regexes, scriptblocks, or commands.
+If they are strings or regexes, they will be applied to the raw text.
+If they are scriptblocks, they will be applied to the deserialized JSON.
+These filters will be run within the WebSocket job.
+
+|Type |Required|Position|PipelineInput |
+|--------------|--------|--------|---------------------|
+|`[PSObject[]]`|false |named |true (ByPropertyName)|
+
#### **WatchFor**
If set, will watch the output of a WebSocket job for one or more conditions.
The conditions are the keys of the dictionary, and can be a regex, a string, or a scriptblock.
The values of the dictionary are what will happen when a match is found.
-|Type |Required|Position|PipelineInput|Aliases |
-|---------------|--------|--------|-------------|----------------------|
-|`[IDictionary]`|false |named |false |WhereFor Wherefore|
+|Type |Required|Position|PipelineInput |Aliases |
+|---------------|--------|--------|---------------------|----------------------|
+|`[IDictionary]`|false |named |true (ByPropertyName)|WhereFor Wherefore|
#### **TimeOut**
-The timeout for the WebSocket connection. If this is provided, after the timeout elapsed, the WebSocket will be closed.
+The timeout for the WebSocket connection.
+If this is provided, after the timeout elapsed, the WebSocket will be closed.
-|Type |Required|Position|PipelineInput|
-|------------|--------|--------|-------------|
-|`[TimeSpan]`|false |named |false |
+|Type |Required|Position|PipelineInput |Aliases |
+|------------|--------|--------|---------------------|--------|
+|`[TimeSpan]`|false |named |true (ByPropertyName)|Lifespan|
#### **PSTypeName**
If provided, will decorate the objects outputted from a websocket job.
This will only decorate objects converted from JSON.
-|Type |Required|Position|PipelineInput|Aliases |
-|------------|--------|--------|-------------|---------------------------------------|
-|`[String[]]`|false |named |false |PSTypeNames Decorate Decoration|
+|Type |Required|Position|PipelineInput |Aliases |
+|------------|--------|--------|---------------------|---------------------------------------|
+|`[String[]]`|false |named |true (ByPropertyName)|PSTypeNames Decorate Decoration|
#### **Maximum**
The maximum number of messages to receive before closing the WebSocket.
-|Type |Required|Position|PipelineInput|
-|---------|--------|--------|-------------|
-|`[Int64]`|false |named |false |
+|Type |Required|Position|PipelineInput |
+|---------|--------|--------|---------------------|
+|`[Int64]`|false |named |true (ByPropertyName)|
+
+#### **ThrottleLimit**
+The throttle limit used when creating background jobs.
+
+|Type |Required|Position|PipelineInput |
+|---------|--------|--------|---------------------|
+|`[Int32]`|false |named |true (ByPropertyName)|
#### **ConnectionTimeout**
The maximum time to wait for a connection to be established.
By default, this is 7 seconds.
-|Type |Required|Position|PipelineInput|
-|------------|--------|--------|-------------|
-|`[TimeSpan]`|false |named |false |
+|Type |Required|Position|PipelineInput |
+|------------|--------|--------|---------------------|
+|`[TimeSpan]`|false |named |true (ByPropertyName)|
#### **Runspace**
The Runspace where the handler should run.
Runspaces allow you to limit the scope of the handler.
-|Type |Required|Position|PipelineInput|
-|------------|--------|--------|-------------|
-|`[Runspace]`|false |named |false |
+|Type |Required|Position|PipelineInput |
+|------------|--------|--------|---------------------|
+|`[Runspace]`|false |named |true (ByPropertyName)|
#### **RunspacePool**
The RunspacePool where the handler should run.
RunspacePools allow you to limit the scope of the handler to a pool of runspaces.
-|Type |Required|Position|PipelineInput|Aliases|
-|----------------|--------|--------|-------------|-------|
-|`[RunspacePool]`|false |named |false |Pool |
+|Type |Required|Position|PipelineInput |Aliases|
+|----------------|--------|--------|---------------------|-------|
+|`[RunspacePool]`|false |named |true (ByPropertyName)|Pool |
+
+#### **IncludeTotalCount**
+
+|Type |Required|Position|PipelineInput|
+|----------|--------|--------|-------------|
+|`[Switch]`|false |named |false |
+
+#### **Skip**
+
+|Type |Required|Position|PipelineInput|
+|----------|--------|--------|-------------|
+|`[UInt64]`|false |named |false |
+
+#### **First**
+
+|Type |Required|Position|PipelineInput|
+|----------|--------|--------|-------------|
+|`[UInt64]`|false |named |false |
---
### Syntax
```PowerShell
-Get-WebSocket [[-WebSocketUri] ] [-Handler ] [-Variable ] [-Name ] [-InitializationScript ] [-BufferSize ] [-OnConnect ] [-OnError ] [-OnOutput ] [-OnWarning ] [-Watch] [-RawText] [-Binary] [-WatchFor ] [-TimeOut ] [-PSTypeName ] [-Maximum ] [-ConnectionTimeout ] [-Runspace ] [-RunspacePool ] []
+Get-WebSocket [[-SocketUrl] ] [-QueryParameter ] [-Handler ] [-ForwardEvent] [-Variable ] [-Header ] [-Name ] [-InitializationScript ] [-BufferSize ] [-Broadcast ] [-OnConnect ] [-OnError ] [-OnOutput ] [-OnWarning ] [-Authenticate ] [-Handshake ] [-Watch] [-RawText] [-Binary] [-Force] [-SubProtocol ] [-NoSubProtocol] [-Filter ] [-WatchFor ] [-TimeOut ] [-PSTypeName ] [-Maximum ] [-ThrottleLimit ] [-ConnectionTimeout ] [-Runspace ] [-RunspacePool ] [-IncludeTotalCount] [-Skip ] [-First ] []
+```
+```PowerShell
+Get-WebSocket -RootUrl [-Route ] [-HTML ] [-PaletteName ] [-GoogleFont ] [-CodeFont ] [-JavaScript ] [-ImportMap ] [-Handler ] [-Variable ] [-Header ] [-Name ] [-InitializationScript ] [-BufferSize ] [-Broadcast ] [-Force] [-TimeOut ] [-Maximum ] [-ThrottleLimit ] [-Runspace ] [-RunspacePool ] [-IncludeTotalCount] [-Skip ] [-First ] []
```
diff --git a/docs/README.md b/docs/README.md
index dce8d2f..a4c7c52 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,5 +1,9 @@
# WebSocket
@@ -46,15 +50,18 @@ To stop watching a websocket, simply stop the background job.
~~~powershell
# Create a WebSocket job that connects to a WebSocket and outputs the results.
-Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+$socketServer = Get-WebSocket -RootUrl "http://localhost:8387/" -HTML "
WebSocket Server
"
+$socketClient = Get-WebSocket -SocketUrl "ws://localhost:8387/"
+foreach ($n in 1..10) { $socketServer.Send(@{n=Get-Random}) }
+$socketClient | Receive-Job -Keep
~~~
#### Get-WebSocket Example 2
~~~powershell
# Get is the default verb, so we can just say WebSocket.
# `-Watch` will output a continous stream of objects from the websocket.
-# For example, let's Watch BlueSky, but just the text.
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+# For example, let's Watch BlueSky, but just the text
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch -Maximum 1kb |
% {
$_.commit.record.text
}
@@ -69,18 +76,28 @@ $blueSkySocketUrl = "wss://jetstream2.us-$(
"wantedCollections=app.bsky.feed.post"
) -join '&')"
websocket $blueSkySocketUrl -Watch |
- % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"} -Max 1kb
~~~
#### Get-WebSocket Example 4
~~~powershell
+# Watch continuously in a background job.
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
~~~
#### Get-WebSocket Example 5
+~~~powershell
+# Watch the first message in -Debug mode.
+# This allows you to literally debug the WebSocket messages as they are encountered.
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -Max 1 -Debug
+~~~
+ #### Get-WebSocket Example 6
+
~~~powershell
# Watch BlueSky, but just the emoji
-websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail -Max 1kb |
Foreach-Object {
$in = $_
if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
@@ -88,7 +105,7 @@ websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.f
}
}
~~~
- #### Get-WebSocket Example 6
+ #### Get-WebSocket Example 7
~~~powershell
$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
@@ -102,7 +119,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 7
+ #### Get-WebSocket Example 8
~~~powershell
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
@@ -113,17 +130,19 @@ websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.
$_.commit.record.embed.external.uri
}
~~~
- #### Get-WebSocket Example 8
+ #### Get-WebSocket Example 9
~~~powershell
# BlueSky, but just the hashtags
-websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{
+ wantedCollections = 'app.bsky.feed.post'
+} -WatchFor @{
{$webSocketoutput.commit.record.text -match "\#\w+"}={
$matches.0
}
-}
+} -Maximum 1kb
~~~
- #### Get-WebSocket Example 9
+ #### Get-WebSocket Example 10
~~~powershell
# BlueSky, but just the hashtags (as links)
@@ -137,7 +156,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 10
+ #### Get-WebSocket Example 11
~~~powershell
websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
@@ -149,7 +168,7 @@ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.
}
}
~~~
- #### Get-WebSocket Example 11
+ #### Get-WebSocket Example 12
~~~powershell
# We can decorate a type returned from a WebSocket, allowing us to add additional properties.
diff --git a/docs/WebSocket/Client/ThreadJob/README.md b/docs/WebSocket/Client/ThreadJob/README.md
new file mode 100644
index 0000000..a15b69a
--- /dev/null
+++ b/docs/WebSocket/Client/ThreadJob/README.md
@@ -0,0 +1,7 @@
+## WebSocket.Client.ThreadJob
+
+
+### Script Methods
+
+
+* [Send()](Send.md)
diff --git a/docs/WebSocket/Client/ThreadJob/Send.md b/docs/WebSocket/Client/ThreadJob/Send.md
new file mode 100644
index 0000000..c18eca5
--- /dev/null
+++ b/docs/WebSocket/Client/ThreadJob/Send.md
@@ -0,0 +1,22 @@
+WebSocket.Client.ThreadJob.Send()
+---------------------------------
+
+### Synopsis
+Sends a WebSocket message.
+
+---
+
+### Description
+
+Sends a message to a WebSocket server.
+
+---
+
+### Parameters
+#### **Message**
+
+|Type |Required|Position|PipelineInput|
+|------------|--------|--------|-------------|
+|`[PSObject]`|false |1 |false |
+
+---
diff --git a/docs/WebSocket/Server/ThreadJob/README.md b/docs/WebSocket/Server/ThreadJob/README.md
new file mode 100644
index 0000000..b98bd30
--- /dev/null
+++ b/docs/WebSocket/Server/ThreadJob/README.md
@@ -0,0 +1,7 @@
+## WebSocket.Server.ThreadJob
+
+
+### Script Methods
+
+
+* [Send()](Send.md)
diff --git a/docs/WebSocket/Server/ThreadJob/Send.md b/docs/WebSocket/Server/ThreadJob/Send.md
new file mode 100644
index 0000000..b7272b0
--- /dev/null
+++ b/docs/WebSocket/Server/ThreadJob/Send.md
@@ -0,0 +1,28 @@
+WebSocket.Server.ThreadJob.Send()
+---------------------------------
+
+### Synopsis
+Sends a WebSocket message.
+
+---
+
+### Description
+
+Sends a message from a WebSocket server.
+
+---
+
+### Parameters
+#### **Message**
+
+|Type |Required|Position|PipelineInput|
+|------------|--------|--------|-------------|
+|`[PSObject]`|false |1 |false |
+
+#### **Pattern**
+
+|Type |Required|Position|PipelineInput|
+|----------|--------|--------|-------------|
+|`[String]`|false |2 |false |
+
+---
diff --git a/docs/_data/Help/Get-WebSocket.json b/docs/_data/Help/Get-WebSocket.json
index 8d58a08..0d88615 100644
--- a/docs/_data/Help/Get-WebSocket.json
+++ b/docs/_data/Help/Get-WebSocket.json
@@ -28,60 +28,69 @@
"Outputs": [
null
],
- "Links": [],
+ "Links": [
+ "https://websocket.powershellweb.com/Get-WebSocket/",
+ "https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket?wt.mc_id=MVP_321542",
+ "https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?wt.mc_id=MVP_321542"
+ ],
"Examples": [
{
"Title": "EXAMPLE 1",
"Markdown": "Create a WebSocket job that connects to a WebSocket and outputs the results.",
- "Code": "Get-WebSocket -WebSocketUri \"wss://localhost:9669/\""
+ "Code": "$socketServer = Get-WebSocket -RootUrl \"http://localhost:8387/\" -HTML \"
WebSocket Server
\"\n$socketClient = Get-WebSocket -SocketUrl \"ws://localhost:8387/\"\nforeach ($n in 1..10) { $socketServer.Send(@{n=Get-Random}) }\n$socketClient | Receive-Job -Keep"
},
{
"Title": "EXAMPLE 2",
- "Markdown": "Get is the default verb, so we can just say WebSocket.\n`-Watch` will output a continous stream of objects from the websocket.\nFor example, let's Watch BlueSky, but just the text. ",
- "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |\n % { \n $_.commit.record.text\n }"
+ "Markdown": "Get is the default verb, so we can just say WebSocket.\n`-Watch` will output a continous stream of objects from the websocket.\nFor example, let's Watch BlueSky, but just the text ",
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch -Maximum 1kb |\n % { \n $_.commit.record.text\n }"
},
{
"Title": "EXAMPLE 3",
"Markdown": "Watch BlueSky, but just the text and spacing",
- "Code": "$blueSkySocketUrl = \"wss://jetstream2.us-$(\n 'east','west'|Get-Random\n).bsky.network/subscribe?$(@(\n \"wantedCollections=app.bsky.feed.post\"\n) -join '&')\"\nwebsocket $blueSkySocketUrl -Watch | \n % { Write-Host \"$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))\"}"
+ "Code": "$blueSkySocketUrl = \"wss://jetstream2.us-$(\n 'east','west'|Get-Random\n).bsky.network/subscribe?$(@(\n \"wantedCollections=app.bsky.feed.post\"\n) -join '&')\"\nwebsocket $blueSkySocketUrl -Watch | \n % { Write-Host \"$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))\"} -Max 1kb"
},
{
"Title": "EXAMPLE 4",
- "Markdown": "",
+ "Markdown": "Watch continuously in a background job.",
"Code": "websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post"
},
{
"Title": "EXAMPLE 5",
- "Markdown": "Watch BlueSky, but just the emoji",
- "Code": "websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |\n Foreach-Object {\n $in = $_\n if ($in.commit.record.text -match '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}]+') {\n Write-Host $matches.0 -NoNewline\n }\n }"
+ "Markdown": "Watch the first message in -Debug mode. \nThis allows you to literally debug the WebSocket messages as they are encountered.",
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{\n wantedCollections = 'app.bsky.feed.post'\n} -Max 1 -Debug"
},
{
"Title": "EXAMPLE 6",
+ "Markdown": "Watch BlueSky, but just the emoji",
+ "Code": "websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail -Max 1kb |\n Foreach-Object {\n $in = $_\n if ($in.commit.record.text -match '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}]+') {\n Write-Host $matches.0 -NoNewline\n }\n }"
+ },
+ {
+ "Title": "EXAMPLE 7",
"Markdown": "",
"Code": "$emojiPattern = '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}\\p{IsVariationSelectors}\\p{IsCombiningHalfMarks}]+)'\nwebsocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |\n Foreach-Object {\n $in = $_\n $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))\n if ($in.commit.record.text -match \"(?>(?:$emojiPattern|\\#\\w+)\") {\n $match = $matches.0 \n Write-Host $spacing,$match,$spacing -NoNewline\n }\n }"
},
{
- "Title": "EXAMPLE 7",
+ "Title": "EXAMPLE 8",
"Markdown": "",
"Code": "websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |\n Where-Object {\n $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'\n } |\n Foreach-Object {\n $_.commit.record.embed.external.uri\n }"
},
{
- "Title": "EXAMPLE 8",
+ "Title": "EXAMPLE 9",
"Markdown": "BlueSky, but just the hashtags",
- "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$webSocketoutput.commit.record.text -match \"\\#\\w+\"}={\n $matches.0\n } \n}"
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe -QueryParameter @{\n wantedCollections = 'app.bsky.feed.post'\n} -WatchFor @{\n {$webSocketoutput.commit.record.text -match \"\\#\\w+\"}={\n $matches.0\n } \n} -Maximum 1kb"
},
{
- "Title": "EXAMPLE 9",
+ "Title": "EXAMPLE 10",
"Markdown": "BlueSky, but just the hashtags (as links)",
"Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$webSocketoutput.commit.record.text -match \"\\#\\w+\"}={\n if ($psStyle.FormatHyperlink) {\n $psStyle.FormatHyperlink($matches.0, \"https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))\")\n } else {\n $matches.0\n }\n }\n}"
},
{
- "Title": "EXAMPLE 10",
+ "Title": "EXAMPLE 11",
"Markdown": "",
"Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$args.commit.record.text -match \"\\#\\w+\"}={\n $matches.0\n }\n {$args.commit.record.text -match '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}]+'}={\n $matches.0\n }\n}"
},
{
- "Title": "EXAMPLE 11",
+ "Title": "EXAMPLE 12",
"Markdown": "We can decorate a type returned from a WebSocket, allowing us to add additional properties.\nFor example, let's add a `Tags` property to the `app.bsky.feed.post` type.",
"Code": "$typeName = 'app.bsky.feed.post'\nUpdate-TypeData -TypeName $typeName -MemberName 'Tags' -MemberType ScriptProperty -Value {\n @($this.commit.record.facets.features.tag)\n} -Force\n\n# Now, let's get 10kb posts ( this should not take too long )\n$somePosts =\n websocket \"wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=$typeName\" -PSTypeName $typeName -Maximum 10kb -Watch\n$somePosts |\n ? Tags |\n Select -ExpandProperty Tags |\n Group |\n Sort Count -Descending |\n Select -First 10"
}
diff --git a/docs/_data/LastDateBuilt.json b/docs/_data/LastDateBuilt.json
index a7d8667..b0f9097 100644
--- a/docs/_data/LastDateBuilt.json
+++ b/docs/_data/LastDateBuilt.json
@@ -1 +1 @@
-"2024-12-20"
\ No newline at end of file
+"2025-03-22"
\ No newline at end of file