I am tying to make single instance Electron application.
I am using app.makeSingleInstance , see my sample below.
SingleInstance issue with middle click :
Single Instance works if I click on app.exe 2nd time
It does not work if I middle click on a link inside my app
What I need:
Make electron app singleInstance and ensure it remaisn single instance even with middle click.
I dont want to compeltey disable middle click in my app as at some places, I have a use case for them on non-link items
How to reproduce:
use repo: https://github.com/electron/electron-quick-start
replace existing with my index.html and main.js , see below
npm install and then npm start
index.html:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Hello World!</title></head>
<body>
<h1>app.makeSingleInstance()</h1>
Middle Click on it
</body>
</html>
main.js
const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const path = require('path')
const url = require('url')
let mainWindow
const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => {
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
}
})
if (isSecondInstance) {
app.quit()
}
function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
mainWindow.on('closed', function () {
mainWindow = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
if (mainWindow === null) {
createWindow()
}
})
The middle click does not create a new instance of your application, but rather a new instance of a BrowserWindow. You can disable middle-clicks on a (actually all) elements using the auxclick event.
In your main window's HTML you could put the following JavaScript to disable middle-clicks on link elements if you do not want to redirect these events to your default browser:
// The following function will catch all non-left (middle and right) clicks
function handleNonLeftClick (e) {
// e.button will be 1 for the middle mouse button.
if (e.button === 1) {
// Check if it is a link (a) element; if so, prevent the execution.
if (e.target.tagName.toLowerCase() === "a") {
e.preventDefault();
}
}
}
window.onload = () => {
// Attach the listener to the whole document.
document.addEventListener("auxclick", handleNonLeftClick);
}
But you can also choose to redirect the middle-click events to your standard browser, namely via Electron's shell module:
// Require Electron's "shell" module
const { shell } = require("electron");
function handleNonLeftClick (e) {
// e.button will be 1 for the middle mouse button.
if (e.button === 1) {
// Check if it is a link (a) element; if so, prevent the execution.
if (e.target.tagName.toLowerCase() === "a") {
// Prevent the default action to fire...
e.preventDefault();
// ...and let the OS handle the URL.
shell.openExternal(e.target.href);
}
}
}
// Also attach the listener this time:
window.onload = () => { document.addEventListener("auxclick", handleNonLeftClick); }
You could remove the if (e.button === 1) if you also want to block right-clicks on a elements.
Related
I've been able to use the preload.js for sending message to API to get things done. I'm able to get responses just fine from iPC, but I'm not able to relay the responses from iPC back to the renderer and I don't understand what I'm missing.
index.js (main)
// Modules to control application life and create native browser window
const { app, BrowserWindow, remote, ipcMain } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
let mainWindow;
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready',() => {
createWindow();
require('./request.js')(mainWindow);
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
request.js - this is used in index/main thread above to process API requests, in this case requests to winax which I use with a separate 32 bit nodeJs interpreter because I couldn't get it to build for use with the same version of Node I can use for electron
const { ipcMain, ipcRenderer } = require('electron');
const { spawn } = require('child_process');
module.exports = function(mainWindow){
let winax;
ipcMain.on('winax', (event,arguments) => {
console.log('winax=');
console.log(arguments);
if(winax === undefined) {
console.log('spawning winax');
winax = spawn(
'C:\\Program Files\\nvm\\v14.20.0\\node.exe',
[ 'winax_microamp/index.js' ], {
shell: false,
stdio: ['inherit', 'inherit', 'inherit', 'ipc' ],
windowsHide: true
}
);
}
console.log('sending winax arguments');
/*
arguments = {
method: 'functionToRun',
owner: 'requestingProcess',
params: { required_params ... }
}
*/
winax.send(arguments);
winax.once('message', (message) => {
console.log('winax response=')
console.log(message);
mainWindow.webContents.postMessage('response', message);
});
});
}
preload.js
'use strict';
// preload.js
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
// MAS: This function executes when the DOM is loaded so we should be able to add button interactions here
const { contextBridge, ipcRenderer } = require('electron');
let testAccDb = 'C:\\Users\\PZYVC7\\OneDrive - Ally Financial\\Access\\electron_microamp.accdb';
//const spawn = require('child_process').spawn
contextBridge.exposeInMainWorld(
'request', {
send: (func) => {
console.log('request.send.func=');
console.log(func);
if(func == 'openStagingTest'){
let args = {
method: 'requestStagingDatabase',
owner: 'ui',
params: {
path: testAccDb
}
};
ipcRenderer.send('winax',args);
} else if(func == 'closeStagingTest') {
let args = {
method: 'closeStagingDatabase',
owner: 'ui'
};
ipcRenderer.send('winax',args);
}
},
response: (message) => {
ipcRenderer.on('message', (message) => {
console.log('winax reply=');
console.log(message);
});
}
}
);
window.addEventListener('DOMContentLoaded', () => {
/*const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency]);
}*/
});
render.js
'use strict';
onLoad();
function onLoad(){
/*document.querySelector('.one').addEventListener('click',() => {
writeLog()
});*/
/*var testButton = document.querySelector('button[class=test]');
testButton.addEventListener('click', (e) => {
//C:\\Program Files\\nvm\\v14.20.0\\node.exe
window.main.asyncProc( '"C:\\Program Files\\nvm\\v14.20.0\\node.exe" winax_microamp/index.js' );
});*/
var buttons = document.querySelectorAll('button');
buttons.forEach((button) => {
//console.log('button=');
//console.log(button.className);
button.addEventListener('click', (event) => {
window.request.send(button.className);
});
});
}
Ugh... ok, so a few things I figured out eventually, I took hours to figure this out, and I'm not 100% sure if I'm understanding it properly so I appreciate any correction.
I think one issue that threw me off for longer than it should have is that the console.log for the preload.js goes into the developer tools rather than the system console like the main thread areas do.
Another thing throwing me off was that I was writing the implementation inside of preload.js, rather than in render.js. I confirmed it does work in both places.
If I want it in preload.js, I leave the implementation like this, and I confirmed it was firing here based on changing the log message a little
preload.js
response: (message) => {
ipcRenderer.on('response', (message) => {
console.log('expose reply=');
console.log(message);
});
}
}
If I want it in render instead, I need my preload to be more basic
response: (message) => {
ipcRenderer.on('response', message);
}
And then I need render to have this to process it there instead.
window.request.response((event,message) => {
console.log('winax reply=');
console.log(message);
});
I want to make a message box which contains yes and no buttons in a electron.js app. I tried to do it with dialog inside the electron. But it didn't work:
const electron = require('electron')
const { dialog } = electron
console.log(dialog) // undefined
const electron = require('electron')
const dialog = electron.remote.dialog
console.log(dialog) // Uncaught Error: Cannot read "dialog" of undefined (remote is undefined)
Then, I tried to do it with dialog which is a module in npm. But it didn't do the thing that I want to do. There wasn't any yes or no buttons also it returned the same responses when I clicked OK or I closed window:
const electron = require('electron')
const dialog = require('dialog')
dialog.info('Are you sure?', 'Confirmation', function(exitCode) {
if (exitCode == 0) {
// Should clicked OK (always response)
}
if (exitCode == 1) {
// Should closed window (but never works)
}
})
What did I do wrong?
You will want to use
Electron's dialog.showMessageBox();
method.
The dialog.showMessageBoxSync();
method would block your main process until a response is received, so you won't want to use that unless intended.
I have placed the creation and management of your dialog box in the main.js file. If you want to move this into its
own file, that's not a problem. All you would need to do is get() the (main) window instance if you want your dialog
box to be a child of the main window.
main.js (main process)
// Import required Electron modules
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
// Import required Node modules
const nodePath = require('path');
// Prevent garbage collection
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
electronIpcMain.on('openDialog', () => {
electronDialog.showMessageBox(window, {
'type': 'question',
'title': 'Confirmation',
'message': "Are you sure?",
'buttons': [
'Yes',
'No'
]
})
// Dialog returns a promise so let's handle it correctly
.then((result) => {
// Bail if the user pressed "No" or escaped (ESC) from the dialog box
if (result.response !== 0) { return; }
// Testing.
if (result.response === 0) {
console.log('The "Yes" button was pressed (main process)');
}
// Reply to the render process
window.webContents.send('dialogResponse', result.response);
})
})
For proper communication between processes, we must
use Inter-Process Communication.
preload.js (main process)
// Import the necessary Electron modules
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// Exposed protected methods in the render process
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods
'ipcRenderer', {
// From render to main
openDialog: () => {
ipcRenderer.send('openDialog');
},
dialogResponse: (response) => {
ipcRenderer.on('dialogResponse', response);
}
}
);
Finally, your index.html file will listen for a button click. Once clicked, send a message to the main process to open
the
dialog box.
Once a valid response is received from the dialog box, the response is sent back to the render process for processing.
PS: The render
method ipcRenderer.invoke()
could be used instead of
the ipcRenderer.send() method.
But if it was, you would need to handle the "No" or escape (ESC) response in the render process.
index.html (render process)
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<input type="button" id="openDialog" value="Show Dialog">
<hr>
<div id="response"></div>
</body>
<script>
// Open dialog (in main process) when "Show Dialog" button is clicked
document.getElementById('openDialog').addEventListener('click', () => {
window.ipcRenderer.openDialog('openDialog');
})
// Response from main process
window.ipcRenderer.dialogResponse((event, response) => {
if (response === 0) {
// Perform your render action here
document.getElementById('response').innerText = 'The "Yes" button was clicked';
}
});
</script>
</html>
To use more than 2 buttons in your dialog box(es), in the creation of your dialog box you may want to designate
a cancelId and check for all valid return values before actioning anything.
I am currently using Electron v16. Apparently on this version, we cannot use built-in Node modules in the renderer thread anymore.
The only way to do it is by using electron-preload.js.
I've read this resource:
https://whoisryosuke.com/blog/2022/using-nodejs-apis-in-electron-with-react/
where the author placed the implementation code in electron-main.js by utilizing the ipcMain and the code is invoked through electron-preload.js contextBridge.
My question is:
Is there any difference if put the entire implementation code in electron-preload instead of having the need to invoke an event from here and send it to ipcMain?
Here is the code from the author:
electron-preload.js:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
ipcRenderer.invoke('blender:version', blenderPath),
},
});
electron-main.js
ipcMain.handle('blender:version', async (_, args) => {
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
})
I am thinking if doing something like this instead is acceptable (pros/cons?):
electron-preload.js:
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
},
});
Functionally, there is no difference between the two at all...
That said, separating the calling of your code from its implementation goes a long way in "Separate your Concerns". If you have a lot of calls from render to main (or vice versa), your preload.js script will become huge. Trying to manage a file of that size, especially if you are using typescript in your Electron application can become problematic. The overhead of such declarations will grow and grow.
I take an even more thorough, but in my mind, simplified approach...
I use the preload.js script purely as a place to declare "channel names" and implement the communication between the main process and the render process(es) via the use of pure IPC handles such as:
ipcRenderer.send(channel, ...args)
ipcRenderer.on(channel, listener)
ipcRenderer.invoke(channel, ...args).
Having such a simple script means I only need to manage one preload.js script and I use it in every window I ever create.
This is my preload.js script. To use, just add your "channel names" to whatever whitelisted array you need to. EG: In this instance, I have added the channel name test:channel for use only between the main to render process.
preload.js (main process)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [
'test:channel' // Our channel name
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
In my main.js script, just IPC with the channel name and any associated data, if any.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.webContents.send('test:channel', 'Hello from the main thread') })
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Finally, in your render just listen for the channel name and implement functionality when called.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
</head>
<body>
<div id="test"></div> // Outputs "Hello from the main thread"
</body>
<script>
window.ipcRender.receive('test:channel', (text) => {
document.getElementById('test').innerText = text;
})
</script>
</html>
To use this preload.js scipt in the main and render processes, the following implementation would apply.
/**
* Render --> Main
* ---------------
* Render: window.ipcRender.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Main --> Render
* ---------------
* Main: windowName.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRender.receive('channel', (data) => { methodName(data); });
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await promiseName(data)
* .then(() => { return result; })
* });
*/
Remember, you can't return functions, promises or other objects that can't be serialised with the structured clone algorithm. See Object serialization for more information.
I'm Kenyon Bowers.
I have some code that opens a open file dialog. It opens .DSCProj (which are specific to my project), and I am going to run some terminal commands in the directory that the opened file is in.
I have no idea how to do that.
preload.ts:
import { ipcRenderer, contextBridge } from "electron";
import { dialog } from '#electron/remote'
contextBridge.exposeInMainWorld("api", {
showOpenFileDialog: () => dialog.showOpenDialogSync({
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
},
],
})
});
NewProject.ts:
declare var api: any;
function OpenProject(): void {
const file = api.showOpenFileDialog();
console.log("Done")
if(file != null){
localStorage.setItem('DirPath', file);
location.href='./views/projectOpen.html'
}
}
(() => {
document.querySelector('#btn-open-project')?.addEventListener('click', () => {
OpenProject();
}),
document.querySelector('#btn-new-project')?.addEventListener('click', () => {
location.href='./views/projectNew.html'
})
})()
As you can see on line 7, I'm setting local storage to the file's path. But I need to set it to the path of the directory that the file is in.
Whilst use of #electron/remote is great and all, you may be better served by implementing certain Electron modules within the main thread instead of the render thread(s). This is primarily for security but as a strong second reason, it keeps your code separated. IE: Separation of concerns.
Unlike vanilla Javascript, node.js has a simple function path.parse(path).dir to easily remove the file name (and extension) from the file path without needing to worry about which OS (IE: Directory separator) you are using. This would also be implemented within your main thread. Implementing something like this within your render thread would take a lot more work with vanilla Javascript to be OS proof.
Lastly, in the code below I will use a preload.js script that only deals with the movement of messages and their data between the main thread and render thread(s). I do not believe that the concrete implementation of functions in your preload.js script(s) is the right approach (though others may argue).
Note: I am not using typescript in the below code, but you should get the general idea.
Let's use the channel name getPath within the invoke method.
preload.js (main thread)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [],
// From render to main and back again.
'sendReceive': [
'getPath'
]
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Now, in the main thread, let's listen for a call on the getPath channel. When called, open up the dialog and upon the return of a path, process it with Node's path.parse(path).dir function to remove the file name (and extension). Lastly, return the modified path.
main.js (main thread)
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// -----
// Let's listen for a call on the 'getPath' channel
electronIpcMain.handle('getPath', async () => {
// Dialog options.
const options = {
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
}
]
}
// When available, return the modified path back to the render thread via IPC
return await openDialog(window, options)
.then((result) => {
// User cancelled the dialog
if (result.canceled === true) { return; }
// Modify and return the path
let path = result.filePaths[0];
let modifiedPath = nodePath.parse(path).dir; // Here's the magic.
console.log(modifiedPath); // Testing
return modifiedPath;
})
})
// Create an open dialog
function openDialog(parentWindow, options) {
return electronDialog.showOpenDialog(parentWindow, options)
.then((result) => { if (result) { return result; } })
.catch((error) => { console.error('Show open dialog error: ' + error); });
}
Here you will get the general idea about how to use the returned result.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Open Project Test</title>
</head>
<body>
<input type="button" id="btn-open-project" value="Open Project">
</body>
<script>
document.getElementById('btn-open-project').addEventListener('click', () => {
openProject();
});
function openProject() {
window.ipcRender.invoke('getPath')
.then((path) => {
// As we are using "invoke" a response will be returned, even if undefined.
if (path === undefined) { return; } // When user cancels dialog.
console.log(path); // Testing.
// window.localStorage.setItem('DirPath', path);
// location.href='./views/projectOpen.html';
});
}
</script>
</html>
I have a CLI application that is built using nodejs, I can pass some flags and it will output to a JSON file. Now I want to make it a desktop application that will show that JSON file data to UI. I'm trying to use reactjs for UI. Also, have to store some information that is going to be used to run the CLI.
I have tried to integrate the CLI app to electronjs using child_process (exec(), spawn()) but it's not working and I couldn't figure out why. Also, I have tried to use expressjs server wrapper that is going to spawn the CLI app child_process and send the output with JSON response but it seems like failed to create the JSON file.
const timestamp = Date.now();
const child = childProcess.exec(`node ./index.js --config ./config.js ${flags} --json ${timestamp}.json --console none`);
child.stdout.on('data', (d) => console.log(d.toString()));
child.on('close', () =>{
res.status(200).json({file: `${timestamp}.json`}); // I want to send this data to frontend
});
I'm also interested to use some other library/framework of javascript to achieve this.
To communicate safely between threads, Electron some time ago introduced the use of a prelaod.js script. This script is effectively the gatekeeper between the main thread and render thread(s). Electron's Context Isolation page will explain this in more detail.
In addition to this, communication between threads is performed by Inter-Process Communication. These IPC commands are used to initiate and listen for communication over 'channels'. Channels are just names of your choosing along with which data can be attached (if needed).
The main component one needs to understand and implement is the preload.js script.
Here, you include whitelisted channel names. Any name format can be used. Any channel name not included in these lists will be denied communication between threads.
PS: Many people (including some Electron documentation) include actual function implementation directly within the preload script. Whilst there is nothing wrong with this it definitely does make things more complicated than necessary. Using the preload script to manage channel names only, frees up implementation of the channel names elsewhere which greatly simplifies things. IE: Separation of Concerns.
preload.js (main thread)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [
'config:data'
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Note, within the whitelisted receive array I have included the channel name config:data.
Now, within your main thread, send the contents of the json file via the previously designated channel.
main.js (main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
.then(() => { runConfig(); }); // Now, run your config.js file
return window;
}
function runConfig() {
const timestamp = Date.now();
const child = childProcess.exec(`node ./index.js --config ./config.js ${flags} --json ${timestamp}.json --console none`);
child.stdout.on('data', (d) => console.log(d.toString()));
child.on('close', () => {
let jsonData = JSON.parse(nodeFs.readFileSync(`${timestamp}.json`, 'utf8'));
console.log(jsonData);
window.webContents.send('config:data', jsonData); // Send the data to the render thread
});
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Lastly, listen for the channel name to be called in the render thread.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body></body>
<script>
window.ipcRender.receive('config:data', (jsonData) => {
console.log(jsonData);
});
</script>
</html>