io/exec.js

1// Copyright 2014 Selenium committers
2// Copyright 2014 Software Freedom Conservancy
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16'use strict';
17
18var childProcess = require('child_process');
19
20var promise = require('..').promise;
21
22
23/**
24 * A hash with configuration options for an executed command.
25 *
26 * - `args` - Command line arguments.
27 * - `env` - Command environment; will inherit from the current process if
28 * missing.
29 * - `stdio` - IO configuration for the spawned server process. For more
30 * information, refer to the documentation of `child_process.spawn`.
31 *
32 * @typedef {{
33 * args: (!Array.<string>|undefined),
34 * env: (!Object.<string, string>|undefined),
35 * stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
36 * }}
37 */
38var Options;
39
40
41/**
42 * Describes a command's termination conditions.
43 * @param {?number} code The exit code, or {@code null} if the command did not
44 * exit normally.
45 * @param {?string} signal The signal used to kill the command, or
46 * {@code null}.
47 * @constructor
48 */
49var Result = function(code, signal) {
50 /** @type {?number} */
51 this.code = code;
52
53 /** @type {?string} */
54 this.signal = signal;
55};
56
57
58/** @override */
59Result.prototype.toString = function() {
60 return 'Result(code=' + this.code + ', signal=' + this.signal + ')';
61};
62
63
64
65/**
66 * Represents a command running in a sub-process.
67 * @param {!promise.Promise.<!Result>} result The command result.
68 * @constructor
69 */
70var Command = function(result, onKill) {
71 /** @return {boolean} Whether this command is still running. */
72 this.isRunning = function() {
73 return result.isPending();
74 };
75
76 /**
77 * @return {!promise.Promise.<!Result>} A promise for the result of this
78 * command.
79 */
80 this.result = function() {
81 return result;
82 };
83
84 /**
85 * Sends a signal to the underlying process.
86 * @param {string=} opt_signal The signal to send; defaults to
87 * {@code SIGTERM}.
88 */
89 this.kill = function(opt_signal) {
90 onKill(opt_signal || 'SIGTERM');
91 };
92};
93
94
95// PUBLIC API
96
97
98/**
99 * Spawns a child process. The returned {@link Command} may be used to wait
100 * for the process result or to send signals to the process.
101 *
102 * @param {string} command The executable to spawn.
103 * @param {Options=} opt_options The command options.
104 * @return {!Command} The launched command.
105 */
106module.exports = function(command, opt_options) {
107 var options = opt_options || {};
108
109 var proc = childProcess.spawn(command, options.args || [], {
110 env: options.env || process.env,
111 stdio: options.stdio || 'ignore'
112 }).once('exit', onExit);
113
114 // This process should not wait on the spawned child, however, we do
115 // want to ensure the child is killed when this process exits.
116 proc.unref();
117 process.once('exit', killCommand);
118
119 var result = promise.defer();
120 var cmd = new Command(result.promise, function(signal) {
121 if (!result.isPending() || !proc) {
122 return; // No longer running.
123 }
124 proc.kill(signal);
125 });
126 return cmd;
127
128 function onExit(code, signal) {
129 proc = null;
130 process.removeListener('exit', killCommand);
131 result.fulfill(new Result(code, signal));
132 }
133
134 function killCommand() {
135 process.removeListener('exit', killCommand);
136 proc && proc.kill('SIGTERM');
137 }
138};