• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / vscode-brightscript-language / #2719

24 Aug 2022 12:51PM UTC coverage: 41.691% (+0.02%) from 41.675%
#2719

push

TwitchBronBron
2.35.0

477 of 1427 branches covered (33.43%)

Branch coverage included in aggregate %.

1126 of 2418 relevant lines covered (46.57%)

7.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.08
/src/ActiveDeviceManager.ts
1
import * as backoff from 'backoff';
1✔
2
import { EventEmitter } from 'events';
1✔
3
import * as xmlParser from 'fast-xml-parser';
1✔
4
import * as http from 'http';
1✔
5
import * as NodeCache from 'node-cache';
1✔
6
import type { SsdpHeaders } from 'node-ssdp';
7
import { Client } from 'node-ssdp';
1✔
8
import { URL } from 'url';
1✔
9
import { util } from './util';
1✔
10
import * as vscode from 'vscode';
1✔
11

12
const DEFAULT_TIMEOUT = 10000;
1✔
13

14
export class ActiveDeviceManager extends EventEmitter {
1✔
15

16
    constructor() {
17
        super();
7✔
18
        this.isRunning = false;
7✔
19
        this.firstRequestForDevices = true;
7✔
20

21
        let config: any = vscode.workspace.getConfiguration('brightscript') || {};
7!
22
        this.enabled = config.deviceDiscovery?.enabled;
7!
23
        this.showInfoMessages = config.deviceDiscovery?.showInfoMessages;
7!
24
        vscode.workspace.onDidChangeConfiguration((e) => {
7✔
25
            let config: any = vscode.workspace.getConfiguration('brightscript') || {};
×
26
            this.enabled = config.deviceDiscovery?.enabled;
×
27
            this.showInfoMessages = config.deviceDiscovery?.showInfoMessages;
×
28
            this.processEnabledState();
×
29
        });
30

31
        this.deviceCache = new NodeCache({ stdTTL: 3600, checkperiod: 120 });
7✔
32
        //anytime a device leaves the cache (either expired or manually deleted)
33
        this.deviceCache.on('del', (deviceId, device) => {
7✔
34
            this.emit('expiredDevice', deviceId, device);
×
35
        });
36
        this.processEnabledState();
7✔
37
    }
38

39
    public firstRequestForDevices: boolean;
40
    public lastUsedDevice: string;
41
    private enabled: boolean;
42
    private showInfoMessages: boolean;
43
    private deviceCache: NodeCache;
44
    private exponentialBackoff: any;
45
    private isRunning: boolean;
46

47
    // Returns an object will all the active devices by device id
48
    public getActiveDevices() {
49
        this.firstRequestForDevices = false;
×
50
        return this.deviceCache.mget(this.deviceCache.keys());
×
51
    }
52

53
    // Returns the device cache statistics.
54
    public getCacheStats() {
55
        return this.deviceCache.getStats();
×
56
    }
57

58
    /**
59
     * Clear the list and re-scan the whole network for devices
60
     */
61
    public refresh() {
62
        this.stop();
×
63
        this.start();
×
64
    }
65

66
    // Will ether stop or start the watching process based on the running state and user settings
67
    private processEnabledState() {
68
        if (this.enabled && !this.isRunning) {
7!
69
            this.start();
×
70
        } else if (!this.enabled && this.isRunning) {
7!
71
            this.stop();
×
72
        }
73
    }
74

75
    private stop() {
76
        if (this.exponentialBackoff) {
×
77
            this.exponentialBackoff.reset();
×
78
        }
79

80
        this.deviceCache.del(
×
81
            this.deviceCache.keys()
82
        );
83
        this.deviceCache.flushAll();
×
84
        this.isRunning = false;
×
85
    }
86

87
    /**
88
     * Begin searching and watching for devices
89
     */
90
    private start() {
91
        if (!this.isRunning) {
×
92
            this.exponentialBackoff = backoff.exponential({
×
93
                randomisationFactor: 0,
94
                initialDelay: 2000,
95
                maxDelay: 60000
96
            });
97

98
            void this.discoverAll(1000);
×
99

100
            this.exponentialBackoff.on('ready', (eventNumber, delay) => {
×
101
                void this.discoverAll(delay);
×
102
                this.exponentialBackoff.backoff();
×
103
            });
104

105
            this.exponentialBackoff.backoff();
×
106
            this.isRunning = true;
×
107
        }
108
    }
109

110
    // Discover all Roku devices on the network and watch for new ones that connect
111
    private discoverAll(timeout: number = DEFAULT_TIMEOUT): Promise<string[]> {
×
112
        return new Promise((resolve, reject) => {
×
113
            const finder = new RokuFinder();
×
114
            const devices: string[] = [];
×
115

116
            finder.on('found', (device: RokuDeviceDetails) => {
×
117
                if (!devices.includes(device.id)) {
×
118
                    if (this.showInfoMessages && this.deviceCache.get(device.id) === undefined) {
×
119
                        // New device found
120
                        void vscode.window.showInformationMessage(`Device found: ${device.deviceInfo['default-device-name']}`);
×
121
                    }
122
                    this.deviceCache.set(device.id, device);
×
123
                    devices.push(device.id);
×
124
                    this.emit('foundDevice', device.id, device);
×
125
                }
126
            });
127

128
            finder.on('timeout', () => {
×
129
                if (devices.length === 0) {
×
130
                    console.info(`Could not find any Roku devices after ${timeout / 1000} seconds`);
×
131
                }
132
                resolve(devices);
×
133
            });
134

135
            finder.start(timeout);
×
136
        });
137
    }
138
}
139

