Functions/StreamDeck/Watch-StreamDeck.ps1
function Watch-StreamDeck { <# .SYNOPSIS Watches StreamDeck .DESCRIPTION Watches StreamDeck for events. This function provides the backbone of a StreamDeck plugin written in PowerShell. Watch-StreamDeck should not be called directly, unless you are testing a plugin. .EXAMPLE # This will start watching the plugin with arguments passed by StreamDeck Watch-StreamDeck -StreamDeckInfo $args .LINK Send-StreamDeck .LINK Receive-StreamDeck #> param( # The StreamDeck Information. # This will be the JSON object initially passed in. [Parameter(ValueFromPipeline)] [PSObject] $StreamDeckInfo, # The path containing event handlers. By default, the current directory. [string] $HandlerPath = $pwd, # If set, will receive events from StreamDeck in a background job. # This allows Watch-StreamDeck to not block and still mostly work as expected. # The main runspace will not be able to send data back to the StreamDeck. [switch] $AsJob, # The log path. # If no log path is provided, one will be created in the same directory as this script. [string] $LogPath ) begin { $allArgs = @() } process { if ($StreamDeckInfo -is [string]) { $allArgs += $StreamDeckInfo } } end { # If we have not been provided with a -LogPath if (-not $LogPath) { # use a global variable if already declared if ($global:STREAMDECK_PLUGINLOGPATH) { $LogPath = $global:STREAMDECK_PLUGINLOGPATH } else { # Set up a log path for this plugin instance (make it based off of the starttime) $global:STREAMDECK_PLUGINLOGPATH = $logPath = Join-Path $psScriptRoot ( ([Datetime]::Now.ToString('o').replace(':','.') -replace '\.\d+(?=[+-])') + '.log' ) # Clear older logs (only keep the last 10 executions around) Get-ChildItem -Path $psScriptRoot -Filter *.log | Sort-Object LastWriteTime -Descending | Select-Object -Skip 10 | Remove-Item "Log Started @ $([DateTime]::Now.ToString('s')). Running under process ID $($pid)" | Add-Content -Path $logPath } } if ($StreamDeckInfo -is [Object[]]) { $allArgs = $StreamDeckInfo } # If the stream deck info is an array, it is an array of arguments if ($allArgs) {# Put each named argument into a dictionary. $argObject = [Ordered]@{} for ($i = 0; $i -lt $allArgs.Length; $i+=2 ) # We do this by going in twos thru the arguments { $k = $allArgs[$i].TrimStart('-') # removing the - from the key $v = $allArgs[$i + 1] $argObject[$k] = if ("$v".StartsWith('{')) { # and converting any JSON-like input. $v | ConvertFrom-Json } else { $v } } $StreamDeckInfo = [PSCustomObject]$argObject } # We will want to declare a few globals to keep track of state. if (-not $Global:STREAMDECK_DEVICES) { $Global:STREAMDECK_DEVICES = @{} } if (-not $Global:STREAMDECK_BUTTONS) { $Global:STREAMDECK_BUTTONS = @{} } if (-not $global:STREAMDECK_SETTINGS) { $global:STREAMDECK_SETTINGS = @{} } # Next, we'll want to wire up all of the files in the handler path. $localFiles = Get-ChildItem -Path $HandlerPath -Filter *.ps1 # Walk over each file we find foreach ($file in $localFiles) { # If it does not look like a .handler.ps1 or is not like On_*.ps1 if ($file.Name -notlike '*.handler.ps1' -and $file.Name -notlike 'On_*.ps1' -and $file.Name -notmatch '@') { continue # keep moving. } # Remove the naming hints from the file to get the source identifier $sourceIdentifier = $file.Name -replace '^On_' -replace '\.ps1$' -replace '\.handler$' -replace '@', '.' # and then break the source identifier into chunks. $sourceIdentifierParts = @($sourceIdentifier -split '\.') # If we only have two chunks and the first chunk is not 'StreamDeck' if (($sourceIdentifierParts -ne '').Length -le 2 -and $sourceIdentifierParts[0] -ne 'StreamDeck') { # then we need to include the plugin UUID in the source identifier $sourceIdentifierParts = @($StreamDeckInfo.info.plugin.uuid) + $sourceIdentifierParts $sourceIdentifier = $sourceIdentifierParts -ne '' -join '.' } # Get the list of event names. $eventNames = # multiple events can be separated with a plus sign or a comma @(if ($sourceIdentifierParts[-1] -match '[\,\+]') { $sourceIdentifierParts[-1] -split '[\,\+]' -ne '' } else { $sourceIdentifierParts[-1] }) # If there was not an event name, register for all possible events. if (-not $eventNames) { $eventNames = 'KeyDown', 'KeyUp', 'WillAppear', 'WillDisappear', 'DeviceDidConnect', 'DeviceDidDisconnect', 'DidReceiveSettings', 'DidReceiveGlobalSettings', 'TitleParametersDidChange', 'ApplicationDidLaunch', 'ApplicationDidTerminate', 'SystemDidWakeUp', 'PropertyInspectorDidAppear', 'PropertyInspectorDidDisappear', 'SendToPlugin' } # Walk over each of the event names foreach ($eventName in $eventNames) { # and get the full source identifier for each event. $sourceIdentifier = @( @( $sourceIdentifierParts[0..$($sourceIdentifierParts.Length - 2)] -notmatch '^\.$' -ne '' ) + $eventName ) -join '.' # Log what we're about to do. Add-Content -Path $logPath -value "Registering Handler for '$sourceIdentifier': $($file.fullname)" $actionScriptBlock = [ScriptBlock]::Create(@" try { . '$($file.Fullname)' } catch { `$err = `$_ `$errorString = `$err | Out-String `$errorString | Add-Content -Path `$global:STREAMDECK_PLUGINLOGPATH if (-not `$IsLinux -or `$IsMac) { Start-Job -ScriptBlock { (New-Object -ComObject WScript.Shell).Popup(`"`$args`",0,`"`StreamDeck - ProcessID: `$pid`", 16) } -ArgumentList `$errorString } } "@) # And register each handler. Register-EngineEvent -SourceIdentifier $sourceIdentifier -Action $actionScriptBlock } } # Now, set up a heartbeat (every 10 minutes) $heartbeatTimer = [Timers.Timer]::new() $heartbeatTimer.Interval = [Timespan]::FromMinutes(10).totalmilliseconds $heartbeatTimer.AutoReset = $true $heartbeatTimer.Start() Register-ObjectEvent -InputObject $heartbeatTimer -EventName Elapsed -Action { "Heartbeat @ $([DateTime]::Now.ToString('s'))" | Add-Content -Path $global:STREAMDECK_PLUGINLOGPATH } if ($AsJob) { $StreamDeckInfo | Receive-StreamDeck -AsJob -OutputType Message return } $Host.UI.RawUI.WindowTitle = "StreamDeck $pid" # And now begin monitoring the StreamDeck do { "Starting Watching Streamdeck @ $([DateTime]::Now.ToString('s'))" | Add-Content -Path $logPath $StreamDeckInfo | Receive-StreamDeck -OutputType Message 2>&1 | Foreach-Object { if ($Global:STREAMDECK_WEBSOCKET.State -in 'Aborted' ,'CloseReceived') { $_ | Out-String | Add-Content -Path $logPath break } $_ | Out-String | Add-Content -Path $logPath } if ($Global:STREAMDECK_WEBSOCKET.State -in 'Aborted' ,'CloseReceived') { break } $sleepTime = (Get-Random -Minimum 30 -Maximum 180) "Finished Watching Streamdeck @ $([DateTime]::Now.ToString('s')). Trying again in $($sleepTime)" | Add-Content -Path $logPath Start-Sleep -Seconds $sleepTime } while ($true) } } |