Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial version of an install and uninstall hook #778

Merged
merged 12 commits into from Jun 16, 2017

Conversation

crankycoder
Copy link
Contributor

This tries to close off #748 by adding an install and uninstall hook into the normandy driver.

This patch does not currently pass test cases though - the uninstall of the XPI file causes an error somewhere down inside the Extensions.jsm module.

I could stand to use a second pair of eyes on this to see why this is breaking so horribly.

My fixture data for the XPI installer is the simplest possible webextension - just a manifest.json file with no JS code, icons or anything else. Just the manifest zipped into a file with an XPI extension.

@crankycoder
Copy link
Contributor Author

The gory stack trace I get from this test case is:

WARN	Exception running bootstrap method shutdown on normandydriver@example.com: TypeError: data['Extension:Extensions'] is undefined (resource://gre/modules/Extension.jsm:1046:5) JS Stack trace: shutdown@Extension.jsm:1046:5 < shutdown@WebExtensionBootstrap.js:33:3 < callBootstrapMethod@XPIProvider.jsm:5060:11 < uninstallAddon@XPIProvider.jsm:5353:11 < uninstall@XPIProvider.jsm:7729:5 < this.NormandyDriver/uninstallAddon/uninstallPromise/</<@NormandyDriver.jsm:224:17 < safeCall@AddonManager.jsm:192:5 < makeSafe/<@AddonManager.jsm:207:25 < process@Promise-backend.js:922:23 < walkerLoop@Promise-backend.js:806:7 < Promise*scheduleWalkerLoop@Promise-backend.js:739:11 < schedulePromise@Promise-backend.js:770:7 < Promise.prototype.then@Promise-backend.js:455:5 < getAddon@XPIProviderUtils.js:1073:12 < getVisibleAddonForID@XPIProviderUtils.js:1122:5 < getAddonByID@XPIProvider.jsm:4274:5 < callProviderAsync@AddonManager.jsm:294:12 < promiseCallProvider/<@AddonManager.jsm:318:53 < Promise@Promise-backend.js:390:5 < promiseCallProvider@AddonManager.jsm:317:10 < getAddonByID/promises<@AddonManager.jsm:2421:12 < getAddonByID@AddonManager.jsm:2420:20 < getAddonByID@AddonManager.jsm:3558:7 < this.NormandyDriver/uninstallAddon/uninstallPromise/<@NormandyDriver.jsm:220:11 < uninstallPromise@NormandyDriver.jsm:219:16 < uninstallAddon@NormandyDriver.jsm:236:14 < installXpi/<@browser_NormandyDriver.js:31:12 < promise callback*installXpi@browser_NormandyDriver.js:29:3 < async*inner@head.js:47:11 < async*inner@head.js:35:11 < Async*Tester_execTest/<@browser-test.js:757:21 < TaskImpl_run@Task.jsm:319:42 < promise callback*TaskImpl_handleResultValue@Task.jsm:396:7 < TaskImpl_run@Task.jsm:327:15 < TaskImpl@Task.jsm:277:3 < asyncFunction@Task.jsm:252:14 < Task_spawn@Task.jsm:166:12 < Tester_execTest@browser-test.js:752:9 < Tester.prototype.nextTest</<@browser-test.js:672:7 < SimpleTest.waitForFocus/waitForFocusInner/focusedOrLoaded/<@SimpleTest.js:795:59
GECKO(37469) | === Everything went well
40 INFO Console message: Warning: attempting to write 5541 bytes to preference extensions.bootstrappedAddons. This is bad for general performance and memory usage. Such an amount of data should rather be written to an external file. This preference will not be sent to any content processes.
41 INFO Console message: 1495648839181	addons.xpi	WARN	Exception running bootstrap method shutdown on normandydriver@example.com: TypeError: data['Extension:Extensions'] is undefined (resource://gre/modules/Extension.jsm:1046:5) JS Stack trace: shutdown@Extension.jsm:1046:5 < shutdown@WebExtensionBootstrap.js:33:3 < callBootstrapMethod@XPIProvider.jsm:5060:11 < uninstallAddon@XPIProvider.jsm:5353:11 < uninstall@XPIProvider.jsm:7729:5 < this.NormandyDriver/uninstallAddon/uninstallPromise/</<@NormandyDriver.jsm:224:17 < safeCall@AddonManager.jsm:192:5 < makeSafe/<@AddonManager.jsm:207:25 < process@Promise-backend.js:922:23 < walkerLoop@Promise-backend.js:806:7 < Promise*scheduleWalkerLoop@Promise-backend.js:739:11 < schedulePromise@Promise-backend.js:770:7 < Promise.prototype.then@Promise-backend.js:455:5 < getAddon@XPIProviderUtils.js:1073:12 < getVisibleAddonForID@XPIProviderUtils.js:1122:5 < getAddonByID@XPIProvider.jsm:4274:5 < callProviderAsync@AddonManager.jsm:294:12 < promiseCallProvider/<@AddonManager.jsm:318:53 < Promise@Promise-backend.js:390:5 < promiseCallProvider@AddonManager.jsm:317:10 < getAddonByID/promises<@AddonManager.jsm:2421:12 < getAddonByID@AddonManager.jsm:2420:20 < getAddonByID@AddonManager.jsm:3558:7 < this.NormandyDriver/uninstallAddon/uninstallPromise/<@NormandyDriver.jsm:220:11 < uninstallPromise@NormandyDriver.jsm:219:16 < uninstallAddon@NormandyDriver.jsm:236:14 < installXpi/<@browser_NormandyDriver.js:31:12 < promise callback*installXpi@browser_NormandyDriver.js:29:3 < async*inner@head.js:47:11 < async*inner@head.js:35:11 < Async*Tester_execTest/<@browser-test.js:757:21 < TaskImpl_run@Task.jsm:319:42 < promise callback*TaskImpl_handleResultValue@Task.jsm:396:7 < TaskImpl_run@Task.jsm:327:15 < TaskImpl@Task.jsm:277:3 < asyncFunction@Task.jsm:252:14 < Task_spawn@Task.jsm:166:12 < Tester_execTest@browser-test.js:752:9 < Tester.prototype.nextTest</<@browser-test.js:672:7 < SimpleTest.waitForFocus/waitForFocusInner/focusedOrLoaded/<@SimpleTest.js:795:59
42 INFO Console message: Warning: attempting to write 5244 bytes to preference extensions.bootstrappedAddons. This is bad for general performance and memory usage. Such an amount of data should rather be written to an external file. This preference will not be sent to any content processes.
GECKO(37469) | Extension error: Failed to open input source 'jar:file:///var/folders/b3/wz8byszx531_gwwzp7bzx8yw0000gn/T/tmprXeu1e.mozrunner/extensions/normandydriver@example.com.xpi!/manifest.json' resource://gre/modules/Extension.jsm:390 :: readJSON/<@resource://gre/modules/Extension.jsm:390:7
GECKO(37469) | readJSON@resource://gre/modules/Extension.jsm:387:12
GECKO(37469) | parseManifest@resource://gre/modules/Extension.jsm:449:7
GECKO(37469) | parseManifest/<@resource://gre/modules/Extension.jsm:818:45
GECKO(37469) | get@resource://gre/modules/ExtensionUtils.jsm:150:25
GECKO(37469) | async*parseManifest@resource://gre/modules/Extension.jsm:817:12
GECKO(37469) | loadManifest@resource://gre/modules/Extension.jsm:499:7
GECKO(37469) | async*loadManifest@resource://gre/modules/Extension.jsm:822:12
GECKO(37469) | startup@resource://gre/modules/Extension.jsm:965:42
GECKO(37469) | async*startup@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/WebExtensionBootstrap.js:29:3
GECKO(37469) | callBootstrapMethod@resource://gre/modules/addons/XPIProvider.jsm:5060:11
GECKO(37469) | startInstall/<@resource://gre/modules/addons/XPIProvider.jsm:5971:13
GECKO(37469) | TaskImpl_run@resource://gre/modules/Task.jsm:319:42
GECKO(37469) | process@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:922:23
GECKO(37469) | walkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:806:7
GECKO(37469) | Promise*scheduleWalkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:739:11
GECKO(37469) | schedulePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:770:7
GECKO(37469) | completePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:707:7
GECKO(37469) | get _worker/worker.onmessage@resource://gre/modules/PromiseWorker.jsm:231:9
GECKO(37469) | EventHandlerNonNull*get _worker@resource://gre/modules/PromiseWorker.jsm:217:5
GECKO(37469) | postMessage@resource://gre/modules/PromiseWorker.jsm:291:9
GECKO(37469) | TaskImpl_run@resource://gre/modules/Task.jsm:319:42
GECKO(37469) | process@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:922:23
GECKO(37469) | walkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:806:7
GECKO(37469) | Promise*scheduleWalkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:739:11
GECKO(37469) | schedulePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:770:7
GECKO(37469) | Promise.prototype.then@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:455:5
GECKO(37469) | _deferredSave@resource://gre/modules/DeferredSave.jsm:212:5
GECKO(37469) | _startTimer/<@resource://gre/modules/DeferredSave.jsm:168:40
GECKO(37469) | syncLoadManifestFromFile@resource://gre/modules/addons/XPIProvider.jsm:1653:5
GECKO(37469) | addMetadata@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/XPIProviderUtils.js:1630:21
GECKO(37469) | processFileChanges@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/XPIProviderUtils.js:2032:23
GECKO(37469) | checkForChanges@resource://gre/modules/addons/XPIProvider.jsm:3869:34
GECKO(37469) | startup@resource://gre/modules/addons/XPIProvider.jsm:2878:25
GECKO(37469) | callProvider@resource://gre/modules/AddonManager.jsm:268:12
GECKO(37469) | _startProvider@resource://gre/modules/AddonManager.jsm:738:5
GECKO(37469) | startup@resource://gre/modules/AddonManager.jsm:905:9
GECKO(37469) | startup@resource://gre/modules/AddonManager.jsm:3087:5
GECKO(37469) | observe@file:///Users/victorng/dev/moz-central/mozilla-unified/obj-x86_64-apple-darwin16.6.0/dist/Nightly.app/Contents/Resources/components/addonManager.js:65:9
43 INFO Console message: [JavaScript Error: "[Exception... "Failed to open input source 'jar:file:///var/folders/b3/wz8byszx531_gwwzp7bzx8yw0000gn/T/tmprXeu1e.mozrunner/extensions/normandydriver@example.com.xpi!/manifest.json'"  nsresult: "0x80520012 (NS_ERROR_FILE_NOT_FOUND)"  location: "JS frame :: resource://gre/modules/Extension.jsm :: readJSON/< :: line 390"  data: yes]"]
readJSON/<@resource://gre/modules/Extension.jsm:390:7
readJSON@resource://gre/modules/Extension.jsm:387:12
parseManifest@resource://gre/modules/Extension.jsm:449:7
parseManifest/<@resource://gre/modules/Extension.jsm:818:45
get@resource://gre/modules/ExtensionUtils.jsm:150:25
async*parseManifest@resource://gre/modules/Extension.jsm:817:12
loadManifest@resource://gre/modules/Extension.jsm:499:7
async*loadManifest@resource://gre/modules/Extension.jsm:822:12
startup@resource://gre/modules/Extension.jsm:965:42
async*startup@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/WebExtensionBootstrap.js:29:3
callBootstrapMethod@resource://gre/modules/addons/XPIProvider.jsm:5060:11
startInstall/<@resource://gre/modules/addons/XPIProvider.jsm:5971:13
TaskImpl_run@resource://gre/modules/Task.jsm:319:42
process@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:922:23
walkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:806:7
Promise*scheduleWalkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:739:11
schedulePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:770:7
completePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:707:7
get _worker/worker.onmessage@resource://gre/modules/PromiseWorker.jsm:231:9
EventHandlerNonNull*get _worker@resource://gre/modules/PromiseWorker.jsm:217:5
postMessage@resource://gre/modules/PromiseWorker.jsm:291:9
TaskImpl_run@resource://gre/modules/Task.jsm:319:42
process@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:922:23
walkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:806:7
Promise*scheduleWalkerLoop@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:739:11
schedulePromise@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:770:7
Promise.prototype.then@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:455:5
_deferredSave@resource://gre/modules/DeferredSave.jsm:212:5
_startTimer/<@resource://gre/modules/DeferredSave.jsm:168:40
syncLoadManifestFromFile@resource://gre/modules/addons/XPIProvider.jsm:1653:5
addMetadata@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/XPIProviderUtils.js:1630:21
processFileChanges@resource://gre/modules/addons/XPIProvider.jsm -> resource://gre/modules/addons/XPIProviderUtils.js:2032:23
checkForChanges@resource://gre/modules/addons/XPIProvider.jsm:3869:34
startup@resource://gre/modules/addons/XPIProvider.jsm:2878:25
callProvider@resource://gre/modules/AddonManager.jsm:268:12
_startProvider@resource://gre/modules/AddonManager.jsm:738:5
startup@resource://gre/modules/AddonManager.jsm:905:9
startup@resource://gre/modules/AddonManager.jsm:3087:5
observe@file:///Users/victorng/dev/moz-central/mozilla-unified/obj-x86_64-apple-darwin16.6.0/dist/Nightly.app/Contents/Resources/components/addonManager.js:65:9

Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locally, this test actually passes for me with the latest mozilla-central. Might be a MacOS thing?

I've asked #fx-team what the best way to get a file:// path to the XPI is, because I don't think installAddon should bother to support chrome:// URIs.

@Osmose
Copy link
Contributor

Osmose commented May 25, 2017

Let me know if you want a full review pass over this, I'm assuming it's still too WIP for me to run through besides getting that test to pass.

@Osmose Osmose assigned crankycoder and unassigned Osmose May 25, 2017
@Osmose
Copy link
Contributor

Osmose commented May 25, 2017

@mnoorenberghe helped point me towards some examples in mozilla-central, and I've added a commit that generates a file:// URL in the test and removes the chrome:// handling code in the driver. It works locally for me; @crankycoder Does it work for you?

@crankycoder
Copy link
Contributor Author

@Osmose - this works for me now. My moz-central repo was completely screwy. I blame the ghost of steve jobs.

}, reason => {
ok(false, `=== Failed to uninstall addon ${reason}`);
});
dump(`SIGNAL: ${gTestPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a left over debug line here.

@mythmon mythmon mentioned this pull request May 31, 2017
@crankycoder crankycoder assigned Osmose and gijsk and unassigned crankycoder May 31, 2017
@crankycoder crankycoder requested a review from gijsk May 31, 2017 21:16
@Osmose Osmose self-requested a review May 31, 2017 21:16
dir.append("normandy.xpi");
const xpiUrl = Services.io.newFileURI(dir).spec;
var addonId = await driver.installAddon(xpiUrl);
await driver.uninstallAddon(addonId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should actually test that the add-on was installed successfully somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installAddon method returns the addon ID only if the installation is successful. The uninstall process would also fail if the addon ID was invalid, or if no installed addon could be found with that addonID. Is that not sufficient or do you want this to check the list of installed addons explicitly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this ever fails (partially or fully), I expect checking explicitly that addonId is not undefined (or whatever else installAddon returns if installation didn't work) and/or that there's an installed add-on with the returned ID, will result in more understandable failure messaging than "uninstallAddon threw an error" (because the install failed).

More generally, I'm surprised the ID would be what this returns, and that's all that it returns - the ID is available without installing the add-on...

Also, late nit: let please. :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should actually just do a quick check to see that the add-on ID we expect is in the add-on list, and then isn't after uninstall, just to be thorough. If that's difficult, then at least a comment explaining the return values would help make this test more readable.

* file:// and downloadable URLs.
*/

var installObjectCallback = (installObject, resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let, here and elsewhere. Though this also doesn't actually seems to use its binding (ie this) and so maybe you just want:

function installObjectCallback(installObject, resolve, reject) {
...
}

installAddon(url) {
/* Install an addon. Note that the AddonManager can only handle
* file:// and downloadable URLs.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing and indenting for this docstring seems off.

var installObjectCallback = (installObject, resolve, reject) => {
installObject.install();
installObject.addListener({
onInstallEnded: (addonInstall, addon) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onInstallEnded(addonInstall, addon) {
...
}

and the same below, please.

});
};

function installObjectPromise(installObject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this does is wrap installObjectCallback, which seems to have no other callers. Just put the contents of installObjectCallback straight into the promise function in here, then you don't need to pass reject/resolve around, either.


function getInstallForURLPromise(url) {
return new Promise(function(resolve, reject) {
AddonManager.getInstallForURL(url, resolve, 'application/x-xpinstall');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this throw, and/or can the callback be invoked with an error status? In either of those cases, I guess it should call reject() ? I would expect getInstallForURL to not always succeed...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback has listeners attached to it which get onInstallFailed invoked. I call a reject() function in that case to signal that the installation failed.

});
};

// Return a promise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this just want to be an async function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so - I'd rather be consistent with the use of promises instead of a mix of async functions and promise objects so that callers know what to expect.

});
});
}
return uninstallPromise(addonId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No point having an inner function here, just start the function with return new Promise(...). And again, maybe this just wants to be an async function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to be an immediately invoked function as I need to capture the addonId within the Promise using a closure.

// Test that we can install an XPI from any URL
const dir = getChromeDir(getResolvedURI(gTestPath));
dir.append("fixtures");
dir.append("normandy.xpi");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check with someone more up-to-date with this stuff (than me) - maybe @Mossop - how this works with add-on signature checks?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If normandy.xpi is unsigned then this will only work in automation in Firefox 55 and higher.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mossop normandy.xpi was indeed unsigned.. I've updated the xpi so that it's signed now.

@gijsk gijsk assigned crankycoder and unassigned gijsk Jun 1, 2017
installAddon(url) {
function getInstallForURLPromise(installUrl) {
return new Promise(function(resolve, reject) {
AddonManager.getInstallForURL(installUrl, resolve, "application/x-xpinstall");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that getInstallForURL returns a promise, no need to create your own.

* Return a promise that resolves to a success messsage if
* addon uninstall is successful.
*/
uninstallAddon(addonId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this an async function and using the promise returned by getAddonByID would make this a lot cleaner.

@gijsk
Copy link
Contributor

gijsk commented Jun 1, 2017

I could be wrong, because async functions are pretty new beasts, but my understanding is that there's no observable difference from the caller's perspective between an async function and a non-async function that returns a promise -- running either without await returns a promise, and you can then await or .then() the promise. It's just less boilerplate to write an async function than function() { return new Promise(resolve => { ... }); }, and of course you can use await inside an async function and can't inside a non-async argument function to the Promise constructor.

@Osmose
Copy link
Contributor

Osmose commented Jun 2, 2017

I could be wrong, because async functions are pretty new beasts, but my understanding is that there's no observable difference from the caller's perspective between an async function and a non-async function that returns a promise -- running either without await returns a promise, and you can then await or .then() the promise. It's just less boilerplate to write an async function than function() { return new Promise(resolve => { ... }); }, and of course you can use await inside an async function and can't inside a non-async argument function to the Promise constructor.

Yep.

The current guidelines for the shield-recipe-client is to prefer async/await functions over Promises where possible. The driver uses Promises directly mostly because it's using Promises from the sandbox to allow for sandbox code to access then and use await properly. Eventually we should port the rest of the driver functions to use SandboxManager.wrapAsync instead of using Promises directly.

Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! The others have already mentioned making these methods async/await, and because they're called by methods in the sandbox, you'll want to return promises that the sandbox can use. Luckily, there's a method on the SandboxManager class called wrapAsync that can be used to wrap an async function and return the right kind of promise for the sandbox. Something like this:

return {
  installAddon: sandboxManager.wrapAsync(async function installAddon(installUrl) {
    await blah;
    return foo;
  }, {cloneInto: true, cloneArguments: true}),
};

I'm not sure if cloneInto and cloneArguments (see the docstring on wrapAsync) are necessary though, as the parameters going in and coming out seem to be strings. @Mossop: Are strings a thing we should be cloning when they pass a sandbox boundary?

The only other thing I noticed was that we could use some tests for failure states for these methods. We could use sinon to mock out addListener and uninstall on the Addon class' prototype, and just use a fake add-on ID to test if getAddonByID fails.

@crankycoder
Copy link
Contributor Author

I don't think I can restructure the installAddon API as an async function.

async functions just decorate a function so that the resolve method of a Promise is wrapped around the return value of the top level function.

AddonManager.getInstallForURL invokes a callback with a single argument - an addon object. I need to wrap getInstallForURL in a Promise so that I can make a subsequent call to .install() on the addon object using a Promise to make the call sequence clearer.

The problem here is that the addon object's .install() method immediately returns with no return value. No callback functions and no promise. The only signal we get back that the install either succeeded or failed is via a listener object that is bound to the addon.

The decorator in the async function won't wrap the return value of the listener object because the final return value we care about is from within listener.onInstallSucceeded or listener.onInstallFailed. The async function decorator will be wrapping the container object to the listener.

If we still really want to have the top level .installAddon function declared as an async function, I could wrap the lines where I register the listener, and invoke addon.install() in a Promise, but that seems to defeat the purpose of async functions entirely - which seems to be provide some syntactic sugar around explicit declaration of Promises.

If you see a way around this - let me know. I haven't been able to figure out a nicer way to clean up this mess of callback functions.

I'm going to try adding the sinon tests though - we should be testing the failure cases.

@Mossop
Copy link
Member

Mossop commented Jun 5, 2017

I'm not sure if cloneInto and cloneArguments (see the docstring on wrapAsync) are necessary though, as the parameters going in and coming out seem to be strings. @Mossop: Are strings a thing we should be cloning when they pass a sandbox boundary?

Not as far as I know

@Mossop
Copy link
Member

Mossop commented Jun 5, 2017

AddonManager.getInstallForURL invokes a callback with a single argument - an addon object. I need to wrap getInstallForURL in a Promise so that I can make a subsequent call to .install() on the addon object using a Promise to make the call sequence clearer.

Note that you getInstallForURL also returns a promise, you don't need to use the callback if that is cleaner.

@gijsk
Copy link
Contributor

gijsk commented Jun 5, 2017

You can still explicitly return a promise from an async function, try this in a console (with a window, so not node.js):

async function a() { return new Promise(r => setTimeout(() => r(5), 1000)) }
a().then(arg => console.log(arg))

will print 5 (not 'promise' or something like that).

So, (untested)

async installAddon(url) {
  let installObj = await AddonManager.getInstallForURL(url, null, "application/x-xpinstall");
  installObj.install();
  return new Promise((resolve, reject) => installObj.addListener({
    onInstallEnded(addonInstall, addon) {
      resolve(addon.id);
    },
    onInstallFailed(addonInstall) {
      reject(`AddonInstall error code: [${addonInstall.error}]`);
    },
  }));
}

seems like it would work?

@Osmose Osmose force-pushed the features/driver-xpi-install branch from bc00ff8 to 86326df Compare June 14, 2017 18:07
@crankycoder crankycoder assigned Osmose and gijsk and unassigned crankycoder Jun 14, 2017
Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Osmose
Copy link
Contributor

Osmose commented Jun 14, 2017

CircleCI failure is #812, we can rebase once it's fixed.

gijsk
gijsk previously requested changes Jun 14, 2017
Copy link
Contributor

@gijsk gijsk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically finished except I'm concerned about the install promise resolving but the test not being happy if we immediately try to uninstall. Should the install promise wait for something else?

onInstallFailed(addonInstall) {
reject(`AddonInstall error code: [${addonInstall.error}]`);
},
onDownloadFailed(install) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unused trailing argument. I'd expect eslint to complain about this...

// Installing kicks off an asychronous process which tries to read the manifest.json
// to startup the addon. Note that onInstallEnded is triggered too early
// which is why we need this delay.
await new Promise(resolve => SimpleTest.executeSoon(resolve));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we wait for something specific, e.g. an observer notification generated by the test add-on once install has actually happened? Otherwise this feels like it's waiting to go intermittent on infra...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More generally, it feels odd that the addon manager doesn't provide something 'better' to wait for, where install is 'properly' finished, instead of only sort of... Maybe @Mossop can suggest something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is really a problem with extensions/internal/XPIInstall.jsm and the way that checkPrompot fires off an async function right away. Using the observer doesn't work right now because the onInstallEnded listeners are triggered prior to the install actually finishing up.

}));

add_task(withDriver(Assert, async function uninstallInvalidAddonId(driver) {
// Test that we can install an XPI from any URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is the same everywhere, and doesn't reflect the test. Please update it to remove confusion.

const invalidAddonId = "not_a_valid_xpi_id@foo.bar";
try {
await driver.uninstallAddon(invalidAddonId);
ok(false, `Uninstall returned an unexpected null for success`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful if this failure message more explicitly indicated why we expected failure, rather than just that we didn't expect success...

@Osmose Osmose assigned crankycoder and unassigned Osmose and gijsk Jun 14, 2017
crankycoder and others added 11 commits June 15, 2017 11:23
…nto the testcase to verify that the addon was properly installed by checking the value of the addonId
…e the closure around the Promise that wraps AddonManager.getAddonByID
…view

* Reordered the listener and install() invocation to avoid possible race
* double check the addonID after installation
* added a timeout to let the XPI installation complete properly.
…en installing an addon

added testcase to uninstall invalid addon id
@crankycoder
Copy link
Contributor Author

@gijsk can we skip on #778 (comment) for now? To fix the race condition between install and uninstall correctly seems to be out of scope for this particular patch.

@gijsk
Copy link
Contributor

gijsk commented Jun 16, 2017

@gijsk can we skip on #778 (comment) for now? To fix the race condition between install and uninstall correctly seems to be out of scope for this particular patch.

We can skip it , but please file a bug against the add-on manager in bugzilla and reference it in a comment in the code.

@crankycoder crankycoder dismissed gijsk’s stale review June 16, 2017 14:48

Filed bug https://bugzilla.mozilla.org/show_bug.cgi?id=1373671 to address the race condition between install and uninstall of XPI

@crankycoder crankycoder merged commit 113e61b into mozilla:master Jun 16, 2017
@gijsk
Copy link
Contributor

gijsk commented Jun 16, 2017

Osmose pointed out I hadn't formally approved since the last push of code - I approve! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants