从简单的脚本到WCF上自己动手的客户端服务器应用程序:为什么我喜欢在CM中工作

配置管理团队中的工作与确保构建过程的功能相关-组装公司产品,初步代码验证,统计分析,文档等等。 此外,我们一直在不断努力优化各种流程,而且很棒的是,我们几乎可以自由选择工具来完成这项有趣的工作。 接下来,我将详细讨论如何仅凭不同的C#和C ++知识,就如何制作用于修复队列的功能性WCF服务。 以及为什么我认为这很重要。



一次或一次自动执行117页指令


有点题外话,让您理解为什么我如此担心流程的自动化和优化。

在加入Veeam之前,我曾在一家大型国际公司工作-我是配置管理团队的团队负责人,参与了构建应用程序并将其部署到测试环境的工作。 该程序已成功开发,添加了新功能,编写了文档,我也提供了支持。 但是我总是感到惊讶的是,为什么这样一个严肃的程序没有一个正常的参数配置系统,而其中却有很多甚至数百个。

我与开发人员就此主题进行了交谈,并得到了答案-客户未为此功能付款,未就其成本达成共识,因此未实施该功能。 但是实际上,质量检查受到了影响,我们的SM小组受到了直接影响。 程序的配置及其初步配置是通过许多配置文件执行的,每个配置文件都有数十个参数。

每个新版本,每个新版本都对其配置进行了更改。 无法使用旧的配置文件,因为它们通常与新版本不兼容。 结果,每次在部署测试版本或在测试人员的工作机上部署版本时,您都必须花费大量时间来配置程序,修复配置错误,就“为什么现在这样不起作用?”这一主题不断与开发人员进行磋商。 总的来说,该过程是极其未经优化的。

为了帮助进行设置,我们使用了Arial字体大小9的117页说明。我们必须非常非常仔细地阅读。 有时候,闭着眼睛在关闭的计算机上构建Linux内核似乎更容易。

很明显,这里无法避免优化。 我开始为程序编写配置程序,该程序支持概要文件并且可以在几秒钟内更改参数,但是该项目进入了最后阶段,因此我转而从事另一个项目。 在其中,我们分析了一个计费系统的许多日志,以了解服务器端可能存在的错误。 使用Python语言自动执行许多操作使我免于繁琐的手工工作。 我真的很喜欢这种脚本语言,在它的帮助下,我们为各种场合制作了一套分析脚本。 这些任务需要根据“ cat logfile123 | cat”进行几天的深入分析。 grep something_special”,花了几分钟。 一切都很棒。。。无聊。

配置管理-新历险记


我是作为CM小型团队的团队负责人来到Veeam的。 许多过程需要自动化,优化和重新思考。 但是选择工具有完全的自由! 开发人员必须使用特定的编程语言,代码风格和特定的库集。 另一方面,如果SM有足够的时间,勇气和耐心来完成任务,他可能根本不使用任何东西来解决任务。

与许多其他公司一样,Veeam的任务是组装产品更新。 此更新包括数百个文件,并且考虑到许多重要条件,有必要仅更改已更改的文件。 为此,我们创建了一个庞大的powershell脚本,可以将其爬到TFS中,选择文件,然后将它们分类到必要的文件夹中。 该脚本的功能得到了补充,它逐渐变得庞大,它花费了很多时间进行调试,并不断出现一些拐杖来启动。 迫切需要做些事情。

开发人员想要什么


以下是主要的投诉:

  • 无法将修复排队。 您必须不断检查网页以查看私有修订程序的组装何时完成,并且可以开始构建自己的补丁程序。
  • 没有关于错误的通知-要在组装应用程序的GUI中查看错误,您必须转到服务器并查看大量日志。
  • 没有私有修订的构建历史记录。

有必要处理这些任务并添加开发人员不会拒绝的令人愉悦的小东西。

什么是私人修补程序


