От простых скриптов к клиент-серверному приложению на WCF своими руками: почему мне нравится работа в CM

Работа в команде Configuration Management связана с обеспечением функциональности билд-процессов — сборки продуктов компании, предварительной проверки кода, статистического анализа, ведения документации и многого другого. Помимо этого, мы постоянно работаем над оптимизацией различных процессов, и, что замечательно, мы практически свободны в выборе инструментов для этой интересной работы. Далее я подробно расскажу о том, как, обладая лишь разного уровня знаниями в C# и C++, я сделал функциональный WCF-сервис для работы с очередями фиксов. И почему решил, что это очень важно.

b47a3e2d9febdfd19dcde2b59d58df1b.png

Автоматизация один раз или инструкция на 117 страниц снова и снова


Небольшое лирическое отступление, чтобы вы поняли, почему я так переживаю из-за автоматизации и оптимизации процессов.

До Veeam я работал в крупной международной компании — был тимлидом команды Configuration Management, занимался сборкой приложения и развертыванием его на тестовых окружениях. Программа успешно разрабатывалась, добавлялись новые функции, писалась документация, поддержкой которой я тоже занимался. Но меня всегда удивляло, почему у такой серьезной программы нет нормальной системы конфигурации параметров, которых были многие десятки, если не сотни.

Я общался на эту тему с разработчиками и получил ответ — заказчик не оплатил эту фичу, не согласовал ее стоимость, поэтому фича не была реализована. А по факту страдали QA и непосредственно мы, команда СМ. Конфигурация программы и ее предварительная настройка осуществлялась через множество файлов конфигурации, в каждом из которых были десятки параметров.

Каждый новый билд, каждая новая версия вносили свои изменения в конфигурацию. Старые файлы конфигурации нельзя было использовать, так как они часто были несовместимы с новой версией. В итоге каждый раз перед разворачиванием билда для теста или на рабочих машинах тестеров, приходилось тратить уйму времени на конфигурирование программы, исправление ошибок конфигурации, постоянные консультации с разработчиками по теме «а почему это теперь не так работает»? В общем, процесс был крайне не оптимизирован.

В помощь при настройке у нас была инструкция на 117 страниц шрифтом Arial размером 9. Читать приходилось очень-очень внимательно. Иногда казалось, что проще собрать ядро линукс с закрытыми глазами на выключенном компьютере.

Стало понятно, что без оптимизации здесь не обойтись. Я начал писать свой конфигуратор для программы с поддержкой профилей и возможностью менять параметры за несколько секунд, но проект подошел к своей финальной стадии, и я перешел на работу в другой проект. В нем мы анализировали множество логов одной биллинговой системы на предмет возможных багов в работе серверной части. От чудовищного объема ручной работы меня спасла автоматизация многих действий с помощью языка Python. Мне очень понравился этот скриптовый язык, и с его помощью мы сделали набор скриптов анализа на все случаи жизни. Те задачи, которые требовали несколько дней вдумчивого анализа по схеме «cat logfile123 | grep something_special», занимали считанные минуты. Все стало здорово… и скучно.

Configuration Management — новые приключения


В компанию Veeam я пришел как тимлид небольшой СM-команды. Множество процессов требовало автоматизации, оптимизации, переосмысления. Зато предоставлялась полная свобода в выборе инструментов! Разработчик обязан использовать определенный язык программирования, код-стайл, определенный набор библиотек. СМ же может вообще ничего не использовать для решения поставленной задачи, если у него хватит на это времени, смелости и терпения.

У Veeam, как и у многих других компаний, существует задача сборки апдейтов для продуктов. В апдейт входили сотни файлов, и менять надо было только те, которые изменились, учитывая еще ряд важных условий. Для этого создали объемный powershell скрипт, который умел лезть в TFS, делать выборку файлов, раскладывать их по нужным папочкам. Функциональность скрипта дополнялась, он постепенно стал огромным, требовал кучу времени на отладку и постоянно каких-то костылей в придачу. Надо было срочно что-то делать.

Что хотели разработчики


Вот к чему сводились основные жалобы:

  • Невозможно поставить фиксы в очередь. Приходится постоянно проверять веб-страницу, чтобы увидеть, когда закончится сборка приватного фикса и можно будет запустить сборку своего.
  • Нет нотификаций об ошибках — чтобы посмотреть ошибки в GUI приложения сборки, приходится заходить на сервер и смотреть множество объемных логов.
  • Нет истории сборки приватных фиксов.


Нужно было разобраться с этими задачами и добавить приятных мелочей, от которых разработчики тоже бы не отказались.

Что такое приватные фиксы


