Wprowadzenie
Rozwijanie usługi systemowej stworzonej z domyślnego szablonu w Visual Studio nie jest prostym zajęciem. Szczególnie debugowanie takiej aplikacji sprawia sporo problemów. Nie można z poziomu IDE uruchomić usługi i jej debugować. Trzeba podpinać się pod działający proces. W przypadku gdy chcemy zdebugować start usługi, musimy posiłkować się różnymi dziwnymi konstrukcjami, które zatrzymają start usługi do momentu podpięcia się debugera.
W dzisiejszym wpisie będę chciał Ci pokazać, jak dzięki bibliotece Topshelf możemy uprościć sobie tworzenie usługi. Zamienimy aplikację konsolową w usługę systemową. Dzięki temu proces tworzenia oraz debugowania takiej usługi jest dużo łatwiejszy. W przykładzie stworzymy nową usługę, która uruchomi serwer z biblioteki Hangfire, którą opisywałem kilka tygodni temu.
Przykład do testów
W przykładzie przygotowałem prostą klasę, której zadaniem będzie pobieranie strony z przekazanego adresu do metody. Z racji tego, że jest to przykład, klasa nie będzie nic z tym robiła. Klasa wygląda tak:
public class Downloader : IDownloader | |
{ | |
public void Download(string url) | |
{ | |
var client = new WebClient(); | |
var content = client.DownloadString(url); | |
} | |
} | |
public interface IDownloader | |
{ | |
void Download(string url); | |
} |
Kod powyższej klasy będzie wykonywany przez serwer z Hangfire, który będzie działał w ramach tworzonej usługi z wykorzystaniem Topshelf. Zadanie do wykonania dodamy z poziomu aplikacji ASP.NET MVC, która będzie tylko je dodawała, bez jego wykonywania (w ramach niej nie będzie działał serwer Hangfire). Kod dodający zadanie do wykonania wygląda tak:
public class DownloadViewModel | |
{ | |
[Required] | |
public string Url { get; set; } | |
} |
public class HomeController : Controller | |
{ | |
private Downloader _downloader = new Downloader(); | |
public ActionResult Index() | |
{ | |
return View(); | |
} | |
public ActionResult Download() | |
{ | |
return View(new DownloadViewModel()); | |
} | |
[HttpPost] | |
public ActionResult Download(DownloadViewModel viewModel) | |
{ | |
if(ModelState.IsValid == false) | |
{ | |
return View(viewModel); | |
} | |
BackgroundJob.Enqueue(() => _downloader.Download(viewModel.Url)); | |
return RedirectToAction("Download"); | |
} | |
} |
Viewmodel przekazywany do akcji zawiera jedną właściwość (Url), za pomocą której przekazujemy adres do pobrania. W kontrolerze w drugiej akcji Download powiązanej z żądaniem typu POST dodajemy zadanie pobrania przekazanego adresu z wykorzystaniem klasy BackgroundJob i metody Enqueue.
Usługa w Topshelf
Topshelf (http://topshelf-project.com/) bardzo ułatwia tworzenie usługi systemowej w stosunku do standardowego szablonu z Visual Studio. Przede wszystkim usługa tworzona z wykorzystaniem biblioteki jest aplikacją konsolową. Dzięki temu możemy ją uruchamiać z poziomu Visual Studio, w szczególności w trybie debugowania, czego nie zrobimy ze standardowym projektem.
Korzystanie z biblioteki Topshelf jest bardzo proste. Po pierwsze instalujemy bibliotekę z Nugeta, po drugie tworzymy klasę, która będzie zawierała kod wykonywany w ramach usługi. Klasa nie musi być w żaden sposób powiązana z biblioteką Topshelf. Wystarczy, że będzie zawierała metody dla startu oraz zatrzymania usługi. Może również zawierać metody dla pozostałych akcji (np. pauzowanie czy wznawianie).
W przykładzie utworzyłem prostą klasę, która tworzy serwer Hangfire, startuje go i zatrzymuje podczas zatrzymywania usługi:
public class HangfireService | |
{ | |
private BackgroundJobServer _server; | |
static HangfireService() | |
{ | |
GlobalConfiguration.Configuration | |
.UseSqlServerStorage(@"Server=localhost\sqlexpress;Database=TopshelfExample;Trusted_Connection=True;"); | |
} | |
public void Start() | |
{ | |
var options = new BackgroundJobServerOptions(); | |
_server = new BackgroundJobServer(options); | |
} | |
public void Stop() | |
{ | |
if(_server != null) | |
{ | |
_server.Dispose(); | |
} | |
} | |
} |
W klasie zdefiniowany jest statyczny konstruktor, który ustawia connection string do bazy w Hangfire, w której znajdować się będą zadania dodane przez aplikację ASP.NET MVC. Metody Start oraz Stop uruchamiają i zatrzymują serwer Hangfire odpowiednio przy starcie oraz zatrzymaniu usługi.
W przykładzie kluczowy kod znajduje się w klasie Program. Tam wykorzystuje bibliotekę Topshelf do zdefiniowania usługi. W podstawowym użyciu kod wygląda tak:
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
HostFactory.Run(x => | |
{ | |
x.UseNLog(); | |
x.Service<HangfireService>(h => | |
{ | |
h.ConstructUsing(n => new HangfireService()); | |
h.WhenStarted(s => s.Start()); | |
h.WhenStopped(s => s.Stop()); | |
}); | |
x.RunAsLocalSystem(); | |
x.SetDescription("Hangfire Service"); | |
x.SetDisplayName("Hangfire Service"); | |
x.SetServiceName("Hangfire Service"); | |
}); | |
} | |
} |
Konfigurację usługi definiujemy w delegacie przekazanym do statycznej metody Run klasy HostFactory. W delegacie określamy przede wszystkim, jaka klasa (w przykładzie HangfireService) jest używana jako klasa usługi, oraz które metody mają zostać uruchomione w poszczególnych etapach życia usługi (Start oraz Stop). Wszystko to określamy w kolejnym delegacie przekazywanym do metody Service.
Poza samą definicją usługi w klasie Run możemy określić też inne rzeczy. W powyższym kodzie określam jeszcze:
- logowanie działania usługi z wykorzystaniem Nloga – wywołanie metody UseNlog,
- działanie usługi z wykorzystaniem konta lokalnego po zainstalowaniu – wywołanie metody RunAsLocalSystem,
- informacje wyświetlane między innymi w oknie z usługami systemowymi – wywołanie metod SetServiceName, SetDisplayName oraz SetDescritpion.
Biblioteka umożliwia określenie jeszcze wielu innych rzeczy związanych z działaniem usługi. Warto przejrzeć stronę z dokumentacją – https://topshelf.readthedocs.io/en/latest/index.html.
Uruchamianie oraz debugowanie
Jednym z plusów z korzystania z biblioteki Topshelf jest możliwość uruchomienia usługi bezpośrednio z poziomu Visual Studio. Wystarczy, że ustawimy projekt z usługą jako startowy i go uruchomimy. Poniżej znajduje się zrzut okna konsoli testowego przykładu:
Widać, że usługa wystartowała oraz w ramach niej wystartował serwer Hangfire (w przykładzie skonfigurowałem, żeby Nlog wyświetlał logi na konsoli).
Działa również debugowanie usługi uruchomionej z poziomu Visual Studio:
Instalacja usługi
Topshelf, obok ułatwienia tworzenia usługi systemowej, dodaje również do aplikacji kilka komend dostępnych z wiersza poleceń. Możemy na przykład za pomocą samej usługi zainstalować ją w systemie. Wystarczy uruchomić aplikację z komendą install. Można również przekazać różne parametry (np. nazwę usługi), które zostaną użyte podczas instalacji usługi. Gdy ich nie przekażemy, użyte zostaną domyślne wartości ustawione w kodzie.
Przykład instalacji widać poniżej na zrzucie ekranu:
Więcej informacji o dostępnych komendach i ich opcjach znajduje się w dokumentacji – https://topshelf.readthedocs.io/en/latest/overview/commandline.html.
Przykład
Na githubie znajduje się kod przykładu – https://github.com/danielplawgo/TopshelfExample. Przed uruchomieniem go należy ustawić poprawne connection stringi do bazy, w której Hangfire zapisuje zadania do wykonania. Connection string znajduje się w dwóch miejscach: w klasie Startup w projekcie TopshelfExample.Web oraz HangfireService w projekcie TopshelfExample.Service.
Podsumowanie
Topshelf bardzo ułatwia proces tworzenia usługi systemowej. Możliwość utworzenia jej z aplikacji konsolowej powoduje, że usługę możemy uruchamiać bezpośrednio z poziomu Visual Studio. To ułatwia jej debugowanie i analizowanie działania usługi.
A Ty lubisz tworzyć usługi systemowe?
Jeśli chodzi o debugowanie usług Windows w VS to pamiętam że dawało się to prosto zrobić. Parę lat tego już nie używałem więc podaje pierwszy link jaki znalazłem.
https://docs.microsoft.com/pl-pl/dotnet/framework/windows-services/how-to-debug-windows-service-applications
Add a method to your service that runs the OnStart and OnStop methods:
internal void TestStartupAndStop(string[] args)
{
this.OnStart(args);
Console.ReadLine();
this.OnStop();
}
Rewrite the Main method as follows:
static void Main(string[] args)
{
if (Environment.UserInteractive)
{
MyNewService service1 = new MyNewService(args);
service1.TestStartupAndStop(args);
}
else
{
// Put the body of your old Main method here.
}
}
In the Application tab of the project’s properties, set the Output type to Console Application.
Choose Start Debugging (F5).
To run the program as a Windows Service again, install it and start it as usual for a Windows Service. It’s not necessary to reverse these changes.