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:
- https://msdn.microsoft.com/pl-pl/library/50614e95(v=vs.110).aspx
- https://msdn.microsoft.com/pl-pl/library/sd8zc8ha(v=vs.110).aspx
Pełen kod znajdziecie po kliknięciu w odnośnik.