Приватный фикс в контексте нашей разработки — это определенный набор исправлений в коде, который сохраняется в шелвсете (shelveset) Team Foundation Server для релизной ветки. Небольшое разъяснение для тех, кто не слишком знаком с терминологией TFS:

  • check-in — набор локальных изменений в исходном коде, который вносится в код, хранящийся в TFS. Данный чекин может проверяться с помощью Continuous Integration/Gated Check-in процессов, позволяющих пропускать только корректный код и отклонять все чекины, нарушающие собираемость конечного проекта.
  • shelveset — набор локальных изменений в исходном коде, который не вносится непосредственно в исходный код, находящийся в TFS, но доступен по его имени. Шелвсет может быть развернут на локальной машине разработчика или билд-системы для работы с измененным кодом, который не внесен в TFS. Также шелвсет может быть добавлен в TFS как чекин после разворачивания, когда все работы с ним будут завершены. К примеру, так работает гейтед-чекин. Сначала проверяется шелвсет на билдере. Если проверка проходит успешно, шелвсет превращается в чекин!


Вот что делает билдер приватных фиксов:

  1. Получает название (номер) шелвсета и разворачивает его на билдере приватных фиксов. В итоге мы получаем исходный код релизного продукта плюс изменения/фиксы из шелвсета. Релизная ветка остается без изменений.
  2. На билдере приватных фиксов собирается проект или ряд проектов, для которых был выполнен приватный фикс.
  3. Набор скомпилированных бинарных файлов копируется в сетевой каталог приватного фикса. Каталог содержит в себя имя шелвсета, которое представляет собой последовательность чисел.
  4. Исходный код на билдере приватных фиксов приводится к первоначальному виду.


Для удобства разработчиков используется веб-интерфейс, где можно указать продукт, для которого надо собрать приватный фикс, указать номер шелвсета, выбрать проекты, для которых требуется собрать приватный фикс, и добавить сборку фикса в очередь. На скриншоте ниже приведен финальный рабочий вариант веб-приложения, где отображается текущий статус билда, очередь приватных фиксов и история их сборки. В нашем примере рассматривается только организация очереди сборки приватных фиксов.

Что было у меня


  • Билдер приватных фиксов, который собирал приватные фиксы из шелвсетов TFS с помощью запуска консольного приложения с заданными параметрами командной строки.
  • Veeam.Builder.Agent — написанный в компании Veeam WCF-сервис, который запускает приложение с параметрами в консольном режиме под текущим пользователем и возвращает текущий статус работы приложения.
  • IIS веб-сервис — приложение на Windows Forms, которое позволяло ввести имя шелвсета, заданные параметры и запустить процесс сборки приватного фикса.
  • Весьма неглубокие знания в программировании — C++, немного C# в университете и написание небольших приложений для автоматизации, добавления новых функций в текущие билд-процессы и в качестве хобби.
  • Опытные коллеги, Google и индийские статьи на MSDN — источники ответов на все вопросы.


Что будем делать


В этой статье я расскажу, как реализовал постановку сборки фиксов в очередь и последовательный их запуск на билдере. Вот из каких частей будет состоять решение:

  • QBuilder.AppQueue — мой WCF-сервис, обеспечивающий работу с очередью сборки и вызывающий сервис Veeam.Builder.Agent для запуска программы сборки.
  • dummybuild.exe — программа-заглушка, используемая для отладки и в качестве наглядного пособия. Нужна для визуализации передаваемых параметров.
  • QBuilder.AppLauncher — WCF-сервис, который запускает приложения в консоли текущего пользователя и работает в интерактивном режиме. Это значительно упрощенный, написанный специально для этой статьи аналог программы Veeam.Builder.Agent. Оригинальный сервис умеет работать как windows-сервис и запускать приложения в консоли, что требует дополнительной работы с Windows API. Чтобы описать все ухищрения, потребовалась бы отдельная статья. Мой же пример работает как простой интерактивный консольный сервис и использует две функции — запуск процесса с параметрами и проверку его состояния.


Дополнительно создали новое удобное веб-приложение, которое умеет работать с несколькими билдерами и вести логи событий. Чтобы не перегружать статью, подробно рассказывать о нем мы тоже пока не будем. Кроме этого, в этой статье не приведена работа с TFS, с историей хранений собранных приватных фиксов и различные вспомогательные классы и функции.

d3cc136b9f661a26314f2a44eff3a4b3.jpg

Создание WCF-сервисов


Есть много подробных статей, описывающих создание WCF-сервисов. Мне больше всех понравился материал с сайта Microsoft. Его я взял за основу при разработке. Чтобы облегчить знакомство с проектом, я дополнительно выложил бинарники. Начнем!

Создаем сервис QBuilder.AppLauncher


Здесь у нас будет только первичная болванка сервиса. На данном этапе нам нужно убедиться, что сервис запускается и работает. Кроме этого, код идентичен как для QBuilder.AppLauncher, так и для QBuilder.AppQueue, поэтому этот процесс необходимо будет повторить два раза.

  1. Создаем новое консольное приложение с именем QBuilder.AppLauncher
  2. Переименовываем Program.cs в Service.cs
  3. Переименовываем namespace в QBuilder.AppLauncher
  4. Добавляем следующие референсы в проект:
    a. System.ServiceModel.dll
    b. System.ServiceProcess.dll
    c. System.Configuration.Install.dll
  5. Добавляем следующие определения в Service.cs
    using System.ComponentModel;
    using System.ServiceModel;
    using System.ServiceProcess;
    using System.Configuration;
    using System.Configuration.Install;
    

    В процессе дальнейшей сборки также понадобятся следующие определения:
    using System.Reflection;
    using System.Xml.Linq;
    using System.Xml.XPath;
    
  6. Определяем интерфейс IAppLauncher и добавляем функции для работы с очередью:
    // Определяем сервис контракт
    [ServiceContract(Namespace = "http://QBuilder.AppLauncher")]
        public interface IAppLauncher
        {
            
         // Добавляем функцию для проверки работы сервиса
            [OperationContract]
            bool TestConnection();
        }
    
    
  7. В классе AppLauncherService имплементируем интерфейс и тестовую функцию TestConnection:
    public class AppLauncherService : IAppLauncher
        {
            public bool TestConnection()
            {
                return true;
            }    
        }
    
    
  8. Создаем новый класс AppLauncherWindowsService, который наследует ServiceBase класс. Добавляем локальную переменную serviceHost — ссылку на ServiceHost. Определяем метод Main, который вызывает ServiceBase.Run (new AppLauncherWindowsService ()):
    public class AppLauncherWindowsService : ServiceBase
        {
            public ServiceHost serviceHost = null;
            public AppLauncherWindowsService()
            {
                // Name the Windows Service
                ServiceName = "QBuilder App Launcher";
            }
     
            public static void Main()
            {
                ServiceBase.Run(new AppLauncherWindowsService());
            }
    
    
  9. Переопределяем функцию OnStart (), создающую новый экземпляр ServiceHost:
    protected override void OnStart(string[] args)
            {
                if (serviceHost != null)
                {
                    serviceHost.Close();
                }
     
                // Create a ServiceHost for the CalculatorService type and 
                // provide the base address.
                serviceHost = new ServiceHost(typeof(AppLauncherService));
     
                // Open the ServiceHostBase to create listeners and start 
                // listening for messages.
                serviceHost.Open();
            }
    
    
  10. Переопределяем функцию onStop, закрывающую экземпляр ServiceHost:
    protected override void OnStop()
            {
                if (serviceHost != null)
                {
                    serviceHost.Close();
                    serviceHost = null;
                }
            }
        }
    
    
  11. Создаем новый класс ProjectInstaller, наследуемый от Installer и отмеченный RunInstallerAttribute, который установлен в True. Это позволяет установить Windows-сервис с помощью программы installutil.exe:
    [RunInstaller(true)]
        public class ProjectInstaller : Installer
        {
            private ServiceProcessInstaller process;
            private ServiceInstaller service;
     
            public ProjectInstaller()
            {
                process = new ServiceProcessInstaller();
                process.Account = ServiceAccount.LocalSystem;
                service = new ServiceInstaller();
                service.ServiceName = "QBuilder App Launcher";
                Installers.Add(process);
                Installers.Add(service);
            }
        }
    
    
  12. Меняем содержимое файла app.config:
    
    
      
        
          
            
              
                
              
            
            
            
          
        
        
          
            
              
              
            
          
        
      
    
    


Проверяем работоспособность сервиса


  1. Компилируем сервис.
  2. Устанавливаем его командой installutil.exe
    1) Переходим в папку, где лежит скомпилированный файл сервиса
    2) Запускаем команду установки:
    C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe
  3. Заходим в оснастку services.msc, проверяем наличие сервиса «QBuilder App Launcher» и запускаем его.
  4. Работоспособность сервиса проверяем с помощью программы WcfTestClient.exe, которая входит в поставку VisualStudio:

    1) Запускаем WcfTestClient
    2) Добавляем адрес сервиса: http://localhost:8000/QBuilderAppLauncher/service
    3) Открывается интерфейс сервиса:

    f32d3835e5b7ddcb399f7e48b3028df9.png

    4) Вызываем тестовую функцию TestConnection, проверяем, что все работает и функция возвращает значение:

    00e96a46098ebee72c99b32357930759.png


Теперь, когда получен рабочая болванка сервиса, добавляем необходимые нам функции.

Зачем мне нужна тестовая функция, которая ничего не делает


Когда я начал изучать, как написать WCF-сервис с нуля, я прочитал кучу статей по этой теме. На столе у меня лежал десяток-другой распечатанных листов, по которым я разбирался, что и как. Признаюсь, сразу запустить сервис у меня не получилось. Я потратил кучу времени и пришел к выводу, что сделать болванку сервиса действительно важно. С ней вы будете уверены, что все работает и можно приступать к реализации необходимых функций. Подход может показаться расточительным, но он облегчит жизнь, если куча написанного кода не заработает как надо.

