blob: d201009ea51e3717d20183473787d67c79756fb3 [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.events;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.syntax.Location;
import net.starlark.java.syntax.SyntaxError;
/**
* A situation encountered by the build system that's worth reporting.
*
* <p>An event specifies an {@link EventKind}, a message, and (optionally) additional properties.
*
* <p>Although this is serializable, use caution if {@link Class}es with the same name and different
* classloaders are expected to be found in the same event's properties. Common class object
* serialization techniques ignore classloaders, so duplicate entry keys may be deserialized,
* violating assumptions.
*/
@Immutable
public final class Event implements Serializable {
private final EventKind kind;
/**
* This field has type {@link String} or {@link byte[]}.
*
* <p>If this field is a byte array then it contains the UTF-8-encoded bytes of a message. This
* optimization avoids converting back and forth between strings and bytes.
*/
private final Object message;
/**
* This map's entries are ordered by {@link Class#getName}.
*
* <p>That is not a total ordering because of classloaders. The order of entries whose key names
* are equal is not deterministic.
*/
private final ImmutableClassToInstanceMap<Object> properties;
private int hashCode;
private Event(EventKind kind, Object message, ImmutableClassToInstanceMap<Object> properties) {
this.kind = checkNotNull(kind);
this.message = checkNotNull(message);
this.properties = checkNotNull(properties);
}
public EventKind getKind() {
return kind;
}
public String getMessage() {
return message instanceof String ? (String) message : new String((byte[]) message, UTF_8);
}
/**
* Returns this event's message as a {@link byte[]}. If this event was instantiated using a {@link
* String}, the returned byte array is encoded using {@link
* java.nio.charset.StandardCharsets#UTF_8}.
*/
public byte[] getMessageBytes() {
return message instanceof byte[] ? (byte[]) message : ((String) message).getBytes(UTF_8);
}
/** Returns the property value associated with {@code type} if any, and {@code null} otherwise. */
@Nullable
public <T> T getProperty(Class<T> type) {
return properties.getInstance(type);
}
/**
* Returns an {@link Event} instance that has the same type, message, and properties as the event
* this is called on, and additionally associates {@code propertyValue} (if non-{@code null}) with
* {@code type}.
*
* <p>If the event this is called on already has a property associated with {@code type} and
* {@code propertyValue} is non-{@code null}, the returned event will have {@code propertyValue}
* associated with it instead. If {@code propertyValue} is non-{@code null}, the returned event
* will have no property associated with {@code type}.
*
* <p>If the event this is called on has no property associated with {@code type}, and {@code
* propertyValue} is {@code null}, then this returns that event (it does not create a new {@link
* Event} instance).
*
* <p>In any case, the event this is called on does not change.
*/
// This implementation would be inefficient if #withProperty is called repeatedly because it may
// copy and sort the key collection. In practice we expect it to be called a small number of times
// per event (e.g. fewer than 5; usually 0).
//
// If that changes then consider an Event.Builder strategy instead.
public <T> Event withProperty(Class<T> type, @Nullable T propertyValue) {
Iterable<Class<?>> orderedKeys;
boolean containsKey = properties.containsKey(type);
if (!containsKey && propertyValue != null) {
orderedKeys =
Stream.concat(properties.keySet().stream(), Stream.of(type))
.sorted(comparing(Class::getName))
.collect(toImmutableList());
} else if (containsKey) {
orderedKeys = properties.keySet();
} else {
// !containsKey and propertyValue is null, so there's nothing to change.
return this;
}
ImmutableClassToInstanceMap.Builder<Object> newProperties =
new ImmutableClassToInstanceMap.Builder<>();
for (Class<?> key : orderedKeys) {
if (key.equals(type)) {
if (propertyValue != null) {
newProperties.put(type, propertyValue);
}
} else {
addToBuilder(newProperties, key);
}
}
return new Event(kind, message, newProperties.build());
}
/**
* This type-parameterized method solves a problem where a {@code properties.getInstance(key)}
* expression would have type {@link Object} when {@code key} is a wildcard-parameterized {@link
* Class}. That {@link Object}-typed expression would then fail to type check in a {@code
* builder.put(key, properties.getInstance(key))} statement.
*/
private <T> void addToBuilder(ImmutableClassToInstanceMap.Builder<Object> builder, Class<T> key) {
builder.put(key, checkNotNull(properties.getInstance(key)));
}
/**
* Like {@link #withProperty(Class, Object)}, with {@code type.equals(String.class)}.
*
* <p>Additionally, if the event this is called on already has a {@link String} property with
* value {@code tag}, or if {@code tag} is {@code null} and the event has no {@link String}
* property, then this returns that event (it does not create a new {@link Event} instance).
*/
public Event withTag(@Nullable String tag) {
if (Objects.equals(tag, getProperty(String.class))) {
return this;
}
return withProperty(String.class, tag);
}
/** Like {@link #withProperty(Class, Object)}, with {@code type.equals(FileOutErr.class)}. */
public Event withStdoutStderr(FileOutErr outErr) {
return withProperty(FileOutErr.class, outErr);
}
/**
* Returns the {@link String} property, if any, asssociated with the event. When non-null, this
* value typically describes some property of the action that generated the event.
*/
// TODO(mschaller): change code which relies on this to rely on a more structured value, using
// types less prone to interference.
@Nullable
public String getTag() {
return getProperty(String.class);
}
/** Indicates if a {@link FileOutErr} property is associated with this event. */
public boolean hasStdoutStderr() {
return getProperty(FileOutErr.class) != null;
}
/**
* Gets the path to the stdout associated with this event (which the caller must not access), or
* null if there is no such path.
*/
@Nullable
public PathFragment getStdOutPathFragment() {
FileOutErr outErr = getProperty(FileOutErr.class);
return outErr == null ? null : outErr.getOutputPathFragment();
}
/** Gets the size of the stdout associated with this event without reading it. */
public long getStdOutSize() throws IOException {
FileOutErr outErr = getProperty(FileOutErr.class);
return outErr == null ? 0 : outErr.outSize();
}
/** Returns the stdout bytes associated with this event if any, and {@code null} otherwise. */
@Nullable
public byte[] getStdOut() {
FileOutErr outErr = getProperty(FileOutErr.class);
if (outErr == null) {
return null;
}
return outErr.outAsBytes();
}
/**
* Gets the path to the stderr associated with this event (which the caller must not access), or
* null if there is no such path.
*/
@Nullable
public PathFragment getStdErrPathFragment() {
FileOutErr outErr = getProperty(FileOutErr.class);
return outErr == null ? null : outErr.getErrorPathFragment();
}
/** Gets the size of the stderr associated with this event without reading it. */
public long getStdErrSize() throws IOException {
FileOutErr outErr = getProperty(FileOutErr.class);
return outErr == null ? 0 : outErr.errSize();
}
/** Returns the stderr bytes associated with this event if any, and {@code null} otherwise. */
@Nullable
public byte[] getStdErr() {
FileOutErr outErr = getProperty(FileOutErr.class);
if (outErr == null) {
return null;
}
return outErr.errAsBytes();
}
/**
* Returns the location of this event, if any. Returns null iff the event wasn't associated with
* any particular location, for example, a progress message.
*/
@Nullable
public Location getLocation() {
return getProperty(Location.class);
}
/** Returns the event formatted as {@code "ERROR foo.bzl:1:2: oops"}. */
@Override
public String toString() {
Location location = getLocation();
// TODO(adonovan): <no location> is just noise.
return kind
+ " "
+ (location != null ? location.toString() : "<no location>")
+ ": "
+ getMessage();
}
@Override
public int hashCode() {
// We defer the computation of hashCode until it is needed to avoid the overhead of computing it
// and then never using it. In particular, we use Event for streaming stdout and stderr, which
// are both large and the hashCode is never used.
//
// This uses the same construction as String.hashCode. We don't lock, so reads and writes to the
// field can race. However, integer reads and writes are atomic, and this code guarantees that
// all writes have the same value, so the memory location can only be either 0 or the final
// value. Note that a reader could see the final value on the first read and 0 on the second
// read, so we must take care to only read the field once.
int h = hashCode;
if (h == 0) {
h =
Objects.hash(
kind,
message instanceof String ? message : Arrays.hashCode((byte[]) message),
properties);
hashCode = h;
}
return h;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other == null || !other.getClass().equals(getClass())) {
return false;
}
Event that = (Event) other;
return Objects.equals(this.kind, that.kind)
&& this.message.getClass().equals(that.message.getClass())
&& (this.message instanceof String
? Objects.equals(this.message, that.message)
: Arrays.equals((byte[]) this.message, (byte[]) that.message))
&& Objects.equals(this.properties, that.properties);
}
/** Constructs an event with the provided {@link EventKind} and {@link String} message. */
public static Event of(EventKind kind, String message) {
return new Event(kind, message, ImmutableClassToInstanceMap.of());
}
/**
* Constructs an event with the provided {@link EventKind}, {@link String} message, and single
* property value.
*
* <p>See {@link #withProperty(Class, Object)} if more than one property value is desired.
*/
public static <T> Event of(
EventKind kind, String message, Class<T> propertyType, T propertyValue) {
return new Event(kind, message, ImmutableClassToInstanceMap.of(propertyType, propertyValue));
}
/** Constructs an event with the provided {@link EventKind} and {@link byte[]} message. */
public static Event of(EventKind kind, byte[] messageBytes) {
return new Event(kind, messageBytes, ImmutableClassToInstanceMap.of());
}
/**
* Constructs an event with the provided {@link EventKind}, {@link byte[]} message, and single
* property value.
*
* <p>See {@link #withProperty(Class, Object)} if more than one property value is desired.
*/
public static <T> Event of(
EventKind kind, byte[] messageBytes, Class<T> propertyType, T propertyValue) {
return new Event(
kind, messageBytes, ImmutableClassToInstanceMap.of(propertyType, propertyValue));
}
/**
* Constructs an event with the provided {@link EventKind} and {@link String} message, with an
* optional {@link Location}.
*/
public static Event of(EventKind kind, @Nullable Location location, String message) {
return location == null ? of(kind, message) : of(kind, message, Location.class, location);
}
/**
* Constructs an event with a {@code byte[]} array instead of a {@link String} for its message.
*
* <p>The bytes must be decodable as UTF-8 text.
*/
public static Event of(EventKind kind, @Nullable Location location, byte[] messageBytes) {
return location == null
? of(kind, messageBytes)
: of(kind, messageBytes, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#FATAL}. */
public static Event fatal(String message) {
return of(EventKind.FATAL, message);
}
/** Constructs an event with kind {@link EventKind#ERROR}, with an optional {@link Location}. */
public static Event error(@Nullable Location location, String message) {
return location == null
? of(EventKind.ERROR, message)
: of(EventKind.ERROR, message, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#ERROR}. */
public static Event error(String message) {
return of(EventKind.ERROR, message);
}
/** Constructs an event with kind {@link EventKind#WARNING}, with an optional {@link Location}. */
public static Event warn(@Nullable Location location, String message) {
return location == null
? of(EventKind.WARNING, message)
: of(EventKind.WARNING, message, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#WARNING}. */
public static Event warn(String message) {
return of(EventKind.WARNING, message);
}
/** Constructs an event with kind {@link EventKind#INFO}, with an optional {@link Location}. */
public static Event info(@Nullable Location location, String message) {
return location == null
? of(EventKind.INFO, message)
: of(EventKind.INFO, message, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#INFO}. */
public static Event info(String message) {
return of(EventKind.INFO, message);
}
/**
* Constructs an event with kind {@link EventKind#PROGRESS}, with an optional {@link Location}.
*/
public static Event progress(@Nullable Location location, String message) {
return location == null
? of(EventKind.PROGRESS, message)
: of(EventKind.PROGRESS, message, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#PROGRESS}. */
public static Event progress(String message) {
return of(EventKind.PROGRESS, message);
}
/** Constructs an event with kind {@link EventKind#DEBUG}, with an optional {@link Location}. */
public static Event debug(@Nullable Location location, String message) {
return location == null
? of(EventKind.DEBUG, message)
: of(EventKind.DEBUG, message, Location.class, location);
}
/** Constructs an event with kind {@link EventKind#DEBUG}. */
public static Event debug(String message) {
return of(EventKind.DEBUG, message);
}
/** Replays a sequence of events on {@code handler}. */
public static void replayEventsOn(EventHandler handler, Iterable<Event> events) {
for (Event event : events) {
handler.handle(event);
}
}
/** Converts a list of {@link SyntaxError}s to events and replays them on {@code handler}. */
public static void replayEventsOn(EventHandler handler, List<SyntaxError> errors) {
for (SyntaxError error : errors) {
handler.handle(Event.error(error.location(), error.message()));
}
}
/**
* Converts a list of {@link SyntaxError}s to events, each with a specified property, and replays
* them on {@code handler}.
*/
public static <T> void replayEventsOn(
EventHandler handler,
List<SyntaxError> errors,
Class<T> propertyType,
Function<SyntaxError, T> toProperty) {
for (SyntaxError error : errors) {
handler.handle(
Event.error(error.location(), error.message())
.withProperty(propertyType, toProperty.apply(error)));
}
}
/**
* Returns a {@link StarlarkThread.PrintHandler} that sends {@link EventKind#DEBUG} events to the
* provided {@link EventHandler}.
*/
public static StarlarkThread.PrintHandler makeDebugPrintHandler(EventHandler h) {
return (thread, msg) -> h.handle(Event.debug(thread.getCallerLocation(), msg));
}
}