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/23 20:06:02 UTC

[1/3] git commit: Delete /prepupdate action, add concept of manifest etag

Repository: cordova-app-harness
Updated Branches:
  refs/heads/master 73ca9ca31 -> e18f4bfed


Delete /prepupdate action, add concept of manifest etag


Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/79b69286
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/79b69286
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/79b69286

Branch: refs/heads/master
Commit: 79b69286cbc8e56b22f54e8b9cd90192fc531cc9
Parents: 73ca9ca
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:02:11 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:02:11 2014 -0400

----------------------------------------------------------------------
 .jshintignore                    |   1 -
 www/cdvah/js/AppsService.js      |   6 +-
 www/cdvah/js/CordovaInstaller.js |   7 +-
 www/cdvah/js/DirectoryManager.js |  80 ++++++++++++++---------
 www/cdvah/js/HarnessServer.js    | 117 +++++++++++++++++++++-------------
 www/cdvah/js/HttpServer.js       |  31 ++++++---
 www/cdvah/js/Installer.js        |   6 +-
 www/cdvah/js/ListCtrl.js         |   5 +-
 www/cdvah/js/app.js              |   2 +
 9 files changed, 157 insertions(+), 98 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/.jshintignore
----------------------------------------------------------------------
diff --git a/.jshintignore b/.jshintignore
deleted file mode 100644
index 4e6ee77..0000000
--- a/.jshintignore
+++ /dev/null
@@ -1 +0,0 @@
-www/cdvah_js/libs/*
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/AppsService.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/AppsService.js b/www/cdvah/js/AppsService.js
index aac7856..3063fc2 100644
--- a/www/cdvah/js/AppsService.js
+++ b/www/cdvah/js/AppsService.js
@@ -116,6 +116,10 @@
                 });
             },
 
+            getActiveApp: function() {
+                return activeInstaller;
+            },
+
             getAppListAsJson : function() {
                 return createAppsJson();
             },
@@ -162,7 +166,7 @@
             },
 
             addApp : function(appType, /* optional */ appId) {
-                var installPath = INSTALL_DIRECTORY + 'app' + new Date().getTime() + '/';
+                var installPath = INSTALL_DIRECTORY + 'app' + Math.floor(Math.random() * 0xFFFFFFFF).toString(36) + '/';
                 return initHandlers().then(function() {
                     var Ctor = _installerFactories[appType];
                     return new Ctor().init(installPath, appId);

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/CordovaInstaller.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/CordovaInstaller.js b/www/cdvah/js/CordovaInstaller.js
index d6a8dda..f277e82 100644
--- a/www/cdvah/js/CordovaInstaller.js
+++ b/www/cdvah/js/CordovaInstaller.js
@@ -59,11 +59,8 @@
         };
 
         CordovaInstaller.prototype.readCordovaPluginsFile = function() {
-            var self = this;
-            return this.directoryManager.getAssetManifest()
-            .then(function(assetManifest) {
-                return self.updateCordovaPluginsFile(assetManifest['orig-cordova_plugins.js']);
-            });
+            var etag = this.directoryManager.getAssetEtag('orig-cordova_plugins.js');
+            return this.updateCordovaPluginsFile(etag);
         };
 
         return CordovaInstaller;

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/DirectoryManager.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/DirectoryManager.js b/www/cdvah/js/DirectoryManager.js
index 86d7543..3ff0ef5 100644
--- a/www/cdvah/js/DirectoryManager.js
+++ b/www/cdvah/js/DirectoryManager.js
@@ -22,36 +22,49 @@
     /* global myApp */
     myApp.factory('DirectoryManager', ['$q', 'ResourcesLoader', function($q, ResourcesLoader) {
         var ASSET_MANIFEST = 'assetmanifest.json';
-        function DirectoryManager(rootURL) {
+
+        function DirectoryManager() {}
+
+        DirectoryManager.prototype.init = function(rootURL) {
             this.rootURL = rootURL;
-            this.lastUpdated = null;
             this.onFileAdded = null;
             this._assetManifest = null;
+            this._assetManifestEtag = null;
             this._flushTimerId = null;
-        }
+            var deferred = $q.defer();
+            var me = this;
+            ResourcesLoader.readJSONFileContents(rootURL + ASSET_MANIFEST)
+            .then(function(json) {
+                me._assetManifest = json['assetManifest'];
+                me._assetManifestEtag = json['etag'];
+                deferred.resolve();
+            }, function() {
+                me._assetManifest = {};
+                me._assetManifestEtag = 0;
+                deferred.resolve();
+            });
+            return deferred.promise;
+        };
 
         DirectoryManager.prototype.deleteAll = function() {
-            this.lastUpdated = null;
             this._assetManifest = null;
+            this._assetManifestEtag = null;
             window.clearTimeout(this._flushTimerId);
             return ResourcesLoader.delete(this.rootURL);
         };
 
         DirectoryManager.prototype.getAssetManifest = function() {
-            if (this._assetManifest) {
-                return $q.when(this._assetManifest);
+            return this._assetManifest;
+        };
+
+        DirectoryManager.prototype.getAssetEtag = function(relativePath) {
+            if (this._assetManifest.hasOwnProperty(relativePath)) {
+                return this._assetManifest[relativePath];
             }
-            var deferred = $q.defer();
-            var me = this;
-            ResourcesLoader.readJSONFileContents(this.rootURL + ASSET_MANIFEST)
-            .then(function(json) {
-                me._assetManifest = json;
-                deferred.resolve(json);
-            }, function() {
-                me._assetManifest = {};
-                deferred.resolve({});
-            });
-            return deferred.promise;
+        };
+
+        DirectoryManager.prototype.getAssetManifestEtag = function() {
+            return (this._assetManifestEtag).toString(36).toUpperCase();
         };
 
         DirectoryManager.prototype._lazyWriteAssetManifest = function() {
@@ -60,9 +73,25 @@
             }
         };
 
+        DirectoryManager.prototype._updateManifest = function(relativePath, etag) {
+            if (etag !== null) {
+                this._assetManifest[relativePath] = etag;
+            } else {
+                delete this._assetManifest[relativePath];
+            }
+            this._assetManifestEtag = Math.floor(Math.random() * 0xFFFFFFFF);
+            this._lazyWriteAssetManifest();
+            if (etag !== null && this.onFileAdded) {
+                return this.onFileAdded(relativePath, etag);
+            }
+        };
+
         DirectoryManager.prototype._writeAssetManifest = function() {
             this._flushTimerId = null;
-            var stringContents = JSON.stringify(this._assetManifest);
+            var stringContents = JSON.stringify({
+                'assetManifest': this._assetManifest,
+                'etag': this._assetManifestEtag
+            });
             return ResourcesLoader.writeFileContents(this.rootURL + ASSET_MANIFEST, stringContents);
         };
 
@@ -70,11 +99,7 @@
             var self = this;
             return ResourcesLoader.moveFile(srcURL, this.rootURL + relativePath)
             .then(function() {
-                self._assetManifest[relativePath] = etag;
-                self._lazyWriteAssetManifest();
-                if (self.onFileAdded) {
-                    return self.onFileAdded(relativePath, etag);
-                }
+                return self._updateManifest(relativePath, etag);
             });
         };
 
@@ -82,11 +107,7 @@
             var self = this;
             return ResourcesLoader.writeFileContents(this.rootURL + relativePath, data)
             .then(function() {
-                self._assetManifest[relativePath] = etag;
-                self._lazyWriteAssetManifest();
-                if (self.onFileAdded) {
-                    return self.onFileAdded(relativePath, etag);
-                }
+                return self._updateManifest(relativePath, etag);
             });
         };
 
@@ -94,8 +115,7 @@
             if (!this._assetManifest[relativePath]) {
                 console.warn('Tried to delete non-existing file: ' + relativePath);
             } else {
-                delete this._assetManifest[relativePath];
-                this._lazyWriteAssetManifest();
+                this._updateManifest(relativePath, null);
                 return ResourcesLoader.delete(this.rootURL + relativePath);
             }
         };

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/HarnessServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HarnessServer.js b/www/cdvah/js/HarnessServer.js
index 6812fdb..78ffc7c 100644
--- a/www/cdvah/js/HarnessServer.js
+++ b/www/cdvah/js/HarnessServer.js
@@ -51,7 +51,7 @@
 //     curl -v -X POST "http://$IP_ADDRESS:2424/deleteapp?appId=a.b.c"
 //     curl -v -X POST "http://$IP_ADDRESS:2424/deleteapp?all=true" # Delete all apps.
 
-    myApp.factory('HarnessServer', ['$q', 'HttpServer', 'ResourcesLoader', 'AppHarnessUI', 'AppsService', 'notifier', function($q, HttpServer, ResourcesLoader, AppHarnessUI, AppsService, notifier) {
+    myApp.factory('HarnessServer', ['$q', 'HttpServer', 'ResourcesLoader', 'AppHarnessUI', 'AppsService', 'notifier', 'APP_VERSION', function($q, HttpServer, ResourcesLoader, AppHarnessUI, AppsService, notifier, APP_VERSION) {
 
         var PROTOCOL_VER = 2;
         var server = null;
@@ -121,49 +121,41 @@
             });
         }
 
-        function handleAssetManifest(req, resp) {
-            var appId = req.getQueryParam('appId');
-            return AppsService.getAppById(appId)
-            .then(function(app) {
-                if (app) {
-                    return app.directoryManager.getAssetManifest();
-                }
-                return null;
-            }).then(function(assetManifest) {
-                resp.sendJsonResponse({
-                    'assetManifest': assetManifest,
-                    'platform': cordova.platformId,
-                    'cordovaVer': cordova.version,
-                    'protocolVer': PROTOCOL_VER,
-                });
-            });
+        function getAssetManifestJson(app) {
+            return {
+                'assetManifest': app && app.directoryManager.getAssetManifest(),
+                'assetManifestEtag': app ? app.directoryManager.getAssetManifestEtag() : '0',
+                'platform': cordova.platformId,
+                'cordovaVer': cordova.version,
+                'protocolVer': PROTOCOL_VER
+            };
         }
 
-        function handlePrepUpdate(req, resp) {
+        function handleAssetManifest(req, resp) {
             var appId = req.getQueryParam('appId');
-            var appType = req.getQueryParam('appType') || 'cordova';
-            return AppsService.getAppById(appId, appType)
+            return AppsService.getAppById(appId)
             .then(function(app) {
-                return req.readAsJson()
-                .then(function(requestJson) {
-                    app.updatingStatus = 0;
-                    app.updateBytesTotal = +requestJson['transferSize'];
-                    app.updateBytesSoFar = 0;
-                    return resp.sendTextResponse(200, '');
-                });
+                return resp.sendJsonResponse(200, getAssetManifestJson(app));
             });
         }
 
         function handleDeleteFiles(req, resp) {
             var appId = req.getQueryParam('appId');
-            var appType = req.getQueryParam('appType') || 'cordova';
-            return AppsService.getAppById(appId, appType)
+            var manifestEtag = req.getQueryParam('manifestEtag');
+            return AppsService.getAppById(appId)
             .then(function(app) {
                 return req.readAsJson()
                 .then(function(requestJson) {
-                    var paths = requestJson['paths'];
-                    for (var i = 0; i < paths.length; ++i) {
-                        app.directoryManager.deleteFile(paths[i]);
+                    if (app) {
+                        if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+                            return resp.sendJsonResponse(409, getAssetManifestJson(app));
+                        }
+                        var paths = requestJson['paths'];
+                        for (var i = 0; i < paths.length; ++i) {
+                            app.directoryManager.deleteFile(paths[i]);
+                        }
+                    } else {
+                        console.log('Warning: tried to delete files from non-existant app: ' + appId);
                     }
                     return resp.sendTextResponse(200, '');
                 });
@@ -194,30 +186,57 @@
             var appType = req.getQueryParam('appType') || 'cordova';
             var path = req.getQueryParam('path');
             var etag = req.getQueryParam('etag');
+            var manifestEtag = req.getQueryParam('manifestEtag');
             if (!path || !etag) {
                 throw new Error('Request is missing path or etag query params');
             }
             return AppsService.getAppById(appId, appType)
             .then(function(app) {
+                // Checking the manifest ETAG is meant to catch the case where
+                // the client has cached the manifest from a first push, and
+                // wants to validate that it is still valid at the start of a
+                // subsequent push (e.g. make sure the device hasn't changed).
+                if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+                    return resp.sendJsonResponse(409, getAssetManifestJson(app));
+                }
+                startUpdateProgress(app, req);
                 var tmpUrl = ResourcesLoader.createTmpFileUrl();
                 return pipeRequestToFile(req, tmpUrl)
                 .then(function() {
                     return importFile(tmpUrl, path, app, etag);
                 })
                 .then(function() {
-                    // TODO: Add a timeout that resets updatingStatus if no more requests come in.
-                    app.updateBytesSoFar += +req.headers['content-length'];
-                    app.updatingStatus = app.updateBytesTotal / app.updateBytesSoFar;
-                    if (app.updatingStatus === 1) {
-                        app.updatingStatus = null;
-                        app.lastUpdated = new Date();
-                        notifier.success('Update complete.');
-                    }
-                    return resp.sendTextResponse(200, '');
+                    return incrementUpdateStatusAndSendManifest(app, req, resp);
                 });
             });
         }
 
+        // This is set at the beginning of a push to show progress bar
+        // across multiple requests.
+        function startUpdateProgress(app, req) {
+            // This is passed for the first file only, and is used to track total progress.
+            var expectTotal = +req.getQueryParam('expectBytes') || req.headers['content-length'];
+            app.updatingStatus = 0;
+            app.updateBytesTotal = expectTotal;
+            app.updateBytesSoFar = 0;
+        }
+
+        function incrementUpdateStatusAndSendManifest(app, req, resp) {
+            if (app.updatingStatus !== null) {
+                // TODO: Add a timeout that resets updatingStatus if no more requests come in.
+                app.updateBytesSoFar += +req.headers['content-length'];
+                app.updatingStatus = app.updateBytesTotal / app.updateBytesSoFar;
+                if (app.updatingStatus === 1) {
+                    app.updatingStatus = null;
+                    app.lastUpdated = new Date();
+                    notifier.success('Update complete.');
+                }
+            }
+            return resp.sendJsonResponse(200, {
+                'assetManifestEtag': app.directoryManager.getAssetManifestEtag()
+            });
+        }
+
         function importFile(fileUrl, destPath, app, etag) {
             console.log('Adding file: ' + destPath);
             if (destPath == 'www/cordova_plugins.js') {
@@ -229,8 +248,13 @@
         function handleZipPush(req, resp) {
             var appId = req.getQueryParam('appId');
             var appType = req.getQueryParam('appType') || 'cordova';
+            var manifestEtag = req.getQueryParam('manifestEtag');
             return AppsService.getAppById(appId, appType)
             .then(function(app) {
+                if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+                    return resp.sendJsonResponse(409, getAssetManifestJson(app));
+                }
+                startUpdateProgress(app, req);
                 var tmpZipUrl = ResourcesLoader.createTmpFileUrl();
                 var tmpDirUrl = ResourcesLoader.createTmpFileUrl() + '/';
                 return pipeRequestToFile(req, tmpZipUrl)
@@ -253,9 +277,7 @@
                     });
                 })
                 .then(function() {
-                    app.lastUpdated = new Date();
-                    notifier.success('Update complete.');
-                    return resp.sendTextResponse(200, '');
+                    return incrementUpdateStatusAndSendManifest(app, req, resp);
                 })
                 .finally(function() {
                     app.updatingStatus = null;
@@ -266,14 +288,18 @@
         }
 
         function handleInfo(req, resp) {
+            var activeApp = AppsService.getActiveApp();
             var json = {
                 'platform': cordova.platformId,
                 'cordovaVer': cordova.version,
                 'protocolVer': PROTOCOL_VER,
+                'harnessVer': APP_VERSION,
+                'supportedAppTypes': ['cordova'],
                 'userAgent': navigator.userAgent,
+                'activeAppId': activeApp && activeApp.appId,
                 'appList': AppsService.getAppListAsJson()
             };
-            resp.sendJsonResponse(json);
+            resp.sendJsonResponse(200, json);
         }
 
         function start() {
@@ -286,7 +312,6 @@
                 .addRoute('/launch', ensureMethodDecorator('POST', handleLaunch))
                 .addRoute('/info', ensureMethodDecorator('GET', handleInfo))
                 .addRoute('/assetmanifest', ensureMethodDecorator('GET', handleAssetManifest))
-                .addRoute('/prepupdate', ensureMethodDecorator('POST', handlePrepUpdate))
                 .addRoute('/deletefiles', ensureMethodDecorator('POST', handleDeleteFiles))
                 .addRoute('/putfile', ensureMethodDecorator('PUT', handlePutFile))
                 .addRoute('/zippush', ensureMethodDecorator('POST', handleZipPush))

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/HttpServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HttpServer.js b/www/cdvah/js/HttpServer.js
index bd4ccb4..028c92e 100644
--- a/www/cdvah/js/HttpServer.js
+++ b/www/cdvah/js/HttpServer.js
@@ -31,6 +31,14 @@
         var STATE_RESPONSE_WAITING_FOR_FLUSH = 5;
         var STATE_COMPLETE = 6;
 
+        function changeState(requestData, newState) {
+            if (newState <= requestData.state) {
+                throw new Error('Socket ' + requestData.socket.socketId + ' state error: ' + requestData.state + '->' + newState);
+            }
+            console.log('Socket ' + requestData.socket.socketId + ' state ' + requestData.state + '->' + newState);
+            requestData.state = newState;
+        }
+
         function HttpRequest(requestData) {
             this._requestData = requestData;
             this.method = requestData.method;
@@ -41,7 +49,7 @@
                 this.bytesRemaining = parseInt(requestData.headers['content-length'] || '0');
             }
             if (this.bytesRemaining === 0) {
-                this._requestData.state = STATE_REQUEST_RECEIVED;
+                changeState(this._requestData, STATE_REQUEST_RECEIVED);
             }
 
             var host = this.headers['host'] || 'localhost';
@@ -110,7 +118,7 @@
                     throw new Error('Bytes remaining negative: ' + self.bytesRemaining);
                 }
                 if (self.bytesRemaining === 0 && self._requestData.state === STATE_HEADERS_RECEIVED) {
-                    self._requestData.state = STATE_REQUEST_RECEIVED;
+                    changeState(self._requestData, STATE_REQUEST_RECEIVED);
                 }
                 return chunk;
             });
@@ -138,7 +146,7 @@
             return this.close();
         };
 
-        HttpResponse.prototype.sendJsonResponse = function(json) {
+        HttpResponse.prototype.sendJsonResponse = function(status, json) {
             return this.sendTextResponse(200, JSON.stringify(json, null, 4), 'application/json');
         };
 
@@ -148,8 +156,11 @@
             }
             var promise = this._requestData.socket.write(arrayBuffer);
             if (!arrayBuffer) {
-                this._requestData.state = STATE_RESPONSE_WAITING_FOR_FLUSH;
-                promise = promise.then(this._finish.bind(this, true));
+                changeState(this._requestData, STATE_RESPONSE_WAITING_FOR_FLUSH);
+                var self = this;
+                promise = promise.then(function() {
+                    self._finish();
+                });
             }
             return promise;
         };
@@ -167,7 +178,7 @@
                 this._requestData.socket.close(new Error('Started to write response before request data was finished.'));
                 return;
             }
-            this._requestData.state = STATE_RESPONSE_STARTED;
+            changeState(this._requestData, STATE_RESPONSE_STARTED);
             var statusMsg = status === 404 ? 'Not Found' :
                             status === 400 ? 'Bad Request' :
                             status === 200 ? 'OK' :
@@ -184,9 +195,9 @@
             if (this._requestData.state === STATE_COMPLETE) {
                 return;
             }
-            this._requestData.state = STATE_COMPLETE;
+            changeState(this._requestData, STATE_COMPLETE);
             this._requestData.socket.onClose = null;
-            var socketId = this._requestData.socketId;
+            var socketId = this._requestData.socket.socketId;
             if (typeof disconnect == 'undefined') {
                 disconnect = (this.headers['Connection'] || '').toLowerCase() != 'keep-alive';
             }
@@ -432,7 +443,7 @@
                         requestData.resource = requestDataParts[1];
                         requestData.httpVersion = requestDataParts[2];
                         console.log(requestData.method + ' requestData received for ' + requestData.resource);
-                        requestData.state = STATE_REQUEST_DATA_RECEIVED;
+                        changeState(requestData, STATE_REQUEST_DATA_RECEIVED);
                         requestData.socket.unread(arrayBuffer);
                         return readRequestHeaders(requestData);
                     }
@@ -442,7 +453,7 @@
                         requestData.headers = parseHeaders(requestData.dataAsStr.substring(0, splitPoint));
                         requestData.dataAsStr = '';
                         arrayBuffer = arrayBuffer.slice(splitPoint + 4 - oldLen);
-                        requestData.state = STATE_HEADERS_RECEIVED;
+                        changeState(requestData, STATE_HEADERS_RECEIVED);
                         requestData.socket.unread(arrayBuffer);
                         return requestData;
                     }

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/Installer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/Installer.js b/www/cdvah/js/Installer.js
index 69bccf6..49a6d92 100644
--- a/www/cdvah/js/Installer.js
+++ b/www/cdvah/js/Installer.js
@@ -31,15 +31,15 @@
             ret.updatingStatus = null;
             ret.lastUpdated = null;
             // Asset manifest is a cache of what files have been downloaded along with their etags.
-            ret.directoryManager = new DirectoryManager(installPath);
-            ret.directoryManager.onFileAdded = this.onFileAdded.bind(this);
             ret.appId = null; // Read from config.xml
             ret.appName = null; // Read from config.xml
             ret.startPage = null; // Read from config.xml
             ret.plugins = {}; // Read from orig-cordova_plugins.js
             ret.appId = appId;
-            return ret.directoryManager.getAssetManifest()
+            ret.directoryManager = new DirectoryManager();
+            return ret.directoryManager.init(installPath)
             .then(function() {
+                ret.directoryManager.onFileAdded = ret.onFileAdded.bind(ret);
                 return ret;
             });
         };

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/ListCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ListCtrl.js b/www/cdvah/js/ListCtrl.js
index 383762b..1dc38cc 100644
--- a/www/cdvah/js/ListCtrl.js
+++ b/www/cdvah/js/ListCtrl.js
@@ -19,9 +19,10 @@
 (function(){
     'use strict';
     /* global myApp */
-    myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer) {
+    myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', 'APP_NAME', 'APP_VERSION', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer, APP_NAME, APP_VERSION) {
         $scope.appList = [];
-        $rootScope.appTitle = document.title;
+        $rootScope.appTitle = APP_NAME + ' v' + APP_VERSION;
+        document.title = APP_NAME + ' v' + APP_VERSION;
 
         function initialise() {
             $scope.$on('$destroy', function() {

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/app.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/app.js b/www/cdvah/js/app.js
index e324ab1..5cb924e 100644
--- a/www/cdvah/js/app.js
+++ b/www/cdvah/js/app.js
@@ -19,6 +19,8 @@
 
 var myApp = angular.module('CordovaAppHarness', ['ngRoute']);
 
+myApp.value('APP_NAME', 'Cordova App Harness');
+myApp.value('APP_VERSION', '1.0');
 
 myApp.config(['$routeProvider', function($routeProvider){
     $routeProvider.when('/', {


[2/3] git commit: Split harness-push into harness-push + harness-client.

Posted by ag...@apache.org.
Split harness-push into harness-push + harness-client.

Also adds the ability to cache asset manifest between pushes.


Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/e82ef5c0
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/e82ef5c0
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/e82ef5c0

Branch: refs/heads/master
Commit: e82ef5c0f58cd1c1621d200154192cff8e310c8c
Parents: 79b6928
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:04:25 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:04:25 2014 -0400

----------------------------------------------------------------------
 harness-push/README.md                          |   4 +-
 harness-push/harness-push.js                    | 347 +------------------
 .../cordova-harness-client/README.md            |   5 +
 .../cordova-harness-client/harnessclient.js     | 196 +++++++++++
 .../cordova-harness-client/package.json         |  26 ++
 .../cordova-harness-client/pushsession.js       | 271 +++++++++++++++
 harness-push/package.json                       |  14 +-
 7 files changed, 516 insertions(+), 347 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/README.md
----------------------------------------------------------------------
diff --git a/harness-push/README.md b/harness-push/README.md
index 0898123..8b1c9e6 100644
--- a/harness-push/README.md
+++ b/harness-push/README.md
@@ -1,6 +1,6 @@
-# harness-push
+# cordova-harness-push
 
-A node module & command-line tool for controlling the Cordova App Harness.
+Command-line tool for controlling the Cordova App Harness.
 
 # Usage:
 

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/harness-push.js
----------------------------------------------------------------------
diff --git a/harness-push/harness-push.js b/harness-push/harness-push.js
index 18a104c..698036b 100755
--- a/harness-push/harness-push.js
+++ b/harness-push/harness-push.js
@@ -18,332 +18,8 @@
   under the License.
  */
 
-var fs = require('fs'),
-    path = require('path'),
-    crypto = require('crypto'),
-    url = require('url'),
-    Q = require('q'),
-    request = require('request'),
-    nopt = require('nopt'),
-    shelljs = require('shelljs'),
-    JSZip = require('jszip');
-
-var IGNORE_PATH_FOR_PUSH_REGEXP = /www\/(?:plugins\/|cordova\.js)/;
-
-function getDerivedWwwDir(dir, platformId) {
-    if (platformId == 'android') {
-        return path.join(dir, 'platforms', platformId, 'assets', 'www');
-    } else if (platformId == 'ios') {
-        return path.join(dir, 'platforms', platformId, 'www');
-    }
-    throw new Error('Platform not supported: ' + platformId);
-}
-
-function getDerivedConfigXmlPath(dir, platformId) {
-    if (platformId == 'android') {
-        return path.join(dir, 'platforms', platformId, 'res', 'xml', 'config.xml');
-    } else if (platformId == 'ios') {
-        var base = path.join(dir, 'platforms', platformId);
-        var ret = null;
-        fs.readdirSync(base).forEach(function(a) {
-            if (a != 'www' && a != 'cordova' && a != 'CordovaLib') {
-                var fullPath = path.join(base, a, 'config.xml');
-                if (fs.existsSync(fullPath)) {
-                    ret = fullPath;
-                }
-            }
-        });
-        return ret;
-    }
-    throw new Error('Platform not supported: ' + platformId);
-}
-
-function discoverAppId(dir) {
-    var configXmlPath = path.join(dir, 'config.xml');
-    if (!fs.existsSync(configXmlPath)) {
-        configXmlPath = path.join(dir, 'www', 'config.xml');
-    }
-    if (!fs.existsSync(configXmlPath)) {
-        throw new Error('Not a Cordova project: ' + dir);
-    }
-
-    var configData = fs.readFileSync(configXmlPath, 'utf8');
-    var m = /\bid="(.*?)"/.exec(configData);
-    var appId = m && m[1];
-    if (!appId) {
-        throw new Error('Could not find app ID within: ' + configXmlPath);
-    }
-    return appId;
-}
-
-exports.push = function(target, dir, pretend) {
-    var appId = discoverAppId(dir);
-    var appType = 'cordova';
-
-    return exports.assetmanifest(target, appId)
-    .then(function(result) {
-        var derivedWwwDir = getDerivedWwwDir(dir, result.body['platform']);
-        var derivedConfigXmlPath = getDerivedConfigXmlPath(dir, result.body['platform']);
-        var cordovaPluginsPath = path.join(derivedWwwDir, 'cordova_plugins.js');
-        if (!fs.existsSync(cordovaPluginsPath)) {
-            throw new Error('Could not find: ' + cordovaPluginsPath);
-        }
-        if (!fs.existsSync(derivedConfigXmlPath)) {
-            throw new Error('Could not find: ' + derivedConfigXmlPath);
-        }
-
-        var existingAssetManifest = result.body['assetManifest'];
-        if (existingAssetManifest) {
-            return doFileSync(target, appId, appType, derivedWwwDir, derivedConfigXmlPath, existingAssetManifest, pretend);
-        }
-        // TODO: It might be faster to use Zip even when some files exist.
-        return doZipPush(target, appId, appType, derivedWwwDir, derivedConfigXmlPath);
-    });
-}
-
-function calculateMd5(fileName) {
-    var BUF_LENGTH = 64*1024;
-    var buf = new Buffer(BUF_LENGTH);
-    var bytesRead = BUF_LENGTH;
-    var pos = 0;
-    var fdr = fs.openSync(fileName, 'r');
-
-    try {
-        var md5sum = crypto.createHash('md5');
-        while (bytesRead === BUF_LENGTH) {
-            bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
-            pos += bytesRead;
-            md5sum.update(buf.slice(0, bytesRead));
-        }
-    } finally {
-        fs.closeSync(fdr);
-    }
-    return md5sum.digest('hex');
-}
-
-function buildAssetManifest(dir, configXmlPath) {
-    var fileList = shelljs.find(dir).filter(function(a) {
-        return !IGNORE_PATH_FOR_PUSH_REGEXP.exec(a) && !fs.statSync(a).isDirectory();
-    });
-
-    var ret = Object.create(null);
-    for (var i = 0; i < fileList.length; ++i) {
-        // TODO: convert windows slash to unix slash here.
-        var appPath = 'www/' + fileList[i].slice(dir == '.' ? 0 : dir.length + 1);
-        ret[appPath] = {
-            path: appPath,
-            realPath: fileList[i],
-            etag: calculateMd5(fileList[i]),
-        };
-    }
-    ret['config.xml'] = {
-        path: 'config.xml',
-        realPath: configXmlPath,
-        etag: calculateMd5(configXmlPath)
-    };
-    return ret;
-}
-
-function buildDeleteList(existingAssetManifest, newAssetManifest) {
-    var toDelete = [];
-    for (var k in existingAssetManifest) {
-        // Don't delete top-level files ever.
-        if (k.slice(0, 4) != 'www/') {
-            continue;
-        }
-        if (!newAssetManifest[k]) {
-            toDelete.push(k);
-        }
-    }
-    return toDelete;
-}
-
-function buildPushList(existingAssetManifest, newAssetManifest) {
-    var ret = [];
-    for (var k in newAssetManifest) {
-        var entry = newAssetManifest[k];
-        if (entry.etag != existingAssetManifest[k]) {
-            if (entry.path == 'config.xml') {
-                ret.unshift(entry);
-            } else {
-                ret.push(entry);
-            }
-        }
-    }
-    return ret;
-}
-
-function calculatePushBytes(pushList) {
-    var ret = 0;
-    for (var i = 0; i < pushList.length; ++i) {
-        ret += fs.statSync(pushList[i].realPath).size;
-    }
-    return ret;
-}
-
-function doFileSync(target, appId, appType, dir, configXmlPath, existingAssetManifest, pretend) {
-    var newAssetManifest = buildAssetManifest(dir, configXmlPath);
-    var deleteList = buildDeleteList(existingAssetManifest, newAssetManifest);
-    var pushList = buildPushList(existingAssetManifest, newAssetManifest);
-    var totalPushBytes = calculatePushBytes(pushList);
-    if (pretend) {
-        console.log('AppId=' + appId);
-        console.log('Would delete: ' + JSON.stringify(deleteList));
-        console.log('Would upload: ' + JSON.stringify(pushList));
-        console.log('Upload bytes: ' + totalPushBytes);
-    } else if (deleteList.length === 0 && pushList.length === 0) {
-        console.log('Application already up-to-date.');
-    } else {
-        return doRequest('POST', target, 'prepupdate', { appId: appId, appType: appType, json: {'transferSize': totalPushBytes}})
-        .then(function() {
-            if (deleteList.length > 0) {
-                return doRequest('POST', target, 'deletefiles', { appId: appId, appType: appType, json: {'paths': deleteList}})
-            }
-        })
-        .then(function pushNextFile() {
-            if (pushList.length === 0) {
-                console.log('Push complete.');
-                return;
-            }
-            var curPushEntry = pushList.shift();
-            return doRequest('PUT', target, '/putfile', {
-                appId: appId,
-                appType: appType,
-                body: fs.readFileSync(curPushEntry.realPath),
-                query: {
-                    path: curPushEntry.path,
-                    etag: curPushEntry.etag
-                }
-            }).then(pushNextFile);
-        });
-    }
-}
-
-function zipDir(dir, zip) {
-    var contents = fs.readdirSync(dir);
-    contents.forEach(function(f) {
-        var fullPath = path.join(dir, f);
-        if (!IGNORE_PATH_FOR_PUSH_REGEXP.exec(fullPath)) {
-            if (fs.statSync(fullPath).isDirectory()) {
-                var inner = zip.folder(f);
-                zipDir(path.join(dir, f), inner);
-            } else {
-                zip.file(f, fs.readFileSync(fullPath, 'binary'), { binary: true });
-            }
-        }
-    });
-}
-
-function doZipPush(target, appId, appType, dir, configXmlPath) {
-    var zip = new JSZip();
-    zipDir(dir, zip.folder('www'));
-    zip.file('config.xml', fs.readFileSync(configXmlPath, 'binary'), { binary: true });
-    var newAssetManifest = buildAssetManifest(dir, configXmlPath);
-    zip.file('zipassetmanifest.json', JSON.stringify(newAssetManifest));
-    var zipData = new Buffer(zip.generate({ type: 'base64' }), 'base64');
-    return doRequest('POST', target, '/zippush', {appId:appId, appType: appType, body: zipData});
-}
-
-function doRequest(method, target, action, options) {
-    var ret = Q.defer();
-    var targetParts = target.split(':');
-    var host = targetParts[0];
-    var port = +(targetParts[1] || 2424);
-    options = options || {};
-
-    var queryParams = {};
-    if (options.query) {
-        Object.keys(options.query).forEach(function(k) {
-            queryParams[k] = options.query[k];
-        });
-    }
-    if (options.appId) {
-        queryParams['appId'] = options.appId;
-    }
-    if (options.appType) {
-        queryParams['appType'] = options.appType;
-    }
-
-    // Send the HTTP request. crxContents is a Node Buffer, which is the payload.
-    // Prepare the form data for upload.
-    var uri = url.format({
-        protocol: 'http',
-        hostname: host,
-        port: port,
-        pathname: action,
-        query: queryParams
-    });
-
-    process.stdout.write(method + ' ' + uri);
-
-    var headers = {};
-    if (options.json) {
-        options.body = JSON.stringify(options.json);
-        headers['Content-Type'] = 'application/json';
-    }
-
-    function statusCheck(err, res, body) {
-        if (err) {
-            err.statusCode = res && res.statusCode;
-        } else if (res) {
-            process.stdout.write(' ==> ' + (res && res.statusCode) + '\n');
-            if (res.statusCode != 200) {
-                err = new Error('Server returned status code: ' + res.statusCode);
-            } else if (options.expectJson) {
-                try {
-                    body = JSON.parse(body);
-                } catch (e) {
-                    err = new Error('Invalid JSON: ' + body.slice(500));
-                }
-            }
-        }
-        if (err) {
-            ret.reject(err);
-        } else {
-            ret.resolve({res:res, body:body});
-        }
-    }
-    var req = request({
-        uri: uri,
-        headers: headers,
-        method: method,
-        body: options.body
-    }, statusCheck);
-
-    if (options.form) {
-        var f = options.form;
-        req.form().append(f.key, f.formBody, { filename: f.filename, contentType: f.contentType });
-    }
-    return ret.promise;
-};
-
-exports.info = function(target) {
-    return doRequest('GET', target, '/info', { expectJson: true });
-};
-
-exports.assetmanifest = function(target, appId) {
-    return doRequest('GET', target, '/assetmanifest', {expectJson: true, appId: appId});
-};
-
-exports.menu = function(target) {
-    return doRequest('POST', target, '/menu');
-};
-
-exports.eval = function(target, someJs) {
-    return doRequest('POST', target, '/exec', { query: {code: someJs} });
-};
-
-exports.launch = function(target, appId) {
-    return doRequest('POST', target, '/launch', { appId: appId});
-};
-
-exports.deleteAllApps = function(target) {
-    return doRequest('POST', target, '/deleteapp', { query: {'all': 1} });
-};
-
-exports.deleteApp = function(target, appId) {
-    return doRequest('POST', target, '/deleteApp', { appId: appId});
-};
+var nopt = require('nopt');
+var HarnessClient = require('cordova-harness-client');
 
 function parseArgs(argv) {
     var opts = {
@@ -389,32 +65,35 @@ function main() {
         }
     }
 
+    var client = new HarnessClient(args.target);
+
     var cmd = args.argv.remain[0];
     if (cmd == 'push') {
         if (!args.argv.remain[1]) {
             usage();
         }
-        exports.push(args.target, args.argv.remain[1], args.pretend).then(onSuccess, onFailure);
+        var pushSession = client.createPushSession(args.argv.remain[1]);
+        pushSession.push().then(onSuccess, onFailure);
     } else if (cmd == 'deleteall') {
-        exports.deleteAllApps(args.target);
+        client.deleteAllApps();
     } else if (cmd == 'delete') {
         if (!args.argv.remain[1]) {
             usage();
         }
-        exports.deleteApp(args.target).then(onSuccess, onFailure);
+        client.deleteApp().then(onSuccess, onFailure);
     } else if (cmd == 'menu') {
-        exports.menu(args.target).then(onSuccess, onFailure);
+        client.menu().then(onSuccess, onFailure);
     } else if (cmd == 'eval') {
         if (!args.argv.remain[1]) {
             usage();
         }
-        exports.eval(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+        client.evalJs(args.argv.remain[1]).then(onSuccess, onFailure);
     } else if (cmd == 'assetmanifest') {
-        exports.assetmanifest(args.target, args.appid).then(onSuccess, onFailure);
+        client.assetmanifest(args.appid).then(onSuccess, onFailure);
     } else if (cmd == 'info') {
-        exports.info(args.target).then(onSuccess, onFailure);
+        client.info().then(onSuccess, onFailure);
     } else if (cmd == 'launch') {
-        exports.launch(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+        client.launch(args.argv.remain[1]).then(onSuccess, onFailure);
     } else {
         usage();
     }

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/README.md
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/README.md b/harness-push/node_modules/cordova-harness-client/README.md
new file mode 100644
index 0000000..357c05c
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/README.md
@@ -0,0 +1,5 @@
+# cordova-harness-client
+
+A node module for controlling the Cordova App Harness.
+
+For example usage of the libary, see `cordova-harness-push`.

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/harnessclient.js
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/harnessclient.js b/harness-push/node_modules/cordova-harness-client/harnessclient.js
new file mode 100644
index 0000000..903aafb
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/harnessclient.js
@@ -0,0 +1,196 @@
+/**
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+ */
+
+var http = require('http');
+var url = require('url');
+var Q = require('q');
+var KeepAliveAgent = require('keep-alive-agent');
+var PushSession = require('./pushsession');
+
+var httpAgent = new KeepAliveAgent();
+
+function doRequest(method, target, action, options) {
+    var deferred = Q.defer();
+    var targetParts = target.split(':');
+    var host = targetParts[0];
+    var port = +(targetParts[1] || 2424);
+    options = options || {};
+
+    var queryParams = {};
+    if (options.query) {
+        Object.keys(options.query).forEach(function(k) {
+            queryParams[k] = options.query[k];
+        });
+    }
+    if (options.appId) {
+        queryParams['appId'] = options.appId;
+    }
+    if (options.appType) {
+        queryParams['appType'] = options.appType;
+    }
+
+    var uri = url.format({
+        protocol: 'http',
+        hostname: host,
+        port: port,
+        pathname: action,
+        query: queryParams
+    });
+    var pathWithQuery = uri.replace(/^.*?\/\/.*?\//, '/');
+    process.stdout.write(method + ' ' + uri);
+
+    var headers = {
+        'Connection': 'keep-alive'
+    };
+    var body = null;
+    if (method == 'POST' || method == 'PUT') {
+        body = options.body || '';
+        if (options.json) {
+            body = JSON.stringify(options.json);
+            headers['Content-Type'] = 'application/json';
+        }
+        headers['Content-Length'] = body.length;
+    }
+
+    var startTime = new Date();
+    var req = http.request({
+        hostname: host,
+        port: port,
+        method: method,
+        path: pathWithQuery,
+        headers: headers,
+        agent: httpAgent
+    });
+
+    req.on('error', function(e) {
+        deferred.reject(e);
+    });
+
+    if (body) {
+        req.write(body);
+    }
+    req.end();
+
+    req.on('response', function(res) {
+        process.stdout.write(' ==> ' + res.statusCode);
+        if (res.statusCode != 200) {
+            deferred.reject(new Error('Server returned status code: ' + res.statusCode));
+            res.destroy();
+            return;
+        }
+        res.setEncoding('utf8');
+        var body = '';
+        res.on('data', function(chunk) {
+            body += chunk;
+        });
+        res.on('end', function() {
+            process.stdout.write(' (' + (new Date() - startTime) + ')\n');
+            try {
+                body = options.expectJson ? JSON.parse(body) : body;
+            } catch (e) {
+                deferred.reject(new Error('Invalid JSON: ' + body.slice(500)));
+                return;
+            }
+            deferred.resolve({res:res, body:body});
+        });
+    });
+    return deferred.promise;
+}
+
+function HarnessClient(target) {
+    this.target = target || '127.0.0.1';
+}
+
+HarnessClient.prototype.info = function() {
+    return doRequest('GET', this.target, '/info', { expectJson: true });
+};
+
+HarnessClient.prototype.assetmanifest = function(/* optional */ appId) {
+    return doRequest('GET', this.target, '/assetmanifest', {expectJson: true, appId: appId});
+};
+
+HarnessClient.prototype.menu = function() {
+    return doRequest('POST', this.target, '/menu');
+};
+
+HarnessClient.prototype.evalJs = function(someJs) {
+    return doRequest('POST', this.target, '/exec', { query: {code: someJs} });
+};
+
+HarnessClient.prototype.launch = function(/* optional */ appId) {
+    return doRequest('POST', this.target, '/launch', { appId: appId});
+};
+
+HarnessClient.prototype.deleteAllApps = function() {
+    return doRequest('POST', this.target, '/deleteapp', { query: {'all': 1} });
+};
+
+HarnessClient.prototype.deleteApp = function(/* optional */ appId) {
+    return doRequest('POST', this.target, '/deleteapp', { appId: appId});
+};
+
+HarnessClient.prototype.pushZip = function(appId, appType, zipData, /* optional */ totalPushBytes, /* optional */ manifestEtag) {
+    var query = {};
+    if (totalPushBytes) {
+        query['expectBytes'] = totalPushBytes;
+    }
+    if (manifestEtag) {
+        query['manifestEtag'] = manifestEtag;
+    }
+    return doRequest('POST', this.target, '/zippush', {
+        appId: appId,
+        appType: appType,
+        body: zipData,
+        query: query
+    });
+};
+
+HarnessClient.prototype.pushFile = function(appId, appType, payload, etag, remotePath, /* optional */ totalPushBytes, /* optional */ manifestEtag) {
+    var query = {
+        'path': remotePath,
+        'etag': etag
+    };
+    if (totalPushBytes) {
+        query['expectBytes'] = totalPushBytes;
+    }
+    if (manifestEtag) {
+        query['manifestEtag'] = manifestEtag;
+    }
+    return doRequest('PUT', this.target, '/putfile', {
+        appId: appId,
+        appType: appType,
+        body: payload,
+        query: query
+    });
+};
+
+HarnessClient.prototype.deleteFiles = function(appId, deleteList, /* optional */ manifestEtag) {
+    var query = {};
+    if (manifestEtag) {
+        query['manifestEtag'] = manifestEtag;
+    }
+    return doRequest('POST', this.target, 'deletefiles', { appId: appId, json: {'paths': deleteList}, query: query});
+};
+
+HarnessClient.prototype.createPushSession = function(dir) {
+    return new PushSession(this, dir);
+};
+
+module.exports = HarnessClient;
+

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/package.json
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/package.json b/harness-push/node_modules/cordova-harness-client/package.json
new file mode 100644
index 0000000..a44b72c
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "cordova-harness-client",
+  "version": "0.0.1",
+  "description": "Client library for communicating with Cordova App Harness.",
+  "main": "harnessclient.js",
+  "author": "The Apache Cordova Team",
+  "license": "Apache 2.0",
+  "repository": {
+    "type": "git",
+    "url": "https://git-wip-us.apache.org/repos/asf/cordova-app-harness.git"
+  },
+  "bugs": {
+    "url": "https://issues.apache.org/jira/browse/CB",
+    "email": "dev@cordova.apache.org"
+  },
+  "keywords": [
+    "cordova",
+    "cordova-app-harness"
+  ],
+  "dependencies": {
+    "jszip": "~2.1",
+    "keep-alive-agent": "0.0.1",
+    "q": "~0.9",
+    "shelljs": "0.1.x"
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/pushsession.js
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/pushsession.js b/harness-push/node_modules/cordova-harness-client/pushsession.js
new file mode 100644
index 0000000..546978b
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/pushsession.js
@@ -0,0 +1,271 @@
+/**
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+ */
+
+var fs = require('fs'),
+    path = require('path'),
+    crypto = require('crypto'),
+    Q = require('q'),
+    shelljs = require('shelljs'),
+    JSZip = require('jszip');
+
+var IGNORE_PATH_FOR_PUSH_REGEXP = /www\/(?:plugins\/|cordova\.js)/;
+
+function getDerivedWwwDir(dir, platformId) {
+    if (platformId == 'android') {
+        return path.join(dir, 'platforms', platformId, 'assets', 'www');
+    } else if (platformId == 'ios') {
+        return path.join(dir, 'platforms', platformId, 'www');
+    }
+    throw new Error('Platform not supported: ' + platformId);
+}
+
+function getDerivedConfigXmlPath(dir, platformId) {
+    if (platformId == 'android') {
+        return path.join(dir, 'platforms', platformId, 'res', 'xml', 'config.xml');
+    } else if (platformId == 'ios') {
+        var base = path.join(dir, 'platforms', platformId);
+        var ret = null;
+        if (fs.existsSync(base)) {
+            fs.readdirSync(base).forEach(function(a) {
+                if (a != 'www' && a != 'cordova' && a != 'CordovaLib') {
+                    var fullPath = path.join(base, a, 'config.xml');
+                    if (fs.existsSync(fullPath)) {
+                        ret = fullPath;
+                    }
+                }
+            });
+        }
+        return ret;
+    }
+    throw new Error('Platform not supported: ' + platformId);
+}
+
+function discoverAppId(dir) {
+    var configXmlPath = path.join(dir, 'config.xml');
+    if (!fs.existsSync(configXmlPath)) {
+        configXmlPath = path.join(dir, 'www', 'config.xml');
+    }
+    if (!fs.existsSync(configXmlPath)) {
+        throw new Error('Not a Cordova project: ' + dir);
+    }
+
+    var configData = fs.readFileSync(configXmlPath, 'utf8');
+    var m = /\bid="(.*?)"/.exec(configData);
+    var appId = m && m[1];
+    if (!appId) {
+        throw new Error('Could not find app ID within: ' + configXmlPath);
+    }
+    return appId;
+}
+
+function calculateMd5(fileName) {
+    var BUF_LENGTH = 64*1024;
+    var buf = new Buffer(BUF_LENGTH);
+    var bytesRead = BUF_LENGTH;
+    var pos = 0;
+    var fdr = fs.openSync(fileName, 'r');
+
+    var md5sum = crypto.createHash('md5');
+    try {
+        while (bytesRead === BUF_LENGTH) {
+            bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
+            pos += bytesRead;
+            md5sum.update(buf.slice(0, bytesRead));
+        }
+    } finally {
+        fs.closeSync(fdr);
+    }
+    return md5sum.digest('hex');
+}
+
+function buildAssetManifest(dir, configXmlPath) {
+    var fileList = shelljs.find(dir).filter(function(a) {
+        return !IGNORE_PATH_FOR_PUSH_REGEXP.exec(a) && !fs.statSync(a).isDirectory();
+    });
+
+    var ret = Object.create(null);
+    for (var i = 0; i < fileList.length; ++i) {
+        // TODO: convert windows slash to unix slash here.
+        var appPath = 'www/' + fileList[i].slice(dir == '.' ? 0 : dir.length + 1);
+        ret[appPath] = {
+            path: appPath,
+            realPath: fileList[i],
+            etag: calculateMd5(fileList[i]),
+        };
+    }
+    ret['config.xml'] = {
+        path: 'config.xml',
+        realPath: configXmlPath,
+        etag: calculateMd5(configXmlPath)
+    };
+    return ret;
+}
+
+function buildDeleteList(existingAssetManifest, newAssetManifest) {
+    var toDelete = [];
+    for (var k in existingAssetManifest) {
+        // Don't delete top-level files ever.
+        if (k.slice(0, 4) != 'www/') {
+            continue;
+        }
+        if (!newAssetManifest[k]) {
+            toDelete.push(k);
+        }
+    }
+    return toDelete;
+}
+
+function buildPushList(existingAssetManifest, newAssetManifest) {
+    var ret = [];
+    for (var k in newAssetManifest) {
+        var entry = newAssetManifest[k];
+        if (entry.etag != existingAssetManifest[k]) {
+            if (entry.path == 'config.xml') {
+                ret.unshift(entry);
+            } else {
+                ret.push(entry);
+            }
+        }
+    }
+    return ret;
+}
+
+function calculatePushBytes(pushList) {
+    var ret = 0;
+    for (var i = 0; i < pushList.length; ++i) {
+        ret += fs.statSync(pushList[i].realPath).size;
+    }
+    return ret;
+}
+
+function zipDir(dir, zip) {
+    var contents = fs.readdirSync(dir);
+    contents.forEach(function(f) {
+        var fullPath = path.join(dir, f);
+        if (!IGNORE_PATH_FOR_PUSH_REGEXP.exec(fullPath)) {
+            if (fs.statSync(fullPath).isDirectory()) {
+                var inner = zip.folder(f);
+                zipDir(path.join(dir, f), inner);
+            } else {
+                zip.file(f, fs.readFileSync(fullPath, 'binary'), { binary: true });
+            }
+        }
+    });
+}
+
+function PushSession(harnessClient, dir) {
+    this.launchAfterPush = true;
+    this.appType_ = 'cordova';
+    this.harnessClient_ = harnessClient;
+    this.rootDir_ = dir;
+    this.wwwDir_ = null;
+    this.appId_ = null;
+    this.platformId_ = null;
+    this.assetManifest_ = null;
+    this.assetManifestEtag_ = null;
+    var self = this;
+    this.boundUpdateAssetManifestFn_ = function(result) {
+        self.assetManifestEtag_ = result.body['assetManifestEtag'];
+    };
+}
+
+PushSession.prototype.initialize = function(opts) {
+    opts = opts || {};
+    this.appId_ = opts.appId || discoverAppId(this.rootDir_);
+    var self = this;
+    return this.harnessClient_.assetmanifest(this.appId_)
+    .then(function(result) {
+        self.platformId_ = result.body['platform'];
+        self.assetManifest_ = result.body['assetManifest'];
+        self.assetManifestEtag_ = result.body['assetManifestEtag'];
+        self.wwwDir_ = opts.wwwDir || getDerivedWwwDir(self.rootDir_, self.platformId_);
+        self.configXmlPath_ = opts.configXmlPath || getDerivedConfigXmlPath(self.rootDir_, self.platformId_);
+        self.cordovaPluginsPath_ = !opts.skipCordovaPlugins && path.join(self.wwwDir_, 'cordova_plugins.js');
+    });
+};
+
+PushSession.prototype.push = function() {
+    var self = this;
+    return Q.when(this.platformId_ || this.initialize())
+    .then(function() {
+        if (!fs.existsSync(self.configXmlPath_)) {
+            throw new Error('Could not find: ' + self.configXmlPath_ + ' you probably need to run: cordova platform add ' + self.platformId_);
+        }
+        if (self.cordovaPluginsPath_ && !fs.existsSync(self.cordovaPluginsPath_)) {
+            throw new Error('Could not find: ' + self.cordovaPluginsPath_);
+        }
+        if (self.assetManifest_) {
+            return self.doFileSync_();
+        }
+        // TODO: It might be faster to use Zip even when some files exist.
+        return self.doZipPush_();
+    }).then(function() {
+        if (self.launchAfterPush) {
+            return self.harnessClient_.launch(self.appId_);
+        }
+    }, function(e) {
+        self.assetManifest_ = null;
+        throw e;
+    });
+};
+
+PushSession.prototype.doFileSync_ = function() {
+    var a = new Date();
+    var newAssetManifest = buildAssetManifest(this.wwwDir_, this.configXmlPath_);
+    var deleteList = buildDeleteList(this.assetManifest_, newAssetManifest);
+    var pushList = buildPushList(this.assetManifest_, newAssetManifest);
+    var totalPushBytes = calculatePushBytes(pushList);
+    console.log('Changes calculated (' + (new Date() - a) + ')');
+    if (deleteList.length === 0 && pushList.length === 0) {
+        console.log('Application already up-to-date.');
+        return;
+    }
+    var origPushListLen = pushList.length;
+    var self = this;
+    return Q.when().then(function pushNextFile() {
+        if (pushList.length === 0) {
+            return;
+        }
+        var firstRequest = pushList.length === origPushListLen;
+        var curPushEntry = pushList.shift();
+        var payload = fs.readFileSync(curPushEntry.realPath);
+        // TODO handle 409 responses.
+        return self.harnessClient_.pushFile(self.appId_, self.appType_, payload, curPushEntry.etag, curPushEntry.path, firstRequest && totalPushBytes, firstRequest && self.assetManifestEtag_)
+        .then(self.boundUpdateAssetManifestFn_)
+        .then(pushNextFile);
+    }).then(function() {
+        if (deleteList.length > 0) {
+            return self.harnessClient_.deleteFiles(self.appType_, deleteList, self.assetManifestEtag_)
+            .then(self.boundUpdateAssetManifestFn_);
+        }
+    });
+};
+
+PushSession.prototype.doZipPush_ = function() {
+    var zip = new JSZip();
+    zipDir(this.wwwDir_, zip.folder('www'));
+    zip.file('config.xml', fs.readFileSync(this.configXmlPath_, 'binary'), { binary: true });
+    var newAssetManifest = buildAssetManifest(this.wwwDir_, this.configXmlPath_);
+    zip.file('zipassetmanifest.json', JSON.stringify(newAssetManifest));
+    var zipData = new Buffer(zip.generate({ type: 'base64' }), 'base64');
+    return this.harnessClient_.pushZip(this.appId_, this.appType_, zipData, null, this.assetManifestEtag_)
+    .then(this.boundUpdateAssetManifestFn_);
+};
+
+module.exports = PushSession;

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/package.json
----------------------------------------------------------------------
diff --git a/harness-push/package.json b/harness-push/package.json
index e58b848..e483416 100644
--- a/harness-push/package.json
+++ b/harness-push/package.json
@@ -1,12 +1,8 @@
 {
-  "name": "harness-push",
+  "name": "cordova-harness-push",
   "version": "0.0.1",
   "description": "Command-line utility for communicating with Cordova App Harness.",
-  "main": "push.js",
-  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
-  },
-  "author": "",
+  "author": "The Apache Cordova Team",
   "license": "Apache 2.0",
   "repository": {
     "type": "git",
@@ -24,10 +20,6 @@
     "harness-push": "./harness-push.js"
   },
   "dependencies": {
-    "jszip": "~2.1",
-    "nopt": "~2.2",
-    "q": "~0.9",
-    "request": "~2.34",
-    "shelljs": "0.1.x"
+    "cordova-harness-push": "~0.0.x"
   }
 }


[3/3] git commit: Enable jshint for node modules as well

Posted by ag...@apache.org.
Enable jshint for node modules as well


Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/e18f4bfe
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/e18f4bfe
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/e18f4bfe

Branch: refs/heads/master
Commit: e18f4bfed84b701b54bd43f084c864598d0a26fa
Parents: e82ef5c
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:05:44 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:05:44 2014 -0400

----------------------------------------------------------------------
 .gitignore | 4 +++-
 .jshintrc  | 3 ++-
 checkjs.sh | 5 ++++-
 3 files changed, 9 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index ea23fd6..8516c5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 .DS_Store
-harness-push/node_modules
+harness-push/node_modules/*
+!harness-push/node_modules/cordova-harness-client
+harness-push/node_modules/cordova-harness-client/node_modules

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/.jshintrc
----------------------------------------------------------------------
diff --git a/.jshintrc b/.jshintrc
index 227f5fc..91a9a7d 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -4,7 +4,7 @@
     "camelcase": true,
     "curly": true,
     "eqeqeq": false,
-    "forin": true,
+    "forin": false,
     "immed": true,
     "indent": 4,
     "latedef": false,
@@ -24,6 +24,7 @@
     "sub":true,
 
     //environment
+    "node": true,
     "browser": true,
     "devel":true,
 

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/checkjs.sh
----------------------------------------------------------------------
diff --git a/checkjs.sh b/checkjs.sh
index 8c1b59b..afb7173 100755
--- a/checkjs.sh
+++ b/checkjs.sh
@@ -28,4 +28,7 @@ if ! which jshint; then
 fi
 echo "Running jsHint"
 cd $(dirname "$0")
-jshint www/ --exclude www/cdvah/js/libs --verbose --show-non-errors
+set -x
+jshint www/ --exclude www/cdvah/js/libs --verbose --show-non-errors && \
+jshint harness-push/ --exclude harness-push/node_modules --verbose --show-non-errors && \
+jshint harness-push/node_modules/cordova-harness-client --exclude harness-push/node_modules/cordova-harness-client/node_modules --verbose --show-non-errors