child_process.on('close') is sometimes painfully slow - node.js

I have a electron application that opens a external program (in my case Office), and has to wait for the program to be closed.
the code I wrote works great but sometimes the child_process.on('close') event is fired 10 or 20 seconds after the program has closed. The code is:
const cp = require("child_process");
child = cp.spawn(path/to/Office.exe + ' "' + path/to/myFile.pptx + '"', {shell: true});
child.on('close', function (code) {
//do something
});
Most of the time it reacts after 1 or 2 seconds which is fine, but sometimes it takes up to 20 seconds until I receive the close event. The program closes fast (according to the task manager), but node seems to wait for something.
I also tried child.on('exit'), calling the program with cp.exec()and using the options.stdio: ignore for spawn, as I thought maybe node is waiting for some stream from the child. But that made no difference.
Does anybody know a safe way to speed that process up?

I have tried your code and the close event triggers with a 0.5-2s delay, bearable i would say.
However, the 20s delay did not occur, but if this problem still persists on your end, you can try the approach below, which consists in checking the spawn pid.
const pidExists = (pid) => {
let pidOk = true;
try {
process.kill(pid, 0);
} catch (e) {
pidOk = false;
}
return pidOk;
};
const cp = require("child_process");
// I added the detach option because we won't need that process anymore since we're following the PID.
let child = cp.spawn(path/to/Office.exe + ' "' + path/to/myFile.pptx + '"', {shell: true, detach: true});
let officePID = child.pid; // this is the spawn pid
setInterval(()=>{
if( pidExists(officePID)){
console.log('file is still open', new Date().getTime());
}else{
console.log('file was closed', new Date().getTime());
process.exit(0);
}
}, 500);
This is a better approach since you said that the task manager shows you that the program was closed.

Related

Node-cli freeze after Spawning a child_process

The cli could not receive keyboard input after execution, and this also includes 'ctrl-c' and 'ctrl-z', thus you have to manually exit the program. It gave me a lot of trouble, please take a look at it;
var { exec, spawn } = require("child_process");
let cmd = (cmdline, consolelog = true) => {
return new Promise((resolve, reject) => {
let cmdarray = cmdline.split(" ");
let result = "";
let error = "";
let child = spawn(cmdarray.shift(), cmdarray);
process.stdin.pipe(child.stdin);
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stderr.on("data", data => {
if (consolelog) process.stdout.write(data.toString());
error = data.toString();
});
child.stdout.on("data", data => {
if (consolelog) process.stdout.write(data.toString());
result = data.toString();
});
child.on("close", code => {
if (consolelog) process.stdout.write(`Exit code: ${code}\n`);
code == 0 ? resolve(result) : reject(error);
});
});
};
OS: osx & ubuntu 19.04
Test case:
cmd("echo hi");
Edit:
Normal circumstances : put the code inside myprogram.js and use node myprogram.js to activate the script. It works perfectly, and you can also try different commands. HOWEVER, if you put following code by using
$ node
> let cmd = require(PATH_TO_CMD_FUNCTION)
> cmd("echo hi");
The node-cli will freeze and stop listening to your keyboard input.
Edit 2:
Found out, you need to channel through {stdio: "inherit"}
UPDATED ANSWER:
I trimmed down your spawner a little in order to be succinct, and eliminate any other possibilities. There is one common test case I could find to reproduce stated issue regarding signals, keyboard shortcuts, and trapped input.
If you spawn the 'sh' command, you will not be able to escape from the spawned process by means of conventional signal keyboard shortcuts. This is because node.js "traps" the input and forwards it directly to the spawned process.
Most processes allow killing via signalling through keyboard shortcuts such as CTRL-C. 'sh', however, does not-- and so is a perfect example.
The only ways to exit are to use the 'exit' command, close the window (which may possibly leave the spawned process running in the background), reboot your machine, etc. Also, internally or by other means sending a signal, but not via stdin or equivalent.
Your CTRL-C input, in other words, is "normally working" not because it is killing your node app, but because it is being forwarded to the spawned process and killing it.
The spawned process will continue to trap your input if it is immune.
require("child_process").spawn("sh", {
shell: true,
encoding: 'utf8',
stdio: [0,1,2]
});
This may not be the best example for your specific program, but it illustrates the principle, which is the closest I can come since I cannot replicate with the given test case (I have tried it on my phone, my laptop, and my cloud server, three different versions of node, two different versions of Ubuntu).
In any case, it sounds like your stdin is not being "let go" by the spawned process. You may need to "reassign" it to the original process.stdin .
As stated here:
https://node.readthedocs.io/en/latest/api/child_process/
Also, note that node establishes signal handlers for 'SIGINT' and
'SIGTERM', so it will not terminate due to receipt of those signals,
it will exit.
PREVIOUSLY:
It looks like your cmd function is only getting one argument (the command itself) due to the split and shift. Spawn expects a string with the whole command, so likely it is only getting "echo" without "hi", so it isn't exiting due to hanging on "echo". May need to append a newline ("\n") as well.
It also may help to nest the command in an sh command that then executes it, so it runs in a shell.
Like this:
var { exec, spawn } = require("child_process");
let cmd = (cmdline, consolelog = true) => {
return new Promise((resolve, reject) => {
let result = "";
let error = "";
// This. Note the shell option.
let child = spawn(cmdline, {shell:true});
process.stdin.pipe(child.stdin);
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stderr.on("data", data => {
if (consolelog) process.stdout.write(data.toString());
error = data.toString();
});
child.stdout.on("data", data => {
if (consolelog) process.stdout.write(data.toString());
result = data.toString();
});
child.on("close", code => {
if (consolelog) process.stdout.write(`Exit code: ${code}\n`);
code == 0 ? resolve(result) : reject(error);
});
});
};
cmd("echo hello");
Output:
hello
Exit code: 0

