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/20 21:52:51 UTC

[1/5] git commit: Stop setting a UrlRemap reset URL (not used anymore)

Repository: cordova-app-harness
Updated Branches:
  refs/heads/master 1fc7bf4a5 -> 42f7e5717


Stop setting a UrlRemap reset URL (not used anymore)


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/f2052142
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/f2052142
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/f2052142

Branch: refs/heads/master
Commit: f20521425493e9cf09628a1f4d4e8987f32e10bc
Parents: de9b285
Author: Andrew Grieve <ag...@chromium.org>
Authored: Thu May 15 21:29:27 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue May 20 14:20:55 2014 -0400

----------------------------------------------------------------------
 www/cdvah/js/Installer.js | 2 --
 1 file changed, 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/f2052142/www/cdvah/js/Installer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/Installer.js b/www/cdvah/js/Installer.js
index 5aea219..40f779c 100644
--- a/www/cdvah/js/Installer.js
+++ b/www/cdvah/js/Installer.js
@@ -130,8 +130,6 @@
                         startLocation = startLocation.replace(harnessDir, nativeInstallUrl + '/www');
                     }
 
-                    // Allow navigations back to the menu.
-                    UrlRemap.setResetUrl('^' + harnessUrl);
                     // Override cordova.js, and www/plugins to point at bundled plugins.
                     UrlRemap.aliasUri('^(?!app-harness://).*/www/cordova\\.js.*', '.+', 'app-harness:///cordova.js', false /* redirect */, true /* allowFurtherRemapping */);
                     UrlRemap.aliasUri('^(?!app-harness://).*/www/plugins/.*', '^.*?/www/plugins/' , 'app-harness:///plugins/', false /* redirect */, true /* allowFurtherRemapping */);


[5/5] git commit: Add harness-push command-line tool for pushing apps.

Posted by ag...@apache.org.
Add harness-push command-line tool for pushing apps.


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/42f7e571
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/42f7e571
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/42f7e571

Branch: refs/heads/master
Commit: 42f7e571745d7a56b962fa0fc19c05e71934201b
Parents: da594df
Author: Andrew Grieve <ag...@chromium.org>
Authored: Tue May 20 15:51:58 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue May 20 15:51:58 2014 -0400

----------------------------------------------------------------------
 .gitignore                   |   3 +-
 harness-push/harness-push.js | 346 ++++++++++++++++++++++++++++++++++++++
 harness-push/package.json    |  32 ++++
 3 files changed, 380 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/42f7e571/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 496ee2c..ea23fd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-.DS_Store
\ No newline at end of file
+.DS_Store
+harness-push/node_modules

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/42f7e571/harness-push/harness-push.js
----------------------------------------------------------------------
diff --git a/harness-push/harness-push.js b/harness-push/harness-push.js
new file mode 100755
index 0000000..d505ea0
--- /dev/null
+++ b/harness-push/harness-push.js
@@ -0,0 +1,346 @@
+#!/usr/bin/env node
+/**
+  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'),
+    url = require('url'),
+    Q = require('q'),
+    request = require('request'),
+    nopt = require('nopt'),
+    shelljs = require('shelljs');
+
+// Takes a Node-style callback: function(err).
+exports.push = function(target, dir, pretend) {
+  var appId;
+  var appType = 'cordova';
+  var configXmlPath = path.join(dir, 'config.xml');
+  dir = path.join(dir, 'www');
+  if (!fs.existsSync(configXmlPath)) {
+    configXmlPath = path.join(dir, 'config.xml');
+  }
+  if (!fs.existsSync(configXmlPath)) {
+    throw new Error('Not a Cordova project.');
+  }
+  var configData = fs.readFileSync(configXmlPath, { encoding: 'utf-8' });
+  var m = /\bid="(.*?)"/.exec(configData);
+  appId = m && m[1];
+
+  // ToDo - Add ability to bootstrap with a zip file.
+  return doFileSync(target, appId, appType, configXmlPath, dir, pretend);
+}
+
+function calculateMd5(fileName) {
+    var BUF_LENGTH = 64*1024,
+        buf = new Buffer(BUF_LENGTH),
+        bytesRead = BUF_LENGTH,
+        pos = 0,
+        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(configXmlPath, dir) {
+  var fileList = shelljs.find(dir).filter(function(a) {
+    return !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.length);
+    ret[appPath] = {
+        path: appPath,
+        realPath: fileList[i],
+        etag: calculateMd5(fileList[i]),
+    };
+  }
+  if (configXmlPath && path.dirname(configXmlPath) != dir) {
+    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' || entry.path == 'www/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, configXmlPath, dir, pretend) {
+  return exports.assetmanifest(target, appId)
+  .then(function(result) {
+    var existingAssetManifest = result.body['assetManifest'] || {};
+    var newAssetManifest = buildAssetManifest(configXmlPath, dir);
+    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 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});
+};
+
+function parseArgs(argv) {
+    var opts = {
+      'help': Boolean,
+      'target': String
+    };
+    var ret = nopt(opts, null, argv);
+    if (!ret.target) {
+      ret.target = 'localhost:2424';
+    }
+    return ret;
+}
+
+function usage() {
+  console.log('Usage: harness-push push path/to/chrome_app --target=IP_ADDRESS:PORT');
+  console.log('Usage: harness-push menu');
+  console.log('Usage: harness-push eval "alert(1)"');
+  console.log('Usage: harness-push info');
+  console.log('Usage: harness-push launch [appId]');
+  console.log();
+  console.log('--target defaults to localhost:2424');
+  console.log('To deploy to Android over USB, use: adb forward tcp:2424 tcp:2424');
+  process.exit(1);
+}
+
+function main() {
+  var args = parseArgs(process.argv);
+
+  function onFailure(err) {
+    console.error(err);
+  }
+  function onSuccess(result) {
+    if (typeof result.body == 'object') {
+      console.log(JSON.stringify(result.body, null, 4));
+    } else if (result.body) {
+      console.log(result.body);
+    }
+  }
+
+  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);
+  } else if (cmd == 'deleteall') {
+    exports.deleteAllApps(args.target);
+  } else if (cmd == 'delete') {
+    if (!args.argv.remain[1]) {
+      usage();
+    }
+    exports.deleteApp(args.target).then(onSuccess, onFailure);
+  } else if (cmd == 'menu') {
+    exports.menu(args.target).then(onSuccess, onFailure);
+  } else if (cmd == 'eval') {
+    if (!args.argv.remain[1]) {
+      usage();
+    }
+    exports.eval(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+  } else if (cmd == 'assetmanifest') {
+    exports.assetmanifest(args.target, args.appid).then(onSuccess, onFailure);
+  } else if (cmd == 'info') {
+    exports.info(args.target).then(onSuccess, onFailure);
+  } else if (cmd == 'launch') {
+    exports.launch(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+  } else {
+    usage();
+  }
+}
+
+if (require.main === module) {
+  main();
+}

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/42f7e571/harness-push/package.json
----------------------------------------------------------------------
diff --git a/harness-push/package.json b/harness-push/package.json
new file mode 100644
index 0000000..4357bd4
--- /dev/null
+++ b/harness-push/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "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": "",
+  "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"
+  ],
+  "bin": {
+    "harness-push": "./harness-push.js"
+  },
+  "dependencies": {
+    "request": "~2.34",
+    "nopt": "~2.2",
+    "shelljs": "0.1.x",
+    "q": "~0.9"
+  }
+}


[2/5] git commit: Delete Add / Edit screens (Allow add/update from server only).

Posted by ag...@apache.org.
Delete Add / Edit screens (Allow add/update from server only).


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/de9b2855
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/de9b2855
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/de9b2855

Branch: refs/heads/master
Commit: de9b2855ed01de82ea9b973486bda8eb73422179
Parents: 1fc7bf4
Author: Andrew Grieve <ag...@chromium.org>
Authored: Thu May 15 16:06:32 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue May 20 14:20:55 2014 -0400

----------------------------------------------------------------------
 www/cdvah/css/style.css       |   9 +++
 www/cdvah/harnessmenu.html    |   1 -
 www/cdvah/js/AddCtrl.js       | 118 -------------------------------------
 www/cdvah/js/HarnessServer.js |   2 +-
 www/cdvah/js/ListCtrl.js      |  15 -----
 www/cdvah/js/app.js           |   8 ---
 www/cdvah/views/add.html      |  55 -----------------
 www/cdvah/views/details.html  |   8 +--
 www/cdvah/views/list.html     |  23 ++++----
 www/cdvahcm/contextMenu.html  |   3 -
 10 files changed, 24 insertions(+), 218 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/css/style.css
----------------------------------------------------------------------
diff --git a/www/cdvah/css/style.css b/www/cdvah/css/style.css
index b26fecf..daab765 100644
--- a/www/cdvah/css/style.css
+++ b/www/cdvah/css/style.css
@@ -16,6 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
 */
+.listen {
+    padding: 2px 20px;
+}
+
+.plugin-list-item {
+    padding-top: 0.2rem;
+    padding-bottom: 0.2rem;
+}
+
 .buttons {
     margin: 15px;
 }

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/harnessmenu.html
----------------------------------------------------------------------
diff --git a/www/cdvah/harnessmenu.html b/www/cdvah/harnessmenu.html
index 883f556..9a562fa 100644
--- a/www/cdvah/harnessmenu.html
+++ b/www/cdvah/harnessmenu.html
@@ -33,7 +33,6 @@
         <script type="text/javascript" src="js/PluginMetadata.js"></script>
         <script type="text/javascript" src="js/UrlRemap.js"></script>
         <script type="text/javascript" src="js/ListCtrl.js"></script>
