io/exec.js

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