Why send method of Electron does not working from the main process? - javascript

I'm in big trouble. I checked other solutions but nothing helped me.
I don't understand why the "send" method doesn't work here. No exceptions, no messages or anything else.
I tried wrapping the send method in another method but it failed.
I tried using the "sendToFrame" method but it failed too.
main:
const { app, BrowserWindow, ipcMain } = require('electron')
let win;
function createWindow () {
win = new BrowserWindow({
width: 800,
height: 600,
frame: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: true,
contextIsolation: false, // false if you want to run 2e2 test with Spectron
enableRemoteModule: true,
}
});
win.loadFile('src/home.html');
win.webContents.openDevTools();
ipcMain.on('main', (event, msg) => {
sendData() // Does not work.
event.reply('new-client', 'res') // It works.
})
function sendData() {
win.webContents.send('new-client', {wsh: 'wshsdqsdqs'});
}
};
app.whenReady().then(createWindow);
Below, the "ipcRenderer.send" method works. It sends data to the main process but receives no response when I use the "webContents.send" method (in the main process).
new-client.js:
const { ipcRenderer } = require('electron');
const {resolve} = require('path');
const {Message} = require(resolve('src/js/shared/class/message/message.js'));
ipcRenderer.on('new-client', (event, arg) => {
console.log(event)
console.log(arg)
})
async function send() {
const msg = new Message(
'new-client',
'clients',
true
);
ipcRenderer.send('main', msg);
}
EDIT:
I forgot this...
I sent the data to the wrong window.
You can use BrowserWindow.getAllWindows, then send the data to a specific window.
Personally, I added an event when windows are open, in this way I can properly send data and manage windows.

Related

Sending messages through Electron IPCmain channel to vue instance only works one way

I'm tyring to send messages back and forth between the main electron process and the vue instance. What i have so far is
Preload.js:
import { contextBridge, ipcRenderer } from 'electron'
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])
}
})
window.ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld('ipcRenderer', {
//Render (Vue) to main (Electron)
send: (channel, data) => {
let validChannels = ['clientMessage'] // <-- Array of all ipcRenderer Channels used in the client
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
//Main (Electron) to Render (Vue)
on: (channel, func) => {
let validChannels = ['electronMessage'] // <-- Array of all ipcMain Channels used in the electron
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
So i have 2 whitelisted channels in ipcRender, one is called 'clientMessage' to send messaged from the vue instance to the electron main process, the other is 'electronMessage' to send messages from the electron main process to the vue instance.
In my background.js i have the following:
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
event.sender.send('message','return Message from electron'+args);
});
And in my app.Vue i have:
mounted () {
window.ipcRenderer.on('electronMessage', (event, data) => {
console.log('message from electron: '+data)
})
},
methods: {
sendMessage(){
window.ipcRenderer.send('clientMessage','testing')
}
}
The app runs fine, and when i call the sendMessage function I correctly get a console log on the electron terminal saying message received. So clearly vue -> electron messaging has worked, but why wont it work for the reverse?
Your contextBridge.exposeInMainWorld "keys" are ipcRenderer.send and ipcRenderer.receive.
You must use these keys to access your defined preload.js IPC methods. IE: send and on.
Specifically, to use the ipcRender.on(...) method, you call it with window.ipcRenderer.receive(...).
app.Vue (render thread)
// To send a message from render thread to main thread.
window.ipcRenderer.send('clientMessage','testing'); // Working
// To receive a message from main thread to render thread.
window.ipcRenderer.receive('electronMessage', (event, data) => {
console.log('message from electron: ' + data);
});
I figured out why, the way to send messages from electron main process to the renderer instance is not the same. Since there is only one electron main process running at all times, we can simple do window.ipcRenderer.send('', 'your message') in the vue renderer instance, but since there can be more than one renderer instances running, the main electron process needs to know which render process to send it to.
So in your main.js/background.js whatever .js you are running the electron main process out of. do this:
let win;
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: true,
preload: path.join(__dirname, "preload.js")
}
})
.
.
.
and then to send a message to the render instance:
win.webContents.send('<channelName>','your message')
that way it sends it to the correct render process. So my code in the end looks like this:
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
win.webContents.send('electronMessage','Reply from main process: '+args)
});

