Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WTL

Using the Background Intelligent Transfer Service (BITS)

4.71/5 (18 votes)
3 Jun 2008CPOL11 min read 1   3K  
Using the BITS system service
Sample image

Introduction

BITS is a system service available in Windows 2000, XP, Server 2003, and Vista. It's used by the system to upload and download Internet files; Windows Update uses this service. The service is a basic COM component, and exposes its objects by some interfaces. This article focuses on how to wrap BITS in an application to add, remove, and monitor jobs, add files to jobs, and some other functionality to automate tasks.

Background

Most probably, BITS was developed to manage background system updates from section to section to maintain up/download data integrity. Actually, only the system uses this service, but Microsoft has documented this "piece of mind" in PSDK, and freely distributes an application to manage it: bitsadmin. It's included in the Windows XP Service Pack 2 Support Tools and supports all BITS commands. This application runs in console mode, so I implemented it as a UI application using the COM component. The demo application is not a download manager, it could be the first step to create one.

Using the Code

The C++ project is created with Visual C++ 2005 Express, PSDK Server 2003 SP1 (2005.03), ATL 3.0 (in PSDK sources), and WTL 7.5 (SourceForge). The WTLWizard is used to create it, but the code was modified to remove the CFrameWnd implementation and some other objects; it's not optimized at all, the unique scope is to implement the BITS component and know how it works. It is compiled in Unicode; ANSI is not supported. BITS is a Unicode component like GDI+, all strings must be in Unicode format. This service is not available in Window 95/98/ME and NT. To insert BITS into a generic project, make sure to compile it for the right platform; see Using the Windows Header to rightly define preprocessor constants and compile it in Unicode format (it's not necessary, but prevents headaches). Finally, include the header bits.h in the project main header file (StdAfx.h for a standard Visual Studio project).

BITS Overview

The service can upload and download files in the background and foreground, resume jobs when connected, and suspend them when closing the system section. Files are transferred only using the HTTP and HTTPS protocols with or without authentication. The component abstraction uses jobs as the basic unit; a job defines the transfer scheduling model and can contain one or more files. It is identified by an ID. Two or more jobs can have the same name, but not the same ID; names are case sensitive. Each job has its own priority; there are four priority types: low, normal, high, and foreground. For the first three types, the transfer is in the background. Other applications that share the connection can stop the job transfer and take the full bandwidth; jobs in the foreground mode use the full bandwidth. All jobs share the transfer time, so big jobs don't blocks small jobs.

Like many basic COM components, BITS exposes a main object, all the others are accessible through the main object.

IBackgroundCopyManagerThe main service COM object, creates and enumerates jobs.
IEnumBackgroundCopyJobsJobs enumerator object. The job count and job properties reflect the status at the time the enumerator is queried. It's used to get jobs and show properties to the user. The jobs' status is stroked at the moment the enumerator request was made.
IBackgroundCopyJob<br />IBackgroundCopyJob2<br />IBackgroundCopyJob3Job object. There are three interfaces for jobs, depending on the BITS version. In the classic COM style, however, the basic interface IBacgroundCopyJob implements all the methods to add, remove, monitor jobs, enumerate, and add files. The others are specialized for special notifications, and upload reply and credentials. These features are not described here. All operations in this document make use of the basic interface. The job objects are accessible through the IEnumBackgroundCopyJobs collection or IBackgroundCopyManager, by ID.
IEnumBackgroundCopyFilesJob file enumerator object, is a file collection of a job. This object has the same function as the job enumerator, and each job has one.
IBackgroundCopyFile<br />IBackgroundCopyFile2File object, it has a few methods to get and set properties. The most relevant functions are implemented by the basic interface; in this document, only this one is used.
IBackgroundCopyErrorError information object to get descriptive error strings.
IBackgroundCopyCallbackThis interface defines the events fired by jobs; it must be implemented and registered to the job. It notifies errors and job statuses; however, the same information can be retrieved by a polling loop. The demo application uses the main message loop to get job status and properties.

Creating the Main Component Object

To simplify COM programming, the BITS interfaces are transformed in CComPtr<> specialized objects:

