/* 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), RequestEndedWhileInProgress(Option), RequestEndPending, } #[xpcom(implement(nsIBitsRequest), nonatomic)] pub struct BitsRequest { bits_id: Guid, bits_service: RefPtr, // Stores the value to be returned by nsIRequest::IsPending. download_pending: Cell, // Stores the value to be returned by nsIRequest::GetStatus. download_status_nsresult: Cell, // Stores an ErrorType if the request has failed, or None to represent the // success state. download_status_error_type: Cell>, // This option will be None only after OnStopRequest has been fired. monitor_thread: Cell>>, monitor_timeout_ms: u32, observer: RefPtr, // started indicates whether or not OnStartRequest has been fired. started: Cell, // 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, cancel_action: Cell, } 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, monitor_timeout_ms: u32, observer: RefPtr, context: Option>, monitor_client: BitsMonitorClient, action: ServiceAction, ) -> Result, 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::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> { 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::() { 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)>) { 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 { 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 { 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 { Ok(self.download_pending.get()) } xpcom_method!( get_status_nsIRequest => GetStatus() -> nsresult ); #[allow(non_snake_case)] fn get_status_nsIRequest(&self) -> Result { 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 = 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>, ) -> 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 = 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 = 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 = 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 = 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 = 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>) -> Result<(), BitsTaskError> { if self.request_has_transferred() { return Err(BitsTaskError::new( TransferAlreadyComplete, Action::Suspend, Pretask, )); } let task: Box = 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>) -> Result<(), BitsTaskError> { if self.request_has_transferred() { return Err(BitsTaskError::new( TransferAlreadyComplete, Action::Resume, Pretask, )); } let task: Box = 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, 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 { 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 { 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 { 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(); } }