XapLoader compatible with application library cache

Nov 23, 2009 at 8:09 AM
/*
Copyright (c) 2009, Dennis Haney <davh@davh.dk>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY Dennis Haney ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Dennis Haney BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Resources;
using System.Xml;

namespace Utilities
{
    /// <summary>
    /// A downloader of XAP files, that is capable of also fetching the "Reduce XAP size by using application library cache" dependencies.
    /// TODO: Implement progress notification
    /// </summary>
    public class XapLoader
    {
        private readonly ObservableCollection<WebClient> _webClients = new ObservableCollection<WebClient>();
        private readonly List<KeyValuePair<AssemblyPart, Stream>> _assemblies = new List<KeyValuePair<AssemblyPart, Stream>>();
        private int _downloadcount;
        private Exception _firstError;
        private volatile bool _wasCancelled;
        private readonly HashSet<Uri> _dependencies = new HashSet<Uri>();

        /// <summary>
        /// Occurs when the download of all files and dependencies are completed
        /// </summary>
        public event Action<AsyncCompletedEventArgs, IEnumerable<Assembly>> DownloadCompleted;

        /// <summary>
        /// Initiate downloads of the given xap files, and all of their dependencies.
        /// Can be called multiple times, but might get multiple callbacks if downloads completed between the calls.
        /// </summary>
        public void StartDownloads(params Uri[] uris)
        {
            StartDownloads(uris.AsEnumerable());
        }


        /// <summary>
        /// Initiate downloads of the given xap files, and all of their dependencies
        /// Can be called multiple times, but might get multiple callbacks if downloads completed between the calls.
        /// </summary>
        public void StartDownloads(IEnumerable<Uri> uris)
        {
            _wasCancelled = false;
            foreach (var uri in uris)
            {
                WebClient wc = new WebClient();
                wc.OpenReadCompleted += FetchCompleted;
                Interlocked.Increment(ref _downloadcount);
                lock (_webClients)
                    _webClients.Add(wc);
                wc.OpenReadAsync(uri);
            }
        }

        /// <summary>
        /// Called when one of the downloads are done
        /// </summary>
        private void FetchCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            if (_wasCancelled) return;
            if (e.Error != null || e.Cancelled)
            {
                _firstError = e.Error;
                CancelAsync();
                return;
            }
            if (!_wasCancelled)
                LoadPackagedAssemblies(e.Result);

            int left = Interlocked.Decrement(ref _downloadcount);
            if (left > 0)
                return;
            //Since we added these as we got them, the dependencies are actually last, 
            //so reverse the order, so that depencies are loaded first
            _assemblies.Reverse(); 
            OnDownloadComplete(_wasCancelled);
        }

        private void OnDownloadComplete(bool wasCancelled)
        {
            IEnumerable<Assembly> assms = null;
            if (!wasCancelled)
                assms = from kvp in _assemblies select kvp.Key.Load(kvp.Value);
            _assemblies.Clear();
            _webClients.Clear();
            Action<AsyncCompletedEventArgs, IEnumerable<Assembly>> evt = DownloadCompleted;
            if (evt != null)
                evt(new AsyncCompletedEventArgs(_firstError, wasCancelled, null), assms);
        }

        /// <summary>
        /// Cancel all pending downloads. Not threadsafe with calls to StartDownloads
        /// </summary>
        public void CancelAsync()
        {
            _wasCancelled = true;
            lock (_webClients)
            {
                foreach (var wc in _webClients.Where(wc => wc.IsBusy))
                    wc.CancelAsync();
            }
            OnDownloadComplete(true);
        }

        /// <summary>
        /// Load all dlls from zip files and xap files, for the xap files also initiate the download of any non-included dependencies
        /// </summary>
        private void LoadPackagedAssemblies(Stream packageStream)
        {
            StreamResourceInfo packageStreamInfo = new StreamResourceInfo(packageStream, null);
            StreamResourceInfo manifestStreamInfo = Application.GetResourceStream(packageStreamInfo, new Uri("AppManifest.xaml", UriKind.Relative));
            if (manifestStreamInfo == null) //Zip file with DLLs only
            {
                foreach (var filename in GetFileNames(packageStream))
                    Add(packageStreamInfo, filename);
                return;
            }

            using (XmlReader reader = XmlReader.Create(manifestStreamInfo.Stream))
            {
                reader.ReadToFollowing("AssemblyPart");
                do
                {
                    string source = reader.GetAttribute("Source");
                    Add(packageStreamInfo, source);
                } while (reader.ReadToNextSibling("AssemblyPart"));

                //Unfortunately the way MS did this, they didn't bother writing what assemblies are actually in those links,
                //so we are forced to fetch the files even if they turn out to already be loaded
                List<Uri> newdependencies = new List<Uri>();
                reader.ReadToFollowing("ExtensionPart");
                do
                {
                    var uri = new Uri(reader.GetAttribute("Source"), UriKind.RelativeOrAbsolute);
                    lock (_dependencies)
                        if (_dependencies.Add(uri))
                            newdependencies.Add(uri);
                } while (reader.ReadToNextSibling("ExtensionPart"));

                if (!_wasCancelled)
                    StartDownloads(newdependencies);
            }
        }

        private void Add(StreamResourceInfo packageStreamInfo, string source)
        {
            Stream stream = Application.GetResourceStream(packageStreamInfo, new Uri(source, UriKind.Relative)).Stream;
            var assemblyPart = new AssemblyPart { Source = source };
            lock (_assemblies) //We dont load them here, so that we can load them in the right order at the end
                _assemblies.Add(new KeyValuePair<AssemblyPart, Stream>(assemblyPart, stream));
        }

        /// <summary>
        /// This really ougth to be in the silverlight library
        /// </summary>
        private static IEnumerable<string> GetFileNames(Stream stream)
        {
            stream.Seek(0, SeekOrigin.Begin); //rewind
            var ret = new List<string>();
            var archiveStream = new BinaryReader(stream);
            while (true)
            {
                string file = GetFileName(archiveStream);
                if (file == null) break;
                ret.Add(file);
            }
            stream.Seek(0, SeekOrigin.Begin); //rewind
            return ret;
        }

        private static string GetFileName(BinaryReader reader)
        {
            // http://www.pkware.com/documents/casestudies/APPNOTE.TXT
            var headerSignature = reader.ReadInt32();  // local file header signature     4 bytes  (0x04034b50)
            if (headerSignature != 0x04034b50)
                return null; // Not a zip file
            reader.ReadInt16();                        // version needed to extract       2 bytes
            reader.ReadInt16();                        // general purpose bit flag        2 bytes
            reader.ReadInt16();                        // compression method              2 bytes
            reader.ReadInt16();                        // last mod file time              2 bytes
            reader.ReadInt16();                        // last mod file date              2 bytes
            reader.ReadInt32();                        // crc-32                          4 bytes 
            int compressedsize = reader.ReadInt32();   // compressed size                 4 bytes
            reader.ReadInt32();                        // uncompressed size               4 bytes
            short filenamelength = reader.ReadInt16();   // file name length                2 bytes
            short extrafieldlength = reader.ReadInt16(); // extra field length              2 bytes
            byte[] fn = reader.ReadBytes(filenamelength); // file name                    (variable size)
            string filename = Encoding.UTF8.GetString(fn, 0, filenamelength);
            //And then make sure to skip the actual data, so that we can loop over it
            reader.BaseStream.Seek(compressedsize + extrafieldlength, SeekOrigin.Current);

            return filename;
        }

    }
}

Can anyone see any problems with this approach?

 

/*
Copyright (c) 2009, Dennis Haney <davh@davh.dk>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY Dennis Haney ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Dennis Haney BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Resources;
using System.Xml;

namespace Utilities
{
    /// <summary>
    /// A downloader of XAP files, that is capable of also fetching the "Reduce XAP size by using application library cache" dependencies.
    /// TODO: Implement progress notification
    /// </summary>
    public class XapLoader
    {
        private readonly ObservableCollection<WebClient> _webClients = new ObservableCollection<WebClient>();
        private readonly List<KeyValuePair<AssemblyPart, Stream>> _assemblies = new List<KeyValuePair<AssemblyPart, Stream>>();
        private int _downloadcount;
        private Exception _firstError;
        private volatile bool _wasCancelled;
        private readonly HashSet<Uri> _dependencies = new HashSet<Uri>();

        /// <summary>
        /// Occurs when the download of all files and dependencies are completed
        /// </summary>
        public event Action<AsyncCompletedEventArgs, IEnumerable<Assembly>> DownloadCompleted;

        /// <summary>
        /// Initiate downloads of the given xap files, and all of their dependencies.
        /// Can be called multiple times, but might get multiple callbacks if downloads completed between the calls.
        /// </summary>
        public void StartDownloads(params Uri[] uris)
        {
            StartDownloads(uris.AsEnumerable());
        }


        /// <summary>
        /// Initiate downloads of the given xap files, and all of their dependencies
        /// Can be called multiple times, but might get multiple callbacks if downloads completed between the calls.
        /// </summary>
        public void StartDownloads(IEnumerable<Uri> uris)
        {
            _wasCancelled = false;
            foreach (var uri in uris)
            {
                WebClient wc = new WebClient();
                wc.OpenReadCompleted += FetchCompleted;
                Interlocked.Increment(ref _downloadcount);
                lock (_webClients)
                    _webClients.Add(wc);
                wc.OpenReadAsync(uri);
            }
        }

        /// <summary>
        /// Called when one of the downloads are done
        /// </summary>
        private void FetchCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            if (_wasCancelled) return;
            if (e.Error != null || e.Cancelled)
            {
                _firstError = e.Error;
                CancelAsync();
                return;
            }
            if (!_wasCancelled)
                LoadPackagedAssemblies(e.Result);

            int left = Interlocked.Decrement(ref _downloadcount);
            if (left > 0)
                return;
            //Since we added these as we got them, the dependencies are actually last,
            //so reverse the order, so that depencies are loaded first
            _assemblies.Reverse();
            OnDownloadComplete(_wasCancelled);
        }

        private void OnDownloadComplete(bool wasCancelled)
        {
            IEnumerable<Assembly> assms = null;
            if (!wasCancelled)
                assms = from kvp in _assemblies select kvp.Key.Load(kvp.Value);
            _assemblies.Clear();
            _webClients.Clear();
            Action<AsyncCompletedEventArgs, IEnumerable<Assembly>> evt = DownloadCompleted;
            if (evt != null)
                evt(new AsyncCompletedEventArgs(_firstError, wasCancelled, null), assms);
        }

        /// <summary>
        /// Cancel all pending downloads. Not threadsafe with calls to StartDownloads
        /// </summary>
        public void CancelAsync()
        {
            _wasCancelled = true;
            lock (_webClients)
            {
                foreach (var wc in _webClients.Where(wc => wc.IsBusy))
                    wc.CancelAsync();
            }
            OnDownloadComplete(true);
        }

        /// <summary>
        /// Load all dlls from zip files and xap files, for the xap files also initiate the download of any non-included dependencies
        /// </summary>
        private void LoadPackagedAssemblies(Stream packageStream)
        {
            StreamResourceInfo packageStreamInfo = new StreamResourceInfo(packageStream, null);
            StreamResourceInfo manifestStreamInfo = Application.GetResourceStream(packageStreamInfo, new Uri("AppManifest.xaml", UriKind.Relative));
            if (manifestStreamInfo == null) //Zip file with DLLs only
            {
                foreach (var filename in GetFileNames(packageStream))
                    Add(packageStreamInfo, filename);
                return;
            }

            using (XmlReader reader = XmlReader.Create(manifestStreamInfo.Stream))
            {
                reader.ReadToFollowing("AssemblyPart");
                do
                {
                    string source = reader.GetAttribute("Source");
                    Add(packageStreamInfo, source);
                } while (reader.ReadToNextSibling("AssemblyPart"));

                //Unfortunately the way MS did this, they didn't bother writing what assemblies are actually in those links,
                //so we are forced to fetch the files even if they turn out to already be loaded
                List<Uri> newdependencies = new List<Uri>();
                reader.ReadToFollowing("ExtensionPart");
                do
                {
                    var uri = new Uri(reader.GetAttribute("Source"), UriKind.RelativeOrAbsolute);
                    lock (_dependencies)
                        if (_dependencies.Add(uri))
                            newdependencies.Add(uri);
                } while (reader.ReadToNextSibling("ExtensionPart"));

                if (!_wasCancelled)
                    StartDownloads(newdependencies);
            }
        }

        private void Add(StreamResourceInfo packageStreamInfo, string source)
        {
            Stream stream = Application.GetResourceStream(packageStreamInfo, new Uri(source, UriKind.Relative)).Stream;
            var assemblyPart = new AssemblyPart { Source = source };
            lock (_assemblies) //We dont load them here, so that we can load them in the right order at the end
                _assemblies.Add(new KeyValuePair<AssemblyPart, Stream>(assemblyPart, stream));
        }

        /// <summary>
        /// This really ougth to be in the silverlight library
        /// </summary>
        private static IEnumerable<string> GetFileNames(Stream stream)
        {
            stream.Seek(0, SeekOrigin.Begin); //rewind
            var ret = new List<string>();
            var archiveStream = new BinaryReader(stream);
            while (true)
            {
                string file = GetFileName(archiveStream);
                if (file == null) break;
                ret.Add(file);
            }
            stream.Seek(0, SeekOrigin.Begin); //rewind
            return ret;
        }

        private static string GetFileName(BinaryReader reader)
        {
            // http://www.pkware.com/documents/casestudies/APPNOTE.TXT
            var headerSignature = reader.ReadInt32();  // local file header signature     4 bytes  (0x04034b50)
            if (headerSignature != 0x04034b50)
                return null; // Not a zip file
            reader.ReadInt16();                        // version needed to extract       2 bytes
            reader.ReadInt16();                        // general purpose bit flag        2 bytes
            reader.ReadInt16();                        // compression method              2 bytes
            reader.ReadInt16();                        // last mod file time              2 bytes
            reader.ReadInt16();                        // last mod file date              2 bytes
            reader.ReadInt32();                        // crc-32                          4 bytes
            int compressedsize = reader.ReadInt32();   // compressed size                 4 bytes
            reader.ReadInt32();                        // uncompressed size               4 bytes
            short filenamelength = reader.ReadInt16();   // file name length                2 bytes
            short extrafieldlength = reader.ReadInt16(); // extra field length              2 bytes
            byte[] fn = reader.ReadBytes(filenamelength); // file name                    (variable size)
            string filename = Encoding.UTF8.GetString(fn, 0, filenamelength);
            //And then make sure to skip the actual data, so that we can loop over it
            reader.BaseStream.Seek(compressedsize + extrafieldlength, SeekOrigin.Current);

            return filename;
        }

    }
}