Electron: How to securely inject global variable into BrowserWindow / BrowserView? - javascript

I want to load an external webpage in Electron using BrowserView. It has pretty much the same API as BrowserWindow.
const currentWindow = remote.getCurrentWindow();
const view = new remote.BrowserView({
webPreferences: {
// contextIsolation: true,
partition: 'my-view-partition',
enableRemoteModule: false,
nodeIntegration: false,
preload: `${__dirname}/preload.js`,
sandbox: true,
},
});
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
In my preload.js file, I simply attach a variable to the global object.
process.once('loaded', () => {
global.baz = 'qux';
});
The app running on localhost:3000 is a React app which references the value like this:
const sharedString = global.baz || 'Not found';
The problem is I have to comment out the setting contextIsolation: true when creating the BrowserView. This exposes a security vulnerability.
Is it possible to (one way - from Electron to the webpage) inject variables into a BrowserView (or BrowserWindow) while still using contextIsolation to make the Electron environment isolated from any changes made to the global environment by the loaded content?
Update:
One possible approach could be intercepting the network protocol, but I'm not sure about this 🤔
app.on('ready', () => {
const { protocol } = session.fromPartition('my-partition')
protocol.interceptBufferProtocol('https', (req, callback) => {
if (req.uploadData) {
// How to handle file uploads?
callback()
return
}
// This is electron.net, docs: https://electronjs.org/docs/api/net
net
.request(req)
.on('response', (res) => {
const chunks = []
res.on('data', (chunk) => {
chunks.push(Buffer.from(chunk))
})
res.on('end', () => {
const blob = Buffer.concat(chunks)
const type = res.headers['content-type'] || []
if (type.includes('text/html') && blob.includes('<head>')) {
// FIXME?
const pos = blob.indexOf('<head>')
// inject contains the Buffer with the injected HTML script
callback(Buffer.concat([blob.slice(0, pos), inject, blob.slice(pos)]))
} else {
callback(blob)
}
})
})
.on('error', (err) => {
console.error('error', err)
callback()
})
.end()
})
})

After doing some digging, I found a few pull requests for Electron that detail the issue you are having. The first describes a reproducible example very similar to the problem you are describing.
Expected Behavior
https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content
A preload script should be able to attach anything to the window or document with contextIsolation: true.
Actual behavior
Anything attached to the window in the preload.js just disappears in the renderer.
It seems the final comment explains that the expected behavior no longer works
It was actually possible until recently, a PR with Isolated Worlds has changed the behavior.
The second has a user suggest what they have found to be their solution:
After many days of research and fiddling with the IPC, I've concluded that the best way is to go the protocol route.
I looked at the docs for BrowserWindow and BrowserView as well as an example that shows the behavior that you desire, but these PRs suggest this is no longer possible (along this route).
Possible Solution
Looking into the documentation, the webContents object you get from view.webContents has the function executeJavaScript, so you could try the following to set the global variable.
...
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
view.webContents.executeJavaScript("global.baz = 'qux';");
...

Other answers are outdated, use contextBridge be sure to use sendToHost() instead of send()
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.sendToHost('do-a-thing')
}
)
// Renderer (Main World)
window.electron.doThing()

So, executeJavaScript as suggested by Zapparatus ended up being part of the solution.
This is what's going on in renderer.js.
view.webContents.executeJavaScript(`
window.communicator = {
request: function(data) {
const url = 'prefix://?data=' + encodeURIComponent(JSON.stringify(data))
const req = new XMLHttpRequest()
req.open('GET', url)
req.send();
},
receive: function(data) {
alert('got: ' + JSON.stringify(data))
}
};
`)
const setContent = data => view.webContents.executeJavaScript(
`window.communicator.receive(${JSON.stringify(data)})`
)
ipcRenderer.on('communicator', (event, message) => {
setContent(`Hello, ${message}!`)
})
We ended up setting up a custom protocol, similar to how its been done here. In your main.js file set up the following:
const { app, session, protocol } = require('electron')
const { appWindows } = require('./main/app-run')
const { URL } = require('url')
protocol.registerSchemesAsPrivileged([
{
scheme: 'prefix',
privileges: {
bypassCSP: true, // ignore CSP, we won't need to patch CSP
secure: true // allow requests from https context
}
}
])
app.on('ready', () => {
const sess = session.fromPartition('my-view-partition')
// https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
sess.setPermissionRequestHandler((webContents, permission, callback) => {
// Denies the permissions request
const decision = false
return callback(decision)
})
sess.protocol.registerStringProtocol('prefix', (req, callback) => {
const url = new URL(req.url)
try {
const data = JSON.parse(url.searchParams.get('data'))
appWindows.main.webContents.send('prefix', data)
} catch (e) {
console.error('Could not parse prefix request!')
}
const response = {
mimeType: 'text/plain',
data: 'ok'
}
callback(response)
})
})
No preload.js or postMessage needed.

