blob: cd4b08c8356bbaf4e24fce196665f96dc4516f74 [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.util.io;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An implementation of {@link OutErr} that captures all out/err output into
* a file for stdout and a file for stderr. The files are only created if any
* output is made.
* The OutErr assumes that the directory that will contain the output file
* must exist.
*
* You should not use this object from multiple different threads.
*/
// Note that it should be safe to treat the Output and Error streams within a FileOutErr each as
// individually ThreadCompatible.
@ThreadSafety.ThreadCompatible
public class FileOutErr extends OutErr {
private final AtomicInteger childCount = new AtomicInteger();
/**
* Create a new FileOutErr that will write its input,
* if any, to the files specified by stdout/stderr.
*
* No other process may write to the files,
*
* @param stdout The file for the stdout of this outErr
* @param stderr The file for the stderr of this outErr
*/
public FileOutErr(Path stdout, Path stderr) {
this(new FileRecordingOutputStream(stdout), new FileRecordingOutputStream(stderr));
}
/**
* Creates a new FileOutErr that writes its input to the file specified by output. Both
* stdout/stderr will be copied into the single file.
*
* @param output The file for the both stdout and stderr of this outErr.
*/
public FileOutErr(Path output) {
// We don't need to create a synchronized funnel here, like in the OutErr -- The
// respective functions in the FileRecordingOutputStream take care of locking.
this(new FileRecordingOutputStream(output));
}
protected FileOutErr(AbstractFileRecordingOutputStream out,
AbstractFileRecordingOutputStream err) {
super(out, err);
}
/**
* Creates a new FileOutErr that discards its input. Useful
* for testing purposes.
*/
@VisibleForTesting
public FileOutErr() {
this(new NullFileRecordingOutputStream());
}
private FileOutErr(OutputStream stream) {
// We need this function to duplicate the single new object into both arguments
// of the super-constructor.
super(stream, stream);
}
/**
* Returns true if any output was recorded.
*/
public boolean hasRecordedOutput() {
return getFileOutputStream().hasRecordedOutput() || getFileErrorStream().hasRecordedOutput();
}
/**
* Returns true if output was recorded on stdout.
*/
public boolean hasRecordedStdout() {
return getFileOutputStream().hasRecordedOutput();
}
/**
* Returns true if output was recorded on stderr.
*/
public boolean hasRecordedStderr() {
return getFileErrorStream().hasRecordedOutput();
}
/**
* Returns the {@link Path} this OutErr uses to buffer stdout
*
* <p>The user must ensure that no other process is writing to the files at time of creation.
*
* @return the path object with the contents of stdout
*/
public Path getOutputPath() {
return getFileOutputStream().getFile();
}
/**
* Returns the {@link Path} this OutErr uses to buffer stderr.
*
* @return the path object with the contents of stderr
*/
public Path getErrorPath() {
return getFileErrorStream().getFile();
}
/** Interprets the captured out content as an {@code ISO-8859-1} encoded string. */
public String outAsLatin1() {
return getFileOutputStream().getRecordedOutput();
}
/**
* Interprets the captured err content as an {@code ISO-8859-1} encoded
* string.
*/
public String errAsLatin1() {
return getFileErrorStream().getRecordedOutput();
}
/** Return a reference to the recorded stderr */
public OutputReference getOutReference() {
return new FileOutputReference(getFileOutputStream());
}
/** Return a reference to the recorded stdout */
public OutputReference getErrReference() {
return new FileOutputReference(getFileErrorStream());
}
/**
* Closes and deletes the error stream.
*/
public void clearErr() throws IOException {
getFileErrorStream().clear();
}
/**
* Closes and deletes the out stream.
*/
public void clearOut() throws IOException {
getFileOutputStream().clear();
}
/**
* Writes the captured out content to the given output stream,
* avoiding keeping the entire contents in memory.
*/
public void dumpOutAsLatin1(OutputStream out) {
getFileOutputStream().dumpOut(out);
}
/**
* Writes the captured error content to the given error stream,
* avoiding keeping the entire contents in memory.
*/
public void dumpErrAsLatin1(OutputStream out) {
getFileErrorStream().dumpOut(out);
}
/**
* Writes the captured content to the given {@link FileOutErr},
* avoiding keeping the entire contents in memory.
*/
public static void dump(FileOutErr from, FileOutErr to) {
from.dumpOutAsLatin1(to.getOutputStream());
from.dumpErrAsLatin1(to.getErrorStream());
}
private AbstractFileRecordingOutputStream getFileOutputStream() {
return (AbstractFileRecordingOutputStream) getOutputStream();
}
private AbstractFileRecordingOutputStream getFileErrorStream() {
return (AbstractFileRecordingOutputStream) getErrorStream();
}
@ThreadSafe
public FileOutErr childOutErr() {
int index = childCount.getAndIncrement();
Path outPath = getFileOutputStream().getFileUnsafe();
Path errPath = getFileErrorStream().getFileUnsafe();
if (outPath == null || errPath == null) {
return new FileOutErr();
}
return new FileOutErr(
outPath.getParentDirectory().getRelative(outPath.getBaseName() + "-" + index),
errPath.getParentDirectory().getRelative(errPath.getBaseName() + "-" + index));
}
/**
* An abstract supertype for the two other inner classes in this type
* to implement streams that can write to a file.
*/
private abstract static class AbstractFileRecordingOutputStream extends OutputStream {
/**
* Returns true if this FileRecordingOutputStream has encountered an error.
*
* @return true there was an error, false otherwise.
*/
abstract boolean hadError();
/**
* Returns the file this FileRecordingOutputStream is writing to.
*/
abstract Path getFile();
/**
* Returns true if the FileOutErr has stored output.
*/
abstract boolean hasRecordedOutput();
/**
* Returns the output this AbstractFileOutErr has recorded.
*/
abstract String getRecordedOutput();
/**
* Writes the output to the given output stream,
* avoiding keeping the entire contents in memory.
*/
abstract void dumpOut(OutputStream out);
abstract Path getFileUnsafe();
abstract boolean mightHaveOutput();
/** Closes and deletes the output. */
abstract void clear() throws IOException;
}
/**
* An output stream that pretends to capture all its output into a file,
* but instead discards it.
*/
private static class NullFileRecordingOutputStream extends AbstractFileRecordingOutputStream {
NullFileRecordingOutputStream() {
}
@Override
boolean hadError() {
return false;
}
@Override
Path getFile() {
return null;
}
@Override
Path getFileUnsafe() {
return null;
}
@Override
boolean hasRecordedOutput() {
return false;
}
@Override
String getRecordedOutput() {
return "";
}
@Override
boolean mightHaveOutput() {
return false;
}
@Override
void dumpOut(OutputStream out) {
return;
}
@Override
public void clear() {
}
@Override
public void write(byte[] b, int off, int len) {
}
@Override
public void write(int b) {
}
@Override
public void write(byte[] b) {
}
}
/**
* An output stream that captures all output into a file. The file is created only if output is
* received.
*
* The user must take care that nobody else is writing to the file that is backing the output
* stream.
*
* The write() methods of type are synchronized to ensure that writes from different threads are
* not mixed up. Note that this class is otherwise
* {@link com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible}. Only the
* write() methods are allowed to be concurrently, and only concurrently with each other. All
* other calls must be serialized.
*
* The outputStream is here only for the benefit of the pumping IO we're currently using for
* execution - Once that is gone we can remove this output stream and fold its code into the
* FileOutErr.
*/
@ThreadSafety.ThreadCompatible
protected static class FileRecordingOutputStream extends AbstractFileRecordingOutputStream {
private final Path outputFile;
private OutputStream outputStream;
private String error;
private boolean mightHaveOutput = false;
protected FileRecordingOutputStream(Path outputFile) {
this.outputFile = outputFile;
}
@Override
boolean hadError() {
return error != null;
}
@Override
Path getFile() {
// The caller is getting a reference to the filesystem path, so conservatively assume the
// file has been modified.
markDirty();
return outputFile;
}
@Override
Path getFileUnsafe() {
return outputFile;
}
private void markDirty() {
mightHaveOutput = true;
}
private OutputStream getOutputStream() throws IOException {
// you should hold the lock before you invoke this method
if (outputStream == null) {
outputStream = outputFile.getOutputStream();
}
return outputStream;
}
private boolean hasOutputStream() {
return outputStream != null;
}
@Override
public synchronized void clear() throws IOException {
close();
outputStream = null;
outputFile.delete();
mightHaveOutput = false;
}
/**
* Called whenever the FileRecordingOutputStream finds an error.
*/
protected void recordError(IOException exception) {
String newErrorText = exception.getMessage();
error = (error == null) ? newErrorText : error + "\n" + newErrorText;
}
@Override
boolean hasRecordedOutput() {
if (hadError()) {
return true;
}
if (!mightHaveOutput) {
return false;
}
if (!outputFile.exists()) {
return false;
}
try {
return outputFile.getFileSize() > 0;
} catch (IOException ex) {
recordError(ex);
return true;
}
}
@Override
boolean mightHaveOutput() {
return mightHaveOutput;
}
@Override
String getRecordedOutput() {
StringBuilder result = new StringBuilder();
try {
if (mightHaveOutput && getFile().exists()) {
result.append(FileSystemUtils.readContentAsLatin1(getFile()));
}
} catch (IOException ex) {
recordError(ex);
}
if (hadError()) {
result.append(error);
}
return result.toString();
}
@Override
void dumpOut(OutputStream out) {
try {
if (mightHaveOutput && getFile().exists()) {
try (InputStream in = getFile().getInputStream()) {
ByteStreams.copy(in, out);
out.flush();
}
}
} catch (IOException ex) {
recordError(ex);
}
if (hadError()) {
PrintStream ps = new PrintStream(out);
ps.print(error);
ps.flush();
}
}
@Override
public synchronized void write(byte[] b, int off, int len) {
if (len > 0) {
markDirty();
try {
getOutputStream().write(b, off, len);
} catch (IOException ex) {
recordError(ex);
}
}
}
@Override
public synchronized void write(int b) {
markDirty();
try {
getOutputStream().write(b);
} catch (IOException ex) {
recordError(ex);
}
}
@Override
public synchronized void write(byte[] b) throws IOException {
if (b.length > 0) {
markDirty();
getOutputStream().write(b);
}
}
@Override
public synchronized void flush() throws IOException {
if (hasOutputStream()) {
getOutputStream().flush();
}
}
@Override
public synchronized void close() throws IOException {
if (hasOutputStream()) {
getOutputStream().close();
}
}
}
/** Provide access to a sequence of bytes that might be too long to be stored in memory. */
public interface OutputReference {
/** Return the length of the output. */
long getLength();
/** Return the final up to n bytes of the message. */
byte[] getFinalBytes(int count) throws IOException;
}
private static class FileOutputReference implements OutputReference {
private final AbstractFileRecordingOutputStream stream;
long fileSize;
FileOutputReference(AbstractFileRecordingOutputStream stream) {
this.stream = stream;
if (!stream.mightHaveOutput()) {
this.fileSize = 0;
return;
}
Path file = stream.getFileUnsafe();
try {
this.fileSize = file.getFileSize();
} catch (IOException ex) {
this.fileSize = 0;
}
}
@Override
public long getLength() {
return fileSize;
}
@Override
public byte[] getFinalBytes(int count) throws IOException {
if (fileSize == 0) {
// We used file size 0 to mark any errors in accessing the underlying file.
// So stick to this to give a consistent view.
return new byte[0];
}
try (InputStream in = stream.getFileUnsafe().getInputStream()) {
if (fileSize > count) {
in.skip(fileSize - (long) count);
}
return ByteStreams.toByteArray(in);
}
}
}
}