Commands/Protocols/UDP-Protocol.ps1


function Protocol.UDP {

<#
.SYNOPSIS
    UDP protocol
.DESCRIPTION
    Converts a UDP protocol command to PowerShell.
.EXAMPLE
    # Creates the code to create a UDP Client
    {udp://127.0.0.1:8568} | Use-PipeScript
.EXAMPLE
    # Creates the code to broadast a message.
    {udp:// -Host [ipaddress]::broadcast -port 911 -Send "It's an emergency!"} | Use-PipeScript
.EXAMPLE
    {send udp:// -Host [ipaddress]::broadcast -Port 911 "It's an emergency!"} | Use-PipeScript
.EXAMPLE
    Use-PipeScript {
        watch udp://*:911
    
        send udp:// -Host [ipaddress]::broadcast -Port 911 "It's an emergency!"

        receive udp://*:911
    }
#>

[ValidateScript({
    $commandAst = $_    
    if ($commandAst -isnot [Management.Automation.Language.CommandAst]) { return $false }
    if ($commandAst.CommandElements[0..1] -match '^udp://' -ne $null) {
        if ($commandAst.CommandElements[0] -notmatch '^udp://') {
            if ($commandAst.CommandElements[0].value -notin 'send', 'receive', 'watch') {
                return $false
            }
        }        
        return $true
    }

    return $false
})]
[Alias('UDP','UDP://')]
param(
# The URI.
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='Protocol',Position=0)]
[Parameter(ValueFromPipeline,ParameterSetName='Interactive',Position=0)]
[Parameter(Mandatory,ParameterSetName='ScriptBlock',Position=0)]
[uri]
$CommandUri,

[ValidateSet('Send','Receive','Watch')]
[Parameter(Position=1)]
[string]
$Method,

# The Command's Abstract Syntax Tree
[Parameter(Mandatory,ParameterSetName='Protocol')]
[Management.Automation.Language.CommandAST]
$CommandAst,

# If the UDP protocol is used as an attribute, this will be the existing script block.
[Parameter(Mandatory,ParameterSetName='ScriptBlock')]
[ScriptBlock]
$ScriptBlock = {},

# The script or message sent via UDP.
$Send,

# If set, will receive result events.
[switch]
$Receive,

# A ScriptBlock used to watch the UDP socket.
[scriptblock]
$Watch,

# The host name. This can be provided via parameter if it is not present in the URI.
[Alias('Host')]
[string]
$HostName,

# The port. This can be provided via parameter if it is not present in the URI.
[int]
$Port,

# Any remaining arguments.
[Parameter(ValueFromRemainingArguments)]
[PSObject[]]
$ArgumentList
)

process {
    $commandParameters = 
        if ($PSCmdlet.ParameterSetName -eq 'Protocol') {
            [Ordered]@{} + $CommandAst.Parameter
        }
        else {
            [Ordered]@{} + $PSBoundParameters
        }
        
    $commandArguments  = 
        if ($PSCmdlet.ParameterSetName -eq 'Protocol') {
            @() + $CommandAst.ArgumentList
        }
        else {
            @() + $args
        }

    if (-not $CommandUri.Scheme) {
        $commandUri = [uri]"udp://$($commandUri.OriginalString -replace '://')"
    }
        
    $methodName  = $Method    

    $udpIP, $udpPort = $null, $null
    $constructorArgs = 
        
        @(
        if ($CommandUri.Host) {
            $udpIP = $commandUri.Host
            $commandUri.Host
            if ($commandUri.Port) {
                $udpPort = $CommandUri.Port
                $commandUri.Port
            }
        } 
        elseif ($commandParameters.Port -and -not ($commandParameters.Host -or $commandParameters.HostName)) {
            $udpPort = $commandParameters.Port
            $commandParameters.Port
        } 
        elseif (($commandParameters.Host -or $commandParameters.HostName) -and $commandParameters.Port) {
            $udpIP   = if ($commandParameters.Host) {
                $commandParameters.Host
            } else {
                $commandParameters.HostName
            }
            $udpIP
            $udpPort = $commandParameters.Port 
            $udpPort 
        })

    # We will always need to construct a client
    # so prepare that code now that we know the host and port.
    $constructUdp    = "new Net.Sockets.UdpClient $constructorArgs"

    
    if ($udpIP -is [string] -and $udpIP -notmatch '^\[') {            
        $udpIP = "'$($udpIP -replace "'", "''")'"
    }

    $UdpOperationScript = 

    # If the method name is send or -Send was provided
    if ($methodName -eq 'send' -or $commandParameters.Send) {
        # ensure we have both and IP and a port
        if (-not $udpIP -or -not $udpPort) {
            Write-Error "Must provide both IP and port to send"
            return
        }

        # If we don't have a -Send parameter, try to bind positionally.
        if (-not $commandParameters.Send -and $commandArguments -and $commandArguments[0]) {
            $commandParameters.Send = $commandArguments[0]
        } elseif (-not $commandParameters.Send -and $argumentList -and $ArgumentList[0]) {
            $commandParameters.Send = $ArgumentList[0]
        }

        # If we still don't have a -Send parameter, error out.
        if (-not $commandParameters.Send) {            
            Write-Verbose "Nothing to $(if ($methodName -ne 'Send') {'-'} else {"Send"})"
            return
        }

        # prepare send to be embedded
        $embedSend = 
            if ($commandParameters.Send -is [ScriptBlock]) {
                "& {$($commandParameters.Send)}"
            } elseif ($commandParameters.Send -is [string]) {
                "[text.Encoding]::utf8.GetBytes('$($commandParameters.Send -replace "'","''")')"
            }
            elseif ($commandParameters.Send -as [byte[]]) {
                "[Convert]::FromBase64String('$([Convert]::ToBase64String($commandParameters.Send))')"
            }
            else {
                $commandParameters.Send
            }
        # Create a UDP client and send the datagram.
[ScriptBlock]::Create("
`$udpClient = $constructUDP
`$datagram = @($embedSend) -as [byte[]]
`$bytesSent = `$udpClient.Send(`$datagram, `$datagram.Length)
"
).Transpile()        
    }
    # If the method name is Watch or -Watch was passed, we'll want to watch for results
    elseif ($methodName -eq 'Watch' -or $commandParameters.Watch) 
    {
        # If -Watch was not passed, try to bind it positionally.
        if (-not $commandParameters.Watch) {
            $commandParameters.Watch = $commandArguments[0]
        }

        # If -Watch was passed and was not a [ScriptBlock], unset it.
        if ($commandParameters.Watch -and $commandParameters.Watch -isnot [ScriptBlock]) {
            $commandParameters.Watch = $null            
        }

        # If -Watch was not provided, default it to creating an event.
        if (-not $commandParameters.Watch) {
            $commandParameters.Watch = {    
    New-Event "$udpEventName" -MessageData $datagram
            }
        }
        
        # If we do not have an IP and port, we cannot watch.
        if (-not $udpIP -or -not $udpPort) {
            Write-Error "Must provide both IP and port to Watch"
            return
        }

        # Receiving UDP results must be run in a background job.
        # Thus we start by creating that script.

        $jobScript = 
[ScriptBlock]::Create(@"
`$udpClient = [Net.Sockets.UdpClient]::new()
`$udpEndpoint = [Net.IpEndpoint]::new(
    ([ipaddress]$udpIP), $udpPort
)
`$udpEventName = "udp://`$udpEndPoint"
Register-EngineEvent -SourceIdentifier `$udpEventName -Forward
`$udpClientBound = `$false
try {
    `$udpClient.Client.Bind(`$udpEndpoint)
    `$udpClientBound = `$true
} catch {
    Write-Error -Message `$_.Message -Exception `$_.Exception
}
:udpReceive while (`$udpClientBound) {
    `$datagram = `$udpClient.Receive([ref]`$udpEndpoint)
    `$watchOutput = & {
        $($commandParameters.Watch)
    } `$datagram

    if (`$watchOutput -isnot [Management.Automation.PSEvent]) {
        New-Event -SourceIdentifier `$udpEventName -MessageData `$watchOutput
    }
}
`$udpClient.Close()
"@
).Transpile()
        # Now we construct the job name.
        $jobName = "'${udpIP}:${udpPort}'" -replace "['`"]"
        # Then we set the jobCommand.
        # It would be nice to be able to use Start-ThreadJob, but Start-ThreadJob will not forward events.
        # (also, Starting a ThreadJob and then doing a blocking call on that thread makes a job that cannot be stopped)
        $jobCommand = "Start-Job "

        $outputScript = 
[ScriptBlock]::create(@"

`$jobName = '$jobName'
`$jobScript = {
    $jobScript
}
`$JobExists = Get-Job | Where-Object Name -eq `$JobName
if ((-not `$JobExists) -or (`$jobExists.State -ne 'Running')) {
    $(
        if (-not $CommandAst.IsAssigned) {
            '$null = '
        }
    )$($jobCommand + '-Name "$jobName" -ScriptBlock $jobScript')
}
"@
)

        # If the command is assigned, wrap it in $() so that only one thing is returned.
        if ($CommandAst.IsAssigned) {
            [ScriptBlock]::create("`$($outputScript)")
        } else {
            $outputScript
        }        
    }
    elseif ($methodName -eq 'Receive' -or $commandParameters.Receive) {
        $jobName = "${udpIP}:${udpPort}" -replace "['`"]"
        $eventName = "udp://${udpIP}:${udpPort}" -replace "['`"]"        
        [scriptblock]::Create("
`$(
`$udpEvents =
    @(Get-Event -SourceIdentifier '$eventName' -ErrorAction Ignore)
[Array]::Reverse(`$udpEvents)
`$udpEvents
)
"
)
        
    }
    else {
        [scriptblock]::Create($constructUdp).Transpile()
    }
    
    switch ($PSCmdlet.ParameterSetName) {
        Protocol {
            $UdpOperationScript
        }
        ScriptBlock {
            # join the existing script with the schema information
            Join-PipeScript -ScriptBlock $ScriptBlock, $UdpOperationScript
        }
        Interactive {
            . $UdpOperationScript
        }
    }    
}



}