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.