-        <script type="text/javascript" src="js/AddCtrl.js"></script>
         <script type="text/javascript" src="js/DetailsCtrl.js"></script>
         <script type="text/javascript" src="js/Notify.js"></script>
         <script type="text/javascript" src="js/HttpServer.js"></script>

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/js/AddCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/AddCtrl.js b/www/cdvah/js/AddCtrl.js
deleted file mode 100644
index 021fb58..0000000
--- a/www/cdvah/js/AddCtrl.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.
-*/
-(function(){
-    'use strict';
-
-    /* global myApp */
-    myApp.controller('AddCtrl', ['$q', 'notifier', '$location', '$rootScope', '$scope', '$window', '$routeParams', 'AppsService', 'urlCleanup', function($q, notifier, $location, $rootScope, $scope, $window, $routeParams, AppsService, urlCleanup) {
-        $scope.editing = $routeParams.appId;
-        var editingApp;
-
-        $rootScope.appTitle = $scope.editing ? 'Edit App' : 'Add App';
-
-        if ($scope.editing) {
-            AppsService.getAppList().then(function(appList) {
-                appList.forEach(function(app) {
-                    if (app.appId == $scope.editing) {
-                        editingApp = app;
-                        $scope.editingApp = app;
-                        $scope.appData = {
-                            appId: app.appId,
-                            appUrl: app.url,
-                            installerType: app.type
-                        };
-                    }
-                });
-                if (!$scope.appData) {
-                    notifier.error('Could not find app to edit');
-                }
-            });
-        } else {
-            $scope.appData = {
-                appUrl: '',
-                installerType: 'serve'
-            };
-        }
-
-        $scope.selectTemplate = function() {
-            $scope.appData.appUrl = $scope.appData.serveTemplateValue;
-        };
-
-        $scope.addApp = function() {
-            if ($scope.editing) {
-                // Update the app, write them out, and return to the list.
-                // We deliberately disallow changing the type, since that wouldn't work at all.
-                var oldUrl = editingApp.url;
-                editingApp.appId = $scope.appData.appId;
-                editingApp.url = urlCleanup($scope.appData.appUrl);
-                var urlChanged = oldUrl != editingApp.url;
-                var p = AppsService.editApp($scope.editing, editingApp).then(function() {
-                    notifier.success('App edited');
-                    $location.path('/');
-                });
-
-                if (urlChanged) {
-                    return p.then(function() {
-                        // If the URL changed, trigger an update.
-                        return AppsService.updateApp(editingApp);
-                    }).then(function() {
-                        notifier.success('Updated app due to URL change');
-                    }, function(err) {
-                        notifier.error(err);
-                    });
-                }
-                return p;
-            }
-            return AppsService.addApp($scope.appData.installerType, $scope.appData.appUrl)
-            .then(function(handler) {
-                notifier.success('App Added. Updating...');
-                $location.path('/');
-                return AppsService.updateApp(handler);
-            })
-            .then(function(){
-                notifier.success('Updated successfully');
-            }, function(error) {
-                notifier.error(error);
-            });
-        };
-
-        // True if the optional barcodescanner plugin is installed.
-        $scope.qrEnabled = !!(cordova.plugins && cordova.plugins.barcodeScanner);
-
-        // Scans a QR code, placing the URL into the currently selected of source and pattern.
-        $scope.fetchQR = function() {
-            var deferred = $q.defer();
-            $window.cordova.plugins.barcodeScanner.scan(function(result) {
-                if (!result || result.cancelled || !result.text) {
-                    notifier.error('No QR code received.');
-                    deferred.reject('No QR code received.');
-                } else {
-                    $scope.appData.appUrl = result.text;
-                    notifier.success('QR code received');
-                    deferred.resolve();
-                }
-            },
-            function(error) {
-                notifier.error(error);
-                deferred.reject(error);
-            });
-            return deferred.promise;
-        };
-    }]);
-})();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/js/HarnessServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HarnessServer.js b/www/cdvah/js/HarnessServer.js
index 06d6922..26097be 100644
--- a/www/cdvah/js/HarnessServer.js
+++ b/www/cdvah/js/HarnessServer.js
@@ -153,7 +153,7 @@
 
         function getListenAddress() {
             if (listenAddress) {
-                return listenAddress;
+                return $q.when(listenAddress);
             }
             var deferred = $q.defer();
             chrome.socket.getNetworkList(function(interfaces) {

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/js/ListCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ListCtrl.js b/www/cdvah/js/ListCtrl.js
index 46eadb9..383762b 100644
--- a/www/cdvah/js/ListCtrl.js
+++ b/www/cdvah/js/ListCtrl.js
@@ -66,16 +66,6 @@
             });
         };
 
-        $scope.updateApp = function(app, event) {
-            event.stopPropagation();
-            return AppsService.updateApp(app)
-            .then(function(){
-                notifier.success('Updated successfully');
-            }, function(error) {
-                notifier.error(error);
-            });
-        };
-
         $scope.removeApp = function(app, event) {
             event.stopPropagation();
             var shouldUninstall = confirm('Are you sure you want to uninstall ' + app.appId + '?');
@@ -87,11 +77,6 @@
             }
         };
 
-        $scope.editApp = function(app, event) {
-            event.stopPropagation();
-            $location.path('/edit/' + app.appId);
-        };
-
         $scope.showDetails = function(index) {
             $location.path('/details/' + index);
         };

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/js/app.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/app.js b/www/cdvah/js/app.js
index 01b82ce..c570e82 100644
--- a/www/cdvah/js/app.js
+++ b/www/cdvah/js/app.js
@@ -25,14 +25,6 @@ myApp.config(['$routeProvider', function($routeProvider){
         templateUrl: 'views/list.html',
         controller: 'ListCtrl'
     });
-    $routeProvider.when('/add', {
-        templateUrl: 'views/add.html',
-        controller: 'AddCtrl'
-    });
-    $routeProvider.when('/edit/:appId', {
-        templateUrl: 'views/add.html',
-        controller: 'AddCtrl'
-    });
     $routeProvider.when('/details/:index', {
         templateUrl: 'views/details.html',
         controller: 'DetailsCtrl'

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/views/add.html
----------------------------------------------------------------------
diff --git a/www/cdvah/views/add.html b/www/cdvah/views/add.html
deleted file mode 100644
index ec63bfe..0000000
--- a/www/cdvah/views/add.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!--
-  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.
--->
-<form name="addForm" ng-controller="AddCtrl">
-  <div>
-    <label ng-show="editing">App name<br />
-        <div><input type="text" class="topcoat-text-input" ng-model="appData.appId" autocapitalize="on" /></div>
-    </label>
-  </div>
-  <div>
-    <label ng-show="!editing">How to retrieve the app:<br>
-        <select ng-model="appData.installerType">
-            <option value="serve">cordova serve</option>
-        </select>
-    </label>
-  </div>
-  <div>
-    <label>Enter URL
-        <div><input class="topcoat-text-input" style="width:100%;-webkit-box-sizing:border-box" type="text" ng-model="appData.appUrl" autocorrect="off" autocapitalize="off"></div>
-    </label>
-  </div>
-  <div>
-    <button class="topcoat-button" ng-click="fetchQR(appData, 'appUrl')" ng-show="qrEnabled">QR Code</button>
-  </div>
-  <div>
-    <select ng-model="appData.serveTemplateValue" ng-change="selectTemplate()">
-        <option value="">Template</option>
-        <option value="goo.gl/">goo.gl</option>
-        <option value="localhost:8000">localhost</option>
-        <option value="KEY.t.proxylocal.com">ProxyLocal</option>
-        <option value="KEY.localtunnel.com">LocalTunnel</option>
-        <option value="KEY.ngrok.com">ngrok</option>
-    </select>
-  </div>
-  <div class="buttons">
-      <button ng-click="addApp()" class="topcoat-button--cta">{{ editing ? 'Edit' : 'Add' }}</button>
-      <a href="#/"><button class="topcoat-button" ng-click="back()">Back</button></a>
-  </div>
-</form>
-

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/views/details.html
----------------------------------------------------------------------
diff --git a/www/cdvah/views/details.html b/www/cdvah/views/details.html
index e61a4cf..cc6945c 100644
--- a/www/cdvah/views/details.html
+++ b/www/cdvah/views/details.html
@@ -17,13 +17,11 @@
   under the License.
 -->
 <h2>{{ app.appId }}</h2>
-<div>{{app.url}}</div>
-<div>Last updated: {{app.lastUpdated || 'never'}}</div>
 
 <div class="topcoat-list__container">
     <h3 class="topcoat-list__header">Missing Plugins: {{ app.plugins.missing.length }}</h3>
     <ul class="topcoat-list">
-        <li class="topcoat-list__item" ng-repeat="plugin in app.plugins.missing">
+        <li class="topcoat-list__item plugin-list-item" ng-repeat="plugin in app.plugins.missing">
             <div><strong>{{ plugin.id }}</strong>: {{ plugin.version }}</div>
         </li>
     </ul>
@@ -32,7 +30,7 @@
 <div class="topcoat-list__container">
     <h3 class="topcoat-list__header">Older Plugins: {{ app.plugins.older.length }}</h3>
     <ul class="topcoat-list">
-        <li class="topcoat-list__item" ng-repeat="plugin in app.plugins.older">
+        <li class="topcoat-list__item plugin-list-item" ng-repeat="plugin in app.plugins.older">
             <div><strong>{{ plugin.id }}</strong></div>
             <div>App wants {{ plugin.versions.child }}</div>
             <div>Harness provides {{ plugin.versions.harness }}</div>
@@ -43,7 +41,7 @@
 <div class="topcoat-list__container">
     <h3 class="topcoat-list__header">Newer Plugins: {{ app.plugins.newer.length }}</h3>
     <ul class="topcoat-list">
-        <li class="topcoat-list__item" ng-repeat="plugin in app.plugins.newer">
+        <li class="topcoat-list__item plugin-list-item" ng-repeat="plugin in app.plugins.newer">
             <div><strong>{{ plugin.id }}</strong></div>
             <div>App wants {{ plugin.versions.child }}</div>
             <div>Harness provides {{ plugin.versions.harness }}</div>

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvah/views/list.html
----------------------------------------------------------------------
diff --git a/www/cdvah/views/list.html b/www/cdvah/views/list.html
index 98492e8..42af081 100644
--- a/www/cdvah/views/list.html
+++ b/www/cdvah/views/list.html
@@ -16,29 +16,28 @@
   specific language governing permissions and limitations
   under the License.
 -->
+<div class="listen">
+    Listening. IP = <strong>{{ipAddress}}</strong>
+</div>
 <div class="topcoat-list__container">
     <h3 class="topcoat-list__header">Installed Apps</h3>
     <ul class="topcoat-list">
-        <li class="topcoat-list__item" ng-repeat="app in appList" ng-click="showDetails($index)">
+        <li class="topcoat-list__item" ng-repeat="app in appList">
             <div>{{app.appId}}</div>
-            <div>{{app.url}}</div>
             <div ng-show="app.updatingStatus === null">Last updated: {{app.lastUpdated || 'never'}}</div>
             <div ng-show="app.updatingStatus !== null">Update in progress: {{app.updatingStatus}}%</div>
-            <div ng-show="app.plugins.missing.length + app.plugins.newer.length + app.plugins.older.length > 0">Plugins: {{ app.plugins.missing.length }} missing, {{ app.plugins.newer.length + app.plugins.older.length }} differ</div>
+            <div ng-show="app.plugins.missing.length + app.plugins.newer.length + app.plugins.older.length > 0">
+                Plugins:
+                <span style="color:#c00" ng-show="app.plugins.missing.length">{{ app.plugins.missing.length }} missing</span><span ng-show="app.plugins.missing.length && (app.plugins.older.length || app.plugins.newer.length)">, </span>
+                <span style="color:#822" ng-show="app.plugins.older.length">{{ app.plugins.older.length }} outdated<span><span ng-show="app.plugins.older.length && app.plugins.newer.length">, </span>
+                <span ng-show="app.plugins.newer.length">{{ app.plugins.newer.length }} newer</span>
+            </div>
             <div ng-show="app.plugins.missing.length + app.plugins.newer.length + app.plugins.older.length == 0">Plugins: OK</div>
             <button ng-click="launchApp(app, $event)">Launch</button>
-            <button ng-click="updateApp(app, $event)">Update</button>
             <button ng-click="removeApp(app, $event)">Remove</button>
-            <button ng-click="editApp(app, $event)">Edit</button>
+            <button ng-click="showDetails($index)">Details</button>
         </li>
     </ul>
 </div>
-<br />
-<div class="buttons">
-    <a href="#/add"><button class="topcoat-button--cta">Add app</button></a>
-</div>
 
-<div class="listen">
-    Listening. IP = <strong>{{ipAddress}}</strong>
-</div>
 

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/de9b2855/www/cdvahcm/contextMenu.html
----------------------------------------------------------------------
diff --git a/www/cdvahcm/contextMenu.html b/www/cdvahcm/contextMenu.html
index 5311fc6..39848aa 100644
--- a/www/cdvahcm/contextMenu.html
+++ b/www/cdvahcm/contextMenu.html
@@ -65,9 +65,6 @@
     <p>Tap Anywhere to Close</p>
     <ul>
         <li>
-            <button class="fullwidthElement" onclick="sendEvent('updateApp')">Update</button>
-        </li>
-        <li>
             <button class="fullwidthElement" onclick="sendEvent('restartApp')">Restart</button>
         </li>
         <li>


[3/5] git commit: Make ResourcesLoader be able to delete files (as well as directories)

Posted by ag...@apache.org.
Make ResourcesLoader be able to delete files (as well as directories)


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/074d388a
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/074d388a
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/074d388a

Branch: refs/heads/master
Commit: 074d388a54ba6942fdbd5b8e9315d52cd8eca7bb
Parents: f205214
Author: Andrew Grieve <ag...@chromium.org>
Authored: Tue May 20 14:42:13 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue May 20 14:42:13 2014 -0400

----------------------------------------------------------------------
 www/cdvah/js/ResourcesLoader.js | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/074d388a/www/cdvah/js/ResourcesLoader.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ResourcesLoader.js b/www/cdvah/js/ResourcesLoader.js
index 8a213d6..442c9c2 100644
--- a/www/cdvah/js/ResourcesLoader.js
+++ b/www/cdvah/js/ResourcesLoader.js
@@ -185,13 +185,19 @@
                 });
             },
 
-            deleteDirectory: function(url) {
+            delete: function(url) {
                 return resolveURL(url)
-                .then(function(dirEntry) {
+                .then(function(entry) {
                     var deferred = $q.defer();
-                    dirEntry.removeRecursively(deferred.resolve, function(error) {
-                        deferred.reject(new Error('There was an error deleting the directory: ' + url + ' ' + JSON.stringify(error)));
-                    });
+                    if (entry.removeRecursively) {
+                        entry.removeRecursively(deferred.resolve, function(error) {
+                            deferred.reject(new Error('There was an error deleting directory: ' + url + ' ' + JSON.stringify(error)));
+                        });
+                    } else {
+                        entry.remove(deferred.resolve, function(error) {
+                            deferred.reject(new Error('There was an error deleting file: ' + url + ' ' + JSON.stringify(error)));
+                        });
+                    }
                     return deferred.promise;
                 }, function() {});
             },


[4/5] git commit: Big refactor to support pushing of apps via HTTP.

Posted by ag...@apache.org.
Big refactor to support pushing of apps via HTTP.

This removes the AppHarness' ability to updating apps by fetching from
`cordova serve`. Instead, app resources are now pushed via
HarnessServer.


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/da594dff
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/da594dff
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/da594dff

Branch: refs/heads/master
Commit: da594dffbab000b2864f306cd3f9f15e7ef8644c
Parents: 074d388
Author: Andrew Grieve <ag...@chromium.org>
Authored: Tue May 20 15:50:26 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue May 20 15:51:05 2014 -0400

----------------------------------------------------------------------
 README.md                        |  48 +++-
 www/cdvah/harnessmenu.html       |   2 +-
 www/cdvah/js/AppsService.js      | 119 +++++-----
 www/cdvah/js/DirectoryManager.js | 100 +++++++++
 www/cdvah/js/HarnessServer.js    | 219 ++++++++++++------
 www/cdvah/js/HttpServer.js       | 403 +++++++++++++++++++++++-----------
 www/cdvah/js/Installer.js        | 168 ++++++++------
 www/cdvah/js/PluginMetadata.js   |   2 +-
 www/cdvah/js/ResourcesLoader.js  |   4 +-
 www/cdvah/js/ServeInstaller.js   | 197 -----------------
 www/cdvah/js/app.js              |   2 +-
 11 files changed, 721 insertions(+), 543 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index a61ee2c..c3d9d40 100644
--- a/README.md
+++ b/README.md
@@ -57,27 +57,57 @@ And also use Chrome DevTool's [Reverse Port Forwarding](https://developers.googl
 
 ## Commands
 
-### /push
-
-Add or update an app's settings, and then update & launch:
-
-    curl -X POST http://$IP_ADDRESS:2424/push?type=serve&name=com.example.YourApp&url=http://$SERVE_HOST_ADDRESS:8000
-
 ### /menu
 
 Show in-app overlay menu.
 
-    curl -X POST http://$IP_ADDRESS:2424/menu
+    curl -v -X POST "http://$IP_ADDRESS:2424/menu"
 
 ### /exec
 
 Executes a JS snippet:
 
-    curl -X POST http://$IP_ADDRESS:2424/exec?code='alert(1)'
+    curl -v -X POST "http://$IP_ADDRESS:2424/exec?code='alert(1)'"
+
+### /launch
+
+Starts the app with the given ID (or the first app if none is given).
+
+    curl -v -X POST "http://$IP_ADDRESS:2424/launch?appId=a.b.c"
 
 ### /info
 
 Returns JSON of server info / app state
 
-    curl http://$IP_ADDRESS:2424/info
+    curl -v "http://$IP_ADDRESS:2424/info"
+
+### /assetmanifest
+
+Returns JSON of the asset manifest for the given app ID (or the first app if none is given).
+
+    curl -v "http://$IP_ADDRESS:2424/assetmanifest?appId=a.b.c"
+
+### /prepupdate
+
+Tell the interface that an update is in progress for the given app ID (or the first app if none is given).
+
+    echo '{"transferSize": 100}' | curl -v -X POST -d @- "http://$IP_ADDRESS:2424/prepupdate?app=foo"
+
+### /deletefiles
+
+Deletes a set of files within the given app ID (or the first app if none is given).
+
+    echo '{"paths":["www/index.html"]}' | curl -v -X POST -d @- "http://$IP_ADDRESS:2424/deletefiles?appId=a.b.c"
+
+### /putfile
+
+Updates a single file within the given app ID (or the first app if none is given).
+
+    cat file | curl -v -X PUT -d @- "http://$IP_ADDRESS:2424/assetmanifest?appId=a.b.c&path=www/index.html&etag=1234"
+
+### /deleteapp
+
+Deletes the app with the given ID (or the first app if none is given).
 
+    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.

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/harnessmenu.html
----------------------------------------------------------------------
diff --git a/www/cdvah/harnessmenu.html b/www/cdvah/harnessmenu.html
index 9a562fa..89f5432 100644
--- a/www/cdvah/harnessmenu.html
+++ b/www/cdvah/harnessmenu.html
@@ -27,8 +27,8 @@
         <script type="text/javascript" src="js/CacheClear.js"></script>
         <script type="text/javascript" src="js/AppHarnessUI.js"></script>
         <script type="text/javascript" src="js/Installer.js"></script>
-        <script type="text/javascript" src="js/ServeInstaller.js"></script>
         <script type="text/javascript" src="js/ResourcesLoader.js"></script>
+        <script type="text/javascript" src="js/DirectoryManager.js"></script>
         <script type="text/javascript" src="js/AppsService.js"></script>
         <script type="text/javascript" src="js/PluginMetadata.js"></script>
         <script type="text/javascript" src="js/UrlRemap.js"></script>

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/AppsService.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/AppsService.js b/www/cdvah/js/AppsService.js
index c044f59..98b920a 100644
--- a/www/cdvah/js/AppsService.js
+++ b/www/cdvah/js/AppsService.js
@@ -19,34 +19,22 @@
 (function() {
     'use strict';
     /* global myApp */
-    myApp.factory('AppsService', ['$q', 'ResourcesLoader', 'INSTALL_DIRECTORY', 'APPS_JSON', 'notifier', 'PluginMetadata', 'AppHarnessUI', function($q, ResourcesLoader, INSTALL_DIRECTORY, APPS_JSON, notifier, PluginMetadata, AppHarnessUI) {
-
+    myApp.factory('AppsService', ['$q', 'ResourcesLoader', 'INSTALL_DIRECTORY', 'APPS_JSON', 'notifier', 'AppHarnessUI', function($q, ResourcesLoader, INSTALL_DIRECTORY, APPS_JSON, notifier, AppHarnessUI) {
         // Map of type -> installer.
-        var _installerFactories = {};
+        var _installerFactories = Object.create(null);
         // Array of installer objects.
         var _installers = null;
         // The app that is currently running.
         var activeInstaller = null;
 
-        function createInstallHandlersFromJson(json) {
-            var appList = json.appList || [];
-            var ret = [];
-            for (var i = 0; i < appList.length; i++) {
-                var entry = appList[i];
-                var factory = _installerFactories[entry.appType];
-                var installer = factory.createFromJson(entry.appUrl, entry.appId);
-                installer.lastUpdated = entry.lastUpdated && new Date(entry.lastUpdated);
-                installer.installPath = entry.installPath;
-                installer.plugins = PluginMetadata.process(entry.plugins);
-                ret.push(installer);
-            }
-            return ret;
-        }
-
         function readAppsJson() {
             var deferred = $q.defer();
             ResourcesLoader.readJSONFileContents(APPS_JSON)
             .then(function(result) {
+                if (result['fileVersion'] !== 1) {
+                    console.warn('Ignoring old version of apps.json');
+                    result = {};
+                }
                 deferred.resolve(result);
             }, function() {
                 // Error means first run.
@@ -61,25 +49,33 @@
             }
 
             return readAppsJson()
-            .then(function(appsJson) {
-                _installers = createInstallHandlersFromJson(appsJson);
+            .then(function(json) {
+                var appList = json['appList'] || [];
+                _installers = [];
+                var i = -1;
+                function next() {
+                    var entry = appList[++i];
+                    if (!entry) {
+                        return;
+                    }
+                    return _installerFactories[entry['appType']].createFromJson(entry)
+                    .then(function(app) {
+                        _installers.push(app);
+                        return next();
+                    }, next);
+                }
+                return next();
             });
         }
 
         function createAppsJson() {
             var appsJson = {
+                'fileVersion': 1,
                 'appList': []
             };
             for (var i = 0; i < _installers.length; ++i) {
                 var installer = _installers[i];
-                appsJson.appList.push({
-                    'appId' : installer.appId,
-                    'appType' : installer.type,
-                    'appUrl' : installer.url,
-                    'lastUpdated': installer.lastUpdated && +installer.lastUpdated,
-                    'installPath': installer.installPath,
-                    'plugins': installer.plugins.raw
-                });
+                appsJson.appList.push(installer.toDiskJson());
             }
             return appsJson;
         }
@@ -99,15 +95,14 @@
                 AppHarnessUI.createOverlay();
             } else if (eventName == 'hideMenu') {
                 AppHarnessUI.destroyOverlay();
-            } else if (eventName == 'updateApp') {
-                AppsService.updateAndLaunchApp(activeInstaller)
-                .then(null, notifier.error);
             } else if (eventName == 'restartApp') {
                 // TODO: Restart in place?
                 AppsService.launchApp(activeInstaller)
                 .then(null, notifier.error);
             } else if (eventName == 'quitApp') {
                 AppsService.quitApp();
+            } else {
+                console.warn('Unknown message from AppHarnessUI: ' + eventName);
             }
         });
 
@@ -124,6 +119,28 @@
                 return createAppsJson();
             },
 
+            // If no appId, then return the first app.
+            // If appId and appType, then create it if it doesn't exist.
+            // Else: return null.
+            getAppById : function(appId, /* optional */ appType) {
+                return initHandlers()
+                .then(function() {
+                    var matches = _installers;
+                    if (appId) {
+                        matches = _installers.filter(function(x) {
+                            return x.appId == appId;
+                        });
+                    }
+                    if (matches.length > 0) {
+                        return matches[0];
+                    }
+                    if (appType) {
+                        return AppsService.addApp(appType, appId);
+                    }
+                    return null;
+                });
+            },
+
             quitApp : function() {
                 if (activeInstaller) {
                     activeInstaller.unlaunch();
@@ -143,10 +160,10 @@
                 });
             },
 
-            addApp : function(installerType, appUrl, /*optional*/ appId) {
-                var installerFactory = _installerFactories[installerType];
+            addApp : function(appType, /* optional */ appId) {
+                var installPath = INSTALL_DIRECTORY + 'app' + new Date().getTime() + '/';
                 return initHandlers().then(function() {
-                    return installerFactory.createFromUrl(appUrl, appId);
+                    return _installerFactories[appType].createNew(installPath, appId);
                 }).then(function(installer) {
                     _installers.push(installer);
                     return writeAppsJson()
@@ -156,13 +173,12 @@
                 });
             },
 
-            editApp : function(oldId, installer) {
-                _installers.forEach(function(inst, i) {
-                    if (inst.appId == oldId) {
-                        _installers.splice(i, 1, installer);
-                    }
-                });
-                return writeAppsJson();
+            uninstallAllApps : function() {
+                var deletePromises = [];
+                for (var i = 0; i < _installers.length; ++i) {
+                    deletePromises.push(AppsService.uninstallApp(_installers[i]));
+                }
+                return $q.all(deletePromises);
             },
 
             uninstallApp : function(installer) {
@@ -173,27 +189,12 @@
                 });
             },
 
-            getLastRunApp : function() {
-                throw new Error('Not implemented.');
-            },
-
-            updateApp : function(installer){
-                var installPath = INSTALL_DIRECTORY + '/' + encodeURIComponent(installer.appId);
-                return installer.updateApp(installPath)
-                .then(writeAppsJson);
-            },
-
-            updateAndLaunchApp : function(installer) {
-                return AppsService.quitApp()
-                .then(function() {
-                    return AppsService.updateApp(installer);
-                }).then(function() {
-                    return AppsService.launchApp(installer);
-                });
+            triggerAppListChange: function() {
+                return writeAppsJson();
             },
 
             registerInstallerFactory : function(installerFactory) {
-                _installerFactories[installerFactory.type] = installerFactory;
+                 _installerFactories[installerFactory.type] = installerFactory;
             },
 
             onAppListChange: null

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/DirectoryManager.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/DirectoryManager.js b/www/cdvah/js/DirectoryManager.js
new file mode 100644
index 0000000..2a85612
--- /dev/null
+++ b/www/cdvah/js/DirectoryManager.js
@@ -0,0 +1,100 @@
+/*
+ * 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.
+*/
+(function(){
+    'use strict';
+
+    /* global myApp */
+    myApp.factory('DirectoryManager', ['$q', 'ResourcesLoader', function($q, ResourcesLoader) {
+        var ASSET_MANIFEST = 'assetmanifest.json';
+        function DirectoryManager(rootURL) {
+            this.rootURL = rootURL;
+            this.lastUpdated = null;
+            this._assetManifest = null;
+            this._flushTimerId = null;
+        }
+
+        DirectoryManager.prototype.deleteAll = function() {
+            this.lastUpdated = null;
+            this._assetManifest = null;
+            window.clearTimeout(this._flushTimerId);
+            return ResourcesLoader.delete(this.rootURL);
+        };
+
+        DirectoryManager.prototype.getAssetManifest = function() {
+            if (this._assetManifest) {
+                return $q.when(this._assetManifest);
+            }
+            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._lazyWriteAssetManifest = function() {
+            if (this._flushTimerId === null) {
+                this._flushTimerId = window.setTimeout(this._writeAssetManifest.bind(this), 1000);
+            }
+        };
+
+        DirectoryManager.prototype._writeAssetManifest = function() {
+            this._flushTimerId = null;
+            var stringContents = JSON.stringify(this._assetManifest);
+            return ResourcesLoader.writeFileContents(this.rootURL + ASSET_MANIFEST, stringContents);
+        };
+
+        DirectoryManager.prototype.addFile = function(srcURL, relativePath, etag) {
+            var self = this;
+            return ResourcesLoader.moveFile(srcURL, this.rootURL + relativePath)
+            .then(function() {
+                self._assetManifest[relativePath] = etag;
+                self._lazyWriteAssetManifest();
+            });
+        };
+
+        DirectoryManager.prototype.writeFile = function(data, relativePath, etag) {
+            var self = this;
+            return ResourcesLoader.writeFileContents(this.rootURL + relativePath, data)
+            .then(function() {
+                self._assetManifest[relativePath] = etag;
+                self._lazyWriteAssetManifest();
+            });
+        };
+
+        DirectoryManager.prototype.deleteFile = function(relativePath) {
+            if (!this._assetManifest[relativePath]) {
+                console.warn('Tried to delete non-existing file: ' + relativePath);
+            } else {
+                delete this._assetManifest[relativePath];
+                this._lazyWriteAssetManifest();
+                return ResourcesLoader.delete(this.rootURL + relativePath);
+            }
+        };
+
+        return DirectoryManager;
+    }]);
+
+})();
+

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/HarnessServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HarnessServer.js b/www/cdvah/js/HarnessServer.js
index 26097be..8f9e45b 100644
--- a/www/cdvah/js/HarnessServer.js
+++ b/www/cdvah/js/HarnessServer.js
@@ -28,17 +28,15 @@
         function ensureMethodDecorator(method, func) {
             return function(req, resp) {
                 if (req.method != method) {
-                    resp.sendTextResponse(405, 'Method Not Allowed\n');
-                } else {
-                    func(req, resp);
+                    return resp.sendTextResponse(405, 'Method Not Allowed\n');
                 }
+                return func(req, resp);
             };
         }
 
         function pipeRequestToFile(req, destUrl) {
-            var outerDeferred = $q.defer();
             var writer = null;
-            req.onData = function(arrayBuffer) {
+            function handleChunk(arrayBuffer) {
                 var ret = $q.when();
                 if (writer == null) {
                    ret = ResourcesLoader.createFileWriter(destUrl)
@@ -46,48 +44,22 @@
                        writer = w;
                    });
                 }
-                return ret
-                .then(function() {
+                return ret.then(function() {
                     var deferred = $q.defer();
                     writer.onwrite = deferred.resolve;
-                    writer.onerror = deferred.reject;
+                    writer.onerror = function() {
+                      deferred.reject(writer.error);
+                    };
                     writer.write(arrayBuffer);
                     return deferred.promise;
                 })
                 .then(function() {
-                    if (req.bytesRemaining === 0) {
-                        outerDeferred.resolve();
+                    if (req.bytesRemaining > 0) {
+                        return req.readChunk().then(handleChunk);
                     }
-                }, outerDeferred.reject);
-            };
-            return outerDeferred.promise;
-        }
-
-        function handlePush(req, resp) {
-            var type = req.getQueryParam('type');
-            var name = req.getQueryParam('name');
-            var url = req.getQueryParam('url');
-            if (!(type && name)) {
-                resp.sendTextResponse(400, 'Missing required query params type=' + type + ' name=' + name + '\n');
-                return;
-            }
-            var ret = $q.when();
-            return ret.then(function() {
-                if (!url) {
-                    resp.sendTextResponse(400, 'Missing required query param "url"\n');
-                    return;
-                }
-                return AppHarnessUI.destroy()
-                .then(function() {
-                    return updateApp(type, name, url);
-                }).then(function() {
-                    notifier.success('Updated ' + name + ' from remote push.');
-                    resp.sendTextResponse(200, '');
-                }, function(e) {
-                    notifier.error(e);
-                    resp.sendTextResponse(500, e + '\n');
                 });
-            });
+            }
+            return req.readChunk().then(handleChunk);
         }
 
         function handleExec(req, resp) {
@@ -103,6 +75,139 @@
             return AppHarnessUI.createOverlay();
         }
 
+        function handleLaunch(req, resp) {
+            var appId = req.getQueryParam('appId');
+            return AppsService.getAppById(appId)
+            .then(function(app) {
+                if (app) {
+                    return AppsService.launchApp(app)
+                    .then(function() {
+                        return resp.sendTextResponse(200, '');
+                    });
+                }
+                return resp.sendTextResponse(412, 'No apps available for launch\n');
+            });
+        }
+
+        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
+                });
+            });
+        }
+
+        function handlePrepUpdate(req, resp) {
+            var appId = req.getQueryParam('appId');
+            var appType = req.getQueryParam('appType') || 'cordova';
+            return AppsService.getAppById(appId, appType)
+            .then(function(app) {
+                return req.readAsJson()
+                .then(function(requestJson) {
+                    app.updatingStatus = 0;
+                    app.updateBytesTotal = +requestJson['transferSize'];
+                    app.updateBytesSoFar = 0;
+                    return resp.sendTextResponse(200, '');
+                });
+            });
+        }
+
+        function handleDeleteFiles(req, resp) {
+            var appId = req.getQueryParam('appId');
+            var appType = req.getQueryParam('appType') || 'cordova';
+            return AppsService.getAppById(appId, appType)
+            .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]);
+                    }
+                    return resp.sendTextResponse(200, '');
+                });
+            });
+        }
+
+        function handleDeleteApp(req, resp) {
+            var appId = req.getQueryParam('appId');
+            var all = req.getQueryParam('all');
+            var ret;
+            if (all) {
+                ret = AppsService.uninstallAllApps();
+            } else {
+                ret = AppsService.getAppById(appId)
+                .then(function(app) {
+                    if (app) {
+                        return AppsService.uninstallApp(app);
+                    }
+                });
+            }
+            return ret.then(function() {
+                return resp.sendTextResponse(200, '');
+            });
+        }
+
+        function handlePutFile(req, resp) {
+            var appId = req.getQueryParam('appId');
+            var appType = req.getQueryParam('appType') || 'cordova';
+            var path = req.getQueryParam('path');
+            var etag = req.getQueryParam('etag');
+            if (!path || !etag) {
+                throw new Error('Request is missing path or etag query params');
+            }
+            return AppsService.getAppById(appId, appType)
+            .then(function(app) {
+                var tmpUrl = ResourcesLoader.createTmpFileUrl();
+                return pipeRequestToFile(req, tmpUrl)
+                .then(function() {
+                    var ret = $q.when();
+                    if (path == 'www/cordova_plugins.js') {
+                        path = 'orig-cordova_plugins.js';
+                    }
+                    if (path == 'www/config.xml') {
+                        ret = ret.then(function() {
+                          return ResourcesLoader.downloadFromUrl(tmpUrl, tmpUrl + '-2');
+                        });
+                    }
+                    ret = ret.then(function() {
+                        return app.directoryManager.addFile(tmpUrl, path, etag);
+                    });
+                    if (path == 'www/config.xml') {
+                        ret = ret.then(function() {
+                            return app.directoryManager.addFile(tmpUrl + '-2', 'config.xml', etag);
+                        });
+                    }
+                    if (path == 'config.xml' || path == 'www/config.xml') {
+                        ret = ret.then(function() {
+                            return app.readConfigXml();
+                        });
+                    } else if (path == 'orig-cordova_plugins.js') {
+                        ret = ret.then(function() {
+                            return app.readCordovaPluginsFile();
+                        });
+                    }
+                    return ret;
+                })
+                .then(function() {
+                    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, '');
+                });
+            });
+        }
+
         function handleInfo(req, resp) {
             var json = {
                 'platform': cordova.platformId,
@@ -114,40 +219,20 @@
             resp.sendJsonResponse(json);
         }
 
-        function updateApp(type, name, url) {
-            return AppsService.getAppList()
-            .then(function(list) {
-                var matches = list && list.filter(function(x) { return x.appId == name; });
-                var promise;
-                if (list && matches.length > 0) {
-                    // App exists.
-                    var app = matches[0];
-                    app.url = url;
-                    promise = $q.when(app);
-                } else {
-                    // New app.
-                    promise = AppsService.addApp(type, url, name).then(function(handler) {
-                        var msg = 'Added new app ' + handler.appId + ' from push';
-                        notifier.success(msg);
-                        return handler;
-                    });
-                }
-
-                return promise.then(function(theApp) {
-                    return AppsService.updateAndLaunchApp(theApp);
-                });
-            });
-        }
-
         function start() {
             if (server) {
                 return;
             }
-            server = HttpServer.create()
-                .addRoute('/push', ensureMethodDecorator('POST', handlePush))
+            server = new HttpServer()
                 .addRoute('/exec', ensureMethodDecorator('POST', handleExec))
                 .addRoute('/menu', ensureMethodDecorator('POST', handleMenu))
-                .addRoute('/info', ensureMethodDecorator('GET', handleInfo));
+                .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('/deleteapp', ensureMethodDecorator('POST', handleDeleteApp))
+                .addRoute('/putfile', ensureMethodDecorator('PUT', handlePutFile));
             return server.start();
         }
 

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/HttpServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HttpServer.js b/www/cdvah/js/HttpServer.js
index cd0ef56..9622c28 100644
--- a/www/cdvah/js/HttpServer.js
+++ b/www/cdvah/js/HttpServer.js
@@ -28,17 +28,21 @@
         var STATE_HEADERS_RECEIVED = 2;
         var STATE_REQUEST_RECEIVED = 3;
         var STATE_RESPONSE_STARTED = 4;
-        var STATE_COMPLETE = 5;
+        var STATE_RESPONSE_WAITING_FOR_FLUSH = 5;
+        var STATE_COMPLETE = 6;
 
         function HttpRequest(requestData) {
             this._requestData = requestData;
             this.method = requestData.method;
             this.headers = requestData.headers;
             this.bytesRemaining = 0;
+            this._readChunkCalled = false;
             if (requestData.method == 'POST' || requestData.method == 'PUT') {
                 this.bytesRemaining = parseInt(requestData.headers['content-length'] || '0');
             }
-            this.onData = null; // function(arrayBuffer, req, resp) : Promise
+            if (this.bytesRemaining === 0) {
+                this._requestData.state = STATE_REQUEST_RECEIVED;
+            }
 
             var host = this.headers['host'] || 'localhost';
             var queryMatch = /\?.*/.exec(requestData.resource);
@@ -53,23 +57,63 @@
             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;
+        HttpRequest.prototype.readAsJson = function() {
+            var self = this;
+            return this.readEntireBody()
+            .then(function(arrayBuffer) {
+                var s = arrayBufferToString(arrayBuffer);
+                return JSON.parse(s);
+            }).then(null, function(e) {
+                return self._requestData.httpResponse.sendTextResponse(400, 'Invalid JSON received.\n')
+                .then(function() {
+                    throw e;
+                });
+            });
+        };
+
+        HttpRequest.prototype.readEntireBody = function() {
+            var byteArray = null;
+            var soFar = 0;
+            var self = this;
+            function handleChunk(chunk) {
+                if (byteArray) {
+                    byteArray.set(chunk, soFar);
+                    soFar += chunk.byteLength;
+                }
+                if (self.bytesRemaining === 0) {
+                    return byteArray ? byteArray.buffer : chunk;
+                }
+                return self.readChunk().then(handleChunk);
             }
-            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 this.readChunk().then(handleChunk);
+        };
+
+        HttpRequest.prototype.readChunk = function(/* optional */maxChunkSize) {
+            // Allow readChunk() to be called *once* after request is already received.
+            // This is convenient for empty payloads.
+            if (this._requestData.state === STATE_REQUEST_RECEIVED) {
+                if (this._readChunkCalled) {
+                    throw new Error('readChunk() when request already received.');
+                }
+                this._readChunkCalled = true;
+                if (this.bytesRemaining === 0) {
+                    return $q.when(new ArrayBuffer(0));
                 }
             }
-            return $q.when();
+            var self = this;
+            return this._requestData.socket.read(maxChunkSize)
+            .then(function(chunk) {
+                var chunkSize = chunk.byteLength;
+                console.log('Processing request chunk of size ' + chunkSize);
+                self.bytesRemaining -= chunkSize;
+                if (self.bytesRemaining < 0) {
+                    throw new Error('Bytes remaining negative: ' + self.bytesRemaining);
+                }
+                if (self.bytesRemaining === 0 && self._requestData.state === STATE_HEADERS_RECEIVED) {
+                    self._requestData.state = STATE_REQUEST_RECEIVED;
+                }
+                return chunk;
+            });
         };
 
         function HttpResponse(requestData) {
@@ -77,7 +121,13 @@
             this.headers = Object.create(null);
             var keepAlive = requestData.headers['connection'] === 'keep-alive';
             this.headers['Connection'] = keepAlive ? 'keep-alive' : 'close';
-            this._writeQueue = [];
+            var self = this;
+            requestData.socket.onClose = function(err) {
+                if (err) {
+                    console.error(err);
+                }
+                self._finish(!!err);
+            };
         }
 
         HttpResponse.prototype.sendTextResponse = function(status, message, /* optional */ contentType) {
@@ -85,56 +135,37 @@
             this.headers['Content-Length'] = message.length;
             this._startResponse(status);
             this.writeChunk(stringToArrayBuffer(message));
-            this.close();
+            return this.close();
         };
 
         HttpResponse.prototype.sendJsonResponse = function(json) {
-            this.sendTextResponse(200, JSON.stringify(json, null, 4), 'application/json');
+            return 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();
+            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));
             }
+            return promise;
         };
 
-        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.close = function() {
+            if (this._requestData.state < STATE_RESPONSE_WAITING_FOR_FLUSH) {
+                return this.writeChunk(null);
             }
         };
 
         HttpResponse.prototype._startResponse = function(status) {
             var headers = this.headers;
-            if (this._requestData.httpRequest.bytesRemaining > 0) {
-                headers['Connection'] = 'Close';
+            // Check if they haven't finished reading the request, and error out.
+            if (this._requestData.state < STATE_REQUEST_RECEIVED) {
+                this._requestData.socket.close(new Error('Started to write response before request data was finished.'));
+                return;
             }
             this._requestData.state = STATE_RESPONSE_STARTED;
             var statusMsg = status === 404 ? 'Not Found' :
@@ -150,19 +181,126 @@
         };
 
         HttpResponse.prototype._finish = function(disconnect) {
+            if (this._requestData.state === STATE_COMPLETE) {
+                return;
+            }
             this._requestData.state = STATE_COMPLETE;
+            this._requestData.socket.onClose = null;
             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);
+                this._requestData.socket.close();
             } else {
                 this._requestData.httpServer._onAccept(socketId);
             }
         };
 
+        function Socket(socketId) {
+            this.socketId = socketId;
+            this.alive = true;
+            this.onClose = null;
+            this._pendingReadChunk = null;
+            this._writeQueue = [];
+            this._readInProgress = false;
+        }
+
+        Socket.prototype.unread = function(chunk) {
+            if (this._pendingReadChunk) {
+                throw new Error('Socket.unread called multiple times.');
+            }
+            this._pendingReadChunk = chunk;
+        };
+
+        Socket.prototype.read = function(maxLength) {
+            if (this._readInProgress) {
+                throw new Error('Read already in progress.');
+            }
+            this._readInProgress = true;
+            maxLength = maxLength || Infinity;
+            var self = this;
+            var deferred = $q.defer();
+            var bufSize = Math.min(200 * 1024, maxLength);
+            var chunk = this._pendingReadChunk;
+            if (chunk) {
+                self._readInProgress = false;
+                if (chunk.byteLength <= maxLength) {
+                    this._pendingReadChunk = null;
+                    deferred.resolve(chunk);
+                } else {
+                    this._pendingReadChunk = chunk.slice(maxLength);
+                    deferred.resolve(chunk.slice(0, maxLength));
+                }
+            } else {
+                chrome.socket.read(this.socketId, bufSize, function(readInfo) {
+                    self._readInProgress = false;
+                    if (!readInfo.data) {
+                        var err = new Error('Socket.read() failed with code ' + readInfo.resultCode);
+                        self.close(err);
+                        deferred.reject(err);
+                    } else {
+                        deferred.resolve(readInfo.data);
+                    }
+                });
+            }
+            return deferred.promise;
+        };
+
+        // Multiple writes in are allowed at a time.
+        // A null arrayBuffer can be used as a synchronization point.
+        Socket.prototype.write = function(arrayBuffer) {
+            var deferred = $q.defer();
+            this._writeQueue.push(arrayBuffer, deferred);
+            if (this._writeQueue.length === 2) {
+                this._pokeWriteQueue();
+            }
+            return deferred.promise;
+        };
+
+        Socket.prototype.close = function(/*(optional*/ error) {
+            if (this.alive) {
+                this.alive = false;
+                chrome.socket.destroy(this.socketId);
+                if (this.onClose) {
+                    this.onClose(error);
+                }
+            }
+        };
+
+        Socket.prototype._pokeWriteQueue = function() {
+            if (this._writeQueue.length === 0) {
+                return;
+            }
+            var arrayBuffer = this._writeQueue[0];
+            var deferred = this._writeQueue[1];
+            if (arrayBuffer && arrayBuffer.byteLength > 0) {
+                var self = this;
+                chrome.socket.write(this.socketId, arrayBuffer, function(writeInfo) {
+                    if (writeInfo.bytesWritten !== arrayBuffer.byteLength) {
+                        console.warn('Failed to write entire ArrayBuffer.');
+                    }
+                    self._writeQueue.shift();
+                    self._writeQueue.shift();
+                    if (writeInfo.bytesWritten < 0) {
+                        var err = new Error('Write error: ' + -writeInfo.bytesWritten);
+                        deferred.reject(err);
+                        self.close(err);
+                    } else {
+                        deferred.resolve();
+                        self._pokeWriteQueue();
+                    }
+                });
+            } else {
+                this._writeQueue.shift();
+                this._writeQueue.shift();
+                deferred.resolve();
+                this._pokeWriteQueue();
+            }
+        };
+
+
         function HttpServer() {
             this._requests = Object.create(null); // Map of socketId -> Object
             this._handlers = Object.create(null); // Map of resourcePath -> function(httpRequest, httpResponse)
@@ -197,12 +335,19 @@
             return deferred.promise;
         };
 
+        function acceptLoop(socketId, acceptCallback) {
+            chrome.socket.accept(socketId, function(acceptInfo) {
+                acceptCallback(acceptInfo.socketId);
+                acceptLoop(socketId, acceptCallback);
+            });
+        }
+
         HttpServer.prototype._onAccept = function(socketId) {
             console.log('Connection established on socket ' + socketId);
             var requestData = {
                 state: STATE_NEW,
+                socket: new Socket(socketId),
                 dataAsStr: '', // Used only when parsing head of request.
-                socketId: socketId,
                 method: null,
                 resource: null,
                 httpVersion: null,
@@ -212,29 +357,45 @@
                 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);
+            var self = this;
+            return readRequestHeaders(requestData)
+            .then(function() {
+                var req = new HttpRequest(requestData);
+                var resp = new HttpResponse(requestData);
+                requestData.httpRequest = req;
+                requestData.httpResponse = resp;
+                // Strip query params.
+                var handler = self._handlers[req.path];
+                if (handler) {
+                    // Wrap to catch exceptions.
+                    return $q.when().then(function() {
+                        return handler(req, resp);
+                    }).then(function() {
+                        if (requestData.state < STATE_RESPONSE_WAITING_FOR_FLUSH) {
+                            if (requestData.state == STATE_REQUEST_RECEIVED) {
+                                console.warn('No response was sent for action ' + requestData.resource);
+                                return resp.sendTextResponse(200, '');
+                            } else {
+                                return requestData.socket.close();
+                            }
+                        }
+                    }, function(err) {
+                        console.error('Error while handling ' + req.path, err);
+                        if (requestData.state !== STATE_RESPONSE_WAITING_FOR_FLUSH) {
+                            if (requestData.state < STATE_RESPONSE_STARTED) {
+                                return req.readEntireBody()
+                                .then(function() {
+                                    return resp.sendTextResponse(500, '' + err);
+                                })
+                            } else {
+                                return requestData.socket.close();
+                            }
+                        }
+                    });
+                }
+                return resp.sendTextResponse(404, 'Not Found');
             });
-        }
+        };
 
         function stringToArrayBuffer(str) {
             var view = new Uint8Array(str.length);
@@ -253,60 +414,41 @@
             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;
-                        }
+        function readRequestHeaders(requestData) {
+            return requestData.socket.read()
+            .then(function(arrayBuffer) {
+                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;
+                        requestData.socket.unread(arrayBuffer);
+                        return readRequestHeaders(requestData);
                     }
-                    break;
-                case STATE_HEADERS_RECEIVED:
-                    requestData.httpRequest._feedData(arrayBuffer)
-                    .then(function() {
-                        receiveHttpData(requestData);
-                    }, function(e) {
-                        requestData.httpResponse.sendTextResponse(500, '' + e);
-                    });
-                    return;
-            }
-            receiveHttpData(requestData);
+                } 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.socket.unread(arrayBuffer);
+                        return requestData;
+                    }
+                }
+                return readRequestHeaders(requestData);
+            });
         }
 
         function strip(str) {
@@ -335,11 +477,6 @@
             return headers;
         }
 
