Najpierw powoli jak żółw ociężale – czyli tworzymy projekt

No to startujemy.

Na początku pokażę wstępne przygotowanie projektów. Oczywiście w trakcie tworzenia aplikacji może się to zmienić, ale mimo wszystko na początku dobrze jest mieć jakiś wstępny podział.

System planuję podzielić na projekty z WebApi, warstwą dostępu do danych, logiką biznesową oraz aplikacją mobilną. Oczywiście nie zapomnę o testach jednostkowych, jednak nie przewiduję prowadzenia projektu w TDD, raczej pokryję testami logikę biznesową.

Czytaj dalej Najpierw powoli jak żółw ociężale – czyli tworzymy projekt

Let’s get it started – DSP2017

Cześć i czołem Drogi Czytelniku,

O DSP dowiedziałem się w tamtym roku, niestety z różnych powodów nie dane mi było wzięcie udziału w edycji 2016.

W tym roku uzbrojony w silne postanowienie sprostania wymaganiom konkursu oraz chęć upieczenia dwóch pieczeni na jednym ogniu (o tym później) postanowiłem się zgłosić do tegorocznej edycji.

Na pomysł projektu wpadłem podczas lektury książki Finansowy Ninja, Michała Szafrańskiego. Nie każdy ma ochotę na spisywanie zawartości paragonów, ja na pewno nie mam. W związku z tym chciałbym mieć aplikację, która pozwoli to robić względnie automatycznie, a że w planach mam pracę magisterską związaną z technikami OCR postanowiłem to połączyć. Z jakim efektem? Zobaczymy 😉

Planowany system pozwoli użytkownikowi na wygodne prowadzenie spisu wydatków na podstawie zdjęć paragonów. W jego skład wejdzie:

  • System OCR z różnymi metodami rozpoznawania obrazu
  • Aplikacja ASP.MVC udostępniająca WebAPI
  • Aplikacja na system Android w oparciu o Xamarin Framework

Aplikacja mobilna po zalogowaniu pozwoli na przeglądanie zawartości dodanych paragonów oraz ich zdjęć. Po zrobieniu zdjęcia paragonowi i jego zatwierdzeniu zostanie on przesłany do aplikacji działającej na serwerze w celu rozpoznania znaków. W odpowiedzi użytkownik uzyska zawartość odczytanego paragonu do akceptacji, po czym zostanie on zapisany w jego profilu. Funkcjonalnością do dalszych rozważań i rozwoju jest rozbijanie wczytanego kwitku na poszczególne pozycje.

 

Do zobaczenia wkrótce.

Uruchamianie usług z parametrami wywołania

Ostatnio podczas konfiguracji kontenera Dockera natrafiłem na problem z instalacją i uruchamianiem własnych usług Windows. Próbowałem między innymi zainstalować Remote Debugger, aby ułatwić sobie wykorzystanie kontenerów jako środowiska developerskiego. Niestety okazało się, że parametry wywołania (np. /noauth), które działają przy ręcznym uruchomieniu aplikacji nie są akceptowane.

W związku z wspomnianym problemem postanowiłem napisać mały program, który będzie działał jako usługa i umożliwiał uruchamianie aplikacji z dowolnymi parametrami. Przy okazji przypomniałem w jaki sposób się to robi. Szczegóły poniżej.

 

Program będzie miał za zadanie uruchamiać i zamykać aplikacje, które zostaną podane w jego pliku konfiguracyjnym.

Jak w przypadku każdego nowego projektu musimy go najpierw utworzyć. W MS Visual Studio tworzymy aplikację konsolową (Console Application).

Modyfikujemy plik Program.cs tak, by znajdująca się w nim klasa dziedziczyła po klasie ServiceBase. Wymagane będzie dodanie referencji do biblioteki System.ServiceProcess oraz dopisanie linijki:

using System.ServiceProcess;

Następnie dodajemy klasy, które będą tworzyły sekcję konfiguracyjną przechowującą listę programów z parametrami, przeznaczonymi do uruchomienia podczas startu naszej usługi:

  • ServiceEntry -> pojedynczy wpis w konfiguracji
  public class ServiceEntry : ConfigurationElement
  {
    [ConfigurationProperty("Name", IsKey = true, IsRequired = true)]
    public string Name
    {
      get { return this["Name"] as string; }
      set { this["Name"] = value; }
    }

    [ConfigurationProperty("Path", IsKey = false, IsRequired = true)]
    public string Path
    {
      get { return this["Path"] as string; }
      set { this["Path"] = value; }
    }

    [ConfigurationProperty("Params", IsKey = false, IsRequired = false)]
    public string Params
    {
      get { return this["Params"] as string; }
      set { this["Params"] = value; }
    }
  }
  • ServiceEntryCollection -> kolekcja wpisów konfiguracyjnych
  public class ServiceEntryCollection : ConfigurationElementCollection
  {
    protected override ConfigurationElement CreateNewElement()
    {
      return new ServiceEntry();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
      if (element == null)
        throw new ArgumentNullException("element");

      return ((ServiceEntry)element).Name;
    }
  }
  • ServicesToRunSection -> sekcja konfiguracyjna
  public class ServicesToRunSection : ConfigurationSection
  {
    public const string SectionName = "servicesToRun";

    [ConfigurationProperty("", IsDefaultCollection = true)]
    public ServiceEntryCollection ServiceEntryCollection
    {
      get
      {
        return this[""] as ServiceEntryCollection;
      }
    }

    public static ServicesToRunSection GetConfig()
    {
      return ConfigurationManager.GetSection(SectionName) as ServicesToRunSection ?? new ServicesToRunSection();
    }
  }

Po dodaniu zmodyfikujmy plik konfiguracyjny zgodnie ze schematem z klas.

Dodajemy informację o wykorzystywanej sekcji do pliku App.config do tagu <configSections>:

  <configSections>
    <section name="servicesToRun" type="ServiceStarter.ServicesToRunSection, ServiceStarter"/>
  </configSections>

oraz samą sekcję:

<configuration>
...
  <servicesToRun>
    <add Name="ping" Path="ping" Params="-t 8.8.8.8" /> <!-- będziemy pingować po starcie systemu -->
  </servicesToRun>
...
</configuration>

Zakończyliśmy właśnie wstępną konfigurację aplikacji do uruchomienia.

Skoro mamy już konfigurację wrócimy teraz do samej usługi.

Otwieramy plik z klasą dziedziczącą po ServiceBase.

W konstruktorze ustawiamy nazwę usługi i jej inne parametry. Dodajemy także statyczną listę otwartych procesów.

    private static List<Process> _processes;

    public ServiceStarterService()
    {
      _processes = new List<Process>();

      ServiceName = "ServiceStarter";
      CanStop = true;
      CanPauseAndContinue = false;
    }

Przeciążamy metody OnStart oraz OnStop.

W OnStart będziemy wczytywali z pliku konfiguracyjnego listę aplikacji do uruchomienia oraz je startowali.

    protected override void OnStart(string[] args)
    {
      var config = ServicesToRunSection.GetConfig();

      var data = config.ServiceEntryCollection;

      foreach (ServiceEntry entry in data)
      {
        var process = Process.Start(entry.Path, entry.Params);
        _processes.Add(process);
      }
    }

OnStop wykorzystamy do zatrzymania uruchomionych procesów oraz posprzątania po sobie.

    protected override void OnStop()
    {
      foreach (var process in _processes)
      {
        if (!process.HasExited)
          process.Close();
      }

      _processes.Clear();
    }

W metodzie Main wskazujemy, że chcemy uruchomić naszą klasę:

    static void Main(string[] args)
    {
      Run(new ServiceStarterService());
    }

Pozostało nam tylko zainstalować nową usługę. Potrzebny będzie nam do tego instalator, czyli klasa dziedzicząca po klasie Installer. Aby wykorzystać ten instalator musimy go oznaczyć atrybutem [RunInstaller(true)].

  [RunInstaller(true)]
  public class ServiceStarterServiceInstaller : Installer
  {
    public ServiceStarterServiceInstaller()
    {
      var serviceProcessInstaller = new ServiceProcessInstaller();
      var serviceInstaller = new ServiceInstaller();

      serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
      serviceProcessInstaller.Username = null;
      serviceProcessInstaller.Password = null;

      serviceInstaller.DisplayName = "ServiceStarter";
      serviceInstaller.StartType = ServiceStartMode.Automatic;

      serviceInstaller.ServiceName = "ServiceStarter";

      Installers.Add(serviceProcessInstaller);
      Installers.Add(serviceInstaller);
    }
  }

 

Po skompilowaniu usługę instalujemy przy pomocy narzędzia InstallUtil.exe. Opis narzędzie i jego użycia znajdziecie pod poniższymi linkami:

Pełen kod znajdziecie po kliknięciu w odnośnik.

TFS – walidacja komentarzy do commitów gita

Ostatnio natrafiłem na ciekawy problem związany z obsługą gita przez Team Foundation Server. W związku z zapominaniem o  wpisywaniu numeru zadania z TFS chciałem to wymusić. Po zapytaniu Wujka Google okazało się, że o ile git sam w sobie wspiera walidację komentarzy do commitów, to TFS niestety takiej funkcjonalności nie udostępnia.

Nie pozostawało mi nic innego niż napisać własny plugin, który by na to pozwalał. W trakcie poszukiwań informacji na ten temat natrafiłem na post Karstena Kemple (link), który rozwiązywał problem dla TFS 2013. Podczas testów rozwiązania na maszynie wirtualnej wyszło na jaw, że tekst, który przypiszemy do parametru out string statusMessage nie zostaje przekazany do klienta gita, który wysyłał kod na serwer. Powodowało to, że użytkownik niestety nie otrzymywał dokładnej informacji zwrotnej czemu jego commit zostawał odrzucony przez zdalną maszynę.

Postanowiłem, więc zaktualizować TFS do wersji 2015 Update 3 aby sprawdzić zachowanie pluginu. Po podmianie bibliotek podanych przez Karstena na nowsze okazało się, że zmieniła się definicja metody ProcessEvent interfejsu ISubscriber. W związku z tym czekało mnie napisanie pluginu właściwie od nowa. Poniżej przedstawiam ten proces.

1. Utwórz nowy projekt typu Class Library

 

2. Następnie dodaj do niego następujace biblioteki

 

3. Dodaj klasę, która implementuje interfejs ISubscriber

Deklaracja:

public interface ISubscriber
  {
    string Name { get; }
    SubscriberPriority Priority { get; }

    EventNotificationStatus ProcessEvent(IVssRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties);
    Type[] SubscribedTypes();
  }
  • Name – nazwa subskrybenta, powinna być unikatowa
  • Priority – priorytet wykonania
  • SubscribedTypes – typy powiadomień, które subskrybent ma obsługiwać
  • ProcessEvent – metoda wywoływana gdy obsługiwane powiadomienie jest publikowane

4. Zaimplementuj metodę ProcessEvent

5. Gdy commit nie spełni wymagań walidacji zwróć EventNotificationStatus.ActionDenied

Jeśli przypiszesz do pola statusMessage jakiś tekst, zostanie on wyświetlony użytkownikowi, który wrzuca kod na zdalne repozytorium

6. Aby zainstalować plugin skopiuj plik .dll będący wynikiem kompilacji projektu do katalogu (katalog główny serwera TFS)\Application Tier\Web Services\bin\Plugins 

TFS powinien wykryć skopiowaną dllkę i automatycznie ją wczytać, ale dla pewności możesz zrestartować pulę aplikacji do niego przypisaną, ewentualnie cały serwer IIS.