最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Executing a bash script from Electron app - Stack Overflow

programmeradmin3浏览0评论

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"

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"
Share Improve this question edited Oct 18, 2022 at 12:04 aynber 23k9 gold badges54 silver badges67 bronze badges asked Apr 22, 2022 at 18:34 yunleyunle 311 silver badge3 bronze badges 3
  • 1 Is this test.sh file in same location? – user7040213 Commented Apr 22, 2022 at 20:07
  • @KishanVaishnani It has to be or otherwise they wouldn't put "./test.sh" but I could be wrong. – WhatTheClown Commented Apr 23, 2022 at 16:33
  • @KishanVaishnani yes, they're all in the same directory. – yunle Commented Apr 24, 2022 at 19:52
Add a ment  | 

2 Answers 2

Reset to default 8

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 munication 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 municate from the render process to the main process.

preload.js (main process)

// Import the necessary Electron ponents.
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 streaming 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>

electron + bash. demo.

A slightly less simple electron app that execs a bash function when you click a button

https://github./patarleth/bashit-electron


#/mydir/bash_src/main.js

const {app, BrowserWindow, ipcMain} = require('electron')
const child_process = require('child_process').execFile;

// 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

function createWindow () {
    // Create the browser window.
    win = new BrowserWindow({width: 800, height: 600})
    
    // and load the index.html of the app.
    win.loadFile('index.html')
    
    // Open the DevTools.
    // win.webContents.openDevTools()
    
    // Emitted when the window is closed.
    win.on('closed', () => {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        win = null
    })
}

function bashit_fn(sender, fnName) {
    var executablePath = '/usr/bin/env'
    var source_path = 'source ' + __dirname + '/bash_src/lib.sh; ' + fnName;
    var parameters = [ 'bash', '-c', source_path ];
    
    child_process(executablePath, parameters, function(err, data) {
        var msg = data.toString();
        if (err) {
            console.error(err);
            msg += err;
        }
        var eventName = 'bash-function-' + fnName;
        sender.send(eventName, msg);
        console.log(msg);
    });
}

function hello_fn(sender) {
    bashit_fn(sender, 'hello_fn');
}

ipcMain.on('call-bash-function-hello_fn', (event, arg) => {
    hello_fn(event.sender);
})

// 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)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    app.quit()
})

app.on('activate', () => {
    // On macOS it's mon to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        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 require them here.


#/mydir/bash_src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>bash exec functions!</title>
  </head>
  <body>
    <h1>bash exec function examples!</h1>

    <button id="hello-button">test bash function</button>
    
    <h2 id="hello-header">...</h2>

    <script>
      var ipc = require('electron').ipcRenderer;
      
      var helloButton = document.getElementById('hello-button');

      // when you click the button it sends an event named 'hello-from-renderer' to the backend process
      helloButton.addEventListener('click', function(){
        ipc.send('call-bash-function-hello_fn', 'hello');
        console.log('hello-button clicked');
      });

      // iff the backend sends an event named 'bash-function-hello_fn' the h2 header named 'hello-header' html is updated
      ipc.on('bash-function-hello_fn',(event,dataFromMain) => {
        console.log( 'bash-function-hello_fn |' + dataFromMain + '|');
        var helloH2 = document.getElementById('hello-header');
        helloH2.innerHTML = dataFromMain;
      });
     </script
  </body>
</html>

#/mydir/bash_src/lib.sh

LIB_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"

hello_fn() {
    echo "hello from hello_fn $(pwd) - LIB_SCRIPT_DIR $LIB_SCRIPT_DIR"
}

发布评论

评论列表(0)

  1. 暂无评论