Extract bundle download logic from DevServerHelper to BundleDownloader
Reviewed By: cwdick Differential Revision: D4729058 fbshipit-source-id: 6ffbe4dbd916ac7ac6afffba2cf070aab9cb87b9
This commit is contained in:
parent
9468c5a733
commit
621148d724
|
@ -0,0 +1,187 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-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.react.devsupport;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.facebook.common.logging.FLog;
|
||||||
|
import com.facebook.infer.annotation.Assertions;
|
||||||
|
import com.facebook.react.common.ReactConstants;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSource;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Sink;
|
||||||
|
|
||||||
|
public class BundleDownloader {
|
||||||
|
public interface DownloadCallback {
|
||||||
|
void onSuccess();
|
||||||
|
void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total);
|
||||||
|
void onFailure(Exception cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OkHttpClient mClient;
|
||||||
|
|
||||||
|
private @Nullable Call mDownloadBundleFromURLCall;
|
||||||
|
|
||||||
|
public BundleDownloader(OkHttpClient client) {
|
||||||
|
mClient = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void downloadBundleFromURL(
|
||||||
|
final DownloadCallback callback,
|
||||||
|
final File outputFile,
|
||||||
|
final String bundleURL) {
|
||||||
|
final Request request = new Request.Builder()
|
||||||
|
.url(bundleURL)
|
||||||
|
.addHeader("Accept", "multipart/mixed")
|
||||||
|
.build();
|
||||||
|
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
|
||||||
|
mDownloadBundleFromURLCall.enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
// ignore callback if call was cancelled
|
||||||
|
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
||||||
|
mDownloadBundleFromURLCall = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDownloadBundleFromURLCall = null;
|
||||||
|
|
||||||
|
callback.onFailure(DebugServerException.makeGeneric(
|
||||||
|
"Could not connect to development server.",
|
||||||
|
"URL: " + call.request().url().toString(),
|
||||||
|
e));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, final Response response) throws IOException {
|
||||||
|
// ignore callback if call was cancelled
|
||||||
|
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
||||||
|
mDownloadBundleFromURLCall = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDownloadBundleFromURLCall = null;
|
||||||
|
|
||||||
|
final String url = response.request().url().toString();
|
||||||
|
|
||||||
|
// Make sure the result is a multipart response and parse the boundary.
|
||||||
|
String contentType = response.header("content-type");
|
||||||
|
Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
|
||||||
|
Matcher match = regex.matcher(contentType);
|
||||||
|
if (match.find()) {
|
||||||
|
String boundary = match.group(1);
|
||||||
|
MultipartStreamReader bodyReader = new MultipartStreamReader(response.body().source(), boundary);
|
||||||
|
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
|
||||||
|
@Override
|
||||||
|
public void execute(Map<String, String> headers, Buffer body, boolean finished) throws IOException {
|
||||||
|
// This will get executed for every chunk of the multipart response. The last chunk
|
||||||
|
// (finished = true) will be the JS bundle, the other ones will be progress events
|
||||||
|
// encoded as JSON.
|
||||||
|
if (finished) {
|
||||||
|
// The http status code for each separate chunk is in the X-Http-Status header.
|
||||||
|
int status = response.code();
|
||||||
|
if (headers.containsKey("X-Http-Status")) {
|
||||||
|
status = Integer.parseInt(headers.get("X-Http-Status"));
|
||||||
|
}
|
||||||
|
processBundleResult(url, status, body, outputFile, callback);
|
||||||
|
} else {
|
||||||
|
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSONObject progress = new JSONObject(body.readUtf8());
|
||||||
|
String status = null;
|
||||||
|
if (progress.has("status")) {
|
||||||
|
status = progress.getString("status");
|
||||||
|
}
|
||||||
|
Integer done = null;
|
||||||
|
if (progress.has("done")) {
|
||||||
|
done = progress.getInt("done");
|
||||||
|
}
|
||||||
|
Integer total = null;
|
||||||
|
if (progress.has("total")) {
|
||||||
|
total = progress.getInt("total");
|
||||||
|
}
|
||||||
|
callback.onProgress(status, done, total);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
FLog.e(ReactConstants.TAG, "Error parsing progress JSON. " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!completed) {
|
||||||
|
callback.onFailure(new DebugServerException(
|
||||||
|
"Error while reading multipart response.\n\nResponse code: " + response.code() + "\n\n" +
|
||||||
|
"URL: " + call.request().url().toString() + "\n\n"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In case the server doesn't support multipart/mixed responses, fallback to normal download.
|
||||||
|
processBundleResult(url, response.code(), Okio.buffer(response.body().source()), outputFile, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelDownloadBundleFromURL() {
|
||||||
|
if (mDownloadBundleFromURLCall != null) {
|
||||||
|
mDownloadBundleFromURLCall.cancel();
|
||||||
|
mDownloadBundleFromURLCall = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processBundleResult(
|
||||||
|
String url,
|
||||||
|
int statusCode,
|
||||||
|
BufferedSource body,
|
||||||
|
File outputFile,
|
||||||
|
DownloadCallback callback) throws IOException {
|
||||||
|
// Check for server errors. If the server error has the expected form, fail with more info.
|
||||||
|
if (statusCode != 200) {
|
||||||
|
String bodyString = body.readUtf8();
|
||||||
|
DebugServerException debugServerException = DebugServerException.parse(bodyString);
|
||||||
|
if (debugServerException != null) {
|
||||||
|
callback.onFailure(debugServerException);
|
||||||
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("The development server returned response error code: ").append(statusCode).append("\n\n")
|
||||||
|
.append("URL: ").append(url).append("\n\n")
|
||||||
|
.append("Body:\n")
|
||||||
|
.append(bodyString);
|
||||||
|
callback.onFailure(new DebugServerException(sb.toString()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sink output = null;
|
||||||
|
try {
|
||||||
|
output = Okio.sink(outputFile);
|
||||||
|
body.readAll(output);
|
||||||
|
callback.onSuccess();
|
||||||
|
} finally {
|
||||||
|
if (output != null) {
|
||||||
|
output.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,6 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
@ -56,8 +54,6 @@ import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
import okio.Buffer;
|
|
||||||
import okio.BufferedSource;
|
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
import okio.Sink;
|
import okio.Sink;
|
||||||
|
|
||||||
|
@ -96,12 +92,6 @@ public class DevServerHelper {
|
||||||
private static final int LONG_POLL_FAILURE_DELAY_MS = 5000;
|
private static final int LONG_POLL_FAILURE_DELAY_MS = 5000;
|
||||||
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
|
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
public interface BundleDownloadCallback {
|
|
||||||
void onSuccess();
|
|
||||||
void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total);
|
|
||||||
void onFailure(Exception cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnServerContentChangeListener {
|
public interface OnServerContentChangeListener {
|
||||||
void onServerContentChanged();
|
void onServerContentChanged();
|
||||||
}
|
}
|
||||||
|
@ -119,13 +109,13 @@ public class DevServerHelper {
|
||||||
private final DevInternalSettings mSettings;
|
private final DevInternalSettings mSettings;
|
||||||
private final OkHttpClient mClient;
|
private final OkHttpClient mClient;
|
||||||
private final Handler mRestartOnChangePollingHandler;
|
private final Handler mRestartOnChangePollingHandler;
|
||||||
|
private final BundleDownloader mBundleDownloader;
|
||||||
|
|
||||||
private boolean mOnChangePollingEnabled;
|
private boolean mOnChangePollingEnabled;
|
||||||
private @Nullable JSPackagerClient mPackagerClient;
|
private @Nullable JSPackagerClient mPackagerClient;
|
||||||
private @Nullable InspectorPackagerConnection mInspectorPackagerConnection;
|
private @Nullable InspectorPackagerConnection mInspectorPackagerConnection;
|
||||||
private @Nullable OkHttpClient mOnChangePollingClient;
|
private @Nullable OkHttpClient mOnChangePollingClient;
|
||||||
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
|
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
|
||||||
private @Nullable Call mDownloadBundleFromURLCall;
|
|
||||||
|
|
||||||
public DevServerHelper(DevInternalSettings settings) {
|
public DevServerHelper(DevInternalSettings settings) {
|
||||||
mSettings = settings;
|
mSettings = settings;
|
||||||
|
@ -134,6 +124,7 @@ public class DevServerHelper {
|
||||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||||
.writeTimeout(0, TimeUnit.MILLISECONDS)
|
.writeTimeout(0, TimeUnit.MILLISECONDS)
|
||||||
.build();
|
.build();
|
||||||
|
mBundleDownloader = new BundleDownloader(mClient);
|
||||||
|
|
||||||
mRestartOnChangePollingHandler = new Handler();
|
mRestartOnChangePollingHandler = new Handler();
|
||||||
}
|
}
|
||||||
|
@ -146,8 +137,7 @@ public class DevServerHelper {
|
||||||
new AsyncTask<Void, Void, Void>() {
|
new AsyncTask<Void, Void, Void>() {
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(Void... backgroundParams) {
|
protected Void doInBackground(Void... backgroundParams) {
|
||||||
Map<String, RequestHandler> handlers =
|
Map<String, RequestHandler> handlers = new HashMap<>();
|
||||||
new HashMap<String, RequestHandler>();
|
|
||||||
handlers.put("reload", new NotificationOnlyHandler() {
|
handlers.put("reload", new NotificationOnlyHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void onNotification(@Nullable Object params) {
|
public void onNotification(@Nullable Object params) {
|
||||||
|
@ -322,6 +312,10 @@ public class DevServerHelper {
|
||||||
AndroidInfoHelpers.getFriendlyDeviceName());
|
AndroidInfoHelpers.getFriendlyDeviceName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BundleDownloader getBundleDownloader() {
|
||||||
|
return mBundleDownloader;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the host to use when connecting to the bundle server from the host itself.
|
* @return the host to use when connecting to the bundle server from the host itself.
|
||||||
*/
|
*/
|
||||||
|
@ -380,161 +374,6 @@ public class DevServerHelper {
|
||||||
getJSMinifyMode());
|
getJSMinifyMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void downloadBundleFromURL(
|
|
||||||
final BundleDownloadCallback callback,
|
|
||||||
final File outputFile,
|
|
||||||
final String bundleURL) {
|
|
||||||
final Request request = new Request.Builder()
|
|
||||||
.url(bundleURL)
|
|
||||||
.addHeader("Accept", "multipart/mixed")
|
|
||||||
.build();
|
|
||||||
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
|
|
||||||
mDownloadBundleFromURLCall.enqueue(new Callback() {
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call call, IOException e) {
|
|
||||||
// ignore callback if call was cancelled
|
|
||||||
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
|
||||||
mDownloadBundleFromURLCall = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mDownloadBundleFromURLCall = null;
|
|
||||||
|
|
||||||
callback.onFailure(DebugServerException.makeGeneric(
|
|
||||||
"Could not connect to development server.",
|
|
||||||
"URL: " + call.request().url().toString(),
|
|
||||||
e));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse(Call call, final Response response) throws IOException {
|
|
||||||
// ignore callback if call was cancelled
|
|
||||||
if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
|
|
||||||
mDownloadBundleFromURLCall = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mDownloadBundleFromURLCall = null;
|
|
||||||
|
|
||||||
final String url = response.request().url().toString();
|
|
||||||
|
|
||||||
// Make sure the result is a multipart response and parse the boundary.
|
|
||||||
String contentType = response.header("content-type");
|
|
||||||
Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
|
|
||||||
Matcher match = regex.matcher(contentType);
|
|
||||||
if (match.find()) {
|
|
||||||
String boundary = match.group(1);
|
|
||||||
MultipartStreamReader bodyReader = new MultipartStreamReader(
|
|
||||||
response.body().source(),
|
|
||||||
boundary);
|
|
||||||
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
|
|
||||||
@Override
|
|
||||||
public void execute(
|
|
||||||
Map<String, String> headers,
|
|
||||||
Buffer body,
|
|
||||||
boolean finished) throws IOException {
|
|
||||||
// This will get executed for every chunk of the multipart response. The last chunk
|
|
||||||
// (finished = true) will be the JS bundle, the other ones will be progress events
|
|
||||||
// encoded as JSON.
|
|
||||||
if (finished) {
|
|
||||||
// The http status code for each separate chunk is in the X-Http-Status header.
|
|
||||||
int status = response.code();
|
|
||||||
if (headers.containsKey("X-Http-Status")) {
|
|
||||||
status = Integer.parseInt(headers.get("X-Http-Status"));
|
|
||||||
}
|
|
||||||
processBundleResult(url, status, body, outputFile, callback);
|
|
||||||
} else {
|
|
||||||
if (!headers.containsKey("Content-Type") ||
|
|
||||||
!headers.get("Content-Type").equals("application/json")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JSONObject progress = new JSONObject(body.readUtf8());
|
|
||||||
String status = null;
|
|
||||||
if (progress.has("status")) {
|
|
||||||
status = progress.getString("status");
|
|
||||||
}
|
|
||||||
Integer done = null;
|
|
||||||
if (progress.has("done")) {
|
|
||||||
done = progress.getInt("done");
|
|
||||||
}
|
|
||||||
Integer total = null;
|
|
||||||
if (progress.has("total")) {
|
|
||||||
total = progress.getInt("total");
|
|
||||||
}
|
|
||||||
callback.onProgress(status, done, total);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
FLog.e(ReactConstants.TAG, "Error parsing progress JSON. " + e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!completed) {
|
|
||||||
callback.onFailure(new DebugServerException(
|
|
||||||
"Error while reading multipart response.\n\nResponse code: " +
|
|
||||||
response.code() + "\n\n" + "URL: " + call.request().url().toString() +
|
|
||||||
"\n\n"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* In case the server doesn't support multipart/mixed responses,
|
|
||||||
* fallback to normal download.
|
|
||||||
*/
|
|
||||||
processBundleResult(
|
|
||||||
url,
|
|
||||||
response.code(),
|
|
||||||
Okio.buffer(response.body().source()),
|
|
||||||
outputFile,
|
|
||||||
callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processBundleResult(
|
|
||||||
String url,
|
|
||||||
int statusCode,
|
|
||||||
BufferedSource body,
|
|
||||||
File outputFile,
|
|
||||||
BundleDownloadCallback callback) throws IOException {
|
|
||||||
// Check for server errors. If the server error has the expected form, fail with more info.
|
|
||||||
if (statusCode != 200) {
|
|
||||||
String bodyString = body.readUtf8();
|
|
||||||
DebugServerException debugServerException = DebugServerException.parse(bodyString);
|
|
||||||
if (debugServerException != null) {
|
|
||||||
callback.onFailure(debugServerException);
|
|
||||||
} else {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("The development server returned response error code: ")
|
|
||||||
.append(statusCode)
|
|
||||||
.append("\n\n")
|
|
||||||
.append("URL: ")
|
|
||||||
.append(url)
|
|
||||||
.append("\n\n")
|
|
||||||
.append("Body:\n")
|
|
||||||
.append(bodyString);
|
|
||||||
callback.onFailure(new DebugServerException(sb.toString()));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sink output = null;
|
|
||||||
try {
|
|
||||||
output = Okio.sink(outputFile);
|
|
||||||
body.readAll(output);
|
|
||||||
callback.onSuccess();
|
|
||||||
} finally {
|
|
||||||
if (output != null) {
|
|
||||||
output.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelDownloadBundleFromURL() {
|
|
||||||
if (mDownloadBundleFromURLCall != null) {
|
|
||||||
mDownloadBundleFromURLCall.cancel();
|
|
||||||
mDownloadBundleFromURLCall = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void isPackagerRunning(final PackagerStatusCallback callback) {
|
public void isPackagerRunning(final PackagerStatusCallback callback) {
|
||||||
String statusURL = createPackagerStatusURL(
|
String statusURL = createPackagerStatusURL(
|
||||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||||
|
|
|
@ -809,8 +809,8 @@ public class DevSupportManagerImpl implements
|
||||||
mDevLoadingViewController.showForUrl(bundleURL);
|
mDevLoadingViewController.showForUrl(bundleURL);
|
||||||
mDevLoadingViewVisible = true;
|
mDevLoadingViewVisible = true;
|
||||||
|
|
||||||
mDevServerHelper.downloadBundleFromURL(
|
mDevServerHelper.getBundleDownloader().downloadBundleFromURL(
|
||||||
new DevServerHelper.BundleDownloadCallback() {
|
new BundleDownloader.DownloadCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess() {
|
public void onSuccess() {
|
||||||
mDevLoadingViewController.hide();
|
mDevLoadingViewController.hide();
|
||||||
|
|
Loading…
Reference in New Issue