De scripts simples a aplicativos cliente-servidor faça você mesmo no WCF: por que eu gosto de trabalhar no CM

O trabalho na equipe de Gerenciamento de configuração está associado a garantir a funcionalidade dos processos de construção - montagem de produtos da empresa, verificação preliminar de código, análise estatística, documentação e muito mais. Além disso, estamos constantemente trabalhando na otimização de vários processos e, o que é maravilhoso, somos praticamente livres para escolher ferramentas para este trabalho interessante. A seguir, falarei em detalhes sobre como, tendo apenas um nível diferente de conhecimento em C # e C ++, criei um serviço WCF funcional para trabalhar com filas de correção. E por que eu decidi que é muito importante.



Automação uma vez ou 117 páginas instrução repetidamente


Uma pequena digressão para que você entenda por que estou tão preocupado com a automação e otimização de processos.

Antes da Veeam, trabalhei para uma grande empresa internacional - eu era o líder da equipe de Gerenciamento de configuração, estava envolvido na criação do aplicativo e na implantação em ambientes de teste. O programa foi desenvolvido com sucesso, novas funções foram adicionadas, documentação foi escrita, cujo suporte eu também lidei. Mas sempre me surpreendi por que um programa tão sério não possui um sistema de configuração de parâmetros normal, do qual havia muitas dezenas, senão centenas.

Conversei com os desenvolvedores sobre esse tópico e recebi uma resposta - o cliente não pagou por esse recurso, não concordou com seu custo e, portanto, o recurso não foi implementado. Mas, na verdade, o controle de qualidade sofreu e nós, a equipe SM, fomos diretamente afetados. A configuração do programa e sua configuração preliminar foram realizadas através de muitos arquivos de configuração, cada um com dezenas de parâmetros.

Cada nova construção, cada nova versão fazia suas alterações na configuração. Arquivos de configuração antigos não puderam ser usados, pois costumavam ser incompatíveis com a nova versão. Como resultado, todas as vezes antes de implantar a compilação para o teste ou nas máquinas de trabalho dos testadores, era necessário gastar muito tempo configurando o programa, corrigindo erros de configuração, consultas constantes com os desenvolvedores sobre o tópico "por que não funciona assim agora?" Em geral, o processo foi extremamente otimizado.

Para ajudar na configuração, tivemos uma instrução de 117 páginas no tamanho de fonte Arial 9. Tivemos que ler com muito, muito cuidado. Às vezes, parecia que era mais fácil construir um kernel Linux com os olhos fechados em um computador desligado.

Ficou claro que a otimização não poderia ser evitada aqui. Comecei a escrever meu configurador para um programa com suporte a perfis e a capacidade de alterar parâmetros em alguns segundos, mas o projeto chegou à sua fase final e passei a trabalhar em outro projeto. Nele, analisamos muitos logs de um sistema de cobrança para possíveis erros no lado do servidor. A automação de muitas ações usando a linguagem Python me salvou da quantidade monstruosa de trabalho manual. Gostei muito dessa linguagem de script e, com sua ajuda, criamos um conjunto de scripts de análise para todas as ocasiões. As tarefas que exigiram vários dias de análise criteriosa de acordo com o “cat logfile123 | grep something_special ”, demorou alguns minutos. Tudo foi ótimo ... e chato.

Gerenciamento de Configuração - Novas Aventuras


Eu vim para a Veeam como líder de uma pequena equipe de CM. Muitos processos exigiram automação, otimização e repensar. Mas havia total liberdade na escolha de ferramentas! O desenvolvedor deve usar uma linguagem de programação específica, estilo de código, um conjunto específico de bibliotecas. SM, por outro lado, pode não usar nada para resolver a tarefa, se ele tiver tempo suficiente, coragem e paciência para isso.

A Veeam, como muitas outras empresas, tem a tarefa de montar atualizações de produtos. A atualização incluiu centenas de arquivos e foi necessário alterar apenas aqueles que foram alterados, levando em consideração várias condições importantes. Para fazer isso, criamos um script volumoso de PowerShell que poderia acessar o TFS, selecionar arquivos e classificá-los nas pastas necessárias. A funcionalidade do script foi complementada, gradualmente se tornou enorme, demorou muito tempo para depurar e constantemente algumas muletas para inicializar. Era urgente fazer alguma coisa.

