Skip to content

Commit

Permalink
Add CLI selection of multiple debug targets (#46627)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #46627

IMPORTANT: Requires a Metro bump, CI will fail until updated.

Introduces a target selection API for launching React Native DevTools when more than one device is connected.

Credit to robhogan for the initial internal implementation of `OpenDebuggerKeyboardHandler`!

(This leverages recent additions to Metro's reporter API — which we should follow up on to use for the rest of `community-cli-plugin`. Notably, using `TerminalReporter` ensures server output won't conflict with Metro's own event and progress logs.)

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D63255295

fbshipit-source-id: da93500358791eabe4cab433cad31b82d518fb5f
  • Loading branch information
huntie authored and facebook-github-bot committed Oct 1, 2024
1 parent 7c2a7de commit aa55d76
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

import type TerminalReporter from 'metro/src/lib/TerminalReporter';

import chalk from 'chalk';
import fetch from 'node-fetch';

type PageDescription = $ReadOnly<{
id: string,
title: string,
description: string,
deviceName: string,
...
}>;

export default class OpenDebuggerKeyboardHandler {
#devServerUrl: string;
#reporter: TerminalReporter;
#targetsShownForSelection: ?$ReadOnlyArray<PageDescription> = null;

constructor({
devServerUrl,
reporter,
}: {
devServerUrl: string,
reporter: TerminalReporter,
}) {
this.#devServerUrl = devServerUrl;
this.#reporter = reporter;
}

async #tryOpenDebuggerForTarget(target: PageDescription): Promise<void> {
this.#targetsShownForSelection = null;
this.#clearTerminalMenu();

try {
await fetch(
new URL(
'/open-debugger?target=' + encodeURIComponent(target.id),
this.#devServerUrl,
).href,
{method: 'POST'},
);
} catch (e) {
this.#log(
'error',
'Failed to open debugger for %s on %s debug targets: %s',
target.description,
target.deviceName,
e.message,
);
this.#clearTerminalMenu();
}
}

/**
* Used in response to 'j' to debug - fetch the available debug targets and:
* - If no targets, warn
* - If one target, open it
* - If more, show a list. The keyboard listener should run subsequent key
* presses through maybeHandleTargetSelection, which will launch the
* debugger if a match is made.
*/
async handleOpenDebugger(): Promise<void> {
this.#setTerminalMenu('Fetching available debugging targets...');
this.#targetsShownForSelection = null;

try {
const res = await fetch(this.#devServerUrl + '/json/list', {
method: 'POST',
});

if (res.status !== 200) {
throw new Error(`Unexpected status code: ${res.status}`);
}
const targets = (await res.json()) as $ReadOnlyArray<PageDescription>;
if (!Array.isArray(targets)) {
throw new Error('Expected array.');
}

if (targets.length === 0) {
this.#log('warn', 'No connected targets');
this.#clearTerminalMenu();
} else if (targets.length === 1) {
const target = targets[0];
// eslint-disable-next-line no-void
void this.#tryOpenDebuggerForTarget(target);
} else {
this.#targetsShownForSelection = targets;

if (targets.length > 9) {
this.#log(
'warn',
'10 or more debug targets available, showing the first 9.',
);
}

this.#setTerminalMenu(
`Multiple debug targets available, please select:\n ${targets
.slice(0, 9)
.map(
({description, deviceName}, i) =>
`${chalk.white.inverse(` ${i + 1} `)} - "${description}" on "${deviceName}"`,
)
.join('\n ')}`,
);
}
} catch (e) {
this.#log('error', `Failed to fetch debug targets: ${e.message}`);
this.#clearTerminalMenu();
}
}

/**
* Handle key presses that correspond to a valid selection from a visible
* selection list.
*
* @return true if we've handled the key as a target selection, false if the
* caller should handle the key.
*/
maybeHandleTargetSelection(keyName: string): boolean {
if (keyName >= '1' && keyName <= '9') {
const targetIndex = Number(keyName) - 1;
if (
this.#targetsShownForSelection != null &&
targetIndex < this.#targetsShownForSelection.length
) {
const target = this.#targetsShownForSelection[targetIndex];
// eslint-disable-next-line no-void
void this.#tryOpenDebuggerForTarget(target);
return true;
}
}
return false;
}

/**
* Dismiss any target selection UI, if shown.
*/
dismiss() {
this.#clearTerminalMenu();
this.#targetsShownForSelection = null;
}

#log(level: 'info' | 'warn' | 'error', ...data: Array<mixed>): void {
this.#reporter.update({
type: 'unstable_server_log',
level,
data,
});
}

#setTerminalMenu(message: string) {
this.#reporter.update({
type: 'unstable_server_menu_updated',
message,
});
}

#clearTerminalMenu() {
this.#reporter.update({
type: 'unstable_server_menu_cleared',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
*/

import type {Config} from '@react-native-community/cli-types';
import type TerminalReporter from 'metro/src/lib/TerminalReporter';

import {KeyPressHandler} from '../../utils/KeyPressHandler';
import {logger} from '../../utils/logger';
import OpenDebuggerKeyboardHandler from './OpenDebuggerKeyboardHandler';
import chalk from 'chalk';
import execa from 'execa';
import fetch from 'node-fetch';

const CTRL_C = '\u0003';
const CTRL_D = '\u0004';
Expand All @@ -36,13 +37,15 @@ export default function attachKeyHandlers({
cliConfig,
devServerUrl,
messageSocket,
reporter,
}: {
cliConfig: Config,
devServerUrl: string,
messageSocket: $ReadOnly<{
broadcast: (type: string, params?: Record<string, mixed> | null) => void,
...
}>,
reporter: TerminalReporter,
}) {
if (process.stdin.isTTY !== true) {
logger.debug('Interactive mode is not supported in this environment');
Expand All @@ -58,7 +61,16 @@ export default function attachKeyHandlers({
messageSocket.broadcast('reload', null);
}, RELOAD_TIMEOUT);

const openDebuggerKeyboardHandler = new OpenDebuggerKeyboardHandler({
reporter,
devServerUrl,
});

const onPress = async (key: string) => {
if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key)) {
return;
}

switch (key.toLowerCase()) {
case 'r':
reload();
Expand Down Expand Up @@ -92,11 +104,11 @@ export default function attachKeyHandlers({
).stdout?.pipe(process.stdout);
break;
case 'j':
// TODO(T192878199): Add multi-target selection
await fetch(devServerUrl + '/open-debugger', {method: 'POST'});
await openDebuggerKeyboardHandler.handleOpenDebugger();
break;
case CTRL_C:
case CTRL_D:
openDebuggerKeyboardHandler.dismiss();
logger.info('Stopping server');
keyPressHandler.stopInterceptingKeyStrokes();
process.emit('SIGINT');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ async function runServer(
cliConfig: ctx,
devServerUrl,
messageSocket: messageSocketEndpoint,
reporter: terminalReporter,
});
}
},
Expand Down

0 comments on commit aa55d76

Please sign in to comment.