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

rokucommunity / roku-debug / 26120672946

19 May 2026 07:37PM UTC coverage: 70.727% (+0.7%) from 70.049%
26120672946

Pull #351

github

web-flow
Merge eb0b2e542 into 5bbd82240
Pull Request #351: 0.23.8

3328 of 5046 branches covered (65.95%)

Branch coverage included in aggregate %.

5834 of 7908 relevant lines covered (73.77%)

35.01 hits per line

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

78.07
/src/SceneGraphDebugCommandController.ts
1
import { logger } from './logging';
2
// eslint-disable-next-line
2✔
3
const Telnet = require('telnet-client');
2✔
4

2✔
5
export class SceneGraphDebugCommandController {
6
    constructor(public host: string, port?: number) {
2✔
7
        this.port = port ?? 8080;
8
    }
9

44✔
10
    private connection: typeof Telnet;
44✔
11

44✔
12
    private shellPrompt = /^>$/img;
44✔
13
    private echoLines = 0;
44✔
14
    public timeout = 5000;
44✔
15
    public execTimeout = 2000;
44✔
16
    private port;
44✔
17
    private maxBufferLength = 5242880;
18
    private logger = logger.createLogger(`[${SceneGraphDebugCommandController.name}]`);
×
19

2✔
20
    public async connect(options: { execTimeout?: number; timeout?: number } = {}) {
2✔
21
        this.removeConnection();
22

2✔
23
        try {
2✔
24
            // Make a new telnet connections object
2✔
25
            let connection = new Telnet();
26

2✔
27
            connection.on('close', () => {
28
                this.removeConnection();
29
            });
30
            const config = {
31
                host: this.host,
32
                port: this.port,
33
                shellPrompt: this.shellPrompt,
34
                echoLines: this.echoLines,
35
                timeout: this.timeout,
36
                execTimeout: this.execTimeout,
2✔
37
                maxBufferLength: this.maxBufferLength,
2✔
38
                ...options
×
39
            };
40
            this.logger.debug('Establishing telnet connection', config);
41
            await connection.connect(config);
2✔
42
            this.connection = connection;
43
        } catch (e) {
44
            throw new Error((e as Error).message);
45
        }
4✔
46
    }
47

48
    private removeConnection() {
49
        this.connection = null;
50
    }
51

52
    /**
3✔
53
     * executes the different bsprof commands used for brightscript profiling.
54
     * @param {('pause'|'resume'|'status')} option Pause, resume, or get BS profiling status.
55
     */
56
    public async bsprof(option: 'pause' | 'resume' | 'status'): Promise<SceneGraphCommandResponse> {
57
        return this.exec(`bsprof-${option}`);
58
    }
59

60
    /**
61
     * Prints the current memory and CPU utilization of a channel (RAM usage is reported in KibiBytes [KiB]). The channel manifest must include the run_as_process=1 attribute to use this command.
62
     *
4✔
63
     * If an interval is provided the device repeats the command the specified number of seconds and outputs the results to port 8085.
4✔
64
     * (Available since Roku OS 10.0)
65
     * @param {{ interval: number }} [options] logging interval in seconds. 0 will stop interval logging.
66
     */
3!
67
    public async chanperf(options?: { interval: number }): Promise<SceneGraphCommandResponse> {
68
        let command = 'chanperf';
4✔
69

70
        if (options) {
71
            // TODO: revisit this as channelId support is documented but the command does not seem to work. Device returns 'ERR: unknown arg: <channelId>'
72
            // command = options?.channelId ? `${command} ${options.channelId}` : command;
73
            command = options?.interval > -1 ? `${command} -r ${options.interval}` : command;
74
        }
1✔
75

76
        return this.exec(command);
77
    }
78

79
    /**
80
     * Clear all caches that can affect channel launch time.
81
     */
3✔
82
    public async clearLaunchCaches(): Promise<SceneGraphCommandResponse> {
3✔
83
        return this.exec('clear_launch_caches');
2✔
84
    }
85

3✔
86
    /**
3!
87
     * Displays frames-per-second and free memory on-screen. Leverage this tool to optimize your channel UI. It presents a 1-second moving average of the current frame rate.
3✔
88
     * @param {('off'|'on'|'toggle')} option
89
     */
3✔
90
    public async fpsDisplay(option: 'off' | 'on' | 'toggle'): Promise<SceneGraphCommandResponse> {
91
        let command = 'fps_display';
92

93
        if (option !== 'toggle') {
94
            command = `${command} ${option === 'on' ? 1 : 0}`;
95
        }
1✔
96

97
        let response = await this.exec(command);
98
        if (!response.error) {
99
            response.result.data = `FPS Display: ${option}`;
100
        }
101
        return response;
1✔
102
    }
103

104
    /**
105
     * Provides a snapshot of the amount of in-use and free memory on the device.
106
     */
107
    public async free(): Promise<SceneGraphCommandResponse> {
1✔
108
        return this.exec('free');
109
    }
110

111
    /**
112
     * Generate a new developer key.
113
     */
114
    public async genkey(): Promise<SceneGraphCommandResponse> {
5✔
115
        return this.exec('genkey');
5✔
116
    }
3✔
117

118

5✔
119
    /**
120
     * Displays the current set of images loaded into texture memory.
121
     */
122
    public async loadedTextures(): Promise<SceneGraphCommandResponse> {
123
        return this.exec('loaded_textures');
124
    }
1✔
125

126
    /**
127
     * Enable, disable, or checks the status of console logging of thread rendezvous.
128
     * @param {('status'|'off'|'on')} option
129
     */
130
    public async logrendezvous(option: 'status' | 'off' | 'on'): Promise<SceneGraphCommandResponse> {
131
        let command = 'logrendezvous';
132

2✔
133
        if (option !== 'status') {
134
            command = `${command} ${option}`;
135
        }
136

137
        return this.exec(command);
138
    }
139

1✔
140
    /**
141
     * Show list of all installed plugins.
142
     */
143
    public async plugins(): Promise<SceneGraphCommandResponse> {
144
        return this.exec('plugins');
145
    }
146

147
    /**
148
     * Simulate a keypress.
149
     * @param {string[]} keys A list of keys to press in sequence
150
     */
151
    public async press(keys: string[]): Promise<SceneGraphCommandResponse> {
152
        // Add 1 second per character to the max execution timeout because roku is really slow......
2✔
153
        return this.exec(`press ${keys.join(', ')}`, { execTimeout: this.execTimeout + (keys.length * 1000) });
154
    }
155

156

157
    /**
158
     * Prints a list of assets loaded into texture memory and the amount of free, used, and maximum available memory on your device, respectively.
159
     * Starting with Roku OS 9.3, the name of each bitmap is included.
160
     */
161
    public async r2d2Bitmaps(): Promise<SceneGraphCommandResponse> {
162
        return this.exec('r2d2_bitmaps');
163
    }
164

165

166
    /**
167
     * Removes the indicated channel from the local device, as well as from all devices linked to the same Roku account. For example, if a channel has a channel id of "987654_cf9a", then the following command would remove it: remove_plugin 987654_cf9a
168
     *
169
     * The list of available channel ids can be seen with the 'plugins' command. The local device must be linked to a Roku account.
170
     *
171
     * To use this command, the local device must be linked to a Roku account. Channels are not removed on another device until it synchronizes with the Roku Channel Store (for example, via an automatic check for updates).
3✔
172
     * (Available since Roku OS 10.0)
173
     *
174
     * @param {string} channelId
175
     */
176
    public async removePlugin(channelId: string): Promise<SceneGraphCommandResponse> {
177
        return this.exec(`remove_plugin ${channelId}`);
178
    }
4✔
179

180

181
    /**
182
     * Prints every existing node created by the currently running channel.
183
     * As of Roku OS 10.0, this prints the number of 'osref' references to the node (held in the Roku platform) and 'bscref' references (held in the channel application).
184
     * The 'bcsref' count includes references from "m." variable and local variables. Child references and field references do not increase 'bscref' counts.
1✔
185
     *
186
     * The 'osref' count also includes child references and references from Roku SceneGraph interface fields. For example, for any node with a parent, the parent will count as one 'osref' on the child.
187
     * Additionally, any field of type 'node', 'nodearray', or 'assocarray' will add one 'osref' to each node referenced from within that field.
188
     * These could be in variables local to a function, arrays, or associative arrays, including a component global m or an associative array field of a node.
189
     *
190
     * The reported 'osref' count may vary from release to release of Roku OS; the information here is provided only to give a sense of the kinds of items that the count includes.
191
     * The 'bscref' count provides a more relevant and accurate indication of the resources that the channel itself controls.
192
     *
1✔
193
     * The sgnodes all, sgnodes roots, and sgnodes node_ID commands are similar to the getAll() , getRoots() , getRootsMeta(), and getAllMeta() ifSGNodeChildren methods, which can be called on any SceneGraph node.
194
     *
195
     * @param {string} id This can be 'all', 'roots', or the id of node(s) in your channel.
196
     */
197
    public async sgnodes(id: string): Promise<SceneGraphCommandResponse> {
198
        return this.exec(`sgnodes ${id}`);
199
    }
×
200

201

202
    /**
203
     * Provides basic node operation performance metrics. This command tracks all node operations by a thread, whether it's being created or an operation on an existing node, and whether it involves a rendezvous.
204
     * @param {('start'|'clear'|'report'|'stop')} action start - enables counting, clear - resets counters to zero, report - prints current counts with rendezvous as a percentage, stop - disables counting.
205
     */
206
    public async sgperf(action: 'start' | 'clear' | 'report' | 'stop'): Promise<SceneGraphCommandResponse> {
207
        return this.exec(`sgperf ${action}`);
208
    }
2✔
209

2✔
210
    /**
2✔
211
     * Show the current developer key
212
     */
2✔
213
    public async showkey(): Promise<SceneGraphCommandResponse> {
2!
214
        return this.exec('showkey');
2✔
215
    }
2✔
216

2✔
217
    /**
218
     * Send a literal text sequence.
219
     * @param text string to be sent to the device.
2✔
220
     */
221
    public async type(text: string): Promise<SceneGraphCommandResponse> {
222
        // Add 1 second per character to the max execution timeout because roku is really slow......
223
        return this.exec(`type ${text}`, { execTimeout: this.execTimeout + (text.length * 1000) });
2!
224
    }
×
225

×
226

×
227
    /**
228
     * Changes the number of brightscript warnings displayed on application install.
229
     * @param warningLimit maximum number of warnings to show
×
230
     */
231
    public async brightscriptWarnings(warningLimit: number): Promise<SceneGraphCommandResponse> {
232
        return this.exec(`brightscript_warnings ${warningLimit ?? 100}`);
233
    }
2!
234

2✔
235

2✔
236
    /**
237
     * Send any custom command to the SceneGraph debug server.
238
     *
2✔
239
     * If this command is called and there is no active connection with the device we will attempt to connect.
240
     * In this case once the command has been executed we will then close the connection.
241
     * @param {string} command command to be run.
242
     */
243
    public async exec(command: string, options: { execTimeout?: number; timeout?: number } = {}): Promise<SceneGraphCommandResponse> {
244
        let response = this.getBlankResponseObject(command);
12!
245
        this.logger.log(`Running SceneGraphDebugger command`, { command });
×
246

×
247
        // Set up a short lived connection if a long lived one has not beed started
×
248
        let closeConnectionAfterCommand = !this.connection;
249
        if (closeConnectionAfterCommand) {
×
250
            this.logger.trace('Opening new connection');
251
            try {
252
                await this.connect(options);
×
253
            } catch (error) {
254
                response.error = error;
×
255
            }
256
        }
257

×
258
        // Send the commend if we have a connection
×
259
        if (this.connection) {
260
            try {
261
                response.result.rawResponse = await this.connection.exec(command, options);
262
                this.logger.debug('Command complete', { command });
263
            } catch (error) {
264
                response.error = error;
265
            }
266
        }
267

2✔
268
        // Close the connection if we opened a short lived one
269
        if (closeConnectionAfterCommand) {
270
            this.logger.trace('Closing connection');
271
            await this.end();
272
        }
273

274
        // Tada! Results.
275
        return response;
2✔
276
    }
277

278

279
    /**
280
     * Closes the socket connection to the device
281
     */
282
    public async end() {
283
        if (this.connection) {
284
            this.connection.removeListener('close', this.removeConnection);
285
            try {
286
                try {
287
                    // Asking the host to close is much faster then running our own connections destroy
288
                    await this.connection.exec('quit', { shellPrompt: 'Quit command received, exiting.' });
289
                } catch (error) {
290
                    this.logger.error(`There was a problem quitting the SceneGraphDebugCommand connection`, error);
291
                }
292
                this.removeConnection();
293
            } catch (error) {
294
                this.removeConnection();
295
                console.log(error, this.connection);
296
            }
297
        }
298
    }
299

300
    /**
301
     * Returns a simple starting object used for responses
302
     * @private
303
     */
304
    private getBlankResponseObject(command: string): SceneGraphCommandResponse {
305
        return {
306
            command: command,
307
            result: {
308
                rawResponse: ''
309
            }
310
        };
311
    }
312
}
313

314
export interface SceneGraphCommandResponse<T = undefined> {
315
    command: string;
316
    error?: SceneGraphCommandError<T>;
317
    result: {
318
        rawResponse: string;
319
        data?: any;
320
    };
321
}
322

323
interface SceneGraphCommandError<T = undefined> {
324
    message: string;
325
    type: 'socket' | 'device';
326
    data?: T;
327
}
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