Skip to content

Commit

Permalink
DevTools: Add Bridge protocol version backend/frontend (#21331)
Browse files Browse the repository at this point in the history
Add an explicit Bridge protocol version to the frontend and backend components as well as a check during initialization to ensure that both are compatible. If not, the frontend will display either upgrade or downgrade instructions.

Note that only the `react-devtools-core` (React Native) and `react-devtools-inline` (Code Sandbox) packages implement this check. Browser extensions inject their own backend and so the check is unnecessary. (Arguably the `react-devtools-inline` check is also unlikely to be necessary _but_ has been added as an extra guard for use cases such as Replay.io.)
  • Loading branch information
Brian Vaughn committed Apr 27, 2021
1 parent 22ab39b commit 8e2bb3e
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 3 deletions.
5 changes: 4 additions & 1 deletion packages/react-devtools-core/src/standalone.js
Expand Up @@ -217,7 +217,10 @@ function initialize(socket: WebSocket) {
socket.close();
});

store = new Store(bridge, {supportsNativeInspection: false});
store = new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsNativeInspection: false,
});

log('Connected');
reload();
Expand Down
5 changes: 4 additions & 1 deletion packages/react-devtools-inline/src/frontend.js
Expand Up @@ -21,7 +21,10 @@ import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {Props} from 'react-devtools-shared/src/devtools/views/DevTools';

export function createStore(bridge: FrontendBridge): Store {
return new Store(bridge, {supportsTraceUpdates: true});
return new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
});
}

export function createBridge(
Expand Down
6 changes: 6 additions & 0 deletions packages/react-devtools-shared/src/backend/agent.js
Expand Up @@ -26,6 +26,7 @@ import {
toggleEnabled as setTraceUpdatesEnabled,
} from './views/TraceUpdates';
import {patch as patchConsole, unpatch as unpatchConsole} from './console';
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';

import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {
Expand Down Expand Up @@ -176,6 +177,7 @@ export default class Agent extends EventEmitter<{|
bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
bridge.addListener('copyElementPath', this.copyElementPath);
bridge.addListener('deletePath', this.deletePath);
bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
bridge.addListener('getProfilingData', this.getProfilingData);
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
bridge.addListener('getOwnersList', this.getOwnersList);
Expand Down Expand Up @@ -308,6 +310,10 @@ export default class Agent extends EventEmitter<{|
return null;
}

getBridgeProtocol = () => {
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
};

getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
Expand Down
45 changes: 45 additions & 0 deletions packages/react-devtools-shared/src/bridge.js
Expand Up @@ -20,6 +20,49 @@ import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-share

const BATCH_DURATION = 100;

// This message specifies the version of the DevTools protocol currently supported by the backend,
// as well as the earliest NPM version (e.g. "4.13.0") that protocol is supported by on the frontend.
// This enables an older frontend to display an upgrade message to users for a newer, unsupported backend.
export type BridgeProtocol = {|
// Version supported by the current frontend/backend.
version: number,

// NPM version range that also supports this version.
// Note that 'maxNpmVersion' is only set when the version is bumped.
minNpmVersion: string,
maxNpmVersion: string | null,
|};

// Bump protocol version whenever a backwards breaking change is made
// in the messages sent between BackendBridge and FrontendBridge.
// This mapping is embedded in both frontend and backend builds.
//
// The backend protocol will always be the latest entry in the BRIDGE_PROTOCOL array.
//
// When an older frontend connects to a newer backend,
// the backend can send the minNpmVersion and the frontend can display an NPM upgrade prompt.
//
// When a newer frontend connects with an older protocol version,
// the frontend can use the embedded minNpmVersion/maxNpmVersion values to display a downgrade prompt.
export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
// This version technically never existed,
// but a backwards breaking change was added in 4.11,
// so the safest guess to downgrade the frontend would be to version 4.10.
{
version: 0,
minNpmVersion: '<4.11.0',
maxNpmVersion: '<4.11.0',
},
{
version: 1,
minNpmVersion: '4.13.0',
maxNpmVersion: null,
},
];

export const currentBridgeProtocol: BridgeProtocol =
BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];

type ElementAndRendererID = {|id: number, rendererID: RendererID|};

type Message = {|
Expand Down Expand Up @@ -119,6 +162,7 @@ type UpdateConsolePatchSettingsParams = {|
|};

export type BackendEvents = {|
bridgeProtocol: [BridgeProtocol],
extensionBackendInitialized: [],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
Expand Down Expand Up @@ -150,6 +194,7 @@ type FrontendEvents = {|
clearWarningsForFiberID: [ElementAndRendererID],
copyElementPath: [CopyElementPathParams],
deletePath: [DeletePath],
getBridgeProtocol: [],
getOwnersList: [ElementAndRendererID],
getProfilingData: [{|rendererID: RendererID|}],
getProfilingStatus: [],
Expand Down
66 changes: 65 additions & 1 deletion packages/react-devtools-shared/src/devtools/store.js
Expand Up @@ -29,10 +29,17 @@ import {localStorageGetItem, localStorageSetItem} from '../storage';
import {__DEBUG__} from '../constants';
import {printStore} from './utils';
import ProfilerStore from './ProfilerStore';
import {
BRIDGE_PROTOCOL,
currentBridgeProtocol,
} from 'react-devtools-shared/src/bridge';

import type {Element} from './views/Components/types';
import type {ComponentFilter, ElementType} from '../types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {
FrontendBridge,
BridgeProtocol,
} from 'react-devtools-shared/src/bridge';

const debug = (methodName, ...args) => {
if (__DEBUG__) {
Expand All @@ -51,6 +58,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
'React::DevTools::recordChangeDescriptions';

type Config = {|
checkBridgeProtocolCompatibility?: boolean,
isProfiling?: boolean,
supportsNativeInspection?: boolean,
supportsReloadAndProfile?: boolean,
Expand All @@ -76,6 +84,7 @@ export default class Store extends EventEmitter<{|
supportsNativeStyleEditor: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
unsupportedRendererVersionDetected: [],
|}> {
_bridge: FrontendBridge;
Expand Down Expand Up @@ -119,6 +128,10 @@ export default class Store extends EventEmitter<{|

_nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;

// Older backends don't support an explicit bridge protocol,
// so we should timeout eventually and show a downgrade message.
_onBridgeProtocolTimeoutID: TimeoutID | null = null;

// Map of element (id) to the set of elements (ids) it owns.
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
_ownersMap: Map<number, Set<number>> = new Map();
Expand Down Expand Up @@ -147,6 +160,7 @@ export default class Store extends EventEmitter<{|
_supportsReloadAndProfile: boolean = false;
_supportsTraceUpdates: boolean = false;

_unsupportedBridgeProtocol: BridgeProtocol | null = null;
_unsupportedRendererVersionDetected: boolean = false;

// Total number of visible elements (within all roots).
Expand Down Expand Up @@ -217,6 +231,20 @@ export default class Store extends EventEmitter<{|
);

this._profilerStore = new ProfilerStore(bridge, this, isProfiling);

// Verify that the frontend version is compatible with the connected backend.
// See github.com/facebook/react/issues/21326
if (config != null && config.checkBridgeProtocolCompatibility) {
// Older backends don't support an explicit bridge protocol,
// so we should timeout eventually and show a downgrade message.
this._onBridgeProtocolTimeoutID = setTimeout(
this.onBridgeProtocolTimeout,
10000,
);

bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
bridge.send('getBridgeProtocol');
}
}

// This is only used in tests to avoid memory leaks.
Expand Down Expand Up @@ -385,6 +413,10 @@ export default class Store extends EventEmitter<{|
return this._supportsTraceUpdates;
}

get unsupportedBridgeProtocol(): BridgeProtocol | null {
return this._unsupportedBridgeProtocol;
}

get unsupportedRendererVersionDetected(): boolean {
return this._unsupportedRendererVersionDetected;
}
Expand Down Expand Up @@ -1168,6 +1200,12 @@ export default class Store extends EventEmitter<{|
'unsupportedRendererVersion',
this.onBridgeUnsupportedRendererVersion,
);
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);

if (this._onBridgeProtocolTimeoutID !== null) {
clearTimeout(this._onBridgeProtocolTimeoutID);
this._onBridgeProtocolTimeoutID = null;
}
};

onBackendStorageAPISupported = (isBackendStorageAPISupported: boolean) => {
Expand All @@ -1187,4 +1225,30 @@ export default class Store extends EventEmitter<{|

this.emit('unsupportedRendererVersionDetected');
};

onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
if (this._onBridgeProtocolTimeoutID !== null) {
clearTimeout(this._onBridgeProtocolTimeoutID);
this._onBridgeProtocolTimeoutID = null;
}

if (bridgeProtocol.version !== currentBridgeProtocol.version) {
this._unsupportedBridgeProtocol = bridgeProtocol;
} else {
// If we should happen to get a response after timing out...
this._unsupportedBridgeProtocol = null;
}

this.emit('unsupportedBridgeProtocolDetected');
};

onBridgeProtocolTimeout = () => {
this._onBridgeProtocolTimeoutID = null;

// If we timed out, that indicates the backend predates the bridge protocol,
// so we can set a fake version (0) to trigger the downgrade message.
this._unsupportedBridgeProtocol = BRIDGE_PROTOCOL[0];

this.emit('unsupportedBridgeProtocolDetected');
};
}
2 changes: 2 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/DevTools.js
Expand Up @@ -25,6 +25,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
import {useLocalStorage} from './hooks';
Expand Down Expand Up @@ -226,6 +227,7 @@ export default function DevTools({
</TreeContextController>
</ViewElementSourceContext.Provider>
</SettingsContextController>
<UnsupportedBridgeProtocolDialog />
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
</ModalDialogContextController>
Expand Down
Expand Up @@ -383,6 +383,13 @@ export function updateThemeVariables(
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
updateStyleHelper(theme, 'color-link', documentElements);
updateStyleHelper(theme, 'color-modal-background', documentElements);
updateStyleHelper(
theme,
'color-bridge-version-npm-background',
documentElements,
);
updateStyleHelper(theme, 'color-bridge-version-npm-text', documentElements);
updateStyleHelper(theme, 'color-bridge-version-number', documentElements);
updateStyleHelper(
theme,
'color-primitive-hook-badge-background',
Expand Down
@@ -0,0 +1,37 @@
.Column {
display: flex;
flex-direction: column;
}

.Title {
font-size: var(--font-size-sans-large);
margin-bottom: 0.5rem;
}

.ReleaseNotesLink {
color: var(--color-button-active);
}

.Version {
color: var(--color-bridge-version-number);
font-weight: bold;
}

.NpmCommand {
display: flex;
justify-content: space-between;
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
background-color: var(--color-bridge-version-npm-background);
color: var(--color-bridge-version-npm-text);
margin: 0;
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-large);
}

.Paragraph {
margin: 0.5rem 0;
}

.Link {
color: var(--color-link);
}

0 comments on commit 8e2bb3e

Please sign in to comment.