Von einfachen Skripten bis hin zu Do-it-yourself-Client-Server-Anwendungen auf WCF: Warum ich gerne in CM arbeite

Die Arbeit im Configuration Management-Team ist mit der Sicherstellung der Funktionalität von Build-Prozessen verbunden - Zusammenstellen von Unternehmensprodukten, vorläufige Codeüberprüfung, statistische Analyse, Dokumentation und vieles mehr. Darüber hinaus arbeiten wir ständig an der Optimierung verschiedener Prozesse, und was wunderbar ist, wir können praktisch frei Werkzeuge für diese interessante Arbeit auswählen. Als nächstes werde ich ausführlich darüber sprechen, wie ich mit nur unterschiedlichen Kenntnissen in C # und C ++ einen funktionierenden WCF-Dienst für die Arbeit mit festen Warteschlangen erstellt habe. Und warum ich beschlossen habe, dass es sehr wichtig ist.



Automatisierung einmal oder 117 Seiten Anleitung immer und immer wieder


Ein kleiner Exkurs, damit Sie verstehen, warum ich mir solche Sorgen um die Automatisierung und Optimierung von Prozessen mache.

Vor Veeam arbeitete ich für ein großes internationales Unternehmen. Ich war Teamleiter des Configuration Management-Teams und war an der Erstellung und Bereitstellung der Anwendung in Testumgebungen beteiligt. Das Programm wurde erfolgreich entwickelt, neue Funktionen hinzugefügt, Dokumentation geschrieben, deren Unterstützung ich auch behandelt habe. Aber ich war immer überrascht, warum ein so ernstes Programm kein normales Parameterkonfigurationssystem hat, von dem es viele Dutzende, wenn nicht Hunderte gab.

Ich habe mit Entwicklern über dieses Thema gesprochen und eine Antwort erhalten. Der Kunde hat für diese Funktion nicht bezahlt, sich nicht auf die Kosten geeinigt, sodass die Funktion nicht implementiert wurde. Tatsächlich litt die Qualitätssicherung, und wir, das SM-Team, waren direkt betroffen. Die Konfiguration des Programms und seine vorläufige Konfiguration wurden über viele Konfigurationsdateien durchgeführt, von denen jede Dutzende von Parametern enthielt.

Bei jedem neuen Build und jeder neuen Version wurden Änderungen an der Konfiguration vorgenommen. Alte Konfigurationsdateien konnten nicht verwendet werden, da sie häufig nicht mit der neuen Version kompatibel waren. Daher mussten Sie jedes Mal, bevor Sie den Build für den Test oder auf den Arbeitsmaschinen des Testers bereitstellen, viel Zeit damit verbringen, das Programm zu konfigurieren, Konfigurationsfehler zu beheben und sich ständig mit Entwicklern über das Thema "Warum funktioniert das jetzt nicht so?" Zu beraten. Im Allgemeinen war der Prozess äußerst unoptimiert.

Um das Setup zu erleichtern, hatten wir eine 117-seitige Anleitung in Arial-Schriftgröße 9. Wir mussten sehr, sehr sorgfältig lesen. Manchmal schien es einfacher zu sein, einen Linux-Kernel mit geschlossenen Augen auf einem ausgeschalteten Computer zu erstellen.

Es wurde deutlich, dass eine Optimierung hier nicht zu vermeiden ist. Ich habe angefangen, meinen Konfigurator für ein Programm mit Unterstützung für Profile und der Möglichkeit, Parameter in wenigen Sekunden zu ändern, zu schreiben, aber das Projekt hat seine letzte Phase erreicht und ich bin zu einem anderen Projekt übergegangen. Darin haben wir viele Protokolle eines Abrechnungssystems auf mögliche Fehler auf der Serverseite analysiert. Die Automatisierung vieler Aktionen mit der Python-Sprache ersparte mir die ungeheure Menge an manueller Arbeit. Diese Skriptsprache hat mir sehr gut gefallen, und mit ihrer Hilfe haben wir eine Reihe von Analyseskripten für alle Gelegenheiten erstellt. Diese Aufgaben erforderten mehrere Tage sorgfältiger Analyse gemäß der „cat logfile123 | grep Something_special “, dauerte einige Minuten. Alles war großartig ... und langweilig.

