diff options
Diffstat (limited to 'toolkit/components/bitsdownload/src')
14 files changed, 3642 insertions, 0 deletions
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; |