Related

Is it possible to implement a shared state management for CLI applications without the need for an external database?

I want to create a CLI application and I think this question is not about a specific technology but for the sake of reproduction purposes I'm using Node with command-line-commands ( but I know there are plenty others, e.g. commander ).
Given the following sample code
#!/usr/bin/env node
'use strict';
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
let isRunning = false; // global state
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
isRunning = true;
} else if (commandResult.command === 'info') {
console.info({ isRunning });
}
The boolean isRunning indicates a shared state. Calling the start command sets its value to true. But calling the info command obviously starts a new process and prints a new variable isRunning with its initial falsy value.
What is the prefered technology to keep such state? Must the CLI use an external database ( e.g. local filesystem) or are there some ways to keep the information in memory until shutdown?
Generating my own file on the system and storing this variable to it feels like an overkill to me.
An old cross-platform hack is to open a known TCP port. The first process able to open the port will get the port. All other processes trying to open the port will get an EADDRINUSE error:
const net = require('net');
const s = net.createServer();
s.on('error',() => {
console.log('Program is already running!');
// handle what to do here
});
s.listen(5123,'127.0.0.1',() => {
console.log('OK');
// run your main function here
});
This works in any language on any OS. There is only one thing you need to be careful of - some other program may be accidentally using the port you are using.
I originally came across this technique on the Tcl wiki: https://wiki.tcl-lang.org/page/singleton+application.
Another old hack for this is to try and create a symlink.
Creating symlinks are generally guaranteed to be atomic by most Unix and Unix-like OSes. Therefore there is no issue with potential race conditions using this technique (unlike creating a regular file). I presume it is also atomic on Windows (as per POSIX spec) but I'm not entirely sure:
const fs = require('fs');
const scriptName = process.argv[1];
const lockFile = '/tmp/my-program.lock';
try {
fs.symlinkSync(lockFile, scriptName);
// run your main function here
fs.unlinkSync(lockFile);
}
catch (err) {
console.log('Program already running');
// handle what to do here
}
Note: While creating symlinks are atomic, other operations on symlinks are not guaranteed to be atomic. Specifically be very careful of assuming that updating a symlink is atomic - it is NOT. Updating symlinks involve two operations: deleting the link and then creating the link. A second process may execute its delete operation after your process creates a symlink causing two processes to think that they're the only ones running. In the example above we delete the link after creating it, not before.
One way would be to use a local web server.
index.js
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
var http = require('http');
let globalState = {
isRunning: false
}
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
globalState.isRunning = true;
http.createServer(function (req, res) {
res.write(JSON.stringify(globalState));
res.end();
}).listen(9615);
} else if (commandResult.command === 'info') {
console.info({ globalState });
}
index2.js
var http = require('http');
var req = http.request({ host: "localhost", port: 9615, path: "/" }, (response) => {
var responseData = "";
response.on("data", (chunk) => {
responseData += chunk;
});
response.on("end", () => {
console.log(JSON.parse(responseData));
});
});
req.end();
req.on("error", (e) => {
console.error(e);
});
Here the index.js is a program that holds the "shared / global state" as well as creates a web server to communicate with. Other programs such as index2.js here can make a http request and ask for the global state. You could also let other programs change the state by having index.js listen to some specific request and act accordingly.
This doesn't have to be done with http like this, you could also use something like node-rpc or node-ipc. I thought the easiest working example would be to do it with a local http client and server.
Either way, I think the word for what you are looking for is Inter Process Communication (IPC) or Remote Procedure Call (RPC). I don't see why one couldn't also utilize websockets as well. Child processes probably won't work here, even if you could implement some kind of parent-child process communication, because only the child processes spawned by the main process could use that.
EDIT
After reading your question more carefully, I think that this is just a matter of "keeping" the "console session" after start command and setting the isRunning variable.
Check this out:
const commandLineArgs = require('command-line-args');
const commandLineCommands = require('command-line-commands');
const commandLineUsage = require('command-line-usage');
const prompt = require('prompt-sync')();
let globalState = {
isRunning: false
}
let commandResult;
try {
commandResult = commandLineCommands([ 'start', 'info', 'help' ]);
} catch (error) {
console.error('Invalid command.');
process.exit(1);
}
if (commandResult.command === null || commandResult.command === 'help') {
const commandInfo = commandLineUsage([
{ header: 'start', content: 'Sets the value to true' },
{ header: 'info', content: 'Gets the current value' },
]);
console.log(commandInfo);
process.exit(0);
}
let options;
try {
options = commandLineArgs([], { argv: commandResult.argv });
} catch (error) {
console.error('Invalid argument.');
process.exit(1);
}
if (commandResult.command === 'start') {
globalState.isRunning = true;
while(globalState.isRunning)
{
let cmd = prompt(">");
if(cmd === "exit")
process.exit(0);
if(cmd === "info")
console.info({ globalState });
}
} else if (commandResult.command === 'info') {
console.info({ globalState });
}
Here I am using prompt-sync library inside a loop when the program is called with a start command. The "console session" is kept indefinitely until the user types exit. I also added and example for in case the user types info.
Example:

Access methods in electron's main process from render process after building

Alright so I have these methods in my index.js main process that I want to access from the render process. I have tried two ways to go about this process.
ipcMain and ipcRender
The first idea was to use ipcMain and ipcRender using an "on" and "sendSync" The error I get back is "an object could not be cloned"
Index.js - Main Process
ipcMain.on( "getData", ( event, callBack ) => {
db = new sqlite3.Database(
dbPath,
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
function(error) {
if(error){
log.error(`exception: ${error}`);
//throw error
if (callBack) callBack(error);
}
data.getModel(db, log, function(rawDataStr) {
if (callBack) callBack(rawDataStr);
})
}
)
return db
} );
App.Js - Render Process
window.require('electron').ipcRenderer.sendSync( "getData",function(rawData){
if (rawData.name && rawData.name == 'Error') {
alert('PRT DB is not present');
} else {
sharedObj.rawData = rawData;
app.advanceReadiness();
}
})
#electron/remote
The other solution I tried was to use #electron/remote. I understand the remote module was depreciated, but I was willing to try it. This works when I run the the app locally, but as soon as I build the app with electron-forge it can no longer find my global variable.
Index.js - Main Process
require('#electron/remote/main').initialize()
global.sharedObj = {
getData:function(callBack){
db = new sqlite3.Database(
dbPath,
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
function(error) {
if(error){
log.error(`exception: ${error}`);
//throw error
if (callBack) callBack(error);
}
data.getModel(db, log, function(rawDataStr) {
if (callBack) callBack(rawDataStr);
})
}
)
}
}
App.js - Render Process
var sharedObj = window.require('#electron/remote').getGlobal('sharedObj');
sharedObj.getData(function (rawData) {
if (rawData.name && rawData.name == 'Error') {
alert('PRT DB is not present');
} else {
sharedObj.rawData = rawData;
app.advanceReadiness();
}
});
I suspect that your DB connection isn't cloneable, because this DB object doesn't fit one of the valid values that can be serialized by IPC (inter-process communication). (See this section to see what we can pass between renderer > main process without issue).
You probably need to do something like this. I'm not familiar with using sqlite3 in JS, but this should hopefully get you started on the right track. The general gist is that you should store a reference to your DB in your main.js file and then set up a listener that listens for requests from your front-end page. Once a message is sent to the main.js file (ie. backend), you will query your DB, and then return the results to the front-end by sending an IPC message back (win.webContents.send("fromMain", data); in the example elow).
main.js
const {
app,
BrowserWindow,
ipcMain
} = require("electron");
const path = require("path");
const fs = require("fs");
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
let db = new sqlite3.Database(
dbPath,
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
function (error) {
if (error) {
log.error(`exception: ${error}`);
//throw error
if (callBack) callBack(error);
}
data.getModel(db, log, function (rawDataStr) {
if (callBack) callBack(rawDataStr);
})
}
);
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
preload: path.join(__dirname, "preload.js") // use a preload script
}
});
// Load app
win.loadFile(path.join(__dirname, "dist/index.html"));
// rest of code..
}
app.on("ready", createWindow);
ipcMain.on("toMain", (event, args) => {
db.retrieveData().then(data => {
// Send result back to renderer process
win.webContents.send("fromMain", data);
});
});
preload.js
const {
contextBridge,
ipcRenderer
} = require("electron");
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
let validChannels = ["toMain"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["fromMain"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}
}
);
index.html
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8"/>
<title>Title</title>
</head>
<body>
<script>
window.api.receive("fromMain", (data) => {
console.log(`Received ${data} from main process`);
});
window.api.send("toMain", "some data");
</script>
</body>
</html>

