mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-02-05 09:17:55 +08:00
Fix deadlock when recreating websocket connection
Summary: When you reload code while using the Chrome debugger we used to create a new websocket connection before closing the old one. This sometimes would cause a in-flight call to not get a response. This in turn would deadlock the JS thread because we try to shut it down before killing the websocket connection. This change instead makes sure to close the old connection before creating a new one. This is done by using a factory for creating the JavascriptExecutor so we can defer the creation until after the old Bridge has been torn down. public Reviewed By: astreet Differential Revision: D2735011 fb-gh-sync-id: 0ce0f35abaeef5457bad8d6b8d10122281192af4
This commit is contained in:
committed by
facebook-github-bot-5
parent
fc98956b65
commit
6d9096fb3f
@@ -108,8 +108,8 @@ import com.facebook.systrace.Systrace;
|
||||
new ReactInstanceDevCommandsHandler() {
|
||||
|
||||
@Override
|
||||
public void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
ReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutor);
|
||||
public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
|
||||
ReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutorFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,18 +132,18 @@ import com.facebook.systrace.Systrace;
|
||||
};
|
||||
|
||||
private class ReactContextInitParams {
|
||||
private final JavaScriptExecutor mJsExecutor;
|
||||
private final JavaScriptExecutor.Factory mJsExecutorFactory;
|
||||
private final JSBundleLoader mJsBundleLoader;
|
||||
|
||||
public ReactContextInitParams(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JavaScriptExecutor.Factory jsExecutorFactory,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
mJsExecutor = Assertions.assertNotNull(jsExecutor);
|
||||
mJsExecutorFactory = Assertions.assertNotNull(jsExecutorFactory);
|
||||
mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader);
|
||||
}
|
||||
|
||||
public JavaScriptExecutor getJsExecutor() {
|
||||
return mJsExecutor;
|
||||
public JavaScriptExecutor.Factory getJsExecutorFactory() {
|
||||
return mJsExecutorFactory;
|
||||
}
|
||||
|
||||
public JSBundleLoader getJsBundleLoader() {
|
||||
@@ -156,8 +156,7 @@ import com.facebook.systrace.Systrace;
|
||||
* be executing one at time, see {@link #recreateReactContextInBackground()}.
|
||||
*/
|
||||
private final class ReactContextInitAsyncTask extends
|
||||
AsyncTask<ReactContextInitParams, Void, ReactApplicationContext> {
|
||||
|
||||
AsyncTask<ReactContextInitParams, Void, Result<ReactApplicationContext>> {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (mCurrentReactContext != null) {
|
||||
@@ -167,15 +166,23 @@ import com.facebook.systrace.Systrace;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactApplicationContext doInBackground(ReactContextInitParams... params) {
|
||||
protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
|
||||
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
|
||||
return createReactContext(params[0].getJsExecutor(), params[0].getJsBundleLoader());
|
||||
try {
|
||||
JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
|
||||
return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));
|
||||
} catch (Exception e) {
|
||||
// Pass exception to onPostExecute() so it can be handled on the main thread
|
||||
return Result.of(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ReactApplicationContext reactContext) {
|
||||
protected void onPostExecute(Result<ReactApplicationContext> result) {
|
||||
try {
|
||||
setupReactContext(reactContext);
|
||||
setupReactContext(result.get());
|
||||
} catch (Exception e) {
|
||||
mDevSupportManager.handleException(e);
|
||||
} finally {
|
||||
mIsContextInitAsyncTaskRunning = false;
|
||||
}
|
||||
@@ -183,13 +190,46 @@ import com.facebook.systrace.Systrace;
|
||||
// Handle enqueued request to re-initialize react context.
|
||||
if (mPendingReactContextInitParams != null) {
|
||||
recreateReactContextInBackground(
|
||||
mPendingReactContextInitParams.getJsExecutor(),
|
||||
mPendingReactContextInitParams.getJsExecutorFactory(),
|
||||
mPendingReactContextInitParams.getJsBundleLoader());
|
||||
mPendingReactContextInitParams = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Result<T> {
|
||||
@Nullable private final T mResult;
|
||||
@Nullable private final Exception mException;
|
||||
|
||||
public static <T, U extends T> Result<T> of(U result) {
|
||||
return new Result<T>(result);
|
||||
}
|
||||
|
||||
public static <T> Result<T> of(Exception exception) {
|
||||
return new Result<>(exception);
|
||||
}
|
||||
|
||||
private Result(T result) {
|
||||
mException = null;
|
||||
mResult = result;
|
||||
}
|
||||
|
||||
private Result(Exception exception) {
|
||||
mException = exception;
|
||||
mResult = null;
|
||||
}
|
||||
|
||||
public T get() throws Exception {
|
||||
if (mException != null) {
|
||||
throw mException;
|
||||
}
|
||||
|
||||
Assertions.assertNotNull(mResult);
|
||||
|
||||
return mResult;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ ReactInstanceManagerImpl(
|
||||
Context applicationContext,
|
||||
@Nullable String jsBundleFile,
|
||||
@@ -311,7 +351,7 @@ import com.facebook.systrace.Systrace;
|
||||
|
||||
private void recreateReactContextInBackgroundFromBundleFile() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
new JSCJavaScriptExecutor.Factory(),
|
||||
JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile));
|
||||
}
|
||||
|
||||
@@ -504,9 +544,9 @@ import com.facebook.systrace.Systrace;
|
||||
return mCurrentReactContext;
|
||||
}
|
||||
|
||||
private void onReloadWithJSDebugger(JavaJSExecutor jsExecutor) {
|
||||
private void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
|
||||
recreateReactContextInBackground(
|
||||
new ProxyJavaScriptExecutor(jsExecutor),
|
||||
new ProxyJavaScriptExecutor.Factory(jsExecutorFactory),
|
||||
JSBundleLoader.createRemoteDebuggerBundleLoader(
|
||||
mDevSupportManager.getJSBundleURLForRemoteDebugging(),
|
||||
mDevSupportManager.getSourceUrl()));
|
||||
@@ -514,18 +554,19 @@ import com.facebook.systrace.Systrace;
|
||||
|
||||
private void onJSBundleLoadedFromServer() {
|
||||
recreateReactContextInBackground(
|
||||
new JSCJavaScriptExecutor(),
|
||||
new JSCJavaScriptExecutor.Factory(),
|
||||
JSBundleLoader.createCachedBundleFromNetworkLoader(
|
||||
mDevSupportManager.getSourceUrl(),
|
||||
mDevSupportManager.getDownloadedJSBundleFile()));
|
||||
}
|
||||
|
||||
private void recreateReactContextInBackground(
|
||||
JavaScriptExecutor jsExecutor,
|
||||
JavaScriptExecutor.Factory jsExecutorFactory,
|
||||
JSBundleLoader jsBundleLoader) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader);
|
||||
ReactContextInitParams initParams =
|
||||
new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
|
||||
if (!mIsContextInitAsyncTaskRunning) {
|
||||
// No background task to create react context is currently running, create and execute one.
|
||||
ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask();
|
||||
|
||||
@@ -14,6 +14,12 @@ import com.facebook.soloader.SoLoader;
|
||||
|
||||
@DoNotStrip
|
||||
public class JSCJavaScriptExecutor extends JavaScriptExecutor {
|
||||
public static class Factory implements JavaScriptExecutor.Factory {
|
||||
@Override
|
||||
public JavaScriptExecutor create() throws Exception {
|
||||
return new JSCJavaScriptExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
|
||||
|
||||
@@ -18,7 +18,11 @@ import com.facebook.proguard.annotations.DoNotStrip;
|
||||
*/
|
||||
@DoNotStrip
|
||||
public interface JavaJSExecutor {
|
||||
public static class ProxyExecutorException extends Exception {
|
||||
interface Factory {
|
||||
JavaJSExecutor create() throws Exception;
|
||||
}
|
||||
|
||||
class ProxyExecutorException extends Exception {
|
||||
public ProxyExecutorException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.facebook.proguard.annotations.DoNotStrip;
|
||||
|
||||
@DoNotStrip
|
||||
public abstract class JavaScriptExecutor extends Countable {
|
||||
public interface Factory {
|
||||
JavaScriptExecutor create() throws Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this executor and cleanup any resources that it was using. No further calls are
|
||||
|
||||
@@ -24,6 +24,18 @@ import com.facebook.proguard.annotations.DoNotStrip;
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class ProxyJavaScriptExecutor extends JavaScriptExecutor {
|
||||
public static class Factory implements JavaScriptExecutor.Factory {
|
||||
private final JavaJSExecutor.Factory mJavaJSExecutorFactory;
|
||||
|
||||
public Factory(JavaJSExecutor.Factory javaJSExecutorFactory) {
|
||||
mJavaJSExecutorFactory = javaJSExecutorFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaScriptExecutor create() throws Exception {
|
||||
return new ProxyJavaScriptExecutor(mJavaJSExecutorFactory.create());
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB);
|
||||
|
||||
@@ -13,10 +13,10 @@ import javax.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
|
||||
@@ -97,9 +97,13 @@ public class WebsocketJavaScriptExecutor implements JavaJSExecutor {
|
||||
String webSocketServerUrl,
|
||||
final JSExecutorConnectCallback callback) {
|
||||
final JSDebuggerWebSocketClient client = new JSDebuggerWebSocketClient();
|
||||
final Handler timeoutHandler = new Handler();
|
||||
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
|
||||
client.connect(
|
||||
webSocketServerUrl, new JSDebuggerWebSocketClient.JSDebuggerCallback() {
|
||||
// It's possible that both callbacks can fire on an error so make sure we only
|
||||
// dispatch results once to our callback.
|
||||
private boolean didSendResult = false;
|
||||
|
||||
@Override
|
||||
public void onSuccess(@Nullable String response) {
|
||||
client.prepareJSRuntime(
|
||||
@@ -108,20 +112,30 @@ public class WebsocketJavaScriptExecutor implements JavaJSExecutor {
|
||||
public void onSuccess(@Nullable String response) {
|
||||
timeoutHandler.removeCallbacksAndMessages(null);
|
||||
mWebSocketClient = client;
|
||||
callback.onSuccess();
|
||||
if (!didSendResult) {
|
||||
callback.onSuccess();
|
||||
didSendResult = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable cause) {
|
||||
timeoutHandler.removeCallbacksAndMessages(null);
|
||||
callback.onFailure(cause);
|
||||
if (!didSendResult) {
|
||||
callback.onFailure(cause);
|
||||
didSendResult = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable cause) {
|
||||
callback.onFailure(cause);
|
||||
timeoutHandler.removeCallbacksAndMessages(null);
|
||||
if (!didSendResult) {
|
||||
callback.onFailure(cause);
|
||||
didSendResult = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
timeoutHandler.postDelayed(
|
||||
|
||||
@@ -15,6 +15,9 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
@@ -35,14 +38,15 @@ import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.CatalystInstance;
|
||||
import com.facebook.react.bridge.JavaJSExecutor;
|
||||
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
|
||||
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WebsocketJavaScriptExecutor;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.ShakeDetector;
|
||||
import com.facebook.react.common.futures.SimpleSettableFuture;
|
||||
import com.facebook.react.devsupport.StackTraceHelper.StackFrame;
|
||||
import com.facebook.react.modules.debug.DeveloperSettings;
|
||||
|
||||
@@ -534,38 +538,47 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
|
||||
// anyway
|
||||
mDevServerHelper.launchChromeDevtools();
|
||||
|
||||
final WebsocketJavaScriptExecutor webSocketJSExecutor = new WebsocketJavaScriptExecutor();
|
||||
webSocketJSExecutor.connect(
|
||||
mDevServerHelper.getWebsocketProxyURL(),
|
||||
new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
progressDialog.dismiss();
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mReactInstanceCommandsHandler.onReloadWithJSDebugger(
|
||||
webSocketJSExecutor);
|
||||
}
|
||||
});
|
||||
}
|
||||
JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
|
||||
@Override
|
||||
public JavaJSExecutor create() throws Exception {
|
||||
WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor();
|
||||
SimpleSettableFuture<Boolean> future = new SimpleSettableFuture<>();
|
||||
executor.connect(
|
||||
mDevServerHelper.getWebsocketProxyURL(),
|
||||
getExecutorConnectCallback(progressDialog, future));
|
||||
// TODO(t9349129) Don't use timeout
|
||||
try {
|
||||
future.get(90, TimeUnit.SECONDS);
|
||||
return executor;
|
||||
} catch (ExecutionException e) {
|
||||
throw (Exception) e.getCause();
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
mReactInstanceCommandsHandler.onReloadWithJSDebugger(factory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable cause) {
|
||||
progressDialog.dismiss();
|
||||
FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause);
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
showNewJavaError(
|
||||
mApplicationContext.getString(R.string.catalyst_remotedbg_error),
|
||||
cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback(
|
||||
final ProgressDialog progressDialog,
|
||||
final SimpleSettableFuture<Boolean> future) {
|
||||
return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
future.set(true);
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable cause) {
|
||||
progressDialog.dismiss();
|
||||
FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause);
|
||||
future.setException(
|
||||
new IOException(
|
||||
mApplicationContext.getString(R.string.catalyst_remotedbg_error), cause));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void reloadJSFromServer(final ProgressDialog progressDialog) {
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface ReactInstanceDevCommandsHandler {
|
||||
/**
|
||||
* Request react instance recreation with JS debugging enabled.
|
||||
*/
|
||||
void onReloadWithJSDebugger(JavaJSExecutor proxyExecutor);
|
||||
void onReloadWithJSDebugger(JavaJSExecutor.Factory proxyExecutorFactory);
|
||||
|
||||
/**
|
||||
* Notify react instance manager about new JS bundle version downloaded from the server.
|
||||
|
||||
Reference in New Issue
Block a user