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 {
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