Konfigurationsmanagement - Neue Abenteuer


Ich kam als Teamleiter für ein kleines CM-Team zu Veeam. Viele Prozesse erforderten Automatisierung, Optimierung und Umdenken. Aber es gab völlige Freiheit bei der Auswahl der Werkzeuge! Der Entwickler muss eine bestimmte Programmiersprache im Codestil und einen bestimmten Satz von Bibliotheken verwenden. SM hingegen verwendet möglicherweise überhaupt nichts, um die Aufgabe zu lösen, wenn er genug Zeit, Mut und Geduld dafür hat.

Veeam hat wie viele andere Unternehmen die Aufgabe, Produktupdates zusammenzustellen. Das Update enthielt Hunderte von Dateien, und es mussten nur die geänderten Dateien geändert werden, wobei eine Reihe wichtiger Bedingungen zu berücksichtigen waren. Zu diesem Zweck haben wir ein umfangreiches Powershell-Skript erstellt, das in TFS einsteigen, Dateien auswählen und in die erforderlichen Ordner sortieren kann. Die Funktionalität des Skripts wurde ergänzt, es wurde allmählich riesig, es dauerte viel Zeit zum Debuggen und ständig einige Krücken zum Booten. Es war dringend notwendig, etwas zu tun.

Was wollten die Entwickler?


Hier sind die Hauptbeschwerden:

  • Fixes können nicht in die Warteschlange gestellt werden. Sie müssen die Webseite ständig überprüfen, um festzustellen, wann die Montage des privaten Fixes abgeschlossen ist, und Sie können mit dem Erstellen Ihres eigenen Fixes beginnen.
  • Es gibt keine Benachrichtigungen über Fehler. Um Fehler in der GUI der Assembly-Anwendung anzuzeigen, müssen Sie zum Server gehen und viele umfangreiche Protokolle anzeigen.
  • Es gibt keinen Build-Verlauf für private Fixes.

Es war notwendig, sich mit diesen Aufgaben zu befassen und angenehme kleine Dinge hinzuzufügen, die Entwickler nicht ablehnen würden.

Was sind private Korrekturen?


Ein privater Fix im Rahmen unserer Entwicklung ist eine Reihe von Korrekturen im Code, die im Regal des Team Foundation Servers für den Release-Zweig gespeichert sind. Eine kleine Klarstellung für diejenigen, die mit der TFS-Terminologie nicht allzu vertraut sind:

  • Einchecken - Eine Reihe lokaler Änderungen im Quellcode, die am in TFS gespeicherten Code vorgenommen werden. Diese Prüfung kann mithilfe von Continuous Integration / Gated Check-in-Prozessen überprüft werden, mit denen Sie nur den richtigen Code überspringen und alle Prüfungen ablehnen können, die gegen die Erfassung des endgültigen Projekts verstoßen.
  • Shelveset - Eine Reihe lokaler Änderungen im Quellcode, die nicht direkt am Quellcode in TFS vorgenommen werden, sondern über dessen Namen zugänglich sind. Das Shellset kann auf dem lokalen Computer des Entwicklers oder Build-Systems bereitgestellt werden, um mit geändertem Code zu arbeiten, der nicht in TFS enthalten ist. Außerdem kann das Shellset nach der Bereitstellung als Überprüfung zu TFS hinzugefügt werden, wenn alle Arbeiten damit abgeschlossen sind. Zum Beispiel funktioniert der Gate-Checker auf diese Weise. Zunächst wird das Shellset auf dem Builder überprüft. Wenn die Prüfung erfolgreich ist, verwandelt sich das Shellset in eine Prüfung!

