Node is designed to handle I/O operations efficiently, but you should know that some types of programs are not suitable for this mode. For example, if you plan to use Node to handle a CPU-intensive task, you may block the event loop and thus reduce the program's response. An alternative is to assign CPU-intensive tasks to a separate process to handle, thus freeing up the event loop. Node allows you to spawn a process and use this new process as a child of its parent process. In Node, the child process can communicate in two-way with the parent process, and to some extent, the parent process can also monitor and manage the child process.
Another case where you need to use a child process is when you want to simply execute an external command and let Node get the return value of the command. For example, you can execute a UNIX command, script, or other commands that cannot be directly executed in Node.
This chapter will show you how to execute external commands, create, and communicate with children, and terminate children. The point is to give you an idea of how to complete a series of tasks outside the Node process. Execute external commands When you need to execute an external shell command or executable, you can use the child_process module to import it like this: The code copy is as follows: var child_process = require('child_process') Then you can use the exec function in the module to execute external commands: The code copy is as follows: var exec = child_process.exec; exec(command, callback); The first parameter of exec is the shell command string you are preparing to execute, and the second parameter is a callback function. This callback function will be called when exec has finished executing external commands or an error occurs. The callback function has three parameters: error, stdout, stderr, see the following example: The code copy is as follows: exec('ls',function(err,stdout,stderr){ //Translator's note: If you use Windows, you can change it to Windows command, such as Dir, and I will not repeat it later. }); If an error occurs, the first parameter will be an instance of the Error class. If the first parameter does not contain an error, the second parameter stdout will contain the standard output of the command. The last parameter contains command-related error output. Listing 8-1 shows a more complex example of executing external commands LISTING 8-1: Execute external commands (source code: chapter8/01_external_command.js) The code copy is as follows: //Import the exec function of the child_process module var exec = require('child_process').exec; //Calling the "cat *.js | wc -l" command exec('cat *.js | wc l ', function(err, stdout, stderr ){ //Line 4 //The command exits or the call fails if( err ){ //Failed to start an external process console.log('child_process Exit, the error code is: ',err.code); return; } } In the fourth line, we pass "cat *.js | wc -l" as the first parameter to exec. You can also try any other command, as long as the command you have used in the shell is OK. Then take a callback function as the second parameter, which will be called when an error occurs or the child process ends. You can also pass a third optional parameter before the callback function, which contains some configuration options, such as: The code copy is as follows: var exec = require('child_process').exec; var options ={ timeout: 1000, killSignal: 'SIGKILL' }; exec('cat *.js | wc l ', options, function(err,stdout,stderr){ //… }); The parameters that can be used are: 1.cwd - the current directory, you can specify the current working directory. 2. encoding -- The encoding format of the child process output content, the default value is "utf8", that is, UTF-8 encoding. If the output of the child process is not utf8, you can use this parameter to set it. The supported encoding formats are: The code copy is as follows: ascii utf8 ucs2 base64 If you want to know more about these encoding formats supported by Node, please refer to Chapter 4 "Using Buffer to Process, Encoding, and Decoding Binary Data". 1.timeout - The command execution timeout in milliseconds, the default is 0, that is, there is no limit, and wait until the child process ends. 2.maxBuffer - Specifies the maximum number of bytes allowed by the stdout stream and stderr stream. If the maximum value is reached, the child process will be killed. The default value is 200*1024. 3. killSignal - The end signal sent to the child process when the timeout or the output cache reaches the maximum value. The default value is "SIGTERM", which will send a termination signal to the child process. This is usually used to end the process in an orderly manner. When using SIGTERM signals, the process can also process or rewrite the default behavior of the signal processor after receiving it. If the target process needs it, you can pass other signals to it at the same time (such as SIGUSR1). You can also choose to send a SIGKILL signal, which will be processed by the operating system and force the child process to be terminated immediately, so that any cleanup operation of the child process will not be performed. If you want to further control the end of the process, you can use the child_process.spawn command, which will be introduced later. 1.evn - Specifies the environment variable passed to the child process. The default is null, which means that the child process will inherit the environment variables of all parent processes before it is created. Note: Using the killSignal option, you can send signals to the target process as a string. In Node, the signal exists as a string. Here is a list of UNIX signals and corresponding default operations: You might want to provide a set of extensible parent environment variables for the child process. If you directly modify the process.env object, you will change the environment variables of all modules in the Node process, which will cause a lot of trouble. The alternative is to create a new object and copy all parameters in process.env, see Example 8-2: LISTING 8-2: Use parameterized environment variables to execute commands (source code: chapter8/02_env_vars_augment.js) The code copy is as follows: var env = process.env, varName, envCopy = {}, exec = require('child_process').exec; //Copy process.env to envCopy for( vaName in ev){ envCopy[varName] = env[varName]; } //Set some custom variables envCopy['CUSTOM ENV VAR1'] = 'some value'; envCopy['CUSTOM ENV VAR2'] = 'some other value'; //Execute commands using process.env and custom variables exec('ls la',{env: envCopy}, function(err,stdout,stderr){ if(err){ throw err; } console.log('stdout:', stdout); console.log('stderr:',stderr); } In the above example, an envCopy variable is created to save environment variables. It first copies the environment variables of the Node process from process.env, then adds or replaces some environment variables that need to be modified, and finally passes envCopy as an environment variable parameter to the exec function and executes external commands. Remember that environment variables are passed between processes through the operating system, and all types of environment variable values arrive at the child process as strings. For example, if the parent process takes the number 123 as an environment variable, the child process will receive "123" as a string. In the following example, two Node scripts will be created in the same directory: parent.js and child.js. The first script will call the second one. Let's create these two files: LISTING 8-3: Parent process sets environment variables (chapter8/03_environment_number_parent.js) The code copy is as follows: var exec = require('child_process').exec; exec('node child.js', {env: {number: 123}}, function(err, stdout, stderr) { if (err) { throw err; } console.log('stdout:/n', stdout); console.log('stderr:/n', stderr); }); Save this code to parent.js. The following is the source code of the child process and save them to child.js (see Example 8-4) Example 8-4: Subprocess parses environment variables (chapter8/04_environment_number_child.js) The code copy is as follows: var number = process.env.number; console.log(typeof(number)); // → "string" number = parseInt(number, 10); console.log(typeof(number)); // → "number" After you save this file as child.js, you can run the following command in this directory: The code copy is as follows: $ node parent.js You will see the following output: The code copy is as follows: sdtou: string number stderr: As you can see, although the parent process passes a numeric environment variable, the child process receives it as a string (see the second line of output), and in the third line you parse the string into a number. Generate child process As you can see, you can use the child_process.exec() function to start an external process and call your callback function at the end of the process. This is very simple to use, but there are some disadvantages: 1. In addition to using command line parameters and environment variables, exec() cannot communicate with child processes. 2. The output of the child process is cached, so you can't stream it, it may run out of memory Fortunately, Node's child_process module allows finer granularity to control the start, stop, and other conventional operations of child processes. You can start a new child process in the application. Node provides a two-way communication channel, allowing the parent and child processes to send and receive string data from each other. The parent process can also have some management operations for the child process, send signals to the child process, and forcefully close the child process. Create a child process You can use the child_process.spawn function to create a new child process, see Example 8-5: Example 8-5: Generate child process. (chapter8/05_spawning_child.js) The code copy is as follows: // Import the spawn function of the child_process module var spawn = require('child_process').spawn; // Generate child processes used to execute the "tail -f /var/log/system.log" command var child = spawn('tail', ['-f', '/var/log/system.log']); The above code generates a child process used to execute tail commands and takes "-f" and "/bar/log/system.log" as parameters. The tail command will monitor the /var/log/system.og file (if present), and then output all appended new data to the stdout standard output stream. The spawn function returns a ChildProcess object, which is a pointer object, encapsulating the access interface of the real process. In this example, we assign this new descriptor to a variable called child. Listen to data from child processes Any child process handle containing the stdout attribute will take the standard output of the child process as a stream object. You can bind the data event on this stream object, so that whenever a data block is available, the corresponding callback function will be called, see the following example: The code copy is as follows: //Print the output of the child process to the console child.stdout.on('data',function(data){ console.log('tail output: ' + data); }); Whenever the child process outputs data to the standard output stdout, the parent process is notified and prints the data to the console. In addition to standard output, the process has another default output stream: the standard error stream, which is usually used to output error information. In this example, if the /var/log/system.log file does not exist, the tail process will output a message similar to the following: "/var/log/system.log: No such file or directory". By listening to the stderr stream, the parent process will be notified when this error occurs. The parent process can listen to standard error streams like this: The code copy is as follows: child.stderr.on('data', function(data) { console.log('tail error output:', data); }); The stderr property, like stdout, is also a read-only stream. Whenever a child process outputs data into the standard error stream, the parent process will be notified and output data. Send data to child process In addition to receiving data from the output stream of the child process, the parent process can also write data into the standard input of the child process through the childPoces.stdin property to send data to the child process. The child process can listen to the standard input data through the process.stdin read-only stream, but be careful that you must first resume the standard input stream, because it is in a paused state by default. Example 8-6 will create a program containing the following functions: 1.+1 Application: A simple application that can receive integers from standard input, then add them, and then output the result after addition to the standard output stream. As a simple computing service, this application simulates the Node process as an external service that can perform specific tasks. 2. Test the client of +1 application, send a random integer, and then output the result. Used to demonstrate how the Node process generates a child process and then lets it perform specific tasks. Use the following code in Example 8-6 to create a file named plus_one.js: Example 8-6: +1 Application (chapter8/06_plus_one.js) The code copy is as follows: // Restore the standard input stream that is paused by default process.stdin.resume(); process.stdin.on('data', function(data) { var number; try { // parse the input data into an integer number = parseInt(data.toString(), 10); // +1 number += 1; // Output result process.stdout.write(number + "/n"); } catch(err) { process.stderr.write(err.message + "/n"); } }); In the above code, we wait for data from the stdin standard input stream. Whenever data is available, we assume it is an integer and parse it into an integer variable, then add 1, and output the result to the standard output stream. You can run this program through the following command: The code copy is as follows: $ node plus_one.js After running, the program starts waiting for input. If you enter an integer and press Enter, you will see a number after being added 1 to be displayed on the screen. You can exit the program by pressing Ctrl-C. A test client Now you want to create a Node process to use the computing services provided by the previous "+1 application". First create a file named plus_one_test.js, see Example 8-7: Example 8-7: Test +1 application (chapter8/07_plus_one_test.js) The code copy is as follows: var spawn = require('child_process').spawn; // Generate a child process to execute +1 application var child = spawn('node', ['plus_one.js']); // Call the function every second setInterval(function() { // Create a random number smaller than 10.000 var number = Math.floor(Math.random() * 10000); // Send that number to the child process: child.stdin.write(number + "/n"); // Get the response from the child process and print it: child.stdout.once('data', function(data) { console.log('child replied to ' + number + ' with: ' + data); }); }, 1000); child.stderr.on('data', function(data) { process.stdout.write(data); }); From the first line to the fourth line, a child process is started to run the "+1 application", and then the following operations are performed every second using the setInterval function: 1. Create a new random number less than 10000 2. Pass this number as a string to the child process 3. Wait for the child process to reply to a string 4. Because you want to receive only 1 number at a time, you need to use child.stdout.once instead of child.stdout.on. If the latter is used, a callback function of a data event will be registered every 1 second. Each registered callback function will be executed when the stdout of the child process receives data. In this way, you will find that the same calculation result will be output multiple times. This behavior is obviously wrong. Receive notifications when child process exits When the child process exits, the exit event will be fired. Example 8-8 shows how to listen to it: Example 8-8: Listen to the exit event of the child process (chapter8/09_listen_child_exit.js) The code copy is as follows: var spawn = require('child_process').spawn; // Generate child process to execute the "ls -la" command var child = spawn('ls', ['-la']); child.stdout.on('data', function(data) { console.log('data from child: ' + data); }); // When the child process exits: <strong>child.on('exit', function(code) { console.log('child process terminated with code ' + code); });</strong> In the last few lines of black code, the parent process uses the child process's exit event to listen for its exit event. When the event occurs, the console displays the corresponding output. The exit code of the child process will be passed to the callback function as the first parameter. Some programs use a non-0 exit code to represent a certain failure state. For example, if you try to execute the command "ls al click filename.txt", but the current directory does not have this file, you will get an exit code with a value of 1, see Example 8-9: Example 8-9: Obtain the exit code of the child process (chapter8/10_child_exit_code.js) The code copy is as follows: var spawn = require('child_process').spawn; // Generate child process and execute the "ls does_not_exist.txt" command var child = spawn('ls', ['does_not_exist.txt']); // When the child process exits child.on('exit', function(code) { console.log('child process terminated with code ' + code); }); In this example, the exit event triggers the callback function and passes the child process's exit code as the first parameter to it. If the child process exits abnormally due to being killed by a signal, the corresponding signal code will be passed to the callback function as a second parameter, as in Example 8-10: LISTING 8-10: Get the exit signal of the child process (chapter8/11_child_exit_signal.js) The code copy is as follows: var spawn = require('child_process').spawn; // Generate child process and run the "sleep 10" command var child = spawn('sleep', ['10']); setTimeout(function() { child.kill(); }, 1000); child.on('exit', function(code, signal) { if (code) { console.log('child process terminated with code ' + code); } else if (signal) { console.log('child process terminated because of signal ' + signal); } }); In this example, a child process is started to perform sleep 10 seconds, but a SIGKILL signal is sent to the child process before 10 seconds, which will result in the following output: The code copy is as follows: child process terminated because of signal SIGTERM Send a signal and kill the process In this section, you will learn how to use signals to manage subprocesses. Signals are an easy way for a parent process to communicate with children and even kill children. Different signal codes represent different meanings, and there are many signals, some of which are the most common ones used to kill processes. If a process receives a signal that it does not know how to handle, the program will be interrupted by exception. Some signals are processed by subprocesses, while others can only be processed by the operating system. Generally, you can use the child.kill method to send a signal to the child process, and send a SIGTERM signal by default: The code copy is as follows: var spawn = require('child_process').spawn; var child = spawn('sleep', ['10']); setTimeout(function() { child.kill(); }, 1000); You can also send a specific signal by passing in a string identifying the signal as the only parameter of the kill method: The code copy is as follows: child.kill('SIGUSR2'); It should be noted that although the name of this method is kill, the sent signal does not necessarily kill the child process. If the child processed the signal, the default signal behavior was overwritten. Subprocesses written in Node can rewrite the definition of the signal processor like the following: The code copy is as follows: process.on('SIGUSR2', function() { console.log('Got a SIGUSR2 signal'); }); Now, you have defined the SIGUSR2 signal processor. When your process receives the SIGUSR2 signal again, it will not be killed, but instead outputs the sentence "Got a SIGUSR2 signal". Using this mechanism, you can design a simple way to communicate with the child process or even command it. Although it is not as rich as using standard input, this method is much simpler. summary In this chapter, we learned to use the child_process.exec method to execute external commands. This way, we can pass parameters to the child process instead of using command line parameters, but instead define environment variables. I also learned how to generate child processes by calling the child_process.spawn method to call external commands. In this way, you can use input streams and output streams to communicate with child processes, or use signals to communicate with child processes and kill processes.