You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2014/05/23 20:06:02 UTC
[1/3] git commit: Delete /prepupdate action,
add concept of manifest etag
Repository: cordova-app-harness
Updated Branches:
refs/heads/master 73ca9ca31 -> e18f4bfed
Delete /prepupdate action, add concept of manifest etag
Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/79b69286
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/79b69286
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/79b69286
Branch: refs/heads/master
Commit: 79b69286cbc8e56b22f54e8b9cd90192fc531cc9
Parents: 73ca9ca
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:02:11 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:02:11 2014 -0400
----------------------------------------------------------------------
.jshintignore | 1 -
www/cdvah/js/AppsService.js | 6 +-
www/cdvah/js/CordovaInstaller.js | 7 +-
www/cdvah/js/DirectoryManager.js | 80 ++++++++++++++---------
www/cdvah/js/HarnessServer.js | 117 +++++++++++++++++++++-------------
www/cdvah/js/HttpServer.js | 31 ++++++---
www/cdvah/js/Installer.js | 6 +-
www/cdvah/js/ListCtrl.js | 5 +-
www/cdvah/js/app.js | 2 +
9 files changed, 157 insertions(+), 98 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/.jshintignore
----------------------------------------------------------------------
diff --git a/.jshintignore b/.jshintignore
deleted file mode 100644
index 4e6ee77..0000000
--- a/.jshintignore
+++ /dev/null
@@ -1 +0,0 @@
-www/cdvah_js/libs/*
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/AppsService.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/AppsService.js b/www/cdvah/js/AppsService.js
index aac7856..3063fc2 100644
--- a/www/cdvah/js/AppsService.js
+++ b/www/cdvah/js/AppsService.js
@@ -116,6 +116,10 @@
});
},
+ getActiveApp: function() {
+ return activeInstaller;
+ },
+
getAppListAsJson : function() {
return createAppsJson();
},
@@ -162,7 +166,7 @@
},
addApp : function(appType, /* optional */ appId) {
- var installPath = INSTALL_DIRECTORY + 'app' + new Date().getTime() + '/';
+ var installPath = INSTALL_DIRECTORY + 'app' + Math.floor(Math.random() * 0xFFFFFFFF).toString(36) + '/';
return initHandlers().then(function() {
var Ctor = _installerFactories[appType];
return new Ctor().init(installPath, appId);
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/CordovaInstaller.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/CordovaInstaller.js b/www/cdvah/js/CordovaInstaller.js
index d6a8dda..f277e82 100644
--- a/www/cdvah/js/CordovaInstaller.js
+++ b/www/cdvah/js/CordovaInstaller.js
@@ -59,11 +59,8 @@
};
CordovaInstaller.prototype.readCordovaPluginsFile = function() {
- var self = this;
- return this.directoryManager.getAssetManifest()
- .then(function(assetManifest) {
- return self.updateCordovaPluginsFile(assetManifest['orig-cordova_plugins.js']);
- });
+ var etag = this.directoryManager.getAssetEtag('orig-cordova_plugins.js');
+ return this.updateCordovaPluginsFile(etag);
};
return CordovaInstaller;
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/DirectoryManager.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/DirectoryManager.js b/www/cdvah/js/DirectoryManager.js
index 86d7543..3ff0ef5 100644
--- a/www/cdvah/js/DirectoryManager.js
+++ b/www/cdvah/js/DirectoryManager.js
@@ -22,36 +22,49 @@
/* global myApp */
myApp.factory('DirectoryManager', ['$q', 'ResourcesLoader', function($q, ResourcesLoader) {
var ASSET_MANIFEST = 'assetmanifest.json';
- function DirectoryManager(rootURL) {
+
+ function DirectoryManager() {}
+
+ DirectoryManager.prototype.init = function(rootURL) {
this.rootURL = rootURL;
- this.lastUpdated = null;
this.onFileAdded = null;
this._assetManifest = null;
+ this._assetManifestEtag = null;
this._flushTimerId = null;
- }
+ var deferred = $q.defer();
+ var me = this;
+ ResourcesLoader.readJSONFileContents(rootURL + ASSET_MANIFEST)
+ .then(function(json) {
+ me._assetManifest = json['assetManifest'];
+ me._assetManifestEtag = json['etag'];
+ deferred.resolve();
+ }, function() {
+ me._assetManifest = {};
+ me._assetManifestEtag = 0;
+ deferred.resolve();
+ });
+ return deferred.promise;
+ };
DirectoryManager.prototype.deleteAll = function() {
- this.lastUpdated = null;
this._assetManifest = null;
+ this._assetManifestEtag = null;
window.clearTimeout(this._flushTimerId);
return ResourcesLoader.delete(this.rootURL);
};
DirectoryManager.prototype.getAssetManifest = function() {
- if (this._assetManifest) {
- return $q.when(this._assetManifest);
+ return this._assetManifest;
+ };
+
+ DirectoryManager.prototype.getAssetEtag = function(relativePath) {
+ if (this._assetManifest.hasOwnProperty(relativePath)) {
+ return this._assetManifest[relativePath];
}
- var deferred = $q.defer();
- var me = this;
- ResourcesLoader.readJSONFileContents(this.rootURL + ASSET_MANIFEST)
- .then(function(json) {
- me._assetManifest = json;
- deferred.resolve(json);
- }, function() {
- me._assetManifest = {};
- deferred.resolve({});
- });
- return deferred.promise;
+ };
+
+ DirectoryManager.prototype.getAssetManifestEtag = function() {
+ return (this._assetManifestEtag).toString(36).toUpperCase();
};
DirectoryManager.prototype._lazyWriteAssetManifest = function() {
@@ -60,9 +73,25 @@
}
};
+ DirectoryManager.prototype._updateManifest = function(relativePath, etag) {
+ if (etag !== null) {
+ this._assetManifest[relativePath] = etag;
+ } else {
+ delete this._assetManifest[relativePath];
+ }
+ this._assetManifestEtag = Math.floor(Math.random() * 0xFFFFFFFF);
+ this._lazyWriteAssetManifest();
+ if (etag !== null && this.onFileAdded) {
+ return this.onFileAdded(relativePath, etag);
+ }
+ };
+
DirectoryManager.prototype._writeAssetManifest = function() {
this._flushTimerId = null;
- var stringContents = JSON.stringify(this._assetManifest);
+ var stringContents = JSON.stringify({
+ 'assetManifest': this._assetManifest,
+ 'etag': this._assetManifestEtag
+ });
return ResourcesLoader.writeFileContents(this.rootURL + ASSET_MANIFEST, stringContents);
};
@@ -70,11 +99,7 @@
var self = this;
return ResourcesLoader.moveFile(srcURL, this.rootURL + relativePath)
.then(function() {
- self._assetManifest[relativePath] = etag;
- self._lazyWriteAssetManifest();
- if (self.onFileAdded) {
- return self.onFileAdded(relativePath, etag);
- }
+ return self._updateManifest(relativePath, etag);
});
};
@@ -82,11 +107,7 @@
var self = this;
return ResourcesLoader.writeFileContents(this.rootURL + relativePath, data)
.then(function() {
- self._assetManifest[relativePath] = etag;
- self._lazyWriteAssetManifest();
- if (self.onFileAdded) {
- return self.onFileAdded(relativePath, etag);
- }
+ return self._updateManifest(relativePath, etag);
});
};
@@ -94,8 +115,7 @@
if (!this._assetManifest[relativePath]) {
console.warn('Tried to delete non-existing file: ' + relativePath);
} else {
- delete this._assetManifest[relativePath];
- this._lazyWriteAssetManifest();
+ this._updateManifest(relativePath, null);
return ResourcesLoader.delete(this.rootURL + relativePath);
}
};
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/HarnessServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HarnessServer.js b/www/cdvah/js/HarnessServer.js
index 6812fdb..78ffc7c 100644
--- a/www/cdvah/js/HarnessServer.js
+++ b/www/cdvah/js/HarnessServer.js
@@ -51,7 +51,7 @@
// curl -v -X POST "http://$IP_ADDRESS:2424/deleteapp?appId=a.b.c"
// curl -v -X POST "http://$IP_ADDRESS:2424/deleteapp?all=true" # Delete all apps.
- myApp.factory('HarnessServer', ['$q', 'HttpServer', 'ResourcesLoader', 'AppHarnessUI', 'AppsService', 'notifier', function($q, HttpServer, ResourcesLoader, AppHarnessUI, AppsService, notifier) {
+ myApp.factory('HarnessServer', ['$q', 'HttpServer', 'ResourcesLoader', 'AppHarnessUI', 'AppsService', 'notifier', 'APP_VERSION', function($q, HttpServer, ResourcesLoader, AppHarnessUI, AppsService, notifier, APP_VERSION) {
var PROTOCOL_VER = 2;
var server = null;
@@ -121,49 +121,41 @@
});
}
- function handleAssetManifest(req, resp) {
- var appId = req.getQueryParam('appId');
- return AppsService.getAppById(appId)
- .then(function(app) {
- if (app) {
- return app.directoryManager.getAssetManifest();
- }
- return null;
- }).then(function(assetManifest) {
- resp.sendJsonResponse({
- 'assetManifest': assetManifest,
- 'platform': cordova.platformId,
- 'cordovaVer': cordova.version,
- 'protocolVer': PROTOCOL_VER,
- });
- });
+ function getAssetManifestJson(app) {
+ return {
+ 'assetManifest': app && app.directoryManager.getAssetManifest(),
+ 'assetManifestEtag': app ? app.directoryManager.getAssetManifestEtag() : '0',
+ 'platform': cordova.platformId,
+ 'cordovaVer': cordova.version,
+ 'protocolVer': PROTOCOL_VER
+ };
}
- function handlePrepUpdate(req, resp) {
+ function handleAssetManifest(req, resp) {
var appId = req.getQueryParam('appId');
- var appType = req.getQueryParam('appType') || 'cordova';
- return AppsService.getAppById(appId, appType)
+ return AppsService.getAppById(appId)
.then(function(app) {
- return req.readAsJson()
- .then(function(requestJson) {
- app.updatingStatus = 0;
- app.updateBytesTotal = +requestJson['transferSize'];
- app.updateBytesSoFar = 0;
- return resp.sendTextResponse(200, '');
- });
+ return resp.sendJsonResponse(200, getAssetManifestJson(app));
});
}
function handleDeleteFiles(req, resp) {
var appId = req.getQueryParam('appId');
- var appType = req.getQueryParam('appType') || 'cordova';
- return AppsService.getAppById(appId, appType)
+ var manifestEtag = req.getQueryParam('manifestEtag');
+ return AppsService.getAppById(appId)
.then(function(app) {
return req.readAsJson()
.then(function(requestJson) {
- var paths = requestJson['paths'];
- for (var i = 0; i < paths.length; ++i) {
- app.directoryManager.deleteFile(paths[i]);
+ if (app) {
+ if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+ return resp.sendJsonResponse(409, getAssetManifestJson(app));
+ }
+ var paths = requestJson['paths'];
+ for (var i = 0; i < paths.length; ++i) {
+ app.directoryManager.deleteFile(paths[i]);
+ }
+ } else {
+ console.log('Warning: tried to delete files from non-existant app: ' + appId);
}
return resp.sendTextResponse(200, '');
});
@@ -194,30 +186,57 @@
var appType = req.getQueryParam('appType') || 'cordova';
var path = req.getQueryParam('path');
var etag = req.getQueryParam('etag');
+ var manifestEtag = req.getQueryParam('manifestEtag');
if (!path || !etag) {
throw new Error('Request is missing path or etag query params');
}
return AppsService.getAppById(appId, appType)
.then(function(app) {
+ // Checking the manifest ETAG is meant to catch the case where
+ // the client has cached the manifest from a first push, and
+ // wants to validate that it is still valid at the start of a
+ // subsequent push (e.g. make sure the device hasn't changed).
+ if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+ return resp.sendJsonResponse(409, getAssetManifestJson(app));
+ }
+ startUpdateProgress(app, req);
var tmpUrl = ResourcesLoader.createTmpFileUrl();
return pipeRequestToFile(req, tmpUrl)
.then(function() {
return importFile(tmpUrl, path, app, etag);
})
.then(function() {
- // TODO: Add a timeout that resets updatingStatus if no more requests come in.
- app.updateBytesSoFar += +req.headers['content-length'];
- app.updatingStatus = app.updateBytesTotal / app.updateBytesSoFar;
- if (app.updatingStatus === 1) {
- app.updatingStatus = null;
- app.lastUpdated = new Date();
- notifier.success('Update complete.');
- }
- return resp.sendTextResponse(200, '');
+ return incrementUpdateStatusAndSendManifest(app, req, resp);
});
});
}
+ // This is set at the beginning of a push to show progress bar
+ // across multiple requests.
+ function startUpdateProgress(app, req) {
+ // This is passed for the first file only, and is used to track total progress.
+ var expectTotal = +req.getQueryParam('expectBytes') || req.headers['content-length'];
+ app.updatingStatus = 0;
+ app.updateBytesTotal = expectTotal;
+ app.updateBytesSoFar = 0;
+ }
+
+ function incrementUpdateStatusAndSendManifest(app, req, resp) {
+ if (app.updatingStatus !== null) {
+ // TODO: Add a timeout that resets updatingStatus if no more requests come in.
+ app.updateBytesSoFar += +req.headers['content-length'];
+ app.updatingStatus = app.updateBytesTotal / app.updateBytesSoFar;
+ if (app.updatingStatus === 1) {
+ app.updatingStatus = null;
+ app.lastUpdated = new Date();
+ notifier.success('Update complete.');
+ }
+ }
+ return resp.sendJsonResponse(200, {
+ 'assetManifestEtag': app.directoryManager.getAssetManifestEtag()
+ });
+ }
+
function importFile(fileUrl, destPath, app, etag) {
console.log('Adding file: ' + destPath);
if (destPath == 'www/cordova_plugins.js') {
@@ -229,8 +248,13 @@
function handleZipPush(req, resp) {
var appId = req.getQueryParam('appId');
var appType = req.getQueryParam('appType') || 'cordova';
+ var manifestEtag = req.getQueryParam('manifestEtag');
return AppsService.getAppById(appId, appType)
.then(function(app) {
+ if (manifestEtag && app.directoryManager.getAssetManifestEtag() !== manifestEtag) {
+ return resp.sendJsonResponse(409, getAssetManifestJson(app));
+ }
+ startUpdateProgress(app, req);
var tmpZipUrl = ResourcesLoader.createTmpFileUrl();
var tmpDirUrl = ResourcesLoader.createTmpFileUrl() + '/';
return pipeRequestToFile(req, tmpZipUrl)
@@ -253,9 +277,7 @@
});
})
.then(function() {
- app.lastUpdated = new Date();
- notifier.success('Update complete.');
- return resp.sendTextResponse(200, '');
+ return incrementUpdateStatusAndSendManifest(app, req, resp);
})
.finally(function() {
app.updatingStatus = null;
@@ -266,14 +288,18 @@
}
function handleInfo(req, resp) {
+ var activeApp = AppsService.getActiveApp();
var json = {
'platform': cordova.platformId,
'cordovaVer': cordova.version,
'protocolVer': PROTOCOL_VER,
+ 'harnessVer': APP_VERSION,
+ 'supportedAppTypes': ['cordova'],
'userAgent': navigator.userAgent,
+ 'activeAppId': activeApp && activeApp.appId,
'appList': AppsService.getAppListAsJson()
};
- resp.sendJsonResponse(json);
+ resp.sendJsonResponse(200, json);
}
function start() {
@@ -286,7 +312,6 @@
.addRoute('/launch', ensureMethodDecorator('POST', handleLaunch))
.addRoute('/info', ensureMethodDecorator('GET', handleInfo))
.addRoute('/assetmanifest', ensureMethodDecorator('GET', handleAssetManifest))
- .addRoute('/prepupdate', ensureMethodDecorator('POST', handlePrepUpdate))
.addRoute('/deletefiles', ensureMethodDecorator('POST', handleDeleteFiles))
.addRoute('/putfile', ensureMethodDecorator('PUT', handlePutFile))
.addRoute('/zippush', ensureMethodDecorator('POST', handleZipPush))
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/HttpServer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/HttpServer.js b/www/cdvah/js/HttpServer.js
index bd4ccb4..028c92e 100644
--- a/www/cdvah/js/HttpServer.js
+++ b/www/cdvah/js/HttpServer.js
@@ -31,6 +31,14 @@
var STATE_RESPONSE_WAITING_FOR_FLUSH = 5;
var STATE_COMPLETE = 6;
+ function changeState(requestData, newState) {
+ if (newState <= requestData.state) {
+ throw new Error('Socket ' + requestData.socket.socketId + ' state error: ' + requestData.state + '->' + newState);
+ }
+ console.log('Socket ' + requestData.socket.socketId + ' state ' + requestData.state + '->' + newState);
+ requestData.state = newState;
+ }
+
function HttpRequest(requestData) {
this._requestData = requestData;
this.method = requestData.method;
@@ -41,7 +49,7 @@
this.bytesRemaining = parseInt(requestData.headers['content-length'] || '0');
}
if (this.bytesRemaining === 0) {
- this._requestData.state = STATE_REQUEST_RECEIVED;
+ changeState(this._requestData, STATE_REQUEST_RECEIVED);
}
var host = this.headers['host'] || 'localhost';
@@ -110,7 +118,7 @@
throw new Error('Bytes remaining negative: ' + self.bytesRemaining);
}
if (self.bytesRemaining === 0 && self._requestData.state === STATE_HEADERS_RECEIVED) {
- self._requestData.state = STATE_REQUEST_RECEIVED;
+ changeState(self._requestData, STATE_REQUEST_RECEIVED);
}
return chunk;
});
@@ -138,7 +146,7 @@
return this.close();
};
- HttpResponse.prototype.sendJsonResponse = function(json) {
+ HttpResponse.prototype.sendJsonResponse = function(status, json) {
return this.sendTextResponse(200, JSON.stringify(json, null, 4), 'application/json');
};
@@ -148,8 +156,11 @@
}
var promise = this._requestData.socket.write(arrayBuffer);
if (!arrayBuffer) {
- this._requestData.state = STATE_RESPONSE_WAITING_FOR_FLUSH;
- promise = promise.then(this._finish.bind(this, true));
+ changeState(this._requestData, STATE_RESPONSE_WAITING_FOR_FLUSH);
+ var self = this;
+ promise = promise.then(function() {
+ self._finish();
+ });
}
return promise;
};
@@ -167,7 +178,7 @@
this._requestData.socket.close(new Error('Started to write response before request data was finished.'));
return;
}
- this._requestData.state = STATE_RESPONSE_STARTED;
+ changeState(this._requestData, STATE_RESPONSE_STARTED);
var statusMsg = status === 404 ? 'Not Found' :
status === 400 ? 'Bad Request' :
status === 200 ? 'OK' :
@@ -184,9 +195,9 @@
if (this._requestData.state === STATE_COMPLETE) {
return;
}
- this._requestData.state = STATE_COMPLETE;
+ changeState(this._requestData, STATE_COMPLETE);
this._requestData.socket.onClose = null;
- var socketId = this._requestData.socketId;
+ var socketId = this._requestData.socket.socketId;
if (typeof disconnect == 'undefined') {
disconnect = (this.headers['Connection'] || '').toLowerCase() != 'keep-alive';
}
@@ -432,7 +443,7 @@
requestData.resource = requestDataParts[1];
requestData.httpVersion = requestDataParts[2];
console.log(requestData.method + ' requestData received for ' + requestData.resource);
- requestData.state = STATE_REQUEST_DATA_RECEIVED;
+ changeState(requestData, STATE_REQUEST_DATA_RECEIVED);
requestData.socket.unread(arrayBuffer);
return readRequestHeaders(requestData);
}
@@ -442,7 +453,7 @@
requestData.headers = parseHeaders(requestData.dataAsStr.substring(0, splitPoint));
requestData.dataAsStr = '';
arrayBuffer = arrayBuffer.slice(splitPoint + 4 - oldLen);
- requestData.state = STATE_HEADERS_RECEIVED;
+ changeState(requestData, STATE_HEADERS_RECEIVED);
requestData.socket.unread(arrayBuffer);
return requestData;
}
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/Installer.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/Installer.js b/www/cdvah/js/Installer.js
index 69bccf6..49a6d92 100644
--- a/www/cdvah/js/Installer.js
+++ b/www/cdvah/js/Installer.js
@@ -31,15 +31,15 @@
ret.updatingStatus = null;
ret.lastUpdated = null;
// Asset manifest is a cache of what files have been downloaded along with their etags.
- ret.directoryManager = new DirectoryManager(installPath);
- ret.directoryManager.onFileAdded = this.onFileAdded.bind(this);
ret.appId = null; // Read from config.xml
ret.appName = null; // Read from config.xml
ret.startPage = null; // Read from config.xml
ret.plugins = {}; // Read from orig-cordova_plugins.js
ret.appId = appId;
- return ret.directoryManager.getAssetManifest()
+ ret.directoryManager = new DirectoryManager();
+ return ret.directoryManager.init(installPath)
.then(function() {
+ ret.directoryManager.onFileAdded = ret.onFileAdded.bind(ret);
return ret;
});
};
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/ListCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/ListCtrl.js b/www/cdvah/js/ListCtrl.js
index 383762b..1dc38cc 100644
--- a/www/cdvah/js/ListCtrl.js
+++ b/www/cdvah/js/ListCtrl.js
@@ -19,9 +19,10 @@
(function(){
'use strict';
/* global myApp */
- myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer) {
+ myApp.controller('ListCtrl', ['$location', 'notifier', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', 'APP_NAME', 'APP_VERSION', function ($location, notifier, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer, APP_NAME, APP_VERSION) {
$scope.appList = [];
- $rootScope.appTitle = document.title;
+ $rootScope.appTitle = APP_NAME + ' v' + APP_VERSION;
+ document.title = APP_NAME + ' v' + APP_VERSION;
function initialise() {
$scope.$on('$destroy', function() {
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/79b69286/www/cdvah/js/app.js
----------------------------------------------------------------------
diff --git a/www/cdvah/js/app.js b/www/cdvah/js/app.js
index e324ab1..5cb924e 100644
--- a/www/cdvah/js/app.js
+++ b/www/cdvah/js/app.js
@@ -19,6 +19,8 @@
var myApp = angular.module('CordovaAppHarness', ['ngRoute']);
+myApp.value('APP_NAME', 'Cordova App Harness');
+myApp.value('APP_VERSION', '1.0');
myApp.config(['$routeProvider', function($routeProvider){
$routeProvider.when('/', {
[2/3] git commit: Split harness-push into harness-push +
harness-client.
Posted by ag...@apache.org.
Split harness-push into harness-push + harness-client.
Also adds the ability to cache asset manifest between pushes.
Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/e82ef5c0
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/e82ef5c0
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/e82ef5c0
Branch: refs/heads/master
Commit: e82ef5c0f58cd1c1621d200154192cff8e310c8c
Parents: 79b6928
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:04:25 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:04:25 2014 -0400
----------------------------------------------------------------------
harness-push/README.md | 4 +-
harness-push/harness-push.js | 347 +------------------
.../cordova-harness-client/README.md | 5 +
.../cordova-harness-client/harnessclient.js | 196 +++++++++++
.../cordova-harness-client/package.json | 26 ++
.../cordova-harness-client/pushsession.js | 271 +++++++++++++++
harness-push/package.json | 14 +-
7 files changed, 516 insertions(+), 347 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/README.md
----------------------------------------------------------------------
diff --git a/harness-push/README.md b/harness-push/README.md
index 0898123..8b1c9e6 100644
--- a/harness-push/README.md
+++ b/harness-push/README.md
@@ -1,6 +1,6 @@
-# harness-push
+# cordova-harness-push
-A node module & command-line tool for controlling the Cordova App Harness.
+Command-line tool for controlling the Cordova App Harness.
# Usage:
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/harness-push.js
----------------------------------------------------------------------
diff --git a/harness-push/harness-push.js b/harness-push/harness-push.js
index 18a104c..698036b 100755
--- a/harness-push/harness-push.js
+++ b/harness-push/harness-push.js
@@ -18,332 +18,8 @@
under the License.
*/
-var fs = require('fs'),
- path = require('path'),
- crypto = require('crypto'),
- url = require('url'),
- Q = require('q'),
- request = require('request'),
- nopt = require('nopt'),
- shelljs = require('shelljs'),
- JSZip = require('jszip');
-
-var IGNORE_PATH_FOR_PUSH_REGEXP = /www\/(?:plugins\/|cordova\.js)/;
-
-function getDerivedWwwDir(dir, platformId) {
- if (platformId == 'android') {
- return path.join(dir, 'platforms', platformId, 'assets', 'www');
- } else if (platformId == 'ios') {
- return path.join(dir, 'platforms', platformId, 'www');
- }
- throw new Error('Platform not supported: ' + platformId);
-}
-
-function getDerivedConfigXmlPath(dir, platformId) {
- if (platformId == 'android') {
- return path.join(dir, 'platforms', platformId, 'res', 'xml', 'config.xml');
- } else if (platformId == 'ios') {
- var base = path.join(dir, 'platforms', platformId);
- var ret = null;
- fs.readdirSync(base).forEach(function(a) {
- if (a != 'www' && a != 'cordova' && a != 'CordovaLib') {
- var fullPath = path.join(base, a, 'config.xml');
- if (fs.existsSync(fullPath)) {
- ret = fullPath;
- }
- }
- });
- return ret;
- }
- throw new Error('Platform not supported: ' + platformId);
-}
-
-function discoverAppId(dir) {
- var configXmlPath = path.join(dir, 'config.xml');
- if (!fs.existsSync(configXmlPath)) {
- configXmlPath = path.join(dir, 'www', 'config.xml');
- }
- if (!fs.existsSync(configXmlPath)) {
- throw new Error('Not a Cordova project: ' + dir);
- }
-
- var configData = fs.readFileSync(configXmlPath, 'utf8');
- var m = /\bid="(.*?)"/.exec(configData);
- var appId = m && m[1];
- if (!appId) {
- throw new Error('Could not find app ID within: ' + configXmlPath);
- }
- return appId;
-}
-
-exports.push = function(target, dir, pretend) {
- var appId = discoverAppId(dir);
- var appType = 'cordova';
-
- return exports.assetmanifest(target, appId)
- .then(function(result) {
- var derivedWwwDir = getDerivedWwwDir(dir, result.body['platform']);
- var derivedConfigXmlPath = getDerivedConfigXmlPath(dir, result.body['platform']);
- var cordovaPluginsPath = path.join(derivedWwwDir, 'cordova_plugins.js');
- if (!fs.existsSync(cordovaPluginsPath)) {
- throw new Error('Could not find: ' + cordovaPluginsPath);
- }
- if (!fs.existsSync(derivedConfigXmlPath)) {
- throw new Error('Could not find: ' + derivedConfigXmlPath);
- }
-
- var existingAssetManifest = result.body['assetManifest'];
- if (existingAssetManifest) {
- return doFileSync(target, appId, appType, derivedWwwDir, derivedConfigXmlPath, existingAssetManifest, pretend);
- }
- // TODO: It might be faster to use Zip even when some files exist.
- return doZipPush(target, appId, appType, derivedWwwDir, derivedConfigXmlPath);
- });
-}
-
-function calculateMd5(fileName) {
- var BUF_LENGTH = 64*1024;
- var buf = new Buffer(BUF_LENGTH);
- var bytesRead = BUF_LENGTH;
- var pos = 0;
- var fdr = fs.openSync(fileName, 'r');
-
- try {
- var md5sum = crypto.createHash('md5');
- while (bytesRead === BUF_LENGTH) {
- bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
- pos += bytesRead;
- md5sum.update(buf.slice(0, bytesRead));
- }
- } finally {
- fs.closeSync(fdr);
- }
- return md5sum.digest('hex');
-}
-
-function buildAssetManifest(dir, configXmlPath) {
- var fileList = shelljs.find(dir).filter(function(a) {
- return !IGNORE_PATH_FOR_PUSH_REGEXP.exec(a) && !fs.statSync(a).isDirectory();
- });
-
- var ret = Object.create(null);
- for (var i = 0; i < fileList.length; ++i) {
- // TODO: convert windows slash to unix slash here.
- var appPath = 'www/' + fileList[i].slice(dir == '.' ? 0 : dir.length + 1);
- ret[appPath] = {
- path: appPath,
- realPath: fileList[i],
- etag: calculateMd5(fileList[i]),
- };
- }
- ret['config.xml'] = {
- path: 'config.xml',
- realPath: configXmlPath,
- etag: calculateMd5(configXmlPath)
- };
- return ret;
-}
-
-function buildDeleteList(existingAssetManifest, newAssetManifest) {
- var toDelete = [];
- for (var k in existingAssetManifest) {
- // Don't delete top-level files ever.
- if (k.slice(0, 4) != 'www/') {
- continue;
- }
- if (!newAssetManifest[k]) {
- toDelete.push(k);
- }
- }
- return toDelete;
-}
-
-function buildPushList(existingAssetManifest, newAssetManifest) {
- var ret = [];
- for (var k in newAssetManifest) {
- var entry = newAssetManifest[k];
- if (entry.etag != existingAssetManifest[k]) {
- if (entry.path == 'config.xml') {
- ret.unshift(entry);
- } else {
- ret.push(entry);
- }
- }
- }
- return ret;
-}
-
-function calculatePushBytes(pushList) {
- var ret = 0;
- for (var i = 0; i < pushList.length; ++i) {
- ret += fs.statSync(pushList[i].realPath).size;
- }
- return ret;
-}
-
-function doFileSync(target, appId, appType, dir, configXmlPath, existingAssetManifest, pretend) {
- var newAssetManifest = buildAssetManifest(dir, configXmlPath);
- var deleteList = buildDeleteList(existingAssetManifest, newAssetManifest);
- var pushList = buildPushList(existingAssetManifest, newAssetManifest);
- var totalPushBytes = calculatePushBytes(pushList);
- if (pretend) {
- console.log('AppId=' + appId);
- console.log('Would delete: ' + JSON.stringify(deleteList));
- console.log('Would upload: ' + JSON.stringify(pushList));
- console.log('Upload bytes: ' + totalPushBytes);
- } else if (deleteList.length === 0 && pushList.length === 0) {
- console.log('Application already up-to-date.');
- } else {
- return doRequest('POST', target, 'prepupdate', { appId: appId, appType: appType, json: {'transferSize': totalPushBytes}})
- .then(function() {
- if (deleteList.length > 0) {
- return doRequest('POST', target, 'deletefiles', { appId: appId, appType: appType, json: {'paths': deleteList}})
- }
- })
- .then(function pushNextFile() {
- if (pushList.length === 0) {
- console.log('Push complete.');
- return;
- }
- var curPushEntry = pushList.shift();
- return doRequest('PUT', target, '/putfile', {
- appId: appId,
- appType: appType,
- body: fs.readFileSync(curPushEntry.realPath),
- query: {
- path: curPushEntry.path,
- etag: curPushEntry.etag
- }
- }).then(pushNextFile);
- });
- }
-}
-
-function zipDir(dir, zip) {
- var contents = fs.readdirSync(dir);
- contents.forEach(function(f) {
- var fullPath = path.join(dir, f);
- if (!IGNORE_PATH_FOR_PUSH_REGEXP.exec(fullPath)) {
- if (fs.statSync(fullPath).isDirectory()) {
- var inner = zip.folder(f);
- zipDir(path.join(dir, f), inner);
- } else {
- zip.file(f, fs.readFileSync(fullPath, 'binary'), { binary: true });
- }
- }
- });
-}
-
-function doZipPush(target, appId, appType, dir, configXmlPath) {
- var zip = new JSZip();
- zipDir(dir, zip.folder('www'));
- zip.file('config.xml', fs.readFileSync(configXmlPath, 'binary'), { binary: true });
- var newAssetManifest = buildAssetManifest(dir, configXmlPath);
- zip.file('zipassetmanifest.json', JSON.stringify(newAssetManifest));
- var zipData = new Buffer(zip.generate({ type: 'base64' }), 'base64');
- return doRequest('POST', target, '/zippush', {appId:appId, appType: appType, body: zipData});
-}
-
-function doRequest(method, target, action, options) {
- var ret = Q.defer();
- var targetParts = target.split(':');
- var host = targetParts[0];
- var port = +(targetParts[1] || 2424);
- options = options || {};
-
- var queryParams = {};
- if (options.query) {
- Object.keys(options.query).forEach(function(k) {
- queryParams[k] = options.query[k];
- });
- }
- if (options.appId) {
- queryParams['appId'] = options.appId;
- }
- if (options.appType) {
- queryParams['appType'] = options.appType;
- }
-
- // Send the HTTP request. crxContents is a Node Buffer, which is the payload.
- // Prepare the form data for upload.
- var uri = url.format({
- protocol: 'http',
- hostname: host,
- port: port,
- pathname: action,
- query: queryParams
- });
-
- process.stdout.write(method + ' ' + uri);
-
- var headers = {};
- if (options.json) {
- options.body = JSON.stringify(options.json);
- headers['Content-Type'] = 'application/json';
- }
-
- function statusCheck(err, res, body) {
- if (err) {
- err.statusCode = res && res.statusCode;
- } else if (res) {
- process.stdout.write(' ==> ' + (res && res.statusCode) + '\n');
- if (res.statusCode != 200) {
- err = new Error('Server returned status code: ' + res.statusCode);
- } else if (options.expectJson) {
- try {
- body = JSON.parse(body);
- } catch (e) {
- err = new Error('Invalid JSON: ' + body.slice(500));
- }
- }
- }
- if (err) {
- ret.reject(err);
- } else {
- ret.resolve({res:res, body:body});
- }
- }
- var req = request({
- uri: uri,
- headers: headers,
- method: method,
- body: options.body
- }, statusCheck);
-
- if (options.form) {
- var f = options.form;
- req.form().append(f.key, f.formBody, { filename: f.filename, contentType: f.contentType });
- }
- return ret.promise;
-};
-
-exports.info = function(target) {
- return doRequest('GET', target, '/info', { expectJson: true });
-};
-
-exports.assetmanifest = function(target, appId) {
- return doRequest('GET', target, '/assetmanifest', {expectJson: true, appId: appId});
-};
-
-exports.menu = function(target) {
- return doRequest('POST', target, '/menu');
-};
-
-exports.eval = function(target, someJs) {
- return doRequest('POST', target, '/exec', { query: {code: someJs} });
-};
-
-exports.launch = function(target, appId) {
- return doRequest('POST', target, '/launch', { appId: appId});
-};
-
-exports.deleteAllApps = function(target) {
- return doRequest('POST', target, '/deleteapp', { query: {'all': 1} });
-};
-
-exports.deleteApp = function(target, appId) {
- return doRequest('POST', target, '/deleteApp', { appId: appId});
-};
+var nopt = require('nopt');
+var HarnessClient = require('cordova-harness-client');
function parseArgs(argv) {
var opts = {
@@ -389,32 +65,35 @@ function main() {
}
}
+ var client = new HarnessClient(args.target);
+
var cmd = args.argv.remain[0];
if (cmd == 'push') {
if (!args.argv.remain[1]) {
usage();
}
- exports.push(args.target, args.argv.remain[1], args.pretend).then(onSuccess, onFailure);
+ var pushSession = client.createPushSession(args.argv.remain[1]);
+ pushSession.push().then(onSuccess, onFailure);
} else if (cmd == 'deleteall') {
- exports.deleteAllApps(args.target);
+ client.deleteAllApps();
} else if (cmd == 'delete') {
if (!args.argv.remain[1]) {
usage();
}
- exports.deleteApp(args.target).then(onSuccess, onFailure);
+ client.deleteApp().then(onSuccess, onFailure);
} else if (cmd == 'menu') {
- exports.menu(args.target).then(onSuccess, onFailure);
+ client.menu().then(onSuccess, onFailure);
} else if (cmd == 'eval') {
if (!args.argv.remain[1]) {
usage();
}
- exports.eval(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+ client.evalJs(args.argv.remain[1]).then(onSuccess, onFailure);
} else if (cmd == 'assetmanifest') {
- exports.assetmanifest(args.target, args.appid).then(onSuccess, onFailure);
+ client.assetmanifest(args.appid).then(onSuccess, onFailure);
} else if (cmd == 'info') {
- exports.info(args.target).then(onSuccess, onFailure);
+ client.info().then(onSuccess, onFailure);
} else if (cmd == 'launch') {
- exports.launch(args.target, args.argv.remain[1]).then(onSuccess, onFailure);
+ client.launch(args.argv.remain[1]).then(onSuccess, onFailure);
} else {
usage();
}
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/README.md
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/README.md b/harness-push/node_modules/cordova-harness-client/README.md
new file mode 100644
index 0000000..357c05c
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/README.md
@@ -0,0 +1,5 @@
+# cordova-harness-client
+
+A node module for controlling the Cordova App Harness.
+
+For example usage of the libary, see `cordova-harness-push`.
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/harnessclient.js
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/harnessclient.js b/harness-push/node_modules/cordova-harness-client/harnessclient.js
new file mode 100644
index 0000000..903aafb
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/harnessclient.js
@@ -0,0 +1,196 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+var http = require('http');
+var url = require('url');
+var Q = require('q');
+var KeepAliveAgent = require('keep-alive-agent');
+var PushSession = require('./pushsession');
+
+var httpAgent = new KeepAliveAgent();
+
+function doRequest(method, target, action, options) {
+ var deferred = Q.defer();
+ var targetParts = target.split(':');
+ var host = targetParts[0];
+ var port = +(targetParts[1] || 2424);
+ options = options || {};
+
+ var queryParams = {};
+ if (options.query) {
+ Object.keys(options.query).forEach(function(k) {
+ queryParams[k] = options.query[k];
+ });
+ }
+ if (options.appId) {
+ queryParams['appId'] = options.appId;
+ }
+ if (options.appType) {
+ queryParams['appType'] = options.appType;
+ }
+
+ var uri = url.format({
+ protocol: 'http',
+ hostname: host,
+ port: port,
+ pathname: action,
+ query: queryParams
+ });
+ var pathWithQuery = uri.replace(/^.*?\/\/.*?\//, '/');
+ process.stdout.write(method + ' ' + uri);
+
+ var headers = {
+ 'Connection': 'keep-alive'
+ };
+ var body = null;
+ if (method == 'POST' || method == 'PUT') {
+ body = options.body || '';
+ if (options.json) {
+ body = JSON.stringify(options.json);
+ headers['Content-Type'] = 'application/json';
+ }
+ headers['Content-Length'] = body.length;
+ }
+
+ var startTime = new Date();
+ var req = http.request({
+ hostname: host,
+ port: port,
+ method: method,
+ path: pathWithQuery,
+ headers: headers,
+ agent: httpAgent
+ });
+
+ req.on('error', function(e) {
+ deferred.reject(e);
+ });
+
+ if (body) {
+ req.write(body);
+ }
+ req.end();
+
+ req.on('response', function(res) {
+ process.stdout.write(' ==> ' + res.statusCode);
+ if (res.statusCode != 200) {
+ deferred.reject(new Error('Server returned status code: ' + res.statusCode));
+ res.destroy();
+ return;
+ }
+ res.setEncoding('utf8');
+ var body = '';
+ res.on('data', function(chunk) {
+ body += chunk;
+ });
+ res.on('end', function() {
+ process.stdout.write(' (' + (new Date() - startTime) + ')\n');
+ try {
+ body = options.expectJson ? JSON.parse(body) : body;
+ } catch (e) {
+ deferred.reject(new Error('Invalid JSON: ' + body.slice(500)));
+ return;
+ }
+ deferred.resolve({res:res, body:body});
+ });
+ });
+ return deferred.promise;
+}
+
+function HarnessClient(target) {
+ this.target = target || '127.0.0.1';
+}
+
+HarnessClient.prototype.info = function() {
+ return doRequest('GET', this.target, '/info', { expectJson: true });
+};
+
+HarnessClient.prototype.assetmanifest = function(/* optional */ appId) {
+ return doRequest('GET', this.target, '/assetmanifest', {expectJson: true, appId: appId});
+};
+
+HarnessClient.prototype.menu = function() {
+ return doRequest('POST', this.target, '/menu');
+};
+
+HarnessClient.prototype.evalJs = function(someJs) {
+ return doRequest('POST', this.target, '/exec', { query: {code: someJs} });
+};
+
+HarnessClient.prototype.launch = function(/* optional */ appId) {
+ return doRequest('POST', this.target, '/launch', { appId: appId});
+};
+
+HarnessClient.prototype.deleteAllApps = function() {
+ return doRequest('POST', this.target, '/deleteapp', { query: {'all': 1} });
+};
+
+HarnessClient.prototype.deleteApp = function(/* optional */ appId) {
+ return doRequest('POST', this.target, '/deleteapp', { appId: appId});
+};
+
+HarnessClient.prototype.pushZip = function(appId, appType, zipData, /* optional */ totalPushBytes, /* optional */ manifestEtag) {
+ var query = {};
+ if (totalPushBytes) {
+ query['expectBytes'] = totalPushBytes;
+ }
+ if (manifestEtag) {
+ query['manifestEtag'] = manifestEtag;
+ }
+ return doRequest('POST', this.target, '/zippush', {
+ appId: appId,
+ appType: appType,
+ body: zipData,
+ query: query
+ });
+};
+
+HarnessClient.prototype.pushFile = function(appId, appType, payload, etag, remotePath, /* optional */ totalPushBytes, /* optional */ manifestEtag) {
+ var query = {
+ 'path': remotePath,
+ 'etag': etag
+ };
+ if (totalPushBytes) {
+ query['expectBytes'] = totalPushBytes;
+ }
+ if (manifestEtag) {
+ query['manifestEtag'] = manifestEtag;
+ }
+ return doRequest('PUT', this.target, '/putfile', {
+ appId: appId,
+ appType: appType,
+ body: payload,
+ query: query
+ });
+};
+
+HarnessClient.prototype.deleteFiles = function(appId, deleteList, /* optional */ manifestEtag) {
+ var query = {};
+ if (manifestEtag) {
+ query['manifestEtag'] = manifestEtag;
+ }
+ return doRequest('POST', this.target, 'deletefiles', { appId: appId, json: {'paths': deleteList}, query: query});
+};
+
+HarnessClient.prototype.createPushSession = function(dir) {
+ return new PushSession(this, dir);
+};
+
+module.exports = HarnessClient;
+
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/package.json
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/package.json b/harness-push/node_modules/cordova-harness-client/package.json
new file mode 100644
index 0000000..a44b72c
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "cordova-harness-client",
+ "version": "0.0.1",
+ "description": "Client library for communicating with Cordova App Harness.",
+ "main": "harnessclient.js",
+ "author": "The Apache Cordova Team",
+ "license": "Apache 2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://git-wip-us.apache.org/repos/asf/cordova-app-harness.git"
+ },
+ "bugs": {
+ "url": "https://issues.apache.org/jira/browse/CB",
+ "email": "dev@cordova.apache.org"
+ },
+ "keywords": [
+ "cordova",
+ "cordova-app-harness"
+ ],
+ "dependencies": {
+ "jszip": "~2.1",
+ "keep-alive-agent": "0.0.1",
+ "q": "~0.9",
+ "shelljs": "0.1.x"
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/node_modules/cordova-harness-client/pushsession.js
----------------------------------------------------------------------
diff --git a/harness-push/node_modules/cordova-harness-client/pushsession.js b/harness-push/node_modules/cordova-harness-client/pushsession.js
new file mode 100644
index 0000000..546978b
--- /dev/null
+++ b/harness-push/node_modules/cordova-harness-client/pushsession.js
@@ -0,0 +1,271 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+var fs = require('fs'),
+ path = require('path'),
+ crypto = require('crypto'),
+ Q = require('q'),
+ shelljs = require('shelljs'),
+ JSZip = require('jszip');
+
+var IGNORE_PATH_FOR_PUSH_REGEXP = /www\/(?:plugins\/|cordova\.js)/;
+
+function getDerivedWwwDir(dir, platformId) {
+ if (platformId == 'android') {
+ return path.join(dir, 'platforms', platformId, 'assets', 'www');
+ } else if (platformId == 'ios') {
+ return path.join(dir, 'platforms', platformId, 'www');
+ }
+ throw new Error('Platform not supported: ' + platformId);
+}
+
+function getDerivedConfigXmlPath(dir, platformId) {
+ if (platformId == 'android') {
+ return path.join(dir, 'platforms', platformId, 'res', 'xml', 'config.xml');
+ } else if (platformId == 'ios') {
+ var base = path.join(dir, 'platforms', platformId);
+ var ret = null;
+ if (fs.existsSync(base)) {
+ fs.readdirSync(base).forEach(function(a) {
+ if (a != 'www' && a != 'cordova' && a != 'CordovaLib') {
+ var fullPath = path.join(base, a, 'config.xml');
+ if (fs.existsSync(fullPath)) {
+ ret = fullPath;
+ }
+ }
+ });
+ }
+ return ret;
+ }
+ throw new Error('Platform not supported: ' + platformId);
+}
+
+function discoverAppId(dir) {
+ var configXmlPath = path.join(dir, 'config.xml');
+ if (!fs.existsSync(configXmlPath)) {
+ configXmlPath = path.join(dir, 'www', 'config.xml');
+ }
+ if (!fs.existsSync(configXmlPath)) {
+ throw new Error('Not a Cordova project: ' + dir);
+ }
+
+ var configData = fs.readFileSync(configXmlPath, 'utf8');
+ var m = /\bid="(.*?)"/.exec(configData);
+ var appId = m && m[1];
+ if (!appId) {
+ throw new Error('Could not find app ID within: ' + configXmlPath);
+ }
+ return appId;
+}
+
+function calculateMd5(fileName) {
+ var BUF_LENGTH = 64*1024;
+ var buf = new Buffer(BUF_LENGTH);
+ var bytesRead = BUF_LENGTH;
+ var pos = 0;
+ var fdr = fs.openSync(fileName, 'r');
+
+ var md5sum = crypto.createHash('md5');
+ try {
+ while (bytesRead === BUF_LENGTH) {
+ bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos);
+ pos += bytesRead;
+ md5sum.update(buf.slice(0, bytesRead));
+ }
+ } finally {
+ fs.closeSync(fdr);
+ }
+ return md5sum.digest('hex');
+}
+
+function buildAssetManifest(dir, configXmlPath) {
+ var fileList = shelljs.find(dir).filter(function(a) {
+ return !IGNORE_PATH_FOR_PUSH_REGEXP.exec(a) && !fs.statSync(a).isDirectory();
+ });
+
+ var ret = Object.create(null);
+ for (var i = 0; i < fileList.length; ++i) {
+ // TODO: convert windows slash to unix slash here.
+ var appPath = 'www/' + fileList[i].slice(dir == '.' ? 0 : dir.length + 1);
+ ret[appPath] = {
+ path: appPath,
+ realPath: fileList[i],
+ etag: calculateMd5(fileList[i]),
+ };
+ }
+ ret['config.xml'] = {
+ path: 'config.xml',
+ realPath: configXmlPath,
+ etag: calculateMd5(configXmlPath)
+ };
+ return ret;
+}
+
+function buildDeleteList(existingAssetManifest, newAssetManifest) {
+ var toDelete = [];
+ for (var k in existingAssetManifest) {
+ // Don't delete top-level files ever.
+ if (k.slice(0, 4) != 'www/') {
+ continue;
+ }
+ if (!newAssetManifest[k]) {
+ toDelete.push(k);
+ }
+ }
+ return toDelete;
+}
+
+function buildPushList(existingAssetManifest, newAssetManifest) {
+ var ret = [];
+ for (var k in newAssetManifest) {
+ var entry = newAssetManifest[k];
+ if (entry.etag != existingAssetManifest[k]) {
+ if (entry.path == 'config.xml') {
+ ret.unshift(entry);
+ } else {
+ ret.push(entry);
+ }
+ }
+ }
+ return ret;
+}
+
+function calculatePushBytes(pushList) {
+ var ret = 0;
+ for (var i = 0; i < pushList.length; ++i) {
+ ret += fs.statSync(pushList[i].realPath).size;
+ }
+ return ret;
+}
+
+function zipDir(dir, zip) {
+ var contents = fs.readdirSync(dir);
+ contents.forEach(function(f) {
+ var fullPath = path.join(dir, f);
+ if (!IGNORE_PATH_FOR_PUSH_REGEXP.exec(fullPath)) {
+ if (fs.statSync(fullPath).isDirectory()) {
+ var inner = zip.folder(f);
+ zipDir(path.join(dir, f), inner);
+ } else {
+ zip.file(f, fs.readFileSync(fullPath, 'binary'), { binary: true });
+ }
+ }
+ });
+}
+
+function PushSession(harnessClient, dir) {
+ this.launchAfterPush = true;
+ this.appType_ = 'cordova';
+ this.harnessClient_ = harnessClient;
+ this.rootDir_ = dir;
+ this.wwwDir_ = null;
+ this.appId_ = null;
+ this.platformId_ = null;
+ this.assetManifest_ = null;
+ this.assetManifestEtag_ = null;
+ var self = this;
+ this.boundUpdateAssetManifestFn_ = function(result) {
+ self.assetManifestEtag_ = result.body['assetManifestEtag'];
+ };
+}
+
+PushSession.prototype.initialize = function(opts) {
+ opts = opts || {};
+ this.appId_ = opts.appId || discoverAppId(this.rootDir_);
+ var self = this;
+ return this.harnessClient_.assetmanifest(this.appId_)
+ .then(function(result) {
+ self.platformId_ = result.body['platform'];
+ self.assetManifest_ = result.body['assetManifest'];
+ self.assetManifestEtag_ = result.body['assetManifestEtag'];
+ self.wwwDir_ = opts.wwwDir || getDerivedWwwDir(self.rootDir_, self.platformId_);
+ self.configXmlPath_ = opts.configXmlPath || getDerivedConfigXmlPath(self.rootDir_, self.platformId_);
+ self.cordovaPluginsPath_ = !opts.skipCordovaPlugins && path.join(self.wwwDir_, 'cordova_plugins.js');
+ });
+};
+
+PushSession.prototype.push = function() {
+ var self = this;
+ return Q.when(this.platformId_ || this.initialize())
+ .then(function() {
+ if (!fs.existsSync(self.configXmlPath_)) {
+ throw new Error('Could not find: ' + self.configXmlPath_ + ' you probably need to run: cordova platform add ' + self.platformId_);
+ }
+ if (self.cordovaPluginsPath_ && !fs.existsSync(self.cordovaPluginsPath_)) {
+ throw new Error('Could not find: ' + self.cordovaPluginsPath_);
+ }
+ if (self.assetManifest_) {
+ return self.doFileSync_();
+ }
+ // TODO: It might be faster to use Zip even when some files exist.
+ return self.doZipPush_();
+ }).then(function() {
+ if (self.launchAfterPush) {
+ return self.harnessClient_.launch(self.appId_);
+ }
+ }, function(e) {
+ self.assetManifest_ = null;
+ throw e;
+ });
+};
+
+PushSession.prototype.doFileSync_ = function() {
+ var a = new Date();
+ var newAssetManifest = buildAssetManifest(this.wwwDir_, this.configXmlPath_);
+ var deleteList = buildDeleteList(this.assetManifest_, newAssetManifest);
+ var pushList = buildPushList(this.assetManifest_, newAssetManifest);
+ var totalPushBytes = calculatePushBytes(pushList);
+ console.log('Changes calculated (' + (new Date() - a) + ')');
+ if (deleteList.length === 0 && pushList.length === 0) {
+ console.log('Application already up-to-date.');
+ return;
+ }
+ var origPushListLen = pushList.length;
+ var self = this;
+ return Q.when().then(function pushNextFile() {
+ if (pushList.length === 0) {
+ return;
+ }
+ var firstRequest = pushList.length === origPushListLen;
+ var curPushEntry = pushList.shift();
+ var payload = fs.readFileSync(curPushEntry.realPath);
+ // TODO handle 409 responses.
+ return self.harnessClient_.pushFile(self.appId_, self.appType_, payload, curPushEntry.etag, curPushEntry.path, firstRequest && totalPushBytes, firstRequest && self.assetManifestEtag_)
+ .then(self.boundUpdateAssetManifestFn_)
+ .then(pushNextFile);
+ }).then(function() {
+ if (deleteList.length > 0) {
+ return self.harnessClient_.deleteFiles(self.appType_, deleteList, self.assetManifestEtag_)
+ .then(self.boundUpdateAssetManifestFn_);
+ }
+ });
+};
+
+PushSession.prototype.doZipPush_ = function() {
+ var zip = new JSZip();
+ zipDir(this.wwwDir_, zip.folder('www'));
+ zip.file('config.xml', fs.readFileSync(this.configXmlPath_, 'binary'), { binary: true });
+ var newAssetManifest = buildAssetManifest(this.wwwDir_, this.configXmlPath_);
+ zip.file('zipassetmanifest.json', JSON.stringify(newAssetManifest));
+ var zipData = new Buffer(zip.generate({ type: 'base64' }), 'base64');
+ return this.harnessClient_.pushZip(this.appId_, this.appType_, zipData, null, this.assetManifestEtag_)
+ .then(this.boundUpdateAssetManifestFn_);
+};
+
+module.exports = PushSession;
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e82ef5c0/harness-push/package.json
----------------------------------------------------------------------
diff --git a/harness-push/package.json b/harness-push/package.json
index e58b848..e483416 100644
--- a/harness-push/package.json
+++ b/harness-push/package.json
@@ -1,12 +1,8 @@
{
- "name": "harness-push",
+ "name": "cordova-harness-push",
"version": "0.0.1",
"description": "Command-line utility for communicating with Cordova App Harness.",
- "main": "push.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "author": "",
+ "author": "The Apache Cordova Team",
"license": "Apache 2.0",
"repository": {
"type": "git",
@@ -24,10 +20,6 @@
"harness-push": "./harness-push.js"
},
"dependencies": {
- "jszip": "~2.1",
- "nopt": "~2.2",
- "q": "~0.9",
- "request": "~2.34",
- "shelljs": "0.1.x"
+ "cordova-harness-push": "~0.0.x"
}
}
[3/3] git commit: Enable jshint for node modules as well
Posted by ag...@apache.org.
Enable jshint for node modules as well
Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/e18f4bfe
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/e18f4bfe
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/e18f4bfe
Branch: refs/heads/master
Commit: e18f4bfed84b701b54bd43f084c864598d0a26fa
Parents: e82ef5c
Author: Andrew Grieve <ag...@chromium.org>
Authored: Fri May 23 14:05:44 2014 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Fri May 23 14:05:44 2014 -0400
----------------------------------------------------------------------
.gitignore | 4 +++-
.jshintrc | 3 ++-
checkjs.sh | 5 ++++-
3 files changed, 9 insertions(+), 3 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index ea23fd6..8516c5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.DS_Store
-harness-push/node_modules
+harness-push/node_modules/*
+!harness-push/node_modules/cordova-harness-client
+harness-push/node_modules/cordova-harness-client/node_modules
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/.jshintrc
----------------------------------------------------------------------
diff --git a/.jshintrc b/.jshintrc
index 227f5fc..91a9a7d 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -4,7 +4,7 @@
"camelcase": true,
"curly": true,
"eqeqeq": false,
- "forin": true,
+ "forin": false,
"immed": true,
"indent": 4,
"latedef": false,
@@ -24,6 +24,7 @@
"sub":true,
//environment
+ "node": true,
"browser": true,
"devel":true,
http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/e18f4bfe/checkjs.sh
----------------------------------------------------------------------
diff --git a/checkjs.sh b/checkjs.sh
index 8c1b59b..afb7173 100755
--- a/checkjs.sh
+++ b/checkjs.sh
@@ -28,4 +28,7 @@ if ! which jshint; then
fi
echo "Running jsHint"
cd $(dirname "$0")
-jshint www/ --exclude www/cdvah/js/libs --verbose --show-non-errors
+set -x
+jshint www/ --exclude www/cdvah/js/libs --verbose --show-non-errors && \
+jshint harness-push/ --exclude harness-push/node_modules --verbose --show-non-errors && \
+jshint harness-push/node_modules/cordova-harness-client --exclude harness-push/node_modules/cordova-harness-client/node_modules --verbose --show-non-errors