Closed Bug 836437 Opened 7 years ago Closed 6 years ago

Add the ability to resume a download from where it stopped

Categories

(Toolkit :: Downloads API, defect)

defect
Not set

Tracking

()

RESOLVED FIXED
mozilla25

People

(Reporter: Paolo, Assigned: Paolo)

References

Details

(Whiteboard: [qa-])

Attachments

(3 files, 9 obsolete files)

87.10 KB, patch
enndeakin
: review+
Paolo
: checkin+
Details | Diff | Splinter Review
78.63 KB, patch
Paolo
: checkin+
Details | Diff | Splinter Review
36.20 KB, patch
Paolo
: checkin+
Details | Diff | Splinter Review
The "cancel" method accepts an aOptions argument, where the keepPartFile option
can be used to determine if the download will restart from the point where it
currently stopped, if supported by the saver object. The "target.partFile"
property keeps a reference to the partially downloaded file.

If the partially downloaded file exists, it will be considered when the "start"
method is called, if the source supports resuming.
If the source does not support resuming from partially downloaded files, then the
file should not be kept, even if requested.
Assignee: nobody → marcos
Hi guys.

Can I get more info on what part of the code is involved and how to implement this?

Thanks for your help.

Marcos.
(In reply to Marcos Aruj from comment #2)
> Can I get more info on what part of the code is involved and how to
> implement this?

Hello Marcos! This bug is about adding off-main-thread resuming support to the
JavaScript API for downloads. It can be divided in two main parts.

The first involves adding resuming support to the BackgroundFileSaver component.
The component is written in C++, you can see its interface defined in
"BackgroundFileSaver.idl" and the implementation in "BackgroundFileSaver.cpp".

The second part, to be done after the first one, involves changes to the code in
the "toolkit/components/jsdownloads" folder, which is not built by default now,
but will be available in Nightly in a few days from now. There is still some time
to be spent to better understand how resuming would work in the new API, before
getting to coding it.

The C++ part, however, can already be compiled and if you can run tests from the
"test_backgroundfilesaver.js" file, you have what you need to move forward on
implementing that part. If you're interested, let me know and I can file a new
dependent bug to let you get started there.

Alternatively, there are some other JavaScript bugs that may be worked on in
the new API, and require less time to complete. I can point you to a few of
them if you're interested.
Thanks Paolo for the info. I think I would then prefer the alternative bugs you mention if that's OK. :) Please point me to them. 

Kind regards.
Blocks: 881058
No longer blocks: jsdownloads
Assignee: marcos → paolo.mozmail
Depends on: 887425
Attached patch Interface definition (obsolete) — Splinter Review
Supporting partial download data in a fully asynchronous environment where we
may have different views controlling the same download is not easy, but I
think this interface is simple enough and prevents race conditions. To do
this, I had to write an interface that is slightly different from comment 0.

Please take a look at this interface, and let me know if you see any possible
race conditions or a use case that is not covered.

This interface supports the current pause/resume logic, but is more clear
about what "pausing" means (in fact, it does not suspend the request, it just
cancels the request and keeps any partial data). So, if hasPartial is true,
it means the download can be "paused": you can call cancel() and start() to
actually pause and resume it.

To completely stop a download, you can call cancel() and then removePartial()
immediately. But, probably, we don't want to do that, because the stop button
should still preserve partial data where possible, until the download is
removed from the list. (Once stopping does the right thing automatically,
we'll unify the concepts in the user interface also.)

On shutdown, we will just cancel() all the downloads, and this will do the
right thing, keeping the partial data if possible, to use when the download
is restarted when the browser is reopened.
Attachment #774667 - Flags: feedback?(mak77)
Attachment #774667 - Flags: feedback?(enndeakin)
Comment on attachment 774667 [details] [diff] [review]
Interface definition

+   *
+   * To have any effect, this property must be set before starting the download.
+   * Setting this property to false after the download already started will not
+   * remove any partial data.
+   *

after the download *has* already started


>+   * @return {Promise}
>+   * @resolves When the partial data has been successfully removed.
>+   * @rejects JavaScript exception if the operation could not be completed.
>+   */
>+  removePartial: function ()
>+  {
>+    return Promise.resolve();
>+  },

So if someone wants to restart a download fresh, do they need to call this method first?

>+  /**
>+   * Prevents the download from starting again after having been stopped.  This
>+   * method does not cancel the download in case it has already started.
>+   *
>+   * This method is used while shutting down or disposing of the download
>+   * object, to prevent other callers to interfere with the operation.  This is
>+   * required because cancellation and other operations are asynchronous.
>+   */
>+  preventStart: function ()
>+  {

This name is a bit confusing. If it is intended for cleanup, it is private to the download manager implementation or would other callers use it?


>           this.remove(download);
>+          // Since the download state may have changed meanwhile, we fully
>+          // execute the procedure that ensures that the download is stopped and
>+          // no partial data is kept.  We don't need to wait for the procedure
>+          // to be complete before processing the other downloads in the list.
>+          download.preventStart();
>+          download.cancel();
>+          download.removePartial();

Could we combine these into a single call that just stops and cleans up?
(In reply to Neil Deakin from comment #6)
> >+  removePartial: function ()
> >+  {
> >+    return Promise.resolve();
> >+  },
> 
> So if someone wants to restart a download fresh, do they need to call this
> method first?

Yes, if they want to restart the same download with the same target file,
but without using any previously stored partial data.

> >+  preventStart: function ()
> >+  {
> 
> This name is a bit confusing. If it is intended for cleanup, it is private
> to the download manager implementation or would other callers use it?
> 
> >+          download.preventStart();
> >+          download.cancel();
> >+          download.removePartial();
> 
> Could we combine these into a single call that just stops and cleans up?

Since the two cases when preventStart is currently used are before throwing
away a download and before shutting down the browser, maybe we might define
a download.shutdown(bool removePartial) method that calls preventStart plus
"cancel", and optionally calls removePartial, returning a promise resolved
when the operation is completed. This would replace the need for a public
preventStart method. What do you think?
> Since the two cases when preventStart is currently used are before throwing
> away a download and before shutting down the browser, maybe we might define
> a download.shutdown(bool removePartial) method that calls preventStart plus
> "cancel", and optionally calls removePartial, returning a promise resolved
> when the operation is completed. This would replace the need for a public
> preventStart method. What do you think?

preventStart isn't a clear api name (nor is removePartial) so I think that would be better.

I probably wouldn't call it 'shutdown' though as it doesn't have to be called at shutdown. Maybe 'reset' or 'cleanup'.
what about just download.finalize()?
Comment on attachment 774667 [details] [diff] [review]
Interface definition

Review of attachment 774667 [details] [diff] [review]:
-----------------------------------------------------------------

I think covered pretty well the request, just a couple notes (and my suggested API name is in previous commment)

::: toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ +417,5 @@
> +   * download failed or has been canceled, using the removePartial method.  If
> +   * this property is still true, partial data will be retained during the next
> +   * download attempt.
> +   */
> +  tryKeepPartial: true,

I see you used this in SimpleDownload, but do we have a real use case for it?
What's the downside if SimpleDownload would just not care and we'd not expose this option?

::: toolkit/components/jsdownloads/src/DownloadList.jsm
@@ +221,5 @@
> +          // no partial data is kept.  We don't need to wait for the procedure
> +          // to be complete before processing the other downloads in the list.
> +          download.preventStart();
> +          download.cancel();
> +          download.removePartial();

do we already handle PB somewhere? cause there we should remove the partial.
Attachment #774667 - Flags: feedback?(mak77) → feedback+
that was "I think Neil covered pretty well the request..."
(In reply to Marco Bonardo [:mak] from comment #10)
> > +  tryKeepPartial: true,
> 
> I see you used this in SimpleDownload, but do we have a real use case for it?
> What's the downside if SimpleDownload would just not care and we'd not
> expose this option?

Consumers of simpleDownload would leak partial files on disk on failure.

Setting tryKeepPartial to false is also useful for those cases where we want to
handle a full download with progress report (that is, not a simpleDownload) but
we just discard the result on failure (for example, this would allow us to
move add-on XPI downloads off the main thread).

Actually, since tryKeepPartial is only safe to use if the download is going to
be persisted across sessions or the caller invokes removePartial before
shutdown, I wonder if we should just have the default to false, and set it
to true only where we add the download to the list (DownloadLegacySaver and
panel front-end code).

> ::: toolkit/components/jsdownloads/src/DownloadList.jsm
> do we already handle PB somewhere? cause there we should remove the partial.

Yes, we should remove partial data when downloads are canceled when exiting
private browsing mode. That's covered by bug 836443.
(In reply to Neil Deakin from comment #8)
> preventStart isn't a clear api name (nor is removePartial)

Would removePartialData be clearer than removePartial?

(In reply to Marco Bonardo [:mak] from comment #9)
> what about just download.finalize()?

finalize looks good to me, with an aRemovePartialData argument.
This bug will make it possible to restart downloads previously started through
a DownloadLegacySaver. This makes the behavior of this saver interchangeable
with DownloadCopySaver.

I've thus refactored the tests so that they are the same for both components.
For the moment, I've disabled the restart tests, that will be enabled in a
separate patch on this bug.

Unfortunately, since with the legacy saver we cannot get a reference to the
Download object before starting the download, some tests have a slightly
different control flow between the two versions.

This patch contains a rename of test_DownloadCore.js to the new name
common_test_Download.js, with the modifications to include all the former
test_DownloadLegacy.js tests.
Attachment #778498 - Flags: review?(enndeakin)
This patch contains the updated interface definitions, as well as the
first implementation of resuming for DownloadCopySaver.  This will need
more tests for all the new methods in the interface, but there should be
enough material for a preliminary review.

Marco, I think I'll do the DownloadError constructor refactoring we talked
about, since it seems we have more cases where we need to set the properties
manually, and not based on the current constructor parameters.

Given the complexity of this patch alone, I think I'll add resuming support
for DownloadLegacySaver in a third, separate patch.
Attachment #774667 - Attachment is obsolete: true
Attachment #774667 - Flags: feedback?(enndeakin)
Attachment #778853 - Flags: feedback?(mak77)
Attachment #778853 - Flags: feedback?(enndeakin)
With the refactoring, some tests now failed more consistently for the same
causes as the intermittent tests in bug 865364. This patch makes each request
independent from the others. This fixed the tests and will probably also have
the effect of resolving the intermittent failures.
Attachment #778853 - Attachment is obsolete: true
Attachment #778853 - Flags: feedback?(mak77)
Attachment #778853 - Flags: feedback?(enndeakin)
Attachment #779715 - Flags: feedback?(mak77)
Attachment #779715 - Flags: feedback?(enndeakin)
Blocks: 865364
This is how the third and last patch in the series would look like.
Attachment #780284 - Flags: feedback?(mak77)
Attachment #780284 - Flags: feedback?(enndeakin)
Comment on attachment 778498 [details] [diff] [review]
Part 1 of 2 - Unify DownloadLegacy tests in preparation for resuming support

>+  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
>+                  .createInstance(Ci.nsIWebBrowserPersist);
>+
>+  // Apply decoding if required by the "Content-Encoding" header.
>+  persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
>+
>+  // We must create the nsITransfer implementation using its class ID because
>+  // the "@mozilla.org/transfer;1" contract is currently implemented in
>+  // "toolkit/components/downloads".  When the other folder is not included in
>+  // builds anymore (bug 851471), we'll be able to use the contract ID.
>+  let transfer =
>+      Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
>+                .createInstance(Ci.nsITransfer);
>+
>+  if (aOptions) {
>+    aOptions.outPersist = persist;
>+  }

Move these outPersist lines above to where persist is set up.


>- * Executes a download, started by constructing the simplest Download object.
>+ * Executes a download and checks its basic properties after constructions.

'construction'


>-add_task(function test_download_construction()
>+add_task(function test_basic()
> {
>-  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
>+  let tempDirectory = FileUtils.getDir("TmpD", []);

tempDirectory is unused.


>+    // The download is already started, wait for completion and report errors.
>+    if (!download.stopped) {
>+      yield download.start();
>+    }

What is this condition checking for? If you want the test to verify that download.stopped must be false, the test should check that. Are there cases where download.stopped could be true here? Same with a number of other places where this condition appears.


>-add_task(function test_download_final_state_notified()
>+add_task(function test_final_state_notified()
> {
>-  let download = yield promiseSimpleDownload();
>+  let deferResponse = deferNextResponse();
>+
>+  let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
> 
...
>-  // Starts the download and waits for completion.
>-  yield download.start();

I'm a bit confused here. Doesn't promiseStartDownload end up calling start() already?


It's a bit confusing that some of the tests use promiseStartDownload, while others check gUseLegacySaver and set things up separately. Some comments as to why this is needed is those cases would be helpful.
Comment on attachment 779715 [details] [diff] [review]
Fix tests - Add the ability to resume to DownloadCopySaver.

>+   * Indicates whether any partially downloaded data should be retained, to use
>+   * it when restarting a failed or canceled download.  The default is false.

remove 'it'


>+   * If this property is set to true, care should be taken that partial data is
>+   * removed before the reference to the download is discarded.  This can be
>+   * done using the removePartialData or the "finalize" methods.
>+   */
>+  tryKeepPartialData: false,

Maybe better as 'tryToKeepPartialData'


>   /**
>+   * String containing the path of the ".part" file containing the data
>+   * downloaded so far, or null to disable the use of a ".part" file to keep
>+   * partially downloaded data.
>+   */
>+  partFilePath: null,
>+
>+  /**

So to retain partial files, does one need to set both target.partFilePath and download.tryKeepPartialData? Seems like only one field should be needed to be set.


>+   *
>+   * This method never called until the promise returned by "execute" is either
>+   * resolved or rejected, and the "execute" method is not called again until
>+   * the promise returned by this method is resolved or rejected.

This method *is* never called...


>-      // Set the target file, that will be deleted if the download fails.
>-      backgroundFileSaver.setTarget(new FileUtils.File(download.target.path),
>-                                    false);

The old code sets the target here before everything starts, but the new code doesn't set it until onStartRequest is called. Is that ok?

>+      // Create a placeholder for the final target file immediately.  If the
>+      // file already exist, don't delete its contents yet.

file already *exists*, ...

Maybe explain why we need to create the file right away.


>+        if (this._canceled) {
>+          // Don't create the BackgroundFileSaver object if we have been
>+          // canceled meanwhile.
>+          throw new DownloadError(Cr.NS_ERROR_FAILURE, "Saver canceled.");
>+        }

Is canceling a download an error, or does this error not propagate?


>+  let deferCancel = Promise.defer();
>+  download.onchange = function () {
>+    if (!download.stopped && !download.canceled && download.progress == 50) {
>+      // After we receive the progress notification, we should wait some time
>+      // for the worker thread of BackgroundFileSaver to receive the data to
>+      // be written to disk.  We don't have control over when this happens.
>+      do_timeout(50, () => {
>+        deferCancel.resolve(download.cancel());
>+      });

This will fail if the test happens to take longer that 50ms, so it should probably iterate.


Is the promiseNextRequestReceived method still needed?


Not sure I like the names blockResponses and unblockResponses. These are used just to delay a response part way and start it again right? Perhaps 'mustPauseResponse' and 'continueResponse' would be clearer.
Attachment #779715 - Flags: feedback?(enndeakin) → feedback+
Comment on attachment 780284 [details] [diff] [review]
Work in progress - Add the ability to resume to DownloadLegacySaver.

Both DownloadCopySaver.removePartialData and DownloadLegacySaver.removePartialData are the same code. Could they be shared or just use an inherited implementation or will they differ?
Attachment #780284 - Flags: feedback?(enndeakin) → feedback+
(In reply to Neil Deakin from comment #18)
> >+    // The download is already started, wait for completion and report errors.
> >+    if (!download.stopped) {
> >+      yield download.start();
> >+    }
> 
> What is this condition checking for?
> 
> It's a bit confusing that some of the tests use promiseStartDownload, while
> others check gUseLegacySaver and set things up separately. Some comments as
> to why this is needed is those cases would be helpful.

To clarify the tests, I've added a promiseDownloadStopped function, renamed
promiseSimpleDownload to promiseNewDownload, added comments when we do things
differently for DownloadLegacySaver, and refactored some tests to include the
DownloadLegacySaver case in error state checks.

It should be much more readable overall, let me know if there is something else
than needs clarification.
Attachment #778498 - Attachment is obsolete: true
Attachment #778498 - Flags: review?(enndeakin)
Attachment #780859 - Flags: review?(enndeakin)
(In reply to Neil Deakin from comment #19)
> So to retain partial files, does one need to set both target.partFilePath
> and download.tryKeepPartialData? Seems like only one field should be needed
> to be set.

Currently, partFilePath may be set even if tryKeepPartialData is false, to
use a ".part" file instead of the final target while downloading. In case
tryKeepPartialData is false, the ".part" file is always deleted.

tryKeepPartialData instead controls whether partial data should be kept, for
all saver types. A DownloadDOMDocumentSaver might for example use this flag to
determine if the responses should be kept using the browser cache, without
actually needing the partFilePath to be set.

Currently, both DownloadCopySaver and DownloadLegacySaver also need partFilePath
to be set, to be able to keep partial data. We may make it so that if
tryKeepPartialData is true and partFilePath is null, a default value is used,
but then, since tryKeepPartialData may be set to false later, the
removePartialData function would always need to check if the file with the
default name exists and delete it, even if we didn't want to use a part file
in the first place. What do you think?

> The old code sets the target here before everything starts, but the new code
> doesn't set it until onStartRequest is called. Is that ok?

Yes, this should work correctly.

> >+        if (this._canceled) {
> >+          // Don't create the BackgroundFileSaver object if we have been
> >+          // canceled meanwhile.
> >+          throw new DownloadError(Cr.NS_ERROR_FAILURE, "Saver canceled.");
> >+        }
> 
> Is canceling a download an error, or does this error not propagate?

The Download object handles cancellation by requesting the DownloadSaver object
to terminate the "execute" method with an exception that is then ignored, so it
does not propagate.

> >+  let deferCancel = Promise.defer();
> >+  download.onchange = function () {
> >+    if (!download.stopped && !download.canceled && download.progress == 50) {
> >+      // After we receive the progress notification, we should wait some time
> >+      // for the worker thread of BackgroundFileSaver to receive the data to
> >+      // be written to disk.  We don't have control over when this happens.
> >+      do_timeout(50, () => {
> >+        deferCancel.resolve(download.cancel());
> >+      });
> 
> This will fail if the test happens to take longer that 50ms, so it should
> probably iterate.

I've implemented an iterative approach that should be more robust. I wasn't
able to test it as the actual code flow depends on timing, and it seems that
the simple call to OS.File.exists makes the timeout unnecessary, probably
because it posts events between threads.

> Not sure I like the names blockResponses and unblockResponses. These are
> used just to delay a response part way and start it again right? Perhaps
> 'mustPauseResponse' and 'continueResponse' would be clearer.

I've gone for mustInterruptResponses and continueResponses (plural since they
apply to all the requests at the same time).
Attachment #780284 - Attachment is obsolete: true
Attachment #780284 - Flags: feedback?(mak77)
Attachment #780890 - Flags: feedback?(enndeakin)
(In reply to Neil Deakin from comment #20)
> Both DownloadCopySaver.removePartialData and
> DownloadLegacySaver.removePartialData are the same code. Could they be
> shared or just use an inherited implementation or will they differ?

They'll be the same, but the function won't apply to other saver types, so I've
just done "return DownloadCopySaver.prototype.removePartialData.call(this);".
Attachment #779715 - Attachment is obsolete: true
Attachment #779715 - Flags: feedback?(mak77)
Marco, let me know if you have any feedback on any of the three patches.
sure, sorry I had some issues keeping up with requests this week.
Depends on: 851454
Flags: needinfo?(mak77)
This patch adds the missing tests and should be ready for review.
Attachment #780890 - Attachment is obsolete: true
Attachment #780890 - Flags: feedback?(enndeakin)
Attachment #781083 - Flags: review?(enndeakin)
This completes the implementation of resuming support in DownloadLegacySaver,
and adds tests for downloads started through nsIExternalHelperAppService.
Attachment #780910 - Attachment is obsolete: true
Attachment #781228 - Flags: review?(enndeakin)
Comment on attachment 780859 [details] [diff] [review]
Part 1 of 3 - Unify DownloadLegacy tests in preparation for resuming support

The comments here and the promiseDownloadStopped method make what's happening here much clearer. Thanks!

The only change is that the comment below, the last comma should be removed, or the sentence reworded:

+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can hook its onchange callback, that will be notified when the
+ // download starts.

And in the next comment:

+ // is created, and it may already have notified all its property changes
+ // until now, thus there 

I can't quite parse this line. Do you mean 'may have already made all needed property change notifications'?
Attachment #780859 - Flags: review?(enndeakin) → review+
Comment on attachment 781083 [details] [diff] [review]
Part 2 of 3 - Add the ability to resume a download from where it stopped to DownloadCopySaver.

Review of attachment 781083 [details] [diff] [review]:
-----------------------------------------------------------------

::: toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ +417,5 @@
>      return this._promiseCanceled;
>    },
>  
>    /**
> +   * Indicates whether any partially downloaded data should be retained, to use

maybe "to be used"?

@@ +425,5 @@
> +   * download source, and may not be known before the download is started.
> +   *
> +   * To have any effect, this property must be set before starting the download.
> +   * Resetting this property to false after the download has already started
> +   * will not remove any partial data.

is that an expected use-case? otherwise you may replace the value with a getter once the download has started, to prevent setting it.

@@ +429,5 @@
> +   * will not remove any partial data.
> +   *
> +   * If this property is set to true, care should be taken that partial data is
> +   * removed before the reference to the download is discarded.  This can be
> +   * done using the removePartialData or the "finalize" methods.

either both should have apices, or none, there's nothing special about finalize.

@@ +444,5 @@
> +  /**
> +   * Removes any partial data kept as part of a canceled or failed download.
> +   *
> +   * If the download is not canceled or failed, this method has no effect, and
> +   * it returns a resolved promise.  If the "cancel" method was called but the

I wonder if that should be considered an error and rejected, like removing partial data of an in-progress download. Couldn't just resolving hide mistakes like that?

@@ +727,5 @@
>    } else if (aSerializable instanceof Ci.nsIURI) {
>      source.url = aSerializable.spec;
>    } else {
> +    // Convert String objects to primitive strings at this point.
> +    source.url = "" + aSerializable.url;

nit: it may be more polite to explicitly use .toString()

@@ +801,5 @@
>      target.path = aSerializable.path;
>    } else {
>      // Read the "path" property of the serializable DownloadTarget
> +    // representation, converting String objects to primitive strings.
> +    target.path = "" + aSerializable.path;

ditto

@@ +1008,5 @@
>     * Implements "DownloadSaver.execute".
>     */
>    execute: function DCS_execute(aSetProgressBytesFn)
>    {
> +    let self = this;

s/self/copySaver/ ?

::: toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ +96,5 @@
> + * saved to disk.  You must call "continueResponses" to allow the interruptible
> + * request to continue.
> + *
> + * TODO: This function uses either DownloadCopySaver or DownloadLegacySaver
> + * based on the current test run.

I'm not sure what's the todo part in that phrase.
I quickly skimmed through the patches and the approach and naming looks good to me, didn't go for a deep line-by-line review though.
Flags: needinfo?(mak77)
(In reply to Marco Bonardo [:mak] from comment #30)
> > +   * Indicates whether any partially downloaded data should be retained, to use
> 
> maybe "to be used"?

I think the grammar is correct per comment 19, right?

> @@ +425,5 @@
> > +   * download source, and may not be known before the download is started.
> > +   *
> > +   * To have any effect, this property must be set before starting the download.
> > +   * Resetting this property to false after the download has already started
> > +   * will not remove any partial data.
> 
> is that an expected use-case? otherwise you may replace the value with a
> getter once the download has started, to prevent setting it.

This gives control on whether to keep partial data when restarting downloads
originally started from DownloadLegacySaver, we have a test case for that.

> @@ +429,5 @@
> > +   * will not remove any partial data.
> > +   *
> > +   * If this property is set to true, care should be taken that partial data is
> > +   * removed before the reference to the download is discarded.  This can be
> > +   * done using the removePartialData or the "finalize" methods.
> 
> either both should have apices, or none, there's nothing special about
> finalize.

Well, finalizeHasNoInterCaps, this is why I've added quotes to mark it clearly
as an identifier. When translating to formatted text, identifiers become
monospace, but in the text-only version I preferred not to use "quotes" or
|pipes| where avoidable without ambiguity.

> @@ +444,5 @@
> > +  /**
> > +   * Removes any partial data kept as part of a canceled or failed download.
> > +   *
> > +   * If the download is not canceled or failed, this method has no effect, and
> > +   * it returns a resolved promise.  If the "cancel" method was called but the
> 
> I wonder if that should be considered an error and rejected, like removing
> partial data of an in-progress download. Couldn't just resolving hide
> mistakes like that?

The idea is that the download might have been restarted by another listener
while we are processing a completion notification, and this shouldn't result
in a possibly intermittent error condition that is visible to the user. This
is the same logic used by other methods on the download, it isn't an error if
calls arrive in a different order.

As long as we keep a consistent internal state, and properly notify changes in
hasPartialData and other properties to the views, whether the download still
has or hasn't performed the requested action because of a conflicting command
coming from another listener should be secondary.

> @@ +727,5 @@
> >    } else if (aSerializable instanceof Ci.nsIURI) {
> >      source.url = aSerializable.spec;
> >    } else {
> > +    // Convert String objects to primitive strings at this point.
> > +    source.url = "" + aSerializable.url;
> 
> nit: it may be more polite to explicitly use .toString()

I'll update the code.

> ::: toolkit/components/jsdownloads/test/unit/common_test_Download.js
> @@ +96,5 @@
> > + * saved to disk.  You must call "continueResponses" to allow the interruptible
> > + * request to continue.
> > + *
> > + * TODO: This function uses either DownloadCopySaver or DownloadLegacySaver
> > + * based on the current test run.
> 
> I'm not sure what's the todo part in that phrase.

In fact, it's implemented in the following patch.

(In reply to Marco Bonardo [:mak] from comment #31)
> I quickly skimmed through the patches and the approach and naming looks good
> to me, didn't go for a deep line-by-line review though.

Thanks a lot for the feedback!
Made changes from comment 29 and landed part 1 of 3 to fx-team:

https://hg.mozilla.org/integration/fx-team/rev/8c3ee4235ec1
Whiteboard: [leave open]
Target Milestone: --- → mozilla25
Attachment #780859 - Flags: checkin+
(In reply to Paolo Amadini [:paolo] from comment #33)
> Made changes from comment 29 and landed part 1 of 3 to fx-team:
> 
> https://hg.mozilla.org/integration/fx-team/rev/8c3ee4235ec1

https://hg.mozilla.org/mozilla-central/rev/8c3ee4235ec1
Blocks: 899102
Attachment #781083 - Flags: review?(enndeakin) → review+
Blocks: 899107
While working on this, I found that bug 899102 is affecting the reliability of
these tests. I think we should try to land this patch despite the issue, so that
we can build upon it, without blocking on bug 899102.

So, I've added the following workaround after every cancellation in the legacy
nsExternalHelperAppService tests:

  // This time-based solution is a workaround to avoid intermittent failures,
  // and will be removed when bug 899102 is resolved.
  if (gUseLegacySaver) {
    yield promiseTimeout(250);
  }
Comment on attachment 781228 [details] [diff] [review]
Part 3 of 3 - Add the ability to resume a download from where it stopped to DownloadLegacySaver.

>+          throw new Components.Exception("Asynchronous version implemented.",
>+                                         Cr.NS_ERROR_NOT_AVAILABLE);

This is a confusing error message. It would make more sense to say 'Synchronous promptForSaveToFile not implemented.'

>+  let legacyDownload = yield promiseStartLegacyDownload();
>+  yield legacyDownload.cancel();
>+  listForSave.add(legacyDownload);

Why do you cancel this download first, but not the others?
Attachment #781228 - Flags: review?(enndeakin) → review+
(In reply to Neil Deakin from comment #36)
> >+  let legacyDownload = yield promiseStartLegacyDownload();
> >+  yield legacyDownload.cancel();
> >+  listForSave.add(legacyDownload);
> 
> Why do you cancel this download first, but not the others?

We cannot create legacy downloads without starting them, while when we use the
other functions that create normal downloads (Downloads.createDownload and
promiseNewDownload) we can just avoid to start them.
This is the rebased version of the same patch.
Attachment #781083 - Attachment is obsolete: true
Attachment #784267 - Attachment description: Part 2 of 3 - Add the ability to resume a download from where it stopped to DownloadCopySaver.bug-836437-2.diff → Part 2 of 3 - Add the ability to resume a download from where it stopped to DownloadCopySaver.
I'm working on resolving some intermittent failures that appeared on Windows, I
hope this version of the patch will address them.
Attachment #781228 - Attachment is obsolete: true
Intermittent failures disappeared with this new version of the patch:

https://tbpl.mozilla.org/?tree=Try&rev=a6f34758d89a

If any other intermittent failure appears, it should be filed as a separate bug.
Attachment #784267 - Flags: checkin+
Attachment #784270 - Flags: checkin+
https://hg.mozilla.org/mozilla-central/rev/5913eecc1447
https://hg.mozilla.org/mozilla-central/rev/9b8995108081
Status: NEW → RESOLVED
Closed: 6 years ago
Flags: in-testsuite+
Resolution: --- → FIXED
Marking as [qa-] since there are unit tests for both pushlogs.
Whiteboard: [qa-]
You need to log in before you can comment on or make changes to this bug.