Bug 911785 - Implement a webapps actor front to ease app install r=paul

This commit is contained in:
Alexandre Poirot 2013-09-10 13:24:00 +02:00
parent c92c39403a
commit 5e6b2ae299
5 changed files with 249 additions and 94 deletions

View File

@ -55,6 +55,7 @@ var BuiltinProvider = {
"devtools": "resource:///modules/devtools", "devtools": "resource:///modules/devtools",
"devtools/server": "resource://gre/modules/devtools/server", "devtools/server": "resource://gre/modules/devtools/server",
"devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole", "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
"devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js",
"devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic", "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
"devtools/client": "resource://gre/modules/devtools/client", "devtools/client": "resource://gre/modules/devtools/client",
@ -95,6 +96,7 @@ var SrcdirProvider = {
let toolkitURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools")); let toolkitURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools"));
let serverURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "server")); let serverURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "server"));
let webconsoleURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "webconsole")); let webconsoleURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "webconsole"));
let appActorURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "apps", "app-actor-front.js"));
let cssLogicURI = this.fileURI(OS.Path.join(toolkitURI, "styleinspector", "css-logic")); let cssLogicURI = this.fileURI(OS.Path.join(toolkitURI, "styleinspector", "css-logic"));
let clientURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "client")); let clientURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "client"));
let mainURI = this.fileURI(OS.Path.join(srcdir, "browser", "devtools", "main.js")); let mainURI = this.fileURI(OS.Path.join(srcdir, "browser", "devtools", "main.js"));
@ -107,6 +109,7 @@ var SrcdirProvider = {
"": "resource://gre/modules/commonjs/", "": "resource://gre/modules/commonjs/",
"devtools/server": serverURI, "devtools/server": serverURI,
"devtools/toolkit/webconsole": webconsoleURI, "devtools/toolkit/webconsole": webconsoleURI,
"devtools/app-actor-front": appActorURI,
"devtools/client": clientURI, "devtools/client": clientURI,
"devtools": devtoolsURI, "devtools": devtoolsURI,
"devtools/styleinspector/css-logic": cssLogicURI, "devtools/styleinspector/css-logic": cssLogicURI,

View File

@ -0,0 +1,216 @@
const {Ci, Cc, Cu, Cr} = require("chrome");
Cu.import("resource://gre/modules/osfile.jsm");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const promise = require("sdk/core/promise");
// XXX: bug 912476 make this module a real protocol.js front
// by converting webapps actor to protocol.js
const PR_USEC_PER_MSEC = 1000;
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
const CHUNK_SIZE = 10000;
function addDirToZip(writer, dir, basePath) {
let files = dir.directoryEntries;
while (files.hasMoreElements()) {
let file = files.getNext().QueryInterface(Ci.nsIFile);
if (file.isHidden() ||
file.isSymlink() ||
file.isSpecial() ||
file.equals(writer.file))
{
continue;
}
if (file.isDirectory()) {
writer.addEntryDirectory(basePath + file.leafName + "/",
file.lastModifiedTime * PR_USEC_PER_MSEC,
true);
addDirToZip(writer, file, basePath + file.leafName + "/");
} else {
writer.addEntryFile(basePath + file.leafName,
Ci.nsIZipWriter.COMPRESSION_DEFAULT,
file,
true);
}
}
}
/**
* Convert an XPConnect result code to its name and message.
* We have to extract them from an exception per bug 637307 comment 5.
*/
function getResultTest(code) {
let regexp =
/^\[Exception... "(.*)" nsresult: "0x[0-9a-fA-F]* \((.*)\)" location: ".*" data: .*\]$/;
let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
createInstance(Ci.nsIXPCException);
ex.initialize(null, code, null, null, null, null);
let [, message, name] = regexp.exec(ex.toString());
return { name: name, message: message };
}
function zipDirectory(zipFile, dirToArchive) {
let deferred = promise.defer();
let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
this.addDirToZip(writer, dirToArchive, "");
writer.processQueue({
onStartRequest: function onStartRequest(request, context) {},
onStopRequest: (request, context, status) => {
if (status == Cr.NS_OK) {
writer.close();
deferred.resolve(zipFile);
}
else {
let { name, message } = getResultText(status);
deferred.reject(name + ": " + message);
}
}
}, null);
return deferred.promise;
}
function uploadPackage(client, webappsActor, packageFile) {
let deferred = promise.defer();
let request = {
to: webappsActor,
type: "uploadPackage"
};
client.request(request, (res) => {
openFile(res.actor);
});
function openFile(actor) {
OS.File.open(packageFile.path)
.then(function (file) {
uploadChunk(actor, file);
});
}
function uploadChunk(actor, file) {
file.read(CHUNK_SIZE)
.then(function (bytes) {
// To work around the fact that JSON.stringify translates the typed
// array to object, we are encoding the typed array here into a string
let chunk = String.fromCharCode.apply(null, bytes);
let request = {
to: actor,
type: "chunk",
chunk: chunk
};
client.request(request, (res) => {
if (bytes.length == CHUNK_SIZE) {
uploadChunk(actor, file);
} else {
file.close().then(function () {
endsUpload(actor);
});
}
});
});
}
function endsUpload(actor) {
let request = {
to: actor,
type: "done"
};
client.request(request, (res) => {
deferred.resolve(actor);
});
}
return deferred.promise;
}
function removeServerTemporaryFile(client, fileActor) {
let request = {
to: fileActor,
type: "remove"
};
client.request(request, function (aResponse) {
console.error("Failed removing server side temporary package file", aResponse);
});
}
function installPackaged(client, webappsActor, packagePath, appId) {
let deferred = promise.defer();
let file = FileUtils.File(packagePath);
let packagePromise;
if (file.isDirectory()) {
let tmpZipFile = FileUtils.getDir("TmpD", [], true);
tmpZipFile.append("application.zip");
tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
packagePromise = zipDirectory(tmpZipFile, file)
} else {
packagePromise = promise.resolve(file);
}
packagePromise.then((zipFile) => {
uploadPackage(client, webappsActor, zipFile)
.then((fileActor) => {
let request = {
to: webappsActor,
type: "install",
appId: appId,
upload: fileActor
};
client.request(request, (res) => {
// If the install method immediatly fails,
// reject immediatly the installPackaged promise.
// Otherwise, wait for webappsEvent for completion
if (res.error) {
deferred.reject(res);
}
});
client.addOneTimeListener("webappsEvent", function (aState, aType, aPacket) {
if ("error" in aType)
deferred.reject({error: aType.error, message: aType.message});
else
deferred.resolve({appId: aType.appId});
});
// Ensure deleting the temporary package file, but only if that a temporary
// package created when we pass a directory as `packagePath`
if (zipFile != file)
zipFile.remove(false);
// In case of success or error, ensure deleting the temporary package file
// also created on the device, but only once install request is done
deferred.promise.then(removeServerTemporaryFile, removeServerTemporaryFile);
});
});
return deferred.promise;
}
exports.installPackaged = installPackaged;
function installHosted(client, webappsActor, appId, metadata, manifest) {
let deferred = promise.defer();
let request = {
to: webappsActor,
type: "install",
appId: appId,
metadata: metadata,
manifest: manifest
};
client.request(request, (res) => {
if (res.error) {
deferred.reject(res);
}
});
client.addOneTimeListener("webappsEvent", function (aState, aType, aPacket) {
if ("error" in aType)
deferred.reject({error: aType.error, message: aType.message});
else
deferred.resolve({appId: aType.appId});
});
return deferred.promise;
}
exports.installHosted = installHosted;

View File

@ -8,5 +8,6 @@ TEST_DIRS += ['tests']
JS_MODULES_PATH = 'modules/devtools' JS_MODULES_PATH = 'modules/devtools'
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
'Simulator.jsm' 'Simulator.jsm',
'app-actor-front.js'
] ]