electron js how to call a function rendered in index

I'm going crazy to be able to call a function that I have rendered in an index and then execute that function with globalShortcut, I was researching and found that IPC could perform, but all the examples are to use functions from the main and not from the index. any idea that can guide me
I assume you have nodeIntegration set to true and contextIsolation set to false, as advised by the Electron documentation. To trigger a function in your renderer process from the main thread you can use ipcMain and ipcRenderer (these modules are part of Electron).
First, you need to expose an API for the renderer using contextBridge. This can be done with a preload.js file that has access to all modules like the main thread.
Example:
main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createMainWindow(){
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
});
}
app.on('ready', () => {
createMainWindow();
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
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));
}
},
});
Then, in your renderer process you can use window.api.send(channel, parameters); to send messages to the main thread and window.api.receive(channel, parameters); to listen for incoming ones. Similarly, you would use ipcMain.on(channel, (event, parameters) => { }); in the main process to handle messages from the renderer. However, to directly send messages to it, you have to use webContents, like so: mainWindow.webContents.send(channel, parameters);.
In your case, to execute a function in the renderer, you would call mainWindow.webContents.send('fromMain', 'trigger-function'); in the main process; and listen for this event in your renderer like so:
window.api.receive('fromMain', (parameters) => {
if (parameters === 'trigger-function'){
// Call your renderer function here
}
});

Noob to electronjs: Question about contextBridge / appropriate usage

I'm quite new to ElectronJS and much of my learning has come from examples and docs since it seems many tutorials out there are outdated especially due to the addition of enforcing nodeIntegration: false and contextIsolation: true.
My research has led me down the path of preload scripts, which by themselves still open themselves up to vulnerabilities, which then led me to contextBridge via preload scripts.
As a disclaimer, what I'm doing is probably unnecessary since I'm not actually connecting to any third-party and everything is local, so theoretically I could edit the webPreferences for nodeIntegration and contextIsolation. However, for my own learning purposes as well as best practice, I felt it necessary to understand how to do what I want to do and expose APIs such as require, fs, path, os etc. without using a "deprecated" feature.
Mainly wanted to post here to see if this is the proper method of using contextBridge.
main.js
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
const path = require("path");
const os = require("os");
...
let mainWindow;
function createMainWindow() {
mainWindow = new BrowserWindow({
title: "Test",
icon: `${__dirname}/assets/icons/Icon_256x256.png`,
width: isDev ? 800 : 500,
height: 600,
resizable: isDev,
backgroundColor: "white",
webPreferences: {
preload: `${__dirname}/preload.js`,
nodeIntegration: false,
contextIsolation: true,
},
});
...
ipcMain.handle("get-file-path", async (event) => {
const filePath = path.join(os.homedir(), "test");
return filePath;
});
...
preload.js
const { ipcRenderer, contextBridge } = require("electron");
contextBridge.exposeInMainWorld("api", {
getFilePath: async () => {
const res = await ipcRenderer.invoke("get-file-path");
return res;
},
});
renderer.js
window.addEventListener("DOMContentLoaded", async () => {
document.getElementById("output-path").innerText =
await window.api.getFilePath();
});
It works as intended. From my understanding of what is happening here is by using contextBridge with a callback method I'm essentially keeping everything isolated within that function and not exposing anymore that what's within that closure--is that correct?
Once again, I'm really new (1 day into electron essentially). I've only been 'developing' for 7 months or so.
Would this also be valid.. offloading to another js file and exposing those functions through context bridge?
test.js
const path = require("path");
const os = require("os");
const getFilePath = () => {
const filePath = path.join(os.homedir(), "test");
return filePath;
};
module.exports = {
path: getFilePath,
};
preload.js
const { contextBridge } = require("electron");
const mainFunctions = require("./test.js");
const { path } = mainFunctions;
contextBridge.exposeInMainWorld("api", {
getFilePath: path(),
});
Both give me the outcome I expect.

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

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.

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