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

Prevent errors/crashing when multiple installs of DevTools are present #22517

Merged
merged 7 commits into from
Oct 14, 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
11 changes: 11 additions & 0 deletions packages/react-devtools-extensions/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ const build = async (tempPath, manifestPath) => {
}
manifest.description += `\n\nCreated from revision ${commit} on ${dateString}.`;

if (process.env.NODE_ENV === 'development') {
jstejada marked this conversation as resolved.
Show resolved Hide resolved
// When building the local development version of the
// extension we want to be able to have a stable extension ID
// for the local build (in order to be able to reliably detect
// duplicate installations of DevTools).
// By specifying a key in the built manifest.json file,
// we can make it so the generated extension ID is stable.
// For more details see the docs here: https://developer.chrome.com/docs/extensions/mv2/manifest/key/
manifest.key = 'reactdevtoolslocalbuilduniquekey';
jstejada marked this conversation as resolved.
Show resolved Hide resolved
}

writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));

// Pack the extension
Expand Down
8 changes: 4 additions & 4 deletions packages/react-devtools-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"private": true,
"scripts": {
"build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge",
"build:dev": "cross-env NODE_ENV=development yarn run build:chrome:dev && yarn run build:firefox:dev && yarn run build:edge:dev",
"build:local": "cross-env NODE_ENV=development yarn run build:chrome:local && yarn run build:firefox:local && yarn run build:edge:local",
"build:chrome": "cross-env NODE_ENV=production node ./chrome/build",
"build:chrome:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx",
"build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build",
"build:chrome:local": "cross-env NODE_ENV=development node ./chrome/build",
"build:firefox": "cross-env NODE_ENV=production node ./firefox/build",
"build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build",
"build:firefox:local": "cross-env NODE_ENV=development node ./firefox/build",
"build:edge": "cross-env NODE_ENV=production node ./edge/build",
"build:edge:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./edge/build --crx",
"build:edge:dev": "cross-env NODE_ENV=development node ./edge/build",
"build:edge:local": "cross-env NODE_ENV=development node ./edge/build",
"test:chrome": "node ./chrome/test",
"test:firefox": "node ./firefox/test",
"test:edge": "node ./edge/test",
Expand Down
10 changes: 10 additions & 0 deletions packages/react-devtools-extensions/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const ports = {};

const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;

import {EXTENSION_INSTALL_CHECK_MESSAGE} from './constants';

chrome.runtime.onConnect.addListener(function(port) {
let tab = null;
let name = null;
Expand Down Expand Up @@ -116,6 +118,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
}
});

chrome.runtime.onMessageExternal.addListener(
(request, sender, sendResponse) => {
if (request === EXTENSION_INSTALL_CHECK_MESSAGE) {
sendResponse(true);
}
},
);