O que os desenvolvedores queriam


Aqui estão as principais reclamações:

  • Não foi possível colocar as correções na fila. Você deve verificar constantemente a página da Web para ver quando a montagem da correção particular termina e pode começar a criar a sua própria.
  • Não há notificações sobre erros - para ver erros na GUI do aplicativo de montagem, você precisa ir ao servidor e observar muitos logs volumosos.
  • Não há histórico de compilação para correções particulares.

Era necessário lidar com essas tarefas e adicionar pequenas coisas agradáveis ​​que os desenvolvedores não recusariam.

O que são correções particulares


Uma correção particular no contexto do nosso desenvolvimento é um determinado conjunto de correções no código, armazenado no shelveset do Team Foundation Server para a ramificação de lançamento. Um pequeno esclarecimento para aqueles que não estão familiarizados com a terminologia do TFS:

  • check-in - um conjunto de alterações locais no código-fonte, que é feito no código armazenado no TFS. Essa verificação pode ser verificada usando processos de Integração Contínua / Check-in Fechado que permitem ignorar apenas o código correto e rejeitar todas as verificações que violam a coleção do projeto final.
  • shelveset - um conjunto de alterações locais no código-fonte que não é feito diretamente no código-fonte localizado no TFS, mas é acessível por seu nome. O shellset pode ser implantado na máquina local do desenvolvedor ou no sistema de compilação para trabalhar com código modificado que não está incluído no TFS. Além disso, o shellset pode ser adicionado ao TFS como uma verificação após a implantação, quando todo o trabalho com ele estiver concluído. Por exemplo, o verificador de portas funciona dessa maneira. Primeiro, o shellset no construtor é verificado. Se a verificação for bem-sucedida, o shellset se transformará em uma verificação!

Aqui está o que o construtor de correções particulares faz:

  1. Obtém o nome (número) do shellset e o implementa no construtor de correções particular. Como resultado, obtemos o código-fonte do produto de lançamento, além de alterações / correções no shellset. O ramo de lançamento permanece inalterado.
  2. Em um construtor de correções particular, um projeto ou uma série de projetos está sendo executado para o qual uma correção particular foi executada.
  3. O conjunto de arquivos binários compilados é copiado para o diretório de rede da correção privada. O catálogo contém o nome do conjunto de shell, que é uma sequência de números.
  4. O código-fonte no construtor de correções privado é restaurado para sua forma original.

Para conveniência dos desenvolvedores, uma interface da web é usada na qual você pode especificar o produto para o qual deseja coletar uma correção privada, especificar o número do conjunto de shell, selecionar os projetos para os quais deseja coletar uma correção privada e adicionar o conjunto da correção à fila. A captura de tela abaixo mostra a versão final de trabalho do aplicativo Web, que exibe o status atual da construção, a fila de correções particulares e o histórico de sua montagem. No nosso exemplo, apenas a fila para montar correções particulares é considerada.

O que era meu


  • Um construtor de correções particular que coletava correções particulares do shellset do TFS iniciando um aplicativo de console com os parâmetros de linha de comando fornecidos.
  • Veeam.Builder.Agent - um serviço WCF criado pela Veeam que inicia o aplicativo com parâmetros no modo de console no usuário atual e retorna o status atual do aplicativo.
  • O serviço web do IIS é um aplicativo no Windows Forms que permite inserir o nome do conjunto de shell, os parâmetros especificados e iniciar o processo de criação de uma correção particular.
  • Um conhecimento muito superficial de programação é C ++, um pouco de C # na universidade, e cria pequenos aplicativos para automação, adicionando novas funções aos processos de compilação atuais e como hobby.
  • Colegas experientes, Google e artigos indianos do MSDN são fontes de respostas para todas as perguntas.

O que faremos


Neste artigo, mostrarei como implementei o enfileiramento do conjunto de correções e seu lançamento sequencial no construtor. Aqui estão as partes da solução:

  • QBuilder.AppQueue é o meu serviço WCF que fornece trabalho com a fila de construção e chama o serviço Veeam.Builder.Agent para executar o programa de construção.
  • O dummybuild.exe é um programa de stub usado para depuração e como auxílio visual. Necessário para visualizar os parâmetros transferidos.
  • QBuilder.AppLauncher - serviço WCF que executa aplicativos no console do usuário atual e funciona no modo interativo. Este é um análogo significativamente simplificado do programa Veeam.Builder.Agent escrito especificamente para este artigo. O serviço original pode funcionar como um serviço do Windows e executar aplicativos no console, o que requer trabalho adicional com a API do Windows. Para descrever todos os truques, seria necessário um artigo separado. Meu exemplo funciona como um serviço de console interativo simples e usa duas funções - iniciar um processo com parâmetros e verificar seu status.