-        return {
-            create: function() {
-                return new HttpServer();
-            }
-        };
-
+        return HttpServer;
     }]);
 })();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/Installer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/Installer.js b/www/cdvah/js/Installer.js
index 40f779c..434fc85 100644
--- a/www/cdvah/js/Installer.js
+++ b/www/cdvah/js/Installer.js
@@ -18,108 +18,125 @@
 */
 (function(){
     'use strict';
-    /* global myApp, cordova */
-    myApp.factory('Installer', ['$q', 'UrlRemap', 'ResourcesLoader', 'PluginMetadata', 'CacheClear', function($q, UrlRemap, ResourcesLoader, PluginMetadata, CacheClear) {
 
-        function getAppStartPageFromConfig(configFile) {
-            return ResourcesLoader.readFileContents(configFile)
-            .then(function(contents) {
-                if(!contents) {
-                    throw new Error('Config file is empty. Unable to find a start page for your app.');
-                } else {
-                    var startLocation = 'index.html';
-                    var parser = new DOMParser();
-                    var xmlDoc = parser.parseFromString(contents, 'text/xml');
-                    var els = xmlDoc.getElementsByTagName('content');
-
-                    if(els.length > 0) {
-                        // go through all 'content' elements looking for the 'src' attribute in reverse order
-                        for(var i = els.length - 1; i >= 0; i--) {
-                            var el = els[i];
-                            var srcValue = el.getAttribute('src');
-                            if (srcValue) {
-                                startLocation = srcValue;
-                                break;
-                            }
-                        }
-                    }
-
-                    return startLocation;
-                }
-            });
-        }
+    /* global myApp, cordova */
+    myApp.factory('Installer', ['$q', 'UrlRemap', 'ResourcesLoader', 'PluginMetadata', 'CacheClear', 'DirectoryManager', function($q, UrlRemap, ResourcesLoader, PluginMetadata, CacheClear, DirectoryManager) {
+        var platformId = cordova.require('cordova/platform').id;
 
-        function Installer(url, appId) {
-            this.url = url;
-            this.appId = appId || '';
+        function Installer(installPath) {
             this.updatingStatus = null;
             this.lastUpdated = null;
-            this.installPath = null;
-            this.plugins = {};
+            // Asset manifest is a cache of what files have been downloaded along with their etags.
+            this.directoryManager = new DirectoryManager(installPath);
+            this.appId = null; // Read from config.xml
+            this.appName = null; // Read from config.xml
+            this.startPage = null; // Read from config.xml
+            this.plugins = {}; // Read from orig-cordova_plugins.js
         }
 
-        Installer.prototype.type = '';
+        Installer.type = 'cordova';
+        Installer.prototype.type = 'cordova';
 
-        Installer.prototype.updateApp = function(installPath) {
-            var self = this;
-            this.updatingStatus = 0;
-            this.installPath = installPath;
-            // Cache clearing necessary only for Android.
-            return CacheClear.clear()
+        Installer.createNew = function(installPath, /* optional */ appId) {
+            var ret = new Installer(installPath);
+            ret.appId = appId;
+            return ret.directoryManager.getAssetManifest()
             .then(function() {
-                return self.doUpdateApp();
-            })
+                return ret;
+            });
+        };
+
+        Installer.createFromJson = function(json) {
+            var ret = new Installer(json['installPath']);
+            ret.lastUpdated = json['lastUpdated'] && new Date(json['lastUpdated']);
+            ret.appId = json['appId'];
+            return ret.directoryManager.getAssetManifest()
             .then(function() {
-                self.lastUpdated = new Date();
-                return self.getPluginMetadata();
-            }, null, function(status) {
-                self.updatingStatus = Math.round(status * 100);
-            }).then(function(metadata) {
-                self.plugins = PluginMetadata.process(metadata);
-                var pluginIds = Object.keys(metadata);
-                var newPluginsFileData = PluginMetadata.createNewPluginListFile(pluginIds);
-                return ResourcesLoader.writeFileContents(installPath + '/www/cordova_plugins.js', newPluginsFileData);
-            }).finally(function() {
-                self.updatingStatus = null;
+                return ret.readCordovaPluginsFile();
+            }).then(function() {
+                return ret.readConfigXml();
+            }).then(function() {
+                return ret;
+            }, function(e) {
+                console.warn('Deleting broken app: ' + json['installPath']);
+                ResourcesLoader.delete(json['installPath']);
+                throw e;
+            });
+        };
+
+        Installer.prototype.toDiskJson = function() {
+            return {
+                'appType' : this.type,
+                'appId' : this.appId,
+                'lastUpdated': this.lastUpdated && +this.lastUpdated,
+                'installPath': this.directoryManager.rootURL
+            };
+        };
+
+        Installer.prototype.readCordovaPluginsFile = function(force) {
+            var self = this;
+            return this.directoryManager.getAssetManifest()
+            .then(function(assetManifest) {
+                if (!force && assetManifest['orig-cordova_plugins.js'] == assetManifest['www/cordova_plugins.js']) {
+                    return null;
+                }
+                return self.getPluginMetadata()
+                .then(function(metadata) {
+                    self.plugins = PluginMetadata.process(metadata);
+                    var pluginIds = Object.keys(metadata);
+                    var newPluginsFileData = PluginMetadata.createNewPluginListFile(pluginIds);
+                    return self.directoryManager.writeFile(newPluginsFileData, 'www/cordova_plugins.js', assetManifest['orig-cordova_plugins.js']);
+                });
             });
         };
 
-        Installer.prototype.doUpdateApp = function() {
-            throw new Error('Installer ' + this.type + ' failed to implement doUpdateApp.');
+        Installer.prototype.readConfigXml = function() {
+            var self = this;
+            return ResourcesLoader.readFileContents(this.directoryManager.rootURL + 'config.xml')
+            .then(function(configStr) {
+                function lastEl(els) {
+                    return els[els.length - 1];
+                }
+                var xmlDoc = new DOMParser().parseFromString(configStr, 'text/xml');
+                self.appId = xmlDoc.firstChild.getAttribute('id');
+                var el = lastEl(xmlDoc.getElementsByTagName('content'));
+                self.startPage = el ? el.getAttribute('src') : 'index.html';
+                el = lastEl(xmlDoc.getElementsByTagName('name'));
+                self.appName = el ? el.nodeValue : 'Unnamed';
+            });
         };
 
         Installer.prototype.getPluginMetadata = function() {
-            throw new Error('Installer ' + this.type + ' failed to implement getPluginMetadata.');
+            return ResourcesLoader.readFileContents(this.directoryManager.rootURL + 'orig-cordova_plugins.js')
+            .then(function(contents) {
+                return PluginMetadata.extractPluginMetadata(contents);
+            });
         };
 
         Installer.prototype.deleteFiles = function() {
             this.lastUpdated = null;
-            if (this.installPath) {
-                return ResourcesLoader.deleteDirectory(this.installPath);
-            }
-            return $q.when();
+            return this.directoryManager.deleteAll();
         };
 
         Installer.prototype.unlaunch = function() {
             return UrlRemap.reset();
         };
 
+        Installer.prototype._prepareForLaunch = function() {
+            // Cache clearing necessary only for Android.
+            return CacheClear.clear();
+        };
+
         Installer.prototype.launch = function() {
-            var installPath = this.installPath;
-            var appId = this.appId;
-            if (!installPath) {
-                throw new Error('App ' + appId + ' requires an update');
-            }
-            var configLocation = installPath + '/config.xml';
-
-            return getAppStartPageFromConfig(configLocation)
-            .then(function(rawStartLocation) {
+            var self = this;
+            return $q.when(this._prepareForLaunch())
+            .then(function() {
                 var urlutil = cordova.require('cordova/urlutil');
                 var harnessUrl = urlutil.makeAbsolute(location.pathname);
                 var harnessDir = harnessUrl.replace(/\/[^\/]*\/[^\/]*$/, '');
-                var installUrl = urlutil.makeAbsolute(installPath);
-                var startLocation = urlutil.makeAbsolute(rawStartLocation).replace('/cdvah/', '/');
-                var useNativeStartLocation = cordova.platformId == 'ios';
+                var installUrl = self.directoryManager.rootURL;
+                var startLocation = urlutil.makeAbsolute(self.startPage).replace('/cdvah/', '/');
+                var useNativeStartLocation = platformId == 'ios';
 
                 // Use toNativeURL() so that scheme is file:/ instead of cdvfile:/ (file: has special access).
                 return ResourcesLoader.toNativeURL(installUrl)
@@ -147,6 +164,11 @@
                 });
             });
         };
+
         return Installer;
     }]);
+    myApp.run(['Installer', 'AppsService', function(Installer, AppsService) {
+        AppsService.registerInstallerFactory(Installer);
+    }]);
 })();
+

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/PluginMetadata.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/PluginMetadata.js b/www/cdvah/js/PluginMetadata.js
index 6e90eab..ead8dd8 100644
--- a/www/cdvah/js/PluginMetadata.js
+++ b/www/cdvah/js/PluginMetadata.js
@@ -46,7 +46,7 @@
 
                 // Extract the JSON data from inside the JS file.
                 // It's between two magic comments created by Plugman.
-                var startIndex = pluginListFileContents.indexOf('TOP OF METADATA') + 16;
+                var startIndex = pluginListFileContents.indexOf('TOP OF METADATA') + 15;
                 var endIndex = pluginListFileContents.indexOf('// BOTTOM OF METADATA');
                 var target = pluginListFileContents.substring(startIndex, endIndex);
                 var metadata = JSON.parse(target);

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/ResourcesLoader.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ResourcesLoader.js b/www/cdvah/js/ResourcesLoader.js
index 442c9c2..f8a0434 100644
--- a/www/cdvah/js/ResourcesLoader.js
+++ b/www/cdvah/js/ResourcesLoader.js
@@ -179,8 +179,8 @@
                     return ensureDirectoryExists(dirName(toUrl))
                     .then(function(destEntry) {
                         var deferred = $q.defer();
-                        fromEntry.moveTo(destEntry, baseName(toUrl), deferred.reslove, deferred.reject);
-                        return deferred;
+                        fromEntry.moveTo(destEntry, baseName(toUrl), deferred.resolve, deferred.reject);
+                        return deferred.promise;
                     });
                 });
             },

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/ServeInstaller.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ServeInstaller.js b/www/cdvah/js/ServeInstaller.js
deleted file mode 100644
index ed7ea98..0000000
--- a/www/cdvah/js/ServeInstaller.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.
-*/
-(function(){
-    'use strict';
-
-    var ASSET_MANIFEST_PATH = 'installmanifest.json';
-
-    /* global myApp */
-    myApp.run(['$q', 'Installer', 'AppsService', 'ResourcesLoader', 'urlCleanup', 'PluginMetadata', function($q, Installer, AppsService, ResourcesLoader, urlCleanup, PluginMetadata) {
-        var platformId = cordova.require('cordova/platform').id;
-
-        function ServeInstaller(url, appId) {
-            Installer.call(this, url, appId);
-            // Asset manifest is a cache of what files have been downloaded along with their etags.
-            this._assetManifest = null;
-            this._cachedProjectJson = null;
-            this._cachedConfigXml = null;
-        }
-        ServeInstaller.prototype = Object.create(Installer.prototype);
-
-        ServeInstaller.prototype.type = 'serve';
-
-        ServeInstaller.prototype._readAssetManifest = function() {
-            var deferred = $q.defer();
-            var me = this;
-            ResourcesLoader.readJSONFileContents(this.installPath + '/' + ASSET_MANIFEST_PATH)
-            .then(function(result) {
-                me._assetManifest = result;
-                deferred.resolve();
-            }, function() {
-                me._assetManifest = {
-                    'etagByPath': {}
-                };
-                deferred.resolve();
-            });
-            return deferred.promise;
-        };
-
-        ServeInstaller.prototype._writeAssetManifest = function() {
-            var stringContents = JSON.stringify(this._assetManifest);
-            return ResourcesLoader.writeFileContents(this.installPath + '/' + ASSET_MANIFEST_PATH, stringContents);
-        };
-
-        function fetchMetaServeData(url) {
-            var projectJsonUrl = url + '/' + platformId + '/project.json';
-            return ResourcesLoader.xhrGet(projectJsonUrl, true)
-            .then(null, function() {
-                // If there was no :8000, try again with one appended.
-                if (!/:(\d)/.test(url)) {
-                    var newUrl = url.replace(/(.*?\/\/[^\/]*)/, '$1:8000');
-                    if (newUrl != url) {
-                        url = newUrl;
-                        projectJsonUrl = url + '/' + platformId + '/project.json';
-                        return ResourcesLoader.xhrGet(projectJsonUrl, true);
-                    }
-                }
-                throw new Error('Could not reach server at: ' + url);
-            })
-            .then(function(projectJson) {
-                return ResourcesLoader.xhrGet(url + projectJson.configPath)
-                .then(function(configXmlRaw) {
-                    var configXml = new DOMParser().parseFromString(configXmlRaw, 'text/xml');
-                    var appId = configXml.firstChild.getAttribute('id');
-                    return {
-                        url: url,
-                        projectJson: projectJson,
-                        configXml: configXmlRaw,
-                        appId: appId
-                    };
-                });
-            });
-        }
-        // TODO: update should be more atomic. Maybe download to a new directory?
-        ServeInstaller.prototype.doUpdateApp = function() {
-            if (this._assetManifest) {
-                return this._doUpdateAppForReal();
-            }
-            var me = this;
-            return this._readAssetManifest().then(function() {
-                return me._doUpdateAppForReal();
-            });
-        };
-
-        ServeInstaller.prototype._bulkDownload = function(files) {
-            var installPath = this.installPath;
-            var wwwPath = this._cachedProjectJson.wwwPath;
-            var deferred = $q.defer();
-            var self = this;
-            // Write the asset manifest to disk at most every 2 seconds.
-            var assetManifestDirty = 0; // 0 = false, 1 = true, 2 = terminate interval.
-            var intervalId = setInterval(function() {
-                if (assetManifestDirty) {
-                    if (assetManifestDirty == 2) {
-                        clearInterval(intervalId);
-                    }
-                    self._writeAssetManifest();
-                    assetManifestDirty = 0;
-                }
-            }, 2000);
-
-            console.log('Number of files to fetch: ' + files.length);
-            var i = 0;
-            var totalFiles = files.length + 1; // + 1 for the updateAppMeta.
-            deferred.notify((i + 1) / totalFiles);
-            function downloadNext() {
-                if (i > 0) {
-                    self._assetManifest[files[i - 1].path] = files[i - 1].etag;
-                    assetManifestDirty = 1;
-                }
-                if (!files[i]) {
-                    assetManifestDirty = 2;
-                    deferred.resolve();
-                    return;
-                }
-                deferred.notify((i + 1) / totalFiles);
-
-                var sourceUrl = self.url + wwwPath + files[i].path;
-                var destPath = installPath + '/www' + files[i].path;
-                if (files[i].path == '/cordova_plugins.js') {
-                    destPath = installPath + '/orig-cordova_plugins.js';
-                }
-                console.log(destPath);
-                i += 1;
-                ResourcesLoader.downloadFromUrl(sourceUrl, destPath).then(downloadNext, deferred.reject);
-            }
-            downloadNext();
-            return deferred.promise;
-        };
-
-        ServeInstaller.prototype._doUpdateAppForReal = function() {
-            var installPath = this.installPath;
-            var self = this;
-
-            return fetchMetaServeData(this.url)
-            .then(function(meta) {
-                self._cachedProjectJson = meta.projectJson;
-                self._cachedConfigXml = meta.configXml;
-                self.appId = self.appId || meta.appId;
-                var files = self._cachedProjectJson.wwwFileList;
-                files = files.filter(function(f) {
-                    // Don't download cordova.js or plugins. We want to use the version bundled with the harness.
-                    // Do download cordova_plugins.js, since we need that to compare plugins with the harness.
-                    var isPlugin = /\/cordova\.js$|^\/plugins\//.exec(f.path);
-                    var haveAlready = self._assetManifest[f.path] == f.etag;
-                    return (!isPlugin && !haveAlready);
-                });
-                return ResourcesLoader.writeFileContents(installPath + '/config.xml', self._cachedConfigXml)
-                .then(function() {
-                    return self._bulkDownload(files);
-                });
-            });
-        };
-
-        ServeInstaller.prototype.getPluginMetadata = function() {
-            return ResourcesLoader.readFileContents(this.installPath + '/orig-cordova_plugins.js')
-            .then(function(contents) {
-                return PluginMetadata.extractPluginMetadata(contents);
-            });
-        };
-
-        function createFromUrl(url, /*option*/ appId) {
-            // Strip platform and trailing slash if they exist.
-            url = urlCleanup(url);
-            // Fetch config.xml.
-            return fetchMetaServeData(url)
-            .then(function(meta) {
-                return new ServeInstaller(meta.url, appId || meta.appId);
-            });
-        }
-
-        function createFromJson(url, appId) {
-            return new ServeInstaller(url, appId);
-        }
-
-        AppsService.registerInstallerFactory({
-            type: 'serve',
-            createFromUrl: createFromUrl, // returns a promise.
-            createFromJson: createFromJson // does not return a promise.
-        });
-    }]);
-})();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/da594dff/www/cdvah/js/app.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/app.js b/www/cdvah/js/app.js
index c570e82..e324ab1 100644
--- a/www/cdvah/js/app.js
+++ b/www/cdvah/js/app.js
@@ -43,7 +43,7 @@ myApp.factory('urlCleanup', function() {
 
 document.addEventListener('deviceready', function() {
     cordova.filesystem.getDataDirectory(false, function(dirEntry) {
-        myApp.value('INSTALL_DIRECTORY', dirEntry.toURL() + 'apps');
+        myApp.value('INSTALL_DIRECTORY', dirEntry.toURL() + 'apps/');
         myApp.value('APPS_JSON', dirEntry.toURL() + 'apps.json');
         window.requestFileSystem(window.TEMPORARY, 1 * 1024 * 1024, function(fs) {
             myApp.value('TEMP_DIR', fs.root.toURL());