140
class RokuFinder extends EventEmitter {
141

142
    constructor() {
143
        super();
×
144

145
        this.client = new Client({
×
146
            //Bind sockets to each discovered interface explicitly instead of relying on the system. Might help with issues with multiple NICs.
147
            explicitSocketBind: true
148
        });
149

150
        this.client.on('response', (headers: SsdpHeaders) => {
×
151
            if (!this.running) {
×
152
                return;
×
153
            }
154

155
            const { ST, LOCATION } = headers;
×
156
            if (ST && LOCATION && ST.includes('roku')) {
×
157
                http.get(`${LOCATION}/query/device-info`, {
×
158
                    headers: {
159
                        'User-Agent': 'https://github.com/RokuCommunity/vscode-brightscript-language'
160
                    }
161
                }, (resp) => {
162
                    // Get the device info
163
                    let data = '';
×
164

165
                    resp.on('data', (chunk) => {
×
166
                        // A chunk of data has been received.
167
                        data += chunk;
×
168
                    });
169

170
                    resp.on('end', () => {
×
171
                        // The whole response has been received.
172
                        let info = xmlParser.parse(data);
×
173
                        for (const key in info['device-info']) {
×
174
                            let value = info['device-info'][key];
×
175
                            if (typeof value === 'string') {
×
176
                                // Clean up the string results to make them more readable
177
                                info['device-info'][key] = util.decodeHtmlEntities(value);
×
178
                            }
179
                        }
180

181
                        let config: any = vscode.workspace.getConfiguration('brightscript') || {};
×
182
                        let includeNonDeveloperDevices = config?.deviceDiscovery?.includeNonDeveloperDevices === true;
×
183
                        if (includeNonDeveloperDevices || info['device-info']['developer-enabled']) {
×
184
                            const url = new URL(LOCATION);
×
185
                            const device: RokuDeviceDetails = {
×
186
                                location: url.origin,
187
                                ip: url.hostname,
188
                                id: info['device-info']['device-id'],
189
                                deviceInfo: info['device-info']
190
                            };
191
                            this.emit('found', device);
×
192
                        }
193
                    });
194
                });
195
            }
196
        });
197
    }
198

199
    private readonly client: Client;
200
    private intervalId: NodeJS.Timer | null = null;
×
201
    private timeoutId: NodeJS.Timer | null = null;
×
202
    private running = false;
×
203

204
    public start(timeout: number) {
205
        this.running = true;
×
206

207
        const search = () => {
×
208
            void this.client.search('roku:ecp');
×
209
        };
210

211
        const done = () => {
×
212
            this.stop();
×
213
            this.emit('timeout');
×
214
        };
215

216
        search();
×
217
        this.intervalId = setInterval(search, 1000);
×
218
        this.timeoutId = setTimeout(done, timeout);
×
219
    }
220

221
    public stop() {
222
        clearInterval(this.intervalId);
×
223
        clearTimeout(this.timeoutId);
×
224
        this.running = false;
×
225
        this.client.stop();
×
226
    }
227
}
228