在我们的开发环境中,一个私人修复程序是对代码中的某些更正集,这些更正集存储在Team Foundation Server的发布分支的架子集中。 对于不太熟悉TFS术语的人,需要澄清一下:

  • 签入-源代码中的一组本地更改,这些更改是对TFS中存储的代码进行的。 可以使用持续集成/门控签入过程来检查此检查,该过程允许您仅跳过正确的代码并拒绝所有违反最终项目集合的检查。
  • 书架集-源代码中的一组本地更改,这些更改不直接对TFS中的源代码进行,但可以通过其名称访问。 可以将Shellset部署在开发人员或构建系统的本地计算机上,以与TFS中未包含的修改后的代码一起使用。 此外,可以在完成部署后将Shellset添加到TFS,作为检查之后的对象。 例如,门检查器以这种方式工作。 首先,检查构建器上的shellset。 如果检查成功,则shellset变成检查!

这是私有修订生成器的作用:

  1. 获取shellset的名称(编号)并将其部署在私有修订构建器上。 结果,我们从外壳集获得了发行产品的源代码以及更改/修复。 发布分支保持不变。
  2. 在私有修补程序构建器上,将执行已执行私有修补程序的一个或多个项目。
  3. 该组已编译的二进制文件将被复制到私有修订的网络目录中。 目录包含shellset名称,它是一个数字序列。
  4. 专用修订生成器上的源代码将还原为原始形式。

为了方便开发人员,使用Web界面在其中可以指定要为其收集私有修订的产品,指定外壳集编号,选择要为其收集私有修订的项目,并将修订程序集添加到队列中。 下面的屏幕快照显示了该Web应用程序的最终工作版本,该版本显示了构建的当前状态,私有修订队列以及其组装历史。 在我们的示例中,仅考虑用于组装专用修订的队列。

我的是什么


  • 私有修订程序构建器,它通过使用给定的命令行参数启动控制台应用程序来从TFS外壳集收集私有修订。
  • Veeam.Builder.Agent-由Veeam编写的WCF服务,该服务在控制台模式下在当前用户下启动具有参数的应用程序,并返回应用程序的当前状态。
  • IIS Web服务是Windows Forms上的一个应用程序,它使您可以输入shellset名称,指定的参数并开始构建私有修订的过程。
  • 一个非常浅薄的编程知识是C ++,在大学里是C#,并编写了一些自动化的小型应用程序,并向当前的构建过程添加了新功能,这是一种业余爱好。
  • 经验丰富的同事,Google和印度MSDN文章是所有问题答案的来源。

我们会怎么做


在本文中,我将介绍如何在构建器上实现修订程序集的排队及其顺序启动。 以下是解决方案的各个部分:

  • QBuilder.AppQueue是我的WCF服务,它提供了构建队列的工作,并调用Veeam.Builder.Agent服务来运行构建程序。
  • dummybuild.exe是一个存根程序,用于调试和视觉辅助。 需要可视化传输的参数。
  • QBuilder.AppLauncher-WCF服务,该服务在当前用户的控制台中运行应用程序并以交互方式工作。 这是专门为本文编写的Veeam.Builder.Agent程序的简化版本。 原始服务可以用作Windows服务,并可以在控制台中运行应用程序,这需要使用Windows API进行其他工作。 为了描述所有技巧,将需要单独的文章。 我的示例充当简单的交互式控制台服务,并使用两个功能-使用参数启动进程并检查其状态。

此外,我们创建了一个新的便捷Web应用程序,可以与多个构建器一起使用并保留事件日志。 为了不使文章过载,我们也不会详细讨论它。 此外,本文不介绍使用TFS的情况,它不包含收集的私有修订以及各种辅助类和功能的存储历史记录。



创建WCF服务


有许多详细的文章描述了WCF服务的创建。 我最喜欢Microsoft网站上的资料。 我把它当作发展的基础。 为了使我更熟悉该项目,我另外布置了二进制文件 。 让我们开始吧!

创建QBuilder.AppLauncher服务


在这里,我们将只有该服务的主光盘。 在此阶段,我们需要确保该服务启动并正常工作。 此外,QBuilder.AppLauncher和QBuilder.AppQueue的代码是相同的,因此此过程将需要重复两次。

  1. 创建一个名为QBuilder.AppLauncher的新控制台应用程序
  2. 将Program.cs重命名为Service.cs
  3. 将名称空间重命名为QBuilder.AppLauncher
  4. 将以下引用添加到项目中:
    一个 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(新的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. 覆盖创建新的ServiceHost实例的OnStart()函数:

     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. 覆盖关闭ServiceHost实例的onStop函数:

     protected override void OnStop()       {           if (serviceHost != null)           {               serviceHost.Close();               serviceHost = null;           }       }   } 
  11. 创建一个新的ProjectInstaller类,该类继承自Installer,并用RunInstallerAttribute标记,该类设置为True。 这使您可以使用installutil.exe程序安装Windows服务:

     [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文件的内容:

     <?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. 我们编译服务。
  2. 使用installutil.exe命令安装
    1)进入编译服务文件所在的文件夹
    2)运行安装命令:
    C:\ Windows \ Microsoft.NET \ Framework64 \ v4.0.30319 \ InstallUtil.exe
  3. 我们进入services.msc管理单元,检查QBuilder App Launcher服务的可用性并运行它。
  4. 我们使用Visual Studio附带的WcfTestClient.exe程序检查服务的可维护性:

    1)运行WcfTestClient
    2)添加服务地址: http://本地主机:8000 / QBuilderAppLauncher / service
    3)服务界面打开:



    4)我们调用测试函数TestConnection,检查是否一切正常,并且该函数返回一个值:


现在我们有了服务的工作光盘,我们添加了所需的功能。

为什么我需要什么都不做的测试功能


当我开始学习如何从头开始编写WCF服务时,我读了很多关于该主题的文章。 在桌子上,我有一打或两张印刷的纸,可以弄清楚什么以及如何打印。 我承认,我没有设法立即启动该服务。 我花了很多时间得出结论,制作服务光盘确实很重要。 有了它,您将确保一切正常,并且可以开始实现必要的功能。 这种方法似乎很浪费,但是如果一堆书面代码无法正常工作,它将使生活变得更轻松。

添加从控制台运行的功能


返回应用程序。 在调试阶段和许多其他情况下,有必要以控制台应用程序的形式启动服务,而无需注册为服务。 这是一项非常有用的功能,可让您无需繁琐地使用调试器。 QBuilder.AppLauncher服务就是在这种模式下工作的。 实施方法如下:

  1. 将RunInteractive过程添加到AppLauncherWindowsService类,该过程以控制台方式提供服务:

     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过程进行了更改-添加命令行参数处理。 使用/控制台选项和打开的活动用户会话,我们以交互方式启动该程序。 否则,我们将其作为服务启动。

     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);   } } 

添加功能以启动应用程序并检查其状态


该服务非常简单,没有其他检查。 他只能在控制台版本中并代表管理员运行应用程序。 它还可以将它们作为服务启动-但是您将看不到它们,它们将在后台旋转,并且您只能通过任务管理器来查看它们。 所有这些都可以实现,但这是另一篇文章的主题。 对我们而言,最主要的是一个明确的工作示例。

  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. 服务的控制台窗口将打开:



服务控制台中的任何按键都会将其关闭,请小心。

  1. 对于测试,运行Visual Studio附带的wcftestclient.exe。 我们在http:// localhost:8000 / QBuilderAppLauncher / service上检查服务的可用性,或者在Internet Explorer中打开链接。

如果一切正常,请转到下一步。

创建QBuilder.AppQueue服务


现在,让我们继续进行最重要的服务,整个文章都是针对该服务编写的! 我们在“创建QBuilder.AppLauncher服务”一章和“从控制台添加启动”一章中重复操作序列,用代码中的AppQueue替换AppLauncher。

将链接添加到QBuilder.AppLauncher服务以在队列服务中使用


  1. 在我们项目的解决方案资源管理器中,选择“添加服务引用”并指定地址: localhost :8000 / QBuilderAppLauncher / service
  2. 选择名称名称空间:AppLauncherService。

现在,我们可以从程序访问服务接口。

创建用于存储队列元素的结构


