diff --git a/Gruntfile.js b/Gruntfile.js index 535ac130e..a4286d9da 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -103,6 +103,9 @@ module.exports = function(grunt) { (grunt.option('rebuild') ? ' -r' : ''), cwd: 'utils/playerglobal-builder' }, + debug_server: { + cmd: 'node examples/inspector/debug/server.js' + }, gate: { cmd: '"utils/jsshell/js" build/ts/shell.js -x -g ' + (grunt.option('verbose') ? '-v ' : '') + diff --git a/examples/inspector/debug/pingpong.js b/examples/inspector/debug/pingpong.js new file mode 100644 index 000000000..2438f22f7 --- /dev/null +++ b/examples/inspector/debug/pingpong.js @@ -0,0 +1,130 @@ +/* + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Simple class for synchronous XHR communication. +// See also examples/inspector/debug/server.js. + +var PingPongConnection = (function () { + function PingPongConnection(url, onlySend) { + this.url = url; + this.onData = null; + this.onError = null; + this.currentXhr = null; + this.closed = false; + + if (!onlySend) { + this.idle(); + } + } + + PingPongConnection.prototype = { + idle: function () { + function requestIncoming(connection) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', connection.url + '?idle', true); + xhr.onload = function () { + if (xhr.status === 204 && + xhr.getResponseHeader('X-PingPong-Error') === 'timeout') { + requestIncoming(connection); + return; + } + if (xhr.status === 200) { + var result; + if (connection.onData) { + var response = xhr.responseText; + result = connection.onData(response ? JSON.parse(response) : undefined); + } + if (xhr.getResponseHeader('X-PingPong-Async') === '1') { + requestIncoming(connection); + } else { + sendResponse(connection, result); + } + return; + } + + if (connection.onError) { + connection.onError(xhr.statusText); + } + }; + xhr.onerror = function () { + if (connection.onError) { + connection.onError(xhr.error); + } + }; + xhr.send(); + connection.currentXhr = xhr; + } + function sendResponse(connection, result) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', connection.url + '?response', false); + xhr.onload = function () { + if (xhr.status !== 204) { + if (connection.onError) { + connection.onError(xhr.statusText); + } + } + requestIncoming(connection); + }; + xhr.onerror = function () { + if (connection.onError) { + connection.onError(xhr.error); + } + }; + xhr.send(result === undefined ? '' : JSON.stringify(result)); + connection.currentXhr = xhr; + } + requestIncoming(this); + }, + send: function (data, async, timeout) { + if (this.closed) { + throw new Error('connection closed'); + } + + async = !!async; + timeout |= 0; + + var encoded = data === undefined ? '' : JSON.stringify(data); + if (async) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', this.url + '?async', true); + xhr.send(encoded); + return; + } else { + var xhr = new XMLHttpRequest(); + xhr.open('POST', this.url, false); + if (timeout > 0) { + xhr.setRequestHeader('X-PingPong-Timeout', timeout); + } + xhr.send(encoded); + if (xhr.status === 204 && + xhr.getResponseHeader('X-PingPong-Error') === 'timeout') { + throw new Error('sync request timeout'); + } + var response = xhr.responseText; + return response ? JSON.parse(response) : undefined; + } + }, + close: function () { + if (this.currentXhr) { + this.currentXhr.abort(); + this.currentXhr = null; + } + this.closed = true; + } + }; + + return PingPongConnection; +})(); diff --git a/examples/inspector/debug/server.js b/examples/inspector/debug/server.js new file mode 100644 index 000000000..15b6bb5b6 --- /dev/null +++ b/examples/inspector/debug/server.js @@ -0,0 +1,217 @@ +/* + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +// Simple HTTP server for synchronous XHR communication. +// See also examples/inspector/debug/pingpong.js. + +'use strict'; + +var http = require('http'); + +var ALLOW_FROM_DOMAIN = 'http://localhost:8000'; +var DEFAULT_BIND_HOST = 'localhost'; +var DEFAULT_BIND_PORT = 8010; + +var IDLE_TIMEOUT = 500; +var SYNC_TIMEOUT = 120000; + +var incomingData = {}; +var incomingResponse = {}; +var outgoingResponse = {}; +var currentReading = []; + +var verbose = false; + +function WebServer() { + this.host = DEFAULT_BIND_HOST; + this.port = DEFAULT_BIND_PORT; + this.server = null; +} +WebServer.prototype = { + start: function (callback) { + this.server = http.createServer(this._handler.bind(this)); + this.server.listen(this.port, this.host, callback); + console.log( + 'Server running at http://' + this.host + ':' + this.port + '/'); + console.log('Allowing requests from: ' + ALLOW_FROM_DOMAIN); + }, + stop: function (callback) { + this.server.close(callback); + this.server = null; + }, + _handler: function (request, response) { + function setStandardHeaders(response) { + response.setHeader('Access-Control-Allow-Origin', ALLOW_FROM_DOMAIN); + response.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-PingPong-Timeout'); + response.setHeader('Access-Control-Expose-Headers', 'Content-Type,X-PingPong-Async,X-PingPong-From,X-PingPong-Error'); + response.setHeader('Content-Type', 'text/plain'); + response.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + response.setHeader('Pragma', 'no-cache'); + response.setHeader('Expires', 0); + } + + function sendData(response, data, isAsync, fromId) { + setStandardHeaders(response); + response.setHeader('Content-Type', 'text/plain'); + if (isAsync) { + response.setHeader('X-PingPong-Async', 1); + } + response.setHeader('X-PingPong-From', fromId); + response.writeHead(200); + response.end(data); + } + + function sendNoData(response) { + setStandardHeaders(response); + response.writeHead(204); + response.end(); + } + + function sendTimeout(response) { + setStandardHeaders(response); + response.setHeader('X-PingPong-Error', 'timeout'); + response.writeHead(204); + response.end(); + } + + var method = request.method; + if (request.method === 'OPTIONS') { + setStandardHeaders(response); + response.writeHead(200); + response.end(); + return; + } + + var url = request.url; + var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url); + var pathParts = urlParts[1].split('/'); + var queryPart = urlParts[3]; + + var sessionId = pathParts[1], fromId = pathParts[2], toId = pathParts[3]; + var isResponse = queryPart === 'response', isAsync = queryPart === 'async'; + verbose && console.log(sessionId + ': ' + fromId + '->' + toId + ' ' + + isResponse + ' ' + isAsync + ' ' + request.method); + var keyId = sessionId + '_' + fromId + '_' + toId; + var reverseKeyId = sessionId + '_' + toId + '_' + fromId; + + if (request.method === 'POST') { + response.on('close', function () { + verbose && console.log('connection closed'); // TODO client closed without response.end + }); + + var body = ''; + request.on('data', function (data) { + body += data; + }); + request.on('end', function () { + verbose && console.log(' ... ' + body.substring(0, 140)); + item.isReady = true; + while (currentReading.length > 0 && currentReading[0].isReady) { + currentReading.shift().fn(); + } + }); + + var item = { + isReady: false, + fn: function () { + if (isResponse) { + if (outgoingResponse[reverseKeyId]) { + sendData(outgoingResponse[reverseKeyId].shift().response, body, true, fromId); + if (outgoingResponse[reverseKeyId].length === 0) { + delete outgoingResponse[reverseKeyId]; + } + } else { + console.error('Out of sequence response for ' + reverseKeyId); + } + sendNoData(response); + } else { + if (!isAsync) { + if (!outgoingResponse[keyId]) { + outgoingResponse[keyId] = []; + } + var requestTimeout = +request.headers['x-pingpong-timeout']; + var syncTimeout = requestTimeout || SYNC_TIMEOUT; + outgoingResponse[keyId].push({response: response}); + setTimeout(function () { + var responses = outgoingResponse[keyId]; + if (!responses) { + return; + } + for (var i = 0; i < responses.length; i++) { + if (responses[i].response === response) { + if (responses.length === 1) { + delete outgoingResponse[keyId]; + } else { + responses.splice(i, 1); + } + sendTimeout(response); + if (!requestTimeout) { + console.error('Sync request timeout: ' + keyId); + } + break; + } + } + }, syncTimeout); + } else { + sendNoData(response); + } + if (incomingResponse[reverseKeyId]) { + sendData(incomingResponse[reverseKeyId].response, body, isAsync, fromId); + delete incomingResponse[reverseKeyId]; + } else { + if (!incomingData[reverseKeyId]) { + incomingData[reverseKeyId] = []; + } + incomingData[reverseKeyId].push({data: body, isAsync: isAsync}); + } + } + } + }; + currentReading.push(item); + return; + } + + if (request.method == 'GET' && !isResponse) { + if (incomingData[keyId]) { + var data = incomingData[keyId].shift(); + sendData(response, data.data, data.isAsync, toId); + if (incomingData[keyId].length === 0) { + delete incomingData[keyId]; + } + } else { + if (incomingResponse[keyId]) { + console.error('Double incoming response from ' + keyId); + } + incomingResponse[keyId] = {response: response}; + } + setTimeout(function () { + if (incomingResponse[keyId] && incomingResponse[keyId].response === response) { + delete incomingResponse[keyId]; + sendTimeout(response); + } + }, IDLE_TIMEOUT); + return; + } + + setStandardHeaders(response); + response.writeHead(500); + response.end('Invalid request'); + } +}; + +var server = new WebServer(); +server.start(); diff --git a/examples/inspector/inspector.html b/examples/inspector/inspector.html index 5b9b2746a..90e130948 100644 --- a/examples/inspector/inspector.html +++ b/examples/inspector/inspector.html @@ -160,6 +160,7 @@ limitations under the License. + @@ -168,6 +169,7 @@ limitations under the License. +
+ +