diff options
Diffstat (limited to 'toolkit/components/bitsdownload')
34 files changed, 8083 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.sys.mjs b/toolkit/components/bitsdownload/Bits.sys.mjs new file mode 100644 index 0000000000..3248d2effa --- /dev/null +++ b/toolkit/components/bitsdownload/Bits.sys.mjs @@ -0,0 +1,826 @@ +/* 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. + */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "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. + */ +export 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. +export 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 + ); + } +} + +export 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. + */ +export 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) {} + + // Check if we are not late in creating new requests. + if ( + Services && + Services.startup && + Services.startup.isInOrBeyondShutdownPhase( + Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED + ) + ) { + let error = new BitsError( + lazy.gBits.ERROR_TYPE_BROWSER_SHUTTING_DOWN, + errorAction, + lazy.gBits.ERROR_STAGE_PRETASK, + lazy.gBits.ERROR_CODE_TYPE_NONE + ); + reject(error); + return; + } + + // 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); + } + }); +} + +export 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 + ); + }); + }, +}; 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..9a2c6e23db --- /dev/null +++ b/toolkit/components/bitsdownload/moz.build @@ -0,0 +1,28 @@ +# -*- 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.sys.mjs", +] + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") diff --git a/toolkit/components/bitsdownload/nsIBits.idl b/toolkit/components/bitsdownload/nsIBits.idl new file mode 100644 index 0000000000..993c10243c --- /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.sys.mjs. + * + * 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; + const long ERROR_TYPE_BROWSER_SHUTTING_DOWN = 51; + /** + * 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.sys.mjs. + * + * 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..460e3b4950 --- /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("BitsMonitor").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; |