/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.inspector.network;
import com.facebook.stetho.inspector.console.CLog;
import com.facebook.stetho.inspector.helper.ChromePeerManager;
import com.facebook.stetho.inspector.protocol.module.Console;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* {@link InputStream} that caches the data as the data is read, and writes them to the given
* {@link OutputStream}. This also guarantees that we will attempt to reach EOF on the
* {@link InputStream} passing all data to the {@link OutputStream}.
* This is done to allow us to guarantee all responses are represented in the webkit inspector.
*/
// @VisibleForTest
public final class ResponseHandlingInputStream extends FilterInputStream {
public static final String TAG = "ResponseHandlingInputStream";
private static final int BUFFER_SIZE = 1024;
private final String mRequestId;
private final OutputStream mOutputStream;
@Nullable private final CountingOutputStream mDecompressedCounter;
private final ChromePeerManager mNetworkPeerManager;
private final ResponseHandler mResponseHandler;
/**
* This stream will no longer be usable if {@link #close()} has been called on this stream.
*/
@GuardedBy("this")
private boolean mClosed;
@GuardedBy("this")
private boolean mEofSeen;
@Nullable
@GuardedBy("this")
private byte[] mSkipBuffer;
private long mLastDecompressedCount = 0;
/**
* @param inputStream
* @param requestId the requestId to use when we call the {@link NetworkEventReporter}
* @param outputStream stream to write to.
* @param decompressedCounter Optional decompressing counting output stream which
* can be queried after each write to determine the number of decompressed bytes
* yielded. Used to implement {@link ResponseHandler#onReadDecoded(int)}.
* @param networkPeerManager A peer manager which is used to log internal errors to the
* Inspector console.
* @param responseHandler Special interface to intercept read events before they are sent
* to peers via {@link NetworkEventReporter} methods.
*/
public ResponseHandlingInputStream(
InputStream inputStream,
String requestId,
OutputStream outputStream,
@Nullable CountingOutputStream decompressedCounter,
ChromePeerManager networkPeerManager,
ResponseHandler responseHandler) {
super(inputStream);
mRequestId = requestId;
mOutputStream = outputStream;
mDecompressedCounter = decompressedCounter;
mNetworkPeerManager = networkPeerManager;
mResponseHandler = responseHandler;
mClosed = false;
}
private synchronized int checkEOF(int n) {
if (n == -1) {
closeOutputStreamQuietly();
mResponseHandler.onEOF();
mEofSeen = true;
}
return n;
}
@Override
public int read() throws IOException {
try {
int result = checkEOF(in.read());
if (result != -1) {
mResponseHandler.onRead(1);
writeToOutputStream(result);
}
return result;
} catch (IOException ex) {
throw handleIOException(ex);
}
}
@Override
public int read(byte[] b) throws IOException {
return this.read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
try {
int result = checkEOF(in.read(b, off, len));
if (result != -1) {
mResponseHandler.onRead(result);
writeToOutputStream(b, off, result);
}
return result;
} catch (IOException ex) {
throw handleIOException(ex);
}
}
@Override
public synchronized long skip(long n) throws IOException {
byte[] buffer = getSkipBufferLocked();
long total = 0;
while (total < n) {
long bytesDiff = n - total;
int bytesToRead = (int) Math.min((long) buffer.length, bytesDiff);
int result = this.read(buffer, 0, bytesToRead);
if (result == -1) {
break;
}
total += result;
}
return total;
}
@Nonnull
private byte[] getSkipBufferLocked() {
if (mSkipBuffer == null) {
mSkipBuffer = new byte[BUFFER_SIZE];
}
return mSkipBuffer;
}
@Override
public boolean markSupported() {
// this can be implemented, but isn't needed for TeedInputStream's behavior
return false;
}
@Override
public void mark(int readlimit) {
// noop -- mark is not supported
}
@Override
public void reset() throws IOException {
throw new UnsupportedOperationException("Mark not supported");
}
@Override
public void close() throws IOException {
try {
long bytesRead = 0;
if (!mEofSeen) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = this.read(buffer)) != -1) {
bytesRead += count;
}
}
if (bytesRead > 0) {
CLog.writeToConsole(
mNetworkPeerManager,
Console.MessageLevel.ERROR,
Console.MessageSource.NETWORK,
"There were " + String.valueOf(bytesRead) + " bytes that were not consumed while "
+ "processing request " + mRequestId);
}
} finally {
super.close();
closeOutputStreamQuietly();
}
}
/**
* Attempts to close all the output stream, and swallows any exceptions.
*/
private synchronized void closeOutputStreamQuietly() {
if (!mClosed) {
try {
mOutputStream.close();
reportDecodedSizeIfApplicable();
} catch (IOException e) {
CLog.writeToConsole(
mNetworkPeerManager,
Console.MessageLevel.ERROR,
Console.MessageSource.NETWORK,
"Could not close the output stream" + e);
} finally {
mClosed = true;
}
}
}
/**
* Handles reporting an {@link IOException}. We do this so we can centralize the logic while still
* maintaining the ability of the catch clause to throw.
* @param ex
* @return
*/
private IOException handleIOException(IOException ex) {
mResponseHandler.onError(ex);
return ex;
}
private void reportDecodedSizeIfApplicable() {
if (mDecompressedCounter != null) {
long currentCount = mDecompressedCounter.getCount();
int delta = (int)(currentCount - mLastDecompressedCount);
mResponseHandler.onReadDecoded(delta);
mLastDecompressedCount = currentCount;
}
}
/**
* Writes the byte to all the output streams. If we get an exception when writing to any
* of the streams, we close all the streams, and then propagate the first exception that
* occurred when writing.
*/
private synchronized void writeToOutputStream(int oneByte) {
if (mClosed) {
return;
}
try {
mOutputStream.write(oneByte);
reportDecodedSizeIfApplicable();
} catch (IOException e) {
handleIOExceptionWritingToStream(e);
}
}
/**
* Same as {@link #writeToOutputStream(int)}, but we write a buffer instead.
*/
private synchronized void writeToOutputStream(byte[] b, int offset, int count) {
if (mClosed) {
return;
}
try {
mOutputStream.write(b, offset, count);
reportDecodedSizeIfApplicable();
} catch (IOException e) {
handleIOExceptionWritingToStream(e);
}
}
private void handleIOExceptionWritingToStream(IOException e) {
CLog.writeToConsole(
mNetworkPeerManager,
Console.MessageLevel.ERROR,
Console.MessageSource.NETWORK,
"Could not write response body to the stream " + e);
closeOutputStreamQuietly();
}
}