1

I have a new browser extension I'm developing, which means that to make it publicly available on the Chrome Web Store, I must use manifest v3. My extension is a DevTools extension, which means that to communicate with the content script, I have to use a background service worker to proxy the messages. Unfortunately, the docs on DevTools extensions haven't been updated for manifest v3, and the technique they suggest for messaging between the content script and the DevTools panel via the background script won't work if the background worker is terminated.

I've seen some answers here and Chromium project issue report comments suggest that the only available workaround is to reset the connection every five minutes. That seems hacky and unreliable. Is there a better mechanism for this, something more event based than an arbitrary timer?

JimEvans
  • 27,201
  • 7
  • 83
  • 108

1 Answers1

3

We can make the connection hub out of the devtools_page itself. This hidden page runs inside devtools for the current tab, it doesn't unload while devtools is open, and it has full access to all of chrome API same as the background script.

manifest.json:

  "devtools_page": "devtools.html",
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"],
    "run_at": "document_start"
  }]

devtools.html:

<script src="devtools.js"></script>

devtools.js:

let portDev, portTab;
const tabId = chrome.devtools.inspectedWindow.tabId;
const onDevMessage = msg => portTab.postMessage(msg);
const onTabMessage = msg => portDev.postMessage(msg);

chrome.runtime.onConnect.addListener(port => {
  if (+port.name !== tabId) return;
  portDev = port;
  portDev.onMessage.addListener(onDevMessage);
  portTab = chrome.tabs.connect(tabId, {name: 'dev'});
  portTab.onMessage.addListener(onTabMessage);
});

// chrome.devtools.panels.create...

panel.js:

const port = chrome.runtime.connect({
  name: `${chrome.devtools.inspectedWindow.tabId}`,
});
port.onMessage.addListener(msg => {
  // This prints in devtools-on-devtools: https://stackoverflow.com/q/12291138
  // To print in tab's console see `chrome.devtools.inspectedWindow.eval`
  console.log(msg);
});
self.onclick = () => port.postMessage('foo');

content.js:

let portDev;
const onMessage = msg => {
  console.log(msg);
  portDev.postMessage('bar');
};
const onDisconnect = () => {
  portDev = null;
};
chrome.runtime.onConnect.addListener(port => {
  if (port.name !== 'dev') return;
  portDev = port;
  portDev.onMessage.addListener(onMessage);
  portDev.onDisconnect.addListener(onDisconnect);
});

P.S. Regarding the 5-minute timer reset trick, if you still need the background script to be persistent, in this case it is reasonably reliable because the tab is guaranteed to be open while devtools for this tab is open.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • That's pretty clever, and I thank you for the idea. I managed to solve this in my solution a slightly different way, listening in panel.js for the port connection's `onDisconnect`, and then re-initiating the port connection to the background service worker once I needed to post the next message. But this might be cleaner for most people (including me; I'll have to evaluate it for the rest of my solution). – JimEvans Jul 07 '22 at 21:41
  • In devtools.js I can use `chrome.tabs.sendMessage(tabId, message, {frameId: myFrameId});` to send a message to a specific frame within a tab. Is there a way to target a frame using `portTab.postMessage(msg);`? – strattonn Oct 01 '22 at 05:07
  • 1
    @strattonn, no, you need to set a new port via chrome.tabs.connect with [frameId](https://developer.chrome.com/docs/extensions/reference/tabs/#method-connect:~:text=Open%20a%20port%20to%20a%20specific%20frame). – wOxxOm Oct 01 '22 at 07:48