229
export interface RokuDeviceDetails {
230
    location: string;
231
    id: string;
232
    ip: string;
233
    deviceInfo: {
234
        'udn'?: string;
235
        'serial-number'?: string;
236
        'device-id'?: string;
237
        'advertising-id'?: string;
238
        'vendor-name'?: string;
239
        'model-name'?: string;
240
        'model-number'?: string;
241
        'model-region'?: string;
242
        'is-tv'?: boolean;
243
        'is-stick'?: boolean;
244
        'ui-resolution'?: string;
245
        'supports-ethernet'?: boolean;
246
        'wifi-mac'?: string;
247
        'wifi-driver'?: string;
248
        'has-wifi-extender'?: boolean;
249
        'has-wifi-5G-support'?: boolean;
250
        'can-use-wifi-extender'?: boolean;
251
        'ethernet-mac'?: string;
252
        'network-type'?: string;
253
        'network-name'?: string;
254
        'friendly-device-name'?: string;
255
        'friendly-model-name'?: string;
256
        'default-device-name'?: string;
257
        'user-device-name'?: string;
258
        'user-device-location'?: string;
259
        'build-number'?: string;
260
        'software-version'?: string;
261
        'software-build'?: number;
262
        'secure-device'?: boolean;
263
        'language'?: string;
264
        'country'?: string;
265
        'locale'?: string;
266
        'time-zone-auto'?: boolean;
267
        'time-zone'?: string;
268
        'time-zone-name'?: string;
269
        'time-zone-tz'?: string;
270
        'time-zone-offset'?: number;
271
        'clock-format'?: string;
272
        'uptime'?: number;
273
        'power-mode'?: string;
274
        'supports-suspend'?: boolean;
275
        'supports-find-remote'?: boolean;
276
        'find-remote-is-possible'?: boolean;
277
        'supports-audio-guide'?: boolean;
278
        'supports-rva'?: boolean;
279
        'developer-enabled'?: boolean;
280
        'keyed-developer-id'?: string;
281
        'search-enabled'?: boolean;
282
        'search-channels-enabled'?: boolean;
283
        'voice-search-enabled'?: boolean;
284
        'notifications-enabled'?: boolean;
285
        'notifications-first-use'?: boolean;
286
        'supports-private-listening'?: boolean;
287
        'headphones-connected'?: boolean;
288
        'supports-audio-settings'?: boolean;
289
        'supports-ecs-textedit'?: boolean;
290
        'supports-ecs-microphone'?: boolean;
291
        'supports-wake-on-wlan'?: boolean;
292
        'supports-airplay'?: boolean;
293
        'has-play-on-roku'?: boolean;
294
        'has-mobile-screensaver'?: boolean;
295
        'support-url'?: string;
296
        'grandcentral-version'?: string;
297
        'trc-version'?: number;
298
        'trc-channel-version'?: string;
299
        'davinci-version'?: string;
300
        'av-sync-calibration-enabled'?: number;
301
        // Anything nre they might add that we do not know about
302
        [key: string]: any;
303
    };
304
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc