chrome-remote-multiplex
Advanced tools
Comparing version 0.1.4 to 0.1.5
@@ -42,2 +42,4 @@ 'use strict'; | ||
var PACKAGE = require("../package.json"); | ||
var Logger = function () { | ||
@@ -58,2 +60,7 @@ function Logger() { | ||
}, { | ||
key: 'info', | ||
value: function info() { | ||
this.log.apply(this, arguments); | ||
} | ||
}, { | ||
key: 'error', | ||
@@ -293,2 +300,10 @@ value: function error() { | ||
_createClass(DevtoolsClient, [{ | ||
key: 'close', | ||
value: function close() { | ||
if (this.ws) { | ||
this.ws.close(); | ||
this.ws = null; | ||
} | ||
} | ||
}, { | ||
key: 'onMessageFromClient', | ||
@@ -381,2 +396,30 @@ value: function onMessageFromClient(data) { | ||
/** | ||
* Closes the connection to the server and clients | ||
*/ | ||
}, { | ||
key: 'close', | ||
value: function close() { | ||
if (this.ws) { | ||
var ws = this.ws; | ||
this.ws = null; | ||
ws.close(); | ||
this.devtoolsClients.forEach(function (client) { | ||
return client.close(); | ||
}); | ||
this.emit("close"); | ||
} | ||
} | ||
/** | ||
* Returns true if there are no devtools clients | ||
*/ | ||
}, { | ||
key: 'isUnused', | ||
value: function isUnused() { | ||
return this.devtoolsClients.length == 0; | ||
} | ||
/** | ||
* Detaches a DevTools client | ||
@@ -388,8 +431,13 @@ */ | ||
value: function detach(devtoolsClient) { | ||
var _this7 = this; | ||
for (var i = 0; i < this.devtoolsClients.length; i++) { | ||
if (this.devtoolsClients[i] === devtoolsClient) { | ||
this.devtoolsClients.splice(i, 1); | ||
return; | ||
break; | ||
} | ||
} | ||
}this.target.numberOfClients = this.devtoolsClients.length; | ||
if (!!this.ws && this.autoClose && this.devtoolsClients.length == 0) this.closeTarget().then(function () { | ||
return _this7.close(); | ||
}); | ||
} | ||
@@ -405,5 +453,22 @@ | ||
this.devtoolsClients.push(devtoolsClient); | ||
this.target.numberOfClients = this.devtoolsClients.length; | ||
} | ||
/** | ||
* Shutsdown the target | ||
*/ | ||
}, { | ||
key: 'closeTarget', | ||
value: function closeTarget() { | ||
var t = this; | ||
return httpGet({ | ||
hostname: t.multiplexServer.options.remoteClientHostname, | ||
port: t.multiplexServer.options.remoteClientPort, | ||
path: "/json/close/" + t.target.id, | ||
method: 'GET' | ||
}); | ||
} | ||
/** | ||
* Upgrade request from express | ||
@@ -550,3 +615,3 @@ */ | ||
var _this7 = _possibleConstructorReturn(this, (MultiplexServer.__proto__ || Object.getPrototypeOf(MultiplexServer)).call(this)); | ||
var _this8 = _possibleConstructorReturn(this, (MultiplexServer.__proto__ || Object.getPrototypeOf(MultiplexServer)).call(this)); | ||
@@ -565,3 +630,3 @@ options = options || {}; | ||
} | ||
_this7.options = { | ||
_this8.options = { | ||
listenPort: options.listenPort || 9223, | ||
@@ -571,4 +636,4 @@ remoteClientHostname: options.remoteClientHostname || "localhost", | ||
}; | ||
_this7.options.remoteClient = _this7.options.remoteClientHostname + ":" + _this7.options.remoteClientPort; | ||
return _this7; | ||
_this8.options.remoteClient = _this8.options.remoteClientHostname + ":" + _this8.options.remoteClientPort; | ||
return _this8; | ||
} | ||
@@ -579,2 +644,4 @@ | ||
value: function listen() { | ||
var _this9 = this; | ||
var t = this; | ||
@@ -594,3 +661,3 @@ | ||
var template = Dot.template('<html><body>\n <h1>Headless proxy</h1>\n <ul>\n {{~ it.multiplex.targets :target }}\n <li>\n <a href="{{= it.url(target) }}">\n {{= target.title }}\n </a>\n </li>\n {{~}}\n </ul>\n</body></html>'); | ||
var template = Dot.template('<html><body>\n <h1>Headless proxy</h1>\n <ul>\n {{~ it.multiplex.targets :target }}\n <li>\n <a href="{{= it.url(target) }}">\n {{= it.title(target) }}\n </a>\n </li>\n {{~}}\n </ul>\n</body></html>'); | ||
@@ -604,2 +671,7 @@ app.get('/', function (req, res) { | ||
return DEFAULT_DEVTOOLS_URL + target.webSocketDebuggerUrl.replace(/^ws:\/\//, "ws=/") + "&remoteFrontend=true"; | ||
}, | ||
title: function title(target) { | ||
var str = target.title; | ||
if (target.autoClose) str += " (set to auto-close)"; | ||
return str; | ||
} | ||
@@ -611,14 +683,117 @@ })); | ||
/* | ||
* JSON data showing the targets which can be connected to | ||
*/ | ||
function getTargetList(req, res) { | ||
function getContentType(response) { | ||
var contentType = response.headers["content-type"]; | ||
if (contentType) { | ||
var pos = contentType.indexOf(';'); | ||
contentType = contentType.substring(0, pos); | ||
} | ||
return contentType; | ||
} | ||
// Gets JSON from the remote server | ||
function getJson(path) { | ||
return httpGet({ | ||
hostname: t.options.remoteClientHostname, | ||
port: t.options.remoteClientPort, | ||
path: path, | ||
method: 'GET' | ||
}).then(function (obj) { | ||
var contentType = getContentType(obj.response); | ||
if (contentType !== "application/json") LOG.warn("Expecting JSON from " + path + " but found wrong content type: " + contentType); | ||
try { | ||
return JSON.parse(obj.data); | ||
} catch (ex) { | ||
LOG.warn("Cannot parse JSON returned from " + path); | ||
return null; | ||
} | ||
}); | ||
} | ||
// Gets JSON from the remote server and copies it to the client | ||
function copyToClient(req, res) { | ||
return httpGet({ | ||
hostname: t.options.remoteClientHostname, | ||
port: t.options.remoteClientPort, | ||
path: req.originalUrl, | ||
method: 'GET' | ||
}).then(function (obj) { | ||
var contentType = getContentType(obj.response); | ||
if (contentType) res.set("Content-Type", contentType); | ||
res.send(obj.data); | ||
}); | ||
} | ||
// REST API: list targets | ||
app.get(["/json", "/json/list"], function (req, res) { | ||
t.refreshTargets().then(function () { | ||
res.set('Content-Type', 'text/json'); | ||
res.set("Content-Type", "application/json"); | ||
res.send(JSON.stringify(t.targets, null, 2)); | ||
}).catch(reportHttpError.bind(this, req)); | ||
} | ||
app.get('/json', getTargetList); | ||
app.get('/json/list', getTargetList); | ||
}).catch(reportHttpError.bind(_this9, req)); | ||
}); | ||
// REST API: create a new target | ||
app.get('/json/new', function (req, res) { | ||
return getJson("/json/new").then(function (target) { | ||
if (target) target = t._addTarget(target); | ||
res.set("Content-Type", "application/json"); | ||
res.send(JSON.stringify(target, null, 2)); | ||
}); | ||
}); | ||
// REST API: close a target | ||
app.get('/json/close/*', function (req, res) { | ||
return httpGet({ | ||
hostname: t.options.remoteClientHostname, | ||
port: t.options.remoteClientPort, | ||
path: req.originalUrl, | ||
method: 'GET' | ||
}).then(function (obj) { | ||
var id = req.originalUrl.match(/\/json\/close\/(.*)$/)[1]; | ||
var proxy = t.proxies[id]; | ||
if (proxy) proxy.close(); | ||
var contentType = getContentType(obj.response); | ||
if (contentType) res.set("Content-Type", contentType); | ||
res.send(obj.data); | ||
}); | ||
}); | ||
// REST API: auto-close a target | ||
app.get('/json/auto-close/*', function (req, res) { | ||
var id = req.originalUrl.match(/\/json\/auto-close\/(.*)$/)[1]; | ||
var proxy = t._proxies[id]; | ||
if (proxy) { | ||
if (proxy.isUnused()) { | ||
proxy.closeTarget().close(); | ||
LOG.info("Closing target " + id + " due to /json/auto-close"); | ||
res.send("Target is closing"); | ||
} else { | ||
proxy.autoClose = true; | ||
t.targetsById[id].autoClose = true; | ||
LOG.info("Marking target " + id + " to auto close"); | ||
res.send("Target set to auto close"); | ||
} | ||
} else { | ||
var target = t.targetsById[id]; | ||
if (target) { | ||
target.autoClose = true; | ||
LOG.info("Marking target " + id + " to auto close after first use"); | ||
res.send("Target will close after first use"); | ||
} else res.status(500).send("Unrecognised target id " + id); | ||
} | ||
}); | ||
// REST API: get version numbers | ||
app.get('/json/version', function (req, res) { | ||
return getJson(req.originalUrl).then(function (json) { | ||
json["Chrome-Remote-Multiplex-Version"] = PACKAGE.version; | ||
res.set("Content-Type", "application/json"); | ||
res.send(JSON.stringify(json, null, 2)); | ||
}); | ||
}); | ||
app.get('/json/protocol', copyToClient); | ||
app.get('/json/activate', copyToClient); | ||
var webServer = Http.createServer(app); | ||
@@ -665,2 +840,12 @@ var proxies = this._proxies = {}; | ||
}); | ||
proxy.on('close', function () { | ||
delete t.targetsById[uuid]; | ||
for (var i = 0; i < t.targets.length; i++) { | ||
if (t.targets[i].id === uuid) { | ||
t.targets.splice(i, 1); | ||
break; | ||
} | ||
} | ||
}); | ||
if (target.autoClose) proxy.autoClose = true; | ||
} else { | ||
@@ -702,5 +887,5 @@ return proxy.upgrade(request, socket, head); | ||
method: 'GET' | ||
}).then(function (data) { | ||
}).then(function (obj) { | ||
var json = null; | ||
if (!data) { | ||
if (!obj.data) { | ||
LOG.debug("No data received from " + t.options.remoteClient); | ||
@@ -710,3 +895,3 @@ return; | ||
try { | ||
json = JSON.parse(data); | ||
json = JSON.parse(obj.data); | ||
} catch (ex) { | ||
@@ -716,16 +901,37 @@ LOG.error("Error while parsing JSON from " + t.options.remoteClient + ": " + ex); | ||
} | ||
var oldTargets = t.targets; | ||
var oldTargetsById = t.targetsById || {}; | ||
t.targets = json; | ||
t.targetsById = {}; | ||
var regex = new RegExp("localhost:" + t.options.remoteClientPort); | ||
json.forEach(function (target) { | ||
if (target.devtoolsFrontendUrl) target.originalDevtoolsFrontendUrl = target.devtoolsFrontendUrl; | ||
target.devtoolsFrontendUrl = "/devtools/inspector.html?ws=localhost:" + t.options.listenPort + "/devtools/page/" + target.id; | ||
if (target.webSocketDebuggerUrl) target.originalWebSocketDebuggerUrl = target.webSocketDebuggerUrl; | ||
target.webSocketDebuggerUrl = "ws://localhost:" + t.options.listenPort + "/devtools/page/" + target.id; | ||
target.title += " (proxied)"; | ||
t.targetsById[target.id] = target; | ||
}); | ||
for (var i = 0; i < t.targets.length; i++) { | ||
var target = t.targets[i]; | ||
t.targets[i] = t._addTarget(target, oldTargetsById[target.id]); | ||
} | ||
}); | ||
} | ||
/** | ||
* Adds a target | ||
*/ | ||
}, { | ||
key: '_addTarget', | ||
value: function _addTarget(src, target) { | ||
var RESERVED_KEYWORDS = ["description", "id", "title", "type", "url", "devtoolsFrontendUrl", "webSocketDebuggerUrl", "originalDevtoolsFrontendUrl", "originalWebSocketDebuggerUrl"]; | ||
var t = this; | ||
if (!target) target = {}; | ||
for (var name in src) { | ||
target[name] = src[name]; | ||
}if (target.devtoolsFrontendUrl) target.originalDevtoolsFrontendUrl = target.devtoolsFrontendUrl; | ||
target.devtoolsFrontendUrl = "/devtools/inspector.html?ws=localhost:" + t.options.listenPort + "/devtools/page/" + target.id; | ||
if (target.webSocketDebuggerUrl) target.originalWebSocketDebuggerUrl = target.webSocketDebuggerUrl; | ||
target.webSocketDebuggerUrl = "ws://localhost:" + t.options.listenPort + "/devtools/page/" + target.id; | ||
target.title += " (proxied)"; | ||
if (target.numberOfClients === undefined) target.numberOfClients = 0; | ||
return t.targetsById[target.id] = target; | ||
} | ||
}]); | ||
@@ -753,3 +959,3 @@ | ||
response.on('end', function () { | ||
resolve(str); | ||
resolve({ data: str, response: response }); | ||
}); | ||
@@ -756,0 +962,0 @@ }); |
{ | ||
"name": "chrome-remote-multiplex", | ||
"version": "0.1.4", | ||
"version": "0.1.5", | ||
"description": "Allows multiple Chrome DevTools Clients to connect to a single Remote Debugger (ie Chrome Headless) instance", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -6,7 +6,9 @@ # chrome-remote-multiplex | ||
Google Chrome Headless (or any other Devtools Protocol implementation) only allows one client to control | ||
it at any particular time; this means that if you have an application which uses https://github.com/cyrus-and/chrome-remote-interface | ||
it at any particular time; this means that if you have an application which uses | ||
[chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) | ||
to operate the web page, you cannot debug that web page while it is being controlled by your application. | ||
By using https://github.com/johnspackman/chrome-remote-multiplex you can work around this restriction, by connecting your app and your | ||
debugger(s) to chrome-remote-multiplex and allowing it to handle the single connection to Chrome Headless. | ||
By using [chrome-remote-multiplex](https://github.com/johnspackman/chrome-remote-multiplex) you can work | ||
around this restriction, by connecting your app and your debugger(s) to chrome-remote-multiplex and allowing | ||
it to handle the single connection to Chrome Headless. | ||
@@ -31,2 +33,54 @@ | ||
## Managing Lifecycle - automatically closing Chrome Tabs | ||
When instrumenting Chrome Headless, you will often create and close instances - for example, [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) | ||
has this example use of the command line to create a instance (i.e. a tab or window) and then close it again: | ||
``` | ||
$ chrome-remote-interface new 'http://example.com' | ||
{ | ||
"description": "", | ||
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01", | ||
"id": "b049bb56-de7d-424c-a331-6ae44cf7ae01", | ||
"thumbnailUrl": "/thumb/b049bb56-de7d-424c-a331-6ae44cf7ae01", | ||
"title": "", | ||
"type": "page", | ||
"url": "http://example.com/", | ||
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01" | ||
} | ||
$ chrome-remote-interface close 'b049bb56-de7d-424c-a331-6ae44cf7ae01' | ||
``` | ||
Or as http requests, try this in your browser: | ||
- `localhost:9222/json/new` -- output the new instance information | ||
- `localhost:9222/json/close/{id}` -- where `{id}` is taken from the output of `/json/new` | ||
If your application is running in a server environment, you obviously need to make sure that you keep track of all of the | ||
instances that you create via the `new` command and make sure that you `close` them when they're no longer needed. | ||
While this is straightforward to do in ideal circumstances, in a complex server application it can become tricky to manage | ||
those instances, especially if you want to recover gracefully from application crashes or occasionally want to sneak in | ||
with a separate connection and keep the the instance open while you debug it. | ||
[chrome-remote-multiplex](https://github.com/johnspackman/chrome-remote-multiplex) adds an automatic close function that | ||
tracks connections and when the last one has disconnected from an instance, the instance itself is closed down. This means | ||
that even if your application crashes, the ab is cleaned up properly because the operating system will close the socket which | ||
will disconnect from the MultiplexServer and then cause the tab to be removed also - this is garbage collection for your browser tabs. | ||
To make a tab automatically close, use the new `/json/auto-close/{id}` API, for example: | ||
``` | ||
$ chrome-remote-interface -p 9223 new 'http://www.google.co.uk' | ||
# Let's say the output from the above command has an "id" of "b049bb56-de7d-424c-a331-6ae44cf7ae01" | ||
$ # use the REST API to make the new tab auto-close | ||
$ wget -O- http://localhost:9223/json/auto-close/b049bb56-de7d-424c-a331-6ae44cf7ae01 | ||
``` | ||
Now browse to `http://localhost:9223` and click on the link to start debugging your new tab; when you close that debugger and | ||
go back to the `http://localhost:9223` you will see that the tab you just finished debugging has gone. | ||
Note that if the instance has never been connected to, then it will only be closed once you have connected a DevTools client(s) to it | ||
and the last client has disconnected; if you have previously connected and closed a DevTools client, the instance will close immmediately. | ||
## Embedding in your application | ||
@@ -50,3 +104,3 @@ You can embed the multiplex proxy server in your own application: | ||
There is a full example in 'https://github.com/johnspackman/chrome-remote-multiplex/blob/master/example/embed.js' | ||
There is a full example in [example/embed.js](https://github.com/johnspackman/chrome-remote-multiplex/blob/master/example/embed.js) | ||
@@ -53,0 +107,0 @@ |
Sorry, the diff of this file is not supported yet
89264
8
853
112