C++
typedef CComPtr<IBackgroundCopyManager>    CComBitsManager;
typedef CComPtr<IEnumBackgroundCopyJobs>   CComBitsEnumJobs;
typedef CComPtr<IBackgroundCopyJob>        CComBitsJob;
typedef CComPtr<IEnumBackgroundCopyFiles>  CComBitsEnumFiles;
typedef CComPtr<IBackgroundCopyFile>       CComBitsFile;
typedef CComPtr<IBackgroundCopyError>      CComBitsError;

All methods of each object are very immediate and simple. BITS is not a complicated component. For methods that return strings, the component allocates memory, you only need to free that memory using the API ::CoTaskMemFree().

C++
...
CString strText;
LPWSTR pwstrText = NULL;
hResult = comobj->SomeMethod (&pwstrText);
if (SUCCEEDED (hResult))
{
   strText = pwstrText;
   ::CoTaskMemFree (pwstrText);
}
...

In the example above, the comobj is a BITS object, and SomeMethod is a method with a string parameter. Before calling the method, declare a Unicode string pointer, and initialize it with NULL. Call the method passing the address of the pointer; if the function succeeds, save the string into another location, for simplicity in a CString object, then free the string returned by the method. If your project is compiled as Unicode, you can use the above lines to do that; if not, a Unicode to ANSI conversion is needed.

Now, we are ready to use the component. The main object can be created when initializing the application and it is released at exit. A CComPtr is not necessary to explicitly release it, the destructor makes this work. The object must be defined globally, or as a member of some global object; in the second option, it is created in the container object constructor and released in the destructor.

C++
// Create BITS manager object
...
CComBitsManager comBits;
hResult = comBits.CoCreateInstance (__uuidof (BackgroundCopyManager),
                                    NULL, CLSCTX_LOCAL_SERVER);
...

// Release BITS manager object
...
comBits.Release ();
...

All other objects must be queried at the moment you want to use them. Don't query for job enumerator objects and save them for later use, job properties and status inside a job collection are not updated. Always query for a job enumerator at the moment you need to get job properties and status, and release it when you are done, as explained in the next paragraph.

Monitoring Jobs

When we need to wrap a service, it is important to get information about its status and the involved objects. In most cases, we cannot save objects to be used later, they most often change in number and form. Each time we want to know how many objects are currently managed by the service, we must query for them. This operation is made in the refresh loop, and objects will be released at the end of the loop. Their status and property values are displayed to the user in a human readable form and saved in other object types. This process takes a lot of work; in the sample code, the refresh time can be customized in the Option dialog, from 1 to 20 seconds. To simplify UI update (TreeView and ListView controls), jobs and files data are saved in these C++ classes: CBitsJob and CBitsFile (see source code).

C++
// Enumerating Jobs
...
CComBitsEnumJobs comEnumJobs;
CComBitsJob comJob;
hResult = comBits.EnumJobs (0, &comEnumJobs);
if (SUCCEEDED (hResult))
{
   ULONG ulCount = 0;
   hResult = comEnumJobs->GetCount (&ulCount);
   hResult = comEnumJobs->Reset ();
   // signed/unsigned syndrome
   int iCount = ulCount;
   for (int i = 0; i < iCount; i++)
   {
      hResult = comEnumJobs->Next (1, &comJob, NULL);
      if (SUCCEEDED (hResult))
      {
         // Get job data and update UI
         ...
         // Get job files and update UI
         ...
         // End using Job
         comJob.Release ();
      }
   }
   // End using EnumJobs
   comEnumJobs.Release ();
}
...

When a function returns an unsigned value other than a DWORD, WORD, or BYTE, I immediately convert it into a signed value. This most often happens when the unsigned value is used as a counter (object count, color count, etc.), but there is a reason for this. Normally, a counter is used in a for/while loop using a signed integer as index, and this index is compared with the counter to stop the loop when it reaches the counter value (minus one). To avoid compiler warning or casting an unsigned to a signed value, I immediately convert it. In the source code sample, I commented this as "signed/unsigned syndrome", don't care about this.

The code above shows how to enumerate jobs. The first parameters of the method EnumJobs are flags. Currently, there is only one flag defined. In the example, flags are set to 0. This means, only jobs of the currently logged user are enumerated. To enumerate jobs for all users, set flags to BG_JOB_ENUM_ALL_USERS. Next, get the job count and seek the enumerator position to start. The for loop iterates jobs in the comJob object. If the function succeeds, take its properties and status and refresh the user interface, then enumerate files in the same manner, as shown in this code:

C++
// Enumerate Files
...
CComBitsEnumFiles comEnumFiles;
CComBitsFile comFile;
hResult = comJob.EnumFiles (&comEnumFiles);
if (SUCCEEDED (hResult))
{
   ULONG ulCount = 0;
   hResult = comEnumFiles->GetCount (&ulCount);
   hResult = comEnumFiles->Reset ();
   // signed/unsigned syndrome

   int iCount = ulCount;
   for (int i = 0; i < iCount; i++)
   {
      hResult = comEnumFiles->Next (1, &comFile, NULL);
      if (SUCCEEDED (hResult))
      {
         // Get file data and update UI
         ...
         // End using File
         comFile.Release ();
      }
   }
   // End using EnumFiles
   comEnumFiles.Release ();
}
...

Enumerating files is similar to enumerating jobs. The job has many properties, a status, and progress data. Properties include the date and time of creation, the transfer priority, the name, ID, and description, the owner, the time delay before trying to connect to a remote server and starting transfers, and a no progress timeout after which the job raises an error and most often loses transferred job data and tries a new connection. These two last parameters are in seconds. The retry delay by default is 10 minutes (600 seconds), the second one is a big timeout - 1,209,600 seconds, corresponding to 14 days - this means if the job cannot transfer data during this time, the status is set to 'error', and the job is reset. The status before this event is called 'transient error'. Progress data is maintained in a struct, the members of the struct are: the total number of files added to the job and the number of transferred files, the total bytes to transfer, and the bytes already transferred.

File data is restricted to remote and local file names and the progress, a struct containing three members: the total bytes to transfer, the bytes already transferred, and a completion flag - this flag specifies that the file is available to the user and not that it was fully transferred. To test if a file was completely transferred, compare the transferred and total byte count; if they contain the same value, the file was successfully transferred.

Creating and Managing Jobs and Files

All BITS operations are implemented by the job object, only the job creation is a main object method. The most important property of a job is the ID. This is the only value needed to create a job. The ID must have the GUID format, normally used for creating COM objects in *.idl files. There is a Windows API to create a random GUID.

C++
...
// Create a Job
CComBitsJob comJob;
GUID guidID;
hResult = ::UuidCreate (&guidID);
if (SUCCEEDED (hResult))
{
   hResult = comBits->CreateJob (TEXT ("The Job"), BG_JOB_TYPE_DOWNLOAD,
                                 &guidID, &comJob);
   ...
}
...

A newly created job has normal priority and its state is suspended. Use the job's methods to set the initial state and add files:

C++
// Job common operations
hResult = comJob->Resume ();
hResult = comJob->Suspend ();
hResult = comJob->Cancel ();
hResult = comJob->Complete ();

The Resume and Suspend methods explain themselves. The Cancel method removes a job from the list and all data is lost. The Complete method makes files available to the user and removes a job from the list. When all the files in a job are transferred, they are not available to the user. For this, all files have to be completed, and only the Complete method does this. The sample application calls the Complete method when all files in a job are transferred, and informs the user with a dialog box.

To add files, use the method AddFile or AddFileSet. The first adds one file at a time; the second one adds a list of files. All you need to add a file is the remote file URL and a local file system path for the transferred file.

C++
// Files operations
hResult = comJob->AddFile (strRemoteFile, strLocalFile);
hResult = comJob->AddFileSet (iCount, oFileSetArray);

The remote URL protocol must be HTTP or HTTPS. The URL must specify a static file. Redirection is not supported. If the URL is not valid, the job rejects the file. A job downloads one file at a time in the same order it was added: the first added file is the first transferred.

Error Handling

When an error occurs, the status of the job becomes 'error'. To get the error information, first get the error object from the job, then call its methods to get the descriptive strings of the context and the message. When getting string messages, you must specify a language ID. Not all languages are supported, so if getting the string fails for a language, try to retrieve the string for the English US language. On my system (Italian), the context error string is available only in the Italian language, and the error message string only in the English US language. Mystic!

Update

2nd June, 2008

  • Updated the comparison operator overload in CBitsFile and CBitsJob classes and the post build process as suggested by 'hagrdan' in the discussion at the bottom of the article
  • Updated stdafx.h for ATL compilation without sources modification and WTL 8.0
  • Updated an error in CApp::Create, module initialization moved at the top of the nested ifs

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)