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 | ;
|
19 |
|
20 | const childProcess = require('child_process');
|
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.Stream|null|undefined>|
|
36 | * undefined)
|
37 | * }}
|
38 | */
|
39 | var Options;
|
40 |
|
41 |
|
42 | /**
|
43 | * Describes a command's termination conditions.
|
44 | */
|
45 | class Result {
|
46 | /**
|
47 | * @param {?number} code The exit code, or {@code null} if the command did not
|
48 | * exit normally.
|
49 | * @param {?string} signal The signal used to kill the command, or
|
50 | * {@code null}.
|
51 | */
|
52 | constructor(code, signal) {
|
53 | /** @type {?number} */
|
54 | this.code = code;
|
55 |
|
56 | /** @type {?string} */
|
57 | this.signal = signal;
|
58 | }
|
59 |
|
60 | /** @override */
|
61 | toString() {
|
62 | return `Result(code=${this.code}, signal=${this.signal})`;
|
63 | }
|
64 | }
|
65 |
|
66 |
|
67 | const COMMAND_RESULT = /** !WeakMap<!Command, !Promise<!Result>> */new WeakMap;
|
68 | const KILL_HOOK = /** !WeakMap<!Command, function(string)> */new WeakMap;
|
69 |
|
70 | /**
|
71 | * Represents a command running in a sub-process.
|
72 | */
|
73 | class Command {
|
74 | /**
|
75 | * @param {!Promise<!Result>} result The command result.
|
76 | * @param {function(string)} onKill The function to call when {@link #kill()}
|
77 | * is called.
|
78 | */
|
79 | constructor(result, onKill) {
|
80 | COMMAND_RESULT.set(this, result);
|
81 | KILL_HOOK.set(this, onKill);
|
82 | }
|
83 |
|
84 | /**
|
85 | * @return {!Promise<!Result>} A promise for the result of this
|
86 | * command.
|
87 | */
|
88 | result() {
|
89 | return /** @type {!Promise<!Result>} */(COMMAND_RESULT.get(this));
|
90 | }
|
91 |
|
92 | /**
|
93 | * Sends a signal to the underlying process.
|
94 | * @param {string=} opt_signal The signal to send; defaults to `SIGTERM`.
|
95 | */
|
96 | kill(opt_signal) {
|
97 | KILL_HOOK.get(this)(opt_signal || 'SIGTERM');
|
98 | }
|
99 | }
|
100 |
|
101 |
|
102 | // PUBLIC API
|
103 |
|
104 |
|
105 | /**
|
106 | * Spawns a child process. The returned {@link Command} may be used to wait
|
107 | * for the process result or to send signals to the process.
|
108 | *
|
109 | * @param {string} command The executable to spawn.
|
110 | * @param {Options=} opt_options The command options.
|
111 | * @return {!Command} The launched command.
|
112 | */
|
113 | module.exports = function exec(command, opt_options) {
|
114 | var options = opt_options || {};
|
115 |
|
116 | var proc = childProcess.spawn(command, options.args || [], {
|
117 | env: options.env || process.env,
|
118 | stdio: options.stdio || 'ignore'
|
119 | });
|
120 |
|
121 | // This process should not wait on the spawned child, however, we do
|
122 | // want to ensure the child is killed when this process exits.
|
123 | proc.unref();
|
124 | process.once('exit', onProcessExit);
|
125 |
|
126 | let result = new Promise(resolve => {
|
127 | proc.once('exit', (code, signal) => {
|
128 | proc = null;
|
129 | process.removeListener('exit', onProcessExit);
|
130 | resolve(new Result(code, signal));
|
131 | });
|
132 | });
|
133 | return new Command(result, killCommand);
|
134 |
|
135 | function onProcessExit() {
|
136 | killCommand('SIGTERM');
|
137 | }
|
138 |
|
139 | function killCommand(signal) {
|
140 | process.removeListener('exit', onProcessExit);
|
141 | if (proc) {
|
142 | proc.kill(signal);
|
143 | proc = null;
|
144 | }
|
145 | }
|
146 | };
|
147 |
|
148 | // Exported to improve generated API documentation.
|
149 |
|
150 | module.exports.Command = Command;
|
151 | /** @typedef {!Options} */
|
152 | module.exports.Options = Options;
|
153 | module.exports.Result = Result;
|