/* * 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.dumpapp; import com.facebook.stetho.common.LogUtil; import javax.annotation.Nullable; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.Charset; /** * Implements framing protocol that allows us to implement a command-line protocol via * sockets complete with stdin/stdout/stderr, exit codes, and input arguments. * <p /> * This is the server handler of that protocol, with the client handler in Stetho's {@code dumpapp} * script. * <p /> * The framing protocol involves 5-byte fixed headers, possibly followed by a variable * size content body. * The grammar is: * <pre> * CLIENT_FRAME = STDIN_FRAME | ENTER_FRAME * SERVER_FRAME = STDIN_REQUEST_FRAME | STDOUT_FRAME | STDERR_FRAME | EXIT_FRAME * STDIN_REQUEST_FRAME = '_' BIG_ENDIAN_INT * STDIN_FRAME = '-' BIG_ENDIAN_INT BLOB * STDOUT_FRAME = '1' BIG_ENDIAN_INT BLOB * STDERR_FRAME = '2' BIG_ENDIAN_INT BLOB * ENTER_FRAME = '!' BIG_ENDIAN_INT [ BIG_ENDIAN_SHORT STRING ]... * EXIT_FRAME = 'x' BIG_ENDIAN_INT * BIG_ENDIAN_SHORT = (2 bytes as written by {@link DataOutputStream#writeShort}) * BIG_ENDIAN_INT = (4 bytes as written by {@link DataOutputStream#writeInt}) * BLOB = (variable-size byte array) * STRING = (variable-size UTF8 string) * </pre> * The BIG_ENDIAN_INT in STDIN/STDOUT/STDERR_FRAME specifies the size (in bytes) of * the immediately following BLOB. For STDIN_REQUEST_FRAME it represents a request * for that much data. * <p /> * The BIG_ENDIAN_INT in ENTER_FRAME specifies the number of arguments, with that number of string * to follow. * <p /> * The BIG_ENDIAN_INT in EXIT_FRAME specifies the exit code. */ class Framer { private static final String TAG = "FramingSocket"; public static final byte STDIN_FRAME_PREFIX = '-'; public static final byte STDIN_REQUEST_FRAME_PREFIX = '_'; public static final byte STDOUT_FRAME_PREFIX = '1'; public static final byte STDERR_FRAME_PREFIX = '2'; public static final byte ENTER_FRAME_PREFIX = '!'; public static final byte EXIT_FRAME_PREFIX = 'x'; private final DataInputStream mInput; private final InputStream mStdin; private final PrintStream mStdout; private final PrintStream mStderr; private final DataOutputStream mMultiplexedOutputStream; public Framer(InputStream input, OutputStream output) throws IOException { mInput = new DataInputStream(input); mMultiplexedOutputStream = new DataOutputStream(output); mStdin = new FramingInputStream(); mStdout = new PrintStream( new BufferedOutputStream( new FramingOutputStream(STDOUT_FRAME_PREFIX))); mStderr = new PrintStream( new FramingOutputStream(STDERR_FRAME_PREFIX)); } public InputStream getStdin() { return mStdin; } public PrintStream getStdout() { return mStdout; } public PrintStream getStderr() { return mStderr; } public byte readFrameType() throws IOException { return mInput.readByte(); } public int readInt() throws IOException { return mInput.readInt(); } public String readString() throws IOException { int size = mInput.readUnsignedShort(); byte[] buf = new byte[size]; mInput.readFully(buf); return new String(buf, Charset.forName("UTF-8")); } public void writeExitCode(int exitCode) throws IOException { mStdout.flush(); mStderr.flush(); writeIntFrame(EXIT_FRAME_PREFIX, exitCode); } public void writeIntFrame(byte type, int intParameter) throws IOException { mMultiplexedOutputStream.write(type); mMultiplexedOutputStream.writeInt(intParameter); } public void writeBlob(byte[] data, int offset, int count) throws IOException { mMultiplexedOutputStream.write(data, offset, count); } private static <T extends Throwable> T handleSuppression(@Nullable T previous, T current) { if (previous == null) { return current; } else { LogUtil.i(TAG, current, "Suppressed while handling " + previous); return previous; } } private class FramingInputStream extends InputStream { private final ClosedHelper mClosedHelper = new ClosedHelper(); @Override public int read() throws IOException { byte[] buf = new byte[1]; if (read(buf) == 0) { return -1; } return buf[0]; } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { mClosedHelper.throwIfClosed(); synchronized (Framer.this) { // Ask the client for more data... writeIntFrame(STDIN_REQUEST_FRAME_PREFIX, byteCount); byte b = readFrameType(); if (b != STDIN_FRAME_PREFIX) { throw new UnexpectedFrameException(STDIN_FRAME_PREFIX, b); } // Read what they gave us... int length = readInt(); if (length > 0) { if (length > byteCount) { throw new DumpappFramingException( "Expected at most " + byteCount + " bytes, got: " + length); } mInput.readFully(buffer, byteOffset, length); } return length; } } @Override public long skip(long byteCount) throws IOException { long skipped = 0; int bufSize = (int)Math.min(byteCount, 2048); byte[] buf = new byte[bufSize]; synchronized (Framer.this) { while (skipped < byteCount) { int n = read(buf); if (n < 0) { break; } skipped += n; } } return skipped; } @Override public void close() throws IOException { mClosedHelper.close(); } } private class FramingOutputStream extends OutputStream { private final byte mPrefix; private final ClosedHelper mClosedHelper = new ClosedHelper(); public FramingOutputStream(byte prefix) { mPrefix = prefix; } @Override public void write(byte[] buffer, int offset, int length) throws IOException { mClosedHelper.throwIfClosed(); if (length > 0) { try { synchronized (Framer.this) { writeIntFrame(mPrefix, length); writeBlob(buffer, offset, length); mMultiplexedOutputStream.flush(); } } catch (IOException e) { // I/O error here can indicate the pipe is broken, so we need to prevent any // further writes. throw new DumpappOutputBrokenException(e); } } } @Override public void write(int oneByte) throws IOException { byte[] buffer = new byte[] { (byte)oneByte }; write(buffer, 0, buffer.length); } @Override public void write(byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } @Override public void close() throws IOException{ mClosedHelper.close(); } } private static class ClosedHelper { private volatile boolean mClosed; public void throwIfClosed() throws IOException { if (mClosed) { throw new IOException("Stream is closed"); } } public void close() { mClosed = true; } } }