You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2014/05/15 19:30:55 UTC

[2/3] Re-write server in JS via chrome.socket.

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/b826b671/www/cdvah/js/HttpServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HttpServer.js b/www/cdvah/js/HttpServer.js
new file mode 100644
index 0000000..74ca771
--- /dev/null
+++ b/www/cdvah/js/HttpServer.js
@@ -0,0 +1,327 @@
+(function() {
+    'use strict';
+    /* global myApp */
+    /* global chrome */
+    myApp.factory('HttpServer', ['$q', function($q) {
+        var DEFAULT_PORT = 2424;
+
+        var STATE_NEW = 0;
+        var STATE_REQUEST_DATA_RECEIVED = 1;
+        var STATE_HEADERS_RECEIVED = 2;
+        var STATE_REQUEST_RECEIVED = 3;
+        var STATE_RESPONSE_STARTED = 4;
+        var STATE_COMPLETE = 5;
+
+        function HttpRequest(requestData) {
+            this._requestData = requestData;
+            this.method = requestData.method;
+            this.headers = requestData.headers;
+            this.bytesRemaining = 0;
+            if (requestData.method == 'POST' || requestData.method == 'PUT') {
+                this.bytesRemaining = parseInt(requestData.headers['content-length'] || '0');
+            }
+            this.onData = null; // function(arrayBuffer, req, resp) : Promise
+
+            var host = this.headers['host'] || 'localhost';
+            var queryMatch = /\?.*/.exec(requestData.resource);
+            this.url = 'http://' + host + requestData.resource;
+            this.path = requestData.resource.replace(/\?.*/, '');
+            this.query = queryMatch ? queryMatch[0] : '';
+        }
+
+        HttpRequest.prototype.getQueryParam = function(name) {
+            var pattern = new RegExp('[\\?&]' + name + '=([^&#]*)');
+            var m = pattern.exec(this.query);
+            return m && decodeURIComponent(m[1]);
+        };
+
+        HttpRequest.prototype._feedData = function(arrayBuffer) {
+            console.log('Processing request chunk of size ' + arrayBuffer.byteLength);
+            this.bytesRemaining -= arrayBuffer.byteLength;
+            if (this.bytesRemaining < 0) {
+                throw new Error('Bytes remaining negative: ' + this.bytesRemaining);
+            } else if (this.bytesRemaining === 0 && this._requestData.state === STATE_HEADERS_RECEIVED) {
+                this._requestData.state = STATE_REQUEST_RECEIVED;
+            }
+            if (arrayBuffer.byteLength > 0) {
+                if (this.onData) {
+                    return this.onData(arrayBuffer, this, this._requestData.httpResponse);
+                } else {
+                    // Set to an empty function to avoid this warning.
+                    console.warn('onData not set when callback is fired for request: ' + this.getUrl());
+                }
+            }
+            return $q.when();
+        };
+
+        function HttpResponse(requestData) {
+            this._requestData = requestData;
+            this.headers = Object.create(null);
+            var keepAlive = requestData.headers['connection'] === 'keep-alive';
+            this.headers['Connection'] = keepAlive ? 'keep-alive' : 'close';
+            this._writeQueue = [];
+        }
+
+        HttpResponse.prototype.sendTextResponse = function(status, message, /* optional */ contentType) {
+            this.headers['Content-Type'] = contentType || 'text/plain';
+            this.headers['Content-Length'] = message.length;
+            this._startResponse(status);
+            this.writeChunk(stringToArrayBuffer(message));
+            this.close();
+        };
+
+        HttpResponse.prototype.sendJsonResponse = function(json) {
+            this.sendTextResponse(200, JSON.stringify(json, null, 4), 'application/json');
+        };
+
+        HttpResponse.prototype.writeChunk = function(arrayBuffer) {
+            if (this._requestData.state !== STATE_RESPONSE_STARTED) {
+                this._startResponse(200);
+            }
+            this._addToWriteQueue(arrayBuffer);
+        };
+
+        HttpResponse.prototype.close = function() {
+            this.writeChunk(null);
+        };
+
+        HttpResponse.prototype._addToWriteQueue = function(arrayBuffer) {
+            this._writeQueue.push(arrayBuffer);
+            if (this._writeQueue.length === 1) {
+                this._pokeWriteQueue();
+            }
+        };
+
+        HttpResponse.prototype._pokeWriteQueue = function() {
+            var arrayBuffer = this._writeQueue[0];
+            if (arrayBuffer) {
+                var self = this;
+                chrome.socket.write(this._requestData.socketId, arrayBuffer, function(writeInfo) {
+                    if (writeInfo.bytesWritten !== arrayBuffer.byteLength) {
+                        console.warn('Failed to write entire ArrayBuffer.');
+                    }
+                    self._writeQueue.shift();
+                    if (writeInfo.bytesWritten < 0) {
+                        console.error('Write error: ' + -writeInfo.bytesWritten);
+                        self._finish(true);
+                    } else {
+                        self._pokeWriteQueue();
+                    }
+                });
+            } else if (arrayBuffer === null) {
+                this._finish();
+            }
+        };
+
+        HttpResponse.prototype._startResponse = function(status) {
+            var headers = this.headers;
+            if (this._requestData.httpRequest.bytesRemaining > 0) {
+                headers['Connection'] = 'Close';
+            }
+            this._requestData.state = STATE_RESPONSE_STARTED;
+            var statusMsg = status === 404 ? 'Not Found' :
+                            status === 400 ? 'Bad Request' :
+                            status === 200 ? 'OK' :
+                            'meh';
+            var lines = ['HTTP/1.1 ' + status + ' ' + statusMsg];
+            Object.keys(headers).forEach(function(k) {
+                lines.push(k + ': ' + headers[k]);
+            });
+            lines.push('', '');
+            this.writeChunk(stringToArrayBuffer(lines.join('\r\n')));
+        };
+
+        HttpResponse.prototype._finish = function(disconnect) {
+            this._requestData.state = STATE_COMPLETE;
+            var socketId = this._requestData.socketId;
+            if (typeof disconnect == 'undefined') {
+                disconnect = (this.headers['Connection'] || '').toLowerCase() != 'keep-alive';
+            }
+            delete this._requestData.httpServer._requests[socketId];
+            if (disconnect) {
+                chrome.socket.destroy(socketId);
+            } else {
+                this._requestData.httpServer._onAccept(socketId);
+            }
+        };
+
+        function HttpServer() {
+            this._requests = Object.create(null); // Map of socketId -> Object
+            this._handlers = Object.create(null); // Map of resourcePath -> function(httpRequest, httpResponse)
+        }
+
+        HttpServer.prototype.addRoute = function(path, func) {
+            this._handlers[path] = func;
+            return this;
+        };
+
+        HttpServer.prototype.start = function(/* optional */ port) {
+            port = port || DEFAULT_PORT;
+            var deferred = $q.defer();
+            var boundAccept = this._onAccept.bind(this);
+            console.log('Starting web server on port ' + port);
+            chrome.socket.create('tcp', function(createInfo) {
+                if (!createInfo) {
+                    console.error('Failed to create socket: ' + chrome.runtime.lastError);
+                    deferred.reject(new Error('Failed to create socket: ' + chrome.runtime.lastError));
+                    return;
+                }
+                chrome.socket.listen(createInfo.socketId, '0.0.0.0', port, function(result) {
+                    if (result === 0) {
+                        acceptLoop(createInfo.socketId, boundAccept);
+                        deferred.resolve();
+                    } else {
+                        console.error('Error on socket.listen: ' + result);
+                        deferred.reject(new Error('Error on socket.listen: ' + result));
+                    }
+                });
+            });
+            return deferred.promise;
+        };
+
+        HttpServer.prototype._onAccept = function(socketId) {
+            console.log('Connection established on socket ' + socketId);
+            var requestData = {
+                state: STATE_NEW,
+                dataAsStr: '', // Used only when parsing head of request.
+                socketId: socketId,
+                method: null,
+                resource: null,
+                httpVersion: null,
+                headers: null,
+                httpServer: this,
+                httpResponse: null,
+                httpRequest: null
+            };
+            this._requests[socketId] = requestData;
+            receiveHttpData(requestData);
+        };
+
+        HttpServer.prototype._onReceivedRequest = function(requestData) {
+            var req = new HttpRequest(requestData);
+            var resp = new HttpResponse(requestData);
+            requestData.httpRequest = req;
+            requestData.httpResponse = resp;
+            // Strip query params.
+            var handler = this._handlers[req.path];
+            if (handler) {
+                handler(req, resp);
+            } else {
+                resp.sendTextResponse(404, 'Not Found');
+            }
+        };
+
+        function acceptLoop(socketId, acceptCallback) {
+            chrome.socket.accept(socketId, function(acceptInfo) {
+                acceptCallback(acceptInfo.socketId);
+                acceptLoop(socketId, acceptCallback);
+            });
+        }
+
+        function stringToArrayBuffer(str) {
+            var view = new Uint8Array(str.length);
+            for (var i = 0; i < str.length; i++) {
+                view[i] = str.charCodeAt(i);
+            }
+            return view.buffer;
+        }
+
+        function arrayBufferToString(buffer) {
+            var str = '';
+            var uArrayVal = new Uint8Array(buffer);
+            for (var s = 0; s < uArrayVal.length; s++) {
+                str += String.fromCharCode(uArrayVal[s]);
+            }
+            return str;
+        }
+
+        function receiveHttpData(requestData) {
+            if (requestData.state < STATE_REQUEST_RECEIVED) {
+                chrome.socket.read(requestData.socketId, function(readInfo) {
+                    processHttpRequest(requestData, readInfo.data);
+                });
+            }
+        }
+
+        function processHttpRequest(requestData, arrayBuffer) {
+            switch (requestData.state) {
+                case STATE_NEW:
+                case STATE_REQUEST_DATA_RECEIVED:
+                    var oldLen = requestData.dataAsStr.length;
+                    var newData = arrayBufferToString(arrayBuffer);
+                    var splitPoint;
+                    requestData.dataAsStr += newData;
+                    if (requestData.state === STATE_NEW) {
+                        splitPoint = requestData.dataAsStr.indexOf('\r\n');
+                        if (splitPoint > -1) {
+                            var requestDataLine = requestData.dataAsStr.substring(0, splitPoint);
+                            requestData.dataAsStr = '';
+                            arrayBuffer = arrayBuffer.slice(splitPoint + 2 - oldLen);
+                            var requestDataParts = requestDataLine.split(' ');
+                            requestData.method = requestDataParts[0].toUpperCase();
+                            requestData.resource = requestDataParts[1];
+                            requestData.httpVersion = requestDataParts[2];
+                            console.log(requestData.method + ' requestData received for ' + requestData.resource);
+                            requestData.state = STATE_REQUEST_DATA_RECEIVED;
+                            processHttpRequest(requestData, arrayBuffer);
+                            return;
+                        }
+                    } else {
+                        splitPoint = requestData.dataAsStr.indexOf('\r\n\r\n');
+                        if (splitPoint > -1) {
+                            requestData.headers = parseHeaders(requestData.dataAsStr.substring(0, splitPoint));
+                            requestData.dataAsStr = '';
+                            arrayBuffer = arrayBuffer.slice(splitPoint + 4 - oldLen);
+                            requestData.state = STATE_HEADERS_RECEIVED;
+                            requestData.httpServer._onReceivedRequest(requestData);
+                            processHttpRequest(requestData, arrayBuffer);
+                            return;
+                        }
+                    }
+                    break;
+                case STATE_HEADERS_RECEIVED:
+                    requestData.httpRequest._feedData(arrayBuffer)
+                    .then(function() {
+                        receiveHttpData(requestData);
+                    }, function(e) {
+                        requestData.httpResponse.sendTextResponse(500, '' + e);
+                    });
+                    return;
+            }
+            receiveHttpData(requestData);
+        }
+
+        function strip(str) {
+            return str.replace(/^\s*|\s*$/g, '');
+        }
+
+        function parseHeaders(headerText) {
+            var headers = Object.create(null);
+            var headerLines = headerText.split('\r\n');
+            var currentKey;
+            for (var i = 0; i < headerLines.length; i++) {
+                if (/^\s/.test(headerLines[i])) {
+                    if (!currentKey) {
+                        break;
+                    }
+                    headers[currentKey] += ' ' + strip(headerLines[i]);
+                } else {
+                    var splitPoint = headerLines[i].indexOf(':');
+                    if (splitPoint == -1) {
+                        break;
+                    }
+                    currentKey = strip(headerLines[i].substring(0,splitPoint).toLowerCase());
+                    headers[currentKey] = strip(headerLines[i].substring(splitPoint+1));
+                }
+            }
+            return headers;
+        }
+
+        return {
+            create: function() {
+                return new HttpServer();
+            }
+        };
+
+    }]);
+})();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/b826b671/www/cdvah/js/ListCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ListCtrl.js b/www/cdvah/js/ListCtrl.js
index 0e74f28..7fce9c9 100644
--- a/www/cdvah/js/ListCtrl.js
+++ b/www/cdvah/js/ListCtrl.js
@@ -1,72 +1,29 @@
 (function(){
     'use strict';
     /* global myApp */
-    /* global appharness */
-    myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService) {
+    myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer) {
         $scope.appList = [];
         $rootScope.appTitle = document.title;
 
         function initialise() {
-            return $scope.loadAppsList()
+            $scope.$on('$destroy', function() {
+                AppsService.onAppListChange = null;
+            });
+            AppsService.onAppListChange = loadAppsList;
+            return loadAppsList()
             .then(function() {
-                if (!window.appharness || !appharness.push) {
-                    return;
-                }
-
-                // listen is a no-op if already listening.
-                appharness.push.listen(function() {
-                    $scope.listening = true;
-                    $scope.$apply();
-                }, notifier.error);
-
-                appharness.push.getListenAddress(function(value) {
+                return HarnessServer.start();
+            }).then(function() {
+                return HarnessServer.getListenAddress()
+                .then(function(value) {
                     $scope.ipAddress = value;
-                    $scope.$apply();
-                });
-
-                appharness.push.pending(function(e) {
-                    var type = e.type;
-                    var extra = e.extra;
-                    if (type == 'updateApp') {
-                        AppsService.getAppList().then(function(list) {
-                            var matches = list && list.filter(function(x) { return x.appId == extra.name; });
-                            var promise;
-                            if (list && matches.length > 0) {
-                                // App exists.
-                                var app = matches[0];
-                                app.url = extra.url;
-                                promise = $q.when(app);
-                            } else {
-                                // New app.
-                                var handler;
-                                promise = AppsService.addApp(extra.type, extra.url, extra.name).then(function(h) {
-                                    handler = h;
-                                    var msg = 'Added new app ' + handler.appId + ' from push';
-                                    notifier.success(msg);
-                                }).then(function() {
-                                    // Reload so the app is visible while it's updating (below).
-                                    return $scope.loadAppsList().then(function() {
-                                        return handler;
-                                    });
-                                });
-                            }
-
-                            promise.then(function(theApp) {
-                                return AppsService.updateApp(theApp)
-                                .then(function() {
-                                    notifier.success('Updated ' + theApp.appId + ' due to remote push.');
-                                    return $scope.launchApp(theApp, { stopPropagation: function() { } });
-                                })
-                            }).then(null, function(err) {
-                                notifier.error(err);
-                            });
-                        });
-                    }
                 });
+            }, function() {
+                $scope.ipAddress = 'Failed to start server';
             });
         }
 
-        $scope.loadAppsList = function() {
+        function loadAppsList() {
             return AppsService.getAppList()
             .then(function(newAppsList){
                 newAppsList.sort(function(a, b){
@@ -81,7 +38,7 @@
             }, function(error){
                 notifier.error(error);
             });
-        };
+        }
 
         $scope.launchApp = function(app, event){
             event.stopPropagation();
@@ -106,9 +63,7 @@
             var shouldUninstall = confirm('Are you sure you want to uninstall ' + app.appId + '?');
             if(shouldUninstall) {
                 return AppsService.uninstallApp(app)
-                .then(function() {
-                    return $scope.loadAppsList();
-                }, function(error) {
+                .then(null, function(error) {
                     notifier.error(error);
                 });
             }
@@ -123,8 +78,7 @@
             $location.path('/details/' + index);
         };
 
-        initialise();
-
+        return initialise();
     }]);
 })();
 

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/b826b671/www/cdvah/js/ServeInstaller.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ServeInstaller.js b/www/cdvah/js/ServeInstaller.js
index ae812cd..05eca96 100644
--- a/www/cdvah/js/ServeInstaller.js
+++ b/www/cdvah/js/ServeInstaller.js
@@ -42,7 +42,7 @@
         function fetchMetaServeData(url) {
             var projectJsonUrl = url + '/' + platformId + '/project.json';
             return ResourcesLoader.xhrGet(projectJsonUrl, true)
-            .then(null, function(e) {
+            .then(null, function() {
                 // If there was no :8000, try again with one appended.
                 if (!/:(\d)/.test(url)) {
                     var newUrl = url.replace(/(.*?\/\/[^\/]*)/, '$1:8000');
@@ -52,7 +52,7 @@
                         return ResourcesLoader.xhrGet(projectJsonUrl, true);
                     }
                 }
-                throw e;
+                throw new Error('Could not reach server at: ' + url);
             })
             .then(function(projectJson) {
                 return ResourcesLoader.xhrGet(url + projectJson.configPath)

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/b826b671/www/cdvah/views/list.html
----------------------------------------------------------------------
diff --git a/www/cdvah/views/list.html b/www/cdvah/views/list.html
index aa368b8..50c9c8e 100644
--- a/www/cdvah/views/list.html
+++ b/www/cdvah/views/list.html
@@ -18,10 +18,9 @@
 <br />
 <div class="buttons">
     <a href="#/add"><button class="topcoat-button--cta">Add app</button></a>
-    <button class="topcoat-button" ng-click="loadAppsList()">Reload</button>
 </div>
 
-<div class="listen" ng-show="listening">
+<div class="listen">
     Listening. IP = <strong>{{ipAddress}}</strong>
 </div>