How to Launch/Focus electron app from browser link? - node.js

I need to launch the electron app or focus it ( if already launched ) from a browser link. I have searched and tried many solutions but not getting it to work, so if someone has any experience with it, can you please help?
Here is the code:
// Single instance app ==========
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
// Register private URI scheme for the current user when running for the first time
app.setAsDefaultProtocolClient('x-protocol');
When I try to launch using this code, I get the goTheLock value as false, but the second-instance event is not getting fired, not sure why.
Version Details:
platform: Windows 10
electron: 8.5.3
electron-builder: 21.2.0
Update:
I added a delay of 5 seconds before quitting the app inside !gotTheLock, and in that case, I'm getting the event.
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
delay(5000); // 5 seconds delay
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}

I don't understand. If you want to launch the app from a browser link then why you're implementing a second-instance? second-instance will fire if you open an application for a second time.
like this,
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
if (win) {
app.quit();
}
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (win) {
win.show();
win.focus();
}
})
}

Related

Debugging and performance profiling ManifestV3 extension Service worker

I'm learning how to build chrome extensions with manifest v3, what I'm trying to do is the following
In my extension background.js (service worker) I want to do this:
connect to WebSocket to get data updates
reconnect to the Websocket when service-worker wake up
those are tasks to get data updates from a WebSocket and update the badge text and send notifications.
I need to do these tasks while not relying on having a port open with the popup or a content script.
I'm using Chrome Alarms to wake up the service worker
it may sound weird that I have to reconnect every time the service worker wakes up considering chrome is shutting the service worker down like every 15s or less once I close the extensions dev tools (which makes me cry blood) but it is better than sending XHR periodically using Chrome alarms, which would result in a lot more requests being sent to an API, so reconnecting to the Websocket seems less problematic.
I'm having a super hard time debugging my service worker (background script) in my chrome extension. The problem is when I have dev tools open the service worker will NEVER go inactive, and what I'm trying to do is identify when the SW wakes up to perform tasks, super-duper weird because I need dev tools open to debugging...
how do you debug an extension SW without devtools open?
do you/anyone reading this have any recommendations/thoughts on what I want to do with this extension and the pain process for debugging the SW?
here is the code I have for the background.js
const extension = {
count: 0,
disconnected: false,
port: {} as any,
ws: null,
};
const fetchData = () => {
return fetch(
'https://api.coingecko.com/api/v3/coins/ethereum?localization=incididuntelit&tickers=false&market_data=true&community_data=true&developer_data=true&sparkline=true'
).then((res) => res.json());
};
// Chrome Alarms
const setupAlarms = () => {
console.log('###ALARMS-SETUP');
chrome.alarms.get('data-fetch', (alarm) => {
if (!alarm) {
chrome.alarms.create('data-fetch', { periodInMinutes: 0.1 });
}
});
chrome.alarms.get('client-pinging-server', (alarm) => {
if (!alarm) {
chrome.alarms.create('client-pinging-server', { periodInMinutes: 0.1 });
}
});
chrome.alarms.onAlarm.addListener((e) => {
if (e.name === 'data-fetch') {
fetchData()
.then((res) => {
// console.log('###ETHEREUM:', res.market_data.current_price.cad);
chrome.action.setBadgeText({ text: `${Math.round(Math.random() * 100)}` });
})
.catch((error) => console.error('###ERROR', error));
}
if (e.name === 'client-pinging-server') {
if (!extension.ws || !extension.ws.getInstance()) {
console.log('\n');
console.log('###reconnecting...', extension.ws);
console.log('\n');
extension.ws = WebSocketClient();
extension.ws.connect();
}
if (extension.ws.getInstance()) {
console.log('###sending-client-ping');
extension.ws.getInstance().send(JSON.stringify({ message: 'client ping - keep alive' }));
}
}
});
};
// ON INSTALL
chrome.runtime.onInstalled.addListener(async (e) => {
const API_URL = 'ws://localhost:8080';
chrome.storage.local.set({ apiUrl: API_URL, count: 0 });
console.info('###Extension installed', e);
chrome.action.setBadgeText({ text: '0' });
chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
});
// ON SUSPEND
chrome.runtime.onSuspend.addListener(() => {
console.log('Unloading.');
chrome.action.setBadgeText({ text: `off` });
});
function WebSocketClient() {
let instance = null;
const connect = () => {
return new Promise((resolve, reject) => {
const ws = new WebSocket('ws://localhost:8080');
const onOpen = () => {
instance = ws;
console.log('###websocket:connected', instance);
return resolve(ws);
};
const onError = (event) => {
console.log('###INIT-FAILED', event);
ws.close(1000, 'closing due to unknown error');
return reject('failed to connect to websocket');
};
const onClose = () => {
console.log('###websocket:disconnected');
instance = null;
// reconnect is happening in the alarm callback
};
ws.onopen = onOpen;
ws.onerror = onError;
ws.onclose = onClose;
});
};
const getInstance = () => {
return instance;
};
return {
connect,
getInstance,
};
}
self.addEventListener('install', async (event) => {
console.log('====install', event);
chrome.action.setBadgeBackgroundColor({ color: '#a6e22e' });
});
self.addEventListener('activate', async (event) => {
console.log('====activate', event);
chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
extension.ws = WebSocketClient();
extension.ws.connect();
setupAlarms();
});
self.addEventListener('push', function (event) {
// Keep the service worker alive until the notification is created.
event.waitUntil(
self.registration.showNotification('Testing PUSH API', {
body: 'coming from push event',
})
);
});
Since Devtools can attach to multiple contexts at once, you can open it for another context so the SW will be secondary and thus will be able to unload normally.
Debugging
Open any visible page of the extension or, if there are none, its manifest.json URL:
chrome-extension://ID/manifest.json where ID is the extension's id
Open devtools and switch to its Application tab, then choose Service worker on the left.
Click start (if shown) to start the service worker, click the background script name to open it in the Sources panel, set breakpoints, etc.
Click stop to stop the service worker, optionally click Update at the top, and skip waiting in the middle (if shown) to force an update.
Click start again - your breakpoints will trigger.
Performance profiling
Open any visible page of the extension or, if there are none, its manifest.json URL:
chrome-extension://ID/manifest.json where ID is the extension's id
Open devtools and switch to its Application tab, then choose Service worker on the left.
Duplicate the tab, open devtools there, go to Performance tab, click "Start" or press Ctrl-E
Switch back to the first tab and click the start button (or stop first, then start). In certain cases you may also see skip waiting in the middle, click it then.
Switch to the second tab, wait for a while and click the recording button again or press Ctrl-E.
Notes
When the service worker is started you can see its context in the Sources panel on the left (in the files panel), on the top-right (in the threads panel), in the console toolbar (the context selector).
This may seem unwieldy at first, but once you try and get the knack of it, it's quite trivial and can even beat devtools that's shown when clicking the "service worker" link in chrome://extensions page because this one a) shows extension's localStorage/IndexedDB in devtools, b) provides control over service worker lifetime/execution, c) supports performance profiling of SW startup.
Note that the ManifestV3 documentation's claims about benefits provided by service workers for extensions are largely exaggerated or completely false, e.g. in your case it's clear that restarting the service worker is bad, so you should use a port to prolong the SW's lifetime as much as possible.

