Can you write a Chrome extension that runs nothing unless the devtools panel is open? [duplicate] - google-chrome-extension

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?

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.

Related

Downloading a large Blob to local file in ManifestV3 service worker

I have a logging mechanism in place that saves the logs into an array. And I need a way to download the logs into a file.
I had this previously working (on manifest v2) with
const url = URL.createObjectURL(new Blob(reallyLongString, { type: 'text/plain' }));
const filename = 'logs.txt';
chrome.downloads.download({url, filename});
Now I am migrating to manifest v3 and since manifest v3 does not have URL.createObjectURL, you cannot create a url to pass to chrome.downloads.download
Instead it is possible to create a Blob URL using something like
const url = `data:text/plain,${reallyLongString}`;
const filename = 'logs.txt';
chrome.downloads.download({url, filename});
The problem is that chrome.downloads.download seems to have a limit on the number of characters passed in the url argument, and the downloaded file only contains a small part of the string.
So what would be a way to overcome this limitation?
Hopefully, a way to download Blob directly in service worker will be implemented in https://crbug.com/1224027.
Workaround via an extension page
Here's the algorithm:
Use an already opened page such as popup or options
Otherwise, inject an iframe into any page that we have access to
Otherwise, open a new minimized window
async function downloadBlob(blob, name, destroyBlob = true) {
// When `destroyBlob` parameter is true, the blob is transferred instantly,
// but it's unusable in SW afterwards, which is fine as we made it only to download
const send = async (dst, close) => {
dst.postMessage({blob, name, close}, destroyBlob ? [await blob.arrayBuffer()] : []);
};
// try an existing page/frame
const [client] = await self.clients.matchAll({type: 'window'});
if (client) return send(client);
const WAR = chrome.runtime.getManifest().web_accessible_resources;
const tab = WAR?.some(r => r.resources?.includes('downloader.html'))
&& (await chrome.tabs.query({url: '*://*/*'})).find(t => t.url);
if (tab) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: () => {
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL('downloader.html');
iframe.style.cssText = 'display:none!important';
document.body.appendChild(iframe);
}
});
} else {
chrome.windows.create({url: 'downloader.html', state: 'minimized'});
}
self.addEventListener('message', function onMsg(e) {
if (e.data === 'sendBlob') {
self.removeEventListener('message', onMsg);
send(e.source, !tab);
}
});
}
downloader.html:
<script src=downloader.js></script>
downloader.js, popup.js, options.js, and other scripts for extension pages (not content scripts):
navigator.serviceWorker.ready.then(swr => swr.active.postMessage('sendBlob'));
navigator.serviceWorker.onmessage = async e => {
if (e.data.blob) {
await chrome.downloads.download({
url: URL.createObjectURL(e.data.blob),
filename: e.data.name,
});
}
if (e.data.close) {
window.close();
}
}
manifest.json:
"web_accessible_resources": [{
"matches": ["<all_urls>"],
"resources": ["downloader.html"],
"use_dynamic_url": true
}]
Warning! Since "use_dynamic_url": true is not yet implemented don't add web_accessible_resources if you don't want to make your extension detectable by web pages.
Workaround via Offscreen document
Soon there'll be another workaround: chrome.offscreen.createDocument instead of chrome.windows.create to start an invisible DOM page where we can call URL.createObjectURL, pass the result back to SW that will use it for chrome.downloads.download.

Multiple instances of chrome in nodejs

I'm reading a list of urls from a file and then for each url I first launch chrome and then navigate to that page and record some tracing events.
var chromeLauncher = require('chrome-launcher')
var Chrome = require('chrome-remote-interface')
function launchChrome(Someurl){
chromeLauncher.launch({
//ports, flags
}).then((launcher) => {
Chrome(function(chrome){
Tracing.Start();
chrome.Page.navigate(SomeUrl)
Tracing.Complete();
});
});
urlList.on('line', (line) => {
launchChrome(line)
});
However since Chrome is an async function we have multiple chromes being launched simultaneously before tracking begins.
I want the chrome instances to fire in sequence, open url , track and exit.

Communicating between two chrome extensions

I am trying to communicate between two chrome extensions, but unable to do so.
Any help would be great in resolving this issue.
1st extension sending msg in background.js:
chrome.browserAction.onClicked.addListener(
function(tab)
{
chrome.runtime.onConnect.addListener(function(port)
{
port.postMessage({status:"hello"});
});
2nd extension receiving msg in background.js:
var port = chrome.runtime.connect({name: "lkddmaimhocofkfhngkdhdicmldnfdpn"});
port.onMessage.addListener(function(message,sender)
{
alert('listened bg');
});
It seems you are confused with the sending part and receiving part.
Also, there are some differences between onConnect
which fires when a connection is made from either an extension process or a content script,
and onConnectExternal
which fires when a connection is made from another extension.
Take a look at Message External and you can use the following sample code to communicate between two extensions.
1st extension sending msg in background.js:
chrome.browserAction.onClicked.addListener(function() {
var port = chrome.runtime.connect("lkddmaimhocofkfhngkdhdicmldnfdpn");
port.postMessage(...);
});
2nd extension receiving msg in background.js:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// Handle your msg
});
});

