/*
* 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.net.LocalServerSocket;
import android.net.LocalSocket;
import android.util.Log;
import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.common.Util;
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 LocalSocketServer {
private static final String WORKER_THREAD_NAME_PREFIX = "StethoWorker";
private static final int MAX_BIND_RETRIES = 2;
private static final int TIME_BETWEEN_BIND_RETRIES_MS = 1000;
private final String mFriendlyName;
private final String mAddress;
private final SocketHandler mSocketHandler;
private final AtomicInteger mThreadId = new AtomicInteger();
private Thread mListenerThread;
private boolean mStopped;
private LocalServerSocket mServerSocket;
/**
* @param friendlyName identifier to help debug this server, used for naming threads and such.
* @param address the local socket address to listen on.
* @param socketHandler functional handler once a socket is accepted.
*/
public LocalSocketServer(
String friendlyName,
String address,
SocketHandler socketHandler) {
mFriendlyName = Util.throwIfNull(friendlyName);
mAddress = Util.throwIfNull(address);
mSocketHandler = socketHandler;
}
public String getName() {
return mFriendlyName;
}
/**
* 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();
}
listenOnAddress(mAddress);
}
private void listenOnAddress(String address) throws IOException {
mServerSocket = bindToSocket(address);
LogUtil.i("Listening on @" + address);
while (!Thread.interrupted()) {
try {
// Use previously accepted socket the first time around, otherwise wait to
// accept another.
LocalSocket socket = mServerSocket.accept();
// Start worker thread
Thread t = new WorkerThread(socket, mSocketHandler);
t.setName(
WORKER_THREAD_NAME_PREFIX +
"-" + mFriendlyName +
"-" + mThreadId.incrementAndGet());
t.setDaemon(true);
t.start();
} catch (SocketException se) {
// ignore exception if interrupting the thread
if (Thread.interrupted()) {
break;
}
LogUtil.w(se, "I/O error");
} catch (InterruptedIOException ex) {
break;
} catch (IOException e) {
LogUtil.w(e, "I/O error initialising connection thread");
break;
}
}
LogUtil.i("Server shutdown on @" + address);
}
/**
* 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) {
// Don't care...
}
}
@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 LocalSocket mSocket;
private final SocketHandler mSocketHandler;
public WorkerThread(LocalSocket socket, SocketHandler socketHandler) {
mSocket = socket;
mSocketHandler = socketHandler;
}
@Override
public void run() {
try {
mSocketHandler.onAccepted(mSocket);
} catch (IOException ex) {
LogUtil.w("I/O error: %s", ex);
} finally {
try {
mSocket.close();
} catch (IOException ignore) {
}
}
}
}
}