You can listen for SIGINT/SIGTERM and stop your app from exiting when the User presses CTRL-C in the terminal (if it was launched from one) in node js using process.on, but the same did not work in my electron application. I also tried binding handlers to the "quit", "before-quit" and "will-quit" events of the app object but to no avail. Is it possible to detect if the user opened the app from the terminal and pressed CTRL-C in an electron app, and to bind handlers to it and stop the app from exiting. And if yes, then how would you achieve this?
Here's how I am using process.on and app.on in a basic electron Main Process, in case I am doing something wrong (my original script contains alot of code not relevant to the question and what I want to achieve does not work with basic electron-quickstart app either):
const { app, BrowserWindow } = require("electron");
const path = require("path");
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});
win.loadFile("index.html");
}
app.whenReady().then(() => {
createWindow();
app.on("activate", function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("will-quit", (e) => {
console.log("App recieved request to quit");
e.preventDefault();
});
app.on("before-quit", (e) => {
console.log("App recieved request to quit");
e.preventDefault();
});
app.on("quit", (e) => {
console.log("App recieved request to quit");
e.preventDefault();
});
process.on("SIGINT SIGTERM", () => {
console.log("Detected SIGINT/SIGTERM");
});
process.on("SIGTERM", () => {
console.log("Detected SIGTERM");
});
This question concerns Linux only.
You're registering the event listeners too early. I modified your code to include all the listeners in the block after whenReady and got the results you were expecting.
Related
Problem
I want to automate CLI tools (like kubectl) with electron GUI. It is working well in development environment... But in production build my app can't find programs in PATH.
in development
I'm using shelljs in electron. But exec method is my own implementation. (shelljs exec electron compatibility issue) It spawn child_process then resolve with stdout.
const { spawn } = require('child_process')
exports.childProcessWithResult = async function (args) {
try {
return new Promise((resolve, reject) => {
let result = ''
const res = spawn('sh', args, { shell: true })
res.stdout.on('data', (data) => {
result += `${data}`
})
res.stderr.on('data', (data) => {
result += `error: ${data}`
console.error(data.toString('utf-8'))
})
res.on('error', (error) => {
console.error(error.toString('utf-8'))
})
res.on('close', (code) => {
resolve(result)
})
})
} catch(error) {
console.error(error)
}
}
npm run electron:build
"electron:build": "npm run build && npx electron ."
On Developer Tools (via IPC)
await window.electronAPI.shell.exec('"which kubectl"')
// '/usr/local/bin/kubectl'
await window.electronAPI.shell.which('kubectl')
// '/usr/local/bin/kubectl'
await window.electronAPI.shell.exec('"command -v kubectl"')
// '/usr/local/bin/kubectl'
console.log(await window.electronAPI.shell.exec('"ls"'))
/*
README.md
arkit.json
auth.json
auto-imports.d.ts
coverage
dist
...
*/
in production build
I'm using electron builder. Target is MacOS dmg format. The app is installed and I click the icon to run my app.
await window.electronAPI.shell.exec('"which kubectl"')
// ''
await window.electronAPI.shell.which('kubectl')
// {error: "Cannot read properties of null (reading 'stdout')"}
await window.electronAPI.shell.exec('"command -v kubectl"')
// ''
console.log(await window.electronAPI.shell.exec('"ls"'))
/*
Applications
Library
System
Users
Volumes
bin
*/
My Research about sandboxing
The scripts runs on the main process of the electron.
The main process runs in a Node.js environment, meaning it has the ability to require modules and use all of Node.js APIs. [Electron docs]
Note that as in Chromium, the main (browser) process is privileged and cannot be sandboxed. [Electron docs]
IPC communication
I'm using IPC to communicate with renderer process (my vue spa).
// electron.js (main process)
const { app, BrowserWindow, ipcMain } = require('electron') // electron": "^16.2.5
const shell = require('shelljs') // "shelljs": "^0.8.5",
// <...>
// create BrowserWindow
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
//<...>
app.whenReady().then(() => {
ipcMain.handle('shell:which', async (event, ...args) => {
return shell.which(...args).catch(e => ({ error: e.message }))
})
ipcMain.handle('shell:exec', async (event, script) => {
return childProcessWithResult(['-c', script])
.then(result => result.trim())
.catch(e => ({ error: e.message }))
})
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// preload.js (renderer process)
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
shell: {
which: (...args) => ipcRenderer.invoke('shell:which', ...args),
exec: (script) => ipcRenderer.invoke('shell:exec', script)
}
})
I grant File Access and Developer Tools Permission to my app, (by Security & Privacy)
The problem is solved!! I don't know what to say. :) But I will try.
As we know, Electron doesn't sandbox the main process. The reason is just the PATH variable.
In Windows PowerShell, I can't see the problem. Why? Because the PATH environment variables are saved in Windows Registry. It is independent of PowerShell.
But in UNIX(like Mac OS, and Linux), they are set when the shell is initiated with ~/.zshrc or ~/.bashrc.
When I click the packaged app icon, to execute the application... shell is not initiated. No Environment Variables. No PATH. The Shell can not find our cli programs!
But we can read the content of ~/xxshrc. So I just execute the script, and get the PATH variable, then set it to process.env.PATH with other defaults (like brew path).
Next is the code.
const env = await childProcessWithResult('zsh -ic export')
const PATH = env.split('\n')
.find(s => s.startsWith('PATH'))
.replace('PATH=', '')
process.env.PATH = [
'/usr/local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/go/bin',
'/Library/Apple/usr/bin',
'/Applications/Privileges.app/Contents/Resources',
PATH
].join(':')
I have 3 components device, server and frontend (admin).
Server
Starts socket.io server with 2 namespaces /admin and /client.
If socket from /admin namespace sends data, server passes it along to /client namespace. If socket from /client namespace sends data, server passes it along to /admin namespace.
const io = require('socket.io');
const device = io.of('/device');
const admin = io.of('/admin');
device.on('connection', (socket) => {
socket.on('data', (data) => {
console.log("PASSING DATA FROM [DEVICE] TO [ADMIN]")
admin.emit('data', data);
})
});
admin.on('connection', (socket) => {
socket.on('data', (data) => {
console.log("PASSING DATA FROM [ADMIN] TO [DEVICE]")
device.emit('data', data);
});
});
io.listen(80);
Device
Uses socket.io-client to connect to socket.io server.
Starts interactive shell session using node-pty.
const io = require('socket.io-client');
const socket = io('http://localhost:80/client');
const os = require('os');
const pty = require('node-pty');
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 30
});
socket.on('connect', () => {
});
// INPUT DATA
socket.on('data', (data) => {
ptyProcess.write(data);
});
// OUTPUTING DATA
ptyProcess.onData = (data) => {
socket.emit('data', data)
}
Frontend
Finally I have the frontend which uses xterm.js to create a terminal inside the browser. I am using vue. The browser client as well connects to socket.io server on the /admin namespace. Basically I have this :
<template>
<div id="app">
<div id="terminal" ref="terminal"></div>
</div>
</template>
<script>
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { io } from 'socket.io-client';
export default {
mounted() {
const term = new Terminal({ cursorBlink : true });
term.open(this.$refs.terminal);
const socket = io('http://localhost:80/admin');
socket.on('connect', () => {
term.write('\r\n*** Connected to backend***\r\n');
term.onData((data) => {
socket.emit('data', data);
})
socket.on('data', (data) => {
term.write(data);
});
socket.on('disconnect', () => {
term.write('\r\n*** Disconnected from backend***\r\n');
});
});
}
}
</script>
Problem
❌ Starting the pty session seems to work, at least there are now errors reported. However it seems the onData listener callback is never fired, even when I ptyProcess.write() something.
❌ Getting input from xterm all the way to the device ptyProcess.write does not seem to work. I can see the data passed along through the socket.io sockets all the way to the device. But from there nothing happens. What do I miss ? Also I don't see my input in the xterm window as well.
After switching from child_process to using node-pty to create an interactive shell session I almost had it right. Following the node-pty documentation it marked the on('data') eventhandler as deprecated. Instead I should use .onData property of the process to register a callback. Like this:
ptyProcess.onData = function(data) {
socket.emit('data', data);
};
But that didn't do anything. So I switched back to the depracated way of adding an event listener:
ptyProcess.on('data', function(data) {
socket.emit('data', data);
});
Now I have a working interactive shell session forwarded from a remote device through websocket inside my browser ✅.
UPDATE
Did more digging for onData property. Realized it's not a property but a method so I used it wrong. This would be the prefered way :
ptyProcess.onData(function(data) {
socket.emit('data', data);
});
Which also works as expected 👍
I have a working dev app in electron that will refuse to run in production due to this strange error
(node:10285) ProtocolDeprecateCallback: The callback argument of protocol module APIs is no longer needed.
writeOut # internal/process/warning.js:32
I've build the app in past with success and was working, but now I have this problem.
This is the background.js file of the electron app I think that the error can be cause from something inside this file
'use strict'
import { app, protocol, BrowserWindow } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import path from 'path';
import './ipc-services';
const isDevelopment = process.env.NODE_ENV !== 'production';
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
]);
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
//If I enable context isolation window.ipcRenderer will stop working also with preload.js script loaded
//contextIsolation: true,
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
preload: path.join(__dirname, 'preload.js'),
icon: path.join(__static, 'icon.png')
}
});
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools();
} else {
createProtocol('app');
// Load the index.html when not in development
win.loadURL(path.join(__dirname, 'index.html'));
}
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
})
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();
})
// 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', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS);
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString());
}
}
createWindow();
});
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit();
}
})
} else {
process.on('SIGTERM', () => {
app.quit();
});
}
}
Is there a fix that I can apply?
After a lot of research and debug I've discovered two problems in my code. The first one is that process.env.IS_TEST is not defined, I've removed that line from my code. No problem occur in electron:serve mode. The main problem with the blank screen in production instead, was because win.loadURL(path.join(__dirname, 'index.html')) was trying to load the files ufing the file: protocol instead of the custom one registered from the app.
I've reverted back to the original code and used the win.loadURL('app://./index.html')
After this little modification the app seems working fine also in production.
I am new to electron and converting an web app to desktop application.I am loading pages from file system.Cookies are working if pages are served from web server but when I load pages from local folder I am not able to save them. I am saving cookie using document.cookie in web.I want to know how I can I enable file:// cookies in electron .
Regards
Well, I want to answer my question in case somebody is having the same problem. I have fixed the cookie problem by registerStandardSchemes. The sample code is as follows and code works for saving cookies from web pages as well:
protocol.registerStandardSchemes(["app"], {
secure: true
});
and on ready event
protocol.registerFileProtocol('app', (request, callback) => {
const urls = request.url.substr(6)
const parsedUrl = url.parse(urls);
// extract URL path
const pathname = `.${parsedUrl.pathname}`;
// based on the URL path, extract the file extention. e.g. .js, .doc, ...
const ext = path.parse(pathname).ext;
callback({
path: path.normalize(`${__dirname}/${parsedUrl.pathname}`)
})
}, (error) => {
if (error) {
console.error('Failed to register protocol');
}
});
Follow the documentation to get it done, and use the standard.https://electronjs.org/docs/api/cookies
const {session} = require('electron')
// Query all cookies.
session.defaultSession.cookies.get({}, (error, cookies) => {
console.log(error, cookies)
})
// Query all cookies associated with a specific url.
session.defaultSession.cookies.get({url: 'http://www.github.com'}, (error, cookies) => {
console.log(error, cookies)
})
// Set a cookie with the given cookie data;
// may overwrite equivalent cookies if they exist.
const cookie = {url: 'http://www.github.com', name: 'dummy_name', value: 'dummy'}
session.defaultSession.cookies.set(cookie, (error) => {
if (error) console.error(error)
})
OK, I got it working with Electron 5. Below are the relevant bits based on #zahid-nisar's solution, and below that a full sample Electron main.js to show how it all fits together. Obviously, change the location of your app in mainWindow.loadURL('app://www/index.html');.
Relevant code to insert in main.js:
const { protocol } = require('electron');
protocol.registerSchemesAsPrivileged([{
scheme: 'app',
privileges: {
standard: true,
secure: true
}
}]);
Inside app.on('ready') function:
protocol.registerFileProtocol('app', (request, callback) => {
const url = request.url.substr(6);
callback({
path: path.normalize(`${__dirname}/${url}`)
});
}, (error) => {
if (error) console.error('Failed to register protocol');
});
Then, inside your createWindow function, load your app like this:
mainWindow.loadURL('app://www/index.html');
And finally, here is a complete sample main.js with the above code (plus extras that I need, like Service Worker):
// Modules to control application life and create native browser window
const {
app,
protocol,
BrowserWindow
} = require('electron');
const path = require('path');
// This is used to set capabilities of the app: protocol in onready event below
protocol.registerSchemesAsPrivileged([{
scheme: 'app',
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true
}
}]);
// 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 mainWindow;
function 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.loadURL('app://www/index.html');
// DEV: Enable code below to check cookies saved by app in console log
// mainWindow.webContents.on('did-finish-load', function() {
// mainWindow.webContents.session.cookies.get({}, (error, cookies) => {
// console.log(cookies);
// });
// });
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// 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.
mainWindow = null;
});
}
// 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', () => {
protocol.registerFileProtocol('app', (request, callback) => {
const url = request.url.substr(6);
callback({
path: path.normalize(`${__dirname}/${url}`)
});
}, (error) => {
if (error) console.error('Failed to register protocol');
});
// Create the new window
createWindow();
});
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', function () {
// 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 (mainWindow === 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.
I have a nodejs restful style service which has no front end, it just accepts data and then does something with it.
I have unit tested most of the method level stuff I want to, however now I want to basically do some automated tests to prove it all works together. When I am using ASP.MVC and IIS its easy as the server is always on, so I just setup the scenario (insert dummy guff into DB) then make a HttpRequest and send it to the server and assert that I get back what I expect.
However there are a few challenges in nodejs as the applications need to be run via command line or some other mechanism, so given that I have an app.js which will start listening, is there some way for me to automatically start that going before I run my tests and then close it once my tests are finished?
I am currently using Yadda with Mocha for my testing so I can keep it written in a BDD style way, however I am hoping the starting of the web app is agnostic of the frameworks I am using.
Just expose some methods to start and stop your webserver. Your app.js file could be something like this:
var app = express()
var server = null
var port = 3000
// configure your app here...
exports.start = function(cb) {
server = http.createServer(app).listen(port, function () {
console.log('Express server listening on port ' + port)
cb && cb()
})
}
exports.close = function(cb) {
if (server) server.close(cb)
}
// when app.js is launched directly
if (module.id === require.main.id) {
exports.start()
}
And then in your tests you can do something like this (mocha based example):
var app = require('../app')
before(function(done) {
app.start(done)
})
after(function(done) {
app.close(done)
})
Have a look to supertest https://github.com/visionmedia/supertest
You can write test like
describe('GET /users', function(){
it('respond with json', function(done){
request(app)
.get('/user')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done);
})
})
Using gimenete's answer, here's an example of a service (server) with async await and express:
service.js:
const app = require('express')()
const config = require('./config')
let runningService
async function start() {
return new Promise((resolve, reject) => {
runningService = app.listen(config.get('port'), config.get('hostname'), () => {
console.log(`API Gateway service running at http://${config.get('hostname')}:${config.get('port')}/`)
resolve()
})
})
}
async function close() {
return new Promise((resolve, reject) => {
if (runningService) {
runningService.close(() => {
})
resolve()
}
reject()
})
}
module.exports = {
start,
close
}
service.spec.js:
const service = require('../service')
beforeEach(async () => {
await service.start()
})
afterEach(async () => {
await service.close()
})