Добавляем возможность запуска из консоли


Вернемся к приложению. На этапе отладки и в ряде других случаев требуется запуск сервиса в виде консольного приложения без регистрации в виде сервиса. Это очень полезная функция, позволяющая обойтись без утомительного использования дебаггеров. Именно в таком режиме работает сервис QBuilder.AppLauncher. Вот как ее реализовать:

  1. Добавляем в класс AppLauncherWindowsService процедуру RunInteractive, обеспечивающую работу сервиса в консольном режиме:
    
    static void RunInteractive(ServiceBase[] services)
    {
        Console.WriteLine("Service is running in interactive mode.");
        Console.WriteLine();
    
        var start = typeof(ServiceBase).GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
    
        foreach (var service in services)
        {
            Console.Write("Starting {0}...", service.ServiceName);
            start.Invoke(service, new object[] { new string[] { } });
            Console.Write("Started {0}", service.ServiceName);
        }
    
        Console.WriteLine();
        Console.WriteLine("Press any key to stop the services and end the process...");
        Console.ReadKey();
        Console.WriteLine();
    
        var stop = typeof(ServiceBase).GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
    
        foreach (var service in services)
        {
            Console.Write("Stopping {0}...", service.ServiceName);
            stop.Invoke(service, null);
            Console.WriteLine("Stopped {0}", service.ServiceName);
        }
    
        Console.WriteLine("All services stopped.");
    }
    
    
  2. Вносим изменения в процедуру Main — добавляем обработку параметров командной строки. При наличии параметра /console и открытой активной сессии пользователя — запускаем программу в интерактивном режиме. В ином случае — запускаем как сервис.
    
    public static void Main(string[] args)
    {
        var services = new ServiceBase[]
        {
            new AppLauncherWindowsService()
        };
     
        // Добавляем возможность запуска сервиса в интерактивном режиме в виде консольного приложения, если есть параметр командной строки /console
        if (args.Length == 1 && args[0] == "/console" && Environment.UserInteractive)
        {
            // Запускаем в виде интерактивного приложения
            RunInteractive(services);
        }
        else
        {
            // Запускаем как сервис
            ServiceBase.Run(services);
        }
    }
    
    


Добавляем функции запуска приложения и проверки его статуса


