blob: 1c0ad1f95e94b11794d036f3df2c2c1ea0beaa92 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.runtime;
15
16import com.google.common.base.Splitter;
Googler2f95be32015-03-20 17:26:11 +000017import com.google.common.collect.ImmutableList;
Florian Weikert90670382015-11-11 14:48:41 +000018import com.google.common.collect.ImmutableSet;
Laurent Le Brun54733f92015-09-03 13:59:44 +000019import com.google.common.collect.Iterators;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.devtools.build.lib.events.Event;
Googler2f95be32015-03-20 17:26:11 +000021import com.google.devtools.build.lib.events.EventKind;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010022import com.google.devtools.build.lib.util.Pair;
23import com.google.devtools.build.lib.util.io.AnsiTerminal;
Klaus Aehliga49f0252016-03-02 15:48:40 +000024import com.google.devtools.build.lib.util.io.AnsiTerminalWriter;
25import com.google.devtools.build.lib.util.io.LineCountingAnsiTerminalWriter;
26import com.google.devtools.build.lib.util.io.LineWrappingAnsiTerminalWriter;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010027import com.google.devtools.build.lib.util.io.OutErr;
28
Googler2f95be32015-03-20 17:26:11 +000029import org.joda.time.Duration;
30import org.joda.time.Instant;
31
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010032import java.io.IOException;
Googler2f95be32015-03-20 17:26:11 +000033import java.util.Calendar;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010034import java.util.Iterator;
Googler2f95be32015-03-20 17:26:11 +000035import java.util.List;
Florian Weikert90670382015-11-11 14:48:41 +000036import java.util.Set;
Googler2f95be32015-03-20 17:26:11 +000037import java.util.concurrent.ThreadLocalRandom;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010038import java.util.logging.Logger;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41
42/**
43 * An event handler for ANSI terminals which uses control characters to
44 * provide eye-candy, reduce scrolling, and generally improve usability
45 * for users running directly from the shell.
46 *
47 * <p/>
48 * This event handler differs from a normal terminal because it only adds
49 * control characters to stderr, not stdout. All blaze status feedback
50 * is sent to stderr, so adding control characters just to that stream gives
51 * the benefits described above without modifying the normal output stream.
52 * For commands like build that don't generate stdout output this doesn't
53 * matter, but for commands like query and ide_build_info, inserting these
54 * control characters in stdout invalidated their output.
55 *
56 * <p/>
57 * The underlying streams may be either line-bufferred or unbuffered.
58 * Normally each event will write out a sequence of output to a single
59 * stream, and will end with a newline, which ensures a flush.
60 * But care is required when outputting incomplete lines, or when mixing
61 * output between the two different streams (stdout and stderr):
62 * it may be necessary to explicitly flush the output in those cases.
63 * However, we also don't want to flush too often; that can lead to
64 * a choppy UI experience.
65 */
66public class FancyTerminalEventHandler extends BlazeCommandEventHandler {
67 private static Logger LOG = Logger.getLogger(FancyTerminalEventHandler.class.getName());
68 private static final Pattern progressPattern = Pattern.compile(
69 // Match strings that look like they start with progress info:
70 // [42%] Compiling base/base.cc
71 // [1,442 / 23,476] Compiling base/base.cc
72 "^\\[(?:(?:\\d\\d?\\d?%)|(?:[\\d+,]+ / [\\d,]+))\\] ");
73 private static final Splitter LINEBREAK_SPLITTER = Splitter.on('\n');
Laurent Le Brun54733f92015-09-03 13:59:44 +000074 private static final List<String> SPECIAL_MESSAGES =
75 ImmutableList.of(
76 "Reading startup options from "
Googlere9e052d2016-04-05 15:29:41 +000077 + "HKEY_LOCAL_MACHINE\\Software\\Google\\Devtools\\CurrentVersion",
Laurent Le Brun54733f92015-09-03 13:59:44 +000078 "Contacting ftp://microsoft.com/win3.1/downloadcenter",
79 "Downloading MSVCR71.DLL",
80 "Installing Windows Update 37 of 118...",
81 "Sending request to Azure server",
Laurent Le Brun54733f92015-09-03 13:59:44 +000082 "Initializing HAL",
83 "Loading NDIS2SUP.VXD",
84 "Initializing DRM",
85 "Contacting license server",
86 "Starting EC2 instances",
87 "Starting MS-DOS 6.0",
88 "Updating virus database",
89 "Linking WIN32.DLL",
90 "Linking GGL32.EXE",
91 "Starting ActiveX controls",
92 "Launching Microsoft Visual Studio 2013",
93 "Launching IEXPLORE.EXE",
94 "Initializing BASIC v2.1 interpreter",
95 "Parsing COM object monikers",
96 "Notifying field agents",
97 "Negotiating with killer robots",
98 "Searching for cellular signal",
Laurent Le Brun54733f92015-09-03 13:59:44 +000099 "Waiting for workstation CPU temperature to decrease");
Googler2f95be32015-03-20 17:26:11 +0000100
Florian Weikert90670382015-11-11 14:48:41 +0000101 private static final Set<Character> PUNCTUATION_CHARACTERS =
102 ImmutableSet.<Character>of(',', '.', ':', '?', '!', ';');
103
Laurent Le Brun54733f92015-09-03 13:59:44 +0000104 private final Iterator<String> messageIterator = Iterators.cycle(SPECIAL_MESSAGES);
Googler2f95be32015-03-20 17:26:11 +0000105 private volatile boolean trySpecial;
106 private volatile Instant skipUntil = Instant.now();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100107
108 private final AnsiTerminal terminal;
109
110 private final boolean useColor;
111 private final boolean useCursorControls;
112 private final boolean progressInTermTitle;
113 public final int terminalWidth;
114
115 private boolean terminalClosed = false;
116 private boolean previousLineErasable = false;
117 private int numLinesPreviousErasable = 0;
118
119 public FancyTerminalEventHandler(OutErr outErr, BlazeCommandEventHandler.Options options) {
120 super(outErr, options);
121 this.terminal = new AnsiTerminal(outErr.getErrorStream());
122 this.terminalWidth = (options.terminalColumns > 0 ? options.terminalColumns : 80);
123 useColor = options.useColor();
124 useCursorControls = options.useCursorControl();
125 progressInTermTitle = options.progressInTermTitle;
Googler2f95be32015-03-20 17:26:11 +0000126
127 Calendar today = Calendar.getInstance();
128 trySpecial = options.forceExternalRepositories
129 || (options.externalRepositories
130 && today.get(Calendar.MONTH) == Calendar.APRIL
131 && today.get(Calendar.DAY_OF_MONTH) == 1);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100132 }
133
134 @Override
135 public void handle(Event event) {
136 if (terminalClosed) {
137 return;
138 }
139 if (!eventMask.contains(event.getKind())) {
140 return;
141 }
Googler2f95be32015-03-20 17:26:11 +0000142 if (trySpecial && !EventKind.ERRORS_AND_WARNINGS_AND_OUTPUT.contains(event.getKind())
143 && skipUntil.isAfterNow()) {
144 // Short-circuit here to avoid wiping out previous terminal contents.
145 return;
146 }
147
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100148 try {
149 boolean previousLineErased = false;
150 if (previousLineErasable) {
151 previousLineErased = maybeOverwritePreviousMessage();
152 }
153 switch (event.getKind()) {
154 case PROGRESS:
155 case START:
156 {
157 String message = event.getMessage();
158 Pair<String,String> progressPair = matchProgress(message);
159 if (progressPair != null) {
160 progress(progressPair.getFirst(), progressPair.getSecond());
Googler2f95be32015-03-20 17:26:11 +0000161 if (trySpecial && ThreadLocalRandom.current().nextInt(0, 20) == 0) {
162 message = getExtraMessage();
163 if (message != null) {
164 // Should always be true, but don't crash on that!
165 previousLineErased = maybeOverwritePreviousMessage();
166 progress(progressPair.getFirst(), message);
167 // Skip unimportant messages for a bit so that this message gets some exposure.
168 skipUntil = Instant.now().plus(
169 Duration.millis(ThreadLocalRandom.current().nextInt(3000, 8000)));
170 }
171 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100172 } else {
173 progress("INFO: ", message);
174 }
175 break;
176 }
177 case FINISH:
178 {
179 String message = event.getMessage();
180 Pair<String,String> progressPair = matchProgress(message);
181 if (progressPair != null) {
182 String percentage = progressPair.getFirst();
183 String rest = progressPair.getSecond();
184 progress(percentage, rest + " DONE");
185 } else {
186 progress("INFO: ", message + " DONE");
187 }
188 break;
189 }
190 case PASS:
191 progress("PASS: ", event.getMessage());
192 break;
193 case INFO:
194 info(event);
195 break;
196 case ERROR:
197 case FAIL:
198 case TIMEOUT:
199 // For errors, scroll the message, so it appears above the status
200 // line, and highlight the word "ERROR" or "FAIL" in boldface red.
201 errorOrFail(event);
202 break;
203 case WARNING:
204 // For warnings, highlight the word "Warning" in boldface magenta,
205 // and scroll it.
206 warning(event);
207 break;
208 case SUBCOMMAND:
209 subcmd(event);
210 break;
211 case STDOUT:
212 if (previousLineErased) {
213 terminal.flush();
214 }
215 previousLineErasable = false;
216 super.handle(event);
217 // We don't need to flush stdout here, because
218 // super.handle(event) will take care of that.
219 break;
220 case STDERR:
221 putOutput(event);
222 break;
223 default:
224 // Ignore all other event types.
225 break;
226 }
227 } catch (IOException e) {
228 // The terminal shouldn't have IO errors, unless the shell is killed, which
229 // should also kill the blaze client. So this isn't something that should
230 // occur here; it will show up in the client/server interface as a broken
231 // pipe.
232 LOG.warning("Terminal was closed during build: " + e);
233 terminalClosed = true;
234 }
235 }
Googler2f95be32015-03-20 17:26:11 +0000236
237 private String getExtraMessage() {
238 synchronized (messageIterator) {
239 if (messageIterator.hasNext()) {
240 return messageIterator.next();
241 }
242 }
243 trySpecial = false;
244 return null;
245 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100246
247 /**
248 * Displays a progress message that may be erased by subsequent messages.
249 *
250 * @param prefix a short string such as "[99%] " or "INFO: ", which will be highlighted
251 * @param rest the remainder of the message; may be multiple lines
252 */
253 private void progress(String prefix, String rest) throws IOException {
254 previousLineErasable = true;
255
256 if (progressInTermTitle) {
257 int newlinePos = rest.indexOf('\n');
258 if (newlinePos == -1) {
259 terminal.setTitle(prefix + rest);
260 } else {
261 terminal.setTitle(prefix + rest.substring(0, newlinePos));
262 }
263 }
264
Klaus Aehliga49f0252016-03-02 15:48:40 +0000265 LineCountingAnsiTerminalWriter countingWriter = new LineCountingAnsiTerminalWriter(terminal);
Klaus Aehlig2224b3e2016-03-30 17:40:30 +0000266 AnsiTerminalWriter terminalWriter = countingWriter;
267
268 if (useCursorControls) {
269 terminalWriter = new LineWrappingAnsiTerminalWriter(terminalWriter, terminalWidth - 1);
270 }
Klaus Aehliga49f0252016-03-02 15:48:40 +0000271
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100272 if (useColor) {
Klaus Aehliga49f0252016-03-02 15:48:40 +0000273 terminalWriter.okStatus();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100274 }
Klaus Aehliga49f0252016-03-02 15:48:40 +0000275 terminalWriter.append(prefix);
276 terminalWriter.normal();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100277 if (showTimestamp) {
278 String timestamp = timestamp();
Klaus Aehliga49f0252016-03-02 15:48:40 +0000279 terminalWriter.append(timestamp);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100280 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100281 Iterator<String> lines = LINEBREAK_SPLITTER.split(rest).iterator();
282 String firstLine = lines.next();
Klaus Aehliga49f0252016-03-02 15:48:40 +0000283 terminalWriter.append(firstLine);
284 terminalWriter.newline();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100285 while (lines.hasNext()) {
286 String line = lines.next();
Klaus Aehliga49f0252016-03-02 15:48:40 +0000287 terminalWriter.append(line);
288 terminalWriter.newline();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100289 }
Klaus Aehliga49f0252016-03-02 15:48:40 +0000290 numLinesPreviousErasable = countingWriter.getWrittenLines();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100291 }
292
293 /**
294 * Try to match a message against the "progress message" pattern. If it
295 * matches, return the progress percentage, and the rest of the message.
296 * @param message the message to match
297 * @return a pair containing the progress percentage, and the rest of the
298 * progress message, or null if the message isn't a progress message.
299 */
300 private Pair<String,String> matchProgress(String message) {
301 Matcher m = progressPattern.matcher(message);
302 if (m.find()) {
303 return Pair.of(message.substring(0, m.end()), message.substring(m.end()));
304 } else {
305 return null;
306 }
307 }
308
309 /**
310 * Send the terminal controls that will put the cursor on the beginning
311 * of the same line if cursor control is on, or the next line if not.
Florian Weikert11b29dd2015-11-20 09:20:08 +0000312 * @return True if it did any output; if so, caller is responsible for
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100313 * flushing the terminal if needed.
314 */
315 private boolean maybeOverwritePreviousMessage() throws IOException {
316 if (useCursorControls && numLinesPreviousErasable != 0) {
317 for (int i = 0; i < numLinesPreviousErasable; i++) {
318 terminal.cr();
319 terminal.cursorUp(1);
320 terminal.clearLine();
321 }
322 return true;
323 } else {
324 return false;
325 }
326 }
327
328 private void errorOrFail(Event event) throws IOException {
329 previousLineErasable = false;
330 if (useColor) {
331 terminal.textRed();
332 terminal.textBold();
333 }
Ulf Adams07dba942015-03-05 14:47:37 +0000334 terminal.writeString(event.getKind() + ": ");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100335 if (useColor) {
336 terminal.resetTerminal();
337 }
338 writeTimestampAndLocation(event);
Florian Weikert90670382015-11-11 14:48:41 +0000339 writeStringWithPotentialPeriod(event.getMessage());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100340 crlf();
341 }
342
343 private void warning(Event warning) throws IOException {
344 previousLineErasable = false;
345 if (useColor) {
346 terminal.textMagenta();
347 }
348 terminal.writeString("WARNING: ");
349 terminal.resetTerminal();
350 writeTimestampAndLocation(warning);
Florian Weikert90670382015-11-11 14:48:41 +0000351 writeStringWithPotentialPeriod(warning.getMessage());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100352 crlf();
353 }
354
355 private void info(Event event) throws IOException {
356 previousLineErasable = false;
357 if (useColor) {
358 terminal.textGreen();
359 }
Ulf Adams07dba942015-03-05 14:47:37 +0000360 terminal.writeString(event.getKind() + ": ");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100361 terminal.resetTerminal();
362 writeTimestampAndLocation(event);
Florian Weikert11b29dd2015-11-20 09:20:08 +0000363 terminal.writeString(event.getMessage());
364 // No period; info messages may end with a URL.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100365 crlf();
366 }
367
Florian Weikert90670382015-11-11 14:48:41 +0000368 /**
369 * Writes the given String to the terminal. This method also writes a trailing period if the
370 * message doesn't end with a punctuation character.
371 */
372 private void writeStringWithPotentialPeriod(String message) throws IOException {
373 terminal.writeString(message);
374 if (!message.isEmpty()) {
375 char lastChar = message.charAt(message.length() - 1);
376 if (!PUNCTUATION_CHARACTERS.contains(lastChar)) {
377 terminal.writeString(".");
378 }
379 }
380 }
381
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100382 private void subcmd(Event subcmd) throws IOException {
383 previousLineErasable = false;
384 if (useColor) {
385 terminal.textBlue();
386 }
387 terminal.writeString(">>>>> ");
388 terminal.resetTerminal();
389 writeTimestampAndLocation(subcmd);
390 terminal.writeString(subcmd.getMessage());
391 crlf();
392 }
393
394 /* Handle STDERR events. */
395 private void putOutput(Event event) throws IOException {
396 previousLineErasable = false;
397 terminal.writeBytes(event.getMessageBytes());
398/*
399 * The following code doesn't work because buildtool.TerminalTestNotifier
400 * writes ANSI-formatted text via this mechanism, one character at a time,
401 * and if we try to insert additional ANSI sequences in between the characters
402 * of another ANSI escape sequence, we screw things up. (?)
403 * TODO(bazel-team): (2009) fix this. TerminalTestNotifier should go via the Reporter
404 * rather than via an AnsiTerminalWriter.
405 */
406// terminal.resetTerminal();
407// writeTimestampAndLocation(event);
408// if (useColor) {
409// terminal.textNormal();
410// }
411// terminal.writeBytes(event.getMessageBytes());
412// terminal.resetTerminal();
413 }
414
415 /**
416 * Add a carriage return, shifting to the next line on the terminal, while
417 * guaranteeing that the terminal control codes don't cause any strange
418 * effects. Without the CR before the "\n", the "\n" can cause a line-break
419 * moving text to the next line, where the new message will be generated.
420 * Emitting a "CR" before means that the actual terminal controls generated
421 * here are CR+CR+LF; the double-CR resets the terminal line state, which
422 * prevents the potentially ugly formatting issue.
423 */
424 private void crlf() throws IOException {
425 terminal.cr();
426 terminal.writeString("\n");
427 }
428
429 private void writeTimestampAndLocation(Event event) throws IOException {
430 if (showTimestamp) {
431 terminal.writeString(timestamp());
432 }
433 if (event.getLocation() != null) {
434 terminal.writeString(event.getLocation() + ": ");
435 }
436 }
437
438 public void resetTerminal() {
439 try {
440 terminal.resetTerminal();
441 } catch (IOException e) {
442 LOG.warning("IO Error writing to user terminal: " + e);
443 }
444 }
445}