aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/util/CommandFailureUtils.java
blob: 9178f985b7f656a8be4c6c5df00b3cac99e018ee (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
// Copyright 2014 Google Inc. 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.util;

import com.google.common.base.Preconditions;
import com.google.common.collect.Ordering;

import java.io.File;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Utility methods for describing command failures.
 * See also the CommandUtils class.
 * Unlike that one, this class does not depend on Command;
 * instead, it just manipulates command lines represented as
 * Collection<String>.
 */
public class CommandFailureUtils {

  // Interface that provides building blocks when describing command.
  private interface DescribeCommandImpl {
    void describeCommandBeginIsolate(StringBuilder message);
    void describeCommandEndIsolate(StringBuilder message);
    void describeCommandCwd(String cwd, StringBuilder message);
    void describeCommandEnvPrefix(StringBuilder message);
    void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry);
    void describeCommandElement(StringBuilder message, String commandElement);
    void describeCommandExec(StringBuilder message);
  }

  private static final class LinuxDescribeCommandImpl implements DescribeCommandImpl {

    @Override
    public void describeCommandBeginIsolate(StringBuilder message) {
      message.append("(");
    }

    @Override
    public void describeCommandEndIsolate(StringBuilder message) {
      message.append(")");
    }

    @Override
    public void describeCommandCwd(String cwd, StringBuilder message) {
      message.append("cd ").append(ShellEscaper.escapeString(cwd)).append(" && \\\n  ");
    }

    @Override
    public void describeCommandEnvPrefix(StringBuilder message) {
      message.append("env - \\\n  ");
    }

    @Override
    public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) {
      message.append(ShellEscaper.escapeString(entry.getKey())).append('=')
          .append(ShellEscaper.escapeString(entry.getValue())).append(" \\\n  ");
    }

    @Override
    public void describeCommandElement(StringBuilder message, String commandElement) {
      message.append(ShellEscaper.escapeString(commandElement));
    }

    @Override
    public void describeCommandExec(StringBuilder message) {
      message.append("exec ");
    }
  }

  // TODO(bazel-team): (2010) Add proper escaping. We can't use ShellUtils.shellEscape() as it is
  // incompatible with CMD.EXE syntax, but something else might be needed.
  private static final class WindowsDescribeCommandImpl implements DescribeCommandImpl {

    @Override
    public void describeCommandBeginIsolate(StringBuilder message) {
      // TODO(bazel-team): Implement this.
    }

    @Override
    public void describeCommandEndIsolate(StringBuilder message) {
      // TODO(bazel-team): Implement this.
    }

    @Override
    public void describeCommandCwd(String cwd, StringBuilder message) {
      message.append("cd ").append(cwd).append("\n");
    }

    @Override
    public void describeCommandEnvPrefix(StringBuilder message) { }

    @Override
    public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) {
      message.append("SET ").append(entry.getKey()).append('=')
          .append(entry.getValue()).append("\n  ");
    }

    @Override
    public void describeCommandElement(StringBuilder message, String commandElement) {
      message.append(commandElement);
    }

    @Override
    public void describeCommandExec(StringBuilder message) {
      // TODO(bazel-team): Implement this if possible for greater efficiency.
    }
  }

  private static final DescribeCommandImpl describeCommandImpl =
      OS.getCurrent() == OS.WINDOWS ? new WindowsDescribeCommandImpl()
                                    : new LinuxDescribeCommandImpl();

  private CommandFailureUtils() {} // Prevent instantiation.

  private static Comparator<Map.Entry<String, String>> mapEntryComparator =
      new Comparator<Map.Entry<String, String>>() {
        @Override
        public int compare(Map.Entry<String, String> x, Map.Entry<String, String> y) {
          // A map can never have two keys with the same value, so we only need to compare the keys.
          return x.getKey().compareTo(y.getKey());
        }
      };

  /**
   * Construct a string that describes the command.
   * Currently this returns a message of the form "foo bar baz",
   * with shell meta-characters appropriately quoted and/or escaped,
   * prefixed (if verbose is true) with an "env" command to set the environment.
   *
   * @param form Form of the command to generate; see the documentation of the
   * {@link CommandDescriptionForm} values.
   */
  public static String describeCommand(CommandDescriptionForm form,
      Collection<String> commandLineElements,
      @Nullable Map<String, String> environment, @Nullable String cwd) {
    Preconditions.checkNotNull(form);
    final int APPROXIMATE_MAXIMUM_MESSAGE_LENGTH = 200;
    StringBuilder message = new StringBuilder();
    int size = commandLineElements.size();
    int numberRemaining = size;
    if (form == CommandDescriptionForm.COMPLETE) {
      describeCommandImpl.describeCommandBeginIsolate(message);
    }
    if (form != CommandDescriptionForm.ABBREVIATED) {
      if (cwd != null) {
        describeCommandImpl.describeCommandCwd(cwd, message);
      }
      /*
       * On Linux, insert an "exec" keyword to save a fork in "blaze run"
       * generated scripts.  If we use "env" as a wrapper, the "exec" needs to
       * be applied to the entire "env" invocation.
       *
       * On Windows, this is a no-op.
       */
      describeCommandImpl.describeCommandExec(message);
      /*
       * Java does not provide any way to invoke a subprocess with the environment variables
       * in a specified order.  The order of environment variables in the 'environ' array
       * (which is set by the 'envp' parameter to the execve() system call)
       * is determined by the order of iteration on a HashMap constructed inside Java's
       * ProcessBuilder class (in the ProcessEnvironment class), which is nondeterministic.
       *
       * Nevertheless, we *print* the environment variables here in sorted order, rather
       * than in the potentially nondeterministic order that will be actually used.
       * This is slightly dubious... in theory a process's behaviour could depend on the order
       * of the environment variables passed to it.  (For example, the order of environment
       * variables in the environ array affects the output of '/usr/bin/env'.)
       * However, in practice very few processes depend on the order of the environment
       * variables, and using a deterministic sorted order here makes Blaze's output more
       * deterministic and easier to read.  So this seems the lesser of two evils... I think.
       * Anyway, it's not like we have much choice... even if we wanted to, there's no way to
       * print out the nondeterministic order that will actually be used, since there's
       * no way to guarantee that the iteration over entrySet() here will return the same
       * sequence as the iteration over entrySet() inside the ProcessBuilder class
       * (in ProcessEnvironment.StringEnvironment.toEnvironmentBlock()).
       */
      if (environment != null) {
        describeCommandImpl.describeCommandEnvPrefix(message);
        for (Map.Entry<String, String> entry :
            Ordering.from(mapEntryComparator).sortedCopy(environment.entrySet())) {
          message.append("  ");
          describeCommandImpl.describeCommandEnvVar(message, entry);
        }
      }
    }
    for (String commandElement : commandLineElements) {
      if (form == CommandDescriptionForm.ABBREVIATED &&
          message.length() + commandElement.length() > APPROXIMATE_MAXIMUM_MESSAGE_LENGTH) {
        message.append(
            " ... (remaining " + numberRemaining + " argument(s) skipped)");
        break;
      } else {
        if (numberRemaining < size) {
          message.append(' ');
        }
        describeCommandImpl.describeCommandElement(message, commandElement);
        numberRemaining--;
      }
    }
    if (form == CommandDescriptionForm.COMPLETE) {
      describeCommandImpl.describeCommandEndIsolate(message);
    }
    return message.toString();
  }

  /**
   * Construct an error message that describes a failed command invocation.
   * Currently this returns a message of the form "error executing command foo
   * bar baz".
   */
  public static String describeCommandError(boolean verbose,
                                            Collection<String> commandLineElements,
                                            Map<String, String> env, String cwd) {
    CommandDescriptionForm form = verbose
        ? CommandDescriptionForm.COMPLETE
        : CommandDescriptionForm.ABBREVIATED;
    return "error executing command " + (verbose ? "\n  " : "")
        + describeCommand(form, commandLineElements, env, cwd);
  }

  /**
   * Construct an error message that describes a failed command invocation.
   * Currently this returns a message of the form "foo failed: error executing
   * command /dir/foo bar baz".
   */
  public static String describeCommandFailure(boolean verbose,
                                              Collection<String> commandLineElements,
                                              Map<String, String> env, String cwd) {
    String commandName = commandLineElements.iterator().next();
    // Extract the part of the command name after the last "/", if any.
    String shortCommandName = new File(commandName).getName();
    return shortCommandName + " failed: " +
        describeCommandError(verbose, commandLineElements, env, cwd);
  }

}