Сервис сделан предельно простым, здесь нет никаких дополнительных проверок. Он умеет запускать приложения только в консольном варианте и от имени администратора. Он может запустить их и как сервис –, но вы их не увидите, они будут крутиться в фоновом режиме и вы сможете увидеть их только через Task Manager. Все это можно реализовать, но это тема для отдельной статьи. Здесь для нас главное — наглядный рабочий пример.

  1. Для начала добавляем глобальную переменную appProcess, хранящую в себе текущий запущенный процесс.

    Добавляем ее в класс public class AppLauncherService : IAppLauncher:

    public class AppLauncherService : IAppLauncher
        {
            Process appProcess;
    
    
  2. Добавляем в этот же класс функцию, проверяющую статус запущенного процесса:
       public bool IsStarted()
            {
                if (appProcess!=null)
                {
                    if (appProcess.HasExited)
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return false;
                }
            }
    
    

    Функция возвращает false, если процесс не существует или уже не запущен, и true — если процесс активен.
  3. Добавляем функцию запуска приложения:
    public bool Start(string fileName, string arguments, string workingDirectory, string domain, string userName, int timeoutInMinutes)
            {
                ProcessStartInfo processStartInfo = new ProcessStartInfo();
     
                processStartInfo.FileName = fileName;
                processStartInfo.Arguments = arguments;
                processStartInfo.Domain = domain;
                processStartInfo.UserName = userName;
                processStartInfo.CreateNoWindow = false;
                processStartInfo.UseShellExecute = false;
     
                try
                {
                    if (appProcess!=null)
                    {
                        if (!appProcess.HasExited)
                        {
                            Console.WriteLine("Process is still running. Waiting...");
                            return false;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error while checking process: {0}", ex);
                }
     
                try
                {
                    appProcess = new Process();
                    appProcess.StartInfo = processStartInfo;
                    appProcess.Start();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error while starting process: {0}",ex);
                }
     
                return true;
                         
            }
    
    


Функция запускает любое приложение с параметрами. Параметры Domain и Username в данном контексте не используются и могут быть пустыми, так как сервис запускает приложение из консольной сессии с правами администратора.

Запуск сервиса QBuilder.AppLauncher


Как ранее описывалось, данный сервис работает в интерактивном режиме и позволяет запускать приложения в текущей сессии пользователя, проверяет, запущен ли процесс или уже завершен.

  1. Для работы необходимы файлы QBuilder.AppLauncher.exe и QBuilder.AppLauncher.exe.config, которые находятся в архиве по ссылке выше. Там же расположен исходный код данного приложения для самостоятельной сборки.
  2. Запускаем сервис с правами администратора.
  3. Откроется консольное окно сервиса:


981e79eb4757690818462d72cbba5cc4.png

Любое нажатие клавиши в консоли сервиса закрывает его, будьте внимательны.

  1. Для тестов запускаем wcftestclient.exe, входящий в поставку Visual Studio. Проверяем доступность сервиса по адресу http://localhost:8000/QBuilderAppLauncher/service или открываем ссылку в Internet Explorer.


Если все работает, переходим к следующему этапу.

Создаем сервис QBuilder.AppQueue


А теперь перейдем к самому главному сервису, ради чего и писалась вся эта статья! Повторяем последовательность действий в главе «Создаем сервис QBuilder.AppLauncher» и в главе «Добавляем возможность запуска из консоли», заменяя в коде AppLauncher на AppQueue.

Добавляем ссылку на сервис QBuilder.AppLauncher для использования в сервисе очереди


  1. В Solution Explorer для нашего проекта выбираем Add Service Reference и указываем адрес: localhost:8000/QBuilderAppLauncher/service
  2. Выбираем имя namespace: AppLauncherService.


Теперь мы можем обращаться к интерфейсу сервиса из своей программы.

Создаем структуру хранения элементов очереди


В namespace QBuilder.AppQueue добавляем класс QBuildRecord:

// Структура, где хранится элемент очереди
    public class QBuildRecord
    {
        // ID билда
        public string BuildId { get; set; }
        // ID задачи
        public string IssueId { get; set; }
        // Название проблемы
        public string IssueName { get; set; }
        // Время начало билда
        public DateTime StartDate { get; set; }
        // Время завершения билда
        public DateTime FinishDate { get; set; }
        // Флаг сборки компонентов C#
        public bool Build_CSharp { get; set; }
        // Флаг сборки компонентов C++
        public bool Build_Cpp { get; set; }         
    }


Имплементируем класс работы с очередью CXmlQueue


Добавим в наш проект класс CXmlQueue.cs, где будет находиться ряд процедур работы с XML-файлом:

  • Конструктор CXmlQueue — задает при инициализации имя файла, где хранится очередь.
  • SetCurrentBuild — записывает информацию о текущем билде в XML-файл очереди. Это элемент, не входящий в очередь, в нем хранится информация о текущем запущенном процессе. Может быть пустым.
  • GetCurrentBuild — получает параметры запущенного процесса из XML-файла очереди. Может быть пустым.
  • ClearCurrentBuild — это очистка элемента currentbuild в XML-файле очереди, если процесс завершился.
  • OpenXmlQueue — функция открытия XML-файла, где хранится очередь. Если файл отсутствует, то создается новый.
  • GetLastQueueBuildNumber — каждый билд в очереди имеет свой уникальный последовательный номер. Данная функция возвращает его значение, которое хранится в root-атрибуте.
  • IncrementLastQueueBuildNumber — увеличивает значение номера билда при постановке нового билда в очередь.
  • GetCurrentQueue — возвращает список элементов QBuildRecord из XML-файла очереди.


В оригинальном коде все эти процедуры были размещены в основном классе, но для наглядности я сделал отдельный класс CXmlQueue. Класс создается в пространстве имен namespace QBuilder.AppQueue, проверьте, что указаны все необходимые определения:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
using System.IO;
 
namespace QBuilder.AppQueue
{
. . . 
}


Итак, имплементируем. Непосредственно класс CXmlQueue:

Нажмите, чтобы раскрыть спойлер с кодом
// Класс работы с очередью в XML файле
    public class CXmlQueue
    {
        // Имя файла, где хранится очередь
        string xmlBuildQueueFile;
 
        public CXmlQueue(string _xmlQueueFile)
        {
            xmlBuildQueueFile = _xmlQueueFile;
        }
 
        public string GetQueueFileName()
        {
            return xmlBuildQueueFile;
        }
 
        // Функция, получающая параметры запущенного процесса из файла xml (отдельная запись в xml)
        public QBuildRecord GetCurrentBuild()
        {
            QBuildRecord qBr;
 
            XElement xRoot = OpenXmlQueue();
            XElement xCurrentBuild = xRoot.XPathSelectElement("currentbuild");
 
            if (xCurrentBuild != null)
            {
                qBr = new QBuildRecord();
 
                qBr.BuildId = xCurrentBuild.Attribute("BuildId").Value;
                qBr.IssueId = xCurrentBuild.Attribute("IssueId").Value;             
                qBr.StartDate = Convert.ToDateTime(xCurrentBuild.Attribute("StartDate").Value);         
 
                return qBr;
            }
 
            return null;
        }
 
        // Функция, устанавливающая параметры запущенного процесса из файла xml (отдельная запись в xml)
        public void SetCurrentBuild(QBuildRecord qbr)
        {
            XElement xRoot = OpenXmlQueue();
 
            XElement newXe = (new XElement(
                "currentbuild",
                new XAttribute("BuildId", qbr.BuildId),
                new XAttribute("IssueId", qbr.IssueId),              
                new XAttribute("StartDate", DateTime.Now.ToString())          
                ));
 
            XElement xCurrentBuild = xRoot.XPathSelectElement("currentbuild");
 
            if (xCurrentBuild != null)
            {
                xCurrentBuild.Remove(); // remove old value
            }
 
            xRoot.Add(newXe);
            xRoot.Save(xmlBuildQueueFile);
        }
 
        // Функция, обнуляющая параметры запущенного процесса из файла xml, в случае, когда процесс закончился
        public void ClearCurrentBuild()
        {
            XElement xRoot = OpenXmlQueue();
 
            try
            {
                XElement xCurrentBuild = xRoot.XPathSelectElement("currentbuild");
 
                if (xCurrentBuild != null)
                {
                    Console.WriteLine("Clearing current build information.");
                    xCurrentBuild.Remove();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("XML queue doesn't have running build yet. Nothing to clear!");
            }
 
            xRoot.Save(xmlBuildQueueFile);
        }
 
        // Функция открытия XML очереди из файла и его создания в случае его отсутствия
        public XElement OpenXmlQueue()
        {
            XElement xRoot;
 
            if (File.Exists(xmlBuildQueueFile))
            {
                xRoot = XElement.Load(xmlBuildQueueFile, LoadOptions.None);
            }
            else
            {
                Console.WriteLine("Queue file {0} not found. Creating...", xmlBuildQueueFile);
                XElement xE = new XElement("BuildsQueue", new XAttribute("BuildNumber", 0));
                xE.Save(xmlBuildQueueFile);
 
                xRoot = XElement.Load(xmlBuildQueueFile, LoadOptions.None);
            }
            return xRoot;
        }
 
        // Получение номера последнего элемента в очереди
        public int GetLastQueueBuildNumber()
        {
            XElement xRoot = OpenXmlQueue();
            if (xRoot.HasAttributes)
                return int.Parse(xRoot.Attribute("BuildNumber").Value);
            return 0;
        }
 
        // Увеличение номера последнего элемента в очереди в случае добавления новых элементов в очередь
        public int IncrementLastQueueBuildNumber()
        {
            int buildIndex = GetLastQueueBuildNumber();
            buildIndex++;
 
            XElement xRoot = OpenXmlQueue();
            xRoot.Attribute("BuildNumber").Value = buildIndex.ToString();
            xRoot.Save(xmlBuildQueueFile);
            return buildIndex;
        }
 
        // Выгрузка очереди из xml файла в виде списка QBuildRecord
        public List GetCurrentQueue()
        {
            List qList = new List();
 
            XElement xRoot = OpenXmlQueue();
 
            if (xRoot.XPathSelectElements("build").Any())
            {
                List xBuilds = xRoot.XPathSelectElements("build").ToList();
 
                foreach (XElement xe in xBuilds)
                {
                    qList.Add(new QBuildRecord
                    {
                        BuildId = xe.Attribute("BuildId").Value,
                        IssueId = xe.Attribute("IssueId").Value,
                        IssueName = xe.Attribute("IssueName").Value,
                        StartDate = Convert.ToDateTime(xe.Attribute("StartDate").Value),
                        Build_CSharp = bool.Parse(xe.Attribute("Build_CSharp").Value),
                        Build_Cpp = bool.Parse(xe.Attribute("Build_Cpp").Value)
                    });
 
                }
            }
 
            return qList;
        }
 
    }


Очередь в XML-файле выглядит следующим образом:



  
  
  
  
  
  
  
  
  
  
  



Создайте файл BuildQueue.xml с данным содержимым и положите в каталог с исполняемым файлом. Данный файл будет использоваться в тестовой отладке для соответствия тестовых результатов.

Добавляем класс AuxFunctions


В данном классе я размещаю вспомогательные функции. Сейчас здесь находится только одна функция, FormatParameters, которая выполняет форматирование параметров для передачи их в консольное приложение с целью запуска. Листинг файла AuxFunctions.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace QBuilder.AppQueue
{
    class AuxFunctions
    {   
        // Функция формирования параметров для запуска приложения
        public static string FormatParameters(string fileName, IDictionary parameters)
        {
            if (String.IsNullOrWhiteSpace(fileName))
            {
                throw new ArgumentNullException("fileName");
            }
 
            if (parameters == null)
            {
                throw new ArgumentNullException("parameters");
            }
 
            var macros = String.Join(" ", parameters.Select(parameter => String.Format("\"{0}={1}\"", parameter.Key, parameter.Value.Replace(@"""", @"\"""))));
            return String.Format("{0} /b \"{1}\"", macros, fileName);
        }
    }
}


Добавляем новые функции в интерфейс сервиса


Тестовую функцию TestConnection на данном этапе можно удалить. Для реализации работы очереди мне потребовался следующий набор функций:

  • PushBuild (QBuildRecord): void. Это функция, добавляющая в XML-файл очереди новое значение с параметрами QBuildRecord
  • TestPushBuild (): void. Это тестовая функция, добавляющая тестовые данные в очередь в XML-файле.
  • PullBuild: QBuildRecord. Это функция, получающая значение QBuildRecord из XML-файла очереди. Он может быть пустым.


Интерфейс будет вот таким:

    public interface IAppQueue
    {
        // Функция добавления в очередь
        [OperationContract]
        void PushBuild(QBuildRecord qBRecord);
 
        // Тестовое добавление в очередь
        [OperationContract]
        void TestPushBuild();
 
        // Функция получения элемента из очереди
        [OperationContract]
        QBuildRecord PullBuild();
    }


Имплементируем функции в классе AppQueueService: IAppQueue:

Нажмите чтобы раскрыть спойлер с кодом
public class AppQueueService : IAppQueue
    {
 
        // Сервис агента, запускающего консольные приложения
        public AppLauncherClient buildAgent;
        
        // Переменная, где хранится имя файла очереди
        private string _xmlQueueFile;
 
        public AppQueueService()
        {
            // Получаем значение файла очереди из конфиг файла. Это не самое лучшее решение, я знаю.
            _xmlQueueFile = ConfigurationManager.AppSettings["QueueFileName"];
        }
 
        public QBuildRecord PullBuild()
        {
            QBuildRecord qBr;
 
            CXmlQueue xmlQueue = new CXmlQueue(_xmlQueueFile);
 
            XElement xRoot = xmlQueue.OpenXmlQueue();
 
            if (xRoot.XPathSelectElements("build").Any())
            {
                qBr = new QBuildRecord();
 
                XElement xe = xRoot.XPathSelectElements("build").FirstOrDefault();
 
                qBr.BuildId = xe.Attribute("BuildId").Value;
                qBr.IssueId = xe.Attribute("IssueId").Value;
                qBr.IssueName = xe.Attribute("IssueName").Value;
            
                qBr.StartDate = Convert.ToDateTime(xe.Attribute("StartDate").Value);
                qBr.Build_CSharp = bool.Parse(xe.Attribute("Build_CSharp").Value);
                qBr.Build_Cpp = bool.Parse(xe.Attribute("Build_Cpp").Value); 
                xe.Remove(); // Remove first element
                xRoot.Save(xmlQueue.GetQueueFileName());
                return qBr;
            }
            return null;
        }
 
        public void PushBuild(QBuildRecord qBRecord)
        {
            CXmlQueue xmlQueue = new CXmlQueue(_xmlQueueFile);
 
            XElement xRoot = xmlQueue.OpenXmlQueue();
 
            xRoot.Add(new XElement(
                "build",
                new XAttribute("BuildId", qBRecord.BuildId),
                new XAttribute("IssueId", qBRecord.IssueId),
                new XAttribute("IssueName", qBRecord.IssueName),            
                new XAttribute("StartDate", qBRecord.StartDate),
                new XAttribute("Build_CSharp", qBRecord.Build_CSharp),
                new XAttribute("Build_Cpp", qBRecord.Build_Cpp) 
                ));
 
            xRoot.Save(xmlQueue.GetQueueFileName());
        }
 
        public void TestPushBuild()
        {
            CXmlQueue xmlQueue = new CXmlQueue(_xmlQueueFile);
 
            Console.WriteLine("Using queue file: {0}",xmlQueue.GetQueueFileName());
 
            int buildIndex = xmlQueue.IncrementLastQueueBuildNumber();
 
            Random rnd = new Random();
 
            PushBuild
                (new QBuildRecord
                {
                    Build_CSharp = true,                 
                    Build_Cpp = true,                   
                    BuildId = buildIndex.ToString(),
                    StartDate = DateTime.Now,
                    IssueId = rnd.Next(100000).ToString(),
                    IssueName = "TestIssueName"               
                }
                );
        }
 
    }


Вносим изменения в класс AppQueueWindowsService: ServiceBase


Добавляем новые переменные в тело класса:

// Таймер, необходимый для обращения к очереди через определенный интервал
        private System.Timers.Timer timer; 
 
        // Переменная, в которой информация о запущенном процессе
        public QBuildRecord currentBuild;
 
        //public QBuildRecord processingBuild;
 
        // Переменная, где будет хранится статус клиентского сервиса
        public bool clientStarted;        
 
        // Имя файла очереди
        public string xmlBuildQueueFileName;
 
        // Класс очереди
        public CXmlQueue xmlQueue;
 
        // Строковые переменные для запуска процесса в клиентском сервисе
        public string btWorkingDir;
        public string btLocalDomain;
        public string btUserName;
        public string buildToolPath;
        public string btScriptPath;
        public int agentTimeoutInMinutes;
 
        // Очередь
        public AppQueueService buildQueueService;


В конструктор AppQueueWindowsService () добавляем функции для чтения файла конфигурации, инициализации сервисов и классов очереди:

// Считываем параметры из файла конфигурации и задаем начальные параметры
            try
            {
                xmlBuildQueueFileName = ConfigurationManager.AppSettings["QueueFileName"];
                buildToolPath = ConfigurationManager.AppSettings["BuildToolPath"];
                btWorkingDir = ConfigurationManager.AppSettings["BuildToolWorkDir"];
                btLocalDomain = ConfigurationManager.AppSettings["LocalDomain"];
                btUserName = ConfigurationManager.AppSettings["UserName"];
                btScriptPath = ConfigurationManager.AppSettings["ScriptPath"];
                agentTimeout= 30000;
 
                // Инициализируем сервис очереди
                buildQueueService = new AppQueueService();
 
                // Инициализируем класс очереди
                xmlQueue = new CXmlQueue(xmlBuildQueueFileName);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error while loading configuration: {0}", ex);
            }


AgentTimeout — частота срабатывания таймера. Указывается в миллисекундах. Здесь мы задаем, что таймер должен срабатывать каждые 30 секунд. В оригинале данный параметр находится в файле конфигурации. Для статьи я его решил задавать в коде.

Добавляем в класс функцию проверки запущенного билд-процесса:

// Функция проверки запущенного приложения в агентском сервисе
        public bool BuildIsStarted()
        {
            IAppLauncher builderAgent;
 
            try
            {
                builderAgent = new AppLauncherClient();
 
                return builderAgent.IsStarted();
            }
            catch (Exception ex)
            {
                return false;
            }
        }


Добавляем процедуру работы с таймером:

 private void TimerTick(object sender, System.Timers.ElapsedEventArgs e) 
        {
            try
            {
                // Если билд не запущен
                if (!BuildIsStarted())
                {
                    // Проверяем значение булевой переменой clientStarted, показывающей статус нашего приложения
                    if (clientStarted)
                    {
                        // Если приложение уже завершило работу, устанавливаем clientStarted в false и присваиваем дату завершения процесса
                        currentBuild.FinishDate = DateTime.Now;
                        clientStarted = false;
                    }
                    else
                    {
                        // Если приложение уже не работает и clientStarted=false (статус приложения) - очищаем информацию о текущем билде
                        xmlQueue.ClearCurrentBuild();
                    }
 
                    // Достаем из очереди информацию об очередном билде
                    currentBuild = buildQueueService.PullBuild();
 
                    // Если значение не нулевое, начинаем работу с билдом
                    if (currentBuild != null)
                    {
                        // Статус клиента меняем на true - клиент в работе
                        clientStarted = true;
                        // Присваиваем значение currentbuild - данная информация отображается в xml и используется в веб приложения для отображения информации о текущем билде
                        xmlQueue.SetCurrentBuild(currentBuild);                      
 
                        // Формируем список параметров командной строки
                        var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase)
                        {
                            {"BUILD_ID", currentBuild.BuildId},
                            {"ISSUE_ID", currentBuild.IssueId},
                            {"ISSUE_NAME", currentBuild.IssueName},                         
                            {"BUILD_CSHARP", currentBuild.Build_CSharp ? "1" : "0"},
                            {"BUILD_CPP", currentBuild.Build_Cpp ? "1" : "0"}                        
                        };
 
                        // Форматируем список параметров для нашей программы
                        var arguments = AuxFunctions.FormatParameters(btScriptPath, parameters);
                        
                        try
                        {
                            // Запускаем нашу программу с параметрами командной строки через сервис AppLauncher
                            IAppLauncher builderAgent = new AppLauncherClient();
                            builderAgent.Start(buildToolPath, arguments, btWorkingDir, btLocalDomain, btUserName, agentTimeout);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex);
                        }
                    }
                }
            }
            catch (Exception ex)
            {              
                Console.WriteLine(ex);
            }
        }


Вносим изменения в функцию OnStart, добавляем функцию работы с таймером:

// Переопределяем процедуру запуска сервиса OnStart
        protected override void OnStart(string[] args)
        {
            if (serviceHost != null)
            {
                serviceHost.Close();
            }
 
            // Добавляем функционал работы с таймером
            this.timer = new System.Timers.Timer(agentTimeout);  // указывается в миллисекундах
            this.timer.AutoReset = true;
   
    
            

© Habrahabr.ru