Execute script from Node in a separate process

What I want to do is when an endpoint in my Express app is hit, I want to run a command line script - without waiting for the result - in a separate process.
Right now I am using the child_process’s spawn function and it is working, but if the Node server were to quit, the child script would quit as well. I need to have the child script run to completion even if the server quits.
I don’t need access to stdout or anything from the child script. I just need a way to basically “fire and forget”
Is there any way to do this with spawn that I may be missing? Or is there another way I should be going about this?
Thanks in advance for any guidance!
What you want here is options.detached of spawn. Setting this option will allow the sub-process to continue even after the main process calling spawn has terminated.
Quoting the documentation:
On Windows, setting options.detached to true makes it possible for the child process to continue running after the parent exits. The child will have its own console window. Once enabled for a child process, it cannot be disabled.
On non-Windows platforms, if options.detached is set to true, the child process will be made the leader of a new process group and session. Note that child processes may continue running after the parent exits regardless of whether they are detached or not. See setsid(2) for more information.
Basically this means what you "launch" keeps running until it actually terminates itself. As 'detached', there is nothing that "ties" the sub-process to the execution of the parent from which it was spawned.
Example:
listing of sub.js:
(async function() {
try {
await new Promise((resolve,reject) => {
let i = 0;
let ival = setInterval(() => {
i++;
console.log('Run ',i);
if (i === 5) {
clearInterval(ival);
resolve();
}
}, 2000);
});
} catch(e) {
console.error(e);
} finally {
process.exit();
}
})();
listing of main.js
const fs = require('fs');
const { spawn } = require('child_process');
(async function() {
try {
const out = fs.openSync('./out.log', 'a');
const err = fs.openSync('./out.log', 'a');
console.log('spawn sub');
const sub = spawn(process.argv[0], ['sub.js'], {
detached: true, // this removes ties to the parent
stdio: [ 'ignore', out, err ]
});
sub.unref();
console.log('waiting..');
await new Promise((resolve,reject) =>
setTimeout(() => resolve(), 3000)
);
console.log('exiting main..');
} catch(e) {
console.error();
} finally {
process.exit();
}
})();
The basics there are that the sub.js listing is going to output every 2 seconds for 5 iterations. The main.js is going to "spawn" this process as detached, then wait for 3 seconds and terminate itself.
Though it's not really needed, for demonstration purposes we are setting up the spawned sub-process to redirect its output ( both stdout and stderr ) to a file named out.log in the same directory.
What you see here is that the main listing does it's job and spawns the new process then terminates after 3 seconds. At this time the sub-process will only have output 1 line, but it will continue to run and produce output to the redirected file for another 7 seconds, despite the main process being terminated.

Electron kill child_process.exec

