- PVSM.RU - https://www.pvsm.ru -
Работа в команде Configuration Management связана с обеспечением функциональности билд-процессов — сборки продуктов компании, предварительной проверки кода, статистического анализа, ведения документации и многого другого. Помимо этого, мы постоянно работаем над оптимизацией различных процессов, и, что замечательно, мы практически свободны в выборе инструментов для этой интересной работы. Далее я подробно расскажу о том, как, обладая лишь разного уровня знаниями в C# и C++, я сделал функциональный WCF-сервис для работы с очередями фиксов. И почему решил, что это очень важно.
Небольшое лирическое отступление, чтобы вы поняли, почему я так переживаю из-за автоматизации и оптимизации процессов.
До Veeam я работал в крупной международной компании — был тимлидом команды Configuration Management, занимался сборкой приложения и развертыванием его на тестовых окружениях. Программа успешно разрабатывалась, добавлялись новые функции, писалась документация, поддержкой которой я тоже занимался. Но меня всегда удивляло, почему у такой серьезной программы нет нормальной системы конфигурации параметров, которых были многие десятки, если не сотни.
Я общался на эту тему с разработчиками и получил ответ – заказчик не оплатил эту фичу, не согласовал ее стоимость, поэтому фича не была реализована. А по факту страдали QA и непосредственно мы, команда СМ. Конфигурация программы и ее предварительная настройка осуществлялась через множество файлов конфигурации, в каждом из которых были десятки параметров.
Каждый новый билд, каждая новая версия вносили свои изменения в конфигурацию. Старые файлы конфигурации нельзя было использовать, так как они часто были несовместимы с новой версией. В итоге каждый раз перед разворачиванием билда для теста или на рабочих машинах тестеров, приходилось тратить уйму времени на конфигурирование программы, исправление ошибок конфигурации, постоянные консультации с разработчиками по теме «а почему это теперь не так работает»? В общем, процесс был крайне не оптимизирован.
В помощь при настройке у нас была инструкция на 117 страниц шрифтом Arial размером 9. Читать приходилось очень-очень внимательно. Иногда казалось, что проще собрать ядро линукс с закрытыми глазами на выключенном компьютере.
Стало понятно, что без оптимизации здесь не обойтись. Я начал писать свой конфигуратор для программы с поддержкой профилей и возможностью менять параметры за несколько секунд, но проект подошел к своей финальной стадии, и я перешел на работу в другой проект. В нем мы анализировали множество логов одной биллинговой системы на предмет возможных багов в работе серверной части. От чудовищного объема ручной работы меня спасла автоматизация многих действий с помощью языка Python. Мне очень понравился этот скриптовый язык, и с его помощью мы сделали набор скриптов анализа на все случаи жизни. Те задачи, которые требовали несколько дней вдумчивого анализа по схеме «cat logfile123 | grep something_special», занимали считанные минуты. Все стало здорово… и скучно.
В компанию Veeam я пришел как тимлид небольшой СM-команды. Множество процессов требовало автоматизации, оптимизации, переосмысления. Зато предоставлялась полная свобода в выборе инструментов! Разработчик обязан использовать определенный язык программирования, код-стайл, определенный набор библиотек. СМ же может вообще ничего не использовать для решения поставленной задачи, если у него хватит на это времени, смелости и терпения.
У Veeam, как и у многих других компаний, существует задача сборки апдейтов для продуктов. В апдейт входили сотни файлов, и менять надо было только те, которые изменились, учитывая еще ряд важных условий. Для этого создали объемный powershell скрипт, который умел лезть в TFS, делать выборку файлов, раскладывать их по нужным папочкам. Функциональность скрипта дополнялась, он постепенно стал огромным, требовал кучу времени на отладку и постоянно каких-то костылей в придачу. Надо было срочно что-то делать.
Вот к чему сводились основные жалобы:
Нужно было разобраться с этими задачами и добавить приятных мелочей, от которых разработчики тоже бы не отказались.
Приватный фикс в контексте нашей разработки — это определенный набор исправлений в коде, который сохраняется в шелвсете (shelveset) Team Foundation Server для релизной ветки. Небольшое разъяснение для тех, кто не слишком знаком с терминологией TFS:
Вот что делает билдер приватных фиксов:
Для удобства разработчиков используется веб-интерфейс, где можно указать продукт, для которого надо собрать приватный фикс, указать номер шелвсета, выбрать проекты, для которых требуется собрать приватный фикс, и добавить сборку фикса в очередь. На скриншоте ниже приведен финальный рабочий вариант веб-приложения, где отображается текущий статус билда, очередь приватных фиксов и история их сборки. В нашем примере рассматривается только организация очереди сборки приватных фиксов.
В этой статье я расскажу, как реализовал постановку сборки фиксов в очередь и последовательный их запуск на билдере. Вот из каких частей будет состоять решение:
Дополнительно создали новое удобное веб-приложение, которое умеет работать с несколькими билдерами и вести логи событий. Чтобы не перегружать статью, подробно рассказывать о нем мы тоже пока не будем. Кроме этого, в этой статье не приведена работа с TFS, с историей хранений собранных приватных фиксов и различные вспомогательные классы и функции.
Есть много подробных статей, описывающих создание WCF-сервисов. Мне больше всех понравился материал с сайта Microsoft [1]. Его я взял за основу при разработке. Чтобы облегчить знакомство с проектом, я дополнительно выложил бинарники [2]. Начнем!
Здесь у нас будет только первичная болванка сервиса. На данном этапе нам нужно убедиться, что сервис запускается и работает. Кроме этого, код идентичен как для QBuilder.AppLauncher, так и для QBuilder.AppQueue, поэтому этот процесс необходимо будет повторить два раза.
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;
// Определяем сервис контракт
[ServiceContract(Namespace = "http://QBuilder.AppLauncher")]
public interface IAppLauncher
{
// Добавляем функцию для проверки работы сервиса
[OperationContract]
bool TestConnection();
}
public class AppLauncherService : IAppLauncher
{
public bool TestConnection()
{
return true;
}
}
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());
}
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();
}
protected override void OnStop()
{
if (serviceHost != null)
{
serviceHost.Close();
serviceHost = null;
}
}
}
[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);
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="QBuilder.AppLauncher.AppLauncherService"
behaviorConfiguration="AppLauncherServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8000/QBuilderAppLauncher/service"/>
</baseAddresses>
</host>
<endpoint address=""
binding="wsHttpBinding"
contract="QBuilder.AppLauncher.IAppLauncher" />
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="AppLauncherServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="False"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
1) Запускаем WcfTestClient
2) Добавляем адрес сервиса: http://localhost:8000/QBuilderAppLauncher/service [3]
3) Открывается интерфейс сервиса:
4) Вызываем тестовую функцию TestConnection, проверяем, что все работает и функция возвращает значение:
Теперь, когда получен рабочая болванка сервиса, добавляем необходимые нам функции.
Когда я начал изучать, как написать WCF-сервис с нуля, я прочитал кучу статей по этой теме. На столе у меня лежал десяток-другой распечатанных листов, по которым я разбирался, что и как. Признаюсь, сразу запустить сервис у меня не получилось. Я потратил кучу времени и пришел к выводу, что сделать болванку сервиса действительно важно. С ней вы будете уверены, что все работает и можно приступать к реализации необходимых функций. Подход может показаться расточительным, но он облегчит жизнь, если куча написанного кода не заработает как надо.
Вернемся к приложению. На этапе отладки и в ряде других случаев требуется запуск сервиса в виде консольного приложения без регистрации в виде сервиса. Это очень полезная функция, позволяющая обойтись без утомительного использования дебаггеров. Именно в таком режиме работает сервис QBuilder.AppLauncher. Вот как ее реализовать:
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.");
}
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. Все это можно реализовать, но это тема для отдельной статьи. Здесь для нас главное — наглядный рабочий пример.
Добавляем ее в класс public class AppLauncherService : IAppLauncher
:
public class AppLauncherService : IAppLauncher
{
Process appProcess;
public bool IsStarted()
{
if (appProcess!=null)
{
if (appProcess.HasExited)
{
return false;
}
else
{
return true;
}
}
else
{
return false;
}
}
Функция возвращает false, если процесс не существует или уже не запущен, и true – если процесс активен.
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» и в главе «Добавляем возможность запуска из консоли», заменяя в коде AppLauncher на AppQueue.
Теперь мы можем обращаться к интерфейсу сервиса из своей программы.
В 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.cs, где будет находиться ряд процедур работы с 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<QBuildRecord> GetCurrentQueue()
{
List<QBuildRecord> qList = new List<QBuildRecord>();
XElement xRoot = OpenXmlQueue();
if (xRoot.XPathSelectElements("build").Any())
{
List<XElement> 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-файле выглядит следующим образом:
<?xml version="1.0" encoding="utf-8"?>
<BuildsQueue BuildNumber="23">
<build BuildId="14" IssueId="26086" IssueName="TestIssueName" StartDate="2018-06-13T16:49:50.515238+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="15" IssueId="59559" IssueName="TestIssueName" StartDate="2018-06-13T16:49:50.6880927+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="16" IssueId="45275" IssueName="TestIssueName" StartDate="2018-06-13T16:49:50.859937+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="17" IssueId="30990" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.0321322+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="18" IssueId="16706" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.2009904+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="19" IssueId="66540" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.3581274+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="20" IssueId="68618" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.5087854+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="21" IssueId="18453" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.6713477+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="22" IssueId="68288" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.8277942+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="23" IssueId="89884" IssueName="TestIssueName" StartDate="2018-06-13T16:49:52.0151294+02:00" Build_CSharp="true" Build_Cpp="true" />
<currentbuild BuildId="13" IssueId="4491" StartDate="13.06.2018 16:53:16" />
</BuildsQueue>
Создайте файл BuildQueue.xml с данным содержимым и положите в каталог с исполняемым файлом. Данный файл будет использоваться в тестовой отладке для соответствия тестовых результатов.
В данном классе я размещаю вспомогательные функции. Сейчас здесь находится только одна функция, 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<string, string> 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 на данном этапе можно удалить. Для реализации работы очереди мне потребовался следующий набор функций:
Интерфейс будет вот таким:
public interface IAppQueue
{
// Функция добавления в очередь
[OperationContract]
void PushBuild(QBuildRecord qBRecord);
// Тестовое добавление в очередь
[OperationContract]
void TestPushBuild();
// Функция получения элемента из очереди
[OperationContract]
QBuildRecord PullBuild();
}
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"
}
);
}
}
Добавляем новые переменные в тело класса:
// Таймер, необходимый для обращения к очереди через определенный интервал
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<string, string>(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;
this.timer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerTick);
this.timer.Start();
// Создаем ServiceHost для сервиса AppQueueService
serviceHost = new ServiceHost(typeof(AppQueueService));
// Открываем ServiceHostBase и ждем обращений к сервису
serviceHost.Open();
}
Вот как он должен теперь выглядеть:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using QBuilder.AppQueue.AppLauncherService;
В секцию добавляем следующий набор параметров:
<appSettings>
<add key="QueueFileName" value="BuildQueue.xml"/>
<add key="BuildToolPath" value="c:tempdummybuild.exe"/>
<add key="BuildToolWorkDir" value="c:temp"/>
<add key="LocalDomain" value="."/>
<add key="UserName" value="username"/>
<add key="ScriptPath" value="C:TempBuildSample.bld"/>
</appSettings>
<?xml version="1.0" encoding="utf-8"?>
<BuildsQueue BuildNumber="23">
<build BuildId="19" IssueId="66540" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.3581274+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="20" IssueId="68618" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.5087854+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="21" IssueId="18453" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.6713477+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="22" IssueId="68288" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.8277942+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="23" IssueId="89884" IssueName="TestIssueName" StartDate="2018-06-13T16:49:52.0151294+02:00" Build_CSharp="true" Build_Cpp="true" />
<currentbuild BuildId="18" IssueId="16706" StartDate="13.06.2018 23:20:06" />
</BuildsQueue>
<?xml version="1.0" encoding="utf-8"?>
<BuildsQueue BuildNumber="23">
<build BuildId="21" IssueId="18453" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.6713477+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="22" IssueId="68288" IssueName="TestIssueName" StartDate="2018-06-13T16:49:51.8277942+02:00" Build_CSharp="true" Build_Cpp="true" />
<build BuildId="23" IssueId="89884" IssueName="TestIssueName" StartDate="2018-06-13T16:49:52.0151294+02:00" Build_CSharp="true" Build_Cpp="true" />
<currentbuild BuildId="20" IssueId="68618" StartDate="13.06.2018 23:24:25" />
</BuildsQueue>
Очередь работает!
Видавший виды powershell-скрипт был отправлен на свалку. Новое приложение полностью написано на C#. У нас появилась возможность использовать rulesets — правила, которые делали выборку файлов по специальным критериям и вставляли их только в определенные места в setup-скрипте. За счет новой системы хеширования решили проблему выборки файлов только по имени и по размеру — она возникала, когда при одинаковом имени и размере файлы отличались по контенту. Новая программа сборки апдейтов не рассматривает файлы как файлы — она их рассматривает как MD5-хеши и создает хеш-таблицу, в которой каждому набору файлов в определенном каталоге соответствовал свой уникальный хеш.
Скриншот финального решения, которое мы используем в нашей работе
Небольшие доработки в решение вносятся постоянно, но мы уже решили самую главную проблему — новый подход позволил полностью убрать человеческий фактор и избавить себя от кучи костылей. Система получилась настолько универсальной, что в ближайшее время будет использоваться для сборки хотфиксов, где меняется несколько файлов. Все это будет работать через веб-интерфейс с помощью другого приложения.
В ходе проекта я разобрался, как работать с XML, с файлами конфигурации, с файловой системой. Теперь же у меня есть свои наработки, которые я успешно использую в других проектах. Для наглядности статьи я убрал большое количество кода, которое может отвлечь от сути, и произвел серьезный рефакторинг.
Надеюсь, эта статья поможет вам в работе с WCF-сервисами, с таймерами в теле сервисов и реализации очередей через XML-файлы. Работу приложений и очереди вы можете посмотреть на видео:
P.S. Хочу выразить благодарность Виктору Бородичу, чьи советы очень помогли довести данную систему до рабочего вида. Виктор доказывает, что если посадить рядом опытных разработчиков и джуниоров, то качество кода у последних обязательно вырастет.
Автор: vhuman
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/287262
Ссылки в тексте:
[1] материал с сайта Microsoft: https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-host-a-wcf-service-in-a-managed-windows-service
[2] бинарники: https://drive.google.com/open?id=11rxZRWnBHHXzAQ23hjGIX4L4WOZNb7Ew
[3] http://localhost:8000/QBuilderAppLauncher/service: http://localhost:8000/QBuilderAppQueue/service
[4] http://localhost:8000/QBuilderAppLauncher/service: http://localhost:8000/QBuilderAppLauncher/service
[5] localhost: http://localhost
[6] Источник: https://habr.com/post/417701/?utm_campaign=417701
Нажмите здесь для печати.