KISS

Flattr this!

KISS is a well known acronym in design circuits. According to Wikipedia, it was first noted by the U.S Navy in the sixties.

When I was going for my second attempt at creating a fully fledged auto updating framework, this acronym flashed before my eyes before I’d written as much as a single line of code. The last one I did was over designed, and I could never get it to work the way I wanted it to.

The main way in which it was over designed was that it kept a record of previous software versions/patches. This was stupid. It required too much bookkeeping, for absolutely no gain. By the time new patches are deployed, any information in a previous patch is outdated anyway.

Therefore, I took it upon myself to do it, this time, the Right Way (to paraphrase John Carmack.)

The first vital decision I made was to put the actual downloading code into a library, so that it could be separated from the application itself and be reused. Obviously, I named the library KISS.

The second vital decision I made was to ignore any notion of previous versions/patches. This resulted in the following:

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Security;
using System.IO;
using System.Threading;
using System.Security.Cryptography.X509Certificates;

namespace KISS
{
    public delegate void FetchedManifestDelegate(ManifestFile Manifest);
    public delegate void FetchedFileDelegate(MemoryStream FileStream);
    public delegate void DownloadTickDelegate(RequestState State);

    /// <summary>
    /// A requester requests and fetches files from a webserver, and calls
    /// events when the requests are complete.
    /// </summary>
    public class Requester
    {
        //This is implementation specific. Personally, I'm ignoring security to avoid headaches.
        public static bool ACCEPT_ALL_CERTIFICATES = true;

        public event FetchedManifestDelegate OnFetchedManifest;
        private string m_ManifestAddress = "";

        /// <summary>
        /// Contains information about a download in progress,
        /// such as: percent complete and KB/sec.
        /// </summary>
        public event DownloadTickDelegate OnTick;

        /// <summary>
        /// Called when a file was fetched!
        /// </summary>
        public event FetchedFileDelegate OnFetchedFile;

        private bool m_HasFetchedManifest = false;

        public Requester(string ManifestAddress)
        {
            m_ManifestAddress = ManifestAddress;
        }

        public void Initialize()
        {
            try
            {
                WebRequest Request = WebRequest.Create(m_ManifestAddress);
                RequestState ReqState = new RequestState();

                ReqState.Request = Request;
                ReqState.TransferStart = DateTime.Now;
                Request.BeginGetResponse(new AsyncCallback(GotInitialResponse), ReqState);
                ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(AcceptAllCertifications);
            }
            catch (Exception E)
            {
                Logger.Log("Exception in Requester.Initialize:\n" + E.ToString(), LogLevel.error);
            }
        }

        /// <summary>
        /// Starts fetching a file, and notifies the OnFetchedFile event
        /// when done.
        /// </summary>
        public void FetchFile(string URL)
        {
            WebRequest Request = WebRequest.Create(URL);
            Request.Method = "GET";
            RequestState ReqState = new RequestState();

            ReqState.Request = Request;
            ReqState.TransferStart = DateTime.Now;
            Request.BeginGetResponse(new AsyncCallback(GotInitialResponse), ReqState);
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(AcceptAllCertifications);
        }

        private void GotInitialResponse(IAsyncResult AResult)
        {
            RequestState ReqState = (RequestState)AResult.AsyncState;
            ReqState.Response = ReqState.Request.EndGetResponse(AResult);
            ReqState.ContentType = ReqState.Response.ContentType;
            ReqState.ContentLength = (int)ReqState.Response.ContentLength;

            Stream ResponseStream = ReqState.Response.GetResponseStream();
            ReqState.ResponseStream = ResponseStream;
            ReqState.RequestBuffer = new byte[ReqState.ContentLength];
            ResponseStream.BeginRead(ReqState.RequestBuffer, 0, (int)ReqState.Response.ContentLength,
                new AsyncCallback(ReadCallback), ReqState);
        }

