Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bridge protocol version backend/frontend #21331

Merged
merged 1 commit into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/react-devtools-core/src/standalone.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
}
Loading