summaryrefslogtreecommitdiffstats
path: root/toolkit/components/bitsdownload/src/bits_interface/mod.rs
blob: 93d2ddeadd8bebcecf0a088df11d0bd91b55680b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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();
    }
}