chrome.devtools.inspectedWindow.eval - frameURL

I am trying to get the selected element to the sidebar pane in my chrome extension.
It's working fine if the page has no frames when the element is in the frame, it's not working.
As per the document I have to pass the frameURL, but how do I get the frame or Iframe URL?
Thank you.
Note: This issue is duplicate that was opened in 3 years ago, but still no solution there, so re-opening it again.
In devtools.js
chrome.devtools.panels.elements.createSidebarPane(name, (panel) => {
// listen for the elements changes
function updatePanel() {
chrome.devtools.inspectedWindow.eval("parseDOM($0)", {
frameURL: // how to pass dynamic
useContentScriptContext: true
}, (result, exceptipon) => {
if (result) {
console.log(result)
}
if (exceptipon) {
console.log(exceptipon)
}
});
}
chrome.devtools.panels.elements.onSelectionChanged.addListener(updatePanel);
});
I ran into this as well. I ended up needing to add a content_script on each page/iframe and a background page to help pass messages between devtools and content scripts.
The key bit is that in the devtools page, we should ask the content_scripts to send back what their current url is. For every content script that was registered, we can then call chrome.devtools.inspectedWindow.eval("setSelectedElement($0)", { useContentScriptContext: true, frameURL: msg.iframe } );
Or in full:
chrome.devtools.panels.elements.createSidebarPane( "example", function( sidebar ) {
const port = chrome.extension.connect({ name: "example-name" });
// announce to content scripts that they should message back with their frame urls
port.postMessage( 'SIDEBAR_INIT' );
port.onMessage.addListener(function ( msg) {
if ( msg.iframe ) {
// register with the correct frame url
chrome.devtools.panels.elements.onSelectionChanged.addListener(
() => {
chrome.devtools.inspectedWindow.eval("setSelectedElement($0)", { useContentScriptContext: true, frameURL: msg.iframe } );
}
);
} else {
// otherwise assume other messages from content scripts should update the sidebar
sidebar.setObject( msg );
}
} );
}
);
Then in the content_script, we should only process the event if we notice that the last selected element ($0) is different, since each frame on the page will also handle this.
let lastElement;
function setSelectedElement( element ) {
// if the selected element is the same, let handlers in other iframe contexts handle it instead.
if ( element !== lastElement ) {
lastElement = element;
// Pass back the object we'd like to set on the sidebar
chrome.extension.sendMessage( nextSidebarObject( element ) );
}
}
There's a bit of setup, including manifest changes, so see this PR for a full example:
https://github.com/gwwar/z-context/pull/21
You can found url of the frame this way:
document.querySelectorAll('iframe')[0].src
Assuming there is at lease one iframe.
Please note, you cannot use useContentScriptContext: true, as it will make the script execute as a context page (per documentation) and it will be in a separate sandboxed environment.
I had a slightly different problem, but it might be helpful for your case too, I was dynamically inserting an iframe to a page, and then tried to eval a script in it. Here the code that worked:
let win = chrome.devtools.inspectedWindow
let code = `
(function () {
let doc = window.document
let insertFrm = doc.createElement('IFRAME')
insertFrm.src = 'about:runner'
body.appendChild(insertFrm)
})()`
win.eval(code, function (result, error) {
if (error) {
console.log('Eror in insertFrame(), result:', result)
console.error(error)
} else {
let code = `
(function () {
let doc = window.document
let sc = doc.createElement('script')
sc.src = '${chrome.runtime.getURL('views/index.js')}'
doc.head.appendChild(sc)
})()`
win.eval(code, { frameURL: 'about:bela-runner' }, function (result, error) {
if (error) {
console.log('Eror in insertFrame(), result:', result)
console.error(error)
}
})
}
})