I have an electron app that uses child_process.exec to run long running tasks.
I am struggling to manage when the user exits the app during those tasks.
If they exit my app or hit close the child processes continue to run until they finish however the electron app window has already closed and exited.
Is there a way to notify the user that there are process still running and when they have finished then close the app window?
All I have in my main.js is the standard code:
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X 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();
}
});
Should I be adding a check somewhere?
Thanks for your help
EDITED
I cannot seem to get the PID of the child_process until it has finished. This is my child_process code
var loader = child_process.exec(cmd, function(error, stdout, stderr) {
console.log(loader.pid)
if (error) {
console.log(error.message);
}
console.log('Loaded: ', value);
});
Should I be trying to get it in a different way?
So after everyones great comments I was able to update my code with a number of additions to get it to work, so am posting my updates for everyone else.
1) Change from child_process.exec to child_process.spawn
var loader = child_process.spawn('program', options, { detached: true })
2) Use the Electron ipcRenderer to communicate from my module to the main.js script. This allows me to send the PIDs to main.js
ipcRenderer.send('pid-message', loader.pid);
ipcMain.on('pid-message', function(event, arg) {
console.log('Main:', arg);
pids.push(arg);
});
3) Add those PIDs to array
4) In my main.js I added the following code to kill any PIDs that exist in the array before exiting the app.
// App close handler
app.on('before-quit', function() {
pids.forEach(function(pid) {
// A simple pid lookup
ps.kill( pid, function( err ) {
if (err) {
throw new Error( err );
}
else {
console.log( 'Process %s has been killed!', pid );
}
});
});
});
Thanks for everyones help.
ChildProcess emits an exit event when the process has finished - if you keep track of the current processes in an array, and have them remove themselves after the exit event fires, you should be able to just foreach over the remaining ones running ChildProcess.kill() when you exit your app.
This may not be 100% working code/not the best way of doing things, as I'm not in a position to test it right now, but it should be enough to set you down the right path.
var processes = [];
// Adding a process
var newProcess = child_process.exec("mycommand");
processes.push(newProcess);
newProcess.on("exit", function () {
processes.splice(processes.indexOf(newProcess), 1);
});
// App close handler
app.on('window-all-closed', function() {
if (process.platform != 'darwin') {
processes.forEach(function(proc) {
proc.kill();
});
app.quit();
}
});
EDIT: As shreik mentioned in a comment, you could also just store the PIDs in the array instead of the ChildProcess objects, then use process.kill(pid) to kill them. Might be a little more efficient!
Another solution. If you want to keep using exec()
In order to kill the child process running by exec() take a look to the module ps-tree. They exaplain what is happening.
in UNIX, a process may terminate by using the exit call, and it's
parent process may wait for that event by using the wait system call.
the wait system call returns the process identifier of a terminated
child, so that the parent tell which of the possibly many children has
terminated. If the parent terminates, however, all it's children have
assigned as their new parent the init process. Thus, the children
still have a parent to collect their status and execution statistics.
(from "operating system concepts")
SOLUTION: use ps-tree to get all processes that a child_process may have started, so that they
exec() actually works like this:
function exec (cmd, cb) {
spawn('sh', ['-c', cmd]);
...
}
So check the example and adapt it to your needs
var cp = require('child_process'),
psTree = require('ps-tree');
var child = cp.exec("node -e 'while (true);'", function () { /*...*/ });
psTree(child.pid, function (err, children) {
cp.spawn('kill', ['-9'].concat(children.map(function (p) { return p.PID })));
});

child_process spawn Race condition possibility in nodejs

