/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * This module is used to interact with the Windows BITS component (Background * Intelligent Transfer Service). This functionality cannot be used unless on * Windows. * * The reason for this file's existence is that the interfaces in nsIBits.idl * are asynchronous, but are unable to use Promises because they are implemented * in Rust, which does not yet support Promises. This file functions as a layer * between the Rust and the JS that provides access to the functionality * provided by nsIBits via Promises rather than callbacks. */ "use strict"; const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; // This conditional prevents errors if this file is imported from operating // systems other than Windows. This is purely for convenient importing, because // attempting to use anything in this file on platforms other than Windows will // result in an error. if (AppConstants.MOZ_BITS_DOWNLOAD) { XPCOMUtils.defineLazyServiceGetter( lazy, "gBits", "@mozilla.org/bits;1", "nsIBits" ); } // This value exists to mitigate a very unlikely problem: If a BITS method // catastrophically fails, it may never call its callback. This would result in // methods in this file returning promises that never resolve. This could, in // turn, lead to download code hanging altogether rather than being able to // report errors and utilize fallback mechanisms. // This problem is mitigated by giving these promises a timeout, the length of // which will be determined by this value. const kBitsMethodTimeoutMs = 10 * 60 * 1000; // 10 minutes /** * This class will wrap the errors returned by the nsIBits interface to make * them more uniform and more easily consumable. * * The values of stored by this error type are entirely numeric. This should * make them easier to consume with JS and telemetry, but does make them fairly * unreadable. nsIBits.idl will need to be referenced to look up what errors * the values correspond to. * * The type of BitsError.code is dependent on the value of BitsError.codeType. * It may be null, a number (corresponding to an nsresult or hresult value), * a string, or an exception. */ class BitsError extends Error { // If codeType == "none", code may be unspecified. constructor(type, action, stage, codeType, code) { let message = `${BitsError.name} {type: ${type}, action: ${action}, ` + `stage: ${stage}`; switch (codeType) { case lazy.gBits.ERROR_CODE_TYPE_NONE: code = null; message += ", codeType: none}"; break; case lazy.gBits.ERROR_CODE_TYPE_NSRESULT: message += `, codeType: nsresult, code: ${code}}`; break; case lazy.gBits.ERROR_CODE_TYPE_HRESULT: message += `, codeType: hresult, code: ${code}}`; break; case lazy.gBits.ERROR_CODE_TYPE_STRING: message += `, codeType: string, code: ${JSON.stringify(code)}}`; break; case lazy.gBits.ERROR_CODE_TYPE_EXCEPTION: message += `, codeType: exception, code: ${code}}`; break; default: message += ", codeType: invalid}"; break; } super(message); this.type = type; this.action = action; this.stage = stage; this.codeType = codeType; this.code = code; this.name = this.constructor.name; this.succeeded = false; } } // These specializations exist to make them easier to construct since they may // need to be constructed outside of this file. class BitsVerificationError extends BitsError { constructor() { super( Ci.nsIBits.ERROR_TYPE_VERIFICATION_FAILURE, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_VERIFICATION, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } } class BitsUnknownError extends BitsError { constructor() { super( Ci.nsIBits.ERROR_TYPE_UNKNOWN, Ci.nsIBits.ERROR_ACTION_UNKNOWN, Ci.nsIBits.ERROR_STAGE_UNKNOWN, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } } /** * Returns a timer object. If the timer expires, reject will be called with * a BitsError error. The timer's cancel method should be called if the promise * resolves or rejects without the timeout expiring. */ function makeTimeout(reject, errorAction) { let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback( () => { let error = new BitsError( lazy.gBits.ERROR_TYPE_METHOD_TIMEOUT, errorAction, lazy.gBits.ERROR_STAGE_UNKNOWN, lazy.gBits.ERROR_CODE_TYPE_NONE ); reject(error); }, kBitsMethodTimeoutMs, Ci.nsITimer.TYPE_ONE_SHOT ); return timer; } /** * This function does all of the wrapping and error handling for an async * BitsRequest method. This allows the implementations for those methods to * simply call this function with a closure that executes appropriate * nsIBitsRequest method. * * Specifically, this function takes an nsBitsErrorAction and a function. * The nsBitsErrorAction will be used when constructing a BitsError, if the * wrapper encounters an error. * The function will be passed the callback function that should be passed to * the nsIBitsRequest method. */ async function requestPromise(errorAction, actionFn) { return new Promise((resolve, reject) => { let timer = makeTimeout(reject, errorAction); let callback = { QueryInterface: ChromeUtils.generateQI(["nsIBitsCallback"]), success() { timer.cancel(); resolve(); }, failure(type, action, stage) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_NONE ); reject(error); }, failureNsresult(type, action, stage, code) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_NSRESULT, code ); reject(error); }, failureHresult(type, action, stage, code) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_HRESULT, code ); reject(error); }, failureString(type, action, stage, message) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_STRING, message ); reject(error); }, }; try { actionFn(callback); } catch (e) { let error = new BitsError( lazy.gBits.ERROR_TYPE_METHOD_THREW, errorAction, lazy.gBits.ERROR_STAGE_PRETASK, lazy.gBits.ERROR_CODE_TYPE_EXCEPTION, e ); reject(error); } }); } /** * This class is a wrapper around nsIBitsRequest that converts functions taking * callbacks to asynchronous functions. This class implements nsIRequest. * * Note that once the request has been shutdown, calling methods on it will * cause an exception to be thrown. The request will be shutdown automatically * when the BITS job is successfully completed or cancelled. If the request is * no longer needed, but the transfer is still in progress, the shutdown method * should be called manually to prevent memory leaks. * Getter methods (except loadGroup and loadFlags) should continue to be * accessible, even after shutdown. */ class BitsRequest { constructor(request) { this._request = request; this._request.QueryInterface(Ci.nsIBitsRequest); } /** * This function releases the Rust request that backs this wrapper. Calling * any method on this request after calling release will result in a BitsError * being thrown. * * This step is important, because otherwise a cycle exists that the cycle * collector doesn't know about. To break this cycle, either the Rust request * needs to let go of the observer, or the JS request wrapper needs to let go * of the Rust request (which is what we do here). * * Once there is support for cycle collection of cycles that extend through * Rust, this function may no longer be necessary. */ shutdown() { if (this.hasShutdown) { return; } // Cache some values before we shut down so they are still available this._name = this._request.name; this._status = this._request.status; this._bitsId = this._request.bitsId; this._transferError = this._request.transferError; this._request = null; } /** * Allows consumers to determine if this request has been shutdown. */ get hasShutdown() { return !this._request; } /** * This is the nsIRequest implementation. Since this._request is an * nsIRequest, these functions just call the corresponding method on it. * * Note that nsIBitsRequest does not yet properly implement load groups or * load flags. This class will still forward those calls, but they will have * not succeed. */ get name() { if (!this._request) { return this._name; } return this._request.name; } isPending() { if (!this._request) { return false; } return this._request.isPending(); } get status() { if (!this._request) { return this._status; } return this._request.status; } cancel(status) { return this.cancelAsync(status); } suspend() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_SUSPEND, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } return this._request.suspend(); } resume() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_RESUME, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } return this._request.resume(); } get loadGroup() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } return this._request.loadGroup; } set loadGroup(group) { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } this._request.loadGroup = group; } get loadFlags() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } return this._request.loadFlags; } set loadFlags(flags) { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } this._request.loadFlags = flags; } /** * This function wraps nsIBitsRequest::bitsId. */ get bitsId() { if (!this._request) { return this._bitsId; } return this._request.bitsId; } /** * This function wraps nsIBitsRequest::transferError. * * Instead of simply returning the nsBitsErrorType value, however, it returns * a BitsError object, or null. */ get transferError() { let result; if (this._request) { result = this._request.transferError; } else { result = this._transferError; } if (result == Ci.nsIBits.ERROR_TYPE_SUCCESS) { return null; } return new BitsError( result, Ci.nsIBits.ERROR_ACTION_NONE, Ci.nsIBits.ERROR_STAGE_MONITOR, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } /** * This function wraps nsIBitsRequest::changeMonitorInterval. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async changeMonitorInterval(monitorIntervalMs) { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL; return requestPromise(action, callback => { this._request.changeMonitorInterval(monitorIntervalMs, callback); }); } /** * This function wraps nsIBitsRequest::cancelAsync. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. * * Adds a default status of NS_ERROR_ABORT if one is not provided. */ async cancelAsync(status) { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_CANCEL, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } if (status === undefined) { status = Cr.NS_ERROR_ABORT; } let action = lazy.gBits.ERROR_ACTION_CANCEL; return requestPromise(action, callback => { this._request.cancelAsync(status, callback); }).then(() => this.shutdown()); } /** * This function wraps nsIBitsRequest::setPriorityHigh. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async setPriorityHigh() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_SET_PRIORITY, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY; return requestPromise(action, callback => { this._request.setPriorityHigh(callback); }); } /** * This function wraps nsIBitsRequest::setPriorityLow. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async setPriorityLow() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_SET_PRIORITY, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY; return requestPromise(action, callback => { this._request.setPriorityLow(callback); }); } /** * This function wraps nsIBitsRequest::setNoProgressTimeout. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async setNoProgressTimeout(timeoutSecs) { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT; return requestPromise(action, callback => { this._request.setNoProgressTimeout(timeoutSecs, callback); }); } /** * This function wraps nsIBitsRequest::complete. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async complete() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_COMPLETE, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_COMPLETE; return requestPromise(action, callback => { this._request.complete(callback); }).then(() => this.shutdown()); } /** * This function wraps nsIBitsRequest::suspendAsync. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async suspendAsync() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_SUSPEND, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_SUSPEND; return requestPromise(action, callback => { this._request.suspendAsync(callback); }); } /** * This function wraps nsIBitsRequest::resumeAsync. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with no data, or rejects with a BitsError. */ async resumeAsync() { if (!this._request) { throw new BitsError( Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN, Ci.nsIBits.ERROR_ACTION_RESUME, Ci.nsIBits.ERROR_STAGE_PRETASK, Ci.nsIBits.ERROR_CODE_TYPE_NONE ); } let action = lazy.gBits.ERROR_ACTION_RESUME; return requestPromise(action, callback => { this._request.resumeAsync(callback); }); } } BitsRequest.prototype.QueryInterface = ChromeUtils.generateQI(["nsIRequest"]); /** * This function does all of the wrapping and error handling for an async * Bits Service method. This allows the implementations for those methods to * simply call this function with a closure that executes appropriate * nsIBits method. * * Specifically, this function takes an nsBitsErrorAction, an observer and a * function. * The nsBitsErrorAction will be used when constructing a BitsError, if the * wrapper encounters an error. * The observer should be the one that the caller passed to the Bits Interface * method. It will be wrapped so that its methods are passed a BitsRequest * rather than an nsIBitsRequest. * The function will be passed the callback function and the wrapped observer, * both of which should be passed to the nsIBitsRequest method. */ async function servicePromise(errorAction, observer, actionFn) { return new Promise((resolve, reject) => { if (!observer) { let error = new BitsError( lazy.gBits.ERROR_TYPE_NULL_ARGUMENT, errorAction, lazy.gBits.ERROR_STAGE_PRETASK, lazy.gBits.ERROR_CODE_TYPE_NONE ); reject(error); return; } try { observer.QueryInterface(Ci.nsIRequestObserver); } catch (e) { let error = new BitsError( lazy.gBits.ERROR_TYPE_INVALID_ARGUMENT, errorAction, lazy.gBits.ERROR_STAGE_PRETASK, lazy.gBits.ERROR_CODE_TYPE_EXCEPTION, e ); reject(error); return; } let isProgressEventSink = false; try { observer.QueryInterface(Ci.nsIProgressEventSink); isProgressEventSink = true; } catch (e) {} // This will be set to the BitsRequest (wrapping the nsIBitsRequest), once // it is available. This prevents a new wrapper from having to be made every // time an observer function is called. let wrappedRequest; let wrappedObserver = { onStartRequest: function wrappedObserver_onStartRequest(request) { if (!wrappedRequest) { wrappedRequest = new BitsRequest(request); } observer.onStartRequest(wrappedRequest); }, onStopRequest: function wrappedObserver_onStopRequest(request, status) { if (!wrappedRequest) { wrappedRequest = new BitsRequest(request); } observer.onStopRequest(wrappedRequest, status); }, onProgress: function wrappedObserver_onProgress( request, progress, progressMax ) { if (isProgressEventSink) { if (!wrappedRequest) { wrappedRequest = new BitsRequest(request); } observer.onProgress(wrappedRequest, progress, progressMax); } }, onStatus: function wrappedObserver_onStatus(request, status, statusArg) { if (isProgressEventSink) { if (!wrappedRequest) { wrappedRequest = new BitsRequest(request); } observer.onStatus(wrappedRequest, status, statusArg); } }, QueryInterface: ChromeUtils.generateQI([ "nsIRequestObserver", "nsIProgressEventSink", ]), }; let timer = makeTimeout(reject, errorAction); let callback = { QueryInterface: ChromeUtils.generateQI(["nsIBitsNewRequestCallback"]), success(request) { timer.cancel(); if (!wrappedRequest) { wrappedRequest = new BitsRequest(request); } resolve(wrappedRequest); }, failure(type, action, stage) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_NONE ); reject(error); }, failureNsresult(type, action, stage, code) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_NSRESULT, code ); reject(error); }, failureHresult(type, action, stage, code) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_HRESULT, code ); reject(error); }, failureString(type, action, stage, message) { timer.cancel(); let error = new BitsError( type, action, stage, lazy.gBits.ERROR_CODE_TYPE_STRING, message ); reject(error); }, }; try { actionFn(wrappedObserver, callback); } catch (e) { let error = new BitsError( lazy.gBits.ERROR_TYPE_METHOD_THREW, errorAction, lazy.gBits.ERROR_STAGE_PRETASK, lazy.gBits.ERROR_CODE_TYPE_EXCEPTION, e ); reject(error); } }); } var Bits = { /** * This function wraps nsIBits::initialized. */ get initialized() { return lazy.gBits.initialized; }, /** * This function wraps nsIBits::init. */ init(jobName, savePathPrefix, monitorTimeoutMs) { return lazy.gBits.init(jobName, savePathPrefix, monitorTimeoutMs); }, /** * This function wraps nsIBits::startDownload. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with a BitsRequest (which is also an * nsIRequest), or rejects with a BitsError. */ async startDownload( downloadURL, saveRelPath, proxy, noProgressTimeoutSecs, monitorIntervalMs, observer, context ) { let action = lazy.gBits.ERROR_ACTION_START_DOWNLOAD; return servicePromise(action, observer, (wrappedObserver, callback) => { lazy.gBits.startDownload( downloadURL, saveRelPath, proxy, noProgressTimeoutSecs, monitorIntervalMs, wrappedObserver, context, callback ); }); }, /** * This function wraps nsIBits::monitorDownload. * * Instead of taking a callback, the function is asynchronous. * This method either resolves with a BitsRequest (which is also an * nsIRequest), or rejects with a BitsError. */ async monitorDownload(id, monitorIntervalMs, observer, context) { let action = lazy.gBits.ERROR_ACTION_MONITOR_DOWNLOAD; return servicePromise(action, observer, (wrappedObserver, callback) => { lazy.gBits.monitorDownload( id, monitorIntervalMs, wrappedObserver, context, callback ); }); }, }; const EXPORTED_SYMBOLS = [ "Bits", "BitsError", "BitsRequest", "BitsSuccess", "BitsUnknownError", "BitsVerificationError", ];