com.facebook.stetho.server.LocalSocketHttpServer.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.stetho.server.LocalSocketHttpServer.java

Source

/*
 * 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.server;

import android.annotation.SuppressLint;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.util.Log;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.common.ProcessUtil;
import com.facebook.stetho.common.Util;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.HttpServerConnection;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.*;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.BindException;
import java.net.SocketException;
import java.util.concurrent.atomic.AtomicInteger;

public class LocalSocketHttpServer {

    private static final String WORKDER_THREAD_NAME_PREFIX = "StethoWorker";
    private static final int MAX_BIND_RETRIES = 2;
    private static final int TIME_BETWEEN_BIND_RETRIES_MS = 1000;
    private static final String SOCKET_NAME_PREFIX = "stetho_";

    /**
     * Convince {@code chrome://inspect/devices} that we're "one of them" :)
     */
    private static final String SOCKET_NAME_SUFFIX = "_devtools_remote";

    private static final AtomicInteger sThreadId = new AtomicInteger();

    private final RegistryInitializer mRegistryInitializer;
    private final String mAddress;
    private Thread mListenerThread;
    private boolean mStopped;
    private LocalServerSocket mServerSocket;

    /**
     * @param registryInitializer lazy initializer for the {@link HttpRequestHandlerRegistry}.
     *     This is only initialized after the first socket has connected, and this determines
     *     what handlers this server uses to process requests.
     */
    public LocalSocketHttpServer(RegistryInitializer registryInitializer) {
        this(registryInitializer, null /* address */);
    }

    /**
     * @param registryInitializer lazy initializer for the {@link HttpRequestHandlerRegistry}.
     *     This is only initialized after the first socket has connected, and this determines
     *     what handlers this server uses to process requests.
     * @param address the local socket address to listen on.
     */
    public LocalSocketHttpServer(RegistryInitializer registryInitializer, String address) {
        mRegistryInitializer = Util.throwIfNull(registryInitializer);
        mAddress = address;
    }

    /**
     * Binds to the address and listens for connections.
     * <p/>
     * If successful, this thread blocks forever or until {@link #stop} is called, whichever
     * happens first.
     *
     * @throws IOException Thrown on failure to bind the socket.
     */
    public void run() throws IOException {
        synchronized (this) {
            if (mStopped) {
                return;
            }
            mListenerThread = Thread.currentThread();
        }

        String address = (mAddress != null) ? mAddress : getDefaultAddress();
        listenOnAddress(address);
    }

    private void listenOnAddress(String address) throws IOException {
        mServerSocket = bindToSocket(address);
        LogUtil.i("Listening on @" + address);

        HttpParams params = null;
        HttpService service = null;

        while (!Thread.interrupted()) {
            LocalSocketHttpServerConnection connection = new LocalSocketHttpServerConnection();
            try {
                // Use previously accepted socket the first time around, otherwise wait to
                // accept another.
                LocalSocket socket = mServerSocket.accept();

                if (params == null) {
                    params = createParams();
                }
                if (service == null) {
                    service = createService(params);
                }
                connection.bind(socket, params);

                // Start worker thread
                Thread t = new WorkerThread(service, connection);
                t.setDaemon(true);
                t.start();
            } catch (SocketException se) {
                // ignore exception if interrupting the thread
                if (!Thread.interrupted()) {
                    LogUtil.w(se, "I/O error");
                }
            } catch (InterruptedIOException ex) {
                break;
            } catch (IOException e) {
                LogUtil.w(e, "I/O error initialising connection thread");
                break;
            }
        }
    }

    private static String getDefaultAddress() throws IOException {
        return SOCKET_NAME_PREFIX + ProcessUtil.getProcessName() + SOCKET_NAME_SUFFIX;
    }

    private HttpParams createParams() {
        return new BasicHttpParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
                .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
                .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
                .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
                .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "Stetho")
                .setParameter(CoreProtocolPNames.PROTOCOL_VERSION, "HTTP/1.1");
    }

    private HttpService createService(HttpParams params) {
        HttpRequestHandlerRegistry registry = mRegistryInitializer.getRegistry();

        BasicHttpProcessor httpproc = new BasicHttpProcessor();
        httpproc.addInterceptor(new ResponseDate());
        httpproc.addInterceptor(new ResponseServer());
        httpproc.addInterceptor(new ResponseContent());
        httpproc.addInterceptor(new ResponseConnControl());

        HttpService service = new HttpService(httpproc, new DefaultConnectionReuseStrategy(),
                new DefaultHttpResponseFactory());
        service.setParams(params);
        service.setHandlerResolver(registry);

        return service;
    }

    /**
     * Stops the listener thread and unbinds the address.
     */
    public void stop() {
        synchronized (this) {
            mStopped = true;
            if (mListenerThread == null) {
                return;
            }
        }

        mListenerThread.interrupt();
        try {
            if (mServerSocket != null) {
                mServerSocket.close();
            }
        } catch (IOException e) {
        }
    }

    @Nonnull
    private static LocalServerSocket bindToSocket(String address) throws IOException {
        int retries = MAX_BIND_RETRIES;
        IOException firstException = null;
        do {
            try {
                if (LogUtil.isLoggable(Log.DEBUG)) {
                    LogUtil.d("Trying to bind to @" + address);
                }
                return new LocalServerSocket(address);
            } catch (BindException be) {
                LogUtil.w(be, "Binding error, sleep " + TIME_BETWEEN_BIND_RETRIES_MS + " ms...");
                if (firstException == null) {
                    firstException = be;
                }
                Util.sleepUninterruptibly(TIME_BETWEEN_BIND_RETRIES_MS);
            }
        } while (retries-- > 0);

        throw firstException;
    }

    private static class WorkerThread extends Thread {

        private final HttpService httpservice;
        private final HttpServerConnection conn;

        public WorkerThread(final HttpService httpservice, final HttpServerConnection conn) {
            super(WORKDER_THREAD_NAME_PREFIX + sThreadId.incrementAndGet());
            this.httpservice = httpservice;
            this.conn = conn;
        }

        @Override
        @SuppressLint("LogMethodNoExceptionInCatch")
        public void run() {
            HttpContext context = new BasicHttpContext(null);
            try {
                if (!Thread.interrupted() && conn.isOpen()) {
                    httpservice.handleRequest(conn, context);
                }
            } catch (ConnectionClosedException ex) {
                LogUtil.w("Client closed connection: %s", ex);
            } catch (IOException ex) {
                LogUtil.w("I/O error: %s", ex);
            } catch (HttpException ex) {
                LogUtil.w("Unrecoverable HTTP protocol violation: %s", ex);
            } finally {
                try {
                    conn.close();
                } catch (IOException ignore) {
                }
            }
        }
    }
}