javascriptgoogle-chromegoogle-chrome-extension

Chrome extension: How to remove orphaned script after chrom extension update


I have a chrome extension with a popup page which passes a boolean variable to my content page via simple one-time requests. The content page would then do some action based on the status of the boolean variable passed from the popup page. This was working perfectly until I accidentally removed the extension (still in developer mode, the extension is unpacked) and had to re-load it.

This caused an extension context invalidated error to appear in the popup inspection console and the webpage console seems to validate that the popup page and content script are not communicating. The webpage with the chrome extension active shows this error: Unchecked runtime.lastError: The message port closed before a response was received.

Based on a few answers I've already seen, it seems that reloading my chrome extension has "orphaned" my original working content script from the rest of my extension, which causes the aforementioned "Unchecked runtime.lastError: The message port closed before a response was received." error on the webpage console.

I believe that I cannot just reinject my content script again as my content script has DOM event listeners. Is there a possible way to remove the currently running orphan script? Or is there any suggested workaround to this problem?

Here is my popup.js:

chrome.tabs.query({'active': true, 'currentWindow': true}, function (tabs) {
    chrome.tabs.sendMessage(tabs[0].id, {cTabSettings: (some boolean variable)});
});

Here is my content.js:

// Listening for message from popup.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.cTabSettings === true) {
      enabled = true;
    } else if (request.cTabSettings === false) {
      enabled = false;
    }
});

// DOM listener and action
document.addEventListener('mousemove', function (e) {
   // Some action
   chrome.runtime.sendMessage({sender: "content", selText : "blah"}, function () {
      console.log("success");
   });
}, false);

I am using chrome developer mode version 76. Just to rephrase, this chrome extension was working (content script communicates with popup) before I accidentally reloaded it.


Solution

  • Since the orphaned content script can still receive DOM messages, send one from your new working content script to the ghosted content script via window, for example. Upon receiving the message you'll unregister all listeners (and nullify any global variables) which will also make your old script eligible for automatic garbage collection.

    background.js:

    See how to re-inject content scripts on reloading/installing the extension in this example.

    content.js:

    var orphanMessageId = chrome.runtime.id + 'orphanCheck';
    window.dispatchEvent(new Event(orphanMessageId));
    window.addEventListener(orphanMessageId, unregisterOrphan);
    
    // register all listeners with named functions to preserve their object reference
    chrome.runtime.onMessage.addListener(onMessage);
    document.addEventListener('mousemove', onMouseMove);
    
    // the popup script checks it to see if a usable instance of content script is running
    window.running = true;
    
    function unregisterOrphan() {
      if (chrome.runtime.id) {
        // someone tried to kick us out but we're not orphaned! 
        return;
      }
      window.removeEventListener(orphanMessageId, unregisterOrphan);
      document.removeEventListener('mousemove', onMouseMove);
      try {
        // 'try' is needed to avoid an exception being thrown in some cases 
        chrome.runtime.onMessage.removeListener(onMessage);
      } catch (e) {}
      return true;
    }
    
    function onMessage(msg, sender, sendResponse) {
      //...........
    }
    
    function onMouseMove(event) {
      // DOM events still fire in the orphaned content script after the extension
      // was disabled/removed and before it's re-enabled or re-installed
      if (unregisterOrphan()) { return }
      //...........
    }
    

    popup.js should ensure a content script is injected before sending a message:

    async function sendMessage(data) {
      const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
      if (await ensureContentScript(tab.id)) {
        return await chrome.tabs.sendMessage(tab.id, data);
      }
    }
    
    async function ensureContentScript(tabId) {
      try {
        const [{result}] = await chrome.scripting.executeScript({
          target: {tabId},
          func: () => window.running === true,
        });
        if (!result) {
          await chrome.scripting.executeScript({
            target: {tabId},
            files: ['content.js'],
          });
        }
        return true;
      } catch (e) {}
    }