Functions/StreamDeck/Watch-StreamDeck.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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)
    }
}