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 * <ul>
26 * <li>
27 * <li>{@code args} - Command line arguments.
28 * <li>{@code env} - Command environment; will inherit from the current process
29 * if missing.
30 * <li>{@code stdio} - IO configuration for the spawned server process. For
31 * more information, refer to the documentation of
32 * {@code child_process.spawn}.
33 * </ul>
34 *
35 * @typedef {{
36 * args: (!Array.<string>|undefined),
37 * env: (!Object.<string, string>|undefined),
38 * stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
39 * }}
40 */
41var Options;
42
43
44/**
45 * Describes a command's termination conditions.
46 * @param {?number} code The exit code, or {@code null} if the command did not
47 * exit normally.
48 * @param {?string} signal The signal used to kill the command, or
49 * {@code null}.
50 * @constructor
51 */
52var Result = function(code, signal) {
53 /** @type {?number} */
54 this.code = code;
55
56 /** @type {?string} */
57 this.signal = signal;
58};
59
60
61/** @override */
62Result.prototype.toString = function() {
63 return 'Result(code=' + this.code + ', signal=' + this.signal + ')';
64};
65
66
67
68/**
69 * Represents a command running in a sub-process.
70 * @param {!promise.Promise.<!Result>} result The command result.
71 * @constructor
72 */
73var Command = function(result, onKill) {
74 /** @return {boolean} Whether this command is still running. */
75 this.isRunning = function() {
76 return result.isPending();
77 };
78
79 /**
80 * @return {!promise.Promise.<!Result>} A promise for the result of this
81 * command.
82 */
83 this.result = function() {
84 return result;
85 };
86
87 /**
88 * Sends a signal to the underlying process.
89 * @param {string=} opt_signal The signal to send; defaults to
90 * {@code SIGTERM}.
91 */
92 this.kill = function(opt_signal) {
93 onKill(opt_signal || 'SIGTERM');
94 };
95};
96
97
98// PUBLIC API
99
100
101/**
102 * Spawns a child process. The returned {@link Command} may be used to wait
103 * for the process result or to send signals to the process.
104 *
105 * @param {string} command The executable to spawn.
106 * @param {Options=} opt_options The command options.
107 * @return {!Command} The launched command.
108 */
109module.exports = function(command, opt_options) {
110 var options = opt_options || {};
111
112 var proc = childProcess.spawn(command, options.args || [], {
113 env: options.env || process.env,
114 stdio: options.stdio || 'ignore'
115 }).once('exit', onExit);
116
117 // This process should not wait on the spawned child, however, we do
118 // want to ensure the child is killed when this process exits.
119 proc.unref();
120 process.once('exit', killCommand);
121
122 var result = promise.defer();
123 var cmd = new Command(result.promise, function(signal) {
124 if (!result.isPending() || !proc) {
125 return; // No longer running.
126 }
127 proc.kill(signal);
128 });
129 return cmd;
130
131 function onExit(code, signal) {
132 proc = null;
133 process.removeListener('exit', killCommand);
134 result.fulfill(new Result(code, signal));
135 }
136
137 function killCommand() {
138 process.removeListener('exit', killCommand);
139 proc && proc.kill('SIGTERM');
140 }
141};