Além disso, criamos um novo aplicativo da Web conveniente que pode funcionar com vários construtores e manter registros de eventos. Para não sobrecarregar o artigo, também não falaremos sobre isso em detalhes. Além disso, este artigo não descreve o trabalho com o TFS, com o histórico de armazenamento de correções particulares coletadas e várias classes e funções auxiliares.



Criando serviços WCF


Existem muitos artigos detalhados que descrevem a criação de serviços WCF. Gostei mais do material do site da Microsoft . Tomei isso como base para o desenvolvimento. Para facilitar o meu conhecimento do projeto, eu também expus os binários . Vamos começar!

Crie o serviço QBuilder.AppLauncher


Aqui teremos apenas o disco principal do serviço. Nesta fase, precisamos garantir que o serviço inicie e funcione. Além disso, o código é idêntico para QBuilder.AppLauncher e QBuilder.AppQueue, portanto, esse processo precisará ser repetido duas vezes.

  1. Crie um novo aplicativo de console chamado QBuilder.AppLauncher
  2. Renomeie Program.cs para Service.cs
  3. Renomeie o espaço para nome para QBuilder.AppLauncher
  4. Adicione as seguintes referências ao projeto:
    a. System.ServiceModel.dll
    b. System.ServiceProcess.dll
    c. System.Configuration.Install.dll
  5. Adicione as seguintes definições ao Service.cs

    using System.ComponentModel; using System.ServiceModel; using System.ServiceProcess; using System.Configuration; using System.Configuration.Install; 

    No processo de montagem adicional, também serão necessárias as seguintes definições:

     using System.Reflection; using System.Xml.Linq; using System.Xml.XPath; 
  6. Definimos a interface IAppLauncher e adicionamos funções para trabalhar com a fila:

     //    [ServiceContract(Namespace = "http://QBuilder.AppLauncher")]   public interface IAppLauncher   {    //             [OperationContract]       bool TestConnection();   } 
  7. Na classe AppLauncherService, implementamos a interface e a função de teste TestConnection:

     public class AppLauncherService : IAppLauncher   {       public bool TestConnection()       {           return true;       }   } 
  8. Crie uma nova classe AppLauncherWindowsService que herda a classe ServiceBase. Adicione a variável local serviceHost - um link para ServiceHost. Definimos o método Main, que chama 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. Substitua a função OnStart () que cria a nova instância 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. Substitua a função onStop que fecha a instância ServiceHost:

     protected override void OnStop()       {           if (serviceHost != null)           {               serviceHost.Close();               serviceHost = null;           }       }   } 
  11. Crie uma nova classe ProjectInstaller, herdada do Installer e marcada com RunInstallerAttribute, definida como True. Isso permite que você instale o serviço do Windows usando o programa 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. Altere o conteúdo do arquivo 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> 

Verificando a capacidade de manutenção do serviço


  1. Nós compilamos o serviço.
  2. Instale-o com o comando installutil.exe
    1) Vá para a pasta onde está o arquivo de serviço compilado
    2) Execute o comando de instalação:
    C: \ Windows \ Microsoft.NET \ Framework64 \ v4.0.30319 \ InstallUtil.exe
  3. Entramos no snap-in services.msc, verificamos a disponibilidade do serviço QBuilder App Launcher e o executamos.
  4. Verificamos a capacidade de manutenção do serviço usando o programa WcfTestClient.exe, incluído no VisualStudio:

    1) Execute WcfTestClient
    2) Adicione o endereço do serviço: http: // localhost: 8000 / QBuilderAppLauncher / service
    3) A interface de serviço é aberta:



    4) Chamamos a função de teste TestConnection, verifique se tudo funciona e a função retorna um valor:


Agora que temos um disco de trabalho do serviço, adicionamos as funções que precisamos.

