1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 mod action;
5 mod dispatch_callback;
6 mod error;
7 mod monitor;
8 mod request;
9 mod string;
10 mod task;
11 mod xpcom_methods;
12 
13 pub use self::error::BitsTaskError;
14 use self::{
15     action::Action,
16     error::{
17         ErrorStage::Pretask,
18         ErrorType::{
19             FailedToConstructTaskRunnable, FailedToDispatchRunnable, FailedToStartThread,
20             InvalidArgument, NotInitialized,
21         },
22     },
23     request::BitsRequest,
24     string::Guid_from_nsCString,
25     task::{ClientInitData, MonitorDownloadTask, StartDownloadTask},
26 };
27 use nsIBits_method; // From xpcom_method.rs
28 
29 use bits_client::BitsProxyUsage;
30 use log::{info, warn};
31 use moz_task::{create_thread, Task, TaskRunnable};
32 use nserror::{nsresult, NS_ERROR_ALREADY_INITIALIZED, NS_OK};
33 use nsstring::{nsACString, nsCString};
34 use std::cell::Cell;
35 use xpcom::{
36     interfaces::{
37         nsIBits, nsIBitsNewRequestCallback, nsIRequestObserver, nsISupports, nsIThread,
38         nsProxyUsage,
39     },
40     xpcom, xpcom_method, RefPtr,
41 };
42 
43 #[no_mangle]
new_bits_service(result: *mut *const nsIBits)44 pub unsafe extern "C" fn new_bits_service(result: *mut *const nsIBits) {
45     let service: RefPtr<BitsService> = BitsService::new();
46     RefPtr::new(service.coerce::<nsIBits>()).forget(&mut *result);
47 }
48 
49 #[derive(xpcom)]
50 #[xpimplements(nsIBits)]
51 #[refcnt = "nonatomic"]
52 pub struct InitBitsService {
53     // This command thread will be used to send commands (ex: Suspend, Resume)
54     // to a running job. It will be started up when the first job is created and
55     // shutdown when all jobs have been completed or cancelled.
56     command_thread: Cell<Option<RefPtr<nsIThread>>>,
57     client_init_data: Cell<Option<ClientInitData>>,
58     // This count will track the number of in-progress requests so that the
59     // service knows when the command_thread is no longer being used and can be
60     // shut down.
61     // `BitsRequest::new()` will increment this and it will be decremented
62     // either when cancel/complete is called, or when the request is dropped
63     // (if it didn't decrement it already).
64     // The count will also be incremented when an action to create a request
65     // starts and decremented when the action ends and returns the result via
66     // the callback. This prevents the command thread from being shut down while
67     // a job is being created.
68     request_count: Cell<u32>,
69 }
70 
71 /// This implements the nsIBits interface, documented in nsIBits.idl, to enable
72 /// BITS job management. Specifically, this interface can start a download or
73 /// connect to an existing download. Doing so will create a BitsRequest through
74 /// which the transfer can be further manipulated.
75 ///
76 /// This is a primarily asynchronous interface, which is accomplished via
77 /// callbacks of type nsIBitsNewRequestCallback. The callback is passed in as
78 /// an argument and is then passed off-thread via a Task. The Task interacts
79 /// with BITS and is dispatched back to the main thread with the BITS result.
80 /// Back on the main thread, it returns that result via the callback including,
81 /// if successful, a BitsRequest.
82 impl BitsService {
new() -> RefPtr<BitsService>83     pub fn new() -> RefPtr<BitsService> {
84         BitsService::allocate(InitBitsService {
85             command_thread: Cell::new(None),
86             client_init_data: Cell::new(None),
87             request_count: Cell::new(0),
88         })
89     }
90 
get_client_init(&self) -> Option<ClientInitData>91     fn get_client_init(&self) -> Option<ClientInitData> {
92         let maybe_init_data = self.client_init_data.take();
93         self.client_init_data.set(maybe_init_data.clone());
94         maybe_init_data
95     }
96 
97     // Returns the handle to the command thread. If it has not been started yet,
98     // the thread will be started.
get_command_thread(&self) -> Result<RefPtr<nsIThread>, nsresult>99     fn get_command_thread(&self) -> Result<RefPtr<nsIThread>, nsresult> {
100         let mut command_thread = self.command_thread.take();
101         if command_thread.is_none() {
102             command_thread.replace(create_thread("BitsCommander")?);
103         }
104         self.command_thread.set(command_thread.clone());
105         Ok(command_thread.unwrap())
106     }
107 
108     // Asynchronously shuts down the command thread. The thread is not shutdown
109     // until the event queue is empty, so any tasks that were dispatched before
110     // this is called will still run.
111     // Leaves None in self.command_thread
shutdown_command_thread(&self)112     fn shutdown_command_thread(&self) {
113         if let Some(command_thread) = self.command_thread.take() {
114             if let Err(rv) = unsafe { command_thread.AsyncShutdown() }.to_result() {
115                 warn!("Failed to shut down command thread: {}", rv);
116                 warn!("Releasing reference to thread that failed to shut down!");
117             }
118         }
119     }
120 
dispatch_runnable_to_command_thread( &self, task: Box<dyn Task + Send + Sync>, task_runnable_name: &'static str, action: Action, ) -> Result<(), BitsTaskError>121     fn dispatch_runnable_to_command_thread(
122         &self,
123         task: Box<dyn Task + Send + Sync>,
124         task_runnable_name: &'static str,
125         action: Action,
126     ) -> Result<(), BitsTaskError> {
127         let command_thread = self
128             .get_command_thread()
129             .map_err(|rv| BitsTaskError::from_nsresult(FailedToStartThread, action, Pretask, rv))?;
130         let runnable = TaskRunnable::new(task_runnable_name, task).map_err(|rv| {
131             BitsTaskError::from_nsresult(FailedToConstructTaskRunnable, action, Pretask, rv)
132         })?;
133         TaskRunnable::dispatch(runnable, &command_thread).map_err(|rv| {
134             BitsTaskError::from_nsresult(FailedToDispatchRunnable, action, Pretask, rv)
135         })
136     }
137 
inc_request_count(&self)138     fn inc_request_count(&self) {
139         self.request_count.set(self.request_count.get() + 1);
140     }
141 
dec_request_count(&self)142     fn dec_request_count(&self) {
143         let mut count = self.request_count.get();
144         if count == 0 {
145             warn!("Attempted to decrement request count, but it is 0");
146             return;
147         }
148         count -= 1;
149         self.request_count.set(count);
150 
151         if count == 0 {
152             self.shutdown_command_thread();
153         }
154     }
155 
156     xpcom_method!(
157         get_initialized => GetInitialized() -> bool
158     );
get_initialized(&self) -> Result<bool, nsresult>159     fn get_initialized(&self) -> Result<bool, nsresult> {
160         Ok(self.get_client_init().is_some())
161     }
162 
163     xpcom_method!(
164         init => Init(
165             job_name: *const nsACString,
166             save_path_prefix: *const nsACString,
167             monitor_timeout_ms: u32
168         )
169     );
init( &self, job_name: &nsACString, save_path_prefix: &nsACString, monitor_timeout_ms: u32, ) -> Result<(), nsresult>170     fn init(
171         &self,
172         job_name: &nsACString,
173         save_path_prefix: &nsACString,
174         monitor_timeout_ms: u32,
175     ) -> Result<(), nsresult> {
176         let previous_data = self.client_init_data.take();
177         if previous_data.is_some() {
178             self.client_init_data.set(previous_data);
179             return Err(NS_ERROR_ALREADY_INITIALIZED);
180         }
181 
182         info!(
183             "BitsService initialized with job_name: {}, save_path_prefix: {}, timeout: {}",
184             job_name, save_path_prefix, monitor_timeout_ms,
185         );
186 
187         self.client_init_data.set(Some(ClientInitData::new(
188             nsCString::from(job_name),
189             nsCString::from(save_path_prefix),
190             monitor_timeout_ms,
191         )));
192 
193         Ok(())
194     }
195 
196     nsIBits_method!(
197         [Action::StartDownload]
198         start_download => StartDownload(
199             download_url: *const nsACString,
200             save_rel_path: *const nsACString,
201             proxy: nsProxyUsage,
202             no_progress_timeout_secs: u32,
203             update_interval_ms: u32,
204             observer: *const nsIRequestObserver,
205             [optional] context: *const nsISupports,
206         )
207     );
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>208     fn start_download(
209         &self,
210         download_url: &nsACString,
211         save_rel_path: &nsACString,
212         proxy: nsProxyUsage,
213         no_progress_timeout_secs: u32,
214         update_interval_ms: u32,
215         observer: &nsIRequestObserver,
216         context: Option<&nsISupports>,
217         callback: &nsIBitsNewRequestCallback,
218     ) -> Result<(), BitsTaskError> {
219         let client_init_data = self
220             .get_client_init()
221             .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::StartDownload, Pretask))?;
222         if update_interval_ms >= client_init_data.monitor_timeout_ms {
223             return Err(BitsTaskError::new(
224                 InvalidArgument,
225                 Action::StartDownload,
226                 Pretask,
227             ));
228         }
229         let proxy = match proxy as i64 {
230             nsIBits::PROXY_NONE => BitsProxyUsage::NoProxy,
231             nsIBits::PROXY_PRECONFIG => BitsProxyUsage::Preconfig,
232             nsIBits::PROXY_AUTODETECT => BitsProxyUsage::AutoDetect,
233             _ => {
234                 return Err(BitsTaskError::new(
235                     InvalidArgument,
236                     Action::StartDownload,
237                     Pretask,
238                 ));
239             }
240         };
241 
242         let task: Box<StartDownloadTask> = Box::new(StartDownloadTask::new(
243             client_init_data,
244             nsCString::from(download_url),
245             nsCString::from(save_rel_path),
246             proxy,
247             no_progress_timeout_secs,
248             update_interval_ms,
249             RefPtr::new(self),
250             RefPtr::new(observer),
251             context.map(RefPtr::new),
252             RefPtr::new(callback),
253         ));
254 
255         let dispatch_result = self.dispatch_runnable_to_command_thread(
256             task,
257             "BitsService::start_download",
258             Action::StartDownload,
259         );
260 
261         if dispatch_result.is_ok() {
262             // Increment the request count when we dispatch an action to start
263             // a job, decrement it when the action completes. See the
264             // declaration of InitBitsService::request_count for details.
265             self.inc_request_count();
266         }
267 
268         dispatch_result
269     }
270 
271     nsIBits_method!(
272         [Action::MonitorDownload]
273         monitor_download => MonitorDownload(
274             id: *const nsACString,
275             update_interval_ms: u32,
276             observer: *const nsIRequestObserver,
277             [optional] context: *const nsISupports,
278         )
279     );
monitor_download( &self, id: &nsACString, update_interval_ms: u32, observer: &nsIRequestObserver, context: Option<&nsISupports>, callback: &nsIBitsNewRequestCallback, ) -> Result<(), BitsTaskError>280     fn monitor_download(
281         &self,
282         id: &nsACString,
283         update_interval_ms: u32,
284         observer: &nsIRequestObserver,
285         context: Option<&nsISupports>,
286         callback: &nsIBitsNewRequestCallback,
287     ) -> Result<(), BitsTaskError> {
288         let client_init_data = self
289             .get_client_init()
290             .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::MonitorDownload, Pretask))?;
291         if update_interval_ms >= client_init_data.monitor_timeout_ms {
292             return Err(BitsTaskError::new(
293                 InvalidArgument,
294                 Action::MonitorDownload,
295                 Pretask,
296             ));
297         }
298         let guid = Guid_from_nsCString(&nsCString::from(id), Action::MonitorDownload, Pretask)?;
299 
300         let task: Box<MonitorDownloadTask> = Box::new(MonitorDownloadTask::new(
301             client_init_data,
302             guid,
303             update_interval_ms,
304             RefPtr::new(self),
305             RefPtr::new(observer),
306             context.map(RefPtr::new),
307             RefPtr::new(callback),
308         ));
309 
310         let dispatch_result = self.dispatch_runnable_to_command_thread(
311             task,
312             "BitsService::monitor_download",
313             Action::MonitorDownload,
314         );
315 
316         if dispatch_result.is_ok() {
317             // Increment the request count when we dispatch an action to start
318             // a job, decrement it when the action completes. See the
319             // declaration of InitBitsService::request_count for details.
320             self.inc_request_count();
321         }
322 
323         dispatch_result
324     }
325 }
326 
327 impl Drop for BitsService {
drop(&mut self)328     fn drop(&mut self) {
329         self.shutdown_command_thread();
330     }
331 }
332