aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/shell/Command.java
blob: 15842751840c35d98e800172230bab3cedbeef61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.build.lib.shell;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.shell.Consumers.OutErrConsumers;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
 * An executable command, including its arguments and runtime environment (environment variables,
 * working directory). It lets a caller execute a command, get its results, and optionally forward
 * interrupts to the subprocess. This class creates threads to ensure timely reading of subprocess
 * outputs.
 *
 * <p>This class is immutable and thread-safe.
 *
 * <p>The use of "shell" in the package name of this class is a misnomer.  In terms of the way its
 * arguments are interpreted, this class is closer to {@code execve(2)} than to {@code system(3)}.
 * No shell is executed.
 *
 * <h4>Examples</h4>
 *
 * <p>The most basic use-case for this class is as follows:
 * <pre>
 *   String[] args = { "/bin/du", "-s", directory };
 *   BlazeCommandResult result = new Command(args).execute();
 *   String output = new String(result.getStdout());
 * </pre>
 * which writes the output of the {@code du(1)} command into {@code output}. More complex cases
 * might inspect the stderr stream, kill the subprocess asynchronously, feed input to its standard
 * input, handle the exceptions thrown if the command fails, or print the termination status (exit
 * code or signal name).
 *
 * <h4>Other Features</h4>
 *
 * <p>A caller can optionally specify bytes to be written to the process's "stdin". The returned
 * {@link CommandResult} object gives the caller access to the exit status, as well as output from
 * "stdout" and "stderr". To use this class with processes that generate very large amounts of
 * input/output, consider {@link #execute(OutputStream, OutputStream)},
 * {@link #executeAsync(OutputStream, OutputStream)}, or
 * {@link #executeAsync(InputStream, OutputStream, OutputStream, boolean)}.
 *
 * <p>This class ensures that stdout and stderr streams are read promptly, avoiding potential
 * deadlock if the output is large. See
 * <a href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html"> when
 * <code>Runtime.exec()</code> won't</a>.
 *
 * <h4>Caution: Invoking Shell Commands</h4>
 *
 * <p>Perhaps the most common command invoked programmatically is the UNIX shell, {@code /bin/sh}.
 * Because the shell is a general-purpose programming language, care must be taken to ensure that
 * variable parts of the shell command (e.g. strings entered by the user) do not contain shell
 * metacharacters, as this poses a correctness and/or security risk.
 *
 * <p>To execute a shell command directly, use the following pattern:
 * <pre>
 *   String[] args = { "/bin/sh", "-c", shellCommand };
 *   BlazeCommandResult result = new Command(args).execute();
 * </pre>
 * {@code shellCommand} is a complete Bourne shell program, possibly containing all kinds of
 * unescaped metacharacters.  For example, here's a shell command that enumerates the working
 * directories of all processes named "foo":
 * <pre>ps auxx | grep foo | awk '{print $1}' |
 *      while read pid; do readlink /proc/$pid/cwd; done</pre>
 * It is the responsibility of the caller to ensure that this string means what they intend.
 *
 * <p>Consider the risk posed by allowing the "foo" part of the previous command to be some
 * arbitrary (untrusted) string called {@code processName}:
 * <pre>
 *  // WARNING: unsafe!
 *  String shellCommand = "ps auxx | grep " + processName + " | awk '{print $1}' | "
 *  + "while read pid; do readlink /proc/$pid/cwd; done";</pre>
 * </pre>
 * Passing this string to {@link Command} is unsafe because if the string {@processName} contains
 * shell metacharacters, the meaning of the command can be arbitrarily changed; consider:
 * <pre>String processName = ". ; rm -fr $HOME & ";</pre>
 *
 * <p>To defend against this possibility, it is essential to properly quote the variable portions of
 * the shell command so that shell metacharacters are escaped.  Use {@link ShellUtils#shellEscape}
 * for this purpose:
 * <pre>
 *  // Safe.
 *  String shellCommand = "ps auxx | grep " + ShellUtils.shellEscape(processName)
 *      + " | awk '{print $1}' | while read pid; do readlink /proc/$pid/cwd; done";
 * </pre>
 *
 * <p>Tip: if you are only invoking a single known command, and no shell features (e.g. $PATH
 * lookup, output redirection, pipelines, etc) are needed, call it directly without using a shell,
 * as in the {@code du(1)} example above.
 */
public final class Command {

  private static final Logger logger =
      Logger.getLogger("com.google.devtools.build.lib.shell.Command");

  /** Pass this value to {@link #execute} to indicate that no input should be written to stdin. */
  public static final InputStream NO_INPUT = new NullInputStream();

  public static final boolean KILL_SUBPROCESS_ON_INTERRUPT = true;
  public static final boolean CONTINUE_SUBPROCESS_ON_INTERRUPT = false;

  private final SubprocessBuilder subprocessBuilder;

  /**
   * Creates a new {@link Command} for the given command line. The environment is inherited from the
   * current process, as is the working directory. No timeout is enforced. The command line is
   * executed exactly as given, without a shell. Subsequent calls to {@link #execute()} will use the
   * JVM's working directory and environment.
   *
   * @param commandLineElements elements of raw command line to execute
   * @throws IllegalArgumentException if commandLine is null or empty
   */
  public Command(String[] commandLineElements) {
    this(commandLineElements, null, null, Duration.ZERO);
  }

  /**
   * Just like {@link #Command(String[], Map, File, Duration)}, but without a timeout.
   */
  public Command(
      String[] commandLineElements,
      @Nullable Map<String, String> environmentVariables,
      @Nullable File workingDirectory) {
    this(commandLineElements, environmentVariables, workingDirectory, Duration.ZERO);
  }

  /**
   * Creates a new {@link Command} for the given command line elements. The command line is executed
   * without a shell.
   *
   * <p>The given environment variables and working directory are used in subsequent calls to
   * {@link #execute()}.
   *
   * <p>This command treats the  0-th element of {@code commandLineElement} (the name of an
   * executable to run) specially.
   * <ul>
   *  <li>If it is an absolute path, it is used as it</li>
   *  <li>If it is a single file name, the PATH lookup is performed</li>
   *  <li>If it is a relative path that is not a single file name, the command will attempt to
   *       execute the the binary at that path relative to {@code workingDirectory}.</li>
   * </ul>
   *
   * @param commandLineElements elements of raw command line to execute
   * @param environmentVariables environment variables to replace JVM's environment variables; may
   *    be null
   * @param workingDirectory working directory for execution; if null, the VM's current working
   *    directory is used
   * @param timeout timeout; a value less than or equal to 0 is treated as no timeout
   * @throws IllegalArgumentException if commandLine is null or empty
   */
  // TODO(ulfjack): Throw a special exception if there was a timeout.
  public Command(
      String[] commandLineElements,
      @Nullable Map<String, String> environmentVariables,
      @Nullable File workingDirectory,
      Duration timeout) {
    Preconditions.checkNotNull(commandLineElements);
    Preconditions.checkArgument(
        commandLineElements.length != 0, "cannot run an empty command line");

    File executable = new File(commandLineElements[0]);
    if (!executable.isAbsolute() && executable.getParent() != null) {
      commandLineElements = commandLineElements.clone();
      commandLineElements[0] = new File(workingDirectory, commandLineElements[0]).getAbsolutePath();
    }

    this.subprocessBuilder = new SubprocessBuilder();
    subprocessBuilder.setArgv(ImmutableList.copyOf(commandLineElements));
    subprocessBuilder.setEnv(environmentVariables);
    subprocessBuilder.setWorkingDirectory(workingDirectory);
    subprocessBuilder.setTimeoutMillis(timeout.toMillis());
  }

  /** Returns the raw command line elements to be executed */
  public String[] getCommandLineElements() {
    final List<String> elements = subprocessBuilder.getArgv();
    return elements.toArray(new String[elements.size()]);
  }

  /** Returns an (unmodifiable) {@link Map} view of command's environment variables or null. */
  @Nullable public Map<String, String> getEnvironmentVariables() {
    return subprocessBuilder.getEnv();
  }

  /** Returns the working directory to be used for execution, or null. */
  @Nullable public File getWorkingDirectory() {
    return subprocessBuilder.getWorkingDirectory();
  }

  /**
   * Execute this command with no input to stdin, and with the output captured in memory. If the
   * current process is interrupted, then the subprocess is also interrupted. This call blocks until
   * the subprocess completes or an error occurs.
   *
   * <p>This method is a convenience wrapper for <code>executeAsync().get()</code>.
   *
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public CommandResult execute() throws CommandException {
    return executeAsync().get();
  }

  /**
   * Execute this command with no input to stdin, and with the output streamed to the given output
   * streams, which must be thread-safe. If the current process is interrupted, then the subprocess
   * is also interrupted. This call blocks until the subprocess completes or an error occurs.
   *
   * <p>Note that the given output streams are never closed by this class.
   *
   * <p>This method is a convenience wrapper for <code>executeAsync(stdOut, stdErr).get()</code>.
   *
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public CommandResult execute(OutputStream stdOut, OutputStream stdErr) throws CommandException {
    return doExecute(
        NO_INPUT, Consumers.createStreamingConsumers(stdOut, stdErr), KILL_SUBPROCESS_ON_INTERRUPT)
            .get();
  }

  /**
   * Execute this command with no input to stdin, and with the output captured in memory. If the
   * current process is interrupted, then the subprocess is also interrupted. This call blocks until
   * the subprocess is started or throws an error if that fails, but does not wait for the
   * subprocess to exit.
   *
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public FutureCommandResult executeAsync() throws CommandException {
    return doExecute(
        NO_INPUT, Consumers.createAccumulatingConsumers(), KILL_SUBPROCESS_ON_INTERRUPT);
  }

  /**
   * Execute this command with no input to stdin, and with the output streamed to the given output
   * streams, which must be thread-safe. If the current process is interrupted, then the subprocess
   * is also interrupted. This call blocks until the subprocess is started or throws an error if
   * that fails, but does not wait for the subprocess to exit.
   *
   * <p>Note that the given output streams are never closed by this class.
   *
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public FutureCommandResult executeAsync(OutputStream stdOut, OutputStream stdErr)
      throws CommandException {
    return doExecute(
        NO_INPUT, Consumers.createStreamingConsumers(stdOut, stdErr), KILL_SUBPROCESS_ON_INTERRUPT);
  }

  /**
   * Execute this command with no input to stdin, and with the output captured in memory. This call
   * blocks until the subprocess is started or throws an error if that fails, but does not wait for
   * the subprocess to exit.
   *
   * @param killSubprocessOnInterrupt whether the subprocess should be killed if the current process
   *     is interrupted
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public FutureCommandResult executeAsync(
      InputStream stdinInput, boolean killSubprocessOnInterrupt) throws CommandException {
    return doExecute(
        stdinInput, Consumers.createAccumulatingConsumers(), killSubprocessOnInterrupt);
  }

  /**
   * Execute this command with no input to stdin, and with the output streamed to the given output
   * streams, which must be thread-safe. This call blocks until the subprocess is started or throws
   * an error if that fails, but does not wait for the subprocess to exit.
   *
   * <p>Note that the given output streams are never closed by this class.
   *
   * @param killSubprocessOnInterrupt whether the subprocess should be killed if the current process
   *     is interrupted
   * @return {@link CommandResult} representing result of the execution
   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any reason
   * @throws AbnormalTerminationException if an {@link IOException} is encountered while reading
   *    from the process, or the process was terminated due to a signal
   * @throws BadExitStatusException if the process exits with a non-zero status
   */
  public FutureCommandResult executeAsync(
      InputStream stdinInput,
      OutputStream stdOut,
      OutputStream stdErr,
      boolean killSubprocessOnInterrupt)
          throws CommandException {
    return doExecute(
        stdinInput, Consumers.createStreamingConsumers(stdOut, stdErr), killSubprocessOnInterrupt);
  }

  /**
   * A string representation of this command object which includes the arguments, the environment,
   * and the working directory. Avoid relying on the specifics of this format. Note that the size of
   * the result string will reflect the size of the command.
   */
  public String toDebugString() {
    StringBuilder message = new StringBuilder(128);
    message.append("Executing (without brackets):");
    for (String arg : subprocessBuilder.getArgv()) {
      message.append(" [");
      message.append(arg);
      message.append(']');
    }
    message.append("; environment: ");
    message.append(subprocessBuilder.getEnv());
    message.append("; working dir: ");
    File workingDirectory = subprocessBuilder.getWorkingDirectory();
    message.append(workingDirectory == null ?
                   "(current)" :
                   workingDirectory.toString());
    return message.toString();
  }

  private FutureCommandResult doExecute(
      InputStream stdinInput, OutErrConsumers outErrConsumers, boolean killSubprocessOnInterrupt) 
          throws ExecFailedException {
    Preconditions.checkNotNull(stdinInput, "stdinInput");
    logCommand();

    Subprocess process = startProcess();

    outErrConsumers.logConsumptionStrategy();
    outErrConsumers.registerInputs(
        process.getInputStream(), process.getErrorStream(), /* closeStreams= */ false);

    // TODO(ulfjack): This call blocks until all input is written. If stdinInput is large (or
    // unbounded), then the async calls can block for a long time, and the timeout is not properly
    // enforced.
    processInput(stdinInput, process);

    return new FutureCommandResultImpl(this, process, outErrConsumers, killSubprocessOnInterrupt);
  }

  private Subprocess startProcess() throws ExecFailedException {
    try {
      return subprocessBuilder.start();
    } catch (IOException ioe) {
      throw new ExecFailedException(this, ioe);
    }
  }

  private static class NullInputStream extends InputStream {
    @Override
    public int read() {
      return -1;
    }

    @Override
    public int available() {
      return 0;
    }
  }

  private static void processInput(InputStream stdinInput, Subprocess process) {
    if (logger.isLoggable(Level.FINER)) {
      logger.finer(stdinInput.toString());
    }
    try (OutputStream out = process.getOutputStream()) {
      ByteStreams.copy(stdinInput, out);
    } catch (IOException ioe) {
      // Note: this is not an error!  Perhaps the command just isn't hungry for our input and exited
      // with success. Process.waitFor (later) will tell us.
      //
      // (Unlike out/err streams, which are read asynchronously, the input stream is written
      // synchronously, in its entirety, before processInput returns.  If the input is infinite, and
      // is passed through e.g. "cat" subprocess and back into the ByteArrayOutputStream, that will
      // eventually run out of memory, causing the output stream to be closed, "cat" to terminate
      // with SIGPIPE, and processInput to receive an IOException.
    }
  }

  private void logCommand() {
    if (!logger.isLoggable(Level.FINE)) {
      return;
    }
    logger.fine(toDebugString());
  }
}