Request handler unable to render view with Vision plugin

I'm working on a HapiJS api and I've defined a plugin that registers the Vision plugin and configures a rendering engine (ejs). But when I try and respond to the request by rendering a view I get the error
AssertionError [ERR_ASSERTION]: Missing views manager
If I register the Vision plugin and its config somewhere outside the plugin the tests pass but my expectation is that I should be able to encapsulate this logic in a plugin.
// plugin
const ejs = require('ejs');
const Vision = require('vision');
module.exports = {
name: 'views',
version: '0.0.1',
register: async (server, { path }) => {
await server.register(Vision);
server.views({
engines: { ejs },
path,
});
},
};
The handler code is
// api
server.route({
path: '/korin/songs',
method: 'GET',
handler: async (request, h) => {
try {
const acceptType = getMediaType(request.headers.accept);
const data = await server.methods.getTopTracks({
getTopTracks,
lastfmApi,
});
if (acceptType === 'text/html') {
return h.view('index'); // <-- this errors
}
return data;
} catch (error) {
console.warn(error);
}
},
});
The error is generated by a failing test which is
suite('render content', () => {
test.only(`given text/html page should respond with header and footer`, async () => {
const { server } = await setup();
const { payload } = await server.inject({
method: 'GET',
url: '/korin/songs',
headers: {
accept: 'text/html',
},
});
expect(payload).to.contain(`<header>`);
expect(payload).to.contain(`<footer>`);
});
});
// test setup
const setup = async options => {
const server = new Hapi.Server();
// truncated for brevity
await server.register({
plugin: require('../../server/api'),
options: {
...defaults,
...options,
},
});
await server.register({
plugin: require('../../server/views'),
options: { path: path.join(__dirname, '../views/templates') },
});
return {
server
};
};
Is there something I'm missing? I've tried running a console.log and the code seems to be running in the right order but failing anyway.
There is an old thread on GitHub about this. TL;DR: the reference to server passed to the plugin when registering is slightly not the same as the "root" server. Some difference about realms, apparently still an issue.
Indeed: in the plugin, server.getViewsManager() (decorated by vision) after registration of vision and server.views will show something, whereas the same call in your route (so, after plugin registration) will show null. So much for "references".
I just tried a similar structure to you, got the same error, and this thread pointed me to a workaround: when registering your views plugin, just pass along a reference to the "real" server in the options.
// plugin
const ejs = require('ejs');
const Vision = require('vision');
module.exports = {
name: 'views',
version: '0.0.1',
register: async (server, { path, realServer }) => { // <= added option
await realServer.register(Vision); // <= replaced server w/ realServer
realServer.views({ // <= replaced server w/ realServer
engines: { ejs },
path,
});
},
};
// test setup
// ...
const server = new Hapi.Server();
// ...
await server.register({
plugin: require('../../server/views'),
options: {
path: path.join(__dirname, '../views/templates'),
realServer: server // <= added option
}
});
And, obviously, have the same options everywhere you register this plugin.

ServiceWorker claiming late in the lifecycle to use client.navigate in notificationclick event handler