chrome.runtime.onMessage.addListener((request, sender) => {
const tab = sender.tab;
if (tab) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* Copyright (c) Facebook, Inc. and its 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
*/

declare var chrome: any;

import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {
EXTENSION_INSTALL_CHECK_MESSAGE,
EXTENSION_INSTALLATION_TYPE,
INTERNAL_EXTENSION_ID,
LOCAL_EXTENSION_ID,
} from './constants';

const UNRECOGNIZED_EXTENSION_WARNING =
'React Developer Tools: You are running an unrecognized installation of the React Developer Tools extension, which might conflict with other versions of the extension installed in your browser. ' +
'Please make sure you only have a single version of the extension installed or enabled. ' +
'If you are developing this extension locally, make sure to build the extension using the `yarn build:<browser>:local` command.';

export function checkForDuplicateInstallations(callback: boolean => void) {
switch (EXTENSION_INSTALLATION_TYPE) {
case 'public': {
// If this is the public extension (e.g. from Chrome Web Store), check if an internal
// or local build of the extension is also installed, and if so, disable this extension.
jstejada marked this conversation as resolved.
Show resolved Hide resolved
// TODO show warning if other installations are present.
checkForInstalledExtensions([
INTERNAL_EXTENSION_ID,
LOCAL_EXTENSION_ID,
]).then(areExtensionsInstalled => {
if (areExtensionsInstalled.some(isInstalled => isInstalled)) {
callback(true);
} else {
callback(false);
}
});
break;
}
case 'internal': {
// If this is the internal extension, check if a local build of the extension
// is also installed, and if so, disable this extension.
// If the public version of the extension is also installed, that extension
// will disable itself.
// TODO show warning if other installations are present.
checkForInstalledExtension(LOCAL_EXTENSION_ID).then(isInstalled => {
if (isInstalled) {
callback(true);
} else {
callback(false);
}
});
break;
}
case 'local': {
if (__DEV__) {
// If this is the local extension (i.e. built locally during development),
// always keep this one enabled. Other installations disable themselves if
// they detect the local build is installed.
callback(false);
break;
}

// If this extension wasn't built locally during development, we can't reliably
// detect if there are other installations of DevTools present.
// In this case, assume there are no duplicate exensions and show a warning about
// potential conflicts.
console.error(UNRECOGNIZED_EXTENSION_WARNING);
chrome.devtools.inspectedWindow.eval(
`console.error("${UNRECOGNIZED_EXTENSION_WARNING}")`,
);
callback(false);
break;
}
case 'unknown': {
// If we don't know how this extension was built, we can't reliably detect if there
// are other installations of DevTools present.
// In this case, assume there are no duplicate exensions and show a warning about
// potential conflicts.
console.error(UNRECOGNIZED_EXTENSION_WARNING);
chrome.devtools.inspectedWindow.eval(
`console.error("${UNRECOGNIZED_EXTENSION_WARNING}")`,
);
callback(false);
break;
}
default: {
(EXTENSION_INSTALLATION_TYPE: empty);
jstejada marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

function checkForInstalledExtensions(
extensionIds: string[],
): Promise<boolean[]> {
return Promise.all(
extensionIds.map(extensionId => checkForInstalledExtension(extensionId)),
);
}

function checkForInstalledExtension(extensionId: string): Promise<boolean> {
return new Promise(resolve => {
chrome.runtime.sendMessage(
extensionId,
EXTENSION_INSTALL_CHECK_MESSAGE,
response => {
if (__DEBUG__) {
console.log(
'checkForDuplicateInstallations: Duplicate installation check responded with',
{
response,
error: chrome.runtime.lastError?.message,
currentExtension: EXTENSION_INSTALLATION_TYPE,
checkingExtension: extensionId,
},
);
}
if (chrome.runtime.lastError != null) {
resolve(false);
} else {
resolve(true);
}
},
);
});
}
31 changes: 31 additions & 0 deletions packages/react-devtools-extensions/src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Facebook, Inc. and its 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
*/

declare var chrome: any;

export const CURRENT_EXTENSION_ID = chrome.runtime.id;

export const EXTENSION_INSTALL_CHECK_MESSAGE = 'extension-install-check';

export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi';
export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc';
export const LOCAL_EXTENSION_ID = 'ikiahnapldjmdmpkmfhjdjilojjhgcbf';

export const EXTENSION_INSTALLATION_TYPE:
| 'public'
| 'internal'
| 'local'
| 'unknown' =
CURRENT_EXTENSION_ID === CHROME_WEBSTORE_EXTENSION_ID
? 'public'
: CURRENT_EXTENSION_ID === INTERNAL_EXTENSION_ID
? 'internal'
: CURRENT_EXTENSION_ID === LOCAL_EXTENSION_ID
? 'local'
: 'unknown';
jstejada marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 17 additions & 2 deletions packages/react-devtools-extensions/src/injectGlobalHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import nullthrows from 'nullthrows';
import {installHook} from 'react-devtools-shared/src/hook';
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
import {
__DEBUG__,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
} from 'react-devtools-shared/src/constants';
import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';

function injectCode(code) {
Expand All @@ -27,7 +31,17 @@ window.addEventListener('message', function onMessage({data, source}) {
if (source !== window || !data) {
return;
}

if (data.extensionId !== CURRENT_EXTENSION_ID) {
jstejada marked this conversation as resolved.
Show resolved Hide resolved
if (__DEBUG__) {
console.log(
`[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`,
{
currentExtension: EXTENSION_INSTALLATION_TYPE,
},
);
}
return;
}
switch (data.source) {
case 'react-devtools-detector':
lastDetectionResult = {
Expand Down Expand Up @@ -102,6 +116,7 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({reactBuildType})
window.postMessage({
source: 'react-devtools-detector',
reactBuildType,
extensionId: "${CURRENT_EXTENSION_ID}",
}, '*');
});
`;
Expand Down
Loading