Por que preciso de uma função de teste que não faz nada


Quando comecei a aprender a escrever um serviço WCF do zero, li vários artigos sobre esse tópico. Na mesa, havia uma dúzia ou duas folhas impressas nas quais eu conseguia descobrir o que e como. Admito que não consegui iniciar imediatamente o serviço. Passei muito tempo e cheguei à conclusão de que é realmente importante fazer um disco de serviço. Com isso, você terá certeza de que tudo funciona e poderá começar a implementar as funções necessárias. A abordagem pode parecer um desperdício, mas facilitará a vida se um monte de código escrito não funcionar como deveria.

Adicione a capacidade de executar no console


Voltar para o aplicativo. No estágio de depuração e em vários outros casos, é necessário iniciar o serviço na forma de um aplicativo de console sem registrar-se como um serviço. Esse é um recurso muito útil que permite que você fique sem o tedioso uso de depuradores. É nesse modo que o serviço QBuilder.AppLauncher funciona. Veja como implementá-lo:

  1. Adicione o procedimento RunInteractive à classe AppLauncherWindowsService, que fornece o serviço no modo do console:

     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. Nós fazemos alterações no procedimento Principal - adicionamos o processamento dos parâmetros da linha de comando. Com a opção / console e uma sessão de usuário ativo aberta, lançamos o programa no modo interativo. Caso contrário, o lançaremos como um serviço.

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

Adicione funções para iniciar o aplicativo e verifique seu status


O serviço é extremamente simples, não há verificações adicionais. Ele pode executar aplicativos apenas na versão do console e em nome do administrador. Ele também pode iniciá-los como um serviço - mas você não os verá, eles girarão em segundo plano e você poderá vê-los apenas através do Gerenciador de Tarefas. Tudo isso pode ser implementado, mas este é um tópico para um artigo separado. A principal coisa para nós aqui é um exemplo claro de trabalho.

  1. Primeiro, adicione a variável global appProcess, que armazena o processo em execução no momento.

    Adicione-o public class AppLauncherService : IAppLauncher :

     public class AppLauncherService : IAppLauncher   {       Process appProcess; 
  2. Adicione uma função à mesma classe que verifica o status do processo em execução:

        public bool IsStarted()       {           if (appProcess!=null)           {               if (appProcess.HasExited)               {                   return false;               }               else               {                   return true;               }           }           else           {               return false;           }       } 

    A função retornará false se o processo não existir ou ainda não estiver em execução e true se o processo estiver ativo.
  3. Adicione a função para iniciar o aplicativo:

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

A função inicia qualquer aplicativo com parâmetros. Os parâmetros Domain e Username não são usados ​​nesse contexto e podem estar vazios, pois o serviço inicia o aplicativo a partir da sessão do console com direitos de administrador.

Iniciando o serviço QBuilder.AppLauncher


Conforme descrito anteriormente, este serviço funciona interativamente e permite executar aplicativos na sessão atual do usuário, verifica se o processo está em execução ou já foi concluído.

  1. Para funcionar, você precisa dos arquivos QBuilder.AppLauncher.exe e QBuilder.AppLauncher.exe.config, que estão no arquivo no link acima. O código fonte deste aplicativo para montagem automática também está localizado lá.
  2. Iniciamos o serviço com direitos de administrador.
  3. A janela do console do serviço será aberta:



Qualquer pressionamento de tecla no console de serviço fecha, tenha cuidado.

  1. Para testes, execute o wcftestclient.exe, incluído no Visual Studio. Verificamos a disponibilidade do serviço em http: // localhost: 8000 / QBuilderAppLauncher / service ou abrimos o link no Internet Explorer.

Se tudo funcionar, vá para o próximo passo.

Criando o serviço QBuilder.AppQueue


E agora vamos ao serviço mais importante, para o qual o artigo inteiro foi escrito! Repetimos a sequência de ações no capítulo "Criando o serviço QBuilder.AppLauncher" e no capítulo "Adicionando a inicialização a partir do console", substituindo o AppLauncher pelo AppQueue no código.

Adicione um link ao serviço QBuilder.AppLauncher para uso no serviço de fila


  1. No Solution Explorer do nosso projeto, selecione Add Service Reference e especifique o endereço: localhost : 8000 / QBuilderAppLauncher / service
  2. Selecione o espaço para nome do nome: AppLauncherService.

Agora podemos acessar a interface de serviço do nosso programa.

Crie uma estrutura para armazenar elementos da fila


No espaço para nome de QBuilder.AppQueue, adicione a classe 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; } } 

