Actually close packager websocket connection when destroying instance

Summary:
Ugh. We never actually closed the websocket connection. Furthermore, upon calling `closeQuietly()`, `onClose()` is called, which does `reconnect()`. Beautiful. This results in `ReactInstanceManager` leaking after calling `destroy()` and nullifying all references to it.

To fix this I made sure `closeQuietly()` actually closes the connection for good, **and** made sure we actually call it when destroying an instance.

Reviewed By: AaaChiuuu

Differential Revision: D3835023

fbshipit-source-id: 31811805dd97b725ea5887cffed9bed49addda83
This commit is contained in:
Felix Oghina 2016-09-09 04:17:19 -07:00 committed by Facebook Github Bot 7
parent 9289e4f7b2
commit 588f0b83e1
3 changed files with 64 additions and 27 deletions

View File

@ -79,7 +79,7 @@ public class DevServerHelper {
} }
public interface PackagerCommandListener { public interface PackagerCommandListener {
void onReload(); void onPackagerReloadCommand();
} }
public interface PackagerStatusCallback { public interface PackagerStatusCallback {
@ -88,15 +88,15 @@ public class DevServerHelper {
private final DevInternalSettings mSettings; private final DevInternalSettings mSettings;
private final OkHttpClient mClient; private final OkHttpClient mClient;
private final JSPackagerWebSocketClient mPackagerConnection;
private final Handler mRestartOnChangePollingHandler; private final Handler mRestartOnChangePollingHandler;
private boolean mOnChangePollingEnabled; private boolean mOnChangePollingEnabled;
private @Nullable JSPackagerWebSocketClient mPackagerConnection;
private @Nullable OkHttpClient mOnChangePollingClient; private @Nullable OkHttpClient mOnChangePollingClient;
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener; private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
private @Nullable Call mDownloadBundleFromURLCall; private @Nullable Call mDownloadBundleFromURLCall;
public DevServerHelper(DevInternalSettings settings, final PackagerCommandListener commandListener) { public DevServerHelper(DevInternalSettings settings) {
mSettings = settings; mSettings = settings;
mClient = new OkHttpClient.Builder() mClient = new OkHttpClient.Builder()
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) .connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
@ -105,18 +105,32 @@ public class DevServerHelper {
.build(); .build();
mRestartOnChangePollingHandler = new Handler(); mRestartOnChangePollingHandler = new Handler();
}
public void openPackagerConnection(final PackagerCommandListener commandListener) {
if (mPackagerConnection != null) {
FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.");
return;
}
mPackagerConnection = new JSPackagerWebSocketClient(getPackagerConnectionURL(), mPackagerConnection = new JSPackagerWebSocketClient(getPackagerConnectionURL(),
new JSPackagerWebSocketClient.JSPackagerCallback() { new JSPackagerWebSocketClient.JSPackagerCallback() {
@Override @Override
public void onMessage(String target, String action) { public void onMessage(String target, String action) {
if (commandListener != null && "bridge".equals(target) && "reload".equals(action)) { if (commandListener != null && "bridge".equals(target) && "reload".equals(action)) {
commandListener.onReload(); commandListener.onPackagerReloadCommand();
} }
} }
}); });
mPackagerConnection.connect(); mPackagerConnection.connect();
} }
public void closePackagerConnection() {
if (mPackagerConnection != null) {
mPackagerConnection.closeQuietly();
mPackagerConnection = null;
}
}
/** Intent action for reloading the JS */ /** Intent action for reloading the JS */
public static String getReloadAppAction(Context context) { public static String getReloadAppAction(Context context) {
return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX;

View File

@ -48,6 +48,7 @@ import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ShakeDetector; import com.facebook.react.common.ShakeDetector;
import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.react.common.futures.SimpleSettableFuture;
import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener;
import com.facebook.react.devsupport.StackTraceHelper.StackFrame; import com.facebook.react.devsupport.StackTraceHelper.StackFrame;
import com.facebook.react.modules.debug.DeveloperSettings; import com.facebook.react.modules.debug.DeveloperSettings;
@ -82,7 +83,7 @@ import okhttp3.RequestBody;
* {@code <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>} * {@code <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>}
* {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>} * {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>}
*/ */
public class DevSupportManagerImpl implements DevSupportManager { public class DevSupportManagerImpl implements DevSupportManager, PackagerCommandListener {
private static final int JAVA_ERROR_COOKIE = -1; private static final int JAVA_ERROR_COOKIE = -1;
private static final int JSEXCEPTION_ERROR_COOKIE = -1; private static final int JSEXCEPTION_ERROR_COOKIE = -1;
@ -120,7 +121,6 @@ public class DevSupportManagerImpl implements DevSupportManager {
private int mLastErrorCookie = 0; private int mLastErrorCookie = 0;
private @Nullable ErrorType mLastErrorType; private @Nullable ErrorType mLastErrorType;
private static class JscProfileTask extends AsyncTask<String, Void, Void> { private static class JscProfileTask extends AsyncTask<String, Void, Void> {
private static final MediaType JSON = private static final MediaType JSON =
MediaType.parse("application/json; charset=utf-8"); MediaType.parse("application/json; charset=utf-8");
@ -178,19 +178,7 @@ public class DevSupportManagerImpl implements DevSupportManager {
mApplicationContext = applicationContext; mApplicationContext = applicationContext;
mJSAppBundleName = packagerPathForJSBundleName; mJSAppBundleName = packagerPathForJSBundleName;
mDevSettings = new DevInternalSettings(applicationContext, this); mDevSettings = new DevInternalSettings(applicationContext, this);
mDevServerHelper = new DevServerHelper( mDevServerHelper = new DevServerHelper(mDevSettings);
mDevSettings,
new DevServerHelper.PackagerCommandListener() {
@Override
public void onReload() {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
handleReloadJS();
}
});
}
});
// Prepare shake gesture detector (will be started/stopped from #reload) // Prepare shake gesture detector (will be started/stopped from #reload)
mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() {
@ -237,8 +225,11 @@ public class DevSupportManagerImpl implements DevSupportManager {
if (e instanceof JSException) { if (e instanceof JSException) {
FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); FLog.e(ReactConstants.TAG, "Exception in native call from JS", e);
// TODO #11638796: convert the stack into something useful // TODO #11638796: convert the stack into something useful
showNewError(e.getMessage() + "\n\n" + ((JSException) e).getStack(), new StackFrame[] {}, showNewError(
JSEXCEPTION_ERROR_COOKIE, ErrorType.JS); e.getMessage() + "\n\n" + ((JSException) e).getStack(),
new StackFrame[] {},
JSEXCEPTION_ERROR_COOKIE,
ErrorType.JS);
} else { } else {
showNewJavaError(e.getMessage(), e); showNewJavaError(e.getMessage(), e);
} }
@ -388,7 +379,7 @@ public class DevSupportManagerImpl implements DevSupportManager {
} }
}); });
options.put( options.put(
mApplicationContext.getString(R.string.catalyst_element_inspector), mApplicationContext.getString(R.string.catalyst_element_inspector),
new DevOptionHandler() { new DevOptionHandler() {
@Override @Override
public void onOptionSelected() { public void onOptionSelected() {
@ -674,6 +665,16 @@ public class DevSupportManagerImpl implements DevSupportManager {
return mLastErrorStack; return mLastErrorStack;
} }
@Override
public void onPackagerReloadCommand() {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
handleReloadJS();
}
});
}
private void updateLastErrorInfo( private void updateLastErrorInfo(
final String message, final String message,
final StackFrame[] stack, final StackFrame[] stack,
@ -802,6 +803,7 @@ public class DevSupportManagerImpl implements DevSupportManager {
mIsReceiverRegistered = true; mIsReceiverRegistered = true;
} }
mDevServerHelper.openPackagerConnection(this);
if (mDevSettings.isReloadOnJSChangeEnabled()) { if (mDevSettings.isReloadOnJSChangeEnabled()) {
mDevServerHelper.startPollingOnChangeEndpoint( mDevServerHelper.startPollingOnChangeEndpoint(
new DevServerHelper.OnServerContentChangeListener() { new DevServerHelper.OnServerContentChangeListener() {
@ -841,6 +843,7 @@ public class DevSupportManagerImpl implements DevSupportManager {
mDevOptionsDialog.dismiss(); mDevOptionsDialog.dismiss();
} }
mDevServerHelper.closePackagerConnection();
mDevServerHelper.stopPollingOnChangeEndpoint(); mDevServerHelper.stopPollingOnChangeEndpoint();
} }
} }

View File

@ -40,6 +40,7 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
private final String mUrl; private final String mUrl;
private final Handler mHandler; private final Handler mHandler;
private boolean mClosed = false;
private boolean mSuppressConnectionErrors; private boolean mSuppressConnectionErrors;
public interface JSPackagerCallback { public interface JSPackagerCallback {
@ -57,6 +58,9 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
} }
public void connect() { public void connect() {
if (mClosed) {
throw new IllegalStateException("Can't connect closed client");
}
OkHttpClient httpClient = new OkHttpClient.Builder() OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS)
@ -69,6 +73,9 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
} }
private void reconnect() { private void reconnect() {
if (mClosed) {
throw new IllegalStateException("Can't reconnect closed client");
}
if (!mSuppressConnectionErrors) { if (!mSuppressConnectionErrors) {
FLog.w(TAG, "Couldn't connect to packager, will silently retry"); FLog.w(TAG, "Couldn't connect to packager, will silently retry");
mSuppressConnectionErrors = true; mSuppressConnectionErrors = true;
@ -77,12 +84,21 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
connect(); // check that we haven't been closed in the meantime
if (!mClosed) {
connect();
}
} }
}, RECONNECT_DELAY_MS); },
RECONNECT_DELAY_MS);
} }
public void closeQuietly() { public void closeQuietly() {
mClosed = true;
closeWebSocketQuietly();
}
private void closeWebSocketQuietly() {
if (mWebSocket != null) { if (mWebSocket != null) {
try { try {
mWebSocket.close(1000, "End of session"); mWebSocket.close(1000, "End of session");
@ -151,7 +167,9 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
if (mWebSocket != null) { if (mWebSocket != null) {
abort("Websocket exception", e); abort("Websocket exception", e);
} }
reconnect(); if (!mClosed) {
reconnect();
}
} }
@Override @Override
@ -163,7 +181,9 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
@Override @Override
public void onClose(int code, String reason) { public void onClose(int code, String reason) {
mWebSocket = null; mWebSocket = null;
reconnect(); if (!mClosed) {
reconnect();
}
} }
@Override @Override
@ -173,6 +193,6 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
private void abort(String message, Throwable cause) { private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause); FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeQuietly(); closeWebSocketQuietly();
} }
} }