Improve Chrome debugger

Summary:
`debugger.html` contained a ton of hacky code that was needed to ensure we have a clean JS runtime every time a client RN app connects. That was needed because we used the page's global environment as runtime. Some time ago WebWorker support was added and now we run RN code inside an isolated WebWorker instance, and we can safely get rid of all these hacks.

This has a bunch of nice side-effects: debug reload works faster, `console.log`s are preserved, `debuggerWorker.js` selection doesn't change.

Made sure the debugging (breakpoints, etc.) still works as before.

Small demo
![](http://g.recordit.co/FPdVHLHPUW.gif)
Closes https://github.com/facebook/react-native/pull/5715

Reviewed By: svcscm

Differential Revision: D2906602

Pulled By: frantic

fb-gh-sync-id: 1a6ab9a5655d7c32ddd23619564e59c377b53a35
This commit is contained in:
Alex Kotliarskyi 2016-02-05 15:16:16 -08:00 committed by facebook-github-bot-7
parent 4fd115ffa2
commit 64d56f34b7
4 changed files with 96 additions and 93 deletions

View File

@ -51,7 +51,7 @@ RCT_EXPORT_MODULE()
if (!_url) {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
NSInteger port = [standardDefaults integerForKey:@"websocket-executor-port"] ?: 8081;
NSString *URLString = [NSString stringWithFormat:@"http://localhost:%zd/debugger-proxy", port];
NSString *URLString = [NSString stringWithFormat:@"http://localhost:%zd/debugger-proxy?role=client", port];
_url = [RCTConvert NSURL:URLString];
}

View File

@ -62,7 +62,7 @@ public class DevServerHelper {
"http://%s/launch-chrome-devtools";
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
"http://%s/onchange";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client";
private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status";
private static final String PACKAGER_OK_STATUS = "packager-status:running";

View File

@ -17,83 +17,82 @@
<script>
(function() {
var sessionID = window.localStorage.getItem('sessionID');
window.localStorage.removeItem('sessionID');
window.onbeforeunload = function() {
if (sessionID) {
return 'If you reload this page, it is going to break the debugging session. ' +
'You should press ⌘R in simulator to reload.';
}
};
// Alias native implementations needed by the debugger before platform-specific
// implementations are loaded into the global namespace
var debuggerSetTimeout = window.setTimeout;
var DebuggerWebSocket = window.WebSocket;
function setStatus(status) {
document.getElementById('status').innerHTML = status;
}
// This worker will run the application javascript code,
// making sure that it's run in an environment without a global
// document, to make it consistent with the JSC executor environment.
var worker = new Worker('debuggerWorker.js');
var messageHandlers = {
// This method is a bit hacky. Catalyst asks for a new clean JS runtime.
// The easiest way to do this is to reload this page. That also means that
// web socket connection will be lost. To send reply back we need to remember
// message id.
// This message also needs to be handled outside of the worker, since the worker
// doesn't have access to local storage.
'prepareJSRuntime': function(message) {
window.onbeforeunload = undefined;
window.localStorage.setItem('sessionID', message.id);
window.location.reload();
}
};
var INITIAL_MESSAGE = 'Waiting, press <span class="shortcut">⌘R</span> in simulator to reload and connect.';
function connectToDebuggerProxy() {
var ws = new DebuggerWebSocket('ws://' + window.location.host + '/debugger-proxy');
var worker;
var ws = new WebSocket('ws://' + window.location.host + '/debugger-proxy?role=debugger&name=Chrome');
function createJSRuntime() {
// This worker will run the application javascript code,
// making sure that it's run in an environment without a global
// document, to make it consistent with the JSC executor environment.
worker = new Worker('debuggerWorker.js');
worker.onmessage = function(message) {
ws.send(JSON.stringify(message.data));
};
window.onbeforeunload = function() {
return 'If you reload this page, it is going to break the debugging session. ' +
'You should press ⌘R in simulator to reload.';
};
}
function shutdownJSRuntime() {
if (worker) {
worker.terminate();
worker = null;
window.onbeforeunload = null;
}
}
ws.onopen = function() {
if (sessionID) {
setStatus('Debugger session #' + sessionID + ' active.');
ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)}));
} else {
setStatus('Waiting, press <span class="shortcut">⌘R</span> in simulator to reload and connect.');
}
setStatus(INITIAL_MESSAGE);
};
ws.onmessage = function(message) {
if (!message.data) {
return;
}
var object = JSON.parse(message.data);
if (object.$event === 'client-disconnected') {
shutdownJSRuntime();
setStatus('Waiting, press <span class="shortcut">⌘R</span> in simulator to reload and connect.');
return;
}
if (!object.method) {
return;
}
var handler = messageHandlers[object.method];
if (handler) {
// If we have a local handler, use it.
handler(object);
// Special message that asks for a new JS runtime
if (object.method === 'prepareJSRuntime') {
shutdownJSRuntime();
createJSRuntime();
ws.send(JSON.stringify({replyID: object.id}));
setStatus('Debugger session #' + object.id + ' active.');
} else if (object.method === '$disconnected') {
shutdownJSRuntime();
setStatus(INITIAL_MESSAGE);
} else {
// Otherwise, pass through to the worker.
worker.postMessage(object);
}
};
ws.onclose = function() {
ws.onclose = function(e) {
shutdownJSRuntime();
setStatus('Disconnected from proxy. Attempting reconnection. Is node server running?');
sessionID = null;
window.localStorage.removeItem('sessionID');
debuggerSetTimeout(connectToDebuggerProxy, 100);
if (e.reason) {
setStatus(e.reason);
console.warn(e.reason);
}
setTimeout(connectToDebuggerProxy, 500);
};
worker.onmessage = function(message) {
ws.send(JSON.stringify(message.data));
}
}
connectToDebuggerProxy();

View File

@ -15,55 +15,59 @@ function attachToServer(server, path) {
server: server,
path: path
});
var clients = [];
var debuggerSocket, clientSocket;
function sendSpecial(message) {
clients.forEach(function (cn) {
try {
cn.send(JSON.stringify(message));
} catch(e) {
// Sometimes this call throws 'not opened'
}
});
function send(dest, message) {
if (!dest) {
return;
}
try {
dest.send(message);
} catch(e) {
console.warn(e);
// Sometimes this call throws 'not opened'
}
}
wss.on('connection', function(ws) {
var id = Math.random().toString(15).slice(10, 20);
sendSpecial({$open: id});
clients.push(ws);
const {url} = ws.upgradeReq;
var allClientsExcept = function(ws) {
return clients.filter(function(cn) { return cn !== ws; });
};
ws.onerror = function() {
clients = allClientsExcept(ws);
sendSpecial({$error: id});
};
ws.onclose = function() {
clients = allClientsExcept(ws);
sendSpecial({$close: id});
};
ws.on('message', function(message) {
allClientsExcept(ws).forEach(function(cn) {
try {
cn.send(message);
} catch(e) {
// Sometimes this call throws 'not opened'
if (url.indexOf('role=debugger') > -1) {
if (debuggerSocket) {
ws.close(1011, 'Another debugger is already connected');
return;
}
debuggerSocket = ws;
debuggerSocket.onerror =
debuggerSocket.onclose = () => {
debuggerSocket = null;
if (clientSocket) {
clientSocket.close(1011, 'Debugger was disconnected');
}
});
});
};
debuggerSocket.onmessage = ({data}) => send(clientSocket, data);
} else if (url.indexOf('role=client') > -1) {
if (clientSocket) {
clientSocket.onerror = clientSocket.onclose = clientSocket.onmessage = null;
clientSocket.close(1011, 'Another client connected');
}
clientSocket = ws;
clientSocket.onerror =
clientSocket.onclose = () => {
clientSocket = null;
send(debuggerSocket, JSON.stringify({method: '$disconnected'}));
};
clientSocket.onmessage = ({data}) => send(debuggerSocket, data);
} else {
ws.close(1011, 'Missing role param');
}
});
return {
server: wss,
isChromeConnected: function() {
return clients
.map(function(ws) { return ws.upgradeReq.headers['user-agent']; })
.filter(Boolean)
.some(function(userAgent) { return userAgent.includes('Chrome'); });
return !!debuggerSocket;
}
};
}