A Simple BITS Client Written In PowerShell and C#
Earlier this week, I started a project which required downloading huge files from a web server. To do this efficiently across the network, we decided to use the Background Intelligent Transfer Service (BITS) built into Windows.
From the initial searching I was only able to find RogerB’s System.Net.Bits.dll which is a collection of light weight managed wrappers to the COM API. This was initially really useful until the series of InvalidComObject “COM object that has been separated from its underlying RCW cannot be used” started occurring. These errors were fixed and all was well, then our build server started to die from running unit tests against this component.
It turns out that with small tweaks of this code it is very easy to get it to dead-lock or in some scenarios kill the CLR due to disposing objects incorrectly. Combine this with Roger’s unwillingness to support the project; this is effectively a dead end.
Then I found that PowerShell has the ability to do all of this and is fully supported! To hide the PowerShell implementation details from the rest of my managed application I created a thin wrapper called BitsClient. It exposes events to receive progress notifications and when the file is finally complete and works asynchronously.
One last note; to build this add a reference to System.Management.Automation.dll, which is part of the PowerShell SDK that is shipped inside of the Windows SDK.
/// <summary>
/// Represents a client for downloading files through the BITS
/// </summary>
public class BitsClient
{
/// <summary>
/// The remote address to be downloaded
/// </summary>
public string SourcePath { get; private set; }
/// <summary>
/// The local file to place the file
/// </summary>
public string DestinationPath { get; private set; }
/// <summary>
/// Optional property to hold a state object
/// </summary>
public object Tag { get; set; }
/// <summary>
/// The PowerShell runspace that invoking the commands
/// </summary>
Runspace DownloadRunspace { get; set; }
/// <summary>
/// Event raised when the download progress changes
/// </summary>
public EventHandler<BitsProgressEventArgs> ProgressChanged;
/// <summary>
/// Event raised after the file has been downloaded
/// </summary>
public EventHandler DownloadCompleted;
/// <summary>
/// Initialize a new download job
/// </summary>
/// <exception cref="ArgumentNullException"/>
public BitsClient(string source, string destination)
{
if (source == null)
throw new ArgumentNullException("source");
if (destination == null)
throw new ArgumentNullException("destination");
SourcePath = source;
DestinationPath = destination;
}
/// <summary>
/// Begin downloading <see cref="P:SourcePath"/>
/// </summary>
/// <exception cref="InvalidOperationException"/>
public void BeginDownloading()
{
if (DownloadRunspace != null)
{
throw new InvalidOperationException
("Unable to download a file while already in progress");
}
DownloadRunspace = RunspaceFactory.CreateRunspace();
DownloadRunspace.Open();
DownloadRunspace.SessionStateProxy
.SetVariable("BitsClientInstance", this);
DownloadRunspace.SessionStateProxy
.SetVariable("dest", DestinationPath);
DownloadRunspace.SessionStateProxy
.SetVariable("source", SourcePath);
var pipeline = DownloadRunspace.CreatePipeline();
var sb = new StringBuilder();
sb.AppendLine("Import-Module 'BitsTransfer'");
sb.AppendLine("$job = Start-BitsTransfer "+
"-Source $source -Destination $dest -Asynchronous");
#if DIAGNOSE
sb.AppendLine
("$BitsClientInstance.Step($job.JobId.ToString());");
#endif
sb.AppendLine("while "+
"($job.BytesTransferred -lt $job.BytesTotal){ ");
sb.AppendLine(" $BitsClientInstance.RaiseProgress("+
"$job.BytesTransferred, $job.BytesTotal);");
sb.AppendLine(" [System.Threading.Thread]::Sleep(5000);");
sb.AppendLine("}");
sb.AppendLine(" $job | Complete-BitsTransfer");
sb.AppendLine("$BitsClientInstance.RaiseComplete();");
#if DIAGNOSE
sb.AppendLine("$BitsClientInstance.Step('Script Complete.');");
#endif
var command = new Command("Invoke-Expression");
command.Parameters.Add("-Command", sb.ToString());
pipeline.Commands.Add(command);
ThreadPool.QueueUserWorkItem((unused) =>
{
try
{
pipeline.Invoke();
pipeline.Dispose();
DownloadRunspace.Dispose();
DownloadRunspace=null;
}
catch (Exception ex)
{
// This should never happen.
Debug.Assert(false, ex.Message);
}
});
}
#if DIAGNOSE
public void Step(string message)
{
Console.WriteLine(message);
}
#endif
public void RaiseProgress(long tranfserred, long total)
{
if (ProgressChanged != null)
{
ProgressChanged(this,
new BitsProgressEventArgs(tranfserred, total));
}
}
public void RaiseComplete()
{
if (DownloadCompleted != null)
DownloadCompleted(this, EventArgs.Empty);
}
}
/// <summary>
/// Represents a Progress Notification
/// </summary>
public class BitsProgressEventArgs : EventArgs
{
/// <summary>
/// Total number of bytes to downloaded
/// </summary>
public long BytesTotal { get; private set; }
/// <summary>
/// Total number of bytes that have been downloaded
/// </summary>
public long BytesTransferred { get; private set; }
/// <summary>
/// The Percent of the file that has been downloaded
/// </summary>
public int PercentComplete
{
get
{
if (BytesTotal == 0)
return -1;
return (int)(((float)BytesTransferred / (float)BytesTotal) * 100);
}
}
[DebuggerHidden]
internal BitsProgressEventArgs(long transfered, long total)
:base()
{
BytesTransferred = transfered;
BytesTotal = total;
}
}
Interview Question: Find the Missing Numbers Between 0 and Infinity Interview Question: How Would you Test a Software Component