Chrome extension background script sometimes does not run after install or update

I have had recent reports of a chrome extension that I develop that stops working after an update or a fresh install. The background script seems to not start at all.
There is no response to messages sent to it from the content scripts.
There is no process for it in the task manager.
Opening background page from chrome://extensions does not show any activity in the console, or show any source files.
Profiling, memory snapshot buttons are disabled.
Once this issue appears, it persists for the chrome profile even after reloading or uninstalling/reinstalling the extension.
Restarting chrome resolves the problem.
The issue has been seen on chrome v79. But I cannot say for sure that it is exclusive to this version, as the issue is difficult to reproduce and seemingly random.
Has anyone seen such an issue, or has any ideas what to look for? I am happy to update my question with any new info I have or with any info you need.
Edit:
Here is my webNavigation listener, which is used to inject content scripts. This handler is wired up in the 'root' context of the background script (not asynchronously inside an event handler)
chrome.webNavigation.onCompleted.addListener((details) ⇒ {
if(details.frameId === 0) {
injectScript(
'js/contentScript.js',
details.tabId,
details.frameId,
details.url
).catch((e) ⇒ {});
}
}
The injectScript function is as follows
export const injectScript = ƒ (scriptPath,tab,frame,tabUrl) {
return new Promise((res,rej) ⇒ {
let options = {
file : scriptPath,
allFrames : false,
frameId : frame,
matchAboutBlank: false,
runAt : 'document_idle',
};
const cb = ƒ () {
if (chrome.runtime.lastError) {
let err = new Error('Could not inject script');
capture(err,{
...options,
tabUrl,
lastError : chrome.runtime.lastError.message,
});
rej(err);
}else{
res();
}
};
if (tabUrl.indexOf('.salesforce.com') !== -1) {
window.setTimeout(() => {
chrome.tabs.executeScript(tab,options,cb);
},500);
}else{
chrome.tabs.executeScript(tab,options,cb);
}
});
};
Note above, the capture function reports the error to a backend and I cannot see it being reported there as well. Cannot add a breakpoint in code because no source appears in the background page, as noted above.
A background service worker is loaded when it is needed, and unloaded when it goes idle.
https://developer.chrome.com/docs/extensions/mv3/service_workers/
You can use the following methods:
// Keep heartbeat
let heartTimer;
const keepAlive = () => {
heartTimer && clearTimeout(heartTimer);
heartTimer = setTimeout(() => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
console.info('[heartbeat]')
tabs.length && chrome.tabs.sendMessage(
tabs[0].id,
{ action: "heartbeat" }
);
});
keepAlive();
}, 10000);
};
keepAlive();

Disable windows key in Electron

I'm making an electron desktop app. I want to disable windows key and function keys while the app is on
I tried using the following code ... it registers the event but the windows menu opens anyways
$(document).keydown(function(e){
if (e.keyCode == 37) {
alert( "windows key pressed" );
return false;
}
});
Any help?
You can try this, but unforunately it will become a global shortcut, meaning when the window doesn't have focus it will still be registered. Try putting a console.log() to see when it fires. win is your electron window variable
const {app, globalShortcut} = require('electron');
win = new BrowserWindow();
globalShortcut.register('Super', () => {
if (win.isFocused()) {
// do something
}
});
You can check the docs here: docs
Or try to use this module here: electron-localshortcut
electronLocalshortcut.register(win, 'Super', () => {
console.log('Windows Button pressed');
return false;
});

Detect if application has been closed or paused

I'm developing my first FirefoxOS app and I need it to backup some data when the user closes or pauses the app. How can I detect this?
I've tried "window.onunload" but it doesn't seem to work.
Thank you.
You should be able to use page visibility API for background switch.
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
console.log("App is hidden");
} else {
console.log("App has focus");
} });
Event launch visible only if app lost focus.
document.addEventListener('visibilitychange', function() {
if (document['visibilityState'] == "hidden") {
//code if is user out app, no close app
} else if(document['visibilityState'] == 'visible') {
//code if user return on app
}
}, false);

Resources