I have a firebase serviceworker that shows notifications when a message is pushed from Firebase Cloud Messaging (FCM).
It also publishes a post so that my React App can update accordingly.
/* eslint-env worker */
/* eslint no-restricted-globals: 1 */
/* global firebase */
/* global clients */
import config from './config'
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js')
const { FIREBASE_MESSAGING_SENDER_ID } = config
firebase.initializeApp({ messagingSenderId: FIREBASE_MESSAGING_SENDER_ID })
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(payload => {
const title = payload.data.title
const options = {
body: payload.data.body,
icon: payload.data.icon,
data: payload.data,
}
clients.matchAll({ includeUncontrolled: true }).then(clientz => {
clientz.forEach(client => {
sendMessageToClient(client, 'NEW_USER_NOTIFICATON')
})
})
return self.registration.showNotification(title, options)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
This all works fine, but I have added it for context.
What I want to do is have a click function that focuses on the correct window/tab and navigates to a link that is passed to it. Or if the tab is not open, open a new window and go to the link.
This is the code I have so far, added to the above file.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.claim()
.then(() => self.clients
.matchAll({
type: 'window',
})
)
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
return matchingClient.navigate(link)
.then(() => matchingClient.focus())
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
So, I realise that the chained navigate and focus inside a then is probably bad practice, but for now, I am just trying to get it to work. Then I will try and come up with a clever solution.
So the problem with my code is that the clients.claim() doesn't seem to be working. The matchAll doesn't return anything to the next then, the argument is an empty array.
I could simply add the includeUncontrolled: true option to the matchAll, but the navigate command only works on a controlled client instance.
If I try the often referenced Google example for claiming and navigation, it works fine:
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(() => {
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
return self.clients.matchAll({type: 'window'});
}).then(clients => {
return clients.map(client => {
// Check to make sure WindowClient.navigate() is supported.
if ('navigate' in client) {
return client.navigate('activated.html');
}
});
}));
});
So I am stuck.
The serviceworker is activated immediately, so I assume that it claim a client at any point after that.
Have I fallen for a random ServiceWorker Gotcha?
Can the claim only be used and navigated to on the handling of an activation event?
I would appreciate any help available.
Cheers
I couldn't get this to work.
But I thought it would be worth documenting my workaround.
I could not get client.navigate to work in the notificationclick event handler.
So instead I just sent a postMessage containing the URL to be picked up in my app to trigger the redirect there, without any client claiming anywhere.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
sendMessageToClient(matchingClient, { type: 'USER_NOTIFICATION_CLICKED', link })
return matchingClient.focus()
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}

Electron: Communicate between BrowserWindow and rendered URL (nodeIntegration: false)

I've spent about an hour reading gist after repo after blog post, but can't seem to figure out how to do do this.
I have a BrowserWindow instance loading a URL (that I control), with nodeIntegration: false.
From the main process, I'd like to communicate with the rendered URL. I'm getting confused between preload scripts, BrowserWindow.send and executeJavascript paradigms.
The data I want to send is very large (eg. file uploads between 50kb and 10mb).
What's the best way to do this? Any any examples/tutorials you may know about would be helpful. Thanks!
main.js
const path = require('path')
const electron = require('electron')
const { app, BrowserWindow, ipcMain } = electron
const window = new BrowserWindow({
minWidth: 1200,
minHeight: 700,
autoHideMenuBar: true,
resizable: true,
show: false,
scrollBounce: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
})
window.webContents.loadURL('https://xxx.xxx.com') // load your web page
ipcMain.on('ping', (event, msg) => {
console.log(msg) // msg from web page
window.webContents.send('pong', 'hi web page') // send to web page
})
preload.js
const { ipcRenderer } = require('electron');
function init() {
// add global variables to your web page
window.isElectron = true
window.ipcRenderer = ipcRenderer
}
init();
your web page
<script>
if (window.isElectron) {
window.ipcRenderer.send('ping', 'hello main')
window.ipcRenderer.on('pong', (event, msg) => console.log(msg))
}
</script>
Using preload script should work. You can use ipcRenderer to communicate with main process and expose it with simple API to renderer window. Simplest preload.js can look like:
const { ipcRenderer } = require('electron');
let listener;
const bridge = {
send: data => ipcRenderer.send('from-renderer', data),
onMessage: callback => listener = callback
}
ipcRenderer.on('to-renderer', (event, arg) => {
if (listener) {
listener(arg);
} else {
console.warn('No listener');
}
});
window.bridge = bridge;
in renderer
window.bridge.send('Data to main process');
window.bridge.onMessage(payload => console.log('Data received', payload))
Please also take a look at this discussion to get more info.

Categories

Resources