Chrome Extension Message Passing With Injected Code

Chrome Extensions: Javascript Injection, Message Passing, and Shutdown.

My latest side-project is stereopaw, a bookmarking service that timestamps streaming music. Imagine a del.icio.us meets Pinterest for music: a user clicks on the browser extension, and the current track’s metadata is available to save in real-time. In building the extension, I wrestled with how to handle cross-communication within the chrome browser ecosystem; namely, how to control injected javascript code on a page, from an extension’s background or content script.

I’ll show an example of javascript injection, and through basic message passing, prevent runaway code when the extension closes by listening for a “shutdown” event.

  • Javascript code injection
  • Motivation and Trade-offs
  • Detecting a “Shutdown” Event

Javascript Code Injection

The stereopaw extension creates a script tag whose ‘src’ points to external javascript. Make sure your extension is granted permission to access and manipulate your target page, as declared in manifest.json. Here is a straightforward example of adding javascript (that itself loads external javascript) in a chrome extension:

/popup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
 * Note this executes in the *extension's* page context, and the chrome.tabs API 
 * isn't available on the visited page.
 * @tab.id: the specified tab's id number, that could be retrieved from a call to 'chrome.tabs.query'
 */

chrome.tabs.executeScript(tab.id,
  {
    code: "(function()
     {
       var e = document.createElement('script')
       e.setAttribute('id', 'sb-script')
       e.setAttribute('mode', 'extension')
       e.setAttribute('src','//[YOURHOST]/[YOURCODE].js)
       document.body.appendChild(e)
     })()"
  }
)

Inject a Function With Arguments

What if you want to inject a pre-defined function in your extension into your target page? Take the above codeblock one step further and pass in a “stringified” function. Concatenation coerces the function into a string, and you can pass in an args array via JSON.stringify(). In the example below, you’ll still need the tab id, and permission as declared in your manifest.json.


/popup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
 * insert(): your code to be injected
 * code_obj = your function 'stringified'
 */

var first = "hello"
var second = "there"

var insert = function(passed_args) {
    var my_first_arg = passed_args[0]
    var my_second_arg = passed_args[1]

    /* 
     * do your stuff
     * great when you need to inject code as defined in your extension,
     * and take advantage of user's input.
     */

    console.log(args[0] + " " + args[1])
}


/* we use our insert function and our parameters (first, second) to build the code_obj*/

var code_obj = {
    code: "(" + insert + ")(" + JSON.stringify([first, second]) + ");"
}

/*don't forget your tab id*/
chrome.tabs.executeScript(tab.id, code_obj)

Motivation and Trade-offs

In the case of stereopaw, both the browser extension and marklet are wrappers that load external javascript – the “stereopaw code” – to access track/artist data from the service (soundcloud, youtube.) This offers marklet users (i.e. firefox and mobile) equal functionality as chrome extension users, but brings its own set of tradeoffs: of particular worry is the case where a service changes their audio player, or the manner in which they store metadata. Rather than force users to reinstall an extension with every breaking change, I can update a single code base loaded by both the marklet and extension. The downside is that the extension must now communicate with this decoupled javascript code – tricky – for instance, when the extension is closed, but the “stereopaw code” remains active.

The External Stereopaw Code

Clicking the marklet adds the external “stereopaw code” to the DOM, and launches a setInterval() loop that repeatedly renders track data and an updated timestamp. When the page overlay is closed, the event kills the loop.

With the chrome extension, the external code is similarly added to the DOM, but the extension’s popup lacks direct access to the page’s running javascript; and subsequently, a direct way to kill the same setInterval() loop. While the astute coder might suggest that the extension should contain the loop to clean itself up on close, I decided that a singular change in the extension would be easier to handle, rather than changing both the “stereopaw code” and extension – though it may be the less robust decision.

NB: Implementing an observer pattern is likely even better, but in the interest of “keeping it simple,” I decided to go with a loop.


Detecting a Shutdown Event

Chrome Extension components can cross-communicate via message passing. With stereopaw, the injected code’s setInterval() repeatedly sends a message containing track data and an updated timestamp. Upon closing the browser extension, the injected loop will waste resources, and continously send messages to nobody in particular. The problem compounds when the user runs the extension again, as another loop activates, and messages are now sent twice as often. The solution is two-fold: detect when the extension is closed, and then send a shutdown message / inject some “shutdown code” – or both.

We mimic a shutdown event – the popup / content script closing – by doing the following:

  • Establish an extension connection
  • Listen for a port.onDisconnect event, which fires when the popup is no longer active
  • Inject a shutdown function, or pass a shutdown message

Establishing a connection is straightforward. Simply stick the following in a content script:

/popup.js
1
   chrome.extension.connect()

And the latter block in a background page.

/bg.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var connection_listener = function(port) {

    var port_listener = function(event) {

      var msg_listener = function(request, sender, sendResponse) {  //3

          if (request.yourmessageobject_from_injected_code) {

                 /*option 1: a shutdown message*/

                 sendResponse({shutdown:true})

                 /*option 2: inject a shutdown function*/

                 var kill = function() {

                /* 
                 * do what you must, like clear a setInterval 
                 * in your target page's javascript
                 */

                     clearInterval(_interval)
                 }

                 chrome.tabs.executeScript(tab.id,
                           {
                             code: "(" + kill + ")();"
                           });
           }

      chrome.runtime.onMessageExternal.removeListener(msg_listener) //4
      }    

    }

    port.onDisconnect.addListener(port_listener); //2
}

chrome.extension.onConnect.addListener(connection_listener) //1

When the content script is opened, chrome.extension.connect() establishes a connection. This event triggers the connection listener (1) in the background script, which adds a listener for the port.OnDisconnect event (2). Now you’re notified when the extension shuts down.

In this example, when the port.onDisconnect event fires, a message listener (3) is added that sends a shutdown response to the injected code, which is continuously messaging track information to a separate listener in the extension. It’s important to note that various messages may be passed at any given time, so the if-statement acts as a filter for the particular message source that can handle a shutdown response. One could additionally initiate a message through the shared DOM via port.postMessage, but opting for simplicity, I chose to keep the shutdown logic solely in the extension code, as opposed to adding an additional listener in the external “stereopaw code”. Lastly, one can inject shutdown code – the kill() function, as done above.

More detailed examples regarding extension message passing can be found in Google’s documentation.

Comments