View File

@ -98,12 +98,13 @@ function do_get_webappsdir() {
var webappsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile); var webappsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
webappsDir.append("test_webapps"); webappsDir.append("test_webapps");
if (!webappsDir.exists()) if (!webappsDir.exists())
webappsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); webappsDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("755", 8));
var coreAppsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile); var coreAppsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
coreAppsDir.append("test_coreapps"); coreAppsDir.append("test_coreapps");
if (!coreAppsDir.exists()) if (!coreAppsDir.exists())
coreAppsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); coreAppsDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("755", 8));
var tmpDir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);
// Register our own provider for the profile directory. // Register our own provider for the profile directory.
// It will return our special docshell profile directory. // It will return our special docshell profile directory.
@ -116,6 +117,7 @@ function do_get_webappsdir() {
else if (prop == "coreAppsDir") { else if (prop == "coreAppsDir") {
return coreAppsDir.clone(); return coreAppsDir.clone();
} }
return tmpDir.clone();
throw Cr.NS_ERROR_FAILURE; throw Cr.NS_ERROR_FAILURE;
}, },
QueryInterface: function(iid) { QueryInterface: function(iid) {

View File

@ -2,6 +2,9 @@
http://creativecommons.org/publicdomain/zero/1.0/ */ http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/osfile.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {installHosted, installPackaged} = require("devtools/app-actor-front");
let gAppId = "actor-test"; let gAppId = "actor-test";
const APP_ORIGIN = "app://" + gAppId; const APP_ORIGIN = "app://" + gAppId;
@ -177,105 +180,35 @@ add_test(function testUninstall() {
}); });
add_test(function testFileUploadInstall() { add_test(function testFileUploadInstall() {
function createUpload() { let packageFile = do_get_file("data/app.zip");
let request = { installPackaged(gClient, gActor, packageFile.path, gAppId)
type: "uploadPackage" .then(function ({ appId }) {
}; do_check_eq(appId, gAppId);
webappActorRequest(request, function (aResponse) {
getPackageContent(aResponse.actor);
});
}
function getPackageContent(uploadActor) {
let packageFile = do_get_file("data/app.zip");
OS.File.read(packageFile.path)
.then(function (bytes) {
// To work around the fact that JSON.stringify translates the typed
// array to object, we are encoding the typed array here into a string
let content = String.fromCharCode.apply(null, bytes);
uploadChunk(uploadActor, content);
});
}
function uploadChunk(uploadActor, content) {
let request = {
to: uploadActor,
type: "chunk",
chunk: content
};
gClient.request(request, function (aResponse) {
endsUpload(uploadActor);
});
}
function endsUpload(uploadActor, content) {
let request = {
to: uploadActor,
type: "done"
};
gClient.request(request, function (aResponse) {
installApp(uploadActor);
});
}
function installApp(uploadActor) {
let request = {type: "install", appId: gAppId, upload: uploadActor};
webappActorRequest(request, function (aResponse) {
do_check_eq(aResponse.appId, gAppId);
});
gClient.addOneTimeListener("webappsEvent", function listener(aState, aType, aPacket) {
do_check_eq(aType.appId, gAppId);
if ("error" in aType) {
do_print("Error: " + aType.error);
}
if ("message" in aType) {
do_print("Error message: " + aType.message);
}
do_check_eq("error" in aType, false);
removeUpload(uploadActor);
});
}
function removeUpload(uploadActor, content) {
let request = {
to: uploadActor,
type: "remove"
};
gClient.request(request, function (aResponse) {
run_next_test(); run_next_test();
}, function (e) {
do_throw("Failed install uploaded packaged app: " + e.error + ": " + e.message);
}); });
}
createUpload();
}); });
add_test(function testInstallHosted() { add_test(function testInstallHosted() {
gAppId = "hosted-app"; gAppId = "hosted-app";
let request = { let metadata = {
type: "install", origin: "http://foo.com",
appId: gAppId, installOrigin: "http://metadata.foo.com",
manifest: { manifestURL: "http://foo.com/metadata/manifest.webapp"
name: "My hosted app"
},
metadata: {
origin: "http://foo.com",
installOrigin: "http://metadata.foo.com",
manifestURL: "http://foo.com/metadata/manifest.webapp"
}
}; };
webappActorRequest(request, function (aResponse) { let manifest = {
do_check_eq(aResponse.appId, gAppId); name: "My hosted app"
}); };
installHosted(gClient, gActor, gAppId, metadata, manifest).then(
// The install request is asynchronous and send back an event to tell function ({ appId }) {
// if the installation succeed or failed do_check_eq(appId, gAppId);
gClient.addOneTimeListener("webappsEvent", function listener(aState, aType, aPacket) { run_next_test();
do_check_eq(aType.appId, gAppId); },
if ("error" in aType) { function (e) {
do_print("Error: " + aType.error); do_throw("Failed installing hosted app: " + e.error + ": " + e.message);
} }
if ("message" in aType) { );
do_print("Error message: " + aType.message);
}
do_check_eq("error" in aType, false);
run_next_test();
});
}); });
add_test(function testCheckHostedApp() { add_test(function testCheckHostedApp() {