Vue/Electron IPC between main and renderer - node.js

I'm trying to setup communication between my Vue browser app and the electron main process, but it is not working.
Before startBot() is even called, I get an error message that __Dirname is unknown. But this constant is nowhere in to be found in the code.
What am I doing wrong?
https://gist.github.com/Quenos/7d6dbe4f5410739499ea9e3b0b4f961a.js
This is the background.js where I open the browser window with a preload. This has the purpose of making window available to the browser process
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1300,
height: 1100,
title: "Hedgehog TRDR Bot",
icon: path.join(__static, "hedgehog.jpg"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// __static is set by webpack and will point to the public directory
preload: path.resolve(__static, "preload.js"),
},
});
This is said preload.js
const { contextBridge, ipcRenderer } = require("electron");
const validChannels = ["READ_FILE", "WRITE_FILE"];
contextBridge.exposeInMainWorld("ipc", {
send: (channel, data) => {
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, func) => {
if (validChannels.includes(channel)) {
// Strip event as it includes `sender` and is a security risk
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
The main process which contains listeners that then will do file handling
const { ipcMain } = require("electron");
const fs = require("fs");
var file;
ipcMain.on("OPEN_FILE", (event, payload) => {
console.log("daaro");
file = fs.createWriteStream(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
ipcMain.on("TEST_FILE", (event, payload) => {
console.log("daaro");
file.write(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
And the browser process which send requests to do file handling:
async startBot() {
window.ipc.send("OPEN_FILE", { path: "./HH_trdr_bot.csv" });
}

In the meantime I've found this article that perfectly answers my question
https://medium.com/swlh/how-to-safely-set-up-an-electron-app-with-vue-and-webpack-556fb491b83

Docs of Vue CLI Plugin Electron Builder contain description of how to do it, but it's a little scattered.
First, see how to configure a preload script, i.e.:
vue.config.js
module.exports = {
// ...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
and then, repeat the path in BrowserWindow's webPreferences.preload constructor option.
preload: path.join(__dirname, 'preload.js')
Finally, in src/preload.js expose the IPC Renderer as described in IPC Without Node Integration.
src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
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))
}
}
})
It might be also a good idea to unsubscribe the event listener when the component that subscribed is about to be destroyed.

I made mine working by putting the preload.js file inside public folder, in the https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#preload-files docs, there is a little diagram under Folder Structure section that says ├── public/ # Files placed here will be available through __static or process.env.BASE_URL, so what I did is simply, use the __static variable as described in docs, and append it with \preload.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __static + '/preload.js', // <-- simple solution
}
})

Related

Kaspersky marks my Electron app as a Trojan

I recently made an Electron app that is essentially a web browser and it has 3 windows and they all get destroyed when the user closes them, leaving no excess process behind this is the only complex part of the app but Kaspersky flags it as a Trojan.
my index.js
const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron');
const path = require('path');
app.commandLine.appendSwitch("disable-http-cache");
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
// eslint-disable-next-line global-require
if (require('electron-squirrel-startup')) {
app.quit();
}
function createMacroWindow(macroUrl) {
let macroWindow = new BrowserWindow({
width:800,
height:500,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences:{
devTools: false,
webviewTag: true,
nodeIntegration: true,
contextIsolation: false
}
})
macroWindow.loadFile(path.join(__dirname, './windows/macro-window.html'));
macroWindow.webContents.on('did-finish-load', () => {
macroWindow.webContents.send("loadURL", macroUrl)
})
macroWindow.center();
macroWindow.openDevTools();
macroWindow.show();
macroWindow.on("close", () => {
macroWindow = null;
})
require("#electron/remote/main").enable(macroWindow.webContents);
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 700,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
webviewTag: true,
nodeIntegration: true,
devTools: true, // Geliştirici konsolunu kökten kısıtlayan kod (bu olmazsa CTRL+SHIFT+I yapınca yine açılır)
contextIsolation: false,
},
});
let splashScreen = new BrowserWindow({
width:1000,
height:700,
frame:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
devTools:false
}
})
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
splashScreen.loadFile(path.join(__dirname, './windows/splash-window.html'));
splashScreen.center();
function destroySplashScreen() {
splashScreen.close();
splashScreen = null;
}
mainWindow.webContents.on('did-finish-load', function() {
mainWindow.show();
mainWindow.maximize();
splashScreen.isDestroyed() ? console.log("splash screen already destroyed") : destroySplashScreen();
});
require('#electron/remote/main').initialize()
require("#electron/remote/main").enable(mainWindow.webContents)
ipcMain.handle(
'DESKTOP_CAPTURER_GET_SOURCES',
(event, opts) => desktopCapturer.getSources(opts)
)
let macroUrl;
ipcMain.on("openMacroWindow", (e, urlToLoad) => {
macroUrl = urlToLoad;
createMacroWindow(macroUrl);
})
ipcMain.on("closeApp", () => {
BrowserWindow.getAllWindows().forEach(window => window.close());
})
ipcMain.on('clearCache', () => {
mainWindow.webContents.session.clearStorageData([], function (data) {
console.log(data);
})
mainWindow.webContents.session.clearCache();
})
mainWindow.on("close", () => {
app.quit();
})
// Open the DevTools.
// mainWindow.webContents.openDevTools(); // Geliştirici konsolunu kapatmak için bu satırı silebilirsiniz
};
// 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);
// app.disableHardwareAcceleration()
// 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();
}
});
app.on('activate', () => {
// On OS X 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();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
splashScreen is the window that opens while the mainWindow loads, it closes when the main loads and is set to null afterwards just like the macroWindow.
macroWindow is opened when the ipcMain is told to openMacroWindow
Fix
I downgraded to electron-forge version 5.2.4 from 6.x version, this fixed my issue but this is not an ideal solution.

NodeJS contextBridge receiving results from index.js

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);
});

