// Licensed under the Apache License, Version 2.0 // or the MIT license // , at your option. // All files in the project carrying such notice may not be copied, modified, or distributed // except according to those terms. //! A safe interface for BITS //! //! The primary entry point into BITS is the //! [`BackgroundCopyManager`](struct.BackgroundCopyManager.html) struct. //! //! Functionality is only provided by this crate on an as-needed basis for //! [bits_client](../bits_client/index.html), so there are vast swathes of the BITS API //! unsupported. extern crate comedy; extern crate filetime_win; extern crate guid_win; extern crate winapi; #[cfg(feature = "status_serde")] extern crate serde; #[cfg(feature = "status_serde")] extern crate serde_derive; mod callback; pub mod status; mod wide; use std::ffi::{OsStr, OsString}; use std::mem; use std::os::windows::ffi::OsStringExt; use std::ptr; use std::result; use comedy::com::{create_instance_local_server, CoTaskMem, ComRef, INIT_MTA}; use comedy::error::{HResult, ResultExt}; use comedy::{com_call, com_call_getter, com_call_taskmem_getter}; use filetime_win::FileTime; use guid_win::Guid; use winapi::shared::minwindef::DWORD; use winapi::shared::ntdef::{HRESULT, LANGIDFROMLCID, ULONG}; use winapi::shared::winerror::S_FALSE; use winapi::um::bits::{ IBackgroundCopyError, IBackgroundCopyFile, IBackgroundCopyJob, IBackgroundCopyManager, IEnumBackgroundCopyFiles, IEnumBackgroundCopyJobs, BG_JOB_PRIORITY, BG_JOB_PRIORITY_FOREGROUND, BG_JOB_PRIORITY_HIGH, BG_JOB_PRIORITY_LOW, BG_JOB_PRIORITY_NORMAL, BG_JOB_PROXY_USAGE, BG_JOB_PROXY_USAGE_AUTODETECT, BG_JOB_PROXY_USAGE_NO_PROXY, BG_JOB_PROXY_USAGE_PRECONFIG, BG_JOB_STATE_ERROR, BG_JOB_STATE_TRANSIENT_ERROR, BG_JOB_TYPE_DOWNLOAD, BG_NOTIFY_DISABLE, BG_NOTIFY_JOB_ERROR, BG_NOTIFY_JOB_MODIFICATION, BG_NOTIFY_JOB_TRANSFERRED, BG_SIZE_UNKNOWN, }; use winapi::um::bits2_5::{IBackgroundCopyJobHttpOptions, BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT}; use winapi::um::bitsmsg::BG_E_NOT_FOUND; use winapi::um::unknwnbase::IUnknown; use winapi::um::winnls::GetThreadLocale; pub use winapi::um::bits::{BG_ERROR_CONTEXT, BG_JOB_STATE}; pub use winapi::um::bitsmsg::{BG_S_PARTIAL_COMPLETE, BG_S_UNABLE_TO_DELETE_FILES}; pub use status::{ BitsErrorContext, BitsJobError, BitsJobProgress, BitsJobState, BitsJobStatus, BitsJobTimes, }; use wide::ToWideNull; pub use winapi::shared::winerror::E_FAIL; #[repr(u32)] #[derive(Copy, Clone, Debug)] pub enum BitsJobPriority { Foreground = BG_JOB_PRIORITY_FOREGROUND, High = BG_JOB_PRIORITY_HIGH, /// Default Normal = BG_JOB_PRIORITY_NORMAL, Low = BG_JOB_PRIORITY_LOW, } #[repr(u32)] #[derive(Copy, Clone, Debug)] pub enum BitsProxyUsage { /// Directly access the network. NoProxy = BG_JOB_PROXY_USAGE_NO_PROXY, /// Use Internet Explorer proxy settings. This is the default. Preconfig = BG_JOB_PROXY_USAGE_PRECONFIG, /// Attempt to auto-detect the connection's proxy settings. AutoDetect = BG_JOB_PROXY_USAGE_AUTODETECT, } type Result = result::Result; pub struct BackgroundCopyManager(ComRef); impl BackgroundCopyManager { /// Get access to the local BITS service. /// /// # COM Initialization and Threading Model # /// /// This method uses a thread local variable to initialize COM with a multithreaded apartment /// model for this thread, and leaves it this way until the thread local is dropped. /// If the thread was in a single-threaded apartment, `connect()` will fail gracefully. /// /// # Safety # /// /// If there are mismatched `CoUninitialize` calls on this thread which lead to COM shutting /// down before this thread ends, unsafe behavior may result. pub fn connect() -> Result { INIT_MTA.with(|com| { if let Err(e) = com { return Err(e.clone()); } Ok(()) })?; // Assuming no mismatched CoUninitialize calls, methods do not have to check for // successfully initialized COM once the object is constructed: `BackgroundCopyManager` // is not `Send` or `Sync` so it must be used on the thread it was constructed on, // which has now successfully inited MTA for the lifetime of thread local `INIT_MTA`. // This also holds for any functions using pointers only derived from these methods, like // the `BitsJob` methods. Ok(BackgroundCopyManager(create_instance_local_server::< winapi::um::bits::BackgroundCopyManager, IBackgroundCopyManager, >()?)) } /// Create a new download job with the given name. pub fn create_job(&self, display_name: &OsStr) -> Result { unsafe { let mut guid = mem::zeroed(); Ok(BitsJob(com_call_getter!( |job| self.0, IBackgroundCopyManager::CreateJob( display_name.to_wide_null().as_ptr(), BG_JOB_TYPE_DOWNLOAD, &mut guid, job, ) )?)) } } /// Cancel all jobs with the given name. /// /// This only attempts to cancel jobs owned by the current user. /// No errors are returned for jobs that failed to cancel. pub fn cancel_jobs_by_name(&self, match_name: &OsStr) -> Result<()> { let jobs = unsafe { com_call_getter!(|jobs| self.0, IBackgroundCopyManager::EnumJobs(0, jobs))? }; loop { let result = unsafe { com_call_getter!( |job| jobs, IEnumBackgroundCopyJobs::Next(1, job, ptr::null_mut()) ) }; match result { Ok(job) => { if job_name_eq(&job, match_name)? { unsafe { let _ = com_call!(job, IBackgroundCopyJob::Cancel()); } } } Err(e) => { if e.code() == S_FALSE { // Ran out of jobs to enumerate return Ok(()); } else { return Err(e); } } } } } /// Get the job with the given GUID. /// /// Returns Err if the job was not found. pub fn get_job_by_guid(&self, guid: &Guid) -> Result { unsafe { com_call_getter!(|job| self.0, IBackgroundCopyManager::GetJob(&guid.0, job)) } .map(BitsJob) } /// Try to find a job with a given GUID. /// /// Returns Ok(None) if the job was not found but there was no other error. pub fn find_job_by_guid(&self, guid: &Guid) -> Result> { Ok(self .get_job_by_guid(guid) .map(Some) .allow_err(BG_E_NOT_FOUND as i32, None)?) } /// Try to find a job with a given GUID and name. /// /// Returns Ok(None) if the job was not found, or if it had the wrong name, as long as there /// was no other error. pub fn find_job_by_guid_and_name( &self, guid: &Guid, match_name: &OsStr, ) -> Result> { Ok(match self.find_job_by_guid(guid)? { None => None, Some(BitsJob(ref job)) if !job_name_eq(job, match_name)? => None, result => result, }) } /// Translate a BITS `HRESULT` to a textual description. /// /// This uses the current thread's locale to look up the message associated with a BITS /// error. It should only be used for `HRESULT`s returned from BITS COM interfaces. pub fn get_error_description(&self, hr: HRESULT) -> Result { unsafe { let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale())); Ok(taskmem_into_lossy_string(com_call_taskmem_getter!( |desc| self.0, IBackgroundCopyManager::GetErrorDescription(hr, language_id, desc) )?)) } } } unsafe fn taskmem_into_lossy_string(taskmem: CoTaskMem) -> String { OsString::from_wide(taskmem.as_slice_until_null()) .to_string_lossy() .into_owned() } fn job_name_eq(job: &ComRef, match_name: &OsStr) -> Result { let job_name = unsafe { OsString::from_wide( com_call_taskmem_getter!(|name| job, IBackgroundCopyJob::GetDisplayName(name))? .as_slice_until_null(), ) }; Ok(job_name == match_name) } pub struct BitsJob(ComRef); impl BitsJob { /// Get the job's GUID. pub fn guid(&self) -> Result { // TODO: cache on create or retrieved by GUID? unsafe { let mut guid = mem::zeroed(); com_call!(self.0, IBackgroundCopyJob::GetId(&mut guid))?; Ok(Guid(guid)) } } /// Add a file to the job. pub fn add_file(&mut self, remote_url: &OsStr, local_file: &OsStr) -> Result<()> { unsafe { com_call!( self.0, IBackgroundCopyJob::AddFile( remote_url.to_wide_null().as_ptr(), local_file.to_wide_null().as_ptr(), ) ) }?; Ok(()) } /// Get the first file in the job. /// /// This is provided for collecting the redirected remote name of single file jobs. pub fn get_first_file(&mut self) -> Result { let files = unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::EnumFiles(e))? }; let file = unsafe { com_call_getter!( |file| files, IEnumBackgroundCopyFiles::Next(1, file, ptr::null_mut()) )? }; Ok(BitsFile(file)) } /// Set the job's description string. /// /// This is different from the display name set when creating the job. pub fn set_description(&mut self, description: &OsStr) -> Result<()> { unsafe { com_call!( self.0, IBackgroundCopyJob::SetDescription(description.to_wide_null().as_ptr()) ) }?; Ok(()) } /// Change the job's proxy usage setting. /// /// The default is `BitsProxyUsage::Preconfig`. pub fn set_proxy_usage(&mut self, usage: BitsProxyUsage) -> Result<()> { use BitsProxyUsage::*; match usage { Preconfig | NoProxy | AutoDetect => { unsafe { com_call!( self.0, IBackgroundCopyJob::SetProxySettings( usage as BG_JOB_PROXY_USAGE, ptr::null_mut(), ptr::null_mut(), ) ) }?; Ok(()) } } } /// Change the job's priority. /// /// The default is `BitsJobPriority::Normal`. pub fn set_priority(&mut self, priority: BitsJobPriority) -> Result<()> { unsafe { com_call!( self.0, IBackgroundCopyJob::SetPriority(priority as BG_JOB_PRIORITY) ) }?; Ok(()) } pub fn set_minimum_retry_delay(&mut self, seconds: ULONG) -> Result<()> { unsafe { com_call!(self.0, IBackgroundCopyJob::SetMinimumRetryDelay(seconds)) }?; Ok(()) } pub fn set_no_progress_timeout(&mut self, seconds: ULONG) -> Result<()> { unsafe { com_call!(self.0, IBackgroundCopyJob::SetNoProgressTimeout(seconds)) }?; Ok(()) } /// Enable HTTP redirect reporting. /// /// The default setting is to allow HTTP redirects, but to not report them in any way. With /// this setting enabled, the remote name of a file will be updated to reflect the redirect. /// /// # Compatibility # /// /// First available in Windows Vista. pub fn set_redirect_report(&mut self) -> Result<()> { unsafe { com_call!( self.0.cast()?, IBackgroundCopyJobHttpOptions::SetSecurityFlags( BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT ) ) }?; Ok(()) } /// Resume the job. This must be done at least once to initially enqueue the job. pub fn resume(&mut self) -> Result<()> { unsafe { com_call!(self.0, IBackgroundCopyJob::Resume()) }?; Ok(()) } pub fn suspend(&mut self) -> Result<()> { unsafe { com_call!(self.0, IBackgroundCopyJob::Suspend()) }?; Ok(()) } /// Complete the job, moving the local files to their final names. /// /// Has two interesting success `HRESULT`s: `BG_S_PARTIAL_COMPLETE` and /// `BG_S_UNABLE_TO_DELETE_FILES`. pub fn complete(&mut self) -> Result { unsafe { com_call!(self.0, IBackgroundCopyJob::Complete()) } } /// Cancel the job, deleting any temporary files. /// /// Has an interesting success `HRESULT`: `BG_S_UNABLE_TO_DELETE_FILES`. pub fn cancel(&mut self) -> Result { unsafe { com_call!(self.0, IBackgroundCopyJob::Cancel()) } } /// Set the notification callbacks to use with this job. /// /// This will replace any previously set callbacks. pub fn register_callbacks( &mut self, transferred_cb: Option>, error_cb: Option>, modification_cb: Option>, ) -> Result<()> { let mut flags = 0; if transferred_cb.is_some() { flags |= BG_NOTIFY_JOB_TRANSFERRED; } if error_cb.is_some() { flags |= BG_NOTIFY_JOB_ERROR; } if modification_cb.is_some() { flags |= BG_NOTIFY_JOB_MODIFICATION; } callback::BackgroundCopyCallback::register( self, transferred_cb, error_cb, modification_cb, )?; unsafe { com_call!(self.0, IBackgroundCopyJob::SetNotifyFlags(flags)) }?; Ok(()) } fn _clear_callbacks(&mut self) -> Result<()> { unsafe { com_call!( self.0, IBackgroundCopyJob::SetNotifyFlags(BG_NOTIFY_DISABLE) )?; self.set_notify_interface(ptr::null_mut() as *mut IUnknown) } } /// Collect the current status of the job, including errors. pub fn get_status(&self) -> Result { let mut state = 0; let mut progress = unsafe { mem::zeroed() }; let mut error_count = 0; let mut times = unsafe { mem::zeroed() }; unsafe { com_call!(self.0, IBackgroundCopyJob::GetState(&mut state))?; com_call!(self.0, IBackgroundCopyJob::GetProgress(&mut progress))?; com_call!(self.0, IBackgroundCopyJob::GetErrorCount(&mut error_count))?; com_call!(self.0, IBackgroundCopyJob::GetTimes(&mut times))?; } Ok(BitsJobStatus { state: BitsJobState::from(state), progress: BitsJobProgress { total_bytes: if progress.BytesTotal == BG_SIZE_UNKNOWN { None } else { Some(progress.BytesTotal) }, transferred_bytes: progress.BytesTransferred, total_files: progress.FilesTotal, transferred_files: progress.FilesTransferred, }, error_count, error: if state == BG_JOB_STATE_ERROR || state == BG_JOB_STATE_TRANSIENT_ERROR { let error_obj = unsafe { com_call_getter!(|e| self.0, IBackgroundCopyJob::GetError(e)) }?; Some(BitsJob::get_error(error_obj)?) } else { None }, times: BitsJobTimes { creation: FileTime(times.CreationTime), modification: FileTime(times.ModificationTime), transfer_completion: if times.TransferCompletionTime.dwLowDateTime == 0 && times.TransferCompletionTime.dwHighDateTime == 0 { None } else { Some(FileTime(times.TransferCompletionTime)) }, }, }) } fn get_error(error_obj: ComRef) -> Result { let mut context = 0; let mut hresult = 0; unsafe { com_call!( error_obj, IBackgroundCopyError::GetError(&mut context, &mut hresult) )?; let language_id = DWORD::from(LANGIDFROMLCID(GetThreadLocale())); let context = BitsErrorContext::from(context); let context_str = com_call_taskmem_getter!( |desc| error_obj, IBackgroundCopyError::GetErrorContextDescription(language_id, desc) ) .map(|s| taskmem_into_lossy_string(s)) .unwrap_or_else(|_| format!("{:?}", context)); let error_str = com_call_taskmem_getter!( |desc| error_obj, IBackgroundCopyError::GetErrorDescription(language_id, desc) ) .map(|s| taskmem_into_lossy_string(s)) .unwrap_or_else(|_| format!("{:#08x}", hresult)); Ok(BitsJobError { context, context_str, error: hresult, error_str, }) } } unsafe fn set_notify_interface(&self, interface: *mut IUnknown) -> Result<()> { com_call!(self.0, IBackgroundCopyJob::SetNotifyInterface(interface))?; Ok(()) } } pub struct BitsFile(ComRef); /// A single file in a BITS job. /// /// This is provided for collecting the redirected remote name. impl BitsFile { /// Get the remote name from which the file is being downloaded. /// /// If [`BitsJob::set_redirect_report()`](struct.BitsJob.html#method.set_redirect_report) /// hasn't been called on the job, this won't be /// updated as HTTP redirects are processed. pub fn get_remote_name(&self) -> Result { unsafe { Ok(OsString::from_wide( com_call_taskmem_getter!(|name| self.0, IBackgroundCopyFile::GetRemoteName(name))? .as_slice_until_null(), )) } } } #[cfg(test)] mod test { use super::BackgroundCopyManager; use std::ffi::OsString; use std::mem; #[test] #[ignore] fn test_find_job() { let bcm = BackgroundCopyManager::connect().unwrap(); let name = OsString::from("bits test job"); let wrong_name = OsString::from("bits test jobbo"); let mut job = bcm.create_job(&name).unwrap(); let guid = job.guid().unwrap(); assert_eq!( bcm.find_job_by_guid(&guid) .unwrap() .unwrap() .guid() .unwrap(), guid ); assert_eq!( bcm.find_job_by_guid_and_name(&guid, &name) .unwrap() .unwrap() .guid() .unwrap(), guid ); assert!(bcm .find_job_by_guid_and_name(&guid, &wrong_name) .unwrap() .is_none()); job.cancel().unwrap(); mem::drop(job); assert!(bcm.find_job_by_guid(&guid).unwrap().is_none()); assert!(bcm .find_job_by_guid_and_name(&guid, &name) .unwrap() .is_none()); } }