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:55 UTC

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

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"
+  }
+}