summaryrefslogtreecommitdiffstats
path: root/toolkit/components/bitsdownload
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/bitsdownload
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--toolkit/components/bitsdownload/Bits.cpp34
-rw-r--r--toolkit/components/bitsdownload/Bits.h9
-rw-r--r--toolkit/components/bitsdownload/Bits.jsm821
-rw-r--r--toolkit/components/bitsdownload/Cargo.toml16
-rw-r--r--toolkit/components/bitsdownload/bits_client/.gitignore3
-rw-r--r--toolkit/components/bitsdownload/bits_client/Cargo.toml19
-rw-r--r--toolkit/components/bitsdownload/bits_client/README.md23
-rw-r--r--toolkit/components/bitsdownload/bits_client/bits/Cargo.toml31
-rw-r--r--toolkit/components/bitsdownload/bits_client/bits/src/callback.rs205
-rw-r--r--toolkit/components/bitsdownload/bits_client/bits/src/lib.rs592
-rw-r--r--toolkit/components/bitsdownload/bits_client/bits/src/status.rs118
-rw-r--r--toolkit/components/bitsdownload/bits_client/bits/src/wide.rs38
-rw-r--r--toolkit/components/bitsdownload/bits_client/examples/test_client.rs285
-rw-r--r--toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs380
-rw-r--r--toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs504
-rw-r--r--toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs628
-rw-r--r--toolkit/components/bitsdownload/bits_client/src/lib.rs258
-rw-r--r--toolkit/components/bitsdownload/components.conf17
-rw-r--r--toolkit/components/bitsdownload/moz.build30
-rw-r--r--toolkit/components/bitsdownload/nsIBits.idl427
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/action.rs118
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/dispatch_callback.rs199
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/error.rs686
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/mod.rs329
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/monitor.rs247
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/request.rs763
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/string.rs80
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/task/client.rs102
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/task/from_threadbound.rs125
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/task/mod.rs18
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/task/request_task.rs425
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/task/service_task.rs332
-rw-r--r--toolkit/components/bitsdownload/src/bits_interface/xpcom_methods.rs195
-rw-r--r--toolkit/components/bitsdownload/src/lib.rs23
34 files changed, 8080 insertions, 0 deletions
diff --git a/toolkit/components/bitsdownload/Bits.cpp b/toolkit/components/bitsdownload/Bits.cpp
new file mode 100644
index 0000000000..da16e40497
--- /dev/null
+++ b/toolkit/components/bitsdownload/Bits.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "nsCOMPtr.h"
+#include "Bits.h"
+
+// This anonymous namespace prevents outside C++ code from improperly accessing
+// these implementation details.
+namespace {
+extern "C" {
+// Implemented in Rust.
+void new_bits_service(nsIBits** result);
+}
+
+static mozilla::StaticRefPtr<nsIBits> sBitsService;
+} // namespace
+
+already_AddRefed<nsIBits> GetBitsService() {
+ nsCOMPtr<nsIBits> bitsService;
+
+ if (sBitsService) {
+ bitsService = sBitsService;
+ } else {
+ new_bits_service(getter_AddRefs(bitsService));
+ sBitsService = bitsService;
+ mozilla::ClearOnShutdown(&sBitsService);
+ }
+
+ return bitsService.forget();
+}
diff --git a/toolkit/components/bitsdownload/Bits.h b/toolkit/components/bitsdownload/Bits.h
new file mode 100644
index 0000000000..1c1e7ea996
--- /dev/null
+++ b/toolkit/components/bitsdownload/Bits.h
@@ -0,0 +1,9 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIBits.h"
+
+already_AddRefed<nsIBits> GetBitsService();
diff --git a/toolkit/components/bitsdownload/Bits.jsm b/toolkit/components/bitsdownload/Bits.jsm
new file mode 100644
index 0000000000..cc6ccedcec
--- /dev/null
+++ b/toolkit/components/bitsdownload/Bits.jsm
@@ -0,0 +1,821 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module is used to interact with the Windows BITS component (Background
+ * Intelligent Transfer Service). This functionality cannot be used unless on
+ * Windows.
+ *
+ * The reason for this file's existence is that the interfaces in nsIBits.idl
+ * are asynchronous, but are unable to use Promises because they are implemented
+ * in Rust, which does not yet support Promises. This file functions as a layer
+ * between the Rust and the JS that provides access to the functionality
+ * provided by nsIBits via Promises rather than callbacks.
+ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+// This conditional prevents errors if this file is imported from operating
+// systems other than Windows. This is purely for convenient importing, because
+// attempting to use anything in this file on platforms other than Windows will
+// result in an error.
+if (AppConstants.MOZ_BITS_DOWNLOAD) {
+ XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gBits",
+ "@mozilla.org/bits;1",
+ "nsIBits"
+ );
+}
+
+// This value exists to mitigate a very unlikely problem: If a BITS method
+// catastrophically fails, it may never call its callback. This would result in
+// methods in this file returning promises that never resolve. This could, in
+// turn, lead to download code hanging altogether rather than being able to
+// report errors and utilize fallback mechanisms.
+// This problem is mitigated by giving these promises a timeout, the length of
+// which will be determined by this value.
+const kBitsMethodTimeoutMs = 10 * 60 * 1000; // 10 minutes
+
+/**
+ * This class will wrap the errors returned by the nsIBits interface to make
+ * them more uniform and more easily consumable.
+ *
+ * The values of stored by this error type are entirely numeric. This should
+ * make them easier to consume with JS and telemetry, but does make them fairly
+ * unreadable. nsIBits.idl will need to be referenced to look up what errors
+ * the values correspond to.
+ *
+ * The type of BitsError.code is dependent on the value of BitsError.codeType.
+ * It may be null, a number (corresponding to an nsresult or hresult value),
+ * a string, or an exception.
+ */
+class BitsError extends Error {
+ // If codeType == "none", code may be unspecified.
+ constructor(type, action, stage, codeType, code) {
+ let message =
+ `${BitsError.name} {type: ${type}, action: ${action}, ` +
+ `stage: ${stage}`;
+ switch (codeType) {
+ case lazy.gBits.ERROR_CODE_TYPE_NONE:
+ code = null;
+ message += ", codeType: none}";
+ break;
+ case lazy.gBits.ERROR_CODE_TYPE_NSRESULT:
+ message += `, codeType: nsresult, code: ${code}}`;
+ break;
+ case lazy.gBits.ERROR_CODE_TYPE_HRESULT:
+ message += `, codeType: hresult, code: ${code}}`;
+ break;
+ case lazy.gBits.ERROR_CODE_TYPE_STRING:
+ message += `, codeType: string, code: ${JSON.stringify(code)}}`;
+ break;
+ case lazy.gBits.ERROR_CODE_TYPE_EXCEPTION:
+ message += `, codeType: exception, code: ${code}}`;
+ break;
+ default:
+ message += ", codeType: invalid}";
+ break;
+ }
+ super(message);
+
+ this.type = type;
+ this.action = action;
+ this.stage = stage;
+ this.codeType = codeType;
+ this.code = code;
+ this.name = this.constructor.name;
+ this.succeeded = false;
+ }
+}
+
+// These specializations exist to make them easier to construct since they may
+// need to be constructed outside of this file.
+class BitsVerificationError extends BitsError {
+ constructor() {
+ super(
+ Ci.nsIBits.ERROR_TYPE_VERIFICATION_FAILURE,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_VERIFICATION,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+}
+class BitsUnknownError extends BitsError {
+ constructor() {
+ super(
+ Ci.nsIBits.ERROR_TYPE_UNKNOWN,
+ Ci.nsIBits.ERROR_ACTION_UNKNOWN,
+ Ci.nsIBits.ERROR_STAGE_UNKNOWN,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+}
+
+/**
+ * Returns a timer object. If the timer expires, reject will be called with
+ * a BitsError error. The timer's cancel method should be called if the promise
+ * resolves or rejects without the timeout expiring.
+ */
+function makeTimeout(reject, errorAction) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ () => {
+ let error = new BitsError(
+ lazy.gBits.ERROR_TYPE_METHOD_TIMEOUT,
+ errorAction,
+ lazy.gBits.ERROR_STAGE_UNKNOWN,
+ lazy.gBits.ERROR_CODE_TYPE_NONE
+ );
+ reject(error);
+ },
+ kBitsMethodTimeoutMs,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return timer;
+}
+
+/**
+ * This function does all of the wrapping and error handling for an async
+ * BitsRequest method. This allows the implementations for those methods to
+ * simply call this function with a closure that executes appropriate
+ * nsIBitsRequest method.
+ *
+ * Specifically, this function takes an nsBitsErrorAction and a function.
+ * The nsBitsErrorAction will be used when constructing a BitsError, if the
+ * wrapper encounters an error.
+ * The function will be passed the callback function that should be passed to
+ * the nsIBitsRequest method.
+ */
+async function requestPromise(errorAction, actionFn) {
+ return new Promise((resolve, reject) => {
+ let timer = makeTimeout(reject, errorAction);
+
+ let callback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBitsCallback"]),
+ success() {
+ timer.cancel();
+ resolve();
+ },
+ failure(type, action, stage) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_NONE
+ );
+ reject(error);
+ },
+ failureNsresult(type, action, stage, code) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_NSRESULT,
+ code
+ );
+ reject(error);
+ },
+ failureHresult(type, action, stage, code) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_HRESULT,
+ code
+ );
+ reject(error);
+ },
+ failureString(type, action, stage, message) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_STRING,
+ message
+ );
+ reject(error);
+ },
+ };
+
+ try {
+ actionFn(callback);
+ } catch (e) {
+ let error = new BitsError(
+ lazy.gBits.ERROR_TYPE_METHOD_THREW,
+ errorAction,
+ lazy.gBits.ERROR_STAGE_PRETASK,
+ lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
+ e
+ );
+ reject(error);
+ }
+ });
+}
+
+/**
+ * This class is a wrapper around nsIBitsRequest that converts functions taking
+ * callbacks to asynchronous functions. This class implements nsIRequest.
+ *
+ * Note that once the request has been shutdown, calling methods on it will
+ * cause an exception to be thrown. The request will be shutdown automatically
+ * when the BITS job is successfully completed or cancelled. If the request is
+ * no longer needed, but the transfer is still in progress, the shutdown method
+ * should be called manually to prevent memory leaks.
+ * Getter methods (except loadGroup and loadFlags) should continue to be
+ * accessible, even after shutdown.
+ */
+class BitsRequest {
+ constructor(request) {
+ this._request = request;
+ this._request.QueryInterface(Ci.nsIBitsRequest);
+ }
+
+ /**
+ * This function releases the Rust request that backs this wrapper. Calling
+ * any method on this request after calling release will result in a BitsError
+ * being thrown.
+ *
+ * This step is important, because otherwise a cycle exists that the cycle
+ * collector doesn't know about. To break this cycle, either the Rust request
+ * needs to let go of the observer, or the JS request wrapper needs to let go
+ * of the Rust request (which is what we do here).
+ *
+ * Once there is support for cycle collection of cycles that extend through
+ * Rust, this function may no longer be necessary.
+ */
+ shutdown() {
+ if (this.hasShutdown) {
+ return;
+ }
+ // Cache some values before we shut down so they are still available
+ this._name = this._request.name;
+ this._status = this._request.status;
+ this._bitsId = this._request.bitsId;
+ this._transferError = this._request.transferError;
+
+ this._request = null;
+ }
+
+ /**
+ * Allows consumers to determine if this request has been shutdown.
+ */
+ get hasShutdown() {
+ return !this._request;
+ }
+
+ /**
+ * This is the nsIRequest implementation. Since this._request is an
+ * nsIRequest, these functions just call the corresponding method on it.
+ *
+ * Note that nsIBitsRequest does not yet properly implement load groups or
+ * load flags. This class will still forward those calls, but they will have
+ * not succeed.
+ */
+ get name() {
+ if (!this._request) {
+ return this._name;
+ }
+ return this._request.name;
+ }
+ isPending() {
+ if (!this._request) {
+ return false;
+ }
+ return this._request.isPending();
+ }
+ get status() {
+ if (!this._request) {
+ return this._status;
+ }
+ return this._request.status;
+ }
+ cancel(status) {
+ return this.cancelAsync(status);
+ }
+ suspend() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_SUSPEND,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ return this._request.suspend();
+ }
+ resume() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_RESUME,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ return this._request.resume();
+ }
+ get loadGroup() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ return this._request.loadGroup;
+ }
+ set loadGroup(group) {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ this._request.loadGroup = group;
+ }
+ get loadFlags() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ return this._request.loadFlags;
+ }
+ set loadFlags(flags) {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ this._request.loadFlags = flags;
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::bitsId.
+ */
+ get bitsId() {
+ if (!this._request) {
+ return this._bitsId;
+ }
+ return this._request.bitsId;
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::transferError.
+ *
+ * Instead of simply returning the nsBitsErrorType value, however, it returns
+ * a BitsError object, or null.
+ */
+ get transferError() {
+ let result;
+ if (this._request) {
+ result = this._request.transferError;
+ } else {
+ result = this._transferError;
+ }
+ if (result == Ci.nsIBits.ERROR_TYPE_SUCCESS) {
+ return null;
+ }
+ return new BitsError(
+ result,
+ Ci.nsIBits.ERROR_ACTION_NONE,
+ Ci.nsIBits.ERROR_STAGE_MONITOR,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::changeMonitorInterval.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async changeMonitorInterval(monitorIntervalMs) {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL;
+ return requestPromise(action, callback => {
+ this._request.changeMonitorInterval(monitorIntervalMs, callback);
+ });
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::cancelAsync.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ *
+ * Adds a default status of NS_ERROR_ABORT if one is not provided.
+ */
+ async cancelAsync(status) {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_CANCEL,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ if (status === undefined) {
+ status = Cr.NS_ERROR_ABORT;
+ }
+ let action = lazy.gBits.ERROR_ACTION_CANCEL;
+ return requestPromise(action, callback => {
+ this._request.cancelAsync(status, callback);
+ }).then(() => this.shutdown());
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::setPriorityHigh.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async setPriorityHigh() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_SET_PRIORITY,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY;
+ return requestPromise(action, callback => {
+ this._request.setPriorityHigh(callback);
+ });
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::setPriorityLow.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async setPriorityLow() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_SET_PRIORITY,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY;
+ return requestPromise(action, callback => {
+ this._request.setPriorityLow(callback);
+ });
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::setNoProgressTimeout.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async setNoProgressTimeout(timeoutSecs) {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT;
+ return requestPromise(action, callback => {
+ this._request.setNoProgressTimeout(timeoutSecs, callback);
+ });
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::complete.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async complete() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_COMPLETE,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_COMPLETE;
+ return requestPromise(action, callback => {
+ this._request.complete(callback);
+ }).then(() => this.shutdown());
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::suspendAsync.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async suspendAsync() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_SUSPEND,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_SUSPEND;
+ return requestPromise(action, callback => {
+ this._request.suspendAsync(callback);
+ });
+ }
+
+ /**
+ * This function wraps nsIBitsRequest::resumeAsync.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with no data, or rejects with a BitsError.
+ */
+ async resumeAsync() {
+ if (!this._request) {
+ throw new BitsError(
+ Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
+ Ci.nsIBits.ERROR_ACTION_RESUME,
+ Ci.nsIBits.ERROR_STAGE_PRETASK,
+ Ci.nsIBits.ERROR_CODE_TYPE_NONE
+ );
+ }
+ let action = lazy.gBits.ERROR_ACTION_RESUME;
+ return requestPromise(action, callback => {
+ this._request.resumeAsync(callback);
+ });
+ }
+}
+BitsRequest.prototype.QueryInterface = ChromeUtils.generateQI(["nsIRequest"]);
+
+/**
+ * This function does all of the wrapping and error handling for an async
+ * Bits Service method. This allows the implementations for those methods to
+ * simply call this function with a closure that executes appropriate
+ * nsIBits method.
+ *
+ * Specifically, this function takes an nsBitsErrorAction, an observer and a
+ * function.
+ * The nsBitsErrorAction will be used when constructing a BitsError, if the
+ * wrapper encounters an error.
+ * The observer should be the one that the caller passed to the Bits Interface
+ * method. It will be wrapped so that its methods are passed a BitsRequest
+ * rather than an nsIBitsRequest.
+ * The function will be passed the callback function and the wrapped observer,
+ * both of which should be passed to the nsIBitsRequest method.
+ */
+async function servicePromise(errorAction, observer, actionFn) {
+ return new Promise((resolve, reject) => {
+ if (!observer) {
+ let error = new BitsError(
+ lazy.gBits.ERROR_TYPE_NULL_ARGUMENT,
+ errorAction,
+ lazy.gBits.ERROR_STAGE_PRETASK,
+ lazy.gBits.ERROR_CODE_TYPE_NONE
+ );
+ reject(error);
+ return;
+ }
+ try {
+ observer.QueryInterface(Ci.nsIRequestObserver);
+ } catch (e) {
+ let error = new BitsError(
+ lazy.gBits.ERROR_TYPE_INVALID_ARGUMENT,
+ errorAction,
+ lazy.gBits.ERROR_STAGE_PRETASK,
+ lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
+ e
+ );
+ reject(error);
+ return;
+ }
+ let isProgressEventSink = false;
+ try {
+ observer.QueryInterface(Ci.nsIProgressEventSink);
+ isProgressEventSink = true;
+ } catch (e) {}
+
+ // This will be set to the BitsRequest (wrapping the nsIBitsRequest), once
+ // it is available. This prevents a new wrapper from having to be made every
+ // time an observer function is called.
+ let wrappedRequest;
+
+ let wrappedObserver = {
+ onStartRequest: function wrappedObserver_onStartRequest(request) {
+ if (!wrappedRequest) {
+ wrappedRequest = new BitsRequest(request);
+ }
+ observer.onStartRequest(wrappedRequest);
+ },
+ onStopRequest: function wrappedObserver_onStopRequest(request, status) {
+ if (!wrappedRequest) {
+ wrappedRequest = new BitsRequest(request);
+ }
+ observer.onStopRequest(wrappedRequest, status);
+ },
+ onProgress: function wrappedObserver_onProgress(
+ request,
+ progress,
+ progressMax
+ ) {
+ if (isProgressEventSink) {
+ if (!wrappedRequest) {
+ wrappedRequest = new BitsRequest(request);
+ }
+ observer.onProgress(wrappedRequest, progress, progressMax);
+ }
+ },
+ onStatus: function wrappedObserver_onStatus(request, status, statusArg) {
+ if (isProgressEventSink) {
+ if (!wrappedRequest) {
+ wrappedRequest = new BitsRequest(request);
+ }
+ observer.onStatus(wrappedRequest, status, statusArg);
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIRequestObserver",
+ "nsIProgressEventSink",
+ ]),
+ };
+
+ let timer = makeTimeout(reject, errorAction);
+ let callback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBitsNewRequestCallback"]),
+ success(request) {
+ timer.cancel();
+ if (!wrappedRequest) {
+ wrappedRequest = new BitsRequest(request);
+ }
+ resolve(wrappedRequest);
+ },
+ failure(type, action, stage) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_NONE
+ );
+ reject(error);
+ },
+ failureNsresult(type, action, stage, code) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_NSRESULT,
+ code
+ );
+ reject(error);
+ },
+ failureHresult(type, action, stage, code) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_HRESULT,
+ code
+ );
+ reject(error);
+ },
+ failureString(type, action, stage, message) {
+ timer.cancel();
+ let error = new BitsError(
+ type,
+ action,
+ stage,
+ lazy.gBits.ERROR_CODE_TYPE_STRING,
+ message
+ );
+ reject(error);
+ },
+ };
+
+ try {
+ actionFn(wrappedObserver, callback);
+ } catch (e) {
+ let error = new BitsError(
+ lazy.gBits.ERROR_TYPE_METHOD_THREW,
+ errorAction,
+ lazy.gBits.ERROR_STAGE_PRETASK,
+ lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
+ e
+ );
+ reject(error);
+ }
+ });
+}
+
+var Bits = {
+ /**
+ * This function wraps nsIBits::initialized.
+ */
+ get initialized() {
+ return lazy.gBits.initialized;
+ },
+
+ /**
+ * This function wraps nsIBits::init.
+ */
+ init(jobName, savePathPrefix, monitorTimeoutMs) {
+ return lazy.gBits.init(jobName, savePathPrefix, monitorTimeoutMs);
+ },
+
+ /**
+ * This function wraps nsIBits::startDownload.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with a BitsRequest (which is also an
+ * nsIRequest), or rejects with a BitsError.
+ */
+ async startDownload(
+ downloadURL,
+ saveRelPath,
+ proxy,
+ noProgressTimeoutSecs,
+ monitorIntervalMs,
+ observer,
+ context
+ ) {
+ let action = lazy.gBits.ERROR_ACTION_START_DOWNLOAD;
+ return servicePromise(action, observer, (wrappedObserver, callback) => {
+ lazy.gBits.startDownload(
+ downloadURL,
+ saveRelPath,
+ proxy,
+ noProgressTimeoutSecs,
+ monitorIntervalMs,
+ wrappedObserver,
+ context,
+ callback
+ );
+ });
+ },
+
+ /**
+ * This function wraps nsIBits::monitorDownload.
+ *
+ * Instead of taking a callback, the function is asynchronous.
+ * This method either resolves with a BitsRequest (which is also an
+ * nsIRequest), or rejects with a BitsError.
+ */
+ async monitorDownload(id, monitorIntervalMs, observer, context) {
+ let action = lazy.gBits.ERROR_ACTION_MONITOR_DOWNLOAD;
+ return servicePromise(action, observer, (wrappedObserver, callback) => {
+ lazy.gBits.monitorDownload(
+ id,
+ monitorIntervalMs,
+ wrappedObserver,
+ context,
+ callback
+ );
+ });
+ },
+};
+
+const EXPORTED_SYMBOLS = [
+ "Bits",
+ "BitsError",
+ "BitsRequest",
+ "BitsSuccess",
+ "BitsUnknownError",
+ "BitsVerificationError",
+];
diff --git a/toolkit/components/bitsdownload/Cargo.toml b/toolkit/components/bitsdownload/Cargo.toml
new file mode 100644
index 0000000000..7aff42359e
--- /dev/null
+++ b/toolkit/components/bitsdownload/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "bitsdownload"
+version = "0.1.0"
+authors = ["Kirk Steuber <bytesized@mozilla.com>"]
+license = "MPL-2.0"
+
+[dependencies]
+bits_client = { path = "./bits_client" }
+comedy = "0.2.0"
+crossbeam-utils = "0.8"
+libc = "0.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
diff --git a/toolkit/components/bitsdownload/bits_client/.gitignore b/toolkit/components/bitsdownload/bits_client/.gitignore
new file mode 100644
index 0000000000..d78faf4575
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+**/.*.swp
diff --git a/toolkit/components/bitsdownload/bits_client/Cargo.toml b/toolkit/components/bitsdownload/bits_client/Cargo.toml
new file mode 100644
index 0000000000..eb0083d8e2
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "bits_client"
+version = "0.2.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+license = "MPL-2.0"
+publish = false
+
+[dependencies]
+bits = { path = "./bits" }
+comedy = "0.2.0"
+guid_win = "0.2.0"
+thiserror = "1"
+
+[dev-dependencies]
+#ctrlc = "3.1.1"
+lazy_static = "1.0.1"
+rand = "0.8"
+regex = { version = "1", default_features = false, features = ["perf", "std"] }
+tempfile = "3"
diff --git a/toolkit/components/bitsdownload/bits_client/README.md b/toolkit/components/bitsdownload/bits_client/README.md
new file mode 100644
index 0000000000..0c21a5b68c
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/README.md
@@ -0,0 +1,23 @@
+bits\_client
+============
+
+Interfaces for BITS.
+
+bits\_client lib
+---------------
+
+`bits_client` is the primary target and provides `BitsClient`, an API for creating and monitoring BITS jobs.
+
+`bits_client::new()` creates a `BitsClient` that does all operations within the current process, as the current user.
+
+bits crate
+----------
+
+`bits` is a safe interface to BITS, providing connections to the
+Background Copy Manager, some basic operations on Background Copy Jobs, and
+methods for implementing `IBackgroundCopyCallback`s in Rust.
+
+test\_client example
+-------------------
+
+`examples/test_client.rs` shows how to use the API.
diff --git a/toolkit/components/bitsdownload/bits_client/bits/Cargo.toml b/toolkit/components/bitsdownload/bits_client/bits/Cargo.toml
new file mode 100644
index 0000000000..36b7b5ff89
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "bits"
+version = "0.2.0"
+authors = ["Adam Gashlin <agashlin@mozilla.com>"]
+license = "MIT/Apache-2.0"
+publish = false
+
+[features]
+status_serde = ["serde", "serde_derive"]
+
+[dependencies]
+comedy = "0.2.0"
+filetime_win = "0.2.0"
+guid_win = "0.2.0"
+serde = { version = "1.0.80", optional = true }
+serde_derive = { version = "1.0.80", optional = true }
+
+[dependencies.winapi]
+version = "0.3.7"
+features = ["basetsd",
+ "bits",
+ "bits2_5",
+ "bitsmsg",
+ "guiddef",
+ "minwindef",
+ "ntdef",
+ "rpcndr",
+ "unknwnbase",
+ "winerror",
+ "winnls",
+ ]
diff --git a/toolkit/components/bitsdownload/bits_client/bits/src/callback.rs b/toolkit/components/bitsdownload/bits_client/bits/src/callback.rs
new file mode 100644
index 0000000000..6dace83be8
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/callback.rs
@@ -0,0 +1,205 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+use std::panic::{catch_unwind, RefUnwindSafe};
+use std::ptr::NonNull;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use comedy::{com::ComRef, HResult};
+use guid_win::Guid;
+use winapi::ctypes::c_void;
+use winapi::shared::guiddef::REFIID;
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::ntdef::ULONG;
+use winapi::shared::winerror::{E_FAIL, E_NOINTERFACE, HRESULT, NOERROR, S_OK};
+use winapi::um::bits::{
+ IBackgroundCopyCallback, IBackgroundCopyCallbackVtbl, IBackgroundCopyError, IBackgroundCopyJob,
+};
+use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl};
+use winapi::Interface;
+
+use BitsJob;
+
+/// The type of a notification callback.
+///
+/// The callbacks must be `Fn()` to be called arbitrarily many times, `RefUnwindSafe` to have a
+/// panic unwind safely caught, `Send`, `Sync` and `'static` to run on any thread COM invokes us on
+/// any time.
+///
+/// If the callback returns a non-success `HRESULT`, the notification may pass to other BITS
+/// mechanisms such as `IBackgroundCopyJob2::SetNotifyCmdLine`.
+pub type TransferredCallback =
+ dyn (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+pub type ErrorCallback = dyn (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+pub type ModificationCallback =
+ dyn (Fn() -> Result<(), HRESULT>) + RefUnwindSafe + Send + Sync + 'static;
+
+#[repr(C)]
+pub struct BackgroundCopyCallback {
+ // Everything assumes that the interface vtable is the first member of this struct.
+ interface: IBackgroundCopyCallback,
+ rc: AtomicUsize,
+ transferred_cb: Option<Box<TransferredCallback>>,
+ error_cb: Option<Box<ErrorCallback>>,
+ modification_cb: Option<Box<ModificationCallback>>,
+}
+
+impl BackgroundCopyCallback {
+ /// Construct the callback object and register it with a job.
+ ///
+ /// Only one notify interface can be present on a job at once, so this will release BITS'
+ /// ref to any previously registered interface.
+ pub fn register(
+ job: &mut BitsJob,
+ transferred_cb: Option<Box<TransferredCallback>>,
+ error_cb: Option<Box<ErrorCallback>>,
+ modification_cb: Option<Box<ModificationCallback>>,
+ ) -> Result<(), HResult> {
+ let cb = Box::new(BackgroundCopyCallback {
+ interface: IBackgroundCopyCallback { lpVtbl: &VTBL },
+ rc: AtomicUsize::new(1),
+ transferred_cb,
+ error_cb,
+ modification_cb,
+ });
+
+ // Leak the callback, it has no Rust owner until we need to drop it later.
+ // The ComRef will Release when it goes out of scope.
+ unsafe {
+ let cb = ComRef::from_raw(NonNull::new_unchecked(Box::into_raw(cb) as *mut IUnknown));
+
+ job.set_notify_interface(cb.as_raw_ptr())?;
+ }
+
+ Ok(())
+ }
+}
+
+extern "system" fn query_interface(
+ this: *mut IUnknown,
+ riid: REFIID,
+ obj: *mut *mut c_void,
+) -> HRESULT {
+ unsafe {
+ // `IBackgroundCopyCallback` is the first (currently only) interface on the
+ // `BackgroundCopyCallback` object, so we can return `this` either as
+ // `IUnknown` or `IBackgroundCopyCallback`.
+ if Guid(*riid) == Guid(IUnknown::uuidof())
+ || Guid(*riid) == Guid(IBackgroundCopyCallback::uuidof())
+ {
+ addref(this);
+ // Cast first to `IBackgroundCopyCallback` to be clear which `IUnknown`
+ // we are pointing at.
+ *obj = this as *mut IBackgroundCopyCallback as *mut c_void;
+ NOERROR
+ } else {
+ E_NOINTERFACE
+ }
+ }
+}
+
+extern "system" fn addref(raw_this: *mut IUnknown) -> ULONG {
+ unsafe {
+ let this = raw_this as *const BackgroundCopyCallback;
+
+ // Forge a reference for just this statement.
+ let old_rc = (*this).rc.fetch_add(1, Ordering::SeqCst);
+ (old_rc + 1) as ULONG
+ }
+}
+
+extern "system" fn release(raw_this: *mut IUnknown) -> ULONG {
+ unsafe {
+ {
+ let this = raw_this as *const BackgroundCopyCallback;
+
+ // Forge a reference for just this statement.
+ let old_rc = (*this).rc.fetch_sub(1, Ordering::SeqCst);
+
+ let rc = old_rc - 1;
+ if rc > 0 {
+ return rc as ULONG;
+ }
+ }
+
+ // rc will have been 0 for us to get here, and we're out of scope of the reference above,
+ // so there should be no references or pointers left (besides `this`).
+ // Re-Box and to drop immediately.
+ let _ = Box::from_raw(raw_this as *mut BackgroundCopyCallback);
+
+ 0
+ }
+}
+
+extern "system" fn transferred_stub(
+ raw_this: *mut IBackgroundCopyCallback,
+ _job: *mut IBackgroundCopyJob,
+) -> HRESULT {
+ unsafe {
+ let this = raw_this as *const BackgroundCopyCallback;
+ // Forge a reference just for this statement.
+ if let Some(ref cb) = (*this).transferred_cb {
+ match catch_unwind(|| cb()) {
+ Ok(Ok(())) => S_OK,
+ Ok(Err(hr)) => hr,
+ Err(_) => E_FAIL,
+ }
+ } else {
+ S_OK
+ }
+ }
+}
+
+extern "system" fn error_stub(
+ raw_this: *mut IBackgroundCopyCallback,
+ _job: *mut IBackgroundCopyJob,
+ _error: *mut IBackgroundCopyError,
+) -> HRESULT {
+ unsafe {
+ let this = raw_this as *const BackgroundCopyCallback;
+ // Forge a reference just for this statement.
+ if let Some(ref cb) = (*this).error_cb {
+ match catch_unwind(|| cb()) {
+ Ok(Ok(())) => S_OK,
+ Ok(Err(hr)) => hr,
+ Err(_) => E_FAIL,
+ }
+ } else {
+ S_OK
+ }
+ }
+}
+
+extern "system" fn modification_stub(
+ raw_this: *mut IBackgroundCopyCallback,
+ _job: *mut IBackgroundCopyJob,
+ _reserved: DWORD,
+) -> HRESULT {
+ unsafe {
+ let this = raw_this as *const BackgroundCopyCallback;
+ // Forge a reference just for this statement.
+ if let Some(ref cb) = (*this).modification_cb {
+ match catch_unwind(|| cb()) {
+ Ok(Ok(())) => S_OK,
+ Ok(Err(hr)) => hr,
+ Err(_) => E_FAIL,
+ }
+ } else {
+ S_OK
+ }
+ }
+}
+
+pub static VTBL: IBackgroundCopyCallbackVtbl = IBackgroundCopyCallbackVtbl {
+ parent: IUnknownVtbl {
+ QueryInterface: query_interface,
+ AddRef: addref,
+ Release: release,
+ },
+ JobTransferred: transferred_stub,
+ JobError: error_stub,
+ JobModification: modification_stub,
+};
diff --git a/toolkit/components/bitsdownload/bits_client/bits/src/lib.rs b/toolkit/components/bitsdownload/bits_client/bits/src/lib.rs
new file mode 100644
index 0000000000..09da2b0349
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/lib.rs
@@ -0,0 +1,592 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! A safe interface for BITS
+//!
+//! The primary entry point into BITS is the
+//! [`BackgroundCopyManager`](struct.BackgroundCopyManager.html) struct.
+//!
+//! Functionality is only provided by this crate on an as-needed basis for
+//! [bits_client](../bits_client/index.html), so there are vast swathes of the BITS API
+//! unsupported.
+
+extern crate comedy;
+extern crate filetime_win;
+extern crate guid_win;
+extern crate winapi;
+
+#[cfg(feature = "status_serde")]
+extern crate serde;
+#[cfg(feature = "status_serde")]
+extern crate serde_derive;
+
+mod callback;
+pub mod status;
+mod wide;
+
+use std::ffi::{OsStr, OsString};
+use std::mem;
+use std::os::windows::ffi::OsStringExt;
+use std::ptr;
+use std::result;
+
+use comedy::com::{create_instance_local_server, CoTaskMem, ComRef, INIT_MTA};
+use comedy::error::{HResult, ResultExt};
+use comedy::{com_call, com_call_getter, com_call_taskmem_getter};
+use filetime_win::FileTime;
+use guid_win::Guid;
+use winapi::shared::minwindef::DWORD;
+use winapi::shared::ntdef::{HRESULT, LANGIDFROMLCID, ULONG};
+use winapi::shared::winerror::S_FALSE;
+use winapi::um::bits::{
+ IBackgroundCopyError, IBackgroundCopyFile, IBackgroundCopyJob, IBackgroundCopyManager,
+ IEnumBackgroundCopyFiles, IEnumBackgroundCopyJobs, BG_JOB_PRIORITY, BG_JOB_PRIORITY_FOREGROUND,
+ BG_JOB_PRIORITY_HIGH, BG_JOB_PRIORITY_LOW, BG_JOB_PRIORITY_NORMAL, BG_JOB_PROXY_USAGE,
+ BG_JOB_PROXY_USAGE_AUTODETECT, BG_JOB_PROXY_USAGE_NO_PROXY, BG_JOB_PROXY_USAGE_PRECONFIG,
+ BG_JOB_STATE_ERROR, BG_JOB_STATE_TRANSIENT_ERROR, BG_JOB_TYPE_DOWNLOAD, BG_NOTIFY_DISABLE,
+ BG_NOTIFY_JOB_ERROR, BG_NOTIFY_JOB_MODIFICATION, BG_NOTIFY_JOB_TRANSFERRED, BG_SIZE_UNKNOWN,
+};
+use winapi::um::bits2_5::{IBackgroundCopyJobHttpOptions, BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT};
+use winapi::um::bitsmsg::BG_E_NOT_FOUND;
+use winapi::um::unknwnbase::IUnknown;
+use winapi::um::winnls::GetThreadLocale;
+
+pub use winapi::um::bits::{BG_ERROR_CONTEXT, BG_JOB_STATE};
+pub use winapi::um::bitsmsg::{BG_S_PARTIAL_COMPLETE, BG_S_UNABLE_TO_DELETE_FILES};
+
+pub use status::{
+ BitsErrorContext, BitsJobError, BitsJobProgress, BitsJobState, BitsJobStatus, BitsJobTimes,
+};
+use wide::ToWideNull;
+
+pub use winapi::shared::winerror::E_FAIL;
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug)]
+pub enum BitsJobPriority {
+ Foreground = BG_JOB_PRIORITY_FOREGROUND,
+ High = BG_JOB_PRIORITY_HIGH,
+ /// Default
+ Normal = BG_JOB_PRIORITY_NORMAL,
+ Low = BG_JOB_PRIORITY_LOW,
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug)]
+pub enum BitsProxyUsage {
+ /// Directly access the network.
+ NoProxy = BG_JOB_PROXY_USAGE_NO_PROXY,
+ /// Use Internet Explorer proxy settings. This is the default.
+ Preconfig = BG_JOB_PROXY_USAGE_PRECONFIG,
+ /// Attempt to auto-detect the connection's proxy settings.
+ AutoDetect = BG_JOB_PROXY_USAGE_AUTODETECT,
+}
+
+type Result<T> = result::Result<T, HResult>;
+
+pub struct BackgroundCopyManager(ComRef<IBackgroundCopyManager>);
+
+impl BackgroundCopyManager {
+ /// Get access to the local BITS service.
+ ///
+ /// # COM Initialization and Threading Model #
+ ///
+ /// This method uses a thread local variable to initialize COM with a multithreaded apartment
+ /// model for this thread, and leaves it this way until the thread local is dropped.
+ /// If the thread was in a single-threaded apartment, `connect()` will fail gracefully.
+ ///
+ /// # Safety #
+ ///
+ /// If there are mismatched `CoUninitialize` calls on this thread which lead to COM shutting
+ /// down before this thread ends, unsafe behavior may result.
+ pub fn connect() -> Result<BackgroundCopyManager> {
+ INIT_MTA.with(|com| {
+ if let Err(e) = com {
+ return Err(e.clone());
+ }
+ Ok(())
+ })?;
+
+ // Assuming no mismatched CoUninitialize calls, methods do not have to check for
+ // successfully initialized COM once the object is constructed: `BackgroundCopyManager`
+ // is not `Send` or `Sync` so it must be used on the thread it was constructed on,
+ // which has now successfully inited MTA for the lifetime of thread local `INIT_MTA`.
+ // This also holds for any functions using pointers only derived from these methods, like
+ // the `BitsJob` methods.
+
+ Ok(BackgroundCopyManager(create_instance_local_server::<
+ winapi::um::bits::BackgroundCopyManager,
+ IBackgroundCopyManager,
+ >()?))
+ }
+
+ /// Create a new download job with the given name.
+ pub fn create_job(&self, display_name: &OsStr) -> Result<BitsJob> {
+ unsafe {
+ let mut guid = mem::zeroed();
+ Ok(BitsJob(com_call_getter!(
+ |job| self.0,
+ IBackgroundCopyManager::CreateJob(
+ display_name.to_wide_null().as_ptr(),
+ BG_JOB_TYPE_DOWNLOAD,
+ &mut guid,
+ job,
+ )
+ )?))
+ }
+ }
+
+ /// Cancel all jobs with the given name.
+ ///
+ /// This only attempts to cancel jobs owned by the current user.
+ /// No errors are returned for jobs that failed to cancel.
+ pub fn cancel_jobs_by_name(&self, match_name: &OsStr) -> Result<()> {
+ let jobs =
+ unsafe { com_call_getter!(|jobs| self.0, IBackgroundCopyManager::EnumJobs(0, jobs))? };
+
+ loop {
+ let result = unsafe {
+ com_call_getter!(
+ |job| jobs,
+ IEnumBackgroundCopyJobs::Next(1, job, ptr::null_mut())
+ )
+ };
+ match result {
+ Ok(job) => {
+ if job_name_eq(&job, match_name)? {
+ unsafe {
+ let _ = com_call!(job, IBackgroundCopyJob::Cancel());
+ }
+ }
+ }
+ Err(e) => {
+ if e.code() == S_FALSE {
+ // Ran out of jobs to enumerate
+ return Ok(());
+ } else {
+ return Err(e);
+ }
+ }
+ }
+ }
+ }
+
+ /// Get the job with the given GUID.
+ ///
+ /// Returns Err if the job was not found.
+ pub fn get_job_by_guid(&self, guid: &Guid) -> Result<BitsJob> {
+ unsafe { com_call_getter!(|job| self.0, IBackgroundCopyManager::GetJob(&guid.0, job)) }
+ .map(BitsJob)
+ }
+
+ /// Try to find a job with a given GUID.
+ ///
+ /// Returns Ok(None) if the job was not found but there was no other error.
+ pub fn find_job_by_guid(&self, guid: &Guid) -> Result<Option<BitsJob>> {
+ Ok(self
+ .get_job_by_guid(guid)
+ .map(Some)
+ .allow_err(BG_E_NOT_FOUND as i32, None)?)
+ }
+
+ /// Try to find a job with a given GUID and name.
+ ///
+ /// Returns Ok(None) if the job was not found, or if it had the wrong name, as long as there
+ /// was no other error.
+ pub fn find_job_by_guid_and_name(
+ &self,
+ guid: &Guid,
+ match_name: &OsStr,
+ ) -> Result<Option<BitsJob>> {
+ Ok(match self.find_job_by_guid(guid)? {
+ None => None,
+ Some(BitsJob(ref job)) if !job_name_eq(job, match_name)? => None,
+ result => result,
+ })
+ }
+
+ /// Translate a BITS `HRESULT` to a textual description.
+ ///
+ /// This uses the current thread's locale to look up the message associated with a BITS
+ /// error. It should only be used for `HRESULT`s returned from BITS COM interfaces.
+ pub fn get_error_description(&self, hr: HRESULT) -> Result<String> {
+ unsafe {
+ let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale()));
+
+ Ok(taskmem_into_lossy_string(com_call_taskmem_getter!(
+ |desc| self.0,
+ IBackgroundCopyManager::GetErrorDescription(hr, language_id, desc)
+ )?))
+ }
+ }
+}
+
+unsafe fn taskmem_into_lossy_string(taskmem: CoTaskMem<u16>) -> String {
+ OsString::from_wide(taskmem.as_slice_until_null())
+ .to_string_lossy()
+ .into_owned()
+}
+
+fn job_name_eq(job: &ComRef<IBackgroundCopyJob>, match_name: &OsStr) -> Result<bool> {
+ let job_name = unsafe {
+ OsString::from_wide(
+ com_call_taskmem_getter!(|name| job, IBackgroundCopyJob::GetDisplayName(name))?
+ .as_slice_until_null(),
+ )
+ };
+
+ Ok(job_name == match_name)
+}
+
+pub struct BitsJob(ComRef<IBackgroundCopyJob>);
+
+impl BitsJob {
+ /// Get the job's GUID.
+ pub fn guid(&self) -> Result<Guid> {
+ // TODO: cache on create or retrieved by GUID?
+ unsafe {
+ let mut guid = mem::zeroed();
+ com_call!(self.0, IBackgroundCopyJob::GetId(&mut guid))?;
+ Ok(Guid(guid))
+ }
+ }
+
+ /// Add a file to the job.
+ pub fn add_file(&mut self, remote_url: &OsStr, local_file: &OsStr) -> Result<()> {
+ unsafe {
+ com_call!(
+ self.0,
+ IBackgroundCopyJob::AddFile(
+ remote_url.to_wide_null().as_ptr(),
+ local_file.to_wide_null().as_ptr(),
+ )
+ )
+ }?;
+ Ok(())
+ }
+
+ /// Get the first file in the job.
+ ///
+ /// This is provided for collecting the redirected remote name of single file jobs.
+ pub fn get_first_file(&mut self) -> Result<BitsFile> {
+ let files = unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::EnumFiles(e))? };
+
+ let file = unsafe {
+ com_call_getter!(
+ |file| files,
+ IEnumBackgroundCopyFiles::Next(1, file, ptr::null_mut())
+ )?
+ };
+
+ Ok(BitsFile(file))
+ }
+
+ /// Set the job's description string.
+ ///
+ /// This is different from the display name set when creating the job.
+ pub fn set_description(&mut self, description: &OsStr) -> Result<()> {
+ unsafe {
+ com_call!(
+ self.0,
+ IBackgroundCopyJob::SetDescription(description.to_wide_null().as_ptr())
+ )
+ }?;
+ Ok(())
+ }
+
+ /// Change the job's proxy usage setting.
+ ///
+ /// The default is `BitsProxyUsage::Preconfig`.
+ pub fn set_proxy_usage(&mut self, usage: BitsProxyUsage) -> Result<()> {
+ use BitsProxyUsage::*;
+
+ match usage {
+ Preconfig | NoProxy | AutoDetect => {
+ unsafe {
+ com_call!(
+ self.0,
+ IBackgroundCopyJob::SetProxySettings(
+ usage as BG_JOB_PROXY_USAGE,
+ ptr::null_mut(),
+ ptr::null_mut(),
+ )
+ )
+ }?;
+ Ok(())
+ }
+ }
+ }
+
+ /// Change the job's priority.
+ ///
+ /// The default is `BitsJobPriority::Normal`.
+ pub fn set_priority(&mut self, priority: BitsJobPriority) -> Result<()> {
+ unsafe {
+ com_call!(
+ self.0,
+ IBackgroundCopyJob::SetPriority(priority as BG_JOB_PRIORITY)
+ )
+ }?;
+ Ok(())
+ }
+
+ pub fn set_minimum_retry_delay(&mut self, seconds: ULONG) -> Result<()> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::SetMinimumRetryDelay(seconds)) }?;
+ Ok(())
+ }
+
+ pub fn set_no_progress_timeout(&mut self, seconds: ULONG) -> Result<()> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::SetNoProgressTimeout(seconds)) }?;
+ Ok(())
+ }
+
+ /// Enable HTTP redirect reporting.
+ ///
+ /// The default setting is to allow HTTP redirects, but to not report them in any way. With
+ /// this setting enabled, the remote name of a file will be updated to reflect the redirect.
+ ///
+ /// # Compatibility #
+ ///
+ /// First available in Windows Vista.
+ pub fn set_redirect_report(&mut self) -> Result<()> {
+ unsafe {
+ com_call!(
+ self.0.cast()?,
+ IBackgroundCopyJobHttpOptions::SetSecurityFlags(
+ BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT
+ )
+ )
+ }?;
+
+ Ok(())
+ }
+
+ /// Resume the job. This must be done at least once to initially enqueue the job.
+ pub fn resume(&mut self) -> Result<()> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::Resume()) }?;
+ Ok(())
+ }
+
+ pub fn suspend(&mut self) -> Result<()> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::Suspend()) }?;
+ Ok(())
+ }
+
+ /// Complete the job, moving the local files to their final names.
+ ///
+ /// Has two interesting success `HRESULT`s: `BG_S_PARTIAL_COMPLETE` and
+ /// `BG_S_UNABLE_TO_DELETE_FILES`.
+ pub fn complete(&mut self) -> Result<HRESULT> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::Complete()) }
+ }
+
+ /// Cancel the job, deleting any temporary files.
+ ///
+ /// Has an interesting success `HRESULT`: `BG_S_UNABLE_TO_DELETE_FILES`.
+ pub fn cancel(&mut self) -> Result<HRESULT> {
+ unsafe { com_call!(self.0, IBackgroundCopyJob::Cancel()) }
+ }
+
+ /// Set the notification callbacks to use with this job.
+ ///
+ /// This will replace any previously set callbacks.
+ pub fn register_callbacks(
+ &mut self,
+ transferred_cb: Option<Box<callback::TransferredCallback>>,
+ error_cb: Option<Box<callback::ErrorCallback>>,
+ modification_cb: Option<Box<callback::ModificationCallback>>,
+ ) -> Result<()> {
+ let mut flags = 0;
+ if transferred_cb.is_some() {
+ flags |= BG_NOTIFY_JOB_TRANSFERRED;
+ }
+ if error_cb.is_some() {
+ flags |= BG_NOTIFY_JOB_ERROR;
+ }
+ if modification_cb.is_some() {
+ flags |= BG_NOTIFY_JOB_MODIFICATION;
+ }
+
+ callback::BackgroundCopyCallback::register(
+ self,
+ transferred_cb,
+ error_cb,
+ modification_cb,
+ )?;
+
+ unsafe { com_call!(self.0, IBackgroundCopyJob::SetNotifyFlags(flags)) }?;
+
+ Ok(())
+ }
+
+ fn _clear_callbacks(&mut self) -> Result<()> {
+ unsafe {
+ com_call!(
+ self.0,
+ IBackgroundCopyJob::SetNotifyFlags(BG_NOTIFY_DISABLE)
+ )?;
+
+ self.set_notify_interface(ptr::null_mut() as *mut IUnknown)
+ }
+ }
+
+ /// Collect the current status of the job, including errors.
+ pub fn get_status(&self) -> Result<BitsJobStatus> {
+ let mut state = 0;
+ let mut progress = unsafe { mem::zeroed() };
+ let mut error_count = 0;
+ let mut times = unsafe { mem::zeroed() };
+
+ unsafe {
+ com_call!(self.0, IBackgroundCopyJob::GetState(&mut state))?;
+ com_call!(self.0, IBackgroundCopyJob::GetProgress(&mut progress))?;
+ com_call!(self.0, IBackgroundCopyJob::GetErrorCount(&mut error_count))?;
+ com_call!(self.0, IBackgroundCopyJob::GetTimes(&mut times))?;
+ }
+
+ Ok(BitsJobStatus {
+ state: BitsJobState::from(state),
+ progress: BitsJobProgress {
+ total_bytes: if progress.BytesTotal == BG_SIZE_UNKNOWN {
+ None
+ } else {
+ Some(progress.BytesTotal)
+ },
+ transferred_bytes: progress.BytesTransferred,
+ total_files: progress.FilesTotal,
+ transferred_files: progress.FilesTransferred,
+ },
+ error_count,
+ error: if state == BG_JOB_STATE_ERROR || state == BG_JOB_STATE_TRANSIENT_ERROR {
+ let error_obj =
+ unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::GetError(e)) }?;
+
+ Some(BitsJob::get_error(error_obj)?)
+ } else {
+ None
+ },
+ times: BitsJobTimes {
+ creation: FileTime(times.CreationTime),
+ modification: FileTime(times.ModificationTime),
+ transfer_completion: if times.TransferCompletionTime.dwLowDateTime == 0
+ && times.TransferCompletionTime.dwHighDateTime == 0
+ {
+ None
+ } else {
+ Some(FileTime(times.TransferCompletionTime))
+ },
+ },
+ })
+ }
+
+ fn get_error(error_obj: ComRef<IBackgroundCopyError>) -> Result<BitsJobError> {
+ let mut context = 0;
+ let mut hresult = 0;
+ unsafe {
+ com_call!(
+ error_obj,
+ IBackgroundCopyError::GetError(&mut context, &mut hresult)
+ )?;
+
+ let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale()));
+
+ let context = BitsErrorContext::from(context);
+ let context_str = com_call_taskmem_getter!(
+ |desc| error_obj,
+ IBackgroundCopyError::GetErrorContextDescription(language_id, desc)
+ )
+ .map(|s| taskmem_into_lossy_string(s))
+ .unwrap_or_else(|_| format!("{:?}", context));
+ let error_str = com_call_taskmem_getter!(
+ |desc| error_obj,
+ IBackgroundCopyError::GetErrorDescription(language_id, desc)
+ )
+ .map(|s| taskmem_into_lossy_string(s))
+ .unwrap_or_else(|_| format!("{:#08x}", hresult));
+
+ Ok(BitsJobError {
+ context,
+ context_str,
+ error: hresult,
+ error_str,
+ })
+ }
+ }
+
+ unsafe fn set_notify_interface(&self, interface: *mut IUnknown) -> Result<()> {
+ com_call!(self.0, IBackgroundCopyJob::SetNotifyInterface(interface))?;
+ Ok(())
+ }
+}
+
+pub struct BitsFile(ComRef<IBackgroundCopyFile>);
+
+/// A single file in a BITS job.
+///
+/// This is provided for collecting the redirected remote name.
+impl BitsFile {
+ /// Get the remote name from which the file is being downloaded.
+ ///
+ /// If [`BitsJob::set_redirect_report()`](struct.BitsJob.html#method.set_redirect_report)
+ /// hasn't been called on the job, this won't be
+ /// updated as HTTP redirects are processed.
+ pub fn get_remote_name(&self) -> Result<OsString> {
+ unsafe {
+ Ok(OsString::from_wide(
+ com_call_taskmem_getter!(|name| self.0, IBackgroundCopyFile::GetRemoteName(name))?
+ .as_slice_until_null(),
+ ))
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::BackgroundCopyManager;
+ use std::ffi::OsString;
+ use std::mem;
+
+ #[test]
+ #[ignore]
+ fn test_find_job() {
+ let bcm = BackgroundCopyManager::connect().unwrap();
+ let name = OsString::from("bits test job");
+ let wrong_name = OsString::from("bits test jobbo");
+
+ let mut job = bcm.create_job(&name).unwrap();
+ let guid = job.guid().unwrap();
+
+ assert_eq!(
+ bcm.find_job_by_guid(&guid)
+ .unwrap()
+ .unwrap()
+ .guid()
+ .unwrap(),
+ guid
+ );
+ assert_eq!(
+ bcm.find_job_by_guid_and_name(&guid, &name)
+ .unwrap()
+ .unwrap()
+ .guid()
+ .unwrap(),
+ guid
+ );
+ assert!(bcm
+ .find_job_by_guid_and_name(&guid, &wrong_name)
+ .unwrap()
+ .is_none());
+
+ job.cancel().unwrap();
+ mem::drop(job);
+
+ assert!(bcm.find_job_by_guid(&guid).unwrap().is_none());
+ assert!(bcm
+ .find_job_by_guid_and_name(&guid, &name)
+ .unwrap()
+ .is_none());
+ }
+}
diff --git a/toolkit/components/bitsdownload/bits_client/bits/src/status.rs b/toolkit/components/bitsdownload/bits_client/bits/src/status.rs
new file mode 100644
index 0000000000..648a26866e
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/status.rs
@@ -0,0 +1,118 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Data types for reporting a job's status
+
+use filetime_win::FileTime;
+use winapi::shared::winerror::HRESULT;
+use winapi::um::bits::{BG_ERROR_CONTEXT, BG_JOB_STATE};
+
+#[cfg(feature = "status_serde")]
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobStatus {
+ pub state: BitsJobState,
+ pub progress: BitsJobProgress,
+ pub error_count: u32,
+ pub error: Option<BitsJobError>,
+ pub times: BitsJobTimes,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobError {
+ pub context: BitsErrorContext,
+ pub context_str: String,
+ pub error: HRESULT,
+ pub error_str: String,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub enum BitsErrorContext {
+ None,
+ Unknown,
+ GeneralQueueManager,
+ QueueManagerNotification,
+ LocalFile,
+ RemoteFile,
+ GeneralTransport,
+ RemoteApplication,
+ /// No other values are documented
+ Other(BG_ERROR_CONTEXT),
+}
+
+impl From<BG_ERROR_CONTEXT> for BitsErrorContext {
+ fn from(ec: BG_ERROR_CONTEXT) -> BitsErrorContext {
+ use self::BitsErrorContext::*;
+ use winapi::um::bits;
+ match ec {
+ bits::BG_ERROR_CONTEXT_NONE => None,
+ bits::BG_ERROR_CONTEXT_UNKNOWN => Unknown,
+ bits::BG_ERROR_CONTEXT_GENERAL_QUEUE_MANAGER => GeneralQueueManager,
+ bits::BG_ERROR_CONTEXT_QUEUE_MANAGER_NOTIFICATION => QueueManagerNotification,
+ bits::BG_ERROR_CONTEXT_LOCAL_FILE => LocalFile,
+ bits::BG_ERROR_CONTEXT_REMOTE_FILE => RemoteFile,
+ bits::BG_ERROR_CONTEXT_GENERAL_TRANSPORT => GeneralTransport,
+ bits::BG_ERROR_CONTEXT_REMOTE_APPLICATION => RemoteApplication,
+ ec => Other(ec),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub enum BitsJobState {
+ Queued,
+ Connecting,
+ Transferring,
+ Suspended,
+ Error,
+ TransientError,
+ Transferred,
+ Acknowledged,
+ Cancelled,
+ /// No other values are documented
+ Other(BG_JOB_STATE),
+}
+
+impl From<BG_JOB_STATE> for BitsJobState {
+ fn from(s: BG_JOB_STATE) -> BitsJobState {
+ use self::BitsJobState::*;
+ use winapi::um::bits;
+ match s {
+ bits::BG_JOB_STATE_QUEUED => Queued,
+ bits::BG_JOB_STATE_CONNECTING => Connecting,
+ bits::BG_JOB_STATE_TRANSFERRING => Transferring,
+ bits::BG_JOB_STATE_SUSPENDED => Suspended,
+ bits::BG_JOB_STATE_ERROR => Error,
+ bits::BG_JOB_STATE_TRANSIENT_ERROR => TransientError,
+ bits::BG_JOB_STATE_TRANSFERRED => Transferred,
+ bits::BG_JOB_STATE_ACKNOWLEDGED => Acknowledged,
+ bits::BG_JOB_STATE_CANCELLED => Cancelled,
+ s => Other(s),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobProgress {
+ pub total_bytes: Option<u64>,
+ pub transferred_bytes: u64,
+ pub total_files: u32,
+ pub transferred_files: u32,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "status_serde", derive(Serialize, Deserialize))]
+pub struct BitsJobTimes {
+ pub creation: FileTime,
+ pub modification: FileTime,
+ pub transfer_completion: Option<FileTime>,
+}
diff --git a/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs b/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs
new file mode 100644
index 0000000000..c108f8d629
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs
@@ -0,0 +1,38 @@
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// All files in the project carrying such notice may not be copied, modified, or distributed
+// except according to those terms.
+
+// Minimal null-terminated wide string support from wio.
+
+use std::ffi::{OsStr, OsString};
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+use std::slice;
+
+pub trait ToWideNull {
+ fn to_wide_null(&self) -> Vec<u16>;
+}
+
+impl<T: AsRef<OsStr>> ToWideNull for T {
+ fn to_wide_null(&self) -> Vec<u16> {
+ self.as_ref().encode_wide().chain(Some(0)).collect()
+ }
+}
+
+pub trait FromWidePtrNull {
+ unsafe fn from_wide_ptr_null(wide: *const u16) -> Self;
+}
+
+impl FromWidePtrNull for OsString {
+ unsafe fn from_wide_ptr_null(wide: *const u16) -> Self {
+ assert!(!wide.is_null());
+
+ for i in 0.. {
+ if *wide.offset(i) == 0 {
+ return Self::from_wide(&slice::from_raw_parts(wide, i as usize));
+ }
+ }
+ unreachable!()
+ }
+}
diff --git a/toolkit/components/bitsdownload/bits_client/examples/test_client.rs b/toolkit/components/bitsdownload/bits_client/examples/test_client.rs
new file mode 100644
index 0000000000..25d7a0d82e
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/examples/test_client.rs
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+extern crate bits_client;
+extern crate comedy;
+//extern crate ctrlc;
+extern crate guid_win;
+extern crate thiserror;
+
+use std::env;
+use std::ffi::{OsStr, OsString};
+use std::process;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+
+use thiserror::Error;
+
+use bits_client::bits_protocol::HResultMessage;
+use bits_client::{BitsClient, BitsJobState, BitsMonitorClient, BitsProxyUsage, Guid, PipeError};
+
+#[derive(Debug, Error)]
+enum MyError {
+ #[error("{0}")]
+ Msg(String),
+ #[error("HResult")]
+ HResult(#[from] comedy::HResult),
+ #[error("Win32Error")]
+ Win32Error(#[from] comedy::Win32Error),
+ #[error("PipeError")]
+ PipeError(#[from] PipeError),
+ #[error("HResultMessage")]
+ HResultMessage(#[from] HResultMessage),
+}
+
+macro_rules! bail {
+ ($e:expr) => {
+ return Err($crate::MyError::Msg($e.to_string()))
+ };
+ ($fmt:expr, $($arg:tt)*) => {
+ return Err($crate::MyError::Msg(format!($fmt, $($arg)*)))
+ };
+}
+
+type Result = std::result::Result<(), MyError>;
+
+pub fn main() {
+ if let Err(err) = entry() {
+ eprintln!("{}", err);
+ let mut err: &dyn std::error::Error = &err;
+ while let Some(source) = err.source() {
+ eprintln!("caused by {}", source);
+ err = source;
+ }
+
+ process::exit(1);
+ } else {
+ println!("OK");
+ }
+}
+
+const EXE_NAME: &'static str = "test_client";
+
+fn usage() -> String {
+ format!(
+ concat!(
+ "Usage {0} <command> ",
+ "[local-service] ",
+ "[args...]\n",
+ "Commands:\n",
+ " bits-start <URL> <local file>\n",
+ " bits-monitor <GUID>\n",
+ " bits-bg <GUID>\n",
+ " bits-fg <GUID>\n",
+ " bits-suspend <GUID>\n",
+ " bits-resume <GUID>\n",
+ " bits-complete <GUID>\n",
+ " bits-cancel <GUID> ...\n"
+ ),
+ EXE_NAME
+ )
+}
+
+fn entry() -> Result {
+ let args: Vec<_> = env::args_os().collect();
+
+ let mut client = BitsClient::new(
+ OsString::from("bits_client test"),
+ OsString::from("C:\\ProgramData"),
+ )?;
+
+ if args.len() < 2 {
+ eprintln!("{}", usage());
+ bail!("not enough arguments");
+ }
+
+ let cmd = &*args[1].to_string_lossy();
+ let cmd_args = &args[2..];
+
+ match cmd {
+ // command line client for testing
+ "bits-start" if cmd_args.len() == 2 => bits_start(
+ Arc::new(Mutex::new(client)),
+ cmd_args[0].clone(),
+ cmd_args[1].clone(),
+ BitsProxyUsage::Preconfig,
+ ),
+ "bits-monitor" if cmd_args.len() == 1 => {
+ bits_monitor(Arc::new(Mutex::new(client)), &cmd_args[0])
+ }
+ // TODO: some way of testing set update interval
+ "bits-bg" if cmd_args.len() == 1 => bits_bg(&mut client, &cmd_args[0]),
+ "bits-fg" if cmd_args.len() == 1 => bits_fg(&mut client, &cmd_args[0]),
+ "bits-suspend" if cmd_args.len() == 1 => bits_suspend(&mut client, &cmd_args[0]),
+ "bits-resume" if cmd_args.len() == 1 => bits_resume(&mut client, &cmd_args[0]),
+ "bits-complete" if cmd_args.len() == 1 => bits_complete(&mut client, &cmd_args[0]),
+ "bits-cancel" if cmd_args.len() >= 1 => {
+ for guid in cmd_args {
+ bits_cancel(&mut client, guid)?;
+ }
+ Ok(())
+ }
+ _ => {
+ eprintln!("{}", usage());
+ bail!("usage error");
+ }
+ }
+}
+
+fn bits_start(
+ client: Arc<Mutex<BitsClient>>,
+ url: OsString,
+ save_path: OsString,
+ proxy_usage: BitsProxyUsage,
+) -> Result {
+ //let interval = 10 * 60 * 1000;
+ let no_progress_timeout_secs = 60;
+ let interval = 1000;
+
+ let result = client.lock().unwrap().start_job(
+ url,
+ save_path,
+ proxy_usage,
+ no_progress_timeout_secs,
+ interval,
+ )?;
+
+ match result {
+ Ok((r, monitor_client)) => {
+ println!("start success, guid = {}", r.guid);
+ client
+ .lock()
+ .unwrap()
+ .set_update_interval(r.guid.clone(), interval)?
+ .unwrap();
+ monitor_loop(client, monitor_client, r.guid.clone(), interval)?;
+ Ok(())
+ }
+ Err(e) => {
+ let _ = e.clone();
+ bail!("error from server {}", e)
+ }
+ }
+}
+
+fn bits_monitor(client: Arc<Mutex<BitsClient>>, guid: &OsStr) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ let result = client.lock().unwrap().monitor_job(guid.clone(), 1000)?;
+ match result {
+ Ok(monitor_client) => {
+ println!("monitor success");
+ monitor_loop(client, monitor_client, guid, 1000)?;
+ Ok(())
+ }
+ Err(e) => bail!("error from server {}", e),
+ }
+}
+
+fn _check_client_send()
+where
+ BitsClient: Send,
+{
+}
+fn _check_monitor_send()
+where
+ BitsMonitorClient: Send,
+{
+}
+
+fn monitor_loop(
+ _client: Arc<Mutex<BitsClient>>,
+ mut monitor_client: BitsMonitorClient,
+ _guid: Guid,
+ wait_millis: u32,
+) -> Result {
+ /*
+ // Commented out to avoid vendoring ctrlc.
+ // This could also possibly be done with `exclude` in the mozilla-central `Cargo.toml`.
+ let client_for_handler = _client.clone();
+ ctrlc::set_handler(move || {
+ eprintln!("Ctrl-C!");
+ let _ = client_for_handler.lock().unwrap().stop_update(_guid.clone());
+ })
+ .expect("Error setting Ctrl-C handler");
+ */
+
+ loop {
+ let status = monitor_client.get_status(wait_millis * 10)??;
+
+ println!("{:?} {:?}", BitsJobState::from(status.state), status);
+
+ //println!("{}", job.get_first_file()?.get_remote_name()?.into_string().unwrap());
+ let transfer_completion_time = if let Some(ft) = status.times.transfer_completion {
+ format!("Some({})", ft.to_system_time_utc()?)
+ } else {
+ String::from("None")
+ };
+ println!(
+ "creation: {}, modification: {}, transfer completion: {}",
+ status.times.creation.to_system_time_utc()?,
+ status.times.modification.to_system_time_utc()?,
+ transfer_completion_time
+ );
+
+ match BitsJobState::from(status.state) {
+ BitsJobState::Connecting
+ | BitsJobState::Transferring
+ | BitsJobState::TransientError => {}
+ _ => break,
+ }
+ }
+ println!("monitor loop ending");
+ println!("sleeping...");
+ std::thread::sleep(std::time::Duration::from_secs(1));
+
+ Ok(())
+}
+
+fn bits_bg(client: &mut BitsClient, guid: &OsStr) -> Result {
+ bits_set_priority(client, guid, false)
+}
+
+fn bits_fg(client: &mut BitsClient, guid: &OsStr) -> Result {
+ bits_set_priority(client, guid, true)
+}
+
+fn bits_set_priority(client: &mut BitsClient, guid: &OsStr, foreground: bool) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ match client.set_job_priority(guid, foreground)? {
+ Ok(()) => Ok(()),
+ Err(e) => bail!("error from server {}", e),
+ }
+}
+
+fn bits_suspend(client: &mut BitsClient, guid: &OsStr) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ match client.suspend_job(guid)? {
+ Ok(()) => Ok(()),
+ Err(e) => bail!("error from server {}", e),
+ }
+}
+
+fn bits_resume(client: &mut BitsClient, guid: &OsStr) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ match client.resume_job(guid)? {
+ Ok(()) => Ok(()),
+ Err(e) => bail!("error from server {}", e),
+ }
+}
+
+fn bits_complete(client: &mut BitsClient, guid: &OsStr) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ match client.complete_job(guid)? {
+ Ok(()) => Ok(()),
+ Err(e) => bail!("error from server {}", e),
+ }
+}
+
+fn bits_cancel(client: &mut BitsClient, guid: &OsStr) -> Result {
+ let guid = Guid::from_str(&guid.to_string_lossy())?;
+ match client.cancel_job(guid)? {
+ Ok(()) => Ok(()),
+ Err(e) => bail!("error from server {}", e),
+ }
+}
diff --git a/toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs b/toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs
new file mode 100644
index 0000000000..6f19f5ef23
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/bits_protocol.rs
@@ -0,0 +1,380 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Command, response, and status types.
+
+use std::error::Error as StdError;
+use std::ffi::OsString;
+use std::fmt;
+use std::result;
+
+use guid_win::Guid;
+use thiserror::Error;
+
+use super::{BitsErrorContext, BitsJobProgress, BitsJobState, BitsJobTimes, BitsProxyUsage};
+
+type HRESULT = i32;
+
+/// An HRESULT with a descriptive message
+#[derive(Clone, Debug)]
+pub struct HResultMessage {
+ pub hr: HRESULT,
+ pub message: String,
+}
+
+impl fmt::Display for HResultMessage {
+ fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
+ self.message.fmt(f)
+ }
+}
+
+impl StdError for HResultMessage {}
+
+/// Commands which can be sent to the server.
+///
+/// This is currently unused as the out-of-process Local Service server is not finished.
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub enum Command {
+ StartJob(StartJobCommand),
+ MonitorJob(MonitorJobCommand),
+ SuspendJob(SuspendJobCommand),
+ ResumeJob(ResumeJobCommand),
+ SetJobPriority(SetJobPriorityCommand),
+ SetNoProgressTimeout(SetNoProgressTimeoutCommand),
+ SetUpdateInterval(SetUpdateIntervalCommand),
+ CompleteJob(CompleteJobCommand),
+ CancelJob(CancelJobCommand),
+}
+
+/// Combine a [`Command`](enum.Command.html) with its success and failure result types.
+#[doc(hidden)]
+pub trait CommandType {
+ type Success;
+ type Failure: StdError;
+ fn wrap(command: Self) -> Command;
+}
+
+// Start Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct StartJobCommand {
+ pub url: OsString,
+ pub save_path: OsString,
+ pub proxy_usage: BitsProxyUsage,
+ pub no_progress_timeout_secs: u32,
+ pub monitor: Option<MonitorConfig>,
+}
+
+impl CommandType for StartJobCommand {
+ type Success = StartJobSuccess;
+ type Failure = StartJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::StartJob(cmd)
+ }
+}
+
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct MonitorConfig {
+ pub pipe_name: OsString,
+ pub interval_millis: u32,
+}
+
+#[derive(Clone, Debug)]
+pub struct StartJobSuccess {
+ pub guid: Guid,
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum StartJobFailure {
+ #[error("Argument validation failed: {0}")]
+ ArgumentValidation(String),
+ #[error("Create job: {0}")]
+ Create(HResultMessage),
+ #[error("Add file to job: {0}")]
+ AddFile(HResultMessage),
+ #[error("Apply settings to job: {0}")]
+ ApplySettings(HResultMessage),
+ #[error("Resume job: {0}")]
+ Resume(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Monitor Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct MonitorJobCommand {
+ pub guid: Guid,
+ pub monitor: MonitorConfig,
+}
+
+impl CommandType for MonitorJobCommand {
+ type Success = ();
+ type Failure = MonitorJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::MonitorJob(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum MonitorJobFailure {
+ #[error("Argument validation failed: {0}")]
+ ArgumentValidation(String),
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Suspend Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SuspendJobCommand {
+ pub guid: Guid,
+}
+
+impl CommandType for SuspendJobCommand {
+ type Success = ();
+ type Failure = SuspendJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::SuspendJob(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum SuspendJobFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Suspend job: {0}")]
+ SuspendJob(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Resume Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct ResumeJobCommand {
+ pub guid: Guid,
+}
+
+impl CommandType for ResumeJobCommand {
+ type Success = ();
+ type Failure = ResumeJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::ResumeJob(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum ResumeJobFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Resume job: {0}")]
+ ResumeJob(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Set Job Priority
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SetJobPriorityCommand {
+ pub guid: Guid,
+ pub foreground: bool,
+}
+
+impl CommandType for SetJobPriorityCommand {
+ type Success = ();
+ type Failure = SetJobPriorityFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::SetJobPriority(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum SetJobPriorityFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Apply settings to job: {0}")]
+ ApplySettings(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Set No Progress Timeout
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SetNoProgressTimeoutCommand {
+ pub guid: Guid,
+ pub timeout_secs: u32,
+}
+
+impl CommandType for SetNoProgressTimeoutCommand {
+ type Success = ();
+ type Failure = SetNoProgressTimeoutFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::SetNoProgressTimeout(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum SetNoProgressTimeoutFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Apply settings to job: {0}")]
+ ApplySettings(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Set Update Interval
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct SetUpdateIntervalCommand {
+ pub guid: Guid,
+ pub interval_millis: u32,
+}
+
+impl CommandType for SetUpdateIntervalCommand {
+ type Success = ();
+ type Failure = SetUpdateIntervalFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::SetUpdateInterval(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum SetUpdateIntervalFailure {
+ #[error("Argument validation: {0}")]
+ ArgumentValidation(String),
+ #[error("Monitor not found")]
+ NotFound,
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Complete Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct CompleteJobCommand {
+ pub guid: Guid,
+}
+
+impl CommandType for CompleteJobCommand {
+ type Success = ();
+ type Failure = CompleteJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::CompleteJob(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum CompleteJobFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Complete job: {0}")]
+ CompleteJob(HResultMessage),
+ #[error("Job only partially completed")]
+ PartialComplete,
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+// Cancel Job
+#[doc(hidden)]
+#[derive(Clone, Debug)]
+pub struct CancelJobCommand {
+ pub guid: Guid,
+}
+
+impl CommandType for CancelJobCommand {
+ type Success = ();
+ type Failure = CancelJobFailure;
+ fn wrap(cmd: Self) -> Command {
+ Command::CancelJob(cmd)
+ }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum CancelJobFailure {
+ #[error("Job not found")]
+ NotFound,
+ #[error("Get job: {0}")]
+ GetJob(HResultMessage),
+ #[error("Cancel job: {0}")]
+ CancelJob(HResultMessage),
+ #[error("Connect to BackgroundCopyManager: {0}")]
+ ConnectBcm(HResultMessage),
+ #[error("BITS error: {0}")]
+ OtherBITS(HResultMessage),
+ #[error("Other failure: {0}")]
+ Other(String),
+}
+
+/// Job status report
+///
+/// This includes a URL which updates with redirect but is otherwise the same as
+/// `bits::status::BitsJobStatus`.
+#[derive(Clone, Debug)]
+pub struct JobStatus {
+ pub state: BitsJobState,
+ pub progress: BitsJobProgress,
+ pub error_count: u32,
+ pub error: Option<JobError>,
+ pub times: BitsJobTimes,
+ /// None means same as last time
+ pub url: Option<OsString>,
+}
+
+/// Job error report
+#[derive(Clone, Debug, Error)]
+#[error("Job error in context {context_str}: {error}")]
+pub struct JobError {
+ pub context: BitsErrorContext,
+ pub context_str: String,
+ pub error: HResultMessage,
+}
diff --git a/toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs b/toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs
new file mode 100644
index 0000000000..c7f6869167
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/in_process/mod.rs
@@ -0,0 +1,504 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cmp;
+use std::collections::{hash_map, HashMap};
+use std::ffi;
+use std::path;
+use std::sync::{Arc, Condvar, Mutex, Weak};
+use std::time::{Duration, Instant};
+
+use bits::{
+ BackgroundCopyManager, BitsJob, BitsJobPriority, BitsProxyUsage, BG_S_PARTIAL_COMPLETE, E_FAIL,
+};
+use guid_win::Guid;
+
+use bits_protocol::*;
+
+use super::Error;
+
+// This is a macro in order to use the NotFound and GetJob variants from whatever enum is in scope.
+macro_rules! get_job {
+ ($bcm:ident, $guid:expr, $name:expr) => {{
+ $bcm = BackgroundCopyManager::connect().map_err(|e| {
+ ConnectBcm(HResultMessage {
+ hr: e.code(),
+ message: e.to_string(),
+ })
+ })?;
+ $bcm.find_job_by_guid_and_name($guid, $name)
+ .map_err(|e| GetJob($crate::in_process::format_error(&$bcm, e)))?
+ .ok_or(NotFound)?
+ }};
+}
+
+fn format_error(bcm: &BackgroundCopyManager, error: comedy::HResult) -> HResultMessage {
+ let bits_description = bcm.get_error_description(error.code()).ok();
+
+ HResultMessage {
+ hr: error.code(),
+ message: if let Some(desc) = bits_description {
+ format!("{}: {}", error, desc)
+ } else {
+ format!("{}", error)
+ },
+ }
+}
+
+// The in-process client uses direct BITS calls via the `bits` crate.
+// See the corresponding functions in BitsClient.
+pub struct InProcessClient {
+ job_name: ffi::OsString,
+ save_path_prefix: path::PathBuf,
+ monitors: HashMap<Guid, InProcessMonitorControl>,
+}
+
+impl InProcessClient {
+ pub fn new(
+ job_name: ffi::OsString,
+ save_path_prefix: ffi::OsString,
+ ) -> Result<InProcessClient, Error> {
+ Ok(InProcessClient {
+ job_name,
+ save_path_prefix: path::PathBuf::from(save_path_prefix),
+ monitors: HashMap::new(),
+ })
+ }
+
+ pub fn start_job(
+ &mut self,
+ url: ffi::OsString,
+ save_path: ffi::OsString,
+ proxy_usage: BitsProxyUsage,
+ no_progress_timeout_secs: u32,
+ monitor_interval_millis: u32,
+ ) -> Result<(StartJobSuccess, InProcessMonitor), StartJobFailure> {
+ use StartJobFailure::*;
+
+ let full_path = self.save_path_prefix.join(save_path);
+
+ // Verify that `full_path` is under the directory called `save_path_prefix`.
+ {
+ let canonical_prefix = self.save_path_prefix.canonicalize().map_err(|e| {
+ ArgumentValidation(format!("save_path_prefix.canonicalize(): {}", e))
+ })?;
+ // Full path minus file name, canonicalize() fails with nonexistent files, but the
+ // parent directory ought to exist.
+ let canonical_full_path = full_path
+ .parent()
+ .ok_or_else(|| ArgumentValidation("full_path.parent(): None".into()))?
+ .canonicalize()
+ .map_err(|e| {
+ ArgumentValidation(format!("full_path.parent().canonicalize(): {}", e))
+ })?;
+
+ if !canonical_full_path.starts_with(&canonical_prefix) {
+ return Err(ArgumentValidation(format!(
+ "{:?} is not within {:?}",
+ canonical_full_path, canonical_prefix
+ )));
+ }
+ }
+
+ // TODO: Should the job be explicitly cleaned up if this fn can't return success?
+ // If the job is dropped before `AddFile` succeeds, I think it automatically gets
+ // deleted from the queue. There is only one fallible call after that (`Resume`).
+
+ let bcm = BackgroundCopyManager::connect().map_err(|e| {
+ ConnectBcm(HResultMessage {
+ hr: e.code(),
+ message: e.to_string(),
+ })
+ })?;
+ let mut job = bcm
+ .create_job(&self.job_name)
+ .map_err(|e| Create(format_error(&bcm, e)))?;
+
+ let guid = job.guid().map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+ (|| {
+ job.set_proxy_usage(proxy_usage)?;
+ job.set_minimum_retry_delay(60)?;
+ job.set_no_progress_timeout(no_progress_timeout_secs)?;
+ job.set_redirect_report()?;
+
+ job.set_priority(BitsJobPriority::Foreground)?;
+
+ Ok(())
+ })()
+ .map_err(|e| ApplySettings(format_error(&bcm, e)))?;
+
+ let (client, control) = InProcessMonitor::new(&mut job, monitor_interval_millis)
+ .map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+ job.add_file(&url, &full_path.into_os_string())
+ .map_err(|e| AddFile(format_error(&bcm, e)))?;
+
+ job.resume().map_err(|e| Resume(format_error(&bcm, e)))?;
+
+ self.monitors.insert(guid.clone(), control);
+
+ Ok((StartJobSuccess { guid }, client))
+ }
+
+ pub fn monitor_job(
+ &mut self,
+ guid: Guid,
+ interval_millis: u32,
+ ) -> Result<InProcessMonitor, MonitorJobFailure> {
+ use MonitorJobFailure::*;
+
+ // Stop any preexisting monitor for the same guid.
+ let _ = self.stop_update(guid.clone());
+
+ let bcm;
+ let (client, control) =
+ InProcessMonitor::new(&mut get_job!(bcm, &guid, &self.job_name), interval_millis)
+ .map_err(|e| OtherBITS(format_error(&bcm, e)))?;
+
+ self.monitors.insert(guid, control);
+
+ Ok(client)
+ }
+
+ pub fn suspend_job(&mut self, guid: Guid) -> Result<(), SuspendJobFailure> {
+ use SuspendJobFailure::*;
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .suspend()
+ .map_err(|e| SuspendJob(format_error(&bcm, e)))?;
+
+ Ok(())
+ }
+
+ pub fn resume_job(&mut self, guid: Guid) -> Result<(), ResumeJobFailure> {
+ use ResumeJobFailure::*;
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .resume()
+ .map_err(|e| ResumeJob(format_error(&bcm, e)))?;
+
+ Ok(())
+ }
+
+ pub fn set_job_priority(
+ &mut self,
+ guid: Guid,
+ foreground: bool,
+ ) -> Result<(), SetJobPriorityFailure> {
+ use SetJobPriorityFailure::*;
+
+ let priority = if foreground {
+ BitsJobPriority::Foreground
+ } else {
+ BitsJobPriority::Normal
+ };
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .set_priority(priority)
+ .map_err(|e| ApplySettings(format_error(&bcm, e)))?;
+
+ Ok(())
+ }
+
+ pub fn set_no_progress_timeout(
+ &mut self,
+ guid: Guid,
+ timeout_secs: u32,
+ ) -> Result<(), SetNoProgressTimeoutFailure> {
+ use SetNoProgressTimeoutFailure::*;
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .set_no_progress_timeout(timeout_secs)
+ .map_err(|e| ApplySettings(format_error(&bcm, e)))?;
+
+ Ok(())
+ }
+
+ fn get_monitor_control_sender(&mut self, guid: Guid) -> Option<Arc<ControlPair>> {
+ if let hash_map::Entry::Occupied(occ) = self.monitors.entry(guid) {
+ if let Some(sender) = occ.get().0.upgrade() {
+ Some(sender)
+ } else {
+ // Remove dangling Weak
+ occ.remove_entry();
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ pub fn set_update_interval(
+ &mut self,
+ guid: Guid,
+ interval_millis: u32,
+ ) -> Result<(), SetUpdateIntervalFailure> {
+ use SetUpdateIntervalFailure::*;
+
+ if let Some(sender) = self.get_monitor_control_sender(guid) {
+ let mut s = sender.1.lock().unwrap();
+ s.interval_millis = interval_millis;
+ sender.0.notify_all();
+ Ok(())
+ } else {
+ Err(NotFound)
+ }
+ }
+
+ pub fn stop_update(&mut self, guid: Guid) -> Result<(), SetUpdateIntervalFailure> {
+ use SetUpdateIntervalFailure::*;
+
+ if let Some(sender) = self.get_monitor_control_sender(guid) {
+ sender.1.lock().unwrap().shutdown = true;
+ sender.0.notify_all();
+ Ok(())
+ } else {
+ Err(NotFound)
+ }
+ }
+
+ pub fn complete_job(&mut self, guid: Guid) -> Result<(), CompleteJobFailure> {
+ use CompleteJobFailure::*;
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .complete()
+ .map_err(|e| CompleteJob(format_error(&bcm, e)))
+ .and_then(|hr| {
+ if hr == BG_S_PARTIAL_COMPLETE as i32 {
+ Err(PartialComplete)
+ } else {
+ Ok(())
+ }
+ })?;
+
+ let _ = self.stop_update(guid);
+
+ Ok(())
+ }
+
+ pub fn cancel_job(&mut self, guid: Guid) -> Result<(), CancelJobFailure> {
+ use CancelJobFailure::*;
+
+ let bcm;
+ get_job!(bcm, &guid, &self.job_name)
+ .cancel()
+ .map_err(|e| CancelJob(format_error(&bcm, e)))?;
+
+ let _ = self.stop_update(guid);
+
+ Ok(())
+ }
+}
+
+// InProcessMonitor can be used on any thread, and `ControlPair` can be synchronously modified to
+// control a blocked `get_status` call from another thread.
+pub struct InProcessMonitor {
+ vars: Arc<ControlPair>,
+ guid: Guid,
+ last_status_time: Option<Instant>,
+ last_url: Option<ffi::OsString>,
+}
+
+// The `Condvar` is notified when `InProcessMonitorVars` changes.
+type ControlPair = (Condvar, Mutex<InProcessMonitorVars>);
+struct InProcessMonitorControl(Weak<ControlPair>);
+
+// RefUnwindSafe is not impl'd for Condvar but likely should be,
+// see https://github.com/rust-lang/rust/issues/54768
+impl std::panic::RefUnwindSafe for InProcessMonitorControl {}
+
+struct InProcessMonitorVars {
+ interval_millis: u32,
+ notified: bool,
+ shutdown: bool,
+}
+
+impl InProcessMonitor {
+ fn new(
+ job: &mut BitsJob,
+ interval_millis: u32,
+ ) -> Result<(InProcessMonitor, InProcessMonitorControl), comedy::HResult> {
+ let guid = job.guid()?;
+
+ let vars = Arc::new((
+ Condvar::new(),
+ Mutex::new(InProcessMonitorVars {
+ interval_millis,
+ notified: false,
+ shutdown: false,
+ }),
+ ));
+
+ let transferred_control = InProcessMonitorControl(Arc::downgrade(&vars));
+ let transferred_cb = Box::new(move || {
+ if let Some(control) = transferred_control.0.upgrade() {
+ if let Ok(mut vars) = control.1.lock() {
+ vars.notified = true;
+ control.0.notify_all();
+ return Ok(());
+ }
+ }
+ Err(E_FAIL)
+ });
+
+ let error_control = InProcessMonitorControl(Arc::downgrade(&vars));
+ let error_cb = Box::new(move || {
+ if let Some(control) = error_control.0.upgrade() {
+ if let Ok(mut vars) = control.1.lock() {
+ vars.notified = true;
+ control.0.notify_all();
+ return Ok(());
+ }
+ }
+ Err(E_FAIL)
+ });
+
+ // Note: These callbacks are never explicitly cleared. They will be freed when the
+ // job is deleted from BITS, and they will be cleared if an attempt is made to call them
+ // when they are no longer valid (e.g. after the process exits). This is done mostly for
+ // simplicity and should be safe.
+
+ job.register_callbacks(Some(transferred_cb), Some(error_cb), None)?;
+
+ let control = InProcessMonitorControl(Arc::downgrade(&vars));
+
+ let monitor = InProcessMonitor {
+ guid,
+ vars,
+ last_status_time: None,
+ last_url: None,
+ };
+
+ Ok((monitor, control))
+ }
+
+ pub fn get_status(
+ &mut self,
+ timeout_millis: u32,
+ ) -> Result<Result<JobStatus, HResultMessage>, Error> {
+ let timeout = Duration::from_millis(u64::from(timeout_millis));
+
+ let started = Instant::now();
+ let timeout_end = started + timeout;
+
+ {
+ let mut s = self.vars.1.lock().unwrap();
+ loop {
+ let wait_start = Instant::now();
+
+ if s.shutdown {
+ // Disconnected, immediately return error.
+ // Note: Shutdown takes priority over simultaneous notification.
+ return Err(Error::NotConnected);
+ }
+
+ if wait_start >= timeout_end {
+ // Timed out, immediately return timeout error.
+ // This should not normally happen with the in-process monitor, but the
+ // monitor interval could be longer than the timeout.
+ s.shutdown = true;
+ return Err(Error::Timeout);
+ }
+
+ // Get the interval every pass through the loop, in case it has changed.
+ let interval = Duration::from_millis(u64::from(s.interval_millis));
+
+ let wait_until = self
+ .last_status_time
+ .map(|last_status_time| cmp::min(last_status_time + interval, timeout_end));
+
+ if s.notified {
+ // Notified, exit loop to get status.
+ s.notified = false;
+ break;
+ }
+
+ if wait_until.is_none() {
+ // First status report, no waiting, exit loop to get status.
+ break;
+ }
+
+ let wait_until = wait_until.unwrap();
+
+ if wait_until <= wait_start {
+ // No time left to wait. This can't be due to timeout because
+ // `wait_until <= wait_start < timeout_end`.
+ // Status report due, exit loop to get status.
+ break;
+ }
+
+ // Wait.
+ // Do not attempt to recover from poisoned Mutex.
+ s = self
+ .vars
+ .0
+ .wait_timeout(s, wait_until - wait_start)
+ .unwrap()
+ .0;
+
+ // Mutex re-acquired, loop.
+ }
+ }
+
+ // No error yet, start getting status now.
+ self.last_status_time = Some(Instant::now());
+
+ let bcm = match BackgroundCopyManager::connect() {
+ Ok(bcm) => bcm,
+ Err(e) => {
+ // On any error, disconnect.
+ self.vars.1.lock().unwrap().shutdown = true;
+
+ // Errors below can use the BCM to do `format_error()`, but this one just gets the
+ // basic `comedy::HResult` treatment.
+ return Ok(Err(HResultMessage {
+ hr: e.code(),
+ message: format!("{}", e),
+ }));
+ }
+ };
+
+ Ok((|| {
+ let mut job = bcm.get_job_by_guid(&self.guid)?;
+
+ let status = job.get_status()?;
+ let url = job.get_first_file()?.get_remote_name()?;
+
+ Ok(JobStatus {
+ state: status.state,
+ progress: status.progress,
+ error_count: status.error_count,
+ error: status.error.map(|e| JobError {
+ context: e.context,
+ context_str: e.context_str,
+ error: HResultMessage {
+ hr: e.error,
+ message: e.error_str,
+ },
+ }),
+ times: status.times,
+ url: if self.last_url.is_some() && *self.last_url.as_ref().unwrap() == url {
+ None
+ } else {
+ self.last_url = Some(url);
+ self.last_url.clone()
+ },
+ })
+ })()
+ .map_err(|e| {
+ // On any error, disconnect.
+ self.vars.1.lock().unwrap().shutdown = true;
+ format_error(&bcm, e)
+ }))
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs b/toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs
new file mode 100644
index 0000000000..5545f6bd74
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/in_process/tests.rs
@@ -0,0 +1,628 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+// These are full integration tests that use the BITS service.
+
+// TODO
+// It may make sense to restrict how many tests can run at once. BITS is only supposed to support
+// four simultaneous notifications per user, it is not impossible that this test suite could
+// exceed that.
+
+#![cfg(test)]
+extern crate bits;
+extern crate lazy_static;
+extern crate rand;
+extern crate regex;
+extern crate tempfile;
+
+use std::ffi::{OsStr, OsString};
+use std::fs::{self, File};
+use std::io::{Read, Write};
+use std::net::{SocketAddr, TcpListener, TcpStream};
+use std::panic;
+use std::sync::{Arc, Condvar, Mutex};
+use std::thread::{self, JoinHandle};
+use std::time::{Duration, Instant};
+
+use self::{
+ bits::BackgroundCopyManager,
+ lazy_static::lazy_static,
+ rand::{thread_rng, Rng},
+ regex::bytes::Regex,
+ tempfile::{Builder, TempDir},
+};
+use super::{
+ super::{BitsJobState, Error},
+ BitsProxyUsage, InProcessClient, StartJobSuccess,
+};
+
+static SERVER_ADDRESS: [u8; 4] = [127, 0, 0, 1];
+
+lazy_static! {
+ static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
+}
+
+fn format_job_name(name: &str) -> OsString {
+ format!("InProcessClient Test {}", name).into()
+}
+
+fn format_dir_prefix(tmp_dir: &TempDir) -> OsString {
+ let mut dir = tmp_dir.path().to_path_buf().into_os_string();
+ dir.push("\\");
+ dir
+}
+
+fn cancel_jobs(name: &OsStr) {
+ BackgroundCopyManager::connect()
+ .unwrap()
+ .cancel_jobs_by_name(name)
+ .unwrap();
+}
+
+struct HttpServerResponses {
+ body: Box<[u8]>,
+ delay: u64,
+ //error: Box<[u8]>,
+}
+
+struct MockHttpServerHandle {
+ port: u16,
+ join: Option<JoinHandle<Result<(), ()>>>,
+ shutdown: Arc<(Mutex<bool>, Condvar)>,
+}
+
+impl MockHttpServerHandle {
+ fn shutdown(&mut self) {
+ if self.join.is_none() {
+ return;
+ }
+
+ {
+ let &(ref lock, ref cvar) = &*self.shutdown;
+ let mut shutdown = lock.lock().unwrap();
+
+ if !*shutdown {
+ *shutdown = true;
+ cvar.notify_all();
+ }
+ }
+ // Wake up the server from `accept()`. Will fail if the server wasn't listening.
+ let _ = TcpStream::connect_timeout(
+ &(SERVER_ADDRESS, self.port).into(),
+ Duration::from_millis(10_000),
+ );
+
+ match self.join.take().unwrap().join() {
+ Ok(_) => {}
+ Err(p) => panic::resume_unwind(p),
+ }
+ }
+
+ fn format_url(&self, name: &str) -> OsString {
+ format!(
+ "http://{}/{}",
+ SocketAddr::from((SERVER_ADDRESS, self.port)),
+ name
+ )
+ .into()
+ }
+}
+
+fn mock_http_server(name: &'static str, responses: HttpServerResponses) -> MockHttpServerHandle {
+ let mut bind_retries = 10;
+ let shutdown = Arc::new((Mutex::new(false), Condvar::new()));
+ let caller_shutdown = shutdown.clone();
+
+ let (listener, port) = loop {
+ let port = thread_rng().gen_range(1024..0x1_0000u32) as u16;
+ match TcpListener::bind(SocketAddr::from((SERVER_ADDRESS, port))) {
+ Ok(listener) => {
+ break (listener, port);
+ }
+ r @ Err(_) => {
+ if bind_retries == 0 {
+ r.unwrap();
+ }
+ bind_retries -= 1;
+ continue;
+ }
+ }
+ };
+
+ let join = thread::Builder::new()
+ .name(format!("mock_http_server {}", name))
+ .spawn(move || {
+ // returns Err(()) if server should shut down immediately
+ fn check_shutdown(shutdown: &Arc<(Mutex<bool>, Condvar)>) -> Result<(), ()> {
+ if *shutdown.0.lock().unwrap() {
+ Err(())
+ } else {
+ Ok(())
+ }
+ }
+ fn sleep(shutdown: &Arc<(Mutex<bool>, Condvar)>, delay_millis: u64) -> Result<(), ()> {
+ let sleep_start = Instant::now();
+ let sleep_end = sleep_start + Duration::from_millis(delay_millis);
+
+ let (ref lock, ref cvar) = **shutdown;
+ let mut shutdown_requested = lock.lock().unwrap();
+ loop {
+ if *shutdown_requested {
+ return Err(());
+ }
+
+ let before_wait = Instant::now();
+ if before_wait >= sleep_end {
+ return Ok(());
+ }
+ let wait_dur = sleep_end - before_wait;
+ shutdown_requested = cvar.wait_timeout(shutdown_requested, wait_dur).unwrap().0;
+ }
+ }
+
+ let error_404 = Regex::new(r"^((GET)|(HEAD)) [[:print:]]*/error_404 ").unwrap();
+ let error_500 = Regex::new(r"^((GET)|(HEAD)) [[:print:]]*/error_500 ").unwrap();
+
+ loop {
+ let (mut socket, _addr) = listener.accept().expect("accept should succeed");
+
+ socket
+ .set_read_timeout(Some(Duration::from_millis(10_000)))
+ .unwrap();
+ let mut s = Vec::new();
+ for b in Read::by_ref(&mut socket).bytes() {
+ if b.is_err() {
+ eprintln!("read error {:?}", b);
+ break;
+ }
+ let b = b.unwrap();
+ s.push(b);
+ if s.ends_with(b"\r\n\r\n") {
+ break;
+ }
+ check_shutdown(&shutdown)?;
+ }
+
+ // request received
+
+ check_shutdown(&shutdown)?;
+
+ // Special error URIs
+ if error_404.is_match(&s) {
+ sleep(&shutdown, responses.delay)?;
+ let result = socket
+ .write(b"HTTP/1.1 404 Not Found\r\n\r\n")
+ .and_then(|_| socket.flush());
+ if let Err(e) = result {
+ eprintln!("error writing 404 header {:?}", e);
+ }
+ continue;
+ }
+
+ if error_500.is_match(&s) {
+ sleep(&shutdown, responses.delay)?;
+ let result = socket
+ .write(b"HTTP/1.1 500 Internal Server Error\r\n\r\n")
+ .and_then(|_| socket.flush());
+ if let Err(e) = result {
+ eprintln!("error writing 500 header {:?}", e);
+ }
+ continue;
+ }
+
+ // Response with a body.
+ if s.starts_with(b"HEAD") || s.starts_with(b"GET") {
+ let result = socket
+ .write(
+ format!(
+ "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n",
+ responses.body.len()
+ )
+ .as_bytes(),
+ )
+ .and_then(|_| socket.flush());
+ if let Err(e) = result {
+ eprintln!("error writing header {:?}", e);
+ continue;
+ }
+ }
+
+ if s.starts_with(b"GET") {
+ sleep(&shutdown, responses.delay)?;
+ let result = socket.write(&responses.body).and_then(|_| socket.flush());
+ if let Err(e) = result {
+ eprintln!("error writing content {:?}", e);
+ continue;
+ }
+ }
+ }
+ });
+
+ MockHttpServerHandle {
+ port,
+ join: Some(join.unwrap()),
+ shutdown: caller_shutdown,
+ }
+}
+
+// Test wrapper to ensure jobs are canceled, set up name strings
+macro_rules! test {
+ (fn $name:ident($param:ident : &str, $tmpdir:ident : &TempDir) $body:block) => {
+ #[test]
+ fn $name() {
+ let $param = stringify!($name);
+ let $tmpdir = &Builder::new().prefix($param).tempdir().unwrap();
+
+ let result = panic::catch_unwind(|| $body);
+
+ cancel_jobs(&format_job_name($param));
+
+ if let Err(e) = result {
+ panic::resume_unwind(e);
+ }
+ }
+ };
+}
+
+test! {
+ fn start_monitor_and_cancel(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 10_000,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), tmp_dir.path().into()).unwrap();
+
+ let no_progress_timeout_secs = 60;
+ let interval = 10_000;
+ let timeout = 10_000;
+
+ let (StartJobSuccess {guid}, mut monitor) =
+ client.start_job(
+ server.format_url(name),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ // cancel in ~250ms
+ let _join = thread::Builder::new()
+ .spawn(move || {
+ thread::sleep(Duration::from_millis(250));
+ client.cancel_job(guid).unwrap();
+ });
+
+ let start = Instant::now();
+
+ // First immediate report
+ monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+ // ~250ms the cancel should cause an immediate disconnect (otherwise we wouldn't get
+ // an update until 10s when the transfer completes or the interval expires)
+ match monitor.get_status(timeout) {
+ Err(Error::NotConnected) => {},
+ Ok(r) => panic!("unexpected success from get_status() {:?}", r),
+ Err(e) => panic!("unexpected failure from get_status() {:?}", e),
+ }
+ assert!(start.elapsed() < Duration::from_millis(9_000));
+
+ server.shutdown();
+ }
+}
+
+test! {
+ fn start_monitor_and_complete(name: &str, tmp_dir: &TempDir) {
+ let file_path = tmp_dir.path().join(name);
+
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 500,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 60;
+ let interval = 100;
+ let timeout = 10_000;
+
+ let (StartJobSuccess {guid}, mut monitor) =
+ client.start_job(
+ server.format_url(name),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ let start = Instant::now();
+
+ // Get status reports until transfer finishes (~500ms)
+ let mut completed = false;
+ loop {
+ match monitor.get_status(timeout) {
+ Err(e) => {
+ if completed {
+ break;
+ } else {
+ panic!("monitor failed before completion {:?}", e);
+ }
+ }
+ Ok(Ok(status)) => match BitsJobState::from(status.state) {
+ BitsJobState::Queued | BitsJobState::Connecting
+ | BitsJobState::Transferring => {
+ //eprintln!("{:?}", BitsJobState::from(status.state));
+ //eprintln!("{:?}", status);
+
+ // As long as there is no error, setting the timeout to 0 will not
+ // fail an active transfer.
+ client.set_no_progress_timeout(guid.clone(), 0).unwrap();
+ }
+ BitsJobState::Transferred => {
+ client.complete_job(guid.clone()).unwrap();
+ completed = true;
+ }
+ _ => {
+ panic!("{:?}", status);
+ }
+ }
+ Ok(Err(e)) => panic!("{:?}", e),
+ }
+
+ // Timeout to prevent waiting forever
+ assert!(start.elapsed() < Duration::from_millis(60_000));
+ }
+
+
+ // Verify file contents
+ let result = panic::catch_unwind(|| {
+ let mut file = File::open(file_path.clone()).unwrap();
+ let mut v = Vec::new();
+ file.read_to_end(&mut v).unwrap();
+ assert_eq!(v, name.as_bytes());
+ });
+
+ let _ = fs::remove_file(file_path);
+
+ if let Err(e) = result {
+ panic::resume_unwind(e);
+ }
+
+ // Save this for last to ensure the file is removed.
+ server.shutdown();
+ }
+}
+
+test! {
+ fn async_transferred_notification(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 250,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 60;
+ let interval = 60_000;
+ let timeout = 10_000;
+
+ let (_, mut monitor) =
+ client.start_job(
+ server.format_url(name),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ // Start the timer now, the initial job creation may be delayed by BITS service startup.
+ let start = Instant::now();
+
+ // First report, immediate
+ let report_1 = monitor.get_status(timeout).expect("should initially be ok").unwrap();
+ let elapsed_to_report_1 = start.elapsed();
+
+ // Transferred notification should come when the job completes in ~250 ms, otherwise we
+ // will be stuck until timeout.
+ let report_2 = monitor.get_status(timeout).expect("should get status update").unwrap();
+ let elapsed_to_report_2 = start.elapsed();
+ assert!(elapsed_to_report_2 < Duration::from_millis(9_000));
+ assert_eq!(report_2.state, BitsJobState::Transferred);
+
+ let short_timeout = 500;
+ let report_3 = monitor.get_status(short_timeout);
+ let elapsed_to_report_3 = start.elapsed();
+
+ if let Ok(report_3) = report_3 {
+ panic!("should be disconnected\n\
+ report_1 ({}.{:03}): {:?}\n\
+ report_2 ({}.{:03}): {:?}\n\
+ report_3 ({}.{:03}): {:?}",
+ elapsed_to_report_1.as_secs(), elapsed_to_report_1.subsec_millis(), report_1,
+ elapsed_to_report_2.as_secs(), elapsed_to_report_2.subsec_millis(), report_2,
+ elapsed_to_report_3.as_secs(), elapsed_to_report_3.subsec_millis(), report_3,
+ );
+ }
+
+ server.shutdown();
+
+ // job will be cancelled by macro
+ }
+}
+
+test! {
+ fn change_interval(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 1000,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 0;
+ let interval = 60_000;
+ let timeout = 10_000;
+
+ let (StartJobSuccess { guid }, mut monitor) =
+ client.start_job(
+ server.format_url(name),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ let start = Instant::now();
+
+ // reduce monitor interval in ~250ms to 500ms
+ let _join = thread::Builder::new()
+ .spawn(move || {
+ thread::sleep(Duration::from_millis(250));
+ client.set_update_interval(guid, 500).unwrap();
+ });
+
+ // First immediate report
+ monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+ // Next report should be rescheduled to 500ms by the spawned thread, otherwise no status
+ // until the original 10s interval.
+ monitor.get_status(timeout).expect("expected second status").unwrap();
+ assert!(start.elapsed() < Duration::from_millis(9_000));
+ assert!(start.elapsed() > Duration::from_millis(400));
+
+ server.shutdown();
+
+ // job will be cancelled by macro
+ }
+}
+
+test! {
+ fn async_error_notification(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 100,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 60;
+ let interval = 60_000;
+ let timeout = 10_000;
+
+ let (_, mut monitor) =
+ client.start_job(
+ server.format_url("error_404"),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ // Start the timer now, the initial job creation may be delayed by BITS service startup.
+ let start = Instant::now();
+
+ // First immediate report
+ monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+ // Error notification should come with HEAD response in 100ms, otherwise no status until
+ // 10s interval or timeout.
+ let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+ assert!(start.elapsed() < Duration::from_millis(9_000));
+ assert_eq!(status.state, BitsJobState::Error);
+
+ server.shutdown();
+
+ // job will be cancelled by macro
+ }
+}
+
+test! {
+ fn transient_error(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 100,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 60;
+ let interval = 1_000;
+ let timeout = 10_000;
+
+ let (StartJobSuccess { guid }, mut monitor) =
+ client.start_job(
+ server.format_url("error_500"),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ // Start the timer now, the initial job creation may be delayed by BITS service startup.
+ let start = Instant::now();
+
+ // First immediate report
+ monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+ // Transient error notification should come when the interval expires in ~1s.
+ let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+ assert!(start.elapsed() > Duration::from_millis(900));
+ assert!(start.elapsed() < Duration::from_millis(9_000));
+ assert_eq!(status.state, BitsJobState::TransientError);
+
+ // Lower no progress timeout to 0
+ let set_timeout_at = Instant::now();
+ client.set_no_progress_timeout(guid, 0).unwrap();
+
+ // Should convert the transient error to a permanent error immediately.
+ let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+ assert!(set_timeout_at.elapsed() < Duration::from_millis(500));
+ assert_eq!(status.state, BitsJobState::Error);
+
+ server.shutdown();
+
+ // job will be cancelled by macro
+ }
+}
+
+test! {
+ fn transient_to_permanent_error(name: &str, tmp_dir: &TempDir) {
+ let mut server = mock_http_server(name, HttpServerResponses {
+ body: name.to_owned().into_boxed_str().into_boxed_bytes(),
+ delay: 100,
+ });
+
+ let mut client = InProcessClient::new(format_job_name(name), format_dir_prefix(tmp_dir)).unwrap();
+
+ let no_progress_timeout_secs = 0;
+ let interval = 1_000;
+ let timeout = 10_000;
+
+ let (_, mut monitor) =
+ client.start_job(
+ server.format_url("error_500"),
+ name.into(),
+ BitsProxyUsage::Preconfig,
+ no_progress_timeout_secs,
+ interval,
+ ).unwrap();
+
+ // Start the timer now, the initial job creation may be delayed by BITS service startup.
+ let start = Instant::now();
+
+ // First immediate report
+ monitor.get_status(timeout).expect("should initially be ok").unwrap();
+
+ // 500 is a transient error, but with no_progress_timeout_secs = 0 it should immediately
+ // produce an error notification with the HEAD response in 100ms. Otherwise no status
+ // until 10s interval or timeout.
+ let status = monitor.get_status(timeout).expect("should get status update").unwrap();
+ assert!(start.elapsed() < Duration::from_millis(500));
+ assert_eq!(status.state, BitsJobState::Error);
+
+ server.shutdown();
+
+ // job will be cancelled by macro
+ }
+}
diff --git a/toolkit/components/bitsdownload/bits_client/src/lib.rs b/toolkit/components/bitsdownload/bits_client/src/lib.rs
new file mode 100644
index 0000000000..7db887bbc5
--- /dev/null
+++ b/toolkit/components/bitsdownload/bits_client/src/lib.rs
@@ -0,0 +1,258 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! An interface for managing and monitoring BITS jobs.
+//!
+//! BITS is a Windows service for performing downloads in the background, independent from an
+//! application, usually via HTTP/HTTPS.
+//!
+//! [`BitsClient`](enum.BitsClient.html) is the main interface, used to issue commands.
+//!
+//! [`BitsMonitorClient`](enum.BitsMonitorClient.html) delivers periodic status reports about a
+//! job.
+//!
+//! Microsoft's documentation for BITS can be found at
+//! <https://docs.microsoft.com/en-us/windows/desktop/Bits/background-intelligent-transfer-service-portal>
+
+extern crate bits;
+extern crate comedy;
+extern crate guid_win;
+extern crate thiserror;
+
+pub mod bits_protocol;
+
+mod in_process;
+
+use std::ffi;
+
+use bits_protocol::*;
+use thiserror::Error;
+
+pub use bits::status::{BitsErrorContext, BitsJobState, BitsJobTimes};
+pub use bits::{BitsJobProgress, BitsJobStatus, BitsProxyUsage};
+pub use bits_protocol::{JobError, JobStatus};
+pub use comedy::HResult;
+pub use guid_win::Guid;
+
+// These errors would come from a Local Service client but are mostly unused currently.
+// PipeError properly lives in the crate that deals with named pipes, but it isn't in use now.
+#[derive(Clone, Debug, Eq, Error, PartialEq)]
+pub enum PipeError {
+ #[error("Pipe is not connected")]
+ NotConnected,
+ #[error("Operation timed out")]
+ Timeout,
+ #[error("Should have written {0} bytes, wrote {1}")]
+ WriteCount(usize, u32),
+ #[error("Windows API error")]
+ Api(#[from] HResult),
+}
+
+pub use PipeError as Error;
+
+/// A client for interacting with BITS.
+///
+/// Methods on `BitsClient` return a `Result<Result<_, XyzFailure>, Error>`. The outer `Result`
+/// is `Err` if there was a communication error in sending the associated command or receiving
+/// its response. Currently this is always `Ok` as all clients are in-process. The inner
+/// `Result` is `Err` if there was an error executing the command.
+///
+/// A single `BitsClient` can be used with multiple BITS jobs simultaneously; generally a job
+/// is not bound tightly to a client.
+///
+/// A `BitsClient` tracks all [`BitsMonitorClient`s](enum.BitsMonitorClient.html) that it started
+/// with `start_job()` or `monitor_job()`, so that the monitor can be stopped or modified.
+pub enum BitsClient {
+ // The `InProcess` variant does all BITS calls directly.
+ #[doc(hidden)]
+ InProcess(in_process::InProcessClient),
+ // Space is reserved here for the LocalService variant, which will work through an external
+ // process running as Local Service.
+}
+
+use BitsClient::InProcess;
+
+impl BitsClient {
+ /// Create an in-process `BitsClient`.
+ ///
+ /// `job_name` will be used when creating jobs, and this `BitsClient` can only be used to
+ /// manipulate jobs with that name.
+ ///
+ /// `save_path_prefix` will be prepended to the local `save_path` given to `start_job()`, it
+ /// must name an existing directory.
+ pub fn new(
+ job_name: ffi::OsString,
+ save_path_prefix: ffi::OsString,
+ ) -> Result<BitsClient, Error> {
+ Ok(InProcess(in_process::InProcessClient::new(
+ job_name,
+ save_path_prefix,
+ )?))
+ }
+
+ /// Start a job to download a single file at `url` to local path `save_path` (relative to the
+ /// `save_path_prefix` given when constructing the `BitsClient`).
+ ///
+ /// `save_path_prefix` combined with `save_path` must name a file (existing or not) in an
+ /// existing directory, which must be under the directory named by `save_path_prefix`.
+ ///
+ /// `proxy_usage` determines what proxy will be used.
+ ///
+ /// When a successful result `Ok(result)` is returned, `result.0.guid` is the id for the
+ /// new job, and `result.1` is a monitor client that can be polled for periodic updates,
+ /// returning a result approximately once per `monitor_interval_millis` milliseconds.
+ pub fn start_job(
+ &mut self,
+ url: ffi::OsString,
+ save_path: ffi::OsString,
+ proxy_usage: BitsProxyUsage,
+ no_progress_timeout_secs: u32,
+ monitor_interval_millis: u32,
+ ) -> Result<Result<(StartJobSuccess, BitsMonitorClient), StartJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client
+ .start_job(
+ url,
+ save_path,
+ proxy_usage,
+ no_progress_timeout_secs,
+ monitor_interval_millis,
+ )
+ .map(|(success, monitor)| (success, BitsMonitorClient::InProcess(monitor)))),
+ }
+ }
+
+ /// Start monitoring the job with id `guid` approximately once per `monitor_interval_millis`
+ /// milliseconds.
+ ///
+ /// The returned `Ok(monitor)` is a monitor client to be polled for periodic updates.
+ ///
+ /// There can only be one ongoing `BitsMonitorClient` for each job associated with a given
+ /// `BitsClient`. If a monitor client already exists for the specified job, it will be stopped.
+ pub fn monitor_job(
+ &mut self,
+ guid: Guid,
+ interval_millis: u32,
+ ) -> Result<Result<BitsMonitorClient, MonitorJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client
+ .monitor_job(guid, interval_millis)
+ .map(BitsMonitorClient::InProcess)),
+ }
+ }
+
+ /// Suspend job `guid`.
+ pub fn suspend_job(&mut self, guid: Guid) -> Result<Result<(), SuspendJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.suspend_job(guid)),
+ }
+ }
+
+ /// Resume job `guid`.
+ pub fn resume_job(&mut self, guid: Guid) -> Result<Result<(), ResumeJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.resume_job(guid)),
+ }
+ }
+
+ /// Set the priority of job `guid`.
+ ///
+ /// `foreground == true` will set the priority to `BG_JOB_PRIORITY_FOREGROUND`,
+ /// `false` will use the default `BG_JOB_PRIORITY_NORMAL`.
+ /// See the Microsoft documentation for `BG_JOB_PRIORITY` for details.
+ ///
+ /// A job created by `start_job()` will be foreground priority, by default.
+ pub fn set_job_priority(
+ &mut self,
+ guid: Guid,
+ foreground: bool,
+ ) -> Result<Result<(), SetJobPriorityFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.set_job_priority(guid, foreground)),
+ }
+ }
+
+ /// Set the "no progress timeout" of job `guid`.
+ pub fn set_no_progress_timeout(
+ &mut self,
+ guid: Guid,
+ timeout_secs: u32,
+ ) -> Result<Result<(), SetNoProgressTimeoutFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.set_no_progress_timeout(guid, timeout_secs)),
+ }
+ }
+
+ /// Change the update interval for an ongoing monitor of job `guid`.
+ pub fn set_update_interval(
+ &mut self,
+ guid: Guid,
+ interval_millis: u32,
+ ) -> Result<Result<(), SetUpdateIntervalFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.set_update_interval(guid, interval_millis)),
+ }
+ }
+
+ /// Stop any ongoing monitor for job `guid`.
+ pub fn stop_update(
+ &mut self,
+ guid: Guid,
+ ) -> Result<Result<(), SetUpdateIntervalFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.stop_update(guid)),
+ }
+ }
+
+ /// Complete the job `guid`.
+ ///
+ /// This also stops any ongoing monitor for the job.
+ pub fn complete_job(&mut self, guid: Guid) -> Result<Result<(), CompleteJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.complete_job(guid)),
+ }
+ }
+
+ /// Cancel the job `guid`.
+ ///
+ /// This also stops any ongoing monitor for the job.
+ pub fn cancel_job(&mut self, guid: Guid) -> Result<Result<(), CancelJobFailure>, Error> {
+ match self {
+ InProcess(client) => Ok(client.cancel_job(guid)),
+ }
+ }
+}
+
+/// The client side of a monitor for a BITS job.
+///
+/// It is intended to be used by calling `get_status` in a loop to receive notifications about
+/// the status of a job. Because `get_status` blocks, it is recommended to run this loop on its
+/// own thread.
+pub enum BitsMonitorClient {
+ InProcess(in_process::InProcessMonitor),
+}
+
+impl BitsMonitorClient {
+ /// `get_status` will return a result approximately every `monitor_interval_millis`
+ /// milliseconds, but in case a result isn't available within `timeout_millis` milliseconds
+ /// this will return `Err(Error::Timeout)`. Any `Err` returned, including timeout, indicates
+ /// that the monitor has been stopped; the `BitsMonitorClient` should then be discarded.
+ ///
+ /// As with methods on `BitsClient`, `BitsMonitorClient::get_status()` has an inner `Result`
+ /// type which indicates an error returned from the server. Any `Err` here also indicates that
+ /// the monitor has stopped after yielding the result.
+ ///
+ /// The first time `get_status` is called it will return a status without any delay.
+ ///
+ /// If there is an error or the transfer completes, a result may be available sooner than
+ /// the monitor interval.
+ pub fn get_status(
+ &mut self,
+ timeout_millis: u32,
+ ) -> Result<Result<JobStatus, HResultMessage>, Error> {
+ match self {
+ BitsMonitorClient::InProcess(client) => client.get_status(timeout_millis),
+ }
+ }
+}
diff --git a/toolkit/components/bitsdownload/components.conf b/toolkit/components/bitsdownload/components.conf
new file mode 100644
index 0000000000..fe7090d366
--- /dev/null
+++ b/toolkit/components/bitsdownload/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = []
+if defined('MOZ_BITS_DOWNLOAD'):
+ Classes += [
+ {
+ 'cid': '{495d6f3d-9748-4d30-8ce5-0290c0001edf}',
+ 'contract_ids': ['@mozilla.org/bits;1'],
+ 'singleton': True,
+ 'constructor': 'GetBitsService',
+ 'headers': ['Bits.h'],
+ },
+ ]
diff --git a/toolkit/components/bitsdownload/moz.build b/toolkit/components/bitsdownload/moz.build
new file mode 100644
index 0000000000..cbed083f03
--- /dev/null
+++ b/toolkit/components/bitsdownload/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_MODULE = "Bits"
+
+XPIDL_SOURCES += [
+ "nsIBits.idl",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+if CONFIG["MOZ_BITS_DOWNLOAD"]:
+ EXPORTS += ["Bits.h"]
+ UNIFIED_SOURCES += ["Bits.cpp"]
+
+EXTRA_JS_MODULES += [
+ "Bits.jsm",
+]
+
+FINAL_LIBRARY = "xul"
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Application Update")
+
+REQUIRES_UNIFIED_BUILD = True
diff --git a/toolkit/components/bitsdownload/nsIBits.idl b/toolkit/components/bitsdownload/nsIBits.idl
new file mode 100644
index 0000000000..b0dd48b7cc
--- /dev/null
+++ b/toolkit/components/bitsdownload/nsIBits.idl
@@ -0,0 +1,427 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIRequest.idl"
+
+interface nsIRequest;
+interface nsIRequestObserver;
+
+interface nsIBitsRequest;
+
+interface nsIBitsNewRequestCallback;
+interface nsIBitsCallback;
+
+typedef long nsProxyUsage;
+typedef long nsBitsErrorType;
+typedef long nsBitsErrorAction;
+typedef long nsBitsErrorStage;
+
+/**
+ * An interface for interacting with Windows Background Intelligent Transfer
+ * Service. This should only be used on Windows.
+ *
+ * It would be preferable for the functions in this interface to return
+ * Promises, but this interface is implemented in Rust, which does not yet have
+ * support for Promises. There is a JS wrapper around this class that should be
+ * preferred over using this interface directly, located in Bits.jsm.
+ *
+ * Methods of this class that take a nsIBitsNewRequestCallback do not return or
+ * throw errors. All errors will be reported through the callback. The only
+ * things that should cause methods to directly throw errors are null arguments.
+ */
+[scriptable, uuid(495d6f3d-9748-4d30-8ce5-0290c0001edf)]
+interface nsIBits : nsISupports
+{
+ /**
+ * nsBitsErrorType values
+ * The BITS interface returns many error codes. These are intended to help
+ * determine appropriate fallback actions and to report to telemetry.
+ */
+ const long ERROR_TYPE_SUCCESS = 0;
+ const long ERROR_TYPE_UNKNOWN = 1;
+ const long ERROR_TYPE_METHOD_THREW = 2;
+ const long ERROR_TYPE_METHOD_TIMEOUT = 3;
+ const long ERROR_TYPE_NULL_ARGUMENT = 4;
+ const long ERROR_TYPE_INVALID_ARGUMENT = 5;
+ const long ERROR_TYPE_NOT_INITIALIZED = 6;
+ const long ERROR_TYPE_NO_UTF8_CONVERSION = 7;
+ const long ERROR_TYPE_INVALID_GUID = 8;
+ const long ERROR_TYPE_PIPE_NOT_CONNECTED = 9;
+ const long ERROR_TYPE_PIPE_TIMEOUT = 10;
+ const long ERROR_TYPE_PIPE_BAD_WRITE_COUNT = 11;
+ const long ERROR_TYPE_PIPE_API_ERROR = 12;
+ const long ERROR_TYPE_FAILED_TO_CREATE_BITS_JOB = 13;
+ const long ERROR_TYPE_FAILED_TO_ADD_FILE_TO_JOB = 14;
+ const long ERROR_TYPE_FAILED_TO_APPLY_BITS_JOB_SETTINGS = 15;
+ const long ERROR_TYPE_FAILED_TO_RESUME_BITS_JOB = 16;
+ const long ERROR_TYPE_OTHER_BITS_ERROR = 17;
+ const long ERROR_TYPE_OTHER_BITS_CLIENT_ERROR = 18;
+ const long ERROR_TYPE_BITS_JOB_NOT_FOUND = 19;
+ const long ERROR_TYPE_FAILED_TO_GET_BITS_JOB = 20;
+ const long ERROR_TYPE_FAILED_TO_SUSPEND_BITS_JOB = 21;
+ const long ERROR_TYPE_FAILED_TO_COMPLETE_BITS_JOB = 22;
+ const long ERROR_TYPE_PARTIALLY_COMPLETED_BITS_JOB = 23;
+ const long ERROR_TYPE_FAILED_TO_CANCEL_BITS_JOB = 24;
+ const long ERROR_TYPE_MISSING_RESULT_DATA = 25;
+ const long ERROR_TYPE_MISSING_CALLBACK = 26;
+ const long ERROR_TYPE_CALLBACK_ON_WRONG_THREAD = 27;
+ const long ERROR_TYPE_MISSING_BITS_SERVICE = 28;
+ const long ERROR_TYPE_BITS_SERVICE_ON_WRONG_THREAD = 29;
+ const long ERROR_TYPE_MISSING_BITS_REQUEST = 30;
+ const long ERROR_TYPE_BITS_REQUEST_ON_WRONG_THREAD = 31;
+ const long ERROR_TYPE_MISSING_OBSERVER = 32;
+ const long ERROR_TYPE_OBSERVER_ON_WRONG_THREAD = 33;
+ const long ERROR_TYPE_MISSING_CONTEXT = 34;
+ const long ERROR_TYPE_CONTEXT_ON_WRONG_THREAD = 35;
+ const long ERROR_TYPE_FAILED_TO_START_THREAD = 36;
+ const long ERROR_TYPE_FAILED_TO_CONSTRUCT_TASK_RUNNABLE = 37;
+ const long ERROR_TYPE_FAILED_TO_DISPATCH_RUNNABLE = 38;
+ const long ERROR_TYPE_TRANSFER_ALREADY_COMPLETE = 39;
+ const long ERROR_TYPE_OPERATION_ALREADY_IN_PROGRESS = 40;
+ const long ERROR_TYPE_MISSING_BITS_CLIENT = 41;
+ const long ERROR_TYPE_FAILED_TO_GET_JOB_STATUS = 42;
+ const long ERROR_TYPE_BITS_STATE_ERROR = 43;
+ const long ERROR_TYPE_BITS_STATE_TRANSIENT_ERROR = 44;
+ const long ERROR_TYPE_BITS_STATE_CANCELLED = 45;
+ const long ERROR_TYPE_BITS_STATE_UNEXPECTED = 46;
+ const long ERROR_TYPE_VERIFICATION_FAILURE = 47;
+ const long ERROR_TYPE_ACCESS_DENIED_EXPECTED = 48;
+ const long ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM = 49;
+ const long ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN = 50;
+
+ /**
+ * nsBitsErrorAction values
+ * These values indicate where the error occurred.
+ */
+ const long ERROR_ACTION_UNKNOWN = 1;
+ const long ERROR_ACTION_NONE = 2;
+ const long ERROR_ACTION_START_DOWNLOAD = 3;
+ const long ERROR_ACTION_MONITOR_DOWNLOAD = 4;
+ const long ERROR_ACTION_CHANGE_MONITOR_INTERVAL = 5;
+ const long ERROR_ACTION_CANCEL = 6;
+ const long ERROR_ACTION_SET_PRIORITY = 7;
+ const long ERROR_ACTION_COMPLETE = 8;
+ const long ERROR_ACTION_SUSPEND = 9;
+ const long ERROR_ACTION_RESUME = 10;
+ const long ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT = 11;
+
+ /**
+ * nsBitsErrorStage values
+ * These values allow the caller to determine at what point in the download
+ * mechanism a failure occurred.
+ */
+ const long ERROR_STAGE_UNKNOWN = 1;
+ const long ERROR_STAGE_PRETASK = 2;
+ const long ERROR_STAGE_COMMAND_THREAD = 3;
+ const long ERROR_STAGE_AGENT_COMMUNICATION = 4;
+ const long ERROR_STAGE_BITS_CLIENT = 5;
+ const long ERROR_STAGE_MAIN_THREAD = 6;
+ const long ERROR_STAGE_MONITOR = 7;
+ const long ERROR_STAGE_VERIFICATION = 8;
+
+ /**
+ * These values indicate what type of error code was returned. These are used
+ * to allow the different types taken by the different callback failure
+ * functions to be made into one generic error type in Javascript.
+ */
+ const long ERROR_CODE_TYPE_NONE = 1;
+ const long ERROR_CODE_TYPE_NSRESULT = 2;
+ const long ERROR_CODE_TYPE_HRESULT = 3;
+ const long ERROR_CODE_TYPE_STRING = 4;
+ const long ERROR_CODE_TYPE_EXCEPTION = 5;
+
+ /**
+ * Indicates whether init() has been called.
+ */
+ readonly attribute boolean initialized;
+
+ /**
+ * Initializes the BITS interface. Unlike other functions here, this happens
+ * synchronously.
+ * init() should only be called only once.
+ *
+ * @param jobName
+ * The name of the BITS job. This is used both to set the name during
+ * job creation and to verify that a job is ours.
+ * @param savePathPrefix
+ * The directory that downloads will be saved to. Providing a safe
+ * directory here ensures that the download path cannot be manipulated
+ * to save files to a malicious location. Downloads are guaranteed to
+ * be saved to this directory or a subdirectory.
+ * @param monitorTimeoutMs
+ * The amount of time to wait between download monitor notifications.
+ * This should be larger than the largest monitorIntervalMs that will
+ * be passed to startDownload(), monitorDownload(), or
+ * changeMonitorInterval(). This value may not be 0.
+ */
+ void init(in AUTF8String jobName,
+ in AUTF8String savePathPrefix,
+ in unsigned long monitorTimeoutMs);
+
+ /**
+ * Downloads the specified URL to the specified location within the
+ * savePathPrefix passed to init().
+ *
+ * @param downloadURL
+ * The URL to be downloaded.
+ * @param saveRelativePath
+ * The location to download to. The path given should be a path
+ * relative to the savePathPrefix passed to init(). If this attempts to
+ * escape the directory specified by savePathPrefix, this call will
+ * fail (ex: Don't pass "../filename").
+ * @param proxy
+ * Specifies what proxy to use when downloading. Valid values are
+ * listed below.
+ * @param noProgressTimeoutSecs
+ * The number of seconds for the "no progress" timeout. After there has
+ * been no download progress for this long, BITS will not retry the job
+ * following a transient error, producing instead a permanent error.
+ * @param monitorIntervalMs
+ * The number of milliseconds between download status notifications.
+ * @param observer
+ * An observer to be notified of various events. OnStartRequest is
+ * called once the BITS job has been created. OnStopRequest is called
+ * when the file transfer has completed or when an error occurs. If
+ * this object implements nsIProgressEventSink, then its OnProgress
+ * method will be called as data is transferred.
+ * IMPORTANT NOTE: When OnStopRequest is called, the download has
+ * completed, but nsIBitsRequest::complete() still
+ * needs to be called to save the file to the
+ * filesystem.
+ * @param context
+ * User defined object forwarded to the observer's onProgress method.
+ * This parameter, unlike others for this interface, can be passed a
+ * null pointer.
+ * @param callback
+ * The callback used to relay the response from BITS.
+ */
+ void startDownload(in AUTF8String downloadURL,
+ in AUTF8String saveRelativePath,
+ in nsProxyUsage proxy,
+ in unsigned long noProgressTimeoutSecs,
+ in unsigned long monitorIntervalMs,
+ in nsIRequestObserver observer,
+ in nsISupports context,
+ in nsIBitsNewRequestCallback callback);
+
+ // nsProxyUsage values
+ const long PROXY_NONE = 1;
+ const long PROXY_PRECONFIG = 2;
+ const long PROXY_AUTODETECT = 3;
+
+ /**
+ * Similar to startDownload, but connects to a BITS transfer that has already
+ * been started.
+ *
+ * @param id
+ * The GUID of the download to monitor.
+ * @param monitorIntervalMs
+ * The number of milliseconds between download status notifications.
+ * @param observer
+ * An observer to be notified of various events. OnStartRequest is
+ * called once the BITS job has been created. OnStopRequest is called
+ * when the file transfer has completed or when an error occurs. If
+ * this object implements nsIProgressEventSink, then its OnProgress
+ * method will be called as data is transferred.
+ * IMPORTANT NOTE: When OnStopRequest is called, the download has
+ * completed, but nsIBitsRequest::complete() still
+ * needs to be called to save the file to the
+ * filesystem.
+ * @param context
+ * User defined object forwarded to the observer's onProgress method.
+ * This parameter, unlike others for this interface, can be passed a
+ * null pointer.
+ * @param callback
+ * The callback used to relay the response from BITS.
+ */
+ void monitorDownload(in AUTF8String id,
+ in unsigned long monitorIntervalMs,
+ in nsIRequestObserver observer,
+ in nsISupports context,
+ in nsIBitsNewRequestCallback callback);
+};
+
+/**
+ * This callback interface is for use by the nsIBits interface for returning
+ * results asynchronously to the caller.
+ */
+[scriptable, uuid(aa12e433-5b9f-452d-b5c9-840a9541328b)]
+interface nsIBitsNewRequestCallback : nsISupports
+{
+ void success(in nsIBitsRequest request);
+ void failure(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage);
+ void failureNsresult(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in nsresult errorCode);
+ void failureHresult(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in long errorCode);
+ void failureString(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in AUTF8String errorMessage);
+};
+
+/*
+ * An interface for managing a running BITS download.
+ *
+ * It would be preferable for the functions in this interface to return
+ * Promises, but this interface is implemented in Rust, which does not yet have
+ * support for Promises. There is a JS wrapper around this class that should be
+ * preferred over using this interface directly, located in Bits.jsm.
+ *
+ * Methods of this class that take a nsIBitsCallback do not return or throw
+ * errors. All errors will be reported through the callback. The only
+ * things that should cause methods to directly throw errors are null arguments.
+ *
+ * Note: Although the nsIBits interface derives from nsIRequest, implementations
+ * may not implement the loadGroup or loadFlags attributes.
+ *
+ * Note: Once the file transfer has stopped (due to completion or error),
+ * calling any method besides complete() or cancel() will result in an
+ * error with type nsIBits::ERROR_TYPE_TRANSFER_ALREADY_COMPLETE.
+ * Calling complete() or cancel() again after either has already been
+ * called will also result in an ERROR_TYPE_TRANSFER_ALREADY_COMPLETE
+ * error.
+ * Attributes and nsIRequest::isPending() can still be accessed at any
+ * time.
+ */
+[scriptable, uuid(ab9da0e9-06bf-4e73-bb1b-c0f2ea9ecc3e)]
+interface nsIBitsRequest : nsIRequest
+{
+ /**
+ * The BITS id of the download. This will be a string representing a UUID.
+ */
+ readonly attribute AUTF8String bitsId;
+
+ /**
+ * The transfer result of the download, meant to be accessed after the
+ * transfer has stopped (i.e. after the observer's onStopRequest method has
+ * been called). Will be nsIBits::ERROR_TYPE_SUCCESS if the transfer is
+ * successful (and before transfer completion). If the transfer failed, this
+ * will be a different nsBitsErrorType value indicating the cause of the
+ * failure.
+ */
+ readonly attribute nsBitsErrorType transferError;
+
+ /**
+ * Requests a change to the frequency that Firefox is receiving download
+ * status notifications.
+ *
+ * @param monitorIntervalMs
+ * The new number of milliseconds between download status
+ * notifications.
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void changeMonitorInterval(in unsigned long monitorIntervalMs,
+ in nsIBitsCallback callback);
+
+ /**
+ * Cancels the download. This function is named this way to avoid conflict
+ * with nsIRequest::cancel.
+ *
+ * @param status
+ * The reason for cancelling the request. This must be a failure code
+ * rather than a success code like NS_OK.
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void cancelAsync(in nsresult status,
+ in nsIBitsCallback callback);
+
+ /**
+ * Sets the priority of the BITS job to high (i.e. foreground download).
+ *
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void setPriorityHigh(in nsIBitsCallback callback);
+
+ /**
+ * Sets the priority of the BITS job to low (i.e. background download).
+ *
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void setPriorityLow(in nsIBitsCallback callback);
+
+ /**
+ * Sets the BITS "no progress" timeout for the job.
+ *
+ * @param timeoutSecs
+ * The new number of seconds for the timeout. After there has been
+ * no progress for this long, BITS will not retry the job following
+ * a transient error, producing instead a permanent error.
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void setNoProgressTimeout(in unsigned long timeoutSecs,
+ in nsIBitsCallback callback);
+
+ /*
+ * Completes the download, moving it out of the BITS system and onto the
+ * disk location specified when startDownload was called.
+ *
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void complete(in nsIBitsCallback callback);
+
+ /*
+ * Suspends the download, preventing more data from being transferred until
+ * the download is resumed. This function is named this way to avoid conflict
+ * with nsIRequest::suspend.
+ *
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void suspendAsync(in nsIBitsCallback callback);
+
+ /*
+ * Resumes a previously suspended download. This function is named this way
+ * to avoid conflict with nsIRequest::resume.
+ *
+ * @param callback
+ * The callback function used to relay success or failure.
+ */
+ void resumeAsync(in nsIBitsCallback callback);
+};
+
+/**
+ * This callback interface is for use by the nsIBitsRequest interface for
+ * returning results asynchronously to the caller.
+ */
+[scriptable, uuid(ea657e66-6bad-4e41-84d9-c6d107e9799d)]
+interface nsIBitsCallback : nsISupports
+{
+ void success();
+ void failure(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage);
+ void failureNsresult(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in nsresult errorCode);
+ void failureHresult(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in long errorCode);
+ void failureString(in nsBitsErrorType errorType,
+ in nsBitsErrorAction errorAction,
+ in nsBitsErrorStage errorStage,
+ in AUTF8String errorMessage);
+};
+
+%{C++
+#define NS_BITS_CID \
+ { 0xa334de05, 0xb9de, 0x46a1, \
+ { 0x98, 0xa9, 0x3f, 0x5c, 0xed, 0x82, 0x1e, 0x68 } }
+#define NS_BITS_CONTRACTID "@mozilla.org/bits;1"
+%}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/action.rs b/toolkit/components/bitsdownload/src/bits_interface/action.rs
new file mode 100644
index 0000000000..1019e29827
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/action.rs
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! `Action` is an enum describing what BITS action is being processed. This is
+//! used mostly for logging and error reporting reasons.
+//! The values of `Action` describe actions that could be in progress for
+//! BitsService or BitsRequest. When specifying a type, `ServiceAction` or
+//! `RequestAction`, can be used to restrict the action type to one of the two
+//! categories.
+//! A value of type `ServiceAction` or `RequestAction` can easily be converted
+//! to an `Action` using the `into()` method.
+
+use std::convert::From;
+use xpcom::interfaces::nsIBits;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Action {
+ StartDownload,
+ MonitorDownload,
+ Complete,
+ Cancel,
+ SetMonitorInterval,
+ SetPriority,
+ SetNoProgressTimeout,
+ Resume,
+ Suspend,
+}
+
+impl Action {
+ pub fn description(&self) -> &'static str {
+ match self {
+ Action::StartDownload => "starting download",
+ Action::MonitorDownload => "monitoring download",
+ Action::Complete => "completing download",
+ Action::Cancel => "cancelling download",
+ Action::SetMonitorInterval => "changing monitor interval",
+ Action::SetPriority => "setting download priority",
+ Action::SetNoProgressTimeout => "setting no progress timeout",
+ Action::Resume => "resuming download",
+ Action::Suspend => "suspending download",
+ }
+ }
+
+ pub fn as_error_code(&self) -> i32 {
+ match self {
+ Action::StartDownload => nsIBits::ERROR_ACTION_START_DOWNLOAD,
+ Action::MonitorDownload => nsIBits::ERROR_ACTION_MONITOR_DOWNLOAD,
+ Action::Complete => nsIBits::ERROR_ACTION_COMPLETE,
+ Action::Cancel => nsIBits::ERROR_ACTION_CANCEL,
+ Action::SetMonitorInterval => nsIBits::ERROR_ACTION_CHANGE_MONITOR_INTERVAL,
+ Action::SetPriority => nsIBits::ERROR_ACTION_SET_PRIORITY,
+ Action::SetNoProgressTimeout => nsIBits::ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT,
+ Action::Resume => nsIBits::ERROR_ACTION_RESUME,
+ Action::Suspend => nsIBits::ERROR_ACTION_SUSPEND,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum ServiceAction {
+ StartDownload,
+ MonitorDownload,
+}
+
+impl From<ServiceAction> for Action {
+ fn from(action: ServiceAction) -> Action {
+ match action {
+ ServiceAction::StartDownload => Action::StartDownload,
+ ServiceAction::MonitorDownload => Action::MonitorDownload,
+ }
+ }
+}
+
+impl ServiceAction {
+ pub fn as_error_code(&self) -> i32 {
+ Action::as_error_code(&(self.clone()).into())
+ }
+
+ pub fn description(&self) -> &'static str {
+ Action::description(&(self.clone()).into())
+ }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum RequestAction {
+ Complete,
+ Cancel,
+ SetMonitorInterval,
+ SetPriority,
+ SetNoProgressTimeout,
+ Resume,
+ Suspend,
+}
+
+impl From<RequestAction> for Action {
+ fn from(action: RequestAction) -> Action {
+ match action {
+ RequestAction::Complete => Action::Complete,
+ RequestAction::Cancel => Action::Cancel,
+ RequestAction::SetMonitorInterval => Action::SetMonitorInterval,
+ RequestAction::SetPriority => Action::SetPriority,
+ RequestAction::SetNoProgressTimeout => Action::SetNoProgressTimeout,
+ RequestAction::Resume => Action::Resume,
+ RequestAction::Suspend => Action::Suspend,
+ }
+ }
+}
+
+impl RequestAction {
+ pub fn as_error_code(&self) -> i32 {
+ Action::as_error_code(&(self.clone()).into())
+ }
+
+ pub fn description(&self) -> &'static str {
+ Action::description(&(self.clone()).into())
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/dispatch_callback.rs b/toolkit/components/bitsdownload/src/bits_interface/dispatch_callback.rs
new file mode 100644
index 0000000000..b42637a993
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/dispatch_callback.rs
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ error::{BitsTaskError, ErrorCode, ErrorType},
+ BitsRequest,
+};
+use log::{error, info, warn};
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use xpcom::{
+ interfaces::{nsIBitsCallback, nsIBitsNewRequestCallback},
+ RefPtr,
+};
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum IsCallbackExpected {
+ CallbackExpected,
+ CallbackOptional,
+}
+pub use self::IsCallbackExpected::{CallbackExpected, CallbackOptional};
+
+// This is meant to be called at the end of a nsIBits Task. It attempts to
+// return the result via the callback given. If the callback is unavailable, a
+// log message will be printed indicating the results and (possibly) warning
+// than an expected callback was missing.
+pub fn maybe_dispatch_request_via_callback(
+ result: Result<RefPtr<BitsRequest>, BitsTaskError>,
+ maybe_callback: Result<&nsIBitsNewRequestCallback, BitsTaskError>,
+ expected: IsCallbackExpected,
+) -> Result<(), nsresult> {
+ if let Err(error) = maybe_callback.as_ref() {
+ if expected == CallbackExpected || error.error_type == ErrorType::CallbackOnWrongThread {
+ error!(
+ "Unexpected error when {} - No callback: {:?}",
+ error.error_action.description(),
+ error,
+ );
+ }
+ }
+ match result {
+ Ok(request) => match (maybe_callback, expected) {
+ (Ok(callback), _) => unsafe { callback.Success(request.coerce()) },
+ (Err(error), CallbackExpected) => {
+ error!(
+ "Success {} but there is no callback to return the result with",
+ error.error_action.description(),
+ );
+ NS_ERROR_FAILURE
+ }
+ (Err(error), CallbackOptional) => {
+ info!("Success {}", error.error_action.description());
+ NS_OK
+ }
+ },
+ Err(error) => match (maybe_callback, expected) {
+ (Ok(callback), _) => match error.error_code {
+ ErrorCode::None => unsafe {
+ callback.Failure(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ )
+ },
+ ErrorCode::Hresult(error_code) => unsafe {
+ callback.FailureHresult(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ error_code,
+ )
+ },
+ ErrorCode::Nsresult(error_code) => unsafe {
+ callback.FailureNsresult(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ error_code,
+ )
+ },
+ ErrorCode::Message(message) => unsafe {
+ callback.FailureString(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ &*message,
+ )
+ },
+ },
+ (Err(_), CallbackExpected) => {
+ error!("Error {}: {:?}", error.error_action.description(), error);
+ NS_ERROR_FAILURE
+ }
+ (Err(_), CallbackOptional) => {
+ warn!("Error {}: {:?}", error.error_action.description(), error);
+ NS_ERROR_FAILURE
+ }
+ },
+ }
+ .to_result()
+}
+
+// Intended to be used by an nsIBits XPCOM wrapper to return errors that occur
+// before dispatching a task off-thread. No return value is returned because it
+// will represent the return value of the callback function, which should not be
+// propagated.
+pub fn dispatch_pretask_interface_error(
+ error: BitsTaskError,
+ callback: &nsIBitsNewRequestCallback,
+) {
+ let _ = maybe_dispatch_request_via_callback(Err(error), Ok(callback), CallbackExpected);
+}
+
+// This is meant to be called at the end of a nsIBitsRequest Task. It attempts
+// to return the result via the callback given. If the callback is unavailable,
+// a log message will be printed indicating the results and (possibly) warning
+// than an expected callback was missing.
+pub fn maybe_dispatch_via_callback(
+ result: Result<(), BitsTaskError>,
+ maybe_callback: Result<&nsIBitsCallback, BitsTaskError>,
+ expected: IsCallbackExpected,
+) -> Result<(), nsresult> {
+ if let Err(error) = maybe_callback.as_ref() {
+ if expected == CallbackExpected || error.error_type == ErrorType::CallbackOnWrongThread {
+ error!(
+ "Unexpected error when {} - No callback: {:?}",
+ error.error_action.description(),
+ error,
+ );
+ }
+ }
+ match result {
+ Ok(()) => match (maybe_callback, expected) {
+ (Ok(callback), _) => unsafe { callback.Success() },
+ (Err(error), CallbackExpected) => {
+ error!(
+ "Success {} but there is no callback to return the result with",
+ error.error_action.description(),
+ );
+ NS_ERROR_FAILURE
+ }
+ (Err(error), CallbackOptional) => {
+ info!("Success {}", error.error_action.description());
+ NS_OK
+ }
+ },
+ Err(error) => match (maybe_callback, expected) {
+ (Ok(callback), _) => match error.error_code {
+ ErrorCode::None => unsafe {
+ callback.Failure(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ )
+ },
+ ErrorCode::Hresult(error_code) => unsafe {
+ callback.FailureHresult(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ error_code,
+ )
+ },
+ ErrorCode::Nsresult(error_code) => unsafe {
+ callback.FailureNsresult(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ error_code,
+ )
+ },
+ ErrorCode::Message(message) => unsafe {
+ callback.FailureString(
+ error.error_type.bits_code(),
+ error.error_action.as_error_code(),
+ error.error_stage.bits_code(),
+ &*message,
+ )
+ },
+ },
+ (Err(_), CallbackExpected) => {
+ error!("Error {}: {:?}", error.error_action.description(), error);
+ NS_ERROR_FAILURE
+ }
+ (Err(_), CallbackOptional) => {
+ warn!("Error {}: {:?}", error.error_action.description(), error);
+ NS_ERROR_FAILURE
+ }
+ },
+ }
+ .to_result()
+}
+
+// Intended to be used by an nsIBitsRequest XPCOM wrapper to return errors that
+// occur before dispatching a task off-thread. No return value is returned
+// because it will represent the return value of the callback function, which
+// should not be propagated.
+pub fn dispatch_pretask_request_error(error: BitsTaskError, callback: &nsIBitsCallback) {
+ let _ = maybe_dispatch_via_callback(Err(error), Ok(callback), CallbackExpected);
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/error.rs b/toolkit/components/bitsdownload/src/bits_interface/error.rs
new file mode 100644
index 0000000000..c046ae5e70
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/error.rs
@@ -0,0 +1,686 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::action::Action;
+use bits_client::{
+ bits_protocol::{
+ CancelJobFailure, CompleteJobFailure, HResultMessage, MonitorJobFailure, ResumeJobFailure,
+ SetJobPriorityFailure, SetNoProgressTimeoutFailure, SetUpdateIntervalFailure,
+ StartJobFailure, SuspendJobFailure,
+ },
+ PipeError,
+};
+use comedy::error::HResult as ComedyError;
+use nserror::{nsresult, NS_ERROR_FAILURE};
+use nsstring::nsCString;
+use std::convert::From;
+use xpcom::interfaces::nsIBits;
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum ErrorType {
+ NullArgument,
+ InvalidArgument,
+ NotInitialized,
+ NoUtf8Conversion,
+ InvalidGuid,
+ PipeNotConnected,
+ PipeTimeout,
+ PipeBadWriteCount,
+ PipeApiError,
+ FailedToCreateBitsJob,
+ FailedToAddFileToJob,
+ FailedToApplyBitsJobSettings,
+ FailedToResumeBitsJob,
+ OtherBitsError,
+ OtherBitsClientError,
+ BitsJobNotFound,
+ FailedToGetBitsJob,
+ FailedToSuspendBitsJob,
+ FailedToCompleteBitsJob,
+ PartiallyCompletedBitsJob,
+ FailedToCancelBitsJob,
+ MissingResultData,
+ MissingCallback,
+ CallbackOnWrongThread,
+ MissingBitsService,
+ BitsServiceOnWrongThread,
+ MissingBitsRequest,
+ BitsRequestOnWrongThread,
+ MissingObserver,
+ ObserverOnWrongThread,
+ MissingContext,
+ ContextOnWrongThread,
+ FailedToStartThread,
+ FailedToConstructTaskRunnable,
+ FailedToDispatchRunnable,
+ TransferAlreadyComplete,
+ OperationAlreadyInProgress,
+ MissingBitsClient,
+ FailedToGetJobStatus,
+ BitsStateError,
+ BitsStateTransientError,
+ BitsStateCancelled,
+ BitsStateUnexpected,
+ FailedToConnectToBcm,
+}
+
+impl ErrorType {
+ pub fn bits_code(&self) -> i32 {
+ match self {
+ ErrorType::NullArgument => nsIBits::ERROR_TYPE_NULL_ARGUMENT,
+ ErrorType::InvalidArgument => nsIBits::ERROR_TYPE_INVALID_ARGUMENT,
+ ErrorType::NotInitialized => nsIBits::ERROR_TYPE_NOT_INITIALIZED,
+ ErrorType::NoUtf8Conversion => nsIBits::ERROR_TYPE_NO_UTF8_CONVERSION,
+ ErrorType::InvalidGuid => nsIBits::ERROR_TYPE_INVALID_GUID,
+ ErrorType::PipeNotConnected => nsIBits::ERROR_TYPE_PIPE_NOT_CONNECTED,
+ ErrorType::PipeTimeout => nsIBits::ERROR_TYPE_PIPE_TIMEOUT,
+ ErrorType::PipeBadWriteCount => nsIBits::ERROR_TYPE_PIPE_BAD_WRITE_COUNT,
+ ErrorType::PipeApiError => nsIBits::ERROR_TYPE_PIPE_API_ERROR,
+ ErrorType::FailedToCreateBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_CREATE_BITS_JOB,
+ ErrorType::FailedToAddFileToJob => nsIBits::ERROR_TYPE_FAILED_TO_ADD_FILE_TO_JOB,
+ ErrorType::FailedToApplyBitsJobSettings => {
+ nsIBits::ERROR_TYPE_FAILED_TO_APPLY_BITS_JOB_SETTINGS
+ }
+ ErrorType::FailedToResumeBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_RESUME_BITS_JOB,
+ ErrorType::OtherBitsError => nsIBits::ERROR_TYPE_OTHER_BITS_ERROR,
+ ErrorType::OtherBitsClientError => nsIBits::ERROR_TYPE_OTHER_BITS_CLIENT_ERROR,
+ ErrorType::BitsJobNotFound => nsIBits::ERROR_TYPE_BITS_JOB_NOT_FOUND,
+ ErrorType::FailedToGetBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_GET_BITS_JOB,
+ ErrorType::FailedToSuspendBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_SUSPEND_BITS_JOB,
+ ErrorType::FailedToCompleteBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_COMPLETE_BITS_JOB,
+ ErrorType::PartiallyCompletedBitsJob => {
+ nsIBits::ERROR_TYPE_PARTIALLY_COMPLETED_BITS_JOB
+ }
+ ErrorType::FailedToCancelBitsJob => nsIBits::ERROR_TYPE_FAILED_TO_CANCEL_BITS_JOB,
+ ErrorType::MissingResultData => nsIBits::ERROR_TYPE_MISSING_RESULT_DATA,
+ ErrorType::MissingCallback => nsIBits::ERROR_TYPE_MISSING_CALLBACK,
+ ErrorType::CallbackOnWrongThread => nsIBits::ERROR_TYPE_CALLBACK_ON_WRONG_THREAD,
+ ErrorType::MissingBitsService => nsIBits::ERROR_TYPE_MISSING_BITS_SERVICE,
+ ErrorType::BitsServiceOnWrongThread => nsIBits::ERROR_TYPE_BITS_SERVICE_ON_WRONG_THREAD,
+ ErrorType::MissingBitsRequest => nsIBits::ERROR_TYPE_MISSING_BITS_REQUEST,
+ ErrorType::BitsRequestOnWrongThread => nsIBits::ERROR_TYPE_BITS_REQUEST_ON_WRONG_THREAD,
+ ErrorType::MissingObserver => nsIBits::ERROR_TYPE_MISSING_OBSERVER,
+ ErrorType::ObserverOnWrongThread => nsIBits::ERROR_TYPE_OBSERVER_ON_WRONG_THREAD,
+ ErrorType::MissingContext => nsIBits::ERROR_TYPE_MISSING_CONTEXT,
+ ErrorType::ContextOnWrongThread => nsIBits::ERROR_TYPE_CONTEXT_ON_WRONG_THREAD,
+ ErrorType::FailedToStartThread => nsIBits::ERROR_TYPE_FAILED_TO_START_THREAD,
+ ErrorType::FailedToConstructTaskRunnable => {
+ nsIBits::ERROR_TYPE_FAILED_TO_CONSTRUCT_TASK_RUNNABLE
+ }
+ ErrorType::FailedToDispatchRunnable => nsIBits::ERROR_TYPE_FAILED_TO_DISPATCH_RUNNABLE,
+ ErrorType::TransferAlreadyComplete => nsIBits::ERROR_TYPE_TRANSFER_ALREADY_COMPLETE,
+ ErrorType::OperationAlreadyInProgress => {
+ nsIBits::ERROR_TYPE_OPERATION_ALREADY_IN_PROGRESS
+ }
+ ErrorType::MissingBitsClient => nsIBits::ERROR_TYPE_MISSING_BITS_CLIENT,
+ ErrorType::FailedToGetJobStatus => nsIBits::ERROR_TYPE_FAILED_TO_GET_JOB_STATUS,
+ ErrorType::BitsStateError => nsIBits::ERROR_TYPE_BITS_STATE_ERROR,
+ ErrorType::BitsStateTransientError => nsIBits::ERROR_TYPE_BITS_STATE_TRANSIENT_ERROR,
+ ErrorType::BitsStateCancelled => nsIBits::ERROR_TYPE_BITS_STATE_CANCELLED,
+ ErrorType::BitsStateUnexpected => nsIBits::ERROR_TYPE_BITS_STATE_UNEXPECTED,
+ ErrorType::FailedToConnectToBcm => nsIBits::ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM,
+ }
+ }
+}
+
+impl From<&PipeError> for ErrorType {
+ fn from(error: &PipeError) -> Self {
+ match error {
+ PipeError::NotConnected => ErrorType::PipeNotConnected,
+ PipeError::Timeout => ErrorType::PipeTimeout,
+ PipeError::WriteCount(_, _) => ErrorType::PipeBadWriteCount,
+ PipeError::Api(_) => ErrorType::PipeApiError,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum ErrorStage {
+ Pretask,
+ CommandThread,
+ AgentCommunication,
+ BitsClient,
+ MainThread,
+}
+
+impl ErrorStage {
+ pub fn bits_code(&self) -> i32 {
+ let val = match self {
+ ErrorStage::Pretask => nsIBits::ERROR_STAGE_PRETASK,
+ ErrorStage::CommandThread => nsIBits::ERROR_STAGE_COMMAND_THREAD,
+ ErrorStage::AgentCommunication => nsIBits::ERROR_STAGE_AGENT_COMMUNICATION,
+ ErrorStage::BitsClient => nsIBits::ERROR_STAGE_BITS_CLIENT,
+ ErrorStage::MainThread => nsIBits::ERROR_STAGE_MAIN_THREAD,
+ };
+ val as i32
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum ErrorCode {
+ None,
+ Hresult(i32),
+ Nsresult(nsresult),
+ Message(nsCString),
+}
+
+impl From<ComedyError> for ErrorCode {
+ fn from(error: ComedyError) -> Self {
+ ErrorCode::Hresult(error.code())
+ }
+}
+
+impl From<HResultMessage> for ErrorCode {
+ fn from(result: HResultMessage) -> Self {
+ ErrorCode::Hresult(result.hr)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct BitsTaskError {
+ pub error_type: ErrorType,
+ pub error_action: Action,
+ pub error_stage: ErrorStage,
+ pub error_code: ErrorCode,
+}
+
+impl BitsTaskError {
+ pub fn new(
+ error_type: ErrorType,
+ error_action: Action,
+ error_stage: ErrorStage,
+ ) -> BitsTaskError {
+ BitsTaskError {
+ error_type,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ }
+ }
+
+ pub fn missing_result(error_action: Action) -> BitsTaskError {
+ BitsTaskError {
+ error_type: ErrorType::MissingResultData,
+ error_action,
+ error_stage: ErrorStage::MainThread,
+ error_code: ErrorCode::None,
+ }
+ }
+
+ pub fn from_nsresult(
+ error_type: ErrorType,
+ error_action: Action,
+ error_stage: ErrorStage,
+ error_code: nsresult,
+ ) -> BitsTaskError {
+ BitsTaskError {
+ error_type,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Nsresult(error_code),
+ }
+ }
+
+ pub fn from_hresult(
+ error_type: ErrorType,
+ error_action: Action,
+ error_stage: ErrorStage,
+ error_code: i32,
+ ) -> BitsTaskError {
+ BitsTaskError {
+ error_type,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Hresult(error_code),
+ }
+ }
+
+ pub fn from_comedy(
+ error_type: ErrorType,
+ error_action: Action,
+ error_stage: ErrorStage,
+ comedy_error: ComedyError,
+ ) -> BitsTaskError {
+ BitsTaskError {
+ error_type,
+ error_action,
+ error_stage,
+ error_code: comedy_error.into(),
+ }
+ }
+
+ pub fn from_pipe(error_action: Action, pipe_error: PipeError) -> BitsTaskError {
+ let error_type = (&pipe_error).into();
+ match pipe_error {
+ PipeError::Api(comedy_error) => BitsTaskError {
+ error_type,
+ error_action,
+ error_stage: ErrorStage::AgentCommunication,
+ error_code: comedy_error.into(),
+ },
+ _ => BitsTaskError {
+ error_type,
+ error_action,
+ error_stage: ErrorStage::AgentCommunication,
+ error_code: ErrorCode::None,
+ },
+ }
+ }
+}
+
+impl From<BitsTaskError> for nsresult {
+ fn from(error: BitsTaskError) -> Self {
+ if let ErrorCode::Nsresult(rv) = error.error_code {
+ rv
+ } else {
+ NS_ERROR_FAILURE
+ }
+ }
+}
+
+impl From<StartJobFailure> for BitsTaskError {
+ fn from(error: StartJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::StartDownload;
+ match error {
+ StartJobFailure::ArgumentValidation(message) => BitsTaskError {
+ error_type: ErrorType::InvalidArgument,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ StartJobFailure::Create(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToCreateBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::AddFile(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToAddFileToJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::ApplySettings(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToApplyBitsJobSettings,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::Resume(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToResumeBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ StartJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<MonitorJobFailure> for BitsTaskError {
+ fn from(error: MonitorJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::MonitorDownload;
+ match error {
+ MonitorJobFailure::ArgumentValidation(message) => BitsTaskError {
+ error_type: ErrorType::InvalidArgument,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ MonitorJobFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ MonitorJobFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ MonitorJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ MonitorJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ MonitorJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<SuspendJobFailure> for BitsTaskError {
+ fn from(error: SuspendJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::Suspend;
+ match error {
+ SuspendJobFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ SuspendJobFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SuspendJobFailure::SuspendJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToSuspendBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SuspendJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SuspendJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SuspendJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<ResumeJobFailure> for BitsTaskError {
+ fn from(error: ResumeJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::Resume;
+ match error {
+ ResumeJobFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ ResumeJobFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ResumeJobFailure::ResumeJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToResumeBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ResumeJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ResumeJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ResumeJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<SetJobPriorityFailure> for BitsTaskError {
+ fn from(error: SetJobPriorityFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::SetPriority;
+ match error {
+ SetJobPriorityFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ SetJobPriorityFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SetJobPriorityFailure::ApplySettings(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToApplyBitsJobSettings,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SetJobPriorityFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SetJobPriorityFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ SetJobPriorityFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<SetNoProgressTimeoutFailure> for BitsTaskError {
+ fn from(error: SetNoProgressTimeoutFailure) -> Self {
+ use self::SetNoProgressTimeoutFailure::*;
+
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::SetNoProgressTimeout;
+ match error {
+ NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ApplySettings(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToApplyBitsJobSettings,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<SetUpdateIntervalFailure> for BitsTaskError {
+ fn from(error: SetUpdateIntervalFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::SetMonitorInterval;
+ match error {
+ SetUpdateIntervalFailure::ArgumentValidation(message) => BitsTaskError {
+ error_type: ErrorType::InvalidArgument,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ SetUpdateIntervalFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ SetUpdateIntervalFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<CompleteJobFailure> for BitsTaskError {
+ fn from(error: CompleteJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::Complete;
+ match error {
+ CompleteJobFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ CompleteJobFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CompleteJobFailure::CompleteJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToCompleteBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CompleteJobFailure::PartialComplete => BitsTaskError {
+ error_type: ErrorType::PartiallyCompletedBitsJob,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ CompleteJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CompleteJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CompleteJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
+
+impl From<CancelJobFailure> for BitsTaskError {
+ fn from(error: CancelJobFailure) -> Self {
+ let error_stage = ErrorStage::BitsClient;
+ let error_action = Action::Cancel;
+ match error {
+ CancelJobFailure::NotFound => BitsTaskError {
+ error_type: ErrorType::BitsJobNotFound,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::None,
+ },
+ CancelJobFailure::GetJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToGetBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CancelJobFailure::CancelJob(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToCancelBitsJob,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CancelJobFailure::ConnectBcm(error_code) => BitsTaskError {
+ error_type: ErrorType::FailedToConnectToBcm,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CancelJobFailure::OtherBITS(error_code) => BitsTaskError {
+ error_type: ErrorType::OtherBitsError,
+ error_action,
+ error_stage,
+ error_code: error_code.into(),
+ },
+ CancelJobFailure::Other(message) => BitsTaskError {
+ error_type: ErrorType::OtherBitsClientError,
+ error_action,
+ error_stage,
+ error_code: ErrorCode::Message(nsCString::from(message)),
+ },
+ }
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/mod.rs b/toolkit/components/bitsdownload/src/bits_interface/mod.rs
new file mode 100644
index 0000000000..93d2ddeadd
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/mod.rs
@@ -0,0 +1,329 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+mod action;
+mod dispatch_callback;
+mod error;
+mod monitor;
+mod request;
+mod string;
+mod task;
+mod xpcom_methods;
+
+pub use self::error::BitsTaskError;
+use self::{
+ action::Action,
+ error::{
+ ErrorStage::Pretask,
+ ErrorType::{
+ FailedToConstructTaskRunnable, FailedToDispatchRunnable, FailedToStartThread,
+ InvalidArgument, NotInitialized,
+ },
+ },
+ request::BitsRequest,
+ string::Guid_from_nsCString,
+ task::{ClientInitData, MonitorDownloadTask, StartDownloadTask},
+};
+use nsIBits_method; // From xpcom_method.rs
+
+use bits_client::BitsProxyUsage;
+use log::{info, warn};
+use moz_task::{create_thread, Task, TaskRunnable};
+use nserror::{nsresult, NS_ERROR_ALREADY_INITIALIZED, NS_OK};
+use nsstring::{nsACString, nsCString};
+use std::cell::Cell;
+use xpcom::{
+ interfaces::{
+ nsIBits, nsIBitsNewRequestCallback, nsIRequestObserver, nsISupports, nsIThread,
+ nsProxyUsage,
+ },
+ xpcom, xpcom_method, RefPtr,
+};
+
+#[no_mangle]
+pub unsafe extern "C" fn new_bits_service(result: *mut *const nsIBits) {
+ let service: RefPtr<BitsService> = BitsService::new();
+ RefPtr::new(service.coerce::<nsIBits>()).forget(&mut *result);
+}
+
+#[xpcom(implement(nsIBits), nonatomic)]
+pub struct BitsService {
+ // This command thread will be used to send commands (ex: Suspend, Resume)
+ // to a running job. It will be started up when the first job is created and
+ // shutdown when all jobs have been completed or cancelled.
+ command_thread: Cell<Option<RefPtr<nsIThread>>>,
+ client_init_data: Cell<Option<ClientInitData>>,
+ // This count will track the number of in-progress requests so that the
+ // service knows when the command_thread is no longer being used and can be
+ // shut down.
+ // `BitsRequest::new()` will increment this and it will be decremented
+ // either when cancel/complete is called, or when the request is dropped
+ // (if it didn't decrement it already).
+ // The count will also be incremented when an action to create a request
+ // starts and decremented when the action ends and returns the result via
+ // the callback. This prevents the command thread from being shut down while
+ // a job is being created.
+ request_count: Cell<u32>,
+}
+
+/// This implements the nsIBits interface, documented in nsIBits.idl, to enable
+/// BITS job management. Specifically, this interface can start a download or
+/// connect to an existing download. Doing so will create a BitsRequest through
+/// which the transfer can be further manipulated.
+///
+/// This is a primarily asynchronous interface, which is accomplished via
+/// callbacks of type nsIBitsNewRequestCallback. The callback is passed in as
+/// an argument and is then passed off-thread via a Task. The Task interacts
+/// with BITS and is dispatched back to the main thread with the BITS result.
+/// Back on the main thread, it returns that result via the callback including,
+/// if successful, a BitsRequest.
+impl BitsService {
+ pub fn new() -> RefPtr<BitsService> {
+ BitsService::allocate(InitBitsService {
+ command_thread: Cell::new(None),
+ client_init_data: Cell::new(None),
+ request_count: Cell::new(0),
+ })
+ }
+
+ fn get_client_init(&self) -> Option<ClientInitData> {
+ let maybe_init_data = self.client_init_data.take();
+ self.client_init_data.set(maybe_init_data.clone());
+ maybe_init_data
+ }
+
+ // Returns the handle to the command thread. If it has not been started yet,
+ // the thread will be started.
+ fn get_command_thread(&self) -> Result<RefPtr<nsIThread>, nsresult> {
+ let mut command_thread = self.command_thread.take();
+ if command_thread.is_none() {
+ command_thread.replace(create_thread("BitsCommander")?);
+ }
+ self.command_thread.set(command_thread.clone());
+ Ok(command_thread.unwrap())
+ }
+
+ // Asynchronously shuts down the command thread. The thread is not shutdown
+ // until the event queue is empty, so any tasks that were dispatched before
+ // this is called will still run.
+ // Leaves None in self.command_thread
+ fn shutdown_command_thread(&self) {
+ if let Some(command_thread) = self.command_thread.take() {
+ if let Err(rv) = unsafe { command_thread.AsyncShutdown() }.to_result() {
+ warn!("Failed to shut down command thread: {}", rv);
+ warn!("Releasing reference to thread that failed to shut down!");
+ }
+ }
+ }
+
+ fn dispatch_runnable_to_command_thread(
+ &self,
+ task: Box<dyn Task + Send + Sync>,
+ task_runnable_name: &'static str,
+ action: Action,
+ ) -> Result<(), BitsTaskError> {
+ let command_thread = self
+ .get_command_thread()
+ .map_err(|rv| BitsTaskError::from_nsresult(FailedToStartThread, action, Pretask, rv))?;
+ let runnable = TaskRunnable::new(task_runnable_name, task).map_err(|rv| {
+ BitsTaskError::from_nsresult(FailedToConstructTaskRunnable, action, Pretask, rv)
+ })?;
+ TaskRunnable::dispatch(runnable, &command_thread).map_err(|rv| {
+ BitsTaskError::from_nsresult(FailedToDispatchRunnable, action, Pretask, rv)
+ })
+ }
+
+ fn inc_request_count(&self) {
+ self.request_count.set(self.request_count.get() + 1);
+ }
+
+ fn dec_request_count(&self) {
+ let mut count = self.request_count.get();
+ if count == 0 {
+ warn!("Attempted to decrement request count, but it is 0");
+ return;
+ }
+ count -= 1;
+ self.request_count.set(count);
+
+ if count == 0 {
+ self.shutdown_command_thread();
+ }
+ }
+
+ xpcom_method!(
+ get_initialized => GetInitialized() -> bool
+ );
+ fn get_initialized(&self) -> Result<bool, nsresult> {
+ Ok(self.get_client_init().is_some())
+ }
+
+ xpcom_method!(
+ init => Init(
+ job_name: *const nsACString,
+ save_path_prefix: *const nsACString,
+ monitor_timeout_ms: u32
+ )
+ );
+ fn init(
+ &self,
+ job_name: &nsACString,
+ save_path_prefix: &nsACString,
+ monitor_timeout_ms: u32,
+ ) -> Result<(), nsresult> {
+ let previous_data = self.client_init_data.take();
+ if previous_data.is_some() {
+ self.client_init_data.set(previous_data);
+ return Err(NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ info!(
+ "BitsService initialized with job_name: {}, save_path_prefix: {}, timeout: {}",
+ job_name, save_path_prefix, monitor_timeout_ms,
+ );
+
+ self.client_init_data.set(Some(ClientInitData::new(
+ nsCString::from(job_name),
+ nsCString::from(save_path_prefix),
+ monitor_timeout_ms,
+ )));
+
+ Ok(())
+ }
+
+ nsIBits_method!(
+ [Action::StartDownload]
+ start_download => StartDownload(
+ download_url: *const nsACString,
+ save_rel_path: *const nsACString,
+ proxy: nsProxyUsage,
+ no_progress_timeout_secs: u32,
+ update_interval_ms: u32,
+ observer: *const nsIRequestObserver,
+ [optional] context: *const nsISupports,
+ )
+ );
+ fn start_download(
+ &self,
+ download_url: &nsACString,
+ save_rel_path: &nsACString,
+ proxy: nsProxyUsage,
+ no_progress_timeout_secs: u32,
+ update_interval_ms: u32,
+ observer: &nsIRequestObserver,
+ context: Option<&nsISupports>,
+ callback: &nsIBitsNewRequestCallback,
+ ) -> Result<(), BitsTaskError> {
+ let client_init_data = self
+ .get_client_init()
+ .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::StartDownload, Pretask))?;
+ if update_interval_ms >= client_init_data.monitor_timeout_ms {
+ return Err(BitsTaskError::new(
+ InvalidArgument,
+ Action::StartDownload,
+ Pretask,
+ ));
+ }
+ let proxy = match proxy {
+ nsIBits::PROXY_NONE => BitsProxyUsage::NoProxy,
+ nsIBits::PROXY_PRECONFIG => BitsProxyUsage::Preconfig,
+ nsIBits::PROXY_AUTODETECT => BitsProxyUsage::AutoDetect,
+ _ => {
+ return Err(BitsTaskError::new(
+ InvalidArgument,
+ Action::StartDownload,
+ Pretask,
+ ));
+ }
+ };
+
+ let task: Box<StartDownloadTask> = Box::new(StartDownloadTask::new(
+ client_init_data,
+ nsCString::from(download_url),
+ nsCString::from(save_rel_path),
+ proxy,
+ no_progress_timeout_secs,
+ update_interval_ms,
+ RefPtr::new(self),
+ RefPtr::new(observer),
+ context.map(RefPtr::new),
+ RefPtr::new(callback),
+ ));
+
+ let dispatch_result = self.dispatch_runnable_to_command_thread(
+ task,
+ "BitsService::start_download",
+ Action::StartDownload,
+ );
+
+ if dispatch_result.is_ok() {
+ // Increment the request count when we dispatch an action to start
+ // a job, decrement it when the action completes. See the
+ // declaration of InitBitsService::request_count for details.
+ self.inc_request_count();
+ }
+
+ dispatch_result
+ }
+
+ nsIBits_method!(
+ [Action::MonitorDownload]
+ monitor_download => MonitorDownload(
+ id: *const nsACString,
+ update_interval_ms: u32,
+ observer: *const nsIRequestObserver,
+ [optional] context: *const nsISupports,
+ )
+ );
+ fn monitor_download(
+ &self,
+ id: &nsACString,
+ update_interval_ms: u32,
+ observer: &nsIRequestObserver,
+ context: Option<&nsISupports>,
+ callback: &nsIBitsNewRequestCallback,
+ ) -> Result<(), BitsTaskError> {
+ let client_init_data = self
+ .get_client_init()
+ .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::MonitorDownload, Pretask))?;
+ if update_interval_ms >= client_init_data.monitor_timeout_ms {
+ return Err(BitsTaskError::new(
+ InvalidArgument,
+ Action::MonitorDownload,
+ Pretask,
+ ));
+ }
+ let guid = Guid_from_nsCString(&nsCString::from(id), Action::MonitorDownload, Pretask)?;
+
+ let task: Box<MonitorDownloadTask> = Box::new(MonitorDownloadTask::new(
+ client_init_data,
+ guid,
+ update_interval_ms,
+ RefPtr::new(self),
+ RefPtr::new(observer),
+ context.map(RefPtr::new),
+ RefPtr::new(callback),
+ ));
+
+ let dispatch_result = self.dispatch_runnable_to_command_thread(
+ task,
+ "BitsService::monitor_download",
+ Action::MonitorDownload,
+ );
+
+ if dispatch_result.is_ok() {
+ // Increment the request count when we dispatch an action to start
+ // a job, decrement it when the action completes. See the
+ // declaration of InitBitsService::request_count for details.
+ self.inc_request_count();
+ }
+
+ dispatch_result
+ }
+}
+
+impl Drop for BitsService {
+ fn drop(&mut self) {
+ self.shutdown_command_thread();
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/monitor.rs b/toolkit/components/bitsdownload/src/bits_interface/monitor.rs
new file mode 100644
index 0000000000..4332efe04b
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/monitor.rs
@@ -0,0 +1,247 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use bits_interface::{error::ErrorType, BitsRequest};
+
+use bits_client::{
+ bits_protocol::HResultMessage, BitsJobState, BitsMonitorClient, Guid, JobStatus, PipeError,
+};
+use crossbeam_utils::atomic::AtomicCell;
+use log::error;
+use moz_task::{get_main_thread, is_main_thread};
+use nserror::{nsresult, NS_ERROR_ABORT, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCString};
+use xpcom::{
+ interfaces::{nsIEventTarget, nsIThread},
+ xpcom, xpcom_method, RefPtr, ThreadBoundRefPtr,
+};
+
+/// This function takes the output of BitsMonitorClient::get_status() and uses
+/// it to determine whether the the transfer has started. If the argument
+/// contains an error, the transfer is considered started because we also
+/// consider a transfer stopped on error.
+/// This function is used to determine whether the OnStartRequest and OnProgress
+/// observer functions should be called.
+fn transfer_started(status: &Result<Result<JobStatus, HResultMessage>, PipeError>) -> bool {
+ match status.as_ref() {
+ Ok(Ok(job_status)) => match job_status.state {
+ BitsJobState::Queued | BitsJobState::Connecting => false,
+ _ => true,
+ },
+ Ok(Err(_)) => true,
+ Err(_) => true,
+ }
+}
+
+/// This function takes the output of BitsMonitorClient::get_status() and uses
+/// it to determine whether the the transfer has stopped. If the argument
+/// contains an error, the transfer is considered stopped.
+/// A number of things will be done when a transfer is completed, such as
+/// calling the observer's OnStopRequest method.
+fn transfer_completed(status: &Result<Result<JobStatus, HResultMessage>, PipeError>) -> bool {
+ match status.as_ref() {
+ Ok(Ok(job_status)) => match job_status.state {
+ BitsJobState::Error
+ | BitsJobState::Transferred
+ | BitsJobState::Acknowledged
+ | BitsJobState::Cancelled => true,
+ _ => false,
+ },
+ Ok(Err(_)) => true,
+ Err(_) => true,
+ }
+}
+
+/// BitsRequest implements nsIRequest, which means that it must be able to
+/// provide an nsresult status code. This function provides such a status code
+/// based on the output of BitsMonitorClient::get_status().
+fn status_to_nsresult(status: &Result<Result<JobStatus, HResultMessage>, PipeError>) -> nsresult {
+ match status.as_ref() {
+ Ok(Ok(job_status)) => match job_status.state {
+ BitsJobState::Cancelled => NS_ERROR_ABORT,
+ BitsJobState::Transferred | BitsJobState::Acknowledged => NS_OK,
+ _ => NS_ERROR_FAILURE,
+ },
+ Ok(Err(_)) => NS_ERROR_FAILURE,
+ Err(_) => NS_ERROR_FAILURE,
+ }
+}
+
+/// This function takes the output of BitsMonitorClient::get_status() and uses
+/// it to determine the result value of the request. This will take the form of
+/// an Optional ErrorType value with a None value indicating success.
+fn status_to_request_result(
+ status: &Result<Result<JobStatus, HResultMessage>, PipeError>,
+) -> Option<ErrorType> {
+ match status.as_ref() {
+ Ok(Ok(job_status)) => match job_status.state {
+ BitsJobState::Transferred | BitsJobState::Acknowledged => None,
+ BitsJobState::Cancelled => Some(ErrorType::BitsStateCancelled),
+ BitsJobState::Error => Some(ErrorType::BitsStateError),
+ BitsJobState::TransientError => Some(ErrorType::BitsStateTransientError),
+ _ => Some(ErrorType::BitsStateUnexpected),
+ },
+ Ok(Err(_)) => Some(ErrorType::FailedToGetJobStatus),
+ Err(pipe_error) => Some(pipe_error.into()),
+ }
+}
+
+/// MonitorRunnable is an nsIRunnable meant to be dispatched off thread. It will
+/// perform the following actions:
+/// 1. Call BitsMonitorClient::get_status and store the result.
+/// 2. Dispatch itself back to the main thread.
+/// 3. Report the status to the observer.
+/// 4. If the transfer has finished, free its data and return, otherwise:
+/// 5. Dispatch itself back to its original thread and repeat from step 1.
+#[xpcom(implement(nsIRunnable, nsINamed), atomic)]
+pub struct MonitorRunnable {
+ request: AtomicCell<Option<ThreadBoundRefPtr<BitsRequest>>>,
+ id: Guid,
+ timeout: u32,
+ monitor_client: AtomicCell<Option<BitsMonitorClient>>,
+ // This cell contains an Option, possibly containing the return value of
+ // BitsMonitorClient::get_status.
+ status: AtomicCell<Option<Result<Result<JobStatus, HResultMessage>, PipeError>>>,
+ request_started: AtomicCell<bool>,
+ in_error_state: AtomicCell<bool>,
+}
+
+impl MonitorRunnable {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ timeout: u32,
+ monitor_client: BitsMonitorClient,
+ ) -> RefPtr<MonitorRunnable> {
+ MonitorRunnable::allocate(InitMonitorRunnable {
+ request: AtomicCell::new(Some(ThreadBoundRefPtr::new(request))),
+ id,
+ timeout,
+ monitor_client: AtomicCell::new(Some(monitor_client)),
+ status: AtomicCell::new(None),
+ request_started: AtomicCell::new(false),
+ in_error_state: AtomicCell::new(false),
+ })
+ }
+
+ pub fn dispatch(&self, thread: RefPtr<nsIThread>) -> Result<(), nsresult> {
+ unsafe { thread.DispatchFromScript(self.coerce(), nsIEventTarget::DISPATCH_NORMAL) }
+ .to_result()
+ }
+
+ fn free_mainthread_data(&self) {
+ if is_main_thread() {
+ // This is not safe to free unless on the main thread
+ self.request.swap(None);
+ } else {
+ error!("Attempting to free data on the main thread, but not on the main thread");
+ }
+ }
+
+ xpcom_method!(run => Run());
+
+ /// This method is essentially a error-handling wrapper around try_run.
+ /// This is done to make it easier to ensure that main-thread data is freed
+ /// on the main thread.
+ pub fn run(&self) -> Result<(), nsresult> {
+ if self.in_error_state.load() {
+ self.free_mainthread_data();
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ self.try_run().or_else(|error_message| {
+ error!("{}", error_message);
+
+ // Once an error has been encountered, we need to free all of our
+ // data, but it all needs to be freed on the main thread.
+ self.in_error_state.store(true);
+ if is_main_thread() {
+ self.free_mainthread_data();
+ Err(NS_ERROR_FAILURE)
+ } else {
+ self.dispatch(get_main_thread()?)
+ }
+ })
+ }
+
+ /// This function performs all the primary functionality of MonitorRunnable.
+ /// See the documentation for InitMonitorRunnable/MonitorRunnable for
+ /// details.
+ pub fn try_run(&self) -> Result<(), String> {
+ if !is_main_thread() {
+ let mut monitor_client = self
+ .monitor_client
+ .swap(None)
+ .ok_or("Missing monitor client")?;
+ self.status
+ .store(Some(monitor_client.get_status(self.timeout)));
+ self.monitor_client.store(Some(monitor_client));
+
+ let main_thread =
+ get_main_thread().map_err(|rv| format!("Unable to get main thread: {}", rv))?;
+
+ self.dispatch(main_thread)
+ .map_err(|rv| format!("Unable to dispatch to main thread: {}", rv))
+ } else {
+ let status = self.status.swap(None).ok_or("Missing status object")?;
+ let tb_request = self.request.swap(None).ok_or("Missing request")?;
+
+ // This block bounds the scope for request to ensure that it ends
+ // before re-storing tb_request.
+ let maybe_next_thread: Option<RefPtr<nsIThread>> = {
+ let request = tb_request
+ .get_ref()
+ .ok_or("BitsRequest is on the wrong thread")?;
+
+ if !self.request_started.load() && transfer_started(&status) {
+ self.request_started.store(true);
+ request.on_start();
+ }
+
+ if self.request_started.load() {
+ if let Ok(Ok(job_status)) = status.as_ref() {
+ let transferred_bytes = job_status.progress.transferred_bytes as i64;
+ let total_bytes = match job_status.progress.total_bytes {
+ Some(total) => total as i64,
+ None => -1i64,
+ };
+ request.on_progress(transferred_bytes, total_bytes);
+ }
+ }
+
+ if transfer_completed(&status) {
+ request.on_stop(Some((
+ status_to_nsresult(&status),
+ status_to_request_result(&status),
+ )));
+
+ // Transfer completed. No need to dispatch back to the monitor thread.
+ None
+ } else {
+ Some(
+ request
+ .get_monitor_thread()
+ .ok_or("Missing monitor thread")?,
+ )
+ }
+ };
+
+ self.request.store(Some(tb_request));
+
+ match maybe_next_thread {
+ Some(next_thread) => self
+ .dispatch(next_thread)
+ .map_err(|rv| format!("Unable to dispatch to thread: {}", rv)),
+ None => {
+ self.free_mainthread_data();
+ Ok(())
+ }
+ }
+ }
+ }
+
+ xpcom_method!(get_name => GetName() -> nsACString);
+ fn get_name(&self) -> Result<nsCString, nsresult> {
+ Ok(nsCString::from("BitsRequest::Monitor"))
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/request.rs b/toolkit/components/bitsdownload/src/bits_interface/request.rs
new file mode 100644
index 0000000000..8d0525ba03
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/request.rs
@@ -0,0 +1,763 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::{
+ action::{Action, ServiceAction},
+ error::{
+ ErrorStage::{MainThread, Pretask},
+ ErrorType,
+ ErrorType::{
+ BitsStateCancelled, FailedToDispatchRunnable, FailedToStartThread, InvalidArgument,
+ OperationAlreadyInProgress, TransferAlreadyComplete,
+ },
+ },
+ monitor::MonitorRunnable,
+ task::{
+ CancelTask, ChangeMonitorIntervalTask, CompleteTask, Priority, ResumeTask,
+ SetNoProgressTimeoutTask, SetPriorityTask, SuspendTask,
+ },
+ BitsService, BitsTaskError,
+};
+use nsIBitsRequest_method; // From xpcom_method.rs
+
+use bits_client::{BitsMonitorClient, Guid};
+use log::{error, info, warn};
+use moz_task::create_thread;
+use nserror::{nsresult, NS_ERROR_ABORT, NS_ERROR_NOT_IMPLEMENTED, NS_OK};
+use nsstring::{nsACString, nsCString};
+use std::{cell::Cell, fmt};
+use xpcom::{
+ interfaces::{
+ nsIBits, nsIBitsCallback, nsILoadGroup, nsIProgressEventSink, nsIRequestObserver,
+ nsISupports, nsIThread, nsLoadFlags,
+ },
+ xpcom, xpcom_method, RefPtr, XpCom,
+};
+
+/// This structure exists to resolve a race condition. If cancel is called, we
+/// don't want to immediately set the request state to cancelled, because the
+/// cancel action could fail. But it's possible that on_stop() could be called
+/// before the cancel action resolves, and the correct status should be sent to
+/// OnStopRequest.
+/// This is how this race condition will be resolved:
+/// 1. cancel() is called, which sets the CancelAction to InProgress and
+/// stores in it the status that should be set if it succeeds.
+/// 2. cancel() dispatches the cancel task off thread.
+/// At this point, things unfold in one of two ways, depending on the race
+/// condition. Either:
+/// 3. The cancel task returns to the main thread and calls
+/// BitsRequest::finish_cancel_action.
+/// 4. If the cancel action succeeded, the appropriate status codes are set
+/// and the CancelAction is set to RequestEndPending.
+/// If the cancel action failed, the CancelAction is set to NotInProgress.
+/// 5. The MonitorRunnable detects that the transfer has ended and calls
+/// BitsRequest::on_stop, passing different status codes.
+/// 6. BitsRequest::on_stop checks the CancelAction and
+/// If the cancel action succeeded and RequestEndPending is set, the
+/// status codes that were set by BitsRequest::finish_cancel_action are
+/// left untouched.
+/// If the cancel action failed and NotInProgress is set, the status codes
+/// passed to BitsRequest::on_stop are set.
+/// 7. onStopRequest is called with the correct status code.
+/// Or, if MonitorRunnable calls on_stop before the cancel task can finish:
+/// 3. The MonitorRunnable detects that the transfer has ended and calls
+/// BitsRequest::on_stop, passing status codes to it.
+/// 4. BitsRequest::on_stop checks the CancelAction, sees it is set to
+/// InProgress, and sets it to RequestEndedWhileInProgress, carrying over
+/// the status code from InProgress.
+/// 5. BitsRequest::on_stop sets the status to the value passed to it, which
+/// will be overwritten if the cancel action succeeds, but kept if it
+/// fails.
+/// 6. BitsRequest::on_stop returns early, without calling OnStopRequest.
+/// 7. The cancel task returns to the main thread and calls
+/// BitsRequest::finish_cancel_action.
+/// 8. If the cancel action succeeded, the status codes are set from the
+/// value stored in RequestEndedWhileInProgress.
+/// If the cancel action failed, the status codes are not changed.
+/// 9. The CancelAction is set to NotInProgress.
+/// 10. BitsRequest::finish_cancel_action calls BitsRequest::on_stop without
+/// passing it any status codes.
+/// 11. onStopRequest is called with the correct status code.
+#[derive(Clone, Copy, PartialEq)]
+enum CancelAction {
+ NotInProgress,
+ InProgress(Option<nsresult>),
+ RequestEndedWhileInProgress(Option<nsresult>),
+ RequestEndPending,
+}
+
+#[xpcom(implement(nsIBitsRequest), nonatomic)]
+pub struct BitsRequest {
+ bits_id: Guid,
+ bits_service: RefPtr<BitsService>,
+ // Stores the value to be returned by nsIRequest::IsPending.
+ download_pending: Cell<bool>,
+ // Stores the value to be returned by nsIRequest::GetStatus.
+ download_status_nsresult: Cell<nsresult>,
+ // Stores an ErrorType if the request has failed, or None to represent the
+ // success state.
+ download_status_error_type: Cell<Option<ErrorType>>,
+ // This option will be None only after OnStopRequest has been fired.
+ monitor_thread: Cell<Option<RefPtr<nsIThread>>>,
+ monitor_timeout_ms: u32,
+ observer: RefPtr<nsIRequestObserver>,
+ // started indicates whether or not OnStartRequest has been fired.
+ started: Cell<bool>,
+ // finished indicates whether or not we have called
+ // BitsService::dec_request_count() to (assuming that there are no other
+ // requests) shutdown the command thread.
+ finished: Cell<bool>,
+ cancel_action: Cell<CancelAction>,
+}
+
+impl fmt::Debug for BitsRequest {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "BitsRequest {{ id: {} }}", self.bits_id)
+ }
+}
+
+/// This implements the nsIBitsRequest interface, documented in nsIBits.idl, to
+/// enable BITS job management. This interface deals only with BITS jobs that
+/// already exist. Jobs can be created via BitsService, which will create a
+/// BitsRequest for that job.
+///
+/// This is a primarily asynchronous interface, which is accomplished via
+/// callbacks of type nsIBitsCallback. The callback is passed in as an argument
+/// and is then passed off-thread via a Task. The Task interacts with BITS and
+/// is dispatched back to the main thread with the BITS result. Back on the main
+/// thread, it returns that result via the callback.
+impl BitsRequest {
+ pub fn new(
+ id: Guid,
+ bits_service: RefPtr<BitsService>,
+ monitor_timeout_ms: u32,
+ observer: RefPtr<nsIRequestObserver>,
+ context: Option<RefPtr<nsISupports>>,
+ monitor_client: BitsMonitorClient,
+ action: ServiceAction,
+ ) -> Result<RefPtr<BitsRequest>, BitsTaskError> {
+ let _ = context;
+ let action: Action = action.into();
+ let monitor_thread = create_thread(&format!("BitsMonitor {}", id)).map_err(|rv| {
+ BitsTaskError::from_nsresult(FailedToStartThread, action, MainThread, rv)
+ })?;
+
+ // BitsRequest.drop() will call dec_request_count
+ bits_service.inc_request_count();
+ let request: RefPtr<BitsRequest> = BitsRequest::allocate(InitBitsRequest {
+ bits_id: id.clone(),
+ bits_service,
+ download_pending: Cell::new(true),
+ download_status_nsresult: Cell::new(NS_OK),
+ download_status_error_type: Cell::new(None),
+ monitor_thread: Cell::new(Some(monitor_thread.clone())),
+ monitor_timeout_ms,
+ observer,
+ started: Cell::new(false),
+ finished: Cell::new(false),
+ cancel_action: Cell::new(CancelAction::NotInProgress),
+ });
+
+ let monitor_runnable =
+ MonitorRunnable::new(request.clone(), id, monitor_timeout_ms, monitor_client);
+
+ if let Err(rv) = monitor_runnable.dispatch(monitor_thread.clone()) {
+ request.shutdown_monitor_thread();
+ return Err(BitsTaskError::from_nsresult(
+ FailedToDispatchRunnable,
+ action,
+ MainThread,
+ rv,
+ ));
+ }
+
+ Ok(request)
+ }
+
+ pub fn get_monitor_thread(&self) -> Option<RefPtr<nsIThread>> {
+ let monitor_thread = self.monitor_thread.take();
+ self.monitor_thread.set(monitor_thread.clone());
+ monitor_thread
+ }
+
+ fn has_monitor_thread(&self) -> bool {
+ let maybe_monitor_thread = self.monitor_thread.take();
+ let transferred = maybe_monitor_thread.is_some();
+ self.monitor_thread.set(maybe_monitor_thread);
+ transferred
+ }
+
+ /// If this returns an true, it means that:
+ /// - The monitor thread and monitor runnable may have been shut down
+ /// - The BITS job is not in the TRANSFERRING state
+ /// - The download either completed, failed, or was cancelled
+ /// - The BITS job may or may not still need complete() or cancel() to be
+ /// called on it
+ fn request_has_transferred(&self) -> bool {
+ self.request_has_completed() || !self.has_monitor_thread()
+ }
+
+ /// If this returns an error, it means that:
+ /// - complete() or cancel() has been called on the BITS job.
+ /// - BitsService::dec_request_count has already been called.
+ /// - The BitsClient object that this request was using may have been
+ /// dropped.
+ fn request_has_completed(&self) -> bool {
+ self.finished.get()
+ }
+
+ fn shutdown_monitor_thread(&self) {
+ if let Some(monitor_thread) = self.monitor_thread.take() {
+ if let Err(rv) = unsafe { monitor_thread.AsyncShutdown() }.to_result() {
+ warn!("Failed to shut down monitor thread: {:?}", rv);
+ warn!("Releasing reference to thread that failed to shut down!");
+ }
+ }
+ }
+
+ /**
+ * To be called when the transfer starts. Fires observer.OnStartRequest exactly once.
+ */
+ pub fn on_start(&self) {
+ if self.started.get() {
+ return;
+ }
+ self.started.set(true);
+ if let Err(rv) = unsafe { self.observer.OnStartRequest(self.coerce()) }.to_result() {
+ // This behavior is specified by nsIRequestObserver.
+ // See nsIRequestObserver.idl
+ info!(
+ "Cancelling download because OnStartRequest rejected with: {:?}",
+ rv
+ );
+ if let Err(rv) = self.cancel(NS_ERROR_ABORT, None) {
+ warn!("Failed to cancel download: {:?}", rv);
+ }
+ }
+ }
+
+ pub fn on_progress(&self, transferred_bytes: i64, total_bytes: i64) {
+ if let Some(progress_event_sink) = self.observer.query_interface::<nsIProgressEventSink>() {
+ unsafe {
+ progress_event_sink.OnProgress(self.coerce(), transferred_bytes, total_bytes);
+ }
+ }
+ }
+
+ /// To be called when the transfer stops (fails or completes). Fires
+ /// observer.OnStopRequest exactly once, though the call may be delayed to
+ /// resolve a race condition.
+ ///
+ /// The status values, if passed, will be stored in download_status_nsresult
+ /// and download_status_error_type, unless they have been overridden by a
+ /// cancel action.
+ ///
+ /// See the documentation for CancelAction for details.
+ pub fn on_stop(&self, maybe_status: Option<(nsresult, Option<ErrorType>)>) {
+ if !self.has_monitor_thread() {
+ // If the request has already stopped, don't stop it again
+ return;
+ }
+
+ match self.cancel_action.get() {
+ CancelAction::InProgress(saved_status)
+ | CancelAction::RequestEndedWhileInProgress(saved_status) => {
+ if let Some((status, result)) = maybe_status {
+ self.download_status_nsresult.set(status);
+ self.download_status_error_type.set(result);
+ }
+
+ info!("Deferring OnStopRequest until Cancel Task completes");
+ self.cancel_action
+ .set(CancelAction::RequestEndedWhileInProgress(saved_status));
+ return;
+ }
+ CancelAction::NotInProgress => {
+ if let Some((status, result)) = maybe_status {
+ self.download_status_nsresult.set(status);
+ self.download_status_error_type.set(result);
+ }
+ }
+ CancelAction::RequestEndPending => {
+ // Don't set the status variables if the end of this request was
+ // the result of a cancel action. The cancel action already set
+ // those values and they should not be changed.
+ // See the CancelAction documentation for details.
+ }
+ }
+
+ self.download_pending.set(false);
+ self.shutdown_monitor_thread();
+ unsafe {
+ self.observer
+ .OnStopRequest(self.coerce(), self.download_status_nsresult.get());
+ }
+ }
+
+ /// To be called after a cancel or complete task has run successfully. If
+ /// this is the only BitsRequest running, this will shut down
+ /// BitsService's command thread, destroying the BitsClient.
+ pub fn on_finished(&self) {
+ if self.finished.get() {
+ return;
+ }
+ self.finished.set(true);
+ self.bits_service.dec_request_count();
+ }
+
+ // Return the same thing for GetBitsId() and GetName().
+ xpcom_method!(
+ maybe_get_bits_id => GetBitsId() -> nsACString
+ );
+ xpcom_method!(
+ maybe_get_bits_id => GetName() -> nsACString
+ );
+ fn maybe_get_bits_id(&self) -> Result<nsCString, nsresult> {
+ Ok(self.get_bits_id())
+ }
+ pub fn get_bits_id(&self) -> nsCString {
+ nsCString::from(self.bits_id.to_string())
+ }
+
+ xpcom_method!(
+ get_bits_transfer_error_nsIBitsRequest => GetTransferError() -> i32
+ );
+ #[allow(non_snake_case)]
+ fn get_bits_transfer_error_nsIBitsRequest(&self) -> Result<i32, nsresult> {
+ let error_type = match self.download_status_error_type.get() {
+ None => nsIBits::ERROR_TYPE_SUCCESS,
+ Some(error_type) => error_type.bits_code(),
+ };
+ Ok(error_type)
+ }
+
+ xpcom_method!(
+ is_pending => IsPending() -> bool
+ );
+ fn is_pending(&self) -> Result<bool, nsresult> {
+ Ok(self.download_pending.get())
+ }
+
+ xpcom_method!(
+ get_status_nsIRequest => GetStatus() -> nsresult
+ );
+ #[allow(non_snake_case)]
+ fn get_status_nsIRequest(&self) -> Result<nsresult, nsresult> {
+ Ok(self.get_status())
+ }
+ pub fn get_status(&self) -> nsresult {
+ self.download_status_nsresult.get()
+ }
+
+ nsIBitsRequest_method!(
+ [Action::SetMonitorInterval]
+ change_monitor_interval => ChangeMonitorInterval(update_interval_ms: u32)
+ );
+ fn change_monitor_interval(
+ &self,
+ update_interval_ms: u32,
+ callback: &nsIBitsCallback,
+ ) -> Result<(), BitsTaskError> {
+ if update_interval_ms == 0 || update_interval_ms >= self.monitor_timeout_ms {
+ return Err(BitsTaskError::new(
+ InvalidArgument,
+ Action::SetMonitorInterval,
+ Pretask,
+ ));
+ }
+ if self.request_has_transferred() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::SetMonitorInterval,
+ Pretask,
+ ));
+ }
+
+ let task: Box<ChangeMonitorIntervalTask> = Box::new(ChangeMonitorIntervalTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ update_interval_ms,
+ RefPtr::new(callback),
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::change_monitor_interval",
+ Action::SetMonitorInterval,
+ )
+ }
+
+ nsIBitsRequest_method!(
+ [Action::Cancel]
+ cancel_nsIBitsRequest => CancelAsync(status: nsresult)
+ );
+ #[allow(non_snake_case)]
+ fn cancel_nsIBitsRequest(
+ &self,
+ status: nsresult,
+ callback: &nsIBitsCallback,
+ ) -> Result<(), BitsTaskError> {
+ self.cancel(status, Some(RefPtr::new(callback)))
+ }
+ xpcom_method!(
+ cancel_nsIRequest => Cancel(status: nsresult)
+ );
+ #[allow(non_snake_case)]
+ fn cancel_nsIRequest(&self, status: nsresult) -> Result<(), BitsTaskError> {
+ self.cancel(status, None)
+ }
+
+ fn cancel(
+ &self,
+ status: nsresult,
+ callback: Option<RefPtr<nsIBitsCallback>>,
+ ) -> Result<(), BitsTaskError> {
+ if status.clone().succeeded() {
+ return Err(BitsTaskError::new(InvalidArgument, Action::Cancel, Pretask));
+ }
+ if self.request_has_completed() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::Cancel,
+ Pretask,
+ ));
+ }
+
+ // If the transfer is still in a success state, cancelling it should move it to the failure
+ // state that was passed. But if the transfer already failed, the only reason to call cancel
+ // is to remove the job from BITS. So in that case, we should keep the failure status that
+ // we already have.
+ let maybe_status: Option<nsresult> = if self.download_status_nsresult.get().failed() {
+ None
+ } else {
+ Some(status)
+ };
+
+ if self.cancel_action.get() != CancelAction::NotInProgress {
+ return Err(BitsTaskError::new(
+ OperationAlreadyInProgress,
+ Action::Cancel,
+ Pretask,
+ ));
+ }
+ self.cancel_action
+ .set(CancelAction::InProgress(maybe_status));
+
+ let task: Box<CancelTask> = Box::new(CancelTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ callback,
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::cancel",
+ Action::Cancel,
+ )
+ }
+
+ /// This function must be called when a cancel action completes.
+ ///
+ /// See the documentation for CancelAction for details.
+ pub fn finish_cancel_action(&self, cancelled_successfully: bool) {
+ let (maybe_status, transfer_ended) = match self.cancel_action.get() {
+ CancelAction::InProgress(maybe_status) => (maybe_status, false),
+ CancelAction::RequestEndedWhileInProgress(maybe_status) => (maybe_status, true),
+ _ => {
+ error!("End of cancel action, but cancel action is not in progress!");
+ return;
+ }
+ };
+ info!(
+ "Finishing cancel action. cancel success = {}",
+ cancelled_successfully
+ );
+ if cancelled_successfully {
+ // If no status was provided, it is because this cancel action removed the BITS job
+ // after the job had already failed. Keep the original error codes.
+ if let Some(status) = maybe_status {
+ self.download_status_nsresult.set(status);
+ self.download_status_error_type
+ .set(Some(BitsStateCancelled));
+ }
+ }
+
+ let next_stage = if cancelled_successfully && !transfer_ended {
+ // This signals on_stop not to allow the status codes set above to
+ // be overridden by the ones passed to it.
+ CancelAction::RequestEndPending
+ } else {
+ CancelAction::NotInProgress
+ };
+ self.cancel_action.set(next_stage);
+
+ if cancelled_successfully {
+ self.on_finished();
+ }
+
+ if transfer_ended {
+ info!("Running deferred OnStopRequest");
+ self.on_stop(None);
+ }
+ }
+
+ nsIBitsRequest_method!(
+ [Action::SetPriority]
+ set_priority_high => SetPriorityHigh()
+ );
+ fn set_priority_high(&self, callback: &nsIBitsCallback) -> Result<(), BitsTaskError> {
+ self.set_priority(Priority::High, callback)
+ }
+
+ nsIBitsRequest_method!(
+ [Action::SetPriority]
+ set_priority_low => SetPriorityLow()
+ );
+ fn set_priority_low(&self, callback: &nsIBitsCallback) -> Result<(), BitsTaskError> {
+ self.set_priority(Priority::Low, callback)
+ }
+
+ fn set_priority(
+ &self,
+ priority: Priority,
+ callback: &nsIBitsCallback,
+ ) -> Result<(), BitsTaskError> {
+ if self.request_has_transferred() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::SetPriority,
+ Pretask,
+ ));
+ }
+
+ let task: Box<SetPriorityTask> = Box::new(SetPriorityTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ priority,
+ RefPtr::new(callback),
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::set_priority",
+ Action::SetPriority,
+ )
+ }
+
+ nsIBitsRequest_method!(
+ [Action::SetNoProgressTimeout]
+ set_no_progress_timeout => SetNoProgressTimeout(timeout_secs: u32)
+ );
+ fn set_no_progress_timeout(
+ &self,
+ timeout_secs: u32,
+ callback: &nsIBitsCallback,
+ ) -> Result<(), BitsTaskError> {
+ if self.request_has_transferred() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::SetNoProgressTimeout,
+ Pretask,
+ ));
+ }
+
+ let task: Box<SetNoProgressTimeoutTask> = Box::new(SetNoProgressTimeoutTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ timeout_secs,
+ RefPtr::new(callback),
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::set_no_progress_timeout",
+ Action::SetNoProgressTimeout,
+ )
+ }
+
+ nsIBitsRequest_method!(
+ [Action::Complete]
+ complete => Complete()
+ );
+ fn complete(&self, callback: &nsIBitsCallback) -> Result<(), BitsTaskError> {
+ if self.request_has_completed() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::Complete,
+ Pretask,
+ ));
+ }
+
+ let task: Box<CompleteTask> = Box::new(CompleteTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ RefPtr::new(callback),
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::complete",
+ Action::Complete,
+ )
+ }
+
+ nsIBitsRequest_method!(
+ [Action::Suspend]
+ suspend_nsIBitsRequest => SuspendAsync()
+ );
+ #[allow(non_snake_case)]
+ fn suspend_nsIBitsRequest(&self, callback: &nsIBitsCallback) -> Result<(), BitsTaskError> {
+ self.suspend(Some(RefPtr::new(callback)))
+ }
+ xpcom_method!(
+ suspend_nsIRequest => Suspend()
+ );
+ #[allow(non_snake_case)]
+ fn suspend_nsIRequest(&self) -> Result<(), BitsTaskError> {
+ self.suspend(None)
+ }
+
+ fn suspend(&self, callback: Option<RefPtr<nsIBitsCallback>>) -> Result<(), BitsTaskError> {
+ if self.request_has_transferred() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::Suspend,
+ Pretask,
+ ));
+ }
+
+ let task: Box<SuspendTask> = Box::new(SuspendTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ callback,
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::suspend",
+ Action::Suspend,
+ )
+ }
+
+ nsIBitsRequest_method!(
+ [Action::Resume]
+ resume_nsIBitsRequest => ResumeAsync()
+ );
+ #[allow(non_snake_case)]
+ fn resume_nsIBitsRequest(&self, callback: &nsIBitsCallback) -> Result<(), BitsTaskError> {
+ self.resume(Some(RefPtr::new(callback)))
+ }
+ xpcom_method!(
+ resume_nsIRequest => Resume()
+ );
+ #[allow(non_snake_case)]
+ fn resume_nsIRequest(&self) -> Result<(), BitsTaskError> {
+ self.resume(None)
+ }
+
+ fn resume(&self, callback: Option<RefPtr<nsIBitsCallback>>) -> Result<(), BitsTaskError> {
+ if self.request_has_transferred() {
+ return Err(BitsTaskError::new(
+ TransferAlreadyComplete,
+ Action::Resume,
+ Pretask,
+ ));
+ }
+
+ let task: Box<ResumeTask> = Box::new(ResumeTask::new(
+ RefPtr::new(self),
+ self.bits_id.clone(),
+ callback,
+ ));
+
+ self.bits_service.dispatch_runnable_to_command_thread(
+ task,
+ "BitsRequest::resume",
+ Action::Resume,
+ )
+ }
+
+ xpcom_method!(
+ get_load_group => GetLoadGroup() -> *const nsILoadGroup
+ );
+
+ /**
+ * As stated in nsIBits.idl, nsIBits interfaces are not expected to
+ * implement the loadGroup or loadFlags attributes. This implementation
+ * provides only null implementations only for these methods.
+ */
+ fn get_load_group(&self) -> Result<RefPtr<nsILoadGroup>, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ set_load_group => SetLoadGroup(_load_group: *const nsILoadGroup)
+ );
+ fn set_load_group(&self, _load_group: &nsILoadGroup) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ get_load_flags => GetLoadFlags() -> nsLoadFlags
+ );
+ fn get_load_flags(&self) -> Result<nsLoadFlags, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ set_load_flags => SetLoadFlags(_load_flags: nsLoadFlags)
+ );
+ fn set_load_flags(&self, _load_flags: nsLoadFlags) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ get_trr_mode => GetTRRMode() -> u32
+ );
+ fn get_trr_mode(&self) -> Result<u32, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ set_trr_mode => SetTRRMode(_trr_mode: u32)
+ );
+ fn set_trr_mode(&self, _trr_mode: u32) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ get_canceled_reason => GetCanceledReason() -> nsACString
+ );
+ fn get_canceled_reason(&self) -> Result<nsCString, nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ set_canceled_reason => SetCanceledReason(_reason: *const nsACString)
+ );
+ fn set_canceled_reason(&self, _reason: *const nsACString) -> Result<(), nsresult> {
+ Err(NS_ERROR_NOT_IMPLEMENTED)
+ }
+
+ xpcom_method!(
+ cancel_with_reason_nsIRequest => CancelWithReason(status: nsresult, _reason: *const nsACString)
+ );
+ #[allow(non_snake_case)]
+ fn cancel_with_reason_nsIRequest(
+ &self,
+ status: nsresult,
+ _reason: *const nsACString,
+ ) -> Result<(), BitsTaskError> {
+ self.cancel(status, None)
+ }
+}
+
+impl Drop for BitsRequest {
+ fn drop(&mut self) {
+ // Make sure that the monitor thread gets cleaned up.
+ self.shutdown_monitor_thread();
+ // Make sure we tell BitsService that we are done with the command thread.
+ self.on_finished();
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/string.rs b/toolkit/components/bitsdownload/src/bits_interface/string.rs
new file mode 100644
index 0000000000..c3111b7cba
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/string.rs
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ action::Action,
+ error::{BitsTaskError, ErrorStage, ErrorType},
+};
+
+use bits_client::Guid;
+use nsstring::nsCString;
+use std::ffi::OsString;
+use std::{str, str::FromStr};
+
+/// This function is fallible, and the consumers would prefer a BitsTaskError
+/// in the failure case. To facilitate that, this function takes some params
+/// that give it the data necessary to construct the BitsTaskError if it fails.
+/// If it succeeds, those values will be unused.
+#[allow(non_snake_case)]
+pub fn nsCString_to_String(
+ value: &nsCString,
+ error_action: Action,
+ error_stage: ErrorStage,
+) -> Result<String, BitsTaskError> {
+ match String::from_utf8(value[..].to_vec()) {
+ Ok(s) => Ok(s),
+ Err(_) => Err(BitsTaskError::new(
+ ErrorType::NoUtf8Conversion,
+ error_action,
+ error_stage,
+ )),
+ }
+}
+
+/// This function is fallible, and the consumers would prefer a BitsTaskError
+/// in the failure case. To facilitate that, this function takes some params
+/// that give it the data necessary to construct the BitsTaskError if it fails.
+/// If it succeeds, those values will be unused.
+#[allow(non_snake_case)]
+pub fn nsCString_to_OsString(
+ value: &nsCString,
+ error_action: Action,
+ error_stage: ErrorStage,
+) -> Result<OsString, BitsTaskError> {
+ Ok(OsString::from(nsCString_to_String(
+ value,
+ error_action,
+ error_stage,
+ )?))
+}
+
+/// This function is fallible, and the consumers would prefer a BitsTaskError
+/// in the failure case. To facilitate that, this function takes some params
+/// that give it the data necessary to construct the BitsTaskError if it fails.
+/// If it succeeds, those values will be unused.
+#[allow(non_snake_case)]
+pub fn Guid_from_nsCString(
+ value: &nsCString,
+ error_action: Action,
+ error_stage: ErrorStage,
+) -> Result<Guid, BitsTaskError> {
+ let vector = &value[..].to_vec();
+ let string = match str::from_utf8(vector) {
+ Ok(s) => s,
+ Err(_) => {
+ return Err(BitsTaskError::new(
+ ErrorType::NoUtf8Conversion,
+ error_action,
+ error_stage,
+ ));
+ }
+ };
+ Guid::from_str(string).map_err(|comedy_error| {
+ BitsTaskError::from_comedy(
+ ErrorType::InvalidGuid,
+ error_action,
+ error_stage,
+ comedy_error,
+ )
+ })
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/task/client.rs b/toolkit/components/bitsdownload/src/bits_interface/task/client.rs
new file mode 100644
index 0000000000..edbf4d0698
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/task/client.rs
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ action::Action,
+ error::{BitsTaskError, ErrorStage::CommandThread, ErrorType::MissingBitsClient},
+ string::nsCString_to_OsString,
+};
+
+use bits_client::BitsClient;
+use nsstring::nsCString;
+use std::cell::Cell;
+
+thread_local! {
+ // This is used to store the `BitsClient` on the Command thread.
+ // Keeping it here solves the problem of how to allow multiple runnables to
+ // be simultaneously queued on the Command thread while giving them all
+ // access to the same `BitsClient`.
+ static BITS_CLIENT: Cell<Option<BitsClient>> = Cell::new(None);
+}
+
+/// This structure holds the data needed to initialize `BitsClient` and
+/// `BitsMonitorClient`.
+#[derive(Debug, Clone)]
+pub struct ClientInitData {
+ pub job_name: nsCString,
+ pub save_path_prefix: nsCString,
+ pub monitor_timeout_ms: u32,
+}
+
+impl ClientInitData {
+ pub fn new(
+ job_name: nsCString,
+ save_path_prefix: nsCString,
+ monitor_timeout_ms: u32,
+ ) -> ClientInitData {
+ ClientInitData {
+ job_name,
+ save_path_prefix,
+ monitor_timeout_ms,
+ }
+ }
+}
+
+/// This function constructs a `BitsClient`, if one does not already exist. If
+/// the `BitsClient` cannot be constructed, a `BitsTaskError` will be returned.
+/// If the `BitsClient` could be obtained, then the function then calls the
+/// closure passed to it, passing a mutable reference to the `BitsClient`.
+/// This function will then return whatever the closure returned, which must be
+/// a `Result<_, BitsTaskError>`.
+pub fn with_maybe_new_bits_client<F, R>(
+ init_data: &ClientInitData,
+ action: Action,
+ closure: F,
+) -> Result<R, BitsTaskError>
+where
+ F: FnOnce(&mut BitsClient) -> Result<R, BitsTaskError>,
+{
+ _with_bits_client(Some(init_data), action, closure)
+}
+
+/// This function assumes that a `BitsClient` has already been constructed. If
+/// there is not one available, a `BitsTaskError` will be returned. Otherwise,
+/// the function calls the closure passed to it, passing a mutable reference to
+/// the `BitsClient`. This function will then return whatever the closure
+/// returned, which must be a `Result<_, BitsTaskError>`.
+pub fn with_bits_client<F, R>(action: Action, closure: F) -> Result<R, BitsTaskError>
+where
+ F: FnOnce(&mut BitsClient) -> Result<R, BitsTaskError>,
+{
+ _with_bits_client(None, action, closure)
+}
+
+fn _with_bits_client<F, R>(
+ maybe_init_data: Option<&ClientInitData>,
+ action: Action,
+ closure: F,
+) -> Result<R, BitsTaskError>
+where
+ F: FnOnce(&mut BitsClient) -> Result<R, BitsTaskError>,
+{
+ BITS_CLIENT.with(|cell| {
+ let maybe_client = cell.take();
+ let mut client = match (maybe_client, maybe_init_data) {
+ (Some(r), _) => r,
+ (None, Some(init_data)) => {
+ // Immediately invoked function to allow for the ? operator
+ BitsClient::new(
+ nsCString_to_OsString(&init_data.job_name, action, CommandThread)?,
+ nsCString_to_OsString(&init_data.save_path_prefix, action, CommandThread)?,
+ )
+ .map_err(|pipe_error| BitsTaskError::from_pipe(action, pipe_error))?
+ }
+ (None, None) => {
+ return Err(BitsTaskError::new(MissingBitsClient, action, CommandThread));
+ }
+ };
+ let result = closure(&mut client);
+ cell.set(Some(client));
+ result
+ })
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/task/from_threadbound.rs b/toolkit/components/bitsdownload/src/bits_interface/task/from_threadbound.rs
new file mode 100644
index 0000000000..2cf2e9189a
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/task/from_threadbound.rs
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ action::Action,
+ error::{BitsTaskError, ErrorStage, ErrorType},
+};
+use log::warn;
+use xpcom::{RefCounted, ThreadBoundRefPtr};
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum DataType {
+ Callback,
+ BitsService,
+ BitsRequest,
+ Observer,
+ Context,
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+enum GetThreadboundError {
+ Missing,
+ WrongThread,
+}
+
+impl DataType {
+ fn error_type(&self, error: GetThreadboundError) -> ErrorType {
+ match self {
+ DataType::Callback => match error {
+ GetThreadboundError::Missing => ErrorType::MissingCallback,
+ GetThreadboundError::WrongThread => ErrorType::CallbackOnWrongThread,
+ },
+ DataType::BitsService => match error {
+ GetThreadboundError::Missing => ErrorType::MissingBitsService,
+ GetThreadboundError::WrongThread => ErrorType::BitsServiceOnWrongThread,
+ },
+ DataType::BitsRequest => match error {
+ GetThreadboundError::Missing => ErrorType::MissingBitsRequest,
+ GetThreadboundError::WrongThread => ErrorType::BitsRequestOnWrongThread,
+ },
+ DataType::Observer => match error {
+ GetThreadboundError::Missing => ErrorType::MissingObserver,
+ GetThreadboundError::WrongThread => ErrorType::ObserverOnWrongThread,
+ },
+ DataType::Context => match error {
+ GetThreadboundError::Missing => ErrorType::MissingContext,
+ GetThreadboundError::WrongThread => ErrorType::ContextOnWrongThread,
+ },
+ }
+ }
+
+ fn name(&self) -> &'static str {
+ match self {
+ DataType::Callback => "Callback",
+ DataType::BitsService => "BITS Service",
+ DataType::BitsRequest => "BITS Request",
+ DataType::Observer => "Observer",
+ DataType::Context => "Context",
+ }
+ }
+}
+
+/// Given a reference to a threadbound option
+/// (i.e. `&Option<ThreadBoundRefPtr<_>>`), this function will attempt to
+/// retrieve a reference to the value stored within. If it is not available
+/// (option is `None` or value is on the wrong thread), `None` is returned
+/// instead.
+pub fn get_from_threadbound_option<T>(
+ maybe_threadbound: &Option<ThreadBoundRefPtr<T>>,
+ data_type: DataType,
+ action: Action,
+) -> Option<&T>
+where
+ T: RefCounted + 'static,
+{
+ maybe_threadbound.as_ref().and_then(|threadbound| {
+ let maybe_reference = threadbound.get_ref();
+ if maybe_reference.is_none() {
+ warn!(
+ "Unexpected error {}: {} is on the wrong thread",
+ action.description(),
+ data_type.name(),
+ );
+ }
+ maybe_reference
+ })
+}
+
+/// Given a reference to a threadbound option
+/// (i.e. `&Option<ThreadBoundRefPtr<_>>`), this function will attempt to
+/// retrieve a reference to the value stored within. If it is not available
+/// (option is `None` or value is on the wrong thread), a `BitsTaskError` is
+/// returned instead.
+pub fn expect_from_threadbound_option<T>(
+ maybe_threadbound: &Option<ThreadBoundRefPtr<T>>,
+ data_type: DataType,
+ action: Action,
+) -> Result<&T, BitsTaskError>
+where
+ T: RefCounted + 'static,
+{
+ match maybe_threadbound.as_ref() {
+ Some(threadbound) => {
+ match threadbound.get_ref() {
+ Some(reference) => Ok(reference),
+ None => Err(BitsTaskError::new(
+ data_type.error_type(GetThreadboundError::WrongThread),
+ action,
+ // Retrieving data from threadbounds all happens on the main thread.
+ // No data is ever bound to other threads so there would be no
+ // reason to retrieve it there.
+ ErrorStage::MainThread,
+ )),
+ }
+ }
+ None => Err(BitsTaskError::new(
+ data_type.error_type(GetThreadboundError::Missing),
+ action,
+ // Retrieving data from threadbounds all happens on the main thread.
+ // No data is ever bound to other threads so there would be no
+ // reason to retrieve it there.
+ ErrorStage::MainThread,
+ )),
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/task/mod.rs b/toolkit/components/bitsdownload/src/bits_interface/task/mod.rs
new file mode 100644
index 0000000000..b6b96d887b
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/task/mod.rs
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+mod from_threadbound;
+
+use super::{action, dispatch_callback, error, request::BitsRequest, string, BitsService};
+
+mod client;
+pub use self::client::ClientInitData;
+
+mod service_task;
+pub use self::service_task::{MonitorDownloadTask, StartDownloadTask};
+
+mod request_task;
+pub use self::request_task::{
+ CancelTask, ChangeMonitorIntervalTask, CompleteTask, Priority, ResumeTask,
+ SetNoProgressTimeoutTask, SetPriorityTask, SuspendTask,
+};
diff --git a/toolkit/components/bitsdownload/src/bits_interface/task/request_task.rs b/toolkit/components/bitsdownload/src/bits_interface/task/request_task.rs
new file mode 100644
index 0000000000..f0fd331ed0
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/task/request_task.rs
@@ -0,0 +1,425 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ action::{Action, RequestAction},
+ client::with_bits_client,
+ dispatch_callback::{
+ maybe_dispatch_via_callback, CallbackExpected, CallbackOptional, IsCallbackExpected,
+ },
+ error::BitsTaskError,
+ from_threadbound::{expect_from_threadbound_option, DataType},
+ BitsRequest,
+};
+
+use bits_client::{BitsClient, Guid};
+use crossbeam_utils::atomic::AtomicCell;
+use log::info;
+use moz_task::Task;
+use nserror::nsresult;
+use xpcom::{interfaces::nsIBitsCallback, RefPtr, ThreadBoundRefPtr};
+
+type RunFn<D> = fn(Guid, &D, &mut BitsClient) -> Result<(), BitsTaskError>;
+type DoneFn = fn(&BitsRequest, bool) -> Result<(), BitsTaskError>;
+
+pub struct RequestTask<D> {
+ request: AtomicCell<Option<ThreadBoundRefPtr<BitsRequest>>>,
+ guid: Guid,
+ action: RequestAction,
+ task_data: D,
+ run_fn: RunFn<D>,
+ maybe_done_fn: Option<DoneFn>,
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIBitsCallback>>>,
+ callback_presence: IsCallbackExpected,
+ result: AtomicCell<Option<Result<(), BitsTaskError>>>,
+}
+
+impl<D> RequestTask<D>
+where
+ D: Sync + Send,
+{
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ guid: Guid,
+ action: RequestAction,
+ task_data: D,
+ run_fn: RunFn<D>,
+ maybe_done_fn: Option<DoneFn>,
+ callback: Option<RefPtr<nsIBitsCallback>>,
+ callback_presence: IsCallbackExpected,
+ ) -> RequestTask<D> {
+ RequestTask {
+ request: AtomicCell::new(Some(ThreadBoundRefPtr::new(request))),
+ guid,
+ action,
+ task_data,
+ run_fn,
+ maybe_done_fn,
+ callback: AtomicCell::new(callback.map(ThreadBoundRefPtr::new)),
+ result: AtomicCell::new(None),
+ callback_presence,
+ }
+ }
+}
+
+impl<D> Task for RequestTask<D> {
+ fn run(&self) {
+ let result = with_bits_client(self.action.into(), |client| {
+ (self.run_fn)(self.guid.clone(), &self.task_data, client)
+ });
+ self.result.store(Some(result));
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ // If TaskRunnable.run() calls Task.done() to return a result
+ // on the main thread before TaskRunnable.run() returns on the worker
+ // thread, then the Task will get dropped on the worker thread.
+ //
+ // But the callback is an nsXPCWrappedJS that isn't safe to release
+ // on the worker thread. So we move it out of the Task here to ensure
+ // it gets released on the main thread.
+ let maybe_tb_callback = self.callback.swap(None);
+ // It also isn't safe to drop the BitsRequest RefPtr off-thread,
+ // because BitsRequest refcounting is non-atomic
+ let maybe_tb_request = self.request.swap(None);
+
+ let action: Action = self.action.into();
+ let maybe_callback =
+ expect_from_threadbound_option(&maybe_tb_callback, DataType::Callback, action);
+
+ // Immediately invoked function expression to allow for the ? operator
+ let result: Result<(), BitsTaskError> = (|| {
+ let request =
+ expect_from_threadbound_option(&maybe_tb_request, DataType::BitsRequest, action)?;
+
+ let maybe_result = self.result.swap(None);
+
+ let success = if let Some(result) = maybe_result.as_ref() {
+ result.is_ok()
+ } else {
+ false
+ };
+
+ if let Some(done_fn) = self.maybe_done_fn {
+ done_fn(request, success)?;
+ }
+
+ maybe_result.ok_or_else(|| BitsTaskError::missing_result(action))?
+ })();
+ info!("BITS Request Task completed: {:?}", result);
+ maybe_dispatch_via_callback(result, maybe_callback, self.callback_presence)
+ }
+}
+
+pub struct CompleteTask(RequestTask<()>);
+
+impl Task for CompleteTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl CompleteTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ callback: RefPtr<nsIBitsCallback>,
+ ) -> CompleteTask {
+ CompleteTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::Complete,
+ (),
+ CompleteTask::run_fn,
+ Some(CompleteTask::done_fn),
+ Some(callback),
+ CallbackExpected,
+ ))
+ }
+
+ fn run_fn(id: Guid, _data: &(), client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .complete_job(id)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(Action::Complete, pipe_error))??;
+ Ok(())
+ }
+
+ fn done_fn(request: &BitsRequest, success: bool) -> Result<(), BitsTaskError> {
+ if success {
+ request.on_finished();
+ }
+ Ok(())
+ }
+}
+
+pub struct CancelTask(RequestTask<()>);
+
+impl Task for CancelTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl CancelTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ callback: Option<RefPtr<nsIBitsCallback>>,
+ ) -> CancelTask {
+ let callback_presence = if callback.is_some() {
+ CallbackExpected
+ } else {
+ CallbackOptional
+ };
+
+ CancelTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::Cancel,
+ (),
+ CancelTask::run_fn,
+ Some(CancelTask::done_fn),
+ callback,
+ callback_presence,
+ ))
+ }
+
+ fn run_fn(id: Guid, _data: &(), client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .cancel_job(id)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(Action::Cancel, pipe_error))??;
+ Ok(())
+ }
+
+ fn done_fn(request: &BitsRequest, success: bool) -> Result<(), BitsTaskError> {
+ request.finish_cancel_action(success);
+ Ok(())
+ }
+}
+
+pub struct SuspendTask(RequestTask<()>);
+
+impl Task for SuspendTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl SuspendTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ callback: Option<RefPtr<nsIBitsCallback>>,
+ ) -> SuspendTask {
+ let callback_presence = if callback.is_some() {
+ CallbackExpected
+ } else {
+ CallbackOptional
+ };
+
+ SuspendTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::Suspend,
+ (),
+ SuspendTask::run_fn,
+ None,
+ callback,
+ callback_presence,
+ ))
+ }
+
+ fn run_fn(id: Guid, _data: &(), client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .suspend_job(id)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(Action::Suspend, pipe_error))??;
+ Ok(())
+ }
+}
+
+pub struct ResumeTask(RequestTask<()>);
+
+impl Task for ResumeTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl ResumeTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ callback: Option<RefPtr<nsIBitsCallback>>,
+ ) -> ResumeTask {
+ let callback_presence = if callback.is_some() {
+ CallbackExpected
+ } else {
+ CallbackOptional
+ };
+
+ ResumeTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::Resume,
+ (),
+ ResumeTask::run_fn,
+ None,
+ callback,
+ callback_presence,
+ ))
+ }
+
+ fn run_fn(id: Guid, _data: &(), client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .resume_job(id)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(Action::Resume, pipe_error))??;
+ Ok(())
+ }
+}
+
+pub struct ChangeMonitorIntervalTask(RequestTask<u32>);
+
+impl Task for ChangeMonitorIntervalTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl ChangeMonitorIntervalTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ update_interval_ms: u32,
+ callback: RefPtr<nsIBitsCallback>,
+ ) -> ChangeMonitorIntervalTask {
+ ChangeMonitorIntervalTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::SetMonitorInterval,
+ update_interval_ms,
+ ChangeMonitorIntervalTask::run_fn,
+ None,
+ Some(callback),
+ CallbackExpected,
+ ))
+ }
+
+ fn run_fn(
+ id: Guid,
+ update_interval_ms: &u32,
+ client: &mut BitsClient,
+ ) -> Result<(), BitsTaskError> {
+ client
+ .set_update_interval(id, *update_interval_ms)
+ .map_err(|pipe_error| {
+ BitsTaskError::from_pipe(Action::SetMonitorInterval, pipe_error)
+ })??;
+ Ok(())
+ }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum Priority {
+ High,
+ Low,
+}
+
+pub struct SetPriorityTask(RequestTask<Priority>);
+
+impl Task for SetPriorityTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl SetPriorityTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ priority: Priority,
+ callback: RefPtr<nsIBitsCallback>,
+ ) -> SetPriorityTask {
+ SetPriorityTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::SetPriority,
+ priority,
+ SetPriorityTask::run_fn,
+ None,
+ Some(callback),
+ CallbackExpected,
+ ))
+ }
+
+ fn run_fn(id: Guid, priority: &Priority, client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .set_job_priority(id, *priority == Priority::High)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(Action::SetPriority, pipe_error))??;
+ Ok(())
+ }
+}
+
+pub struct SetNoProgressTimeoutTask(RequestTask<u32>);
+
+impl Task for SetNoProgressTimeoutTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl SetNoProgressTimeoutTask {
+ pub fn new(
+ request: RefPtr<BitsRequest>,
+ id: Guid,
+ timeout_secs: u32,
+ callback: RefPtr<nsIBitsCallback>,
+ ) -> SetNoProgressTimeoutTask {
+ SetNoProgressTimeoutTask(RequestTask::new(
+ request,
+ id,
+ RequestAction::SetNoProgressTimeout,
+ timeout_secs,
+ SetNoProgressTimeoutTask::run_fn,
+ None,
+ Some(callback),
+ CallbackExpected,
+ ))
+ }
+
+ fn run_fn(id: Guid, timeout_secs: &u32, client: &mut BitsClient) -> Result<(), BitsTaskError> {
+ client
+ .set_no_progress_timeout(id, *timeout_secs)
+ .map_err(|pipe_error| {
+ BitsTaskError::from_pipe(Action::SetNoProgressTimeout, pipe_error)
+ })??;
+ Ok(())
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/task/service_task.rs b/toolkit/components/bitsdownload/src/bits_interface/task/service_task.rs
new file mode 100644
index 0000000000..c66f127f7a
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/task/service_task.rs
@@ -0,0 +1,332 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use super::{
+ action::{
+ Action,
+ Action::{MonitorDownload, StartDownload},
+ ServiceAction,
+ },
+ client::{with_maybe_new_bits_client, ClientInitData},
+ dispatch_callback::{maybe_dispatch_request_via_callback, CallbackExpected},
+ error::{BitsTaskError, ErrorStage::CommandThread},
+ from_threadbound::{expect_from_threadbound_option, get_from_threadbound_option, DataType},
+ string::nsCString_to_OsString,
+ BitsRequest, BitsService,
+};
+
+use bits_client::{BitsClient, BitsMonitorClient, BitsProxyUsage, Guid};
+use crossbeam_utils::atomic::AtomicCell;
+use log::{info, warn};
+use moz_task::Task;
+use nserror::nsresult;
+use nsstring::nsCString;
+use xpcom::{
+ interfaces::{nsIBitsNewRequestCallback, nsIRequestObserver, nsISupports},
+ RefPtr, ThreadBoundRefPtr,
+};
+
+// D is the Data Type that the RunFn function needs to make S.
+// S is the Success Type that the RunFn returns on success and that the
+// DoneFn needs to make the BitsRequest.
+type RunFn<D, S> = fn(&D, &mut BitsClient) -> Result<S, BitsTaskError>;
+type DoneFn<D, S> = fn(
+ &D,
+ S,
+ &ClientInitData,
+ &BitsService,
+ &nsIRequestObserver,
+ Option<&nsISupports>,
+) -> Result<RefPtr<BitsRequest>, BitsTaskError>;
+
+pub struct ServiceTask<D, S> {
+ client_init_data: ClientInitData,
+ action: ServiceAction,
+ task_data: D,
+ run_fn: RunFn<D, S>,
+ done_fn: DoneFn<D, S>,
+ bits_service: AtomicCell<Option<ThreadBoundRefPtr<BitsService>>>,
+ observer: AtomicCell<Option<ThreadBoundRefPtr<nsIRequestObserver>>>,
+ context: AtomicCell<Option<ThreadBoundRefPtr<nsISupports>>>,
+ callback: AtomicCell<Option<ThreadBoundRefPtr<nsIBitsNewRequestCallback>>>,
+ result: AtomicCell<Option<Result<S, BitsTaskError>>>,
+}
+
+impl<D, S> ServiceTask<D, S>
+where
+ D: Sync + Send,
+ S: Sync + Send,
+{
+ pub fn new(
+ client_init_data: ClientInitData,
+ action: ServiceAction,
+ task_data: D,
+ run_fn: RunFn<D, S>,
+ done_fn: DoneFn<D, S>,
+ bits_service: RefPtr<BitsService>,
+ observer: RefPtr<nsIRequestObserver>,
+ context: Option<RefPtr<nsISupports>>,
+ callback: RefPtr<nsIBitsNewRequestCallback>,
+ ) -> ServiceTask<D, S> {
+ ServiceTask {
+ client_init_data,
+ action,
+ task_data,
+ run_fn,
+ done_fn,
+ bits_service: AtomicCell::new(Some(ThreadBoundRefPtr::new(bits_service))),
+ observer: AtomicCell::new(Some(ThreadBoundRefPtr::new(observer))),
+ context: AtomicCell::new(context.map(ThreadBoundRefPtr::new)),
+ callback: AtomicCell::new(Some(ThreadBoundRefPtr::new(callback))),
+ result: AtomicCell::new(None),
+ }
+ }
+}
+
+impl<D, S> Task for ServiceTask<D, S> {
+ fn run(&self) {
+ let result =
+ with_maybe_new_bits_client(&self.client_init_data, self.action.into(), |client| {
+ (self.run_fn)(&self.task_data, client)
+ });
+ self.result.store(Some(result));
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ // If TaskRunnable.run() calls Task.done() to return a result
+ // on the main thread before TaskRunnable.run() returns on the worker
+ // thread, then the Task will get dropped on the worker thread.
+ //
+ // But the callback is an nsXPCWrappedJS that isn't safe to release
+ // on the worker thread. So we move it out of the Task here to ensure
+ // it gets released on the main thread.
+ let maybe_tb_callback = self.callback.swap(None);
+ // It also isn't safe to drop the BitsService RefPtr off-thread,
+ // because BitsService refcounting is non-atomic
+ let maybe_tb_service = self.bits_service.swap(None);
+ // The observer and context are also an nsXPCWrappedJS that aren't safe
+ // to release on the worker thread.
+ let maybe_tb_observer = self.observer.swap(None);
+ let maybe_tb_context = self.context.swap(None);
+
+ let action: Action = self.action.into();
+ let maybe_callback =
+ expect_from_threadbound_option(&maybe_tb_callback, DataType::Callback, action);
+
+ // Immediately invoked function expression to allow for the ? operator
+ let result: Result<RefPtr<BitsRequest>, BitsTaskError> = (|| {
+ let bits_service =
+ expect_from_threadbound_option(&maybe_tb_service, DataType::BitsService, action)?;
+ let observer =
+ expect_from_threadbound_option(&maybe_tb_observer, DataType::Observer, action)?;
+ let maybe_context =
+ get_from_threadbound_option(&maybe_tb_context, DataType::Context, action);
+ let success = self
+ .result
+ .swap(None)
+ .ok_or_else(|| BitsTaskError::missing_result(action))??;
+
+ (self.done_fn)(
+ &self.task_data,
+ success,
+ &self.client_init_data,
+ bits_service,
+ observer,
+ maybe_context,
+ )
+ })();
+ info!("BITS Interface Task completed: {:?}", result);
+ // We incremented the request count when we dispatched an action to
+ // start the job. Now we will decrement since the action completed.
+ // See the declaration of InitBitsService::request_count for details.
+ let bits_service_result =
+ expect_from_threadbound_option(&maybe_tb_service, DataType::BitsService, action);
+ match bits_service_result {
+ Ok(bits_service) => {
+ bits_service.dec_request_count();
+ }
+ Err(error) => {
+ warn!(
+ concat!(
+ "Unable to decrement the request count when finishing ServiceTask. ",
+ "The command thread may not be shut down. Error: {:?}"
+ ),
+ error
+ );
+ }
+ }
+
+ maybe_dispatch_request_via_callback(result, maybe_callback, CallbackExpected)
+ }
+}
+
+struct StartDownloadData {
+ download_url: nsCString,
+ save_rel_path: nsCString,
+ proxy: BitsProxyUsage,
+ no_progress_timeout_secs: u32,
+ update_interval_ms: u32,
+}
+
+struct StartDownloadSuccess {
+ guid: Guid,
+ monitor_client: BitsMonitorClient,
+}
+
+pub struct StartDownloadTask(ServiceTask<StartDownloadData, StartDownloadSuccess>);
+
+impl Task for StartDownloadTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl StartDownloadTask {
+ pub fn new(
+ client_init_data: ClientInitData,
+ download_url: nsCString,
+ save_rel_path: nsCString,
+ proxy: BitsProxyUsage,
+ no_progress_timeout_secs: u32,
+ update_interval_ms: u32,
+ bits_service: RefPtr<BitsService>,
+ observer: RefPtr<nsIRequestObserver>,
+ context: Option<RefPtr<nsISupports>>,
+ callback: RefPtr<nsIBitsNewRequestCallback>,
+ ) -> StartDownloadTask {
+ StartDownloadTask(ServiceTask::new(
+ client_init_data,
+ ServiceAction::StartDownload,
+ StartDownloadData {
+ download_url,
+ save_rel_path,
+ proxy,
+ no_progress_timeout_secs,
+ update_interval_ms,
+ },
+ StartDownloadTask::run_fn,
+ StartDownloadTask::done_fn,
+ bits_service,
+ observer,
+ context,
+ callback,
+ ))
+ }
+
+ fn run_fn(
+ data: &StartDownloadData,
+ client: &mut BitsClient,
+ ) -> Result<StartDownloadSuccess, BitsTaskError> {
+ let url = nsCString_to_OsString(&data.download_url, StartDownload, CommandThread)?;
+ let path = nsCString_to_OsString(&data.save_rel_path, StartDownload, CommandThread)?;
+ let (success, monitor_client) = client
+ .start_job(
+ url,
+ path,
+ data.proxy,
+ data.no_progress_timeout_secs,
+ data.update_interval_ms,
+ )
+ .map_err(|pipe_error| BitsTaskError::from_pipe(StartDownload, pipe_error))??;
+ Ok(StartDownloadSuccess {
+ guid: success.guid,
+ monitor_client,
+ })
+ }
+
+ fn done_fn(
+ _data: &StartDownloadData,
+ success: StartDownloadSuccess,
+ client_init_data: &ClientInitData,
+ bits_service: &BitsService,
+ observer: &nsIRequestObserver,
+ maybe_context: Option<&nsISupports>,
+ ) -> Result<RefPtr<BitsRequest>, BitsTaskError> {
+ BitsRequest::new(
+ success.guid.clone(),
+ RefPtr::new(bits_service),
+ client_init_data.monitor_timeout_ms,
+ RefPtr::new(&observer),
+ maybe_context.map(RefPtr::new),
+ success.monitor_client,
+ ServiceAction::StartDownload,
+ )
+ }
+}
+
+struct MonitorDownloadData {
+ guid: Guid,
+ update_interval_ms: u32,
+}
+
+pub struct MonitorDownloadTask(ServiceTask<MonitorDownloadData, BitsMonitorClient>);
+
+impl Task for MonitorDownloadTask {
+ fn run(&self) {
+ self.0.run();
+ }
+
+ fn done(&self) -> Result<(), nsresult> {
+ self.0.done()
+ }
+}
+
+impl MonitorDownloadTask {
+ pub fn new(
+ client_init_data: ClientInitData,
+ guid: Guid,
+ update_interval_ms: u32,
+ bits_service: RefPtr<BitsService>,
+ observer: RefPtr<nsIRequestObserver>,
+ context: Option<RefPtr<nsISupports>>,
+ callback: RefPtr<nsIBitsNewRequestCallback>,
+ ) -> MonitorDownloadTask {
+ MonitorDownloadTask(ServiceTask::new(
+ client_init_data,
+ ServiceAction::MonitorDownload,
+ MonitorDownloadData {
+ guid,
+ update_interval_ms,
+ },
+ MonitorDownloadTask::run_fn,
+ MonitorDownloadTask::done_fn,
+ bits_service,
+ observer,
+ context,
+ callback,
+ ))
+ }
+
+ fn run_fn(
+ data: &MonitorDownloadData,
+ client: &mut BitsClient,
+ ) -> Result<BitsMonitorClient, BitsTaskError> {
+ let result = client
+ .monitor_job(data.guid.clone(), data.update_interval_ms)
+ .map_err(|pipe_error| BitsTaskError::from_pipe(MonitorDownload, pipe_error));
+ Ok(result??)
+ }
+
+ fn done_fn(
+ data: &MonitorDownloadData,
+ monitor_client: BitsMonitorClient,
+ client_init_data: &ClientInitData,
+ bits_service: &BitsService,
+ observer: &nsIRequestObserver,
+ maybe_context: Option<&nsISupports>,
+ ) -> Result<RefPtr<BitsRequest>, BitsTaskError> {
+ BitsRequest::new(
+ data.guid.clone(),
+ RefPtr::new(bits_service),
+ client_init_data.monitor_timeout_ms,
+ RefPtr::new(&observer),
+ maybe_context.map(RefPtr::new),
+ monitor_client,
+ ServiceAction::MonitorDownload,
+ )
+ }
+}
diff --git a/toolkit/components/bitsdownload/src/bits_interface/xpcom_methods.rs b/toolkit/components/bitsdownload/src/bits_interface/xpcom_methods.rs
new file mode 100644
index 0000000000..cb267b9634
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/bits_interface/xpcom_methods.rs
@@ -0,0 +1,195 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/// This macro is very similar to xpcom_macro, but works a bit differently:
+///
+/// When possible, it returns errors via the callback rather than via the return
+/// value.
+///
+/// It implicitly adds the callback argument of type: nsIBitsNewRequestCallback
+///
+/// It needs an action type, to be specified before the rust name, in square
+/// brackets.
+///
+/// The rustic implementation that the xpcom method calls is expected to return
+/// the type: Result<_, BitsTaskError>. If this value is Ok, it will be ignored.
+/// If the value is Err, it will be returned via the callback passed.
+///
+/// Usage like this:
+///
+/// ```ignore
+/// nsIBits_method!(
+/// [ActionType]
+/// rust_method => XpcomMethod(
+/// foo: *const nsACString,
+/// bar: *const nsIBar,
+/// baz: bool,
+/// [optional] qux: *const nsIQux,
+/// )
+/// );
+/// ```
+///
+/// Results in the macro generating a method like:
+///
+/// ```ignore
+/// unsafe fn XpcomMethod(
+/// &self,
+/// foo: *const nsACString,
+/// bar: *const nsIBar,
+/// baz: bool,
+/// qux: *const nsIQux,
+/// callback: *const nsIBitsNewRequestCallback,
+/// ) -> nsresult {
+/// let callback: &nsIBitsNewRequestCallback = match xpcom::Ensure::ensure(callback) {
+/// Ok(val) => val,
+/// Err(result) => return result,
+/// };
+/// let foo = match xpcom::Ensure::ensure(foo) {
+/// Ok(val) => val,
+/// Err(_) => {
+/// dispatch_pretask_interface_error(BitsTaskError::new(ErrorType::NullArgument, ActionType.into(), ErrorStage::Pretask), callback);
+/// return NS_OK;
+/// }
+/// };
+/// let bar = match xpcom::Ensure::ensure(bar) {
+/// Ok(val) => val,
+/// Err(_) => {
+/// dispatch_pretask_interface_error(BitsTaskError::new(ErrorType::NullArgument, ActionType.into(), ErrorStage::Pretask), callback);
+/// return NS_OK;
+/// }
+/// };
+/// let baz = match xpcom::Ensure::ensure(baz) {
+/// Ok(val) => val,
+/// Err(_) => {
+/// dispatch_pretask_interface_error(BitsTaskError::new(ErrorType::NullArgument, ActionType.into(), ErrorStage::Pretask), callback);
+/// return NS_OK;
+/// }
+/// };
+/// let qux = match xpcom::Ensure::ensure(qux) {
+/// Ok(val) => Some(val),
+/// Err(_) => None,
+/// };
+///
+/// if let Err(error) = self.rust_method(foo, bar, baz, qux, callback) {
+/// dispatch_pretask_interface_error(error, callback);
+/// }
+///
+/// NS_OK
+/// }
+/// ```
+///
+/// Which expects a Rustic implementation method like:
+///
+/// ```ignore
+/// fn rust_method(
+/// &self,
+/// foo: &nsACString,
+/// bar: &nsIBar,
+/// baz: bool,
+/// qux: Option<&nsIQux>,
+/// callback: &nsIBitsNewRequestCallback,
+/// ) -> Result<(), BitsTaskError> {
+/// do_something()
+/// }
+/// ```
+#[macro_export]
+macro_rules! nsIBits_method {
+ // The internal rule @ensure_param converts raw pointer arguments to
+ // references, calling dispatch_pretask_interface_error and returning if the
+ // argument is null.
+ // If, however, the type is optional, the reference will also be wrapped
+ // in an option and null pointers will be converted to None.
+ (@ensure_param [optional] $name:ident, $action:expr, $callback:ident) => {
+ let $name = match Ensure::ensure($name) {
+ Ok(val) => Some(val),
+ Err(_) => None,
+ };
+ };
+ (@ensure_param $name:ident, $action:expr, $callback:ident) => {
+ let $name = match Ensure::ensure($name) {
+ Ok(val) => val,
+ Err(_) => {
+ dispatch_pretask_interface_error(BitsTaskError::new(NullArgument, $action.into(), Pretask), $callback);
+ return NS_OK;
+ }
+ };
+ };
+
+ ([$action:expr] $rust_name:ident => $xpcom_name:ident($($([$param_required:ident])* $param_name:ident: $param_type:ty $(,)*)*)) => {
+ #[allow(non_snake_case)]
+ unsafe fn $xpcom_name(&self, $($param_name: $param_type, )* callback: *const nsIBitsNewRequestCallback) -> nsresult {
+ use xpcom::Ensure;
+ use nserror::NS_OK;
+ // When no params are passed, the imports below will not be used, so silence the
+ // warning
+ #[allow(unused_imports)]
+ use bits_interface::{
+ dispatch_callback::dispatch_pretask_interface_error,
+ error::{BitsTaskError, ErrorStage::Pretask, ErrorType::NullArgument},
+ };
+
+ let callback: &nsIBitsNewRequestCallback = match Ensure::ensure(callback) {
+ Ok(val) => val,
+ Err(result) => return result,
+ };
+ $(nsIBits_method!(@ensure_param $([$param_required])* $param_name, $action, callback);)*
+ if let Err(error) = self.$rust_name($($param_name, )* callback) {
+ dispatch_pretask_interface_error(error, callback);
+ }
+ NS_OK
+ }
+ };
+}
+
+/*
+ * Same as above, but expects a nsIBitsCallback as its callback.
+ */
+#[macro_export]
+macro_rules! nsIBitsRequest_method {
+ // The internal rule @ensure_param converts raw pointer arguments to
+ // references, calling dispatch_pretask_interface_error and returning if the
+ // argument is null.
+ // If, however, the type is optional, the reference will also be wrapped
+ // in an option and null pointers will be converted to None.
+ (@ensure_param [optional] $name:ident, $action:expr, $callback:ident) => {
+ let $name = match Ensure::ensure($name) {
+ Ok(val) => Some(val),
+ Err(_) => None,
+ };
+ };
+ (@ensure_param $name:ident, $action:expr, $callback:ident) => {
+ let $name = match Ensure::ensure($name) {
+ Ok(val) => val,
+ Err(_) => {
+ dispatch_pretask_request_error(BitsTaskError::new(NullArgument, $action.into(), Pretask), $callback);
+ return NS_OK;
+ }
+ };
+ };
+
+ ([$action:expr] $rust_name:ident => $xpcom_name:ident($($([$param_required:ident])* $param_name:ident: $param_type:ty $(,)*)*)) => {
+ #[allow(non_snake_case)]
+ unsafe fn $xpcom_name(&self, $($param_name: $param_type, )* callback: *const nsIBitsCallback) -> nsresult {
+ use xpcom::Ensure;
+ use nserror::NS_OK;
+ // When no params are passed, the imports below will not be used, so silence the
+ // warning
+ #[allow(unused_imports)]
+ use bits_interface::{
+ dispatch_callback::dispatch_pretask_request_error,
+ error::{BitsTaskError, ErrorStage::Pretask, ErrorType::NullArgument},
+ };
+
+ let callback: &nsIBitsCallback = match Ensure::ensure(callback) {
+ Ok(val) => val,
+ Err(result) => return result,
+ };
+ $(nsIBitsRequest_method!(@ensure_param $([$param_required])* $param_name, $action, callback);)*
+ if let Err(error) = self.$rust_name($($param_name, )* callback) {
+ dispatch_pretask_request_error(error, callback);
+ }
+ NS_OK
+ }
+ };
+}
diff --git a/toolkit/components/bitsdownload/src/lib.rs b/toolkit/components/bitsdownload/src/lib.rs
new file mode 100644
index 0000000000..3b8ef021da
--- /dev/null
+++ b/toolkit/components/bitsdownload/src/lib.rs
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! This crate is meant to be used in Windows only. It provides the
+//! bits_interface module, which implements the nsIBits an nsIBitsRequest
+//! XPCOM interfaces. These interfaces allow usage of the Windows component:
+//! BITS (Background Intelligent Transfer Service). Further documentation can
+//! be found in the XPCOM interface definition, located in nsIBits.idl
+
+#![cfg(target_os = "windows")]
+
+extern crate bits_client;
+extern crate comedy;
+extern crate crossbeam_utils;
+extern crate libc;
+extern crate log;
+extern crate moz_task;
+extern crate nserror;
+extern crate nsstring;
+extern crate xpcom;
+
+pub mod bits_interface;