blob: 1e6b033b3f4d267ce0d3072d2f2c52dd99866895 [file] [log] [blame]
// 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.runtime;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.EnumConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.EnumSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* BlazeCommandEventHandler: an event handler established for the duration of a
* single Blaze command.
*/
public class BlazeCommandEventHandler implements EventHandler {
private static final Logger logger = Logger.getLogger(BlazeCommandEventHandler.class.getName());
public enum UseColor { YES, NO, AUTO }
public enum UseCurses { YES, NO, AUTO }
public static class UseColorConverter extends EnumConverter<UseColor> {
public UseColorConverter() {
super(UseColor.class, "--color setting");
}
}
public static class UseCursesConverter extends EnumConverter<UseCurses> {
public UseCursesConverter() {
super(UseCurses.class, "--curses setting");
}
}
public static class Options extends OptionsBase {
@Option(
name = "show_progress",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Display progress messages during a build."
)
public boolean showProgress;
@Option(
name = "show_task_finish",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Display progress messages when tasks complete, not just when they start."
)
public boolean showTaskFinish;
@Option(
name = "show_progress_rate_limit",
defaultValue = "0.2", // A nice middle ground; snappy but not too spammy in logs.
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Minimum number of seconds between progress messages in the output."
)
public double showProgressRateLimit;
@Option(
name = "color",
defaultValue = "auto",
converter = UseColorConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Use terminal controls to colorize output going to stderr."
)
public UseColor useColorEnum;
@Option(
name = "curses",
defaultValue = "auto",
converter = UseCursesConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Use terminal cursor controls to minimize scrolling output going to stderr."
)
public UseCurses useCursesEnum;
@Option(
name = "terminal_columns",
defaultValue = "80",
metadataTags = {OptionMetadataTag.HIDDEN},
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "A system-generated parameter which specifies the terminal width in columns."
)
public int terminalColumns;
@Option(
name = "is_stderr_atty",
// TODO(b/63386499): Old name should be removed after 2019-02-28.
oldName = "isatty",
defaultValue = "false",
metadataTags = {OptionMetadataTag.HIDDEN},
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"A system-generated parameter which is used to notify the server whether this client is"
+ " running in a terminal. If this is set to false, then '--color=auto' will be"
+ " treated as '--color=no'. If this is set to true, then '--color=auto' will be"
+ " treated as '--color=yes'. As we only treat the stderr as a terminal, we only"
+ " care if that file descriptor is connected to a TTY."
)
public boolean isStderrATty;
// This lives here (as opposed to the more logical BuildRequest.Options)
// because the client passes it to the server *always*. We don't want the
// client to have to figure out when it should or shouldn't to send it.
@Option(
name = "emacs",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"A system-generated parameter which is true iff EMACS=t or INSIDE_EMACS is set "
+ "in the environment of the client. This option controls certain display "
+ "features."
)
public boolean runningInEmacs;
@Option(
name = "show_timestamps",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Include timestamps in messages"
)
public boolean showTimestamp;
@Option(
name = "progress_in_terminal_title",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Show the command progress in the terminal title. "
+ "Useful to see what bazel is doing when having multiple terminal tabs.")
public boolean progressInTermTitle;
@Option(
name = "experimental_external_repositories",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Use external repositories for improved stability and speed when available."
)
public boolean externalRepositories;
@Option(
name = "force_experimental_external_repositories",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Forces --experimental_external_repositories."
)
public boolean forceExternalRepositories;
@Option(
name = "attempt_to_print_relative_paths",
oldName = "experimental_ui_attempt_to_print_relative_paths",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help =
"When printing the location part of messages, attempt to use a path relative to the "
+ "workspace directory or one of the directories specified by --package_path.")
public boolean attemptToPrintRelativePaths;
@Option(
name = "experimental_ui",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Switches to an alternative progress bar that more explicitly shows progress, such "
+ "as loaded packages and executed actions."
)
public boolean experimentalUi;
@Option(
name = "experimental_ui_debug_all_events",
defaultValue = "false",
metadataTags = {OptionMetadataTag.HIDDEN},
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.UNKNOWN},
help = "Report all events known to the experimental new Bazel UI."
)
public boolean experimentalUiDebugAllEvents;
@Option(
name = "experimental_ui_actions_shown",
defaultValue = "8",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Number of concurrent actions shown in the alternative progress bar; each "
+ "action is shown on a separate line. The alternative progress bar always shows "
+ "at least one one, all numbers less than 1 are mapped to 1. "
+ "This option has no effect unless --experimental_ui is set."
)
public int experimentalUiActionsShown;
@Option(
name = "experimental_ui_limit_console_output",
defaultValue = "0",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Number of bytes to which the experimental UI will limit its output (non-positive "
+ "values indicate unlimited). Once the limit is approaching, the experimental UI "
+ "will try hard to limit in a meaningful way, but will ultimately just drop all "
+ "output."
)
public int experimentalUiLimitConsoleOutput;
@Option(
name = "experimental_ui_deduplicate",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help = "Make the experimental UI deduplicate messages to have a cleaner scroll-back log.")
public boolean experimentalUiDeduplicate;
public boolean useColor() {
return useColorEnum == UseColor.YES || (useColorEnum == UseColor.AUTO && isStderrATty);
}
public boolean useCursorControl() {
return useCursesEnum == UseCurses.YES || (useCursesEnum == UseCurses.AUTO && isStderrATty);
}
}
private static final DateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("(MM-dd HH:mm:ss.SSS) ");
protected final OutErr outErr;
private final PrintStream errPrintStream;
protected final Set<EventKind> eventMask =
EnumSet.copyOf(EventKind.ERRORS_WARNINGS_AND_INFO_AND_OUTPUT);
protected final boolean showTimestamp;
protected final LocationPrinter locationPrinter;
public BlazeCommandEventHandler(
OutErr outErr, Options eventOptions, PathFragment workspacePathFragment) {
this.outErr = outErr;
this.errPrintStream = new PrintStream(outErr.getErrorStream(), true);
if (eventOptions.showProgress) {
eventMask.add(EventKind.PROGRESS);
eventMask.add(EventKind.START);
} else {
// Skip PASS events if --noshow_progress is requested.
eventMask.remove(EventKind.PASS);
}
if (eventOptions.showTaskFinish) {
eventMask.add(EventKind.FINISH);
}
eventMask.add(EventKind.SUBCOMMAND);
this.showTimestamp = eventOptions.showTimestamp;
this.locationPrinter =
new LocationPrinter(eventOptions.attemptToPrintRelativePaths, workspacePathFragment);
}
/** See EventHandler.handle. */
@Override
public void handle(Event event) {
if (!eventMask.contains(event.getKind())) {
return;
}
String prefix;
switch (event.getKind()) {
case STDOUT:
putOutput(outErr.getOutputStream(), event);
return;
case STDERR:
putOutput(outErr.getErrorStream(), event);
return;
case PASS:
case FAIL:
case TIMEOUT:
case ERROR:
case WARNING:
case DEBUG:
case DEPCHECKER:
prefix = event.getKind() + ": ";
break;
case SUBCOMMAND:
prefix = ">>>>>>>>> ";
break;
case INFO:
case PROGRESS:
case START:
case FINISH:
prefix = "____";
break;
default:
throw new IllegalStateException("" + event.getKind());
}
StringBuilder buf = new StringBuilder();
buf.append(prefix);
if (showTimestamp) {
buf.append(timestamp());
}
Location location = event.getLocation();
if (location != null) {
buf.append(locationPrinter.getLocationString(location)).append(": ");
}
buf.append(event.getMessage());
if (event.getKind() == EventKind.FINISH) {
buf.append(" DONE");
}
// Add a trailing period for ERROR and WARNING messages, which are
// typically English sentences composed from exception messages.
if (event.getKind() == EventKind.WARNING ||
event.getKind() == EventKind.ERROR) {
buf.append('.');
}
// Event messages go to stderr; results (e.g. 'blaze query') go to stdout.
errPrintStream.println(buf);
if (event.getStdErr() != null) {
handle(
Event.of(
EventKind.STDERR, null, event.getStdErr().getBytes(StandardCharsets.ISO_8859_1)));
}
if (event.getStdOut() != null) {
handle(
Event.of(
EventKind.STDOUT, null, event.getStdOut().getBytes(StandardCharsets.ISO_8859_1)));
}
}
private void putOutput(OutputStream out, Event event) {
try {
out.write(event.getMessageBytes());
out.flush();
} catch (IOException e) {
// This can happen in server mode if the blaze client has exited, or if output is redirected
// to a file and the disk is full, etc. May be moot in the case of full disk, or useful in
// the case of real bug in our handling of streams.
logger.log(Level.WARNING, "Failed to write event", e);
}
}
/**
* @return a string representing the current time, eg "04-26 13:47:32.124".
*/
protected String timestamp() {
return TIMESTAMP_FORMAT.format(ZonedDateTime.now(ZoneId.systemDefault()));
}
@Subscribe
public void packageLocatorCreated(PathPackageLocator packageLocator) {
locationPrinter.packageLocatorCreated(packageLocator);
}
}