        private void ReadCallback(IAsyncResult AResult)
        {
            RequestState ReqState = ((RequestState)(AResult.AsyncState));

            Stream ResponseStream = ReqState.ResponseStream;

            // Get results of read operation
            int BytesRead = ResponseStream.EndRead(AResult);

            // Got some data, need to read more
            if (BytesRead > 0)
            {
                // Report some progress, including total # bytes read, % complete, and transfer rate
                ReqState.BytesRead += BytesRead;
                ReqState.PctComplete = ((double)ReqState.BytesRead / (double)ReqState.ContentLength) * 100.0f;

                // Note: bytesRead/totalMS is in bytes/ms. Convert to kb/sec.
                TimeSpan totalTime = DateTime.Now - ReqState.TransferStart;
                ReqState.KBPerSec = (ReqState.BytesRead * 1000.0f) / (totalTime.TotalMilliseconds * 1024.0f);

                OnTick(ReqState);

                // Kick off another read
                IAsyncResult ar = ResponseStream.BeginRead(ReqState.RequestBuffer, ReqState.BytesRead,
                    (ReqState.RequestBuffer.Length - ReqState.BytesRead), new AsyncCallback(ReadCallback), ReqState);
                return;
            }

            // EndRead returned 0, so no more data to be read
            else
            {
                ResponseStream.Close();
                ReqState.Response.Close();
                ReqState.ResponseStream.Close();
                OnFinishedFile(new MemoryStream(ReqState.RequestBuffer));
            }
        }

        /// <summary>
        /// Finished downloading a file!
        /// </summary>
        /// <param name="FileStr">The stream of the file that was downloaded.</param>
        private void OnFinishedFile(MemoryStream FileStr)
        {
            if (!m_HasFetchedManifest)
            {
                m_HasFetchedManifest = true;
                OnFetchedManifest(new ManifestFile(FileStr));
            }
            else
            {
                OnFetchedFile(FileStr);
            }
        }

        private bool AcceptAllCertifications(object sender, X509Certificate certification, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return ACCEPT_ALL_CERTIFICATES;
        }
    }
}

This is just sexy. It is short, to the point, easy to maintain, flexible and elegant. I will eventually port this to C++.

To explain, the KISS framework does most of its work based on a local and remote manifest file, which can be generated by a tool called Manifestation. A manifest file simply contains the version of a patch (or the client’s current version, if the manifest is a local one), the number of files contained in the manifest, and then a listing of the files that make up the patch. A file is defined as:

  • Address – the local address of the file, starting from the root folder of the client.
  • Hash – Hash of a file, to determine if the file has changed (and thus, if it needs to be downloaded).
  • URL – The URL of the file.

The actual code is equally simple:

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace KISS
{
    /// <summary>
    /// A manifest file is a file that has a version and references a bunch of patch files.
    /// </summary>
    public class ManifestFile
    {
        public string Version = "";
        public List<PatchFile> PatchFiles = new List<PatchFile>();

        public ManifestFile(string Path, string Version, List<PatchFile> PatchFiles)
        {
            bool HasURLs = false;
            BinaryWriter Writer = new BinaryWriter(File.Create(Path));
            Writer.Write((string)Version);

            if (PatchFiles[0].URL != "")
                HasURLs = true;

            foreach (PatchFile PFile in PatchFiles)
            {
                if (!HasURLs)
                    Writer.Write((string)PFile.Address + "," + PFile.FileHash);
                else
                    Writer.Write((string)PFile.Address + "," + PFile.FileHash + PFile.URL);
            }

            Writer.Flush();
            Writer.Close();
        }

        /// <summary>
        /// Creates a ManifestFile instance from a downloaded stream.
        /// </summary>
        /// <param name="ManifestStream"></param>
        public ManifestFile(Stream ManifestStream)
        {
            BinaryReader Reader = new BinaryReader(ManifestStream);
            Reader.BaseStream.Position = 0; //IMPORTANT!

            Version = Reader.ReadString();
            int NumFiles = Reader.ReadInt32();

            for(int i = 0; i < NumFiles; i++)
            {
                string PatchFileStr = Reader.ReadString();
                string[] SplitPatchFileStr = PatchFileStr.Split(",".ToCharArray());

                PatchFiles.Add(new PatchFile()
                {
                    Address = SplitPatchFileStr[0],
                    FileHash = SplitPatchFileStr[1],
                    URL = SplitPatchFileStr[2]
                });
            }

            Reader.Close();
        }
    }
}

This has but one disadvantage: A hash is stored as text, so that comparing them isn’t particularly fast, but this can obviously be changed to suit your need if you decide to use the framework. My plan is to eventually port the framework to C++; for now, it is available as C# code at Github. The license is Mozilla 2.0 – my personal favorite. If you change anything, you have to share it. Other than that, use as you please!

11 thoughts on “KISS

Leave a Reply

Your email address will not be published. Required fields are marked *