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