From 373eb61f1f411458d4c1c24bb08033bc6d16a316 Mon Sep 17 00:00:00 2001 From: Charles Dick Date: Wed, 15 Mar 2017 06:31:47 -0700 Subject: [PATCH] Download files through RN packager connection Differential Revision: D4650999 fbshipit-source-id: 298e3e65bfc13a3610a588c9bffb4808fb2135e3 --- .../react/devsupport/DevServerHelper.java | 2 + .../devsupport/DevSupportManagerImpl.java | 29 ++- .../devsupport/DisabledDevSupportManager.java | 5 - .../react/devsupport/JSCHeapUpload.java | 77 ------- .../interfaces/DevSupportManager.java | 1 - .../packagerconnection/FileIoHandler.java | 192 ++++++++++++++++++ .../packagerconnection/JSPackagerClient.java | 1 + 7 files changed, 208 insertions(+), 99 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/packagerconnection/FileIoHandler.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index 5cebb1bc5..5c3750dfe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -32,6 +32,7 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; +import com.facebook.react.packagerconnection.FileIoHandler; import com.facebook.react.packagerconnection.JSPackagerClient; import org.json.JSONException; @@ -149,6 +150,7 @@ public class DevServerHelper { commandListener.onPokeSamplingProfilerCommand(responder); } }); + handlers.putAll(new FileIoHandler().handlers()); mPackagerClient = new JSPackagerClient(getPackagerConnectionURL(), handlers); mPackagerClient.init(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 28ab65214..d141dc932 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -422,14 +422,6 @@ public class DevSupportManagerImpl implements mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); } }); - options.put( - mApplicationContext.getString(R.string.catalyst_heap_capture), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - handleCaptureHeap(null); - } - }); options.put( mApplicationContext.getString(R.string.catalyst_poke_sampling_profiler), new DevOptionHandler() { @@ -540,11 +532,6 @@ public class DevSupportManagerImpl implements return mJSBundleTempFile.getAbsolutePath(); } - @Override - public String getHeapCaptureUploadUrl() { - return mDevServerHelper.getHeapCaptureUploadUrl(); - } - /** * @return {@code true} if {@link com.facebook.react.ReactInstanceManager} should use downloaded JS bundle file * instead of using JS file from assets. This may happen when app has not been updated since @@ -687,7 +674,7 @@ public class DevSupportManagerImpl implements } @Override - public void onCaptureHeapCommand(@Nullable final JSPackagerClient.Responder responder) { + public void onCaptureHeapCommand(final JSPackagerClient.Responder responder) { UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { @@ -706,14 +693,24 @@ public class DevSupportManagerImpl implements }); } - private void handleCaptureHeap(@Nullable final JSPackagerClient.Responder responder) { + private void handleCaptureHeap(final JSPackagerClient.Responder responder) { if (mCurrentContext == null) { return; } JSCHeapCapture heapCapture = mCurrentContext.getNativeModule(JSCHeapCapture.class); heapCapture.captureHeap( mApplicationContext.getCacheDir().getPath(), - JSCHeapUpload.captureCallback(mDevServerHelper.getHeapCaptureUploadUrl(), responder)); + new JSCHeapCapture.CaptureCallback() { + @Override + public void onSuccess(File capture) { + responder.respond(capture.toString()); + } + + @Override + public void onFailure(JSCHeapCapture.CaptureException error) { + responder.error(error.toString()); + } + }); } private void handlePokeSamplingProfiler(@Nullable final JSPackagerClient.Responder responder) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 23455e027..63f803ca6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -109,11 +109,6 @@ public class DisabledDevSupportManager implements DevSupportManager { return null; } - @Override - public String getHeapCaptureUploadUrl() { - return null; - } - @Override public boolean hasUpToDateJSBundleInCache() { return false; diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java deleted file mode 100644 index f678a6d66..000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2016-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 android.util.Log; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import com.facebook.react.packagerconnection.JSPackagerClient; - -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** - * Created by cwdick on 7/22/16. - */ -public class JSCHeapUpload { - public static JSCHeapCapture.CaptureCallback captureCallback( - final String uploadUrl, - @Nullable final JSPackagerClient.Responder responder) { - return new JSCHeapCapture.CaptureCallback() { - @Override - public void onSuccess(File capture) { - OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); - httpClientBuilder.connectTimeout(1, TimeUnit.MINUTES) - .writeTimeout(5, TimeUnit.MINUTES) - .readTimeout(5, TimeUnit.MINUTES); - OkHttpClient httpClient = httpClientBuilder.build(); - RequestBody body = RequestBody.create(MediaType.parse("application/json"), capture); - Request request = new Request.Builder() - .url(uploadUrl) - .method("POST", body) - .build(); - Call call = httpClient.newCall(request); - call.enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - String message = "Upload of heap capture failed: " + e.toString(); - Log.e("JSCHeapCapture", message); - responder.error(message); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - if (!response.isSuccessful()) { - String message = "Upload of heap capture failed with code " + Integer.toString(response.code()) + ": " + response.body().string(); - Log.e("JSCHeapCapture", message); - responder.error(message); - } - responder.respond(response.body().string()); - } - }); - } - - @Override - public void onFailure(JSCHeapCapture.CaptureException e) { - String message = "Heap capture failed: " + e.toString(); - Log.e("JSCHeapCapture", message); - responder.error(message); - } - }; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java index a8fedc356..b6bcdec4e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java @@ -40,7 +40,6 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { String getSourceUrl(); String getJSBundleURLForRemoteDebugging(); String getDownloadedJSBundleFile(); - String getHeapCaptureUploadUrl(); boolean hasUpToDateJSBundleInCache(); void reloadSettings(); void handleReloadJS(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/FileIoHandler.java b/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/FileIoHandler.java new file mode 100644 index 000000000..5a0adb1dd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/FileIoHandler.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2016-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.packagerconnection; + +import javax.annotation.Nullable; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import android.os.Handler; +import android.os.Looper; +import android.util.Base64; + +import com.facebook.common.logging.FLog; + +import org.json.JSONObject; + +public class FileIoHandler implements Runnable { + private static final String TAG = JSPackagerClient.class.getSimpleName(); + private static final long FILE_TTL = 30 * 1000; + + private static class TtlFileInputStream { + private final FileInputStream mStream; + private long mTtl; + + public TtlFileInputStream(String path) throws FileNotFoundException { + mStream = new FileInputStream(path); + mTtl = System.currentTimeMillis() + FILE_TTL; + } + + private void extendTtl() { + mTtl = System.currentTimeMillis() + FILE_TTL; + } + + public boolean expiredTtl() { + return System.currentTimeMillis() >= mTtl; + } + + public String read(int size) throws IOException { + extendTtl(); + byte[] buffer = new byte[size]; + int bytesRead = mStream.read(buffer); + return Base64.encodeToString(buffer, 0, bytesRead, Base64.DEFAULT); + } + + public void close() throws IOException { + mStream.close(); + } + }; + + private int mNextHandle; + private final Handler mHandler; + private final Map mOpenFiles; + private final Map mRequestHandlers; + + public FileIoHandler() { + mNextHandle = 1; + mHandler = new Handler(Looper.getMainLooper()); + mOpenFiles = new HashMap<>(); + mRequestHandlers = new HashMap<>(); + mRequestHandlers.put("fopen", new JSPackagerClient.RequestOnlyHandler() { + @Override + public void onRequest( + @Nullable Object params, JSPackagerClient.Responder responder) { + synchronized (mOpenFiles) { + try { + JSONObject paramsObj = (JSONObject)params; + if (paramsObj == null) { + throw new Exception("params must be an object { mode: string, filename: string }"); + } + String mode = paramsObj.optString("mode"); + if (mode == null) { + throw new Exception("missing params.mode"); + } + String filename = paramsObj.optString("filename"); + if (filename == null) { + throw new Exception("missing params.filename"); + } + if (!mode.equals("r")) { + throw new IllegalArgumentException("unsupported mode: " + mode); + } + + responder.respond(addOpenFile(filename)); + } catch (Exception e) { + responder.error(e.toString()); + } + } + } + }); + mRequestHandlers.put("fclose", new JSPackagerClient.RequestOnlyHandler() { + @Override + public void onRequest( + @Nullable Object params, JSPackagerClient.Responder responder) { + synchronized (mOpenFiles) { + try { + if (!(params instanceof Number)) { + throw new Exception("params must be a file handle"); + } + TtlFileInputStream stream = mOpenFiles.get((int)params); + if (stream == null) { + throw new Exception("invalid file handle, it might have timed out"); + } + + mOpenFiles.remove((int)params); + stream.close(); + responder.respond(""); + } catch (Exception e) { + responder.error(e.toString()); + } + } + } + }); + mRequestHandlers.put("fread", new JSPackagerClient.RequestOnlyHandler() { + @Override + public void onRequest( + @Nullable Object params, JSPackagerClient.Responder responder) { + synchronized (mOpenFiles) { + try { + JSONObject paramsObj = (JSONObject)params; + if (paramsObj == null) { + throw new Exception("params must be an object { file: handle, size: number }"); + } + int file = paramsObj.optInt("file"); + if (file == 0) { + throw new Exception("invalid or missing file handle"); + } + int size = paramsObj.optInt("size"); + if (size == 0) { + throw new Exception("invalid or missing read size"); + } + TtlFileInputStream stream = mOpenFiles.get(file); + if (stream == null) { + throw new Exception("invalid file handle, it might have timed out"); + } + + responder.respond(stream.read(size)); + } catch (Exception e) { + responder.error(e.toString()); + } + } + } + }); + } + + public Map handlers() { + return mRequestHandlers; + } + + private int addOpenFile(String filename) throws FileNotFoundException { + int handle = mNextHandle++; + mOpenFiles.put(handle, new TtlFileInputStream(filename)); + if (mOpenFiles.size() == 1) { + mHandler.postDelayed(FileIoHandler.this, FILE_TTL); + } + return handle; + } + + @Override + public void run() { + // clean up files that are past their expiry date + synchronized (mOpenFiles) { + Iterator i = mOpenFiles.values().iterator(); + while (i.hasNext()) { + TtlFileInputStream stream = i.next(); + if (stream.expiredTtl()) { + i.remove(); + try { + stream.close(); + } catch (IOException e) { + FLog.e( + TAG, + "closing expired file failed: " + e.toString()); + } + } + } + if (!mOpenFiles.isEmpty()) { + mHandler.postDelayed(this, FILE_TTL); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/JSPackagerClient.java b/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/JSPackagerClient.java index 5170d46d0..d8cdc6544 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/JSPackagerClient.java +++ b/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/JSPackagerClient.java @@ -10,6 +10,7 @@ package com.facebook.react.packagerconnection; import javax.annotation.Nullable; +import java.util.HashMap; import java.util.Map; import com.facebook.common.logging.FLog;