'Chrome extension: Checking if content script has been injected or not

I'm developing a Chrome extension. Instead of using manifest.json to match content script for all URLs, I lazily inject the content script by calling chrome.tabs.executeScript when user do click the extension icon.

What I'm trying is to avoid executing the script more than once. So I have following code in my content script:

if (!window.ALREADY_INJECTED_FLAG) {
    window.ALREADY_INJECTED_FLAG = true
    init() // <- All side effects go here
}

Question #1, is this safe enough to naively call chrome.tabs.executeScript every time the extension icon got clicked? In other words, is this idempotent?

Question #2, is there a similar method for chrome.tabs.insertCSS?

It seems impossible to check the content script inject status in the backgroud script since it can not access the DOM of web page. I've tried a ping/pong method for checking any content script instance is alive. But this introduces an overhead and complexity of designing the ping-timeout.

Question #3, any better method for background script to check the inject status of content script, so I can just prevent calling chrome.tabs.executeScript every time when user clicked the icon?

Thanks in advance!



Solution 1:[1]

Rob W's option 3 worked great for me. Basically the background script pings the content script and if there's no response it will add all the necessary files. I only do this when a tab is activated to avoid complications of having to add to every single open tab in the background:

background.js

chrome.tabs.onActivated.addListener(function(activeInfo){
  tabId = activeInfo.tabId

  chrome.tabs.sendMessage(tabId, {text: "are_you_there_content_script?"}, function(msg) {
    msg = msg || {};
    if (msg.status != 'yes') {
      chrome.tabs.insertCSS(tabId, {file: "css/mystyle.css"});
      chrome.tabs.executeScript(tabId, {file: "js/content.js"});
    }
  });
});

content.js

chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
    if (msg.text === 'are_you_there_content_script?') {
      sendResponse({status: "yes"});
    }
});

Solution 2:[2]

Just a side note to the great answer from Rob.

I've found the Chrome extension from Pocket is using a similar method. In their dynamic injected script:

if (window.thePKT_BM)
    window.thePKT_BM.save();
else {
    var PKT_BM_OVERLAY = function(a) {
        // ... tons of code
    },
    $(document).ready(function() {
        if (!window.thePKT_BM) {
            var a = new PKT_BM;
            window.thePKT_BM = a,
            a.init()
        }
        window.thePKT_BM.save()
    }
    )
}

Solution 3:[3]

For MV3 Chrome extension, I use this code, no chrome.runtime.lastError "leaking" as well:

In Background/Extension page (Popup for example)

    private async injectIfNotAsync(tabId: number) {
        let injected = false;
        try {
            injected = await new Promise((r, rej) => {
                chrome.tabs.sendMessage(tabId, { op: "confirm" }, (res: boolean) => {
                    const err = chrome.runtime.lastError;
                    if (err) {
                        rej(err);
                    }

                    r(res);
                });
            });
        } catch {
            injected = false;
        }
        if (injected) { return tabId; }

        await chrome.scripting.executeScript({
            target: {
                tabId
            },
            files: ["/js/InjectScript.js"]
        });
        return tabId;
    }

NOTE that currently in Chrome/Edge 96, chrome.tabs.sendMessage does NOT return a Promise that waits for sendResponse although the documentation says so.

In content script:

const extId = chrome.runtime.id;
class InjectionScript{

    init() {
        chrome.runtime.onMessage.addListener((...params) => this.onMessage(...params));
    }

    onMessage(msg: any, sender: ChrSender, sendRes: SendRes) {
        if (sender.id != extId || !msg?.op) { return; }

        switch (msg.op) {
            case "confirm":
                console.debug("Already injected");
                return void sendRes(true);
            // Other ops
            default:
                console.error("Unknown OP: " + msg.op);
        }

    }

}
new InjectionScript().init();

What it does:

  • When user opens the extension popup for example, attempt to ask the current tab to "confirm".

  • If the script isn't injected yet, no response would be found and chrome.runtime.lastError would have value, rejecting the promise.

  • If the script was already injected, a true response would result in the background script not performing it again.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 GivP
Solution 2 KF Lin
Solution 3 Luke Vo