Preload script not loading in electron react

I'm building an app with React and Electron and trying to create a communication channel between the main (electron.js in my case) and renderer processes. By achieving this, I want to access the Store object created in my main process to save user preferences etc.
I have set up a preload script and provided the path in my main.js (electron.js). But when I try to access the method defined in the preload.js from the renderer like this window.electronAPI.sendData('user-data', this.state), it does not recognise the electronAPI (undefined variable electronAPI). Also the console.log in my preload.js is never shown when a window is loaded. Hence I assume the preload script never gets loaded and I don't know why. Any help is very much appreciated!
EDIT: The preload script seems to load now because the console.log gets printed. Maybe preload.js did load before too but I could not see it since I could only access the application by opening localhost:3000 in the browser. Now the app opens in a BrowserWindow. However the 'electronAPI' defined in the preload script cannot be accessed still.
Here is the code:
electron.js (main.js)
const electron = require('electron');
const path = require('path');
const {app, BrowserWindow, Menu, ipcMain} = electron;
let isDev = process.env.APP_DEV ? (process.env.APP_DEV.trim() === "true") : false;
function createWindow (){
const win = new BrowserWindow({
width: 970,
height: 600,
backgroundColor: '#0C0456',
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js')
}
});
//if in development mode, load window from localhost:3000 otherwise build from index.html
try {
win.webContents.loadURL(isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname,'../build/index.html')}`
)
} catch(e){
console.error(e)
}
win.webContents.openDevTools({"mode":"detach"});
//Remove menu
Menu.setApplicationMenu(null);
win.once('ready-to-show', () => win.show());
console.log("main window is shown");
console.log(typeof path.join(__dirname, 'preload.js'));
win.on('crashed',() => {
console.error(`The renderer process has crashed.`);
})
}
app.on( 'ready', () => {
createWindow(); // open default window
} );
ipcMain.on('user-data', (event, args)=>{
console.log(args);
});
preload.js
const {contextBridge, ipcRenderer} = require("electron");
const validChannels = ['user-data'];
console.log("this is the preload script");
contextBridge.exposeInMainWorld('electronAPI', {
sendData: (channel, data) => {
if(validChannels.includes(channel)){
ipcRenderer.send(channel, data);
}
}
})

Electron Preload vs Electron Main

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.

How to get the path to the directory of the opened file?

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>

Resources