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.