在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-如果进程终止,这将清除队列XML文件中的currentbuild元素。
  • OpenXmlQueue-用于打开存储队列的XML文件的功能。 如果文件丢失,则会创建一个新文件。
  • GetLastQueueBuildNumber-队列中的每个构建都有其自己的唯一序列号。 该函数返回其值,该值存储在root属性中。
  • IncrementLastQueueBuildNumber-在排队新的构建时增加构建号的值。
  • GetCurrentQueue-从队列XML文件返回QBuildRecord元素的列表。

在原始代码中,所有这些过程都放置在主类中,但为清楚起见,我制作了一个单独的类CXmlQueue。 该类是在命名空间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文件,并将其放入带有可执行文件的目录中。 该文件将在测试调试中用于匹配测试结果。

添加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<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。 为了实现队列的工作,我需要以下功能集:

  • PushBuild(QBuildRecord):无效。 此函数使用QBuildRecord参数将新值添加到队列XML文件中
  • TestPushBuild():无效。 这是一个测试功能,可将测试数据添加到XML文件中的队列中。
  • PullBuild:QBuildRecord。 此函数从队列XML文件中检索QBuildRecord值。 它可能是空的。

该界面将如下所示:

  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<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; 

将配置部分添加到App.config


将以下参数集添加到该部分:

 <appSettings> <add key="QueueFileName" value="BuildQueue.xml"/> <add key="BuildToolPath" value="c:\temp\dummybuild.exe"/> <add key="BuildToolWorkDir" value="c:\temp\"/> <add key="LocalDomain" value="."/> <add key="UserName" value="username"/> <add key="ScriptPath" value="C:\Temp\BuildSample.bld"/> </appSettings> 


检查服务


  1. 解压缩QBuilder.AppLauncher.zip存档。他和其他必要的文件在这里可用
  2. 将dummybuild.exe文件从二进制归档文件内的目录复制到该目录,例如,在c:\ temp中。该程序是一个测试存根,仅显示该服务传递给正在启动的应用程序的命令行参数。如果使用其他目录,请确保更改配置文件中的BuildToolPath和BuildToolWorkDir参数。
  3. 转到\ QBuilder.AppLauncher \ binaries \ QBuilder.AppLauncher \目录,并在管理员模式下运行QBuilder.AppLauncher.exe文件。您也可以从源代码编译此服务。
  4. 我们使用具有管理员权限的QBuilder.AppQueue.exe / console命令启动已编译服务的控制台版本。
  5. 检查服务是否已启动并正在运行:


  6. 我们开始等待。如果一切顺利,则30秒后将出现以下窗口:


  7. 打开BuildQueue.xml文件,观察队列如何减少以及currentbuild的值如何更改:

     <?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> 

  8. 每次关闭伪程序后,都会模拟过程的结束,然后启动队列中的下一个元素:

     <?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#编写。现在,我们有机会使用规则集-根据特殊条件选择文件并将其仅插入设置脚本中某些位置的规则。由于采用了新的哈希系统,他们解决了仅按名称和大小选择文件的问题-当具有相同名称和大小的文件内容不同时出现。新的更新生成程序不会将文件视为文件,而是将其视为MD5哈希,并创建一个哈希表,其中特定目录中的每个文件集都有自己的唯一哈希。


我们在工作中使用的最终解决方案的屏幕截图

该解决方案一直在进行一些细微的改进,但是我们已经解决了最重要的问题-新方法使我们能够完全消除人为因素,并摆脱一堆拐杖。事实证明,该系统是如此通用,以至于在不久的将来它将被用于构建修补程序,其中一些文件会更改。所有这些都将通过使用另一个应用程序的Web界面进行。

在项目期间,我弄清楚了如何使用XML,配置文件和文件系统。现在,我有了自己的想法,并在其他项目中成功使用了这些想法。为了清楚起见,我删除了很多代码,这些代码可能会干扰本质,并进行了认真的重构。

我希望本文能帮助您使用WCF服务,在服务主体中使用计时器以及通过XML文件实现队列。您可以在视频中观看应用程序的操作和队列:



PS:我想对Viktor Borodich表示感谢,他的建议对将这个系统付诸实践很有帮助。Victor证明,如果您将经验丰富的开发人员和初级人员放在一起,那么后者的代码质量肯定会提高。

Source: https://habr.com/ru/post/zh-CN417701/


All Articles