Der private Fix Builder macht Folgendes:

  1. Ruft den Namen (die Nummer) des Shellsets ab und stellt ihn auf dem privaten Fix Builder bereit. Als Ergebnis erhalten wir den Quellcode des Release-Produkts sowie Änderungen / Korrekturen aus dem Shellset. Der Release-Zweig bleibt unverändert.
  2. Auf einem Builder für private Fixes wird ein Projekt oder eine Reihe von Projekten ausgeführt, für die ein privater Fix durchgeführt wurde.
  3. Der Satz kompilierter Binärdateien wird in das Netzwerkverzeichnis des privaten Fixes kopiert. Der Katalog enthält den Shellset-Namen, eine Folge von Zahlen.
  4. Der Quellcode im privaten Fix Builder wird in seiner ursprünglichen Form wiederhergestellt.

Zur Vereinfachung für Entwickler wird eine Weboberfläche verwendet, über die Sie das Produkt angeben können, für das Sie einen privaten Fix erfassen möchten, die Shellset-Nummer angeben, die Projekte auswählen, für die Sie einen privaten Fix erfassen möchten, und die Fix-Assembly zur Warteschlange hinzufügen können. Der folgende Screenshot zeigt die endgültige Arbeitsversion der Webanwendung, in der der aktuelle Status des Builds, die Warteschlange für private Fixes und der Verlauf ihrer Assembly angezeigt werden. In unserem Beispiel wird nur die Warteschlange zum Zusammenstellen privater Fixes berücksichtigt.

Was war meins?


  • Ein Builder für private Fixes, der private Fixes aus dem TFS-Shellset sammelte, indem er eine Konsolenanwendung mit den angegebenen Befehlszeilenparametern startete.
  • Veeam.Builder.Agent - Ein von Veeam geschriebener WCF-Dienst, der die Anwendung mit Parametern im Konsolenmodus unter dem aktuellen Benutzer startet und den aktuellen Status der Anwendung zurückgibt.
  • Der IIS-Webdienst ist eine Anwendung in Windows Forms, mit der Sie den Shellset-Namen und die angegebenen Parameter eingeben und mit dem Erstellen eines privaten Fixes beginnen können.
  • Ein sehr geringes Programmierwissen ist C ++, ein bisschen C # an der Universität, und das Schreiben kleiner Anwendungen für die Automatisierung, das Hinzufügen neuer Funktionen zu aktuellen Erstellungsprozessen und als Hobby.
  • Erfahrene Kollegen, Google und indische MSDN-Artikel sind Antworten auf alle Fragen.

Was machen wir


In diesem Artikel werde ich erläutern, wie ich die Warteschlange für das Zusammenstellen von Fixes und deren sequentiellen Start im Builder implementiert habe. Hier sind die Teile der Lösung:

  • QBuilder.AppQueue ist mein WCF-Dienst, der die Arbeit mit der Erstellungswarteschlange bereitstellt und den Dienst Veeam.Builder.Agent aufruft, um das Erstellungsprogramm auszuführen.
  • dummybuild.exe ist ein Stub-Programm, das zum Debuggen und als visuelle Hilfe verwendet wird. Wird benötigt, um die übertragenen Parameter zu visualisieren.
  • QBuilder.AppLauncher - WCF-Dienst, der Anwendungen in der Konsole des aktuellen Benutzers ausführt und im interaktiven Modus arbeitet. Dies ist ein erheblich vereinfachtes Analogon des speziell für diesen Artikel geschriebenen Programms Veeam.Builder.Agent. Der ursprüngliche Dienst kann als Windows-Dienst arbeiten und Anwendungen in der Konsole ausführen, was zusätzliche Arbeit mit der Windows-API erfordert. Um alle Tricks zu beschreiben, wäre ein separater Artikel erforderlich. Mein Beispiel funktioniert als einfacher interaktiver Konsolendienst und verwendet zwei Funktionen: Starten eines Prozesses mit Parametern und Überprüfen seines Status.