I'm starting to learn and use node and I like it but I'm not really sure how certain features work. Maybe you can help me resolve one such issue:
I want to spawn local scripts and programs from my node server upon rest commands. looking at the fs library I saw the example below of how to spawn a child process and add some pipes/event handlers on it.
var spawn = require('child_process').spawn,
ps = spawn('ps', ['ax']),
grep = spawn('grep', ['ssh']);
ps.stdout.on('data', function (data) {
grep.stdin.write(data);
});
ps.stderr.on('data', function (data) {
console.log('ps stderr: ' + data);
});
ps.on('close', function (code) {
if (code !== 0) {
console.log('ps process exited with code ' + code);
}
grep.stdin.end();
});
grep.stdout.on('data', function (data) {
console.log('' + data);
});
grep.stderr.on('data', function (data) {
console.log('grep stderr: ' + data);
});
grep.on('close', function (code) {
if (code !== 0) {
console.log('grep process exited with code ' + code);
}
});
What's weird to me is that I don't understand how I can be guaranteed that the event handler code will be registered before the program starts to run. It's not like there's a 'resume' function that you run to start up the child. Isn't this a race condition? Granted the condition would be minisculy small and would almost never hit because its such a short snipping of code afterward but still, if it is I'd rather not code it this way out of good habits.
So:
1) if it's not a race condition why?
2) if it is a race condition how could I write it the right way?
Thanks for your time!
Given the slight conflict and ambiguity in the accepted answer's comments, the sample and output below tells me two things:
The child process (referring to the node object returned by spawn) emits no events even though the real underlying process is live / executing.
The pipes for the IPC are setup before the child process is executed.
Both are obvious. The conflict is w.r.t. interpretation of the OP's question:-
Actually 'yes', this is the epitome of a data race condition if one needs to consider the real child process's side effects. But 'no', there's no data race as far as IPC pipe plumbing is concerned. The data is written to a buffer and retrieved as a (bigger) blob as and when (as already well described) the context completes allowing the event loop to continue.
The first data event seen below pushes not 1 but 5 chunks written to stdout by the child process whilst we were blocking.. thus nothing is lost.
sample:
let t = () => (new Date()).toTimeString().split(' ')[0]
let p = new Promise(function (resolve, reject) {
console.log(`[${t()}|info] spawning`);
let cp = spawn('bash', ['-c', 'for x in `seq 1 1 10`; do printf "$x\n"; sleep 1; done']);
let resolved = false;
if (cp === undefined)
reject();
cp.on('error', (err) => {
console.log(`error: ${err}`);
reject(err);
});
cp.stdout.on('data', (data) => {
if (!resolved) {
console.log(`[${t()}|info] spawn succeeded`);
resolved = true;
resolve();
}
process.stdout.write(`[${t()}|data] ${data}`);
});
let ts = parseInt(Date.now() / 1000);
while (parseInt(Date.now() / 1000) - ts < 5) {
// waste some cycles in the current context
ts--; ts++;
}
console.log(`[${t()}|info] synchronous time wasted`);
});
Promise.resolve(p);
output:
[18:54:18|info] spawning
[18:54:23|info] synchronous time wasted
[18:54:23|info] spawn succeeded
[18:54:23|data] 1
2
3
4
5
[18:54:23|data] 6
[18:54:24|data] 7
[18:54:25|data] 8
[18:54:26|data] 9
[18:54:27|data] 10
It is not a race condition. Node.js is single threaded and handles events on a first come first serve basis. New events are put at the end of the event loop. Node will execute your code in a synchronous manner, part of which will involve setting up event emitters. When these event emitters emit events, they will be put to the end of the queue, and will not be handled until Node finishes executing whatever piece of code its currently working on, which happens to be the same code that registers the listener. Therefore, the listener will always be registered before the event is handled.

Prevent sending data to stdin if spawn fails

In my Node.js (v0.10.9) code I'm trying to detect 2 cases:
an external tool (dot) is installed - in that case I want to send some data to stdin of created process
the external tool is not installed - in that case I want to display warning and I don't want to send anything to process' stdin
My problem is that I don't know how to send data to child's stdin if and only if the process was spawned successfully (i.e. stdin is ready for writing).
Following code works fine if dot is installed, but otherwise it tries to send data to the child although the child wasn't spawned.
var childProcess = require('child_process');
var child = childProcess.spawn('dot');
child.on('error', function (err) {
console.error('Failed to start child process: ' + err.message);
});
child.stdin.on('error', function(err) {
console.error('Working with child.stdin failed: ' + err.message);
});
// I want to execute following lines only if child process was spawned correctly
child.stdin.write('data');
child.stdin.end();
I'd need something like this
child.on('successful_spawn', function () {
child.stdin.write('data');
child.stdin.end();
});
From the node.js docs: http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
Example of checking for failed exec:
var spawn = require('child_process').spawn,
child = spawn('bad_command');
child.stderr.setEncoding('utf8');
child.stderr.on('data', function (data) {
if (/^execvp\(\)/.test(data)) {
console.log('Failed to start child process.');
}
});
Have a look at core-worker:
https://www.npmjs.com/package/core-worker
This package makes it a lot easier to handle processes.
I think what you want to do is something like that (from the docs):
import { process } from "core-worker";
const simpleChat = process("node chat.js", "Chat ready");
setTimeout(() => simpleChat.kill(), 360000); // wait an hour and close the chat
simpleChat.ready(500)
.then(console.log.bind(console, "You are now able to send messages."))
.then(::simpleChat.death)
.then(console.log.bind(console, "Chat closed"))
.catch(() => /* handle err */);
So if the process is not started correctly, none of the .then statements are executed which is exactly what you want to do, right?

Resources