'Executing a bash script from Electron app

I am trying to execute bash script within Electron index.html button click. Here's my code which index.html calling renderer.js, and renderer.js opens the bash script. When I run the code, I see a button that I can click, but even when I click it, I do not see "Hello World" from stdout terminal. Does anyone have any advice for solving this problem? Any help much appreciated!

index.html

 <h0>My-Flection</h0>
 <button id="openBtn">Open</button>
 <script>
   require('./renderer.js')
 </script>

renderer.js

const openBtn = document.getElementById('openBtn')
const shell = require('electron').shell

openBtn.addEventListener('click', function(event) {
    shell.openItem("./test.sh")
})

test.sh

echo "Hello World"


Solution 1:[1]

Electron's shell method is not really used for running scripts. It's used for showing a file in the systems file manager, opening a file "in the desktop's default manner", moving files to trash / recycle bin and playing a beep sound among other things.

As you are using Electron you should really take advantage of the different processes and run your script(s) within the main process. Doing so will prevent any possible locking of your render process (plus separate your concerns if you are so inclined).

Below is a preload.js script that allows communication between your main process and render process(es) via the use of whitelisted channel names. The only implementations within this preload.js script is the use of ipcRenderer. See Context Isolation and Inter-Process Communication for more information


In this preload.js script we are using the channel name runScript to communicate from the render process to the main 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': [
            'runScript' // Channel name
        ],
        // From main to render.
        'receive': [],
        // 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);
            }
        }
    }
);

This preload.js script is used like so...

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

In this main.js script, listen for a message on channel name runScript, then run the script using spawn.

exec could be used if steaming is not needed. IE: exec buffers output.

main.js (main process)

'use strict';

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;

const nodePath = require("path");
const nodeChildProcess = require('child_process');

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('runScript', () => {
    // Windows
    let script = nodeChildProcess.spawn('cmd.exe', ['/c', 'test.bat', 'arg1', 'arg2']);

    // MacOS & Linux
    // let script = nodeChildProcess.spawn('bash', ['test.sh', 'arg1', 'arg2']);

    console.log('PID: ' + script.pid);

    script.stdout.on('data', (data) => {
        console.log('stdout: ' + data);
    });

    script.stderr.on('data', (err) => {
        console.log('stderr: ' + err);
    });

    script.on('exit', (code) => {
        console.log('Exit Code: ' + code);
    });
})

Some test scripts.

test.bat (for Windows)

echo "hello World"

echo %1%
echo %2%

test.sh (for MacOS & Linux)

echo "Hello World"

echo $1
echo $2

Lastly, here is a simplified index.html file. On button click, send a message to the main process via the channel name runScript.

index.html (render process)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Electron Test</title>
    </head>

    <body>
        <input type="button" id="button" value="Run Script">
    </body>

    <script>
        document.getElementById('button').addEventListener('click', () => {
            window.ipcRender.send('runScript');
        })
    </script>
</html>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 midnight-coding