Darüber hinaus haben wir eine neue praktische Webanwendung erstellt, die mit mehreren Buildern zusammenarbeiten und Ereignisprotokolle führen kann. Um den Artikel nicht zu überladen, werden wir auch nicht im Detail darauf eingehen. Darüber hinaus beschreibt dieser Artikel nicht die Arbeit mit TFS mit dem Speicherverlauf gesammelter privater Fixes und verschiedener Hilfsklassen und -funktionen.



WCF-Dienste erstellen


Es gibt viele detaillierte Artikel, die die Erstellung von WCF-Diensten beschreiben. Das Material von der Microsoft-Website hat mir am besten gefallen. Ich habe es als Grundlage für die Entwicklung genommen. Um mich mit dem Projekt vertraut zu machen, habe ich zusätzlich die Binärdateien angelegt . Fangen wir an!

Erstellen Sie den QBuilder.AppLauncher-Dienst


Hier haben wir nur die primäre CD des Dienstes. In dieser Phase müssen wir sicherstellen, dass der Dienst gestartet wird und funktioniert. Darüber hinaus ist der Code für QBuilder.AppLauncher und QBuilder.AppQueue identisch, sodass dieser Vorgang zweimal wiederholt werden muss.

  1. Erstellen Sie eine neue Konsolenanwendung mit dem Namen QBuilder.AppLauncher
  2. Benennen Sie Program.cs in Service.cs um
  3. Benennen Sie den Namespace in QBuilder.AppLauncher um
  4. Fügen Sie dem Projekt die folgenden Verweise hinzu:
    a. System.ServiceModel.dll
    b. System.ServiceProcess.dll
    c. System.Configuration.Install.dll
  5. Fügen Sie Service.cs die folgenden Definitionen hinzu

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

    Bei der weiteren Montage werden auch die folgenden Definitionen benötigt:

     using System.Reflection; using System.Xml.Linq; using System.Xml.XPath; 
  6. Wir definieren die IAppLauncher-Schnittstelle und fügen Funktionen für die Arbeit mit der Warteschlange hinzu:

     //    [ServiceContract(Namespace = "http://QBuilder.AppLauncher")]   public interface IAppLauncher   {    //             [OperationContract]       bool TestConnection();   } 
  7. In der AppLauncherService-Klasse implementieren wir die Schnittstelle und die Testfunktion TestConnection:

     public class AppLauncherService : IAppLauncher   {       public bool TestConnection()       {           return true;       }   } 
  8. Erstellen Sie eine neue AppLauncherWindowsService-Klasse, die die ServiceBase-Klasse erbt. Fügen Sie die lokale Variable serviceHost hinzu - einen Link zu ServiceHost. Wir definieren die Main-Methode, die ServiceBase.Run (neues AppLauncherWindowsService ()) aufruft:

     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. Überschreiben Sie die OnStart () - Funktion, mit der die neue ServiceHost-Instanz erstellt wird:

     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. Überschreiben Sie die onStop-Funktion, mit der die ServiceHost-Instanz geschlossen wird:

     protected override void OnStop()       {           if (serviceHost != null)           {               serviceHost.Close();               serviceHost = null;           }       }   } 
  11. Erstellen Sie eine neue ProjectInstaller-Klasse, die von Installer geerbt und mit RunInstallerAttribute markiert wurde, die auf True festgelegt ist. Auf diese Weise können Sie den Windows-Dienst mit dem Programm installutil.exe installieren:

     [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. Ändern Sie den Inhalt der Datei 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> 

Überprüfen der Wartungsfreundlichkeit des Dienstes


  1. Wir stellen den Service zusammen.
  2. Installieren Sie es mit dem Befehl installutil.exe
    1) Wechseln Sie in den Ordner, in dem sich die kompilierte Servicedatei befindet
    2) Führen Sie den Installationsbefehl aus:
    C: \ Windows \ Microsoft.NET \ Framework64 \ v4.0.30319 \ InstallUtil.exe
  3. Wir rufen das Snap-In services.msc auf, überprüfen die Verfügbarkeit des QBuilder App Launcher-Dienstes und führen ihn aus.
  4. Wir überprüfen die Wartungsfreundlichkeit des Dienstes mit dem Programm WcfTestClient.exe, das in VisualStudio enthalten ist:

    1) Führen Sie WcfTestClient aus
    2) Fügen Sie die Dienstadresse hinzu: http: // localhost: 8000 / QBuilderAppLauncher / service
    3) Die Serviceschnittstelle wird geöffnet:



    4) Wir rufen die Testfunktion TestConnection auf, prüfen, ob alles funktioniert und die Funktion gibt einen Wert zurück:


Nachdem wir eine funktionierende CD des Dienstes haben, fügen wir die Funktionen hinzu, die wir benötigen.

Warum brauche ich eine Testfunktion, die nichts bewirkt?


Als ich anfing zu lernen, wie man einen WCF-Dienst von Grund auf neu schreibt, las ich eine Reihe von Artikeln zu diesem Thema. Auf dem Tisch lag ein Dutzend oder zwei gedruckte Blätter, auf denen ich herausfinden konnte, was und wie. Ich gebe zu, ich habe es nicht geschafft, den Dienst sofort zu starten. Ich habe viel Zeit verbracht und bin zu dem Schluss gekommen, dass es wirklich wichtig ist, eine Service-CD zu erstellen. Damit sind Sie sicher, dass alles funktioniert und Sie können mit der Implementierung der erforderlichen Funktionen beginnen. Der Ansatz mag verschwenderisch erscheinen, aber er wird das Leben leichter machen, wenn ein Haufen geschriebener Code nicht so funktioniert, wie er sollte.

Fügen Sie die Möglichkeit hinzu, von der Konsole aus zu laufen


Zurück zur Anwendung. In der Debugging-Phase und in einer Reihe anderer Fälle muss der Dienst in Form einer Konsolenanwendung gestartet werden, ohne sich als Dienst zu registrieren. Dies ist eine sehr nützliche Funktion, mit der Sie auf den mühsamen Einsatz von Debuggern verzichten können. In diesem Modus funktioniert der Dienst QBuilder.AppLauncher. So implementieren Sie es:

  1. Fügen Sie die Prozedur RunInteractive zur AppLauncherWindowsService-Klasse hinzu, die den Dienst im Konsolenmodus bereitstellt:

     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. Wir nehmen Änderungen an der Hauptprozedur vor - fügen Sie die Verarbeitung von Befehlszeilenparametern hinzu. Mit der Option / console und einer geöffneten aktiven Benutzersitzung starten wir das Programm im interaktiven Modus. Andernfalls starten wir es als Service.

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

Fügen Sie Funktionen hinzu, um die Anwendung zu starten und ihren Status zu überprüfen


Der Service ist extrem einfach gemacht, es gibt keine zusätzlichen Überprüfungen. Er kann Anwendungen nur in der Konsolenversion und im Auftrag des Administrators ausführen. Es kann sie auch als Dienst starten - aber Sie werden sie nicht sehen, sie werden im Hintergrund gedreht und Sie können sie nur über den Task-Manager sehen. All dies kann implementiert werden, aber dies ist ein Thema für einen separaten Artikel. Die Hauptsache für uns hier ist ein klares Arbeitsbeispiel.

  1. Fügen Sie zunächst die globale Variable appProcess hinzu, in der der aktuell ausgeführte Prozess gespeichert wird.

    Fügen Sie es der public class AppLauncherService : IAppLauncher :

     public class AppLauncherService : IAppLauncher   {       Process appProcess; 
  2. Fügen Sie derselben Klasse eine Funktion hinzu, die den Status des laufenden Prozesses überprüft:

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

    Die Funktion gibt false zurück, wenn der Prozess nicht vorhanden ist oder noch nicht ausgeführt wird, und true, wenn der Prozess aktiv ist.
  3. Fügen Sie die Funktion zum Starten der Anwendung hinzu:

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

Die Funktion startet jede Anwendung mit Parametern. Die Parameter Domain und Benutzername werden in diesem Kontext nicht verwendet und sind möglicherweise leer, da der Dienst die Anwendung über die Konsolensitzung mit Administratorrechten startet.

Starten des QBuilder.AppLauncher-Dienstes


Wie zuvor beschrieben, arbeitet dieser Dienst interaktiv und ermöglicht es Ihnen, Anwendungen in der aktuellen Sitzung des Benutzers auszuführen und zu überprüfen, ob der Prozess ausgeführt wird oder bereits abgeschlossen ist.

  1. Zum Arbeiten benötigen Sie die Dateien QBuilder.AppLauncher.exe und QBuilder.AppLauncher.exe.config, die sich im Archiv unter dem obigen Link befinden. Dort befindet sich auch der Quellcode dieser Anwendung zur Selbstorganisation.
  2. Wir starten den Dienst mit Administratorrechten.
  3. Das Konsolenfenster des Dienstes wird geöffnet:



Jeder Tastendruck in der Servicekonsole schließt sie. Seien Sie vorsichtig.

  1. Führen Sie für Tests wcftestclient.exe aus, das in Visual Studio enthalten ist. Wir überprüfen die Verfügbarkeit des Dienstes unter http: // localhost: 8000 / QBuilderAppLauncher / service oder öffnen den Link in Internet Explorer.

Wenn alles funktioniert, fahren Sie mit dem nächsten Schritt fort.

Erstellen des QBuilder.AppQueue-Dienstes


Und jetzt kommen wir zum wichtigsten Dienst, für den der gesamte Artikel geschrieben wurde! Wir wiederholen die Abfolge der Aktionen im Kapitel „Erstellen des QBuilder.AppLauncher-Dienstes“ und im Kapitel „Hinzufügen des Starts über die Konsole“, wobei AppLauncher im Code durch AppQueue ersetzt wird.

Fügen Sie einen Link zum QBuilder.AppLauncher-Dienst zur Verwendung im Warteschlangendienst hinzu


  1. Wählen Sie im Projektmappen-Explorer für unser Projekt Dienstreferenz hinzufügen aus und geben Sie die Adresse an: localhost : 8000 / QBuilderAppLauncher / service
  2. Wählen Sie den Namensnamensraum aus: AppLauncherService.

Jetzt können wir von unserem Programm aus auf die Serviceschnittstelle zugreifen.

Erstellen Sie eine Struktur zum Speichern von Warteschlangenelementen


Fügen Sie im Namespace von QBuilder.AppQueue die Klasse QBuildRecord hinzu:

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

Implementieren der CXmlQueue-Warteschlangenklasse


Wir fügen unserem Projekt die Klasse CXmlQueue.cs hinzu, in der sich eine Reihe von Verfahren zum Arbeiten mit der XML-Datei befinden:

  • CXmlQueue-Konstruktor - Legt den Anfangsnamen der Datei fest, in der die Warteschlange gespeichert ist.
  • SetCurrentBuild - schreibt Informationen zum aktuellen Build in die XML-Warteschlangendatei. Dies ist ein Element, das sich nicht in der Warteschlange befindet. Es speichert Informationen über den aktuell ausgeführten Prozess. Kann leer sein.
  • GetCurrentBuild - Ruft die Parameter des laufenden Prozesses aus der XML-Datei der Warteschlange ab. Kann leer sein.
  • ClearCurrentBuild - Hiermit wird das aktuelle Build-Element in der XML-Warteschlangendatei gelöscht, wenn der Prozess beendet wird.
  • OpenXmlQueue - Funktion zum Öffnen der XML-Datei, in der die Warteschlange gespeichert ist. Wenn die Datei fehlt, wird eine neue erstellt.
  • GetLastQueueBuildNumber - Jeder Build in der Warteschlange hat eine eigene eindeutige Seriennummer. Diese Funktion gibt ihren Wert zurück, der im Root-Attribut gespeichert ist.
  • IncrementLastQueueBuildNumber - Erhöht den Wert der Build-Nummer, wenn ein neuer Build in die Warteschlange gestellt wird.
  • GetCurrentQueue - Gibt eine Liste der QBuildRecord-Elemente aus der XML-Warteschlangendatei zurück.

Im ursprünglichen Code wurden alle diese Prozeduren in die Hauptklasse eingeordnet, aber aus Gründen der Übersichtlichkeit habe ich eine separate Klasse CXmlQueue erstellt. Die Klasse wird im Namespace QBuilder.AppQueue erstellt. Überprüfen Sie, ob alle erforderlichen Definitionen angegeben sind:

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

Also implementieren wir. Die CXmlQueue-Klasse selbst:

Klicken Sie hier, um den Spoiler mit Code zu erweitern
 //      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; } } 


Die Warteschlange in der XML-Datei lautet wie folgt:

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

Erstellen Sie eine BuildQueue.xml-Datei mit diesem Inhalt und legen Sie sie im Verzeichnis mit der ausführbaren Datei ab. Diese Datei wird beim Test-Debugging verwendet, um die Testergebnisse abzugleichen.

Fügen Sie die AuxFunctions-Klasse hinzu


In dieser Klasse platziere ich Hilfsfunktionen. Derzeit gibt es nur eine Funktion, FormatParameters, die die Formatierung der Parameter durchführt, um sie zum Start an die Konsolenanwendung zu übergeben. Auflistung der Datei 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); } } } 

Fügen Sie der Serviceschnittstelle neue Funktionen hinzu


Die Testfunktion TestConnection kann zu diesem Zeitpunkt gelöscht werden. Um die Arbeit der Warteschlange zu implementieren, benötigte ich die folgenden Funktionen:

  • PushBuild (QBuildRecord): ungültig. Dies ist eine Funktion, die der Warteschlangen-XML-Datei mit QBuildRecord-Parametern einen neuen Wert hinzufügt
  • TestPushBuild (): ungültig. Dies ist eine Testfunktion, die einer Warteschlange in einer XML-Datei Testdaten hinzufügt.
  • PullBuild: QBuildRecord. Dies ist eine Funktion, die den QBuildRecord-Wert aus der Warteschlangen-XML-Datei abruft. Es kann leer sein.

Die Schnittstelle wird wie folgt aussehen:

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

Wir implementieren Funktionen in der AppQueueService: IAppQueue-Klasse:



Klicken Sie hier, um den Spoiler mit Code zu erweitern
 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" } ); } } 


Vornehmen von Änderungen an der AppQueueWindowsService: ServiceBase-Klasse


Fügen Sie dem Klassenkörper neue Variablen hinzu:

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

Fügen Sie im Konstruktor AppQueueWindowsService () Funktionen zum Lesen der Konfigurationsdatei, zum Initialisieren von Diensten und Warteschlangenklassen hinzu:

 //          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 - Timer-Antwortfrequenz. Wird in Millisekunden angezeigt. Hier stellen wir ein, dass der Timer alle 30 Sekunden ausgelöst werden soll. Im Original befindet sich dieser Parameter in der Konfigurationsdatei. Für den Artikel habe ich beschlossen, ihn in Code zu setzen.

Fügen Sie der Klasse die Funktion zum Überprüfen des laufenden Erstellungsprozesses hinzu:

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

Fügen Sie die Prozedur zum Arbeiten mit dem Timer hinzu:

  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> 

!

Ergebnisse


powershell- . C#. rulesets — , setup-. — , . — MD5- -, .


,

, — . , , . - .

, XML, , . , . , , .

, WCF-, XML-. :



PS , . , , .

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


All Articles