Implementando a classe de fila CXmlQueue


Adicionamos a classe CXmlQueue.cs ao nosso projeto, que conterá vários procedimentos para trabalhar com o arquivo XML:

  • Construtor CXmlQueue - define o nome inicial do arquivo em que a fila está armazenada.
  • SetCurrentBuild - grava informações sobre a compilação atual no arquivo XML da fila. Este é um elemento que não está na fila e armazena informações sobre o processo em execução no momento. Pode estar vazio.
  • GetCurrentBuild - Obtém os parâmetros do processo em execução no arquivo XML da fila. Pode estar vazio.
  • ClearCurrentBuild - limpa o elemento currentbuild no arquivo XML da fila se o processo terminar.
  • OpenXmlQueue - função para abrir o arquivo XML em que a fila está armazenada. Se o arquivo estiver ausente, um novo será criado.
  • GetLastQueueBuildNumber - cada build na fila possui seu próprio número de série exclusivo. Esta função retorna seu valor, que é armazenado no atributo raiz.
  • IncrementLastQueueBuildNumber - aumenta o valor do número da compilação ao enfileirar uma nova compilação.
  • GetCurrentQueue - Retorna uma lista de elementos QBuildRecord do arquivo XML da fila.

No código original, todos esses procedimentos foram colocados na classe principal, mas para maior clareza, criei uma classe separada CXmlQueue. A classe é criada no namespace QBuilder.AppQueue, verifique se todas as definições necessárias estão especificadas:

 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 { . . . } 

Então, estamos implementando. A própria classe CXmlQueue:

Clique para expandir o spoiler com código
 //      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; } } 


A fila no arquivo XML é a seguinte:

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

Crie um arquivo BuildQueue.xml com este conteúdo e coloque-o no diretório com o arquivo executável. Este arquivo será usado na depuração de teste para corresponder aos resultados do teste.

Adicionar classe AuxFunctions


Nesta classe, coloco funções auxiliares. No momento, existe apenas uma função, FormatParameters, que executa a formatação dos parâmetros para transmiti-los ao aplicativo do console para inicialização. Listagem do arquivo 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); } } } 

Adicione novos recursos à interface de serviço


A função de teste TestConnection pode ser excluída neste estágio. Para implementar o trabalho da fila, eu precisava do seguinte conjunto de funções:

  • PushBuild (QBuildRecord): nulo. Esta é uma função que adiciona um novo valor ao arquivo XML da fila com os parâmetros QBuildRecord
  • TestPushBuild (): nulo. Esta é uma função de teste que adiciona dados de teste a uma fila em um arquivo XML.
  • PullBuild: QBuildRecord. Esta é uma função que recupera o valor QBuildRecord do arquivo XML da fila. Pode estar vazio.

A interface será assim:

  public interface IAppQueue { //     [OperationContract] void PushBuild(QBuildRecord qBRecord); //     [OperationContract] void TestPushBuild(); //      [OperationContract] QBuildRecord PullBuild(); } 

Implementamos funções na classe AppQueueService: IAppQueue:



Clique para expandir o spoiler com código
 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" } ); } } 


Fazendo alterações na classe AppQueueWindowsService: ServiceBase


Adicione novas variáveis ​​ao corpo da classe:

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

No construtor AppQueueWindowsService (), inclua funções para ler o arquivo de configuração, inicialize serviços e classes de fila:

 //          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 - frequência de resposta do timer. Indicado em milissegundos. Aqui, definimos que o timer deve disparar a cada 30 segundos. No original, esse parâmetro está no arquivo de configuração. Para o artigo, decidi defini-lo no código.

Adicione a função para verificar o processo de construção em execução na classe:

 //        public bool BuildIsStarted() { IAppLauncher builderAgent; try { builderAgent = new AppLauncherClient(); return builderAgent.IsStarted(); } catch (Exception ex) { return false; } } 

Adicione o procedimento para trabalhar com o timer:

  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 binaries , , 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. dummy , :

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



PS , . , , .

Source: https://habr.com/ru/post/pt417701/


All Articles