Firefox Extension Development

In Chrome Extension Development we have Background Page Concepts. Is any thing similar available in Firefox Extension Development also. While Developing Chrome Extensions I have seen methods like
window.Bkg = chrome.extension.getBackgroundPage().Bkg;
$(function () {
var view = null;
if (Bkg.account.isLoggedIn()) {
view = new Views.Popup();
$("#content").append(view.render().el);
} else {
$("#content").append(Template('logged_out')());
Bkg.refresh();
}
}...........
Where the main logic are written in Background Page(like isLoggedIn etc) and from the Extension Popup page we are calling Background page. Here for instance the background page is always loaded which manages the session. How can we have similar functionality in Firefox Extension Development.
All communication between the background page (main.js) and content scripts (your popup script) occurs via events. You cannot call functions immediately, so you won't receive any return values, but you can send an event from a content script to the background script that sends an event back to the content script and calls a new function, like so:
main.js partial
// See docs below on how to create a panel/popup
panel.port.on('loggedIn', function(message) {
panel.port.emit('isLoggedIn', someBoolean);
});
panel.port.on('refresh', function() {
// do something to refresh the view
});
popup.js
var view = null;
addon.port.on('isLoggedIn', function(someBool) {
if (someBool) {
// Obviously not code that's going to work in FF, just want you to know how to structure
view = new Views.Popup();
$("#content").append(view.render().el);
} else {
$("#content").append(Template('logged_out')());
addon.port.emit('refresh');
}
});
addon.port.emit('loggedIn', 'This is a message. I can pass any var along with the event, but I don't have to');
You should read this stuff:
Panel
Communicating between scripts

How to implement Chrome extension 's chrome.tabs.sendMessage API in Firefox addon

I'm working on a Firefox addon development with Addon-Builder. I have no idea about how to implement Chrome extension 's chrome.tabs.sendMessage API in Firefox addon. The code is like this (the code is in the background.js, something like main.js in the Firefox addon):
function sendMessageToTabs(message, callbackFunc){
chrome.tabs.query({}, function(tabsArray){
for(var i=0; i<tabsArray.length; i++){
//console.log("Tab id: "+tabsArray[i].id);
chrome.tabs.sendMessage(tabsArray[i].id,message,callbackFunc);
}
});
}
So, How can I achieve this?
In add-ons build using the Add-on SDK, content scripts are managed by main.js. There's no built-in way to access all of your add-on's content scripts. To send a message to all tabs, you need to manually keep track of the content scripts.
One-way messages are easily implemented by the existing APIs. Callbacks are not built-in, though.
My browser-action SDK library contains a module called "messaging", which implements the Chrome messaging API. In the following example, the content script and the main script use an object called "extension". This object exposes the onMessage and sendMessage methods, modelled after the Chrome extension messaging APIs.
The following example adds a content script to every page on Stack Overflow, and upon click, the titles of the tabs are logged to the console (the one opened using Ctrl + Shift + J).
lib/main.js
// https://github.com/Rob--W/browser-action-jplib/blob/master/lib/messaging.js
const { createMessageChannel, messageContentScriptFile } = require('messaging');
const { PageMod } = require('sdk/page-mod');
const { data } = require('sdk/self');
// Adds the message API to every page within the add-on
var ports = [];
var pagemod = PageMod({
include: ['http://stackoverflow.com/*'],
contentScriptWhen: 'start',
contentScriptFile: [messageContentScriptFile, data.url('contentscript.js')],
contentScriptOptions: {
channelName: 'whatever you want',
endAtPage: false
},
onAttach: function(worker) {
var extension = createMessageChannel(pagemod.contentScriptOptions, worker.port);
ports.push(extension);
worker.on('detach', function() {
// Remove port from list of workers when the content script is deactivated.
var index = ports.indexOf(extension);
if (index !== -1) ports.splice(index, 1);
});
}
});
function sendMessageToTabs(message, callbackFunc) {
for (var i=0; i<ports.length; i++) {
ports[i].sendMessage(message, callbackFunc);
}
}
// Since we've included the browser-action module, we can use it in the demo
var badge = require('browserAction').BrowserAction({
default_title: 'Click to send a message to all tabs on Stack Overflow'
});
badge.onClicked.addListener(function() {
sendMessageToTabs('gimme title', function(response) {
// Use console.error to make sure that the log is visible in the console.
console.error(response);
});
});
For the record, the interesting part of main.js is inside the onAttach event.
data/contentscript.js
extension.onMessage.addListener(function(message, sender, sendResponse) {
if (message === 'gimme title') {
sendResponse(document.title);
}
});

Resources