Pull to refresh

Пишем свой сервис авто-обновлений

Reading time7 min
Views21K
Большинство разработчиков stand-alone приложение рано или поздно сталкиваются с проблемой доставки обновлений для своего приложения. В этой статье я постараюсь решить эту проблему наилучшим, на мой взгляд, способом — написать свой собственный универсальный сервис авто-обновлений, который будет висеть в процессах в единственном экземпляре и доставлять обновления для всех подписавшихся приложений.

Существует несколько готовых решений для .NET, но самое актуальное — это ClickOnce. Эту технологию уже нельзя назвать новой, однако серьёзное развитие, на мой взгляд, она получила не так давно, и не обладает исчерпывающим функционалом.
Если вы не хотите изобретать велосипед, то советую вам пристально изучить возможности ClickOnce, и если вам будет достаточно предлагаемого функционала, то это определенно ваш выбор. Однако ClickOnce не панацея и далеко не всегда ею можно обойтись.

Сейчас же я хочу рассказать о своём видении механизма авто-обновлений. Я не претендую на истинность в последней инстанции, так что конструктивная критика и предложения в комментариях приветствуются.

UPD: Суть реализации заключается в том, чтобы уменьшить количество процессов и служб, которые занимаются обновлением. Если у вас несколько приложений, то все они смогут «получать» обновления от одного единственного Windows-сервиса. Не надо будет для каждого приложения запускать лаунчер, держать соединение с сервером обновлений. Теоретически в системе всеми обновлениями может заниматься один процесс, и возможно этим процессом скоро станет ClickOnce, если разработчики перестанут делать свои «велосипеды». А разработчики перестанут делать свои велосипеды тогда, когда им будет достаточно функционала ClickOnce. Сейчас, к сожалению, это не всегда так.

Итак задача


Пусть у нас есть несколько разных приложений, установленных на компьютере пользователя. Мне бы хотелось написать универсальный сервис авто-обновлений, чтобы потом я мог использовать его и в других приложениях. И все приложения обновлялись используя только одну службу, что сэкономило бы ресурсы при большом количестве софта. Так же желательно, чтобы в существующих приложениях мне потребовалось внести минимальные изменения для подключения и настройки авто-обновлений. Процесс обновления должен быть настраиваемый для каждого приложения.

Реализация


Чтобы как-то конкретизировать задачу, решать её я буду применительно к ОС Windows, код писать на C#.NET, хотя в этой статье я буду в основном оперировать абстракциями и приводить только небольшие отрывки кода.

Мой сервис авто-обновлений состоит из 3 модулей:
1) Веб-сервис с самими обновлениями, учетом версий, базой данных всех поддерживаемых приложений и всем остальным что можно сюда вынести (например некоторые “удаленные” настройки процесса обновления конкретного приложения).
2) Windows-сервис, который будет коннектиться к веб-сервису и поверять обновления для всех подписанных приложений, по таймеру и по требованию.
3) Client-библиотека, которая будет знать как коннектиться к Windows-сервису Updater’а, а так же предоставлять приложению callback интерфейс.

Я разнес это все на 3 сборки и назвал соответственно.

Updater.Online — веб-сервис
Updater.Service — windows-сервис
Updater.Client — клиентский-модуль

Так же я выделил еще одну сборку для общих абстракций — Updater.Domain.

Updater Online


Начнем с Веб-сервиса. Тут всё просто, все можно впихнуть в один метод CheckForUpdates, который принимает ApplicationID и CurrentVersion, смотрит в базе есть ли актуальные обновления для данного приложения, и если есть возвращает путь к .zip файлу с обновление или null если обновлений нету. Это простейший случай, вообще как и запрашиваемых параметров так и результатов запроса может быть больше.
Сервис может возвращать дополнительную информацию, которая может пригодиться в процессе обновления. Например на сервисе можно указывать возможно ли обновление в silent-mode для данного приложения, дополнительные данные о том как скачать обновление, в каком оно формате, какой у него размер и т.д.

Updater Service


Это пожалуй самый объёмный модуль. Здесь находится WCF сервис UpdaterService, ниже приведен интерфейс, который он реализует, и callback-интерфейс.

[ServiceContract(CallbackContract = typeof(IUpdateServiceCallback))]
    public interface IUpdaterService
    {
        #region Callback subsctibe/unsibscribe methods
 
        [OperationContract(IsOneWay = true)]
        void Subscribe(SubscribeRequest request);
 
        [OperationContract(IsOneWay = true)]
        void Unsubscribe(UnsubscribeRequest request);
 
        #endregion
 
        [OperationContract(IsOneWay = true)]
        void InstallAvalibleUpdates(InstallAvalibleUpdatesRequst request);
 
        [OperationContract(IsOneWay = true)]
        void DownloadUpdate(Guid applicationId);
 
        [OperationContract(IsOneWay = true)]
        void CheckForUpdates(Guid applicationId);
 
    }
 
    [ServiceContract]
    public interface IUpdateServiceCallback
    {
        [OperationContract(IsOneWay = true)]
        void OnUpdateDetected(UpdateDetectedEventArgs eventArgs);
 
        [OperationContract(IsOneWay = true)]
        void OnUpdateDownloaded(UpdateDownloadedEventArgs updateDetectedEventArgs);
 
        [OperationContract(IsOneWay = true)]
        void OnUpdateInstalled(UpdateInstalledEventArgs eventArgs);
    } 


В методе Subscribe я добавляю callback, который пришел от клиента в статический Dictionary<TAppID,CallbackList>, где TAppID — айдишник приложения ( у меня Guid), для каждого приложения отдельный список callback’ов.
Вот реализация метода Subscribe

public void Subscribe(SubscribeRequest request)
    {
        // Достаем из контекста calback переданный клиентом
        IUpdateServiceCallback callback = OperationContext.Current.GetCallbackChannel<IUpdateServiceCallback>();
        //делаем lock, чтобы синхронизировать работу с коллекцией приложений
        lock (sync)
        {
            // Получаем от сервиса объект типа Application по его ID. Если такого нету,
            // то создаём новый. Добавляем/обновляем информацию о подписанном приложении
            var app = applicationService.Get(request.ApplicationId);
            if (app == null)
            {
                app = new Application()
                {
                    Id = request.ApplicationId
                };
                applicationService.Add(app);
            }
            app.CurrentVersion = request.Version ?? app.CurrentVersion;
            app.RootFolderPath = request.RootFolder ?? app.RootFolderPath;
            app.Name = request.ApplicationName ?? app.Name;
        }
        // Получаем лист callback’ов для текущего приложения или создаём новый, 
        // добавляем подписку к делегатам
        var list = GetEventList(request.ApplicationId) ?? new CallbacksList();
        list.OnUpdateDetected += callback.OnUpdateDetected;
        list.OnUpdateDownloaded += callback.OnUpdateDownloaded;
        if (registredCallbacks.ContainsKey(request.ApplicationId))
        {
            registredCallbacks[request.ApplicationId] = list;
        }
        else
        {
            registredCallbacks.Add(request.ApplicationId, list);
        }
        // Подписываемся на события объекта связи, чтобы отслеживать
        // закрытие клиента и выполнять некоторые действия (например 
        // проверять отписался ли он)
        ICommunicationObject obj = (ICommunicationObject)callback;
        obj.Closing += ClientClosing;
        obj.Closed += ClientClosed;
        applicationService.SaveChanges();
    } 


Так же в реализации UpdaterService я добавил статические методы для вызова Callback, чтобы немного скрыть саму реализацию их вызова. Эти методы вызваются на стороне Updater Service, когда необходимо инициировать соответствующие события на сторон клиента.

private static Dictionary<Guid, CallbacksList> registredCallbacks;
 
    // Метод выбирает из словоря список callback’ов или возвращает null если его там нету
    private static CallbacksList GetEventList(Guid appId)
    {
        CallbacksList result;
        return registredCallbacks.TryGetValue(appId, out result) ? result : null;
    }
 
    // Предоставляет список callback’ов и выполняет выбранный
    private static void PerformCallback(Guid applicationId, Action<CallbacksList> func)
    {
        try
        {
            var list = GetEventList(applicationId);
            if (list != null)
            {
                func(list);
            }
        }
        catch
        {
        }
    }
 
    // Метод инициирует событие OnUpdateDetected на всех подписанных клиентах 
    // с указанным ApplicatioId
    public static void OnUpdateDetected(UpdateDetectedEventArgs args)
    {
        PerformCallback(args.ApplicationId, callbacks => callbacks.OnUpdateDetected(args));
    } 


Так же на сервисе я запускаю “таймер”, который проверяет обновления для всех подписанных приложений через заданный промежуток времени.
У меня в коде фигурирует ApplicationService, хоть я и назвал его сервисом, он больше похож на хранилище информации о подписанных приложениях и обновлений для них.
Вот классы приложения и обновления.

public class Application
    {
        public Guid Id { get; set; }
 
        public String Name { get; set; }
 
        public Version CurrentVersion { get; set; }
 
        public String RootFolderPath { get; set; }
 
        public List<Update> Updates { get; set; }
    }
 
    public class Update
    {
        public String UpdateUrl { get; set; }
 
        public Version Version { get; set; }
 
        public bool IsInstalled { get; set; }
 
        public bool IsDownloaded { get; set; }
 
        public string UpdateLocalPath { get; set; }
    } 


Updater Client



Эта сборка подключаеься в приложении и следит за сообщениями с Updater сервиса, сообщая о них приложению через объект реализующий IUpdateServiceCallback.В качестве callback’а передаваемого сервису можно использовать тот же объект, что был передан из приложения в Updater Client, но лучше сделать обертку, чтобы фильтровать информацию, передаваемую клиентскому приложению, а не пихать ему все подряд, что вернет Updater сервис. В приведенной мной реализации обертка не используеться. Так же клиентское приложение передает данные о себе в виде объекта, реализующего интерфейс IUpdatаble.

public interface IUpdatаble
    {
        Guid ApplicationId { get; }
 
        String ApplicationName { get; }
 
        String RootFolder { get; }
    }
 
    public class UpdaterClient
    {
        private IUpdaterService client;
 
        private IUpdateble settings;
 
        private DuplexChannelFactory<IUpdaterService> factory;
 
        public UpdaterClient(IUpdateServiceCallback callback, IUpdateble settings)
        {
            this.settings = settings;
            var context = new InstanceContext(callback);
            var binding = new NetTcpBinding();
            // Создаем фабрику двусторонних каналов связи, в качестве колбэка
            // используем переданный в конструктор пользовательский объект,
            // который реализует интерфейс IUpdateServiceCallback
            factory = new DuplexChannelFactory<IUpdaterService>(context, binding,
                                        new EndpointAddress(UpdaterSettings.Default.UpdaterServiceUrl));
            client = factory.CreateChannel();
        }
 
        // Предоставляем клиенту методы сервиса, в качестве 
        // параметров используем данный из Iupdateble объекта, 
        // который был передан в конструкторе
        public void Subscribe()
        {
            client.Subscribe(new SubscribeRequest()
            {
                ApplicationId = settings.ApplicationId,
                RootFolder = settings.RootFolder,
                Version = settings.Version,
                ApplicationName = settings.ApplicationName
            });
        }
 
        public void Unsubscribe()
        {
            client.Unsubscribe(new UnsubscribeRequest()
            {
                ApplicationId = settings.ApplicationId
            });
        }
 
        public void InstallUpdates(bool reopenOnComplete)
        {
            InstallUpdates(reopenOnComplete);
        }
 
        public void DownloadUpdate()
        {
            client.DownloadUpdate(new DownloadUpdateRequest()
            {
                ApplicationId = settings.ApplicationId
            });
        }
 
        public void CheckForUpdates()
        {
            client.CheckForUpdates(settings.ApplicationId);
        }
 
        public void InstallUpdates(bool reopenOnComplete)
        {
            client.InstallAvalibleUpdates(new InstallAvalibleUpdatesRequst()
            {
                ApplicationId = settings.ApplicationId,
                RestartOnComplete = reopenOnComplete
            });
        }
    } 


Ну вот собственно и всё. Используется на стороне клиента следующим образом — подключаем сборку Updater.Client, создаём объект типа UpdaterClient, вызываем метод Subscribe, и наше приложение начинает получать сообщения от сервиса о новых обновлениях.
Tags:
Hubs:
Total votes 36: ↑33 and ↓3+30
Comments26

Articles