Capture screenshot of Electron window before quitting - javascript

In an Electron app, I can take a screenshot of my window from the main process using this:
let win = new BrowserWindow(/* ... */);
let capturedPicFilePath = /* where I want that saved */
win.capturePage((img) => {
fs.writeFile(capturedPicFilePath, img.toPng(), () => console.log(`Saved ${capturedPicFilePath}`))
})
Awesome. Now I'd like to do that right before app quits. Electron emits a particular event for that, that I tried to use:
Event: 'before-quit' : emitted before the application starts closing its windows.
Problem: if I use the same code as above in a handler for that event, the file is created but empty.
I'm guessing it's because the screenshot is taken in an asynchronous way, and the window is already closed when it happens.
So this does not work for me:
app.on('before-quit', (event) => {
win.capturePage(function(img) {
fs.writeFile(capturedPicFilePath, img.toPng(), () => console.log(`Saved ${capturedPicFilePath}`))
})
})
Edit 1 : Doing this in the renderer process with window.onbeforeunload fails too. It's also too late to perform the screenshot. I get this in the main console (i.e. it goes to terminal):
Attempting to call a function in a renderer window that has been closed or released.
Context: for now I'm exploring the limits of what is possible to do with screen capture (essentially for support purposes), and found that one. Not sure yet what I would do with that edge case, I thought about it not just for support, I'm also considering displaying at startup a blurred pic of previous state (some parts of my app take 1-2 seconds to load).
Any ideas?

I have had a similar problem before, I got around it by using the window close event and then preventing it from closing. Once my action had performed I then ran app.quit().
window.on('close', function (event) {
event.preventDefault();
let capturedPicFilePath = /* where you want it saved */
window.capturePage((img) => {
fs.writeFile(capturedPicFilePath, img.toPng(), () =>
console.log(`Saved ${capturedPicFilePath}`));
app.quit(); // quit once screenshot has saved.
});
});
Hope this helps!

Related

Electron.js: how to create a separate download webContents.session for a window?

I have an Electron file manager app, which creates 2 windows for different purposes:
quickView a renderer window used for previewing local files. It uses "will-download" listener for detecting unsupported files by preventing download.
main the main renderer window. It uses "will-download" listener for downloading files.
Each with their own will-download listeners attached to their session. But for some reason the quickView listener overwrites the main listener.
Window 1
In the following line I'm creating a will-download listener for the "main" process. The purpose of this listener is to download files:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/55fd2462cf83898610883191807b7488fb5bdf89/src/utils/downloadManager.js#L133
win.webContents.session.on('will-download', listener)
The windows.main parameter in the line below is the win reference in the line above:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/55fd2462cf83898610883191807b7488fb5bdf89/src/electronMain.js#L516
const resultInfo = await downloadManager.download(windows.main, {
Window 2
In the following line I'm creating a will-download listener for the "quickView" window. The purpose of this listener is to detect unsupported files (which triggers download event in Chromium) and prevent the download event:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/55fd2462cf83898610883191807b7488fb5bdf89/src/electronMain.js#L232
windows.quickViewWindow.webContents.session.once('will-download', _willDownloadHandler)
I haven't found another way to detect unsupported files, which is why I'm using a will-download event in the first place.
Problem
For some reason the will-download handler of the quickView window overrides the handler of the main:
When I trigger the app update download event here (from the main process):
https://github.com/aleksey-hoffman/sigma-file-manager/blob/55fd2462cf83898610883191807b7488fb5bdf89/src/electronMain.js#L516
const resultInfo = await downloadManager.download(windows.main, {
It triggers the event handler of the quickView renderer window:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/55fd2462cf83898610883191807b7488fb5bdf89/src/electronMain.js#L241
function _willDownloadHandler (event, item, webContents) {
...
windows.main.webContents.send('load:webview::failed', {path: fileURL})
Partial fix
I partially fixed the problem in this commit by specifying a custom partition name for the session of the quickView window, so it doesn't use the default session and do not overwrite the will-download listener created by main:
Main process:
windows.quickViewWindow = new electron.BrowserWindow({
...
webPreferences: {
partition: 'quickPreview',
...
windows.quickViewWindow.webContents.session.once(
'will-download',
(event, item, webContents) => {
event.preventDefault()
...
}
)
quickViewWindow.html:
ipcRenderer.on('load:webview', (event, data) => {
...
webviewNode.setAttribute('partition', 'quickPreview')
But this fix resulted in another problem:
The quick view window stopped working in production build (perhaps it has something to do with the protocol not working with non default session partition)
Setting a custom partition to a webview causes the Windows protocol link association pop up in production when the window containing this webview is created:
I think it might be caused by the custom app:// protocol created by the electron-builder-plugin. It seems the pop up is triggered by the "app" link.
Or maybe it's happening because I'm setting the protocol incorrectly when I'm creating the window somewhere around this line:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/47ce65bdac78e5c9b17315f16623d40d81dcf1bb/src/electronMain.js#L203
To reproduce:
Download the project
git clone https://github.com/aleksey-hoffman/sigma-file-manager.git
cd sigma-file-manager
npm install
git checkout 47ce65b
npm run electron:build
Install the built app from ./dist_electron
During the app launch you can see the pop up
Notes:
I just rolled back the 47ce65b commit and added some test values
so it's easier to debug
To switch to the latest commit and create the production build:
git checkout 5246252
npm run electron:build
All the console.log() inside electronMain.js are displayed in the terminal (command line) window (not the developer tools console).
To trigger quick view feature:
Select any supported file (image/ text / etc) on the navigator page
Press Space (the quick view window should open)
To trigger a download event, you can just open "Navigator" page and drag & drop any file (or website URL) from the internet. It will trigger the wrong will-download event handler (the handler of quickView window), and you should see the console message.
The quickView window containing this webview is created on app.ready event. When partition is specified, the pop up will appear right after the quickView window is created:
https://github.com/aleksey-hoffman/sigma-file-manager/blob/47ce65bdac78e5c9b17315f16623d40d81dcf1bb/src/electronMain.js#L698
UPDATE:
Smaller reproduction example:
I was able to reproduce it with this code:
let window1 = null
let window2 = null
electron.app.on('ready', async () => {
createWindow1()
createWindow2()
setTimeout(() => {
console.log('trigger window 1 download')
window1.webContents.downloadURL('https://stackoverflow.com')
}, 1000)
})
function createWindow2 () {
window1.webContents.session.once('will-download', downloadHandler1)
window2.webContents.session.once('will-download', downloadHandler2)
}
function createWindow1 () {
window1 = new electron.BrowserWindow()
window1.loadURL('app://./quickViewWindow.html')
window1.webContents.session.once('will-download', downloadHandler1)
}
function createWindow2 () {
window2 = new electron.BrowserWindow()
window2.loadURL('app://./quickViewWindow.html')
window2.webContents.session.once('will-download', downloadHandler2)
}
function downloadHandler1 (event, item, webContents) {
console.log('window will-download handler 1')
}
function downloadHandler2 (event, item, webContents) {
console.log('window will-download handler 2')
}
When the setTimeout runs, I see the following console.log() messages:
trigger window 1 download
window will-download handler 1
window will-download handler 2
As you can see from the log, the will-download event triggers event handlers of both windows
If I specify a separate partition for each window, the problem with shared event handlers gets resolved, but I get the 2nd problem mentioned above - the link association pop up on launch
window1 = new electron.BrowserWindow({
webPreferences: {
partition: 'partition1',
}
})
window2 = new electron.BrowserWindow({
webPreferences: {
partition: 'partition2',
}
})
I figured it out. If that's a wrong way to do it, please someone let me know.
Here's how I fixed it:
Fix for the problem #1:
Set a custom partition name for the window, so it uses its own webContents.session instead of sharing the default one.
Main process:
windows.quickViewWindow = new electron.BrowserWindow({
...
webPreferences: {
partition: 'quickView',
...
windows.quickViewWindow.webContents.session.once(
'will-download',
(event, item, webContents) => {
event.preventDefault()
...
}
)
quickViewWindow.html:
ipcRenderer.on('load:webview', (event, data) => {
...
webviewNode.setAttribute('partition', 'quickView')
Fix for the problem #2:
Set file:// protocol in the production path for the window URL:
productionPath = `file://${__static}/quickViewWindow.html`
Here's the commit: https://github.com/aleksey-hoffman/sigma-file-manager/commit/31208809cda7614a7c2f32237ae14f6c9c602f8f

Enforce single instance of electron app on Windows, but make a launcher click open new window

I want to enforce a Single Instance for my electron app. When a user clicks the app shortcut icon on Windows, I want to enforce the single instance, but still open up a new window on the main instance when this happens.
All other current solutions seem to just quit the new instance and refocus the current instance.
const singleInstanceLock = app.requestSingleInstanceLock();
if (!singleInstanceLock) {
app.exit();
} else {
app.focus();
}
app.on('second-instance', (_event: Electron.Event, argv: string[]) => {
app.focus();
// code to open up second window lives here. As far as I can tell, it doesnt get called
});
app.exit(); Is a more severe way of quitting an app which quits all the instances of Electron instead of just the current one.
Try using app.quit() instead.
Also, separately, I'd re-arrange your code so it's like this:
const singleInstanceLock = app.requestSingleInstanceLock();
if (!singleInstanceLock) {
app.quit();
} else {
app.on('second-instance', (_event: Electron.Event, argv: string[]) => {
app.focus();
// Code to open up second window goes here.
});
}
Replacing your first app.focus() with the app.on('second-instance'.
This is because you're running app.requestSingleInstanceLock() when the app first starts (as it should), but doing app.focus() wouldn't do anything since the app was only just opened, and there wouldn't be any windows to focus.

JS Performing actions on a popup window

So lately I have been learning JS and trying to interact with webpages, scraping at first but now also doing interactions on a specific webpage.
For instance, I have a webpage that contains a button, I want to press this button roughly every 30 seconds and then it refreshes (and the countdown starts again). I wrote to following script to do this:
var klikCount = 0;
function getPlayElement() {
var playElement = document.querySelector('.button_red');
return playElement;
}
function doKlik() {
var playElement = getPlayElement();
klikCount++;
console.log('Watched ' + klikCount);
playElement.click();
setTimeout(doKlik, 30000);
}
doKlik()
But now I need to step up my game, and every time I click the button a new window pops up and I need to perform an action in there too, then close it and go back to the 'main' script.
Is this possible through JS? Please keep in mind I am a total javascript noob and not aware of a lot of basic functionality.
Thank you,
Alex
DOM events have an isTrusted property that is true only when the event has been generated by the user, instead of synthetically, as it is for the el.click() case.
The popup is one of the numerous Web mechanism that works only if the click, or similar action, has been performed by the user, not the code itself.
Giving a page the ability to open infinite amount of popups has never been a great idea so that very long time ago they killed the feature in many ways.
You could, in your own tab/window, create iframes and perform actions within these frames through postMessage, but I'm not sure that's good enough for you.
Regardless, the code that would work if the click was generated from the user, is something like the following:
document.body.addEventListener(
'click',
event => {
const outer = open(
'about:blank',
'blanka',
'menubar=no,location=yes,resizable=no,scrollbars=no,status=yes'
);
outer.document.open();
outer.document.write('This is a pretty big popup!');
// post a message to the opener (aka current window)
outer.document.write(
'<script>opener.postMessage("O hi Mark!", "*");</script>'
);
// set a timer to close the popup
outer.document.write(
'<script>setTimeout(close, 1000)</script>'
);
outer.document.close();
// you could also outer.close()
// instead of waiting the timeout
}
);
// will receive the message and log
// "O hi Mark!"
addEventListener('message', event => {
console.log(event.data);
});
Every popup has an opener, and every different window can communicate via postMessage.
You can read more about window.open in MDN.

Chrome extension popup close fired at wrong time

I have a chrome extension which is currently consists of a background page and a popup page. There are some initialization happens when the popup is opened. I am using DOM event
doc.addEventListener('DOMContentLoaded', function() { ... }
And the behaviour is correct.
The issue is when closing the popup. Since chrome popup does not throw unload event I am using what was suggested here. Here is my code
popup.js
bgPage = chrome.extension.getBackgroundPage();
bgPageManager = bgPage.manager(); // This is the exposed api from bg page
bgPageManager.init(chrome.runtime.connect({
name: 'P1'
}));
Here connecting to the runtime and sending the port to background page so that it can listen to onDisconnect event.
Background.js
function init(port) {
port.onDisconnect.addListener(function() {
// Clean up happens here
stateManager.unregister();
});
}
This works as well.
But the issue is, this onDisconnect getting fired when the popup is getting opened, not when it is getting closed
The documentation for onDisconnect event is
An object which allows the addition and removal of listeners for a Chrome event.
Which is not every helpful ;)
So is there anything wrong I am doing, or any way when I can detect the popup close?
Unless there's something listening to chrome.runtime.onConnect in the popup page (from your comment, doesn't seem so), chrome.runtime.connect() will return a Port that immediately closes (as no-one was willing to listen).
You're definitely making it more difficult than it should be with involving getBackgroundPage though. The popup can initiate the port itself. All you need to do is:
// popup code
var bgPort = chrome.runtime.connect({name: "P1"});
// background code
var popupPort;
chrome.runtime.onConnect.addListener(function(port) {
if(port.name == "P1") {
popupPort = port;
popupPort.onDisconnect.addListener(function() {
/* Clean up happens here */
});
}
});
In case you want to preserve what you already have, the minimal code to put into the popup is this:
var bgPort;
chrome.runtime.onConnect.addListener(function(port) {
if(port.name == "P1") {
bgPort = port;
}
});
Note that in all cases you need to keep a reference to the Port object on both sides. If the garbage collector collects it, the port will be disconnected.
Finally, a port name is optional; if it's the only port you use, you can drop the code that sets/checks the name.

In a Firefox restartless add-on, how do I run code when a new window opens (listen for window open)?

I am starting to build a restartless Firefox add-on and I am having trouble setting up the bootstrap.js. Everyone seems to agree that the core of a bootstrap.js is pretty much boilerplate code, along these lines:
const Cc = Components.classes;
const Ci = Components.interfaces;
function startup() {
let wm = Cc["#mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
// then can control what happens with domWindow.document
}
}
function shutdown() {}
function install() {}
function uninstall() {}
This code works and I can control things in the existing windows. For example, domWindow.alert("text") successfully creates a standard alert saying "text" on every window that is currently open.
However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?
Edit: Using the nsWindowMediator class and the code sample from MDN, I now have this:
var windowListener = {
onOpenWindow: function (aWindow) {
try {
let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
domWindow.addEventListener("load", function () {
domWindow.removeEventListener("load", arguments.callee, false);
//window has now loaded now do stuff to it
domWindow.alert("text");
}, false);
} catch (err) {
Services.prompt.alert(null, "Error", err);
}
},
onCloseWindow: function (aWindow) {},
onWindowTitleChange: function (aWindow, aTitle) {}
};
function startup(aData, aReason) {
// Load into any existing windows
try {
let wm = Cc["#mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
loadIntoWindow(domWindow);
}
} catch (err) {
Services.prompt.alert(null, "Error", err);
}
Services.wm.addListener(windowListener);
}
However, there is still no output from the onOpenWindow call - the "text" alert does not appear, nor does the error alert in the catch block. I can confirm that onOpenWindow is actually being entered; if I put a Services.prompt.alert() at the beginning of onOpenWindow, I get the alert when I create a new window. Unfortunately, I get an infinite loop of alerts and I have no idea why.
However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?
The correct way to act on each window when it opens is to use addListener() from nsIWindowMediator. The example code below does this. The nsIWindowMediator is included in Services.jsm and is accessed through Services.wm.addListener(WindowListener). In order to use a window listener, you have to pass it an nsIWindowMediatorListener (ref2) object. An nsIWindowMediatorListener contains three keys: onOpenWindow, onCloseWindow, and onWindowTitleChange. Each should be defined as a function which will be called when the appropriate event occurs.
The MDN document How to convert an overlay extension to restartless in "Step 9: bootstrap.js" contains an example of a basic bootstrap.js which will run the code in the function loadIntoWindow(window) for each currently open browser window and any browser window which opens in the future. I have used code modified from this in a couple of different add-ons. The example is substantially similar to the code you are already using. The example is (slightly modified):
const Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/Services.jsm");
function startup(data,reason) {
// Load this add-ons module(s):
Components.utils.import("chrome://myAddon/content/myModule.jsm");
// Do whatever initial startup stuff is needed for this add-on.
// Code is in module just loaded.
myModule.startup();
// Make changes to the Firefox UI to hook in this add-on
forEachOpenWindow(loadIntoWindow);
// Listen for any windows that open in the future
Services.wm.addListener(WindowListener);
}
function shutdown(data,reason) {
if (reason == APP_SHUTDOWN)
return;
// Unload the UI from each window
forEachOpenWindow(unloadFromWindow);
// Stop listening for new windows to open.
Services.wm.removeListener(WindowListener);
// Do whatever shutdown stuff you need to do on add-on disable
myModule.shutdown();
// Unload the module(s) loaded specific to this extension.
// Use the same URL for your module(s) as when loaded:
Components.utils.unload("chrome://myAddon/content/myModule.jsm");
// HACK WARNING: The Addon Manager does not properly clear all add-on related caches
// on update. In order to fully update images and locales, their
// caches need clearing here.
Services.obs.notifyObservers(null, "chrome-flush-caches", null);
}
function install(data,reason) { }
function uninstall(data,reason) { }
function loadIntoWindow(window) {
/* call/move your UI construction function here */
}
function unloadFromWindow(window) {
/* call/move your UI tear down function here */
}
function forEachOpenWindow(todo) {
// Apply a function to all open browser windows
var windows = Services.wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements())
todo(windows.getNext().QueryInterface(Ci.nsIDOMWindow));
}
var WindowListener = {
onOpenWindow: function(xulWindow) {
var window = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
function onWindowLoad() {
window.removeEventListener("load",onWindowLoad);
// Only add UI changes if this is a browser window
if (window.document.documentElement.getAttribute("windowtype")
== "navigator:browser")
loadIntoWindow(window);
}
window.addEventListener("load",onWindowLoad);
},
onCloseWindow: function(xulWindow) { },
onWindowTitleChange: function(xulWindow, newTitle) { }
};
While there is quite a bit more that your might want to do in your bootstrap.js code, the above is organized reasonably well and keeps all of the code to load into the Firefox UI within loadIntoWindow(window) and unloading the UI within unloadFromWindow(window). However, it should be noted that some UI elements you should only be adding/removing once (e.g. australis widgets, like buttons) and other elements (e.g. direct changes to the Firefox DOM) have to be added once in each window.
Unfortunately, I get an infinite loop of alerts and I have no idea why.
One of the significant differences between this example and what you are currently using is the test for the type of window that has opened. This is done so that we are only acting on newly opened windows which are browser windows instead of all newly opened windows:
if (window.document.documentElement.getAttribute("windowtype") == "navigator:browser")
loadIntoWindow(window);
The problem you describe of getting an infinite loop of alert() popups is caused by not checking to make sure that you are only acting on browser windows. The alert() popup is a window. Thus, you are calling alert() for every alert() window you open which, of course, just opens another alert() window on which you call alert(). This is your infinite loop.
Additional references:
1. Working with windows in chrome code
However, I can't find any code that will allow me to do things in new windows
When working with XPCOM objects you generally want to study their interfaces, which are often found on MDN. In this case your starting point would be nsIWindowMediator, since that's the service you're using in line 5.
As you can see it has an addListener function, which takes a parameter implementing nsIWindowMediatorListener. There's a code-example right there on the page.
But let's assume for the moment there isn't a code example. You could search for the interface on MDN, but it isn't listed. The next step would be searching MXR for the .idl. idl = interface description language
Once you got the interface contract you can more or less just implement it in javascript, at least for listeners. Implementing your own xpcom services would be a little more complicated.
Searching the addon sdk can often provide some hints too. In this case they don't seem to be using .addListener, but the file hints at another interesting service, which in turn you can find on MDN: nsIWindowWatcher.
Basically, if you're writing restartless addons you're rummaging through the entrails of firefox and will have to do some detective work to find the exact components you need. If you want something more convenient I would recommend the addon sdk, which provides a more organized but also more restricted set of commonly used APIs

Categories