mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Migrate to Kotlin - DevServerHelper (#50558)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50558 This diff migrates the following file to Kotlin - DevServerHelper as part of our ongoing effort of migrating the codebase to Kotlin Changelog: [Internal] [Changed] - DevServerHelper to Kotlin. Reviewed By: javache Differential Revision: D72632266 fbshipit-source-id: 5dab7206dcf11de042d9e274e33a38a4491f1d17
This commit is contained in:
committed by
Facebook GitHub Bot
parent
be46391654
commit
dd0cd07405
@@ -1967,28 +1967,28 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
|
||||
public final fun setDevLoadingEnabled (Z)V
|
||||
}
|
||||
|
||||
public class com/facebook/react/devsupport/DevServerHelper {
|
||||
public final class com/facebook/react/devsupport/DevServerHelper {
|
||||
public fun <init> (Lcom/facebook/react/modules/debug/interfaces/DeveloperSettings;Landroid/content/Context;Lcom/facebook/react/packagerconnection/PackagerConnectionSettings;)V
|
||||
public fun closeInspectorConnection ()V
|
||||
public fun closePackagerConnection ()V
|
||||
public fun disableDebugger ()V
|
||||
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
|
||||
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
|
||||
public fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
|
||||
public fun getDevServerBundleURL (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun getDevServerSplitBundleURL (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun getSourceMapUrl (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun getSourceUrl (Ljava/lang/String;)Ljava/lang/String;
|
||||
public fun getWebsocketProxyURL ()Ljava/lang/String;
|
||||
public fun isPackagerRunning (Lcom/facebook/react/devsupport/interfaces/PackagerStatusCallback;)V
|
||||
public fun openDebugger (Lcom/facebook/react/bridge/ReactContext;Ljava/lang/String;)V
|
||||
public fun openInspectorConnection ()V
|
||||
public fun openPackagerConnection (Ljava/lang/String;Lcom/facebook/react/devsupport/DevServerHelper$PackagerCommandListener;)V
|
||||
public final fun closeInspectorConnection ()V
|
||||
public final fun closePackagerConnection ()V
|
||||
public final fun disableDebugger ()V
|
||||
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
|
||||
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
|
||||
public static synthetic fun downloadBundleFromURL$default (Lcom/facebook/react/devsupport/DevServerHelper;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;ILjava/lang/Object;)V
|
||||
public final fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
|
||||
public final fun getDevServerBundleURL (Ljava/lang/String;)Ljava/lang/String;
|
||||
public final fun getDevServerSplitBundleURL (Ljava/lang/String;)Ljava/lang/String;
|
||||
public final fun getSourceMapUrl (Ljava/lang/String;)Ljava/lang/String;
|
||||
public final fun getSourceUrl (Ljava/lang/String;)Ljava/lang/String;
|
||||
public final fun getWebsocketProxyURL ()Ljava/lang/String;
|
||||
public final fun isPackagerRunning (Lcom/facebook/react/devsupport/interfaces/PackagerStatusCallback;)V
|
||||
public final fun openDebugger (Lcom/facebook/react/bridge/ReactContext;Ljava/lang/String;)V
|
||||
public final fun openInspectorConnection ()V
|
||||
public final fun openPackagerConnection (Ljava/lang/String;Lcom/facebook/react/devsupport/DevServerHelper$PackagerCommandListener;)V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/devsupport/DevServerHelper$PackagerCommandListener {
|
||||
public abstract fun customCommandHandlers ()Ljava/util/Map;
|
||||
public fun onCaptureHeapCommand (Lcom/facebook/react/packagerconnection/Responder;)V
|
||||
public abstract fun onPackagerConnected ()V
|
||||
public abstract fun onPackagerDevMenuCommand ()V
|
||||
public abstract fun onPackagerDisconnected ()V
|
||||
|
||||
-505
@@ -1,505 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.Settings.Secure;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
|
||||
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
|
||||
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
|
||||
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers;
|
||||
import com.facebook.react.packagerconnection.FileIoHandler;
|
||||
import com.facebook.react.packagerconnection.JSPackagerClient;
|
||||
import com.facebook.react.packagerconnection.NotificationOnlyHandler;
|
||||
import com.facebook.react.packagerconnection.PackagerConnectionSettings;
|
||||
import com.facebook.react.packagerconnection.ReconnectingWebSocket.ConnectionCallback;
|
||||
import com.facebook.react.packagerconnection.RequestHandler;
|
||||
import com.facebook.react.packagerconnection.Responder;
|
||||
import com.facebook.react.util.RNLog;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
|
||||
/**
|
||||
* Helper class for all things about the debug server running in the engineer's host machine.
|
||||
*
|
||||
* <p>One can use 'debug_http_host' shared preferences key to provide a host name for the debug
|
||||
* server. If the setting is empty we support and detect two basic configuration that works well for
|
||||
* android emulators connection to debug server running on emulator's host:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Android stock emulator with standard non-configurable local loopback alias: 10.0.2.2
|
||||
* <li>Genymotion emulator with default settings: 10.0.3.2
|
||||
* </ul>
|
||||
*/
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class DevServerHelper {
|
||||
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
|
||||
|
||||
private static final String DEBUGGER_MSG_DISABLE = "{ \"id\":1,\"method\":\"Debugger.disable\" }";
|
||||
|
||||
public interface PackagerCommandListener {
|
||||
void onPackagerConnected();
|
||||
|
||||
void onPackagerDisconnected();
|
||||
|
||||
void onPackagerReloadCommand();
|
||||
|
||||
void onPackagerDevMenuCommand();
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
default void onCaptureHeapCommand(final Responder responder) {}
|
||||
|
||||
// Allow apps to provide listeners for custom packager commands.
|
||||
@Nullable
|
||||
Map<String, RequestHandler> customCommandHandlers();
|
||||
}
|
||||
|
||||
private enum BundleType {
|
||||
BUNDLE("bundle"),
|
||||
MAP("map");
|
||||
|
||||
private final String mTypeID;
|
||||
|
||||
BundleType(String typeID) {
|
||||
mTypeID = typeID;
|
||||
}
|
||||
|
||||
public String typeID() {
|
||||
return mTypeID;
|
||||
}
|
||||
}
|
||||
|
||||
private final DeveloperSettings mSettings;
|
||||
|
||||
private final PackagerConnectionSettings mPackagerConnectionSettings;
|
||||
|
||||
private final OkHttpClient mClient;
|
||||
private final BundleDownloader mBundleDownloader;
|
||||
private final PackagerStatusCheck mPackagerStatusCheck;
|
||||
private final Context mApplicationContext;
|
||||
private final String mPackageName;
|
||||
|
||||
private @Nullable JSPackagerClient mPackagerClient;
|
||||
private @Nullable IInspectorPackagerConnection mInspectorPackagerConnection;
|
||||
|
||||
public DevServerHelper(
|
||||
DeveloperSettings developerSettings,
|
||||
Context applicationContext,
|
||||
PackagerConnectionSettings packagerConnectionSettings) {
|
||||
mSettings = developerSettings;
|
||||
mPackagerConnectionSettings = packagerConnectionSettings;
|
||||
mClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
mBundleDownloader = new BundleDownloader(mClient);
|
||||
mPackagerStatusCheck = new PackagerStatusCheck(mClient);
|
||||
mApplicationContext = applicationContext;
|
||||
mPackageName = applicationContext.getPackageName();
|
||||
}
|
||||
|
||||
public void openPackagerConnection(
|
||||
final String clientId, final PackagerCommandListener commandListener) {
|
||||
if (mPackagerClient != null) {
|
||||
FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.");
|
||||
return;
|
||||
}
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... backgroundParams) {
|
||||
Map<String, RequestHandler> handlers = new HashMap<>();
|
||||
handlers.put(
|
||||
"reload",
|
||||
new NotificationOnlyHandler() {
|
||||
@Override
|
||||
public void onNotification(@Nullable Object params) {
|
||||
commandListener.onPackagerReloadCommand();
|
||||
}
|
||||
});
|
||||
handlers.put(
|
||||
"devMenu",
|
||||
new NotificationOnlyHandler() {
|
||||
@Override
|
||||
public void onNotification(@Nullable Object params) {
|
||||
commandListener.onPackagerDevMenuCommand();
|
||||
}
|
||||
});
|
||||
Map<String, RequestHandler> customHandlers = commandListener.customCommandHandlers();
|
||||
if (customHandlers != null) {
|
||||
handlers.putAll(customHandlers);
|
||||
}
|
||||
handlers.putAll(new FileIoHandler().handlers());
|
||||
|
||||
ConnectionCallback onPackagerConnectedCallback =
|
||||
new ConnectionCallback() {
|
||||
@Override
|
||||
public void onConnected() {
|
||||
commandListener.onPackagerConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
commandListener.onPackagerDisconnected();
|
||||
}
|
||||
};
|
||||
|
||||
mPackagerClient =
|
||||
new JSPackagerClient(
|
||||
clientId, mPackagerConnectionSettings, handlers, onPackagerConnectedCallback);
|
||||
mPackagerClient.init();
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void closePackagerConnection() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (mPackagerClient != null) {
|
||||
mPackagerClient.close();
|
||||
mPackagerClient = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void openInspectorConnection() {
|
||||
if (mInspectorPackagerConnection != null) {
|
||||
FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.");
|
||||
return;
|
||||
}
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
Map<String, String> metadata =
|
||||
AndroidInfoHelpers.getInspectorHostMetadata(mApplicationContext);
|
||||
String deviceName = metadata.get("deviceName");
|
||||
if (deviceName == null) {
|
||||
FLog.w(ReactConstants.TAG, "Could not get device name from Inspector Host Metadata.");
|
||||
return null;
|
||||
}
|
||||
mInspectorPackagerConnection =
|
||||
new CxxInspectorPackagerConnection(getInspectorDeviceUrl(), deviceName, mPackageName);
|
||||
mInspectorPackagerConnection.connect();
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void disableDebugger() {
|
||||
if (mInspectorPackagerConnection != null) {
|
||||
mInspectorPackagerConnection.sendEventToAllConnections(DEBUGGER_MSG_DISABLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void closeInspectorConnection() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (mInspectorPackagerConnection != null) {
|
||||
mInspectorPackagerConnection.closeQuietly();
|
||||
mInspectorPackagerConnection = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public String getWebsocketProxyURL() {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"ws://%s/debugger-proxy?role=client",
|
||||
mPackagerConnectionSettings.getDebugServerHost());
|
||||
}
|
||||
|
||||
private static String getSHA256(String string) {
|
||||
MessageDigest digest = null;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("Could not get standard SHA-256 algorithm", e);
|
||||
}
|
||||
digest.reset();
|
||||
byte[] result;
|
||||
try {
|
||||
result = digest.digest(string.getBytes("UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("This environment doesn't support UTF-8 encoding", e);
|
||||
}
|
||||
return String.format(
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
result[0],
|
||||
result[1],
|
||||
result[2],
|
||||
result[3],
|
||||
result[4],
|
||||
result[5],
|
||||
result[6],
|
||||
result[7],
|
||||
result[8],
|
||||
result[9],
|
||||
result[10],
|
||||
result[11],
|
||||
result[12],
|
||||
result[13],
|
||||
result[14],
|
||||
result[15],
|
||||
result[16],
|
||||
result[17],
|
||||
result[18],
|
||||
result[19]);
|
||||
}
|
||||
|
||||
// Returns an opaque ID which is stable for the current combination of device and app, stable
|
||||
// across installs, and unique across devices.
|
||||
private String getInspectorDeviceId() {
|
||||
// Every Android app has a unique application ID that looks like a Java or Kotlin package name,
|
||||
// such as com.example.myapp. This ID uniquely identifies your app on the device and in the
|
||||
// Google Play Store.
|
||||
// [Source: Android docs]
|
||||
String packageName = mPackageName;
|
||||
|
||||
// A 64-bit number expressed as a hexadecimal string, which is either:
|
||||
// * unique to each combination of app-signing key, user, and device (API level >= 26), or
|
||||
// * randomly generated when the user first sets up the device and should remain constant for
|
||||
// the lifetime of the user's device (API level < 26).
|
||||
// [Source: Android docs]
|
||||
String androidId =
|
||||
Secure.getString(mApplicationContext.getContentResolver(), Secure.ANDROID_ID);
|
||||
|
||||
String rawDeviceId =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"android-%s-%s-%s",
|
||||
packageName,
|
||||
androidId,
|
||||
InspectorFlags.getFuseboxEnabled() ? "fusebox" : "legacy");
|
||||
|
||||
return getSHA256(rawDeviceId);
|
||||
}
|
||||
|
||||
private String getInspectorDeviceUrl() {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"http://%s/inspector/device?name=%s&app=%s&device=%s&profiling=%b",
|
||||
mPackagerConnectionSettings.getDebugServerHost(),
|
||||
Uri.encode(AndroidInfoHelpers.getFriendlyDeviceName()),
|
||||
Uri.encode(mPackageName),
|
||||
Uri.encode(getInspectorDeviceId()),
|
||||
InspectorFlags.getIsProfilingBuild());
|
||||
}
|
||||
|
||||
public void downloadBundleFromURL(
|
||||
DevBundleDownloadListener callback,
|
||||
File outputFile,
|
||||
String bundleURL,
|
||||
@Nullable BundleDownloader.BundleInfo bundleInfo) {
|
||||
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo);
|
||||
}
|
||||
|
||||
public void downloadBundleFromURL(
|
||||
DevBundleDownloadListener callback,
|
||||
File outputFile,
|
||||
String bundleURL,
|
||||
@Nullable BundleDownloader.BundleInfo bundleInfo,
|
||||
Request.Builder requestBuilder) {
|
||||
mBundleDownloader.downloadBundleFromURL(
|
||||
callback, outputFile, bundleURL, bundleInfo, requestBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the host to use when connecting to the bundle server from the host itself.
|
||||
*/
|
||||
private String getHostForJSProxy() {
|
||||
// Use custom port if configured. Note that host stays "localhost".
|
||||
String host = Assertions.assertNotNull(mPackagerConnectionSettings.getDebugServerHost());
|
||||
int portOffset = host.lastIndexOf(':');
|
||||
if (portOffset > -1) {
|
||||
return "localhost" + host.substring(portOffset);
|
||||
} else {
|
||||
return AndroidInfoHelpers.DEVICE_LOCALHOST;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether we should enable dev mode when requesting JS bundles.
|
||||
*/
|
||||
private boolean getDevMode() {
|
||||
return mSettings.isJSDevModeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether we should request minified JS bundles.
|
||||
*/
|
||||
private boolean getJSMinifyMode() {
|
||||
return mSettings.isJSMinifyEnabled();
|
||||
}
|
||||
|
||||
private String createBundleURL(String mainModuleID, BundleType type, String host) {
|
||||
return createBundleURL(mainModuleID, type, host, false, true);
|
||||
}
|
||||
|
||||
private String createSplitBundleURL(String mainModuleID, String host) {
|
||||
return createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false);
|
||||
}
|
||||
|
||||
private String createBundleURL(
|
||||
String mainModuleID, BundleType type, String host, boolean modulesOnly, boolean runModule) {
|
||||
boolean dev = getDevMode();
|
||||
StringBuilder additionalOptionsBuilder = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry :
|
||||
mPackagerConnectionSettings.getAdditionalOptionsForPackager().entrySet()) {
|
||||
if (entry.getValue().length() == 0) {
|
||||
continue;
|
||||
}
|
||||
additionalOptionsBuilder.append("&" + entry.getKey() + "=" + Uri.encode(entry.getValue()));
|
||||
}
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"http://%s/%s.%s?platform=android&dev=%s&lazy=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s",
|
||||
host,
|
||||
mainModuleID,
|
||||
type.typeID(),
|
||||
dev, // dev
|
||||
dev, // lazy
|
||||
getJSMinifyMode(),
|
||||
mPackageName,
|
||||
modulesOnly ? "true" : "false",
|
||||
runModule ? "true" : "false")
|
||||
+ (InspectorFlags.getFuseboxEnabled() ? "&excludeSource=true&sourcePaths=url-server" : "")
|
||||
+ additionalOptionsBuilder.toString();
|
||||
}
|
||||
|
||||
private String createBundleURL(String mainModuleID, BundleType type) {
|
||||
return createBundleURL(mainModuleID, type, mPackagerConnectionSettings.getDebugServerHost());
|
||||
}
|
||||
|
||||
private static String createResourceURL(String host, String resourcePath) {
|
||||
// This is what we get for not using a proper URI library.
|
||||
if (resourcePath.startsWith("/")) {
|
||||
FLog.w(ReactConstants.TAG, "Resource path should not begin with `/`, removing it.");
|
||||
resourcePath = resourcePath.substring(1);
|
||||
}
|
||||
return String.format(Locale.US, "http://%s/%s", host, resourcePath);
|
||||
}
|
||||
|
||||
public String getDevServerBundleURL(final String jsModulePath) {
|
||||
return createBundleURL(
|
||||
jsModulePath, BundleType.BUNDLE, mPackagerConnectionSettings.getDebugServerHost());
|
||||
}
|
||||
|
||||
public String getDevServerSplitBundleURL(String jsModulePath) {
|
||||
return createSplitBundleURL(jsModulePath, mPackagerConnectionSettings.getDebugServerHost());
|
||||
}
|
||||
|
||||
public void isPackagerRunning(final PackagerStatusCallback callback) {
|
||||
String host = mPackagerConnectionSettings.getDebugServerHost();
|
||||
if (host == null) {
|
||||
FLog.w(ReactConstants.TAG, "No packager host configured.");
|
||||
callback.onPackagerStatusFetched(false);
|
||||
} else {
|
||||
mPackagerStatusCheck.run(host, callback);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSourceMapUrl(String mainModuleName) {
|
||||
return createBundleURL(mainModuleName, BundleType.MAP);
|
||||
}
|
||||
|
||||
public String getSourceUrl(String mainModuleName) {
|
||||
return createBundleURL(mainModuleName, BundleType.BUNDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a debug-only utility to allow fetching a file via packager. It's made synchronous for
|
||||
* simplicity, but should only be used if it's absolutely necessary.
|
||||
*
|
||||
* @return the file with the fetched content, or null if there's any failure.
|
||||
*/
|
||||
public @Nullable File downloadBundleResourceFromUrlSync(
|
||||
final String resourcePath, final File outputFile) {
|
||||
final String resourceURL =
|
||||
createResourceURL(mPackagerConnectionSettings.getDebugServerHost(), resourcePath);
|
||||
final Request request = new Request.Builder().url(resourceURL).build();
|
||||
|
||||
try (Response response = mClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (Sink output = Okio.sink(outputFile)) {
|
||||
Okio.buffer(response.body().source()).readAll(output);
|
||||
}
|
||||
|
||||
return outputFile;
|
||||
} catch (Exception ex) {
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Failed to fetch resource synchronously - resourcePath: \"%s\", outputFile: \"%s\"",
|
||||
resourcePath,
|
||||
outputFile.getAbsolutePath(),
|
||||
ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt to open the JS debugger on the host machine (on-device CDP debugging). */
|
||||
public void openDebugger(@Nullable final ReactContext context, final String errorMessage) {
|
||||
// TODO(huntie): Requests to dev server should not assume 'http' URL scheme
|
||||
String requestUrl =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"http://%s/open-debugger?device=%s",
|
||||
mPackagerConnectionSettings.getDebugServerHost(),
|
||||
Uri.encode(getInspectorDeviceId()));
|
||||
Request request =
|
||||
new Request.Builder().url(requestUrl).method("POST", RequestBody.create(null, "")).build();
|
||||
|
||||
mClient
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
new Callback() {
|
||||
@Override
|
||||
public void onFailure(@NonNull Call _call, @NonNull IOException _e) {
|
||||
RNLog.w(context, errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call _call, @NonNull Response _response) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// Conflicting okhttp versions + This class needs to be rewritten to don't
|
||||
// use AsyncTasks
|
||||
@file:Suppress("DEPRECATION", "DEPRECATION_ERROR")
|
||||
|
||||
package com.facebook.react.devsupport
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.provider.Settings.Secure
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled
|
||||
import com.facebook.react.devsupport.InspectorFlags.getIsProfilingBuild
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
|
||||
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback
|
||||
import com.facebook.react.modules.debug.interfaces.DeveloperSettings
|
||||
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers.getFriendlyDeviceName
|
||||
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers.getInspectorHostMetadata
|
||||
import com.facebook.react.packagerconnection.FileIoHandler
|
||||
import com.facebook.react.packagerconnection.JSPackagerClient
|
||||
import com.facebook.react.packagerconnection.NotificationOnlyHandler
|
||||
import com.facebook.react.packagerconnection.PackagerConnectionSettings
|
||||
import com.facebook.react.packagerconnection.ReconnectingWebSocket
|
||||
import com.facebook.react.packagerconnection.RequestHandler
|
||||
import com.facebook.react.util.RNLog
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okio.Okio
|
||||
|
||||
/**
|
||||
* Helper class for all things about the debug server running in the engineer's host machine.
|
||||
*
|
||||
* One can use 'debug_http_host' shared preferences key to provide a host name for the debug server.
|
||||
* If the setting is empty we support and detect two basic configuration that works well for android
|
||||
* emulators connection to debug server running on emulator's host:
|
||||
* - Android stock emulator with standard non-configurable local loopback alias: 10.0.2.2
|
||||
* - Genymotion emulator with default settings: 10.0.3.2
|
||||
*/
|
||||
@SuppressLint(
|
||||
"StaticFieldLeak") // TODO: This entire class should be rewritten to don't use AsyncTask
|
||||
public class DevServerHelper(
|
||||
private val settings: DeveloperSettings,
|
||||
private val applicationContext: Context,
|
||||
private val packagerConnectionSettings: PackagerConnectionSettings
|
||||
) {
|
||||
public interface PackagerCommandListener {
|
||||
public fun onPackagerConnected()
|
||||
|
||||
public fun onPackagerDisconnected()
|
||||
|
||||
public fun onPackagerReloadCommand()
|
||||
|
||||
public fun onPackagerDevMenuCommand()
|
||||
|
||||
// Allow apps to provide listeners for custom packager commands.
|
||||
public fun customCommandHandlers(): Map<String, RequestHandler>?
|
||||
}
|
||||
|
||||
public val websocketProxyURL: String
|
||||
get() = "ws://${packagerConnectionSettings.debugServerHost}/debugger-proxy?role=client"
|
||||
|
||||
private enum class BundleType(val typeID: String) {
|
||||
BUNDLE("bundle"),
|
||||
MAP("map")
|
||||
}
|
||||
|
||||
private val client: OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
private val bundleDownloader: BundleDownloader = BundleDownloader(client)
|
||||
private val packagerStatusCheck: PackagerStatusCheck = PackagerStatusCheck(client)
|
||||
private val packageName: String = applicationContext.packageName
|
||||
|
||||
private var packagerClient: JSPackagerClient? = null
|
||||
private var inspectorPackagerConnection: IInspectorPackagerConnection? = null
|
||||
|
||||
/** Returns an opaque ID which is stable for the current combination of device and app, stable */
|
||||
private val inspectorDeviceId: String
|
||||
get() {
|
||||
// Every Android app has a unique application ID that looks like a Java or Kotlin package
|
||||
// name,
|
||||
// such as com.example.myapp. This ID uniquely identifies your app on the device and in the
|
||||
// Google Play Store. [Source: Android docs]
|
||||
val packageName = packageName
|
||||
|
||||
// A 64-bit number expressed as a hexadecimal string, which is either:
|
||||
// * unique to each combination of app-signing key, user, and device (API level >= 26), or
|
||||
// * randomly generated when the user first sets up the device and should remain constant for
|
||||
// the lifetime of the user's device (API level < 26).
|
||||
// [Source: Android docs]
|
||||
val androidId = Secure.getString(applicationContext.contentResolver, Secure.ANDROID_ID)
|
||||
val rawDeviceId =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"android-%s-%s-%s",
|
||||
packageName,
|
||||
androidId,
|
||||
if (getFuseboxEnabled()) "fusebox" else "legacy")
|
||||
return getSHA256(rawDeviceId)
|
||||
}
|
||||
|
||||
private val inspectorDeviceUrl: String
|
||||
get() =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"http://%s/inspector/device?name=%s&app=%s&device=%s&profiling=%b",
|
||||
packagerConnectionSettings.debugServerHost,
|
||||
Uri.encode(getFriendlyDeviceName()),
|
||||
Uri.encode(packageName),
|
||||
Uri.encode(inspectorDeviceId),
|
||||
getIsProfilingBuild())
|
||||
|
||||
/** Whether we should enable dev mode when requesting JS bundles. */
|
||||
private val devMode: Boolean
|
||||
get() = settings.isJSDevModeEnabled
|
||||
|
||||
/** Whether we should request minified JS bundles. */
|
||||
private val jSMinifyMode: Boolean
|
||||
get() = settings.isJSMinifyEnabled
|
||||
|
||||
public fun openPackagerConnection(clientId: String?, commandListener: PackagerCommandListener) {
|
||||
if (packagerClient != null) {
|
||||
FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.")
|
||||
return
|
||||
}
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@Deprecated("This needs to be rewritten to not use AsyncTasks")
|
||||
override fun doInBackground(vararg backgroundParams: Void): Void? {
|
||||
val handlers: MutableMap<String, RequestHandler> = mutableMapOf()
|
||||
handlers["reload"] =
|
||||
object : NotificationOnlyHandler() {
|
||||
override fun onNotification(params: Any?) {
|
||||
commandListener.onPackagerReloadCommand()
|
||||
}
|
||||
}
|
||||
handlers["devMenu"] =
|
||||
object : NotificationOnlyHandler() {
|
||||
override fun onNotification(params: Any?) {
|
||||
commandListener.onPackagerDevMenuCommand()
|
||||
}
|
||||
}
|
||||
commandListener.customCommandHandlers()?.let { handlers.putAll(it) }
|
||||
handlers.putAll(FileIoHandler().handlers())
|
||||
|
||||
val onPackagerConnectedCallback: ReconnectingWebSocket.ConnectionCallback =
|
||||
object : ReconnectingWebSocket.ConnectionCallback {
|
||||
override fun onConnected() {
|
||||
commandListener.onPackagerConnected()
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
commandListener.onPackagerDisconnected()
|
||||
}
|
||||
}
|
||||
|
||||
checkNotNull(clientId)
|
||||
packagerClient =
|
||||
JSPackagerClient(
|
||||
clientId, packagerConnectionSettings, handlers, onPackagerConnectedCallback)
|
||||
.apply { init() }
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
}
|
||||
|
||||
public fun closePackagerConnection() {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@Deprecated("This class needs to be rewritten to don't use AsyncTasks")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
packagerClient?.close()
|
||||
packagerClient = null
|
||||
return null
|
||||
}
|
||||
}
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
}
|
||||
|
||||
public fun openInspectorConnection() {
|
||||
if (inspectorPackagerConnection != null) {
|
||||
FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.")
|
||||
return
|
||||
}
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@Deprecated("This class needs to be rewritten to don't use AsyncTasks")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
val metadata = getInspectorHostMetadata(applicationContext)
|
||||
val deviceName = metadata["deviceName"]
|
||||
if (deviceName == null) {
|
||||
FLog.w(ReactConstants.TAG, "Could not get device name from Inspector Host Metadata.")
|
||||
return null
|
||||
}
|
||||
inspectorPackagerConnection =
|
||||
CxxInspectorPackagerConnection(
|
||||
this@DevServerHelper.inspectorDeviceUrl, deviceName, packageName)
|
||||
.apply { connect() }
|
||||
return null
|
||||
}
|
||||
}
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
}
|
||||
|
||||
public fun disableDebugger() {
|
||||
inspectorPackagerConnection?.sendEventToAllConnections(DEBUGGER_MSG_DISABLE)
|
||||
}
|
||||
|
||||
public fun closeInspectorConnection() {
|
||||
object : AsyncTask<Void, Void, Void>() {
|
||||
@Deprecated("This class needs to be rewritten to don't use AsyncTasks")
|
||||
override fun doInBackground(vararg params: Void): Void? {
|
||||
inspectorPackagerConnection?.closeQuietly()
|
||||
inspectorPackagerConnection = null
|
||||
return null
|
||||
}
|
||||
}
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
public fun downloadBundleFromURL(
|
||||
callback: DevBundleDownloadListener,
|
||||
outputFile: File,
|
||||
bundleURL: String?,
|
||||
bundleInfo: BundleDownloader.BundleInfo?,
|
||||
requestBuilder: Request.Builder = Request.Builder()
|
||||
) {
|
||||
bundleDownloader.downloadBundleFromURL(
|
||||
callback, outputFile, bundleURL, bundleInfo, requestBuilder)
|
||||
}
|
||||
|
||||
private fun createSplitBundleURL(mainModuleID: String, host: String): String =
|
||||
createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false)
|
||||
|
||||
private fun createBundleURL(
|
||||
mainModuleID: String,
|
||||
type: BundleType,
|
||||
host: String = packagerConnectionSettings.debugServerHost,
|
||||
modulesOnly: Boolean = false,
|
||||
runModule: Boolean = true
|
||||
): String {
|
||||
val dev = devMode
|
||||
val additionalOptionsBuilder = StringBuilder()
|
||||
for ((key, value) in packagerConnectionSettings.additionalOptionsForPackager) {
|
||||
if (value.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
additionalOptionsBuilder.append("&" + key + "=" + Uri.encode(value))
|
||||
}
|
||||
return (String.format(
|
||||
Locale.US,
|
||||
"http://%s/%s.%s?platform=android&dev=%s&lazy=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s",
|
||||
host,
|
||||
mainModuleID,
|
||||
type.typeID,
|
||||
dev, // dev
|
||||
dev, // lazy
|
||||
jSMinifyMode,
|
||||
packageName,
|
||||
if (modulesOnly) "true" else "false",
|
||||
if (runModule) "true" else "false") +
|
||||
(if (getFuseboxEnabled()) "&excludeSource=true&sourcePaths=url-server" else "") +
|
||||
additionalOptionsBuilder.toString())
|
||||
}
|
||||
|
||||
public fun getDevServerBundleURL(jsModulePath: String): String =
|
||||
createBundleURL(jsModulePath, BundleType.BUNDLE, packagerConnectionSettings.debugServerHost)
|
||||
|
||||
public fun getDevServerSplitBundleURL(jsModulePath: String): String =
|
||||
createSplitBundleURL(jsModulePath, packagerConnectionSettings.debugServerHost)
|
||||
|
||||
public fun isPackagerRunning(callback: PackagerStatusCallback) {
|
||||
packagerStatusCheck.run(packagerConnectionSettings.debugServerHost, callback)
|
||||
}
|
||||
|
||||
public fun getSourceMapUrl(mainModuleName: String): String =
|
||||
createBundleURL(mainModuleName, BundleType.MAP)
|
||||
|
||||
public fun getSourceUrl(mainModuleName: String): String =
|
||||
createBundleURL(mainModuleName, BundleType.BUNDLE)
|
||||
|
||||
/**
|
||||
* This is a debug-only utility to allow fetching a file via packager. It's made synchronous for
|
||||
* simplicity, but should only be used if it's absolutely necessary.
|
||||
*
|
||||
* @return the file with the fetched content, or null if there's any failure.
|
||||
*/
|
||||
public fun downloadBundleResourceFromUrlSync(resourcePath: String, outputFile: File): File? {
|
||||
val resourceURL = createResourceURL(packagerConnectionSettings.debugServerHost, resourcePath)
|
||||
val request = Request.Builder().url(resourceURL).build()
|
||||
|
||||
try {
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful || response.body() == null) {
|
||||
return null
|
||||
}
|
||||
Okio.sink(outputFile).use { output -> response.body()?.source()?.buffer()?.readAll(output) }
|
||||
return outputFile
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Failed to fetch resource synchronously - resourcePath: \"%s\", outputFile: \"%s\"",
|
||||
resourcePath,
|
||||
outputFile.absolutePath,
|
||||
e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt to open the JS debugger on the host machine (on-device CDP debugging). */
|
||||
public fun openDebugger(context: ReactContext?, errorMessage: String?) {
|
||||
// TODO(huntie): Requests to dev server should not assume 'http' URL scheme
|
||||
val requestUrl =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"http://%s/open-debugger?device=%s",
|
||||
packagerConnectionSettings.debugServerHost,
|
||||
Uri.encode(inspectorDeviceId))
|
||||
val request =
|
||||
Request.Builder().url(requestUrl).method("POST", RequestBody.create(null, "")).build()
|
||||
|
||||
client
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
RNLog.w(context, errorMessage ?: "openDebugger error")
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val HTTP_CONNECT_TIMEOUT_MS = 5000
|
||||
private const val DEBUGGER_MSG_DISABLE = "{ \"id\":1,\"method\":\"Debugger.disable\" }"
|
||||
|
||||
private fun getSHA256(string: String): String {
|
||||
val digest =
|
||||
try {
|
||||
MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw AssertionError("Could not get standard SHA-256 algorithm", e)
|
||||
}
|
||||
digest.reset()
|
||||
val result =
|
||||
try {
|
||||
digest.digest(string.toByteArray(charset("UTF-8")))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw AssertionError("This environment doesn't support UTF-8 encoding", e)
|
||||
}
|
||||
return String.format(
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
result[0],
|
||||
result[1],
|
||||
result[2],
|
||||
result[3],
|
||||
result[4],
|
||||
result[5],
|
||||
result[6],
|
||||
result[7],
|
||||
result[8],
|
||||
result[9],
|
||||
result[10],
|
||||
result[11],
|
||||
result[12],
|
||||
result[13],
|
||||
result[14],
|
||||
result[15],
|
||||
result[16],
|
||||
result[17],
|
||||
result[18],
|
||||
result[19])
|
||||
}
|
||||
|
||||
private fun createResourceURL(host: String, resourcePathParam: String): String {
|
||||
// This is what we get for not using a proper URI library.
|
||||
var resourcePath = resourcePathParam
|
||||
if (resourcePath.startsWith("/")) {
|
||||
FLog.w(ReactConstants.TAG, "Resource path should not begin with `/`, removing it.")
|
||||
resourcePath = resourcePath.substring(1)
|
||||
}
|
||||
return String.format(Locale.US, "http://%s/%s", host, resourcePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user