1

I have built a chrome extension and I'm getting hit by a race condition that I need help with.

If you see the answer chrome extension: sharing an object between content scripts and background script it tells us that you cannot share a variable between content and background scripts.

My goal is to generate a unique ID per-browser tab and then share it in between the content.js and the background.js. Then I need to use this value in a content injected javascript as explained in this answer: In Chrome extensions, can you force some javascript to be injected before everything?

The only way I have been able to figure out how to do this is by doing the following async code then I just use the tab ID as the unique ID:

content.js

await pingBackground();
async function pingBackground() {
    var info;
    await new Promise(function (resolve, reject) {
        chrome.runtime.sendMessage({ type: 1 }, function (response) {
            if (response !== undefined) {
                info = response;
                resolve();
            }
            else {
                reject();
            }
        });
    });
    console.log("Id is " + info.id);
}

background.js

chrome.runtime.onMessage.addListener(messageHandler);
function messageHandler(message, sender, reply) {
    switch (message.type) {
        case 1:
        reply({ 'id': sender['tab'].id, 'active': true });
        break;
    }
}

manifest.json

{
    "name": "oogi",
    "version": "0.1",
    "manifest_version": 2,
    "background": {
        "scripts": [
            "common.js",
            "background.js"
        ],
        "persistent": true
    },
    "content_scripts": [
        {
            "matches": ["*://*/*"],
            "js": ["content.js"],
            "run_at": "document_start"
        }
    ],
    "permissions": [
        "contentSettings",
        "webRequest",
        "webRequestBlocking",
        "*://*/*"
    ]
}

But the problem with this is by the time I get the tab ID from background js, the script's content has already been loaded.

Is there some way to make it so this variable can be asynchronously shared between background.js and content.js? Or is this simply impossible?

Can I switch it around and have background.js load a variable from content.js asynchronously?

UPDATE:

A terrible hack which works is to do this in the foreground of the content.js:

var sleepScript = document.createElement('script');
var sleepCode = `function sleep (ms) {
    var start = new Date().getTime();
    while (new Date() < start + ms) {}
    return 0;
}
sleep(500);`;
sleepScript.textContent = sleepCode;
(document.head || document.documentElement).appendChild(sleepScript);

This will force the page to wait for a bit giving the time to query the background before running the inline dom.

It works but that's awful.

Nicholas DiPiazza
  • 10,029
  • 11
  • 83
  • 152
  • You can share it by setting an http cookie header in your background script using webRequest API in blocking mode. The cookie will be available via document.cookie in your content script. I believe I saw an answer for that by Xan. If the value is big you can additionally use blob + synchronous XHR (Tampermonkey does that). – wOxxOm Nov 06 '18 at 04:36
  • Found it: [Injecting javascript variable before content script](//stackoverflow.com/a/45105934) – wOxxOm Nov 06 '18 at 05:06
  • You are definitely my favorite person. I would have never figured this out. It worked like a charm. – Nicholas DiPiazza Nov 06 '18 at 05:38
  • Darn. Show-stopper on this one for me is that I need it to work in multiple tabs. As soon as one of my tabs opens this up, bam I'm stuck with it. – Nicholas DiPiazza Nov 06 '18 at 05:52
  • The problem is the seed value that it creates is created in the `background.js` foreground which is once per browser window. So now every page is using that same seed value. What I really need is the Tab ID in the `content.js` page of the tab that it belongs to, or some seed value set per tab to achieve the same thing. – Nicholas DiPiazza Nov 06 '18 at 06:03
  • Not sure I understand what's this about. The webRequest callback parameter has tabId and frameId for each request so you can trivially set them in that cookie header. – wOxxOm Nov 06 '18 at 06:07
  • Ah. Didn't realize that oops. OK that seems like it is working now. Testing. – Nicholas DiPiazza Nov 06 '18 at 06:16
  • It feels like this way might be susceptible to race conditions if i open a few windows or tabs at the exact same time. the seed cookie might be overwritten while loading. am I correct about that or just being a worrier – Nicholas DiPiazza Nov 06 '18 at 06:25
  • JavaScript is single-threaded per context so there's no such thing as "exact same time" - the callbacks are executed sequentially using the *event loop*. – wOxxOm Nov 06 '18 at 06:41
  • I put this all into an answer. @wOxxOm i see like dozens of the cookies created that I used to share this data. I wonder if that's going to cause problems like hitting max cookies sizes? – Nicholas DiPiazza Nov 06 '18 at 13:59
  • Sure it can cause problems. – wOxxOm Nov 06 '18 at 14:03
  • So I'll need to clean up these cookies somehow. – Nicholas DiPiazza Nov 06 '18 at 14:06
  • Yeah, I guess it should be possible in the content script using the standard ancient DOM approach. Otherwise you'll need to use chrome.cookies API. Or maybe just set an expiration time of the cookie to like 1 second? – wOxxOm Nov 06 '18 at 14:12
  • yeah that expiry thing was what I was thinking too. will try it out – Nicholas DiPiazza Nov 06 '18 at 14:17
  • yep. works like a charm. – Nicholas DiPiazza Nov 06 '18 at 14:21

1 Answers1

1

This question was already answered previously, although it is hard to tell that this is the same issue at first glance.

https://stackoverflow.com/a/45105934

The answer is pretty descriptive so give it a read.

Here is the script changes that make it work:

// background.js
function addSeedCookie(details) {
  details.responseHeaders.push({
    name: "Set-Cookie",
    value: `tab_id=${details.tabId}; Max-Age=2`
  });
  return {
    responseHeaders: details.responseHeaders
  };
}

chrome.webRequest.onHeadersReceived.addListener(
  addSeedCookie, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]
);

// inject.js
function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
  return document.cookie.split(';').reduce(function(prev, c) {
    var arr = c.split('=');
    return (arr[0].trim() === cookie) ? arr[1] : prev;
  }, undefined);
}

var tabId = getCookie("tab_id");
Nicholas DiPiazza
  • 10,029
  • 11
  • 83
  • 152