BREAKING: Change the js1 - packager - RN App protocol to version 2

Reviewed By: cwdick

Differential Revision: D4551991

fbshipit-source-id: 395c38ee5c71ddc24d8743e7ec90cc89de087503
This commit is contained in:
Lukas Piatkowski 2017-02-16 04:20:32 -08:00 committed by Facebook Github Bot
parent 33817b83d6
commit a2addbd932
6 changed files with 326 additions and 73 deletions

View File

@ -269,7 +269,7 @@ RCT_EXPORT_MODULE()
if (!port) {
port = @8081; // Packager default port
}
return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=shell", scheme, host, port]];
return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=ios-rn-rctdevmenu", scheme, host, port]];
}
// TODO: Move non-UI logic into separate RCTDevSettings module
@ -310,26 +310,24 @@ RCT_EXPORT_MODULE()
- (BOOL)isSupportedVersion:(NSNumber *)version
{
NSArray<NSNumber *> *const kSupportedVersions = @[ @1 ];
NSArray<NSNumber *> *const kSupportedVersions = @[ @2 ];
return [kSupportedVersions containsObject:version];
}
- (void)didReceiveWebSocketMessage:(NSDictionary<NSString *, id> *)message
{
if ([self isSupportedVersion:message[@"version"]]) {
[self processTarget:message[@"target"] action:message[@"action"] options:message[@"options"]];
[self processMethod:message[@"method"] params:message[@"params"]];
}
}
- (void)processTarget:(NSString *)target action:(NSString *)action options:(NSDictionary<NSString *, id> *)options
- (void)processMethod:(NSString *)method params:(NSDictionary<NSString *, id> *)params
{
if ([target isEqualToString:@"bridge"]) {
if ([action isEqualToString:@"reload"]) {
if ([options[@"debug"] boolValue]) {
_bridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor");
}
[_bridge reload];
if ([method isEqualToString:@"reload"]) {
if (![params isEqual:[NSNull null]] && [params[@"debug"] boolValue]) {
_bridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor");
}
[_bridge reload];
}
}

View File

@ -65,7 +65,7 @@ public class DevServerHelper {
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
"http://%s/onchange";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client";
private static final String PACKAGER_CONNECTION_URL_FORMAT = "ws://%s/message?role=shell";
private static final String PACKAGER_CONNECTION_URL_FORMAT = "ws://%s/message?role=android-rn-devserverhelper";
private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status";
private static final String HEAP_CAPTURE_UPLOAD_URL_FORMAT = "http://%s/jscheapcaptureupload";
private static final String INSPECTOR_DEVICE_URL_FORMAT = "http://%s/inspector/device?name=%s";

View File

@ -25,7 +25,7 @@ import org.json.JSONObject;
*/
final public class JSPackagerClient implements ReconnectingWebSocket.MessageCallback {
private static final String TAG = JSPackagerClient.class.getSimpleName();
private static final int PROTOCOL_VERSION = 1;
private static final int PROTOCOL_VERSION = 2;
public class Responder {
private Object mId;
@ -38,8 +38,8 @@ final public class JSPackagerClient implements ReconnectingWebSocket.MessageCall
try {
JSONObject message = new JSONObject();
message.put("version", PROTOCOL_VERSION);
message.put("target", "profiler");
message.put("action", result);
message.put("id", mId);
message.put("result", result);
mWebSocket.sendMessage(RequestBody.create(WebSocket.TEXT, message.toString()));
} catch (Exception e) {
FLog.e(TAG, "Responding failed", e);
@ -109,8 +109,9 @@ final public class JSPackagerClient implements ReconnectingWebSocket.MessageCall
JSONObject message = new JSONObject(response.string());
int version = message.optInt("version");
String target = message.optString("target");
String action = message.optString("action");
String method = message.optString("method");
Object id = message.opt("id");
Object params = message.opt("params");
if (version != PROTOCOL_VERSION) {
FLog.e(
@ -119,20 +120,21 @@ final public class JSPackagerClient implements ReconnectingWebSocket.MessageCall
return;
}
if (!"bridge".equals(target)) {
if (method == null) {
abortOnMessage(id, "No method provided");
return;
}
RequestHandler handler = mRequestHandlers.get(action);
RequestHandler handler = mRequestHandlers.get(method);
if (handler == null) {
FLog.e(TAG, "No request handler for action: " + action);
abortOnMessage(id, "No request handler for method: " + method);
return;
}
if (!"pokeSamplingProfiler".equals(action)) {
handler.onNotification(null);
if (id == null) {
handler.onNotification(params);
} else {
handler.onRequest(null, new Responder("profiler"));
handler.onRequest(params, new Responder(id));
}
} catch (Exception e) {
FLog.e(TAG, "Handling the message failed", e);
@ -140,4 +142,12 @@ final public class JSPackagerClient implements ReconnectingWebSocket.MessageCall
response.close();
}
}
private void abortOnMessage(Object id, String reason) {
if (id != null) {
(new Responder(id)).error(reason);
}
FLog.e(TAG, "Handling the message failed with reason: " + reason);
}
}

View File

@ -33,93 +33,114 @@ public class JSPackagerClientTest {
}
@Test
public void test_onMessage_ShouldTriggerCallback() throws IOException {
public void test_onMessage_ShouldTriggerNotification() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 1, \"target\": \"bridge\", \"action\": \"actionValue\"}"));
verify(handler).onNotification(any());
"{\"version\": 2, \"method\": \"methodValue\", \"params\": \"paramsValue\"}"));
verify(handler).onNotification(eq("paramsValue"));
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_ShouldTriggerRequest() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 2, \"id\": \"idValue\", \"method\": \"methodValue\", \"params\": \"paramsValue\"}"));
verify(handler, never()).onNotification(any());
verify(handler).onRequest(eq("paramsValue"), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_WithoutParams_ShouldTriggerNotification() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 2, \"method\": \"methodValue\"}"));
verify(handler).onNotification(eq(null));
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_WithInvalidContentType_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.BINARY,
"{\"version\": 1, \"target\": \"bridge\", \"action\": \"actionValue\"}"));
"{\"version\": 2, \"method\": \"methodValue\"}"));
verify(handler, never()).onNotification(any());
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_WithoutTarget_ShouldNotTriggerCallback() throws IOException {
public void test_onMessage_WithoutMethod_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 1, \"action\": \"actionValue\"}"));
verify(handler, never()).onNotification(any());
}
@Test
public void test_onMessage_With_Null_Target_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 1, \"target\": null, \"action\": \"actionValue\"}"));
verify(handler, never()).onNotification(any());
}
@Test
public void test_onMessage_WithoutAction_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 1, \"target\": \"bridge\"}"));
"{\"version\": 2}"));
verify(handler, never()).onNotification(any());
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_With_Null_Action_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 1, \"target\": \"bridge\", \"action\": null}"));
"{\"version\": 2, \"method\": null}"));
verify(handler, never()).onNotification(any());
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_WithInvalidMethod_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.BINARY,
"{\"version\": 2, \"method\": \"methodValue2\"}"));
verify(handler, never()).onNotification(any());
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
@Test
public void test_onMessage_WrongVersion_ShouldNotTriggerCallback() throws IOException {
JSPackagerClient.RequestHandler handler = mock(JSPackagerClient.RequestHandler.class);
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("actionValue", handler));
final JSPackagerClient client = new JSPackagerClient("ws://not_needed", createRH("methodValue", handler));
WebSocket webSocket = mock(WebSocket.class);
client.onMessage(
ResponseBody.create(
WebSocket.TEXT,
"{\"version\": 2, \"target\": \"bridge\", \"action\": \"actionValue\"}"));
"{\"version\": 1, \"method\": \"methodValue\"}"));
verify(handler, never()).onNotification(any());
verify(handler, never()).onRequest(any(), any(JSPackagerClient.Responder.class));
}
}

View File

@ -0,0 +1,115 @@
/**
* Copyright (c) 2013-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.
*
*/
'use strict';
const WebSocket = require('ws');
const parseMessage = require('./messageSocket').parseMessage;
const PROTOCOL_VERSION = 2;
const TARGET_SERVER = 'server';
function getMessageId() {
return `${Date.now()}:${Math.random()}`;
}
class JsPackagerClient {
constructor(url) {
this.ws = new WebSocket(url);
this.msgCallbacks = new Map();
this.openPromise = new Promise((resolve, reject) => {
this.ws.on('error', error => reject(error));
this.ws.on('open', resolve);
});
this.ws.on('message', (data, flags) => {
const message = parseMessage(data, flags.binary);
const msgCallback = this.msgCallbacks.get(message.id);
if (message === undefined || message.id === undefined) {
// gracefully ignore wrong messages or broadcasts
} else if (msgCallback === undefined) {
console.warn(`Response with non-existing message id: '${message.id}'`);
} else {
if (message.error === undefined) {
msgCallback.resolve(message.result);
} else {
msgCallback.reject(message.error);
}
}
});
}
sendRequest(method, target, params) {
return this.openPromise.then(() => new Promise((resolve, reject) => {
const messageId = getMessageId();
this.msgCallbacks.set(messageId, {resolve: resolve, reject: reject});
this.ws.send(
JSON.stringify({
version: PROTOCOL_VERSION,
target: target,
method: method,
id: messageId,
params: params,
}),
error => {
if (error !== undefined) {
this.msgCallbacks.delete(messageId);
reject(error);
}
});
}));
}
sendNotification(method, target, params) {
return this.openPromise.then(() => new Promise((resolve, reject) => {
this.ws.send(
JSON.stringify({
version: PROTOCOL_VERSION,
target: target,
method: method,
params: params,
}),
error => {
if (error !== undefined) {
reject(error);
} else {
resolve();
}
});
}));
}
sendBroadcast(method, params) {
return this.sendNotification(method, undefined, params);
}
getPeers() {
return new Promise((resolve, reject) => {
this.sendRequest('getpeers', TARGET_SERVER, undefined).then(
response => {
if (!response instanceof Map) {
reject('Results received from server are of wrong format:\n' +
JSON.stringify(response));
} else {
resolve(response);
}
},
reject);
});
}
getId() {
return this.sendRequest('getid', TARGET_SERVER, undefined);
}
}
module.exports = JsPackagerClient;

View File

@ -8,8 +8,9 @@
*/
'use strict';
const url = require('url');
const WebSocketServer = require('ws').Server;
const PROTOCOL_VERSION = 1;
const PROTOCOL_VERSION = 2;
function parseMessage(data, binary) {
if (binary) {
@ -29,6 +30,30 @@ function parseMessage(data, binary) {
return undefined;
}
function isBroadcast(message) {
return (
typeof message.method === 'string' &&
message.id === undefined &&
message.target === undefined
);
}
function isRequest(message) {
return (
typeof message.method === 'string' &&
typeof message.target === 'string');
}
function isResponse(message) {
return (
typeof message.id === 'object' &&
typeof message.id.requestId !== undefined &&
typeof message.id.clientId === 'string' && (
message.result !== undefined ||
message.error !== undefined
));
}
function attachToServer(server, path) {
const wss = new WebSocketServer({
server: server,
@ -37,11 +62,19 @@ function attachToServer(server, path) {
const clients = new Map();
let nextClientId = 0;
function getClientWs(clientId) {
const clientWs = clients.get(clientId);
if (clientWs === undefined) {
throw `could not find id "${clientId}" while forwarding request`;
}
return clientWs;
}
function handleSendBroadcast(broadcasterId, message) {
const forwarded = {
version: PROTOCOL_VERSION,
target: message.target,
action: message.action,
method: message.method,
params: message.params,
};
for (const [otherId, otherWs] of clients) {
if (otherId !== broadcasterId) {
@ -58,14 +91,78 @@ function attachToServer(server, path) {
wss.on('connection', function(clientWs) {
const clientId = `client#${nextClientId++}`;
function handleCatchedError(message, error) {
function handleCaughtError(message, error) {
const errorMessage = {
id: message.id,
method: message.method,
target: message.target,
action: message.action === undefined ? 'undefined' : 'defined',
error: message.error === undefined ? 'undefined' : 'defined',
params: message.params === undefined ? 'undefined' : 'defined',
result: message.result === undefined ? 'undefined' : 'defined',
};
console.error(
`Handling message from ${clientId} failed with:\n${error}\n` +
`message:\n${JSON.stringify(errorMessage)}`);
if (message.id === undefined) {
console.error(
`Handling message from ${clientId} failed with:\n${error}\n` +
`message:\n${JSON.stringify(errorMessage)}`);
} else {
try {
clientWs.send(JSON.stringify({
version: PROTOCOL_VERSION,
error: error,
id: message.id,
}));
} catch (e) {
console.error(`Failed to reply to ${clientId} with error:\n${error}` +
`\nmessage:\n${JSON.stringify(errorMessage)}` +
`\ndue to error: ${e.toString()}`);
}
}
}
function handleServerRequest(message) {
let result = null;
switch (message.method) {
case 'getid':
result = clientId;
break;
case 'getpeers':
result = {};
clients.forEach((otherWs, otherId) => {
if (clientId !== otherId) {
result[otherId] = url.parse(otherWs.upgradeReq.url).query;
}
});
break;
default:
throw `unkown method: ${message.method}`;
}
clientWs.send(JSON.stringify({
version: PROTOCOL_VERSION,
result: result,
id: message.id
}));
}
function forwardRequest(message) {
getClientWs(message.target).send(JSON.stringify({
version: PROTOCOL_VERSION,
method: message.method,
params: message.params,
id: (message.id === undefined
? undefined
: {requestId: message.id, clientId: clientId}),
}));
}
function forwardResponse(message) {
getClientWs(message.id.clientId).send(JSON.stringify({
version: PROTOCOL_VERSION,
result: message.result,
error: message.error,
id: message.id.requestId,
}));
}
clients.set(clientId, clientWs);
@ -82,16 +179,28 @@ function attachToServer(server, path) {
}
try {
handleSendBroadcast(clientId, message);
if (isBroadcast(message)) {
handleSendBroadcast(clientId, message);
} else if (isRequest(message)) {
if (message.target === 'server') {
handleServerRequest(message);
} else {
forwardRequest(message);
}
} else if (isResponse(message)) {
forwardResponse(message);
} else {
throw 'Invalid message, did not match the protocol';
}
} catch (e) {
handleCatchedError(message, e.toString());
handleCaughtError(message, e.toString());
}
};
});
return {
broadcast: (target, action) => {
handleSendBroadcast(null, {target: target, action: action});
broadcast: (method, params) => {
handleSendBroadcast(null, {method: method, params: params});
}
};
}