Vytvoření služby systému Windows pomocí BackgroundService

Vývojáři rozhraní .NET Framework jsou pravděpodobně obeznámeni s aplikacemi služby pro Windows. Před .NET Core a .NET 5+ mohou vývojáři, kteří se spoléhali na rozhraní .NET Framework, vytvářet služby Windows k provádění úloh na pozadí nebo spouštět dlouhotrvající procesy. Tato funkce je stále dostupná a můžete vytvořit služby pracovního procesu, které běží jako služba systému Windows.

V tomto kurzu se naučíte:

  • Publikujte pracovní aplikaci .NET jako jeden spustitelný soubor.
  • Vytvořte službu systému Windows.
  • BackgroundService Vytvořte aplikaci jako službu pro Windows.
  • Spusťte a zastavte službu systému Windows.
  • Zobrazení protokolů událostí
  • Odstraňte službu systému Windows.

Tip

Všechny ukázkové zdrojové kódy Pracovních procesů v .NET jsou k dispozici v prohlížeči ukázek ke stažení. Další informace najdete v tématu Procházení ukázek kódu: Pracovní procesy v .NET.

Důležité

Instalace sady .NET SDK také nainstaluje Microsoft.NET.Sdk.Worker šablonu pracovního procesu. Jinými slovy, po instalaci sady .NET SDK můžete vytvořit nový pracovní proces pomocí příkazu dotnet new worker . Pokud používáte Visual Studio, šablona se skryje, dokud se nenainstaluje volitelná ASP.NET a úloha vývoje webu.

Požadavky

Vytvoření nového projektu

Pokud chcete vytvořit nový projekt Služby pracovního procesu pomocí sady Visual Studio, vyberte Soubor>nový>projekt.... V dialogovém okně Vytvořit nový projekt vyhledejte "Pracovní služba" a vyberte šablonu pracovní služby. Pokud raději použijete .NET CLI, otevřete svůj oblíbený terminál v pracovním adresáři. dotnet new Spusťte příkaz a nahraďte název požadovaného <Project.Name> projektu.

dotnet new worker --name <Project.Name>

Další informace o novém příkazu pracovního procesu rozhraní příkazového řádku .NET CLI najdete v tématu dotnet new worker.

Tip

Pokud používáte Visual Studio Code, můžete z integrovaného terminálu spustit příkazy .NET CLI. Další informace naleznete v tématu Visual Studio Code: Integrovaný terminál.

Instalace balíčku NuGet

Pokud chcete spolupracovat s nativními službami Windows z implementací .NET IHostedService , budete muset nainstalovat Microsoft.Extensions.Hosting.WindowsServices balíček NuGet.

Pokud chcete tuto instalaci nainstalovat ze sady Visual Studio, použijte dialogové okno Spravovat balíčky NuGet... Vyhledejte "Microsoft.Extensions.Hosting.WindowsServices" a nainstalujte ho. Pokud byste raději použili .NET CLI, spusťte dotnet add package příkaz:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Další informace o příkazu pro přidání balíčku rozhraní .NET CLI najdete v tématu dotnet add package.

Po úspěšném přidání balíčků by teď soubor projektu měl obsahovat následující odkazy na balíčky:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>

Aktualizace souboru projektu

Tento pracovní projekt využívá odkazové typy C#s možnou hodnotou null. Pokud je chcete povolit pro celý projekt, aktualizujte soubor projektu odpovídajícím způsobem:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

Předchozí soubor projektu změní <Nullable>enable<Nullable> uzel. Další informace naleznete v tématu Nastavení kontextu s možnou hodnotou null.

Vytvoření služby

Přidejte do projektu novou třídu s názvem JokeService.cs a nahraďte její obsah následujícím kódem jazyka C#:

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

Předchozí kód zdroje služby vtipu zveřejňuje jednu část funkčnosti, metodu GetJoke . Jedná se o návratovou metodu string , která představuje náhodný programovací vtip. Pole s oborem _jokes třídy slouží k uložení seznamu vtipů. Náhodný vtip je vybrán ze seznamu a vrácen.

Přepsání Worker třídy

Nahraďte existující Worker ze šablony následujícím kódem jazyka C# a přejmenujte soubor na WindowsBackgroundService.cs:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

V předchozím kódu JokeService se vloží spolu s znakem ILogger. Obě jsou k dispozici pro třídu jako private readonly pole. ExecuteAsync V metodě služba vtip požádá o vtip a zapíše ho do protokolovacího nástroje. V tomto případě je protokolovací nástroj implementován protokolem událostí systému Windows - Microsoft.Extensions.Logging.EventLog.EventLogLogger. Protokoly se zapisují a jsou k dispozici pro zobrazení v Prohlížeč událostí.

Poznámka:

Ve výchozím nastavení je Warningzávažnost protokolu událostí . To je možné nakonfigurovat, ale pro demonstrační účely protokoly WindowsBackgroundService s metodou LogWarning rozšíření. Pokud chcete konkrétně cílit na EventLog úroveň, přidejte do appsettings položku.{ Environment}.json nebo zadejte EventLogSettings.Filter hodnotu.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Další informace o konfiguraci úrovní protokolu naleznete v tématu Zprostředkovatelé protokolování v .NET: Konfigurace protokolu událostí systému Windows.

Přepsání Program třídy

Obsah souboru Program.cs šablony nahraďte následujícím kódem jazyka C#:

using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = ".NET Joke Service";
});

LoggerProviderOptions.RegisterProviderOptions<
    EventLogSettings, EventLogLoggerProvider>(builder.Services);

builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();

IHost host = builder.Build();
host.Run();

Metoda AddWindowsService rozšíření nakonfiguruje aplikaci tak, aby fungovala jako služba systému Windows. Název služby je nastaven na ".NET Joke Service"hodnotu . Hostovaná služba je zaregistrovaná pro injektáž závislostí.

Další informace o registraci služeb naleznete v tématu Injektáž závislostí v .NET.

Publikování aplikace

Pokud chcete vytvořit aplikaci .NET Worker Service jako službu systému Windows, doporučujeme aplikaci publikovat jako spustitelný soubor. Je méně náchylné k chybám mít samostatný spustitelný soubor, protože neexistují žádné závislé soubory kolem systému souborů. Můžete ale zvolit jiný způsob publikování, což je naprosto přijatelné, pokud vytvoříte soubor *.exe , který může být cílem Správce řízení služeb systému Windows.

Důležité

Alternativním přístupem k publikování je sestavení knihovny *.dll (místo *.exe) a při instalaci publikované aplikace pomocí Správce řízení služeb systému Windows, který delegujete na rozhraní příkazového řádku .NET a předáte knihovnu DLL. Další informace najdete v tématu .NET CLI: příkaz dotnet.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
    <OutputType>exe</OutputType>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

Předchozí zvýrazněné řádky souboru projektu definují následující chování:

  • <OutputType>exe</OutputType>: Vytvoří konzolovou aplikaci.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: Povolí publikování s jedním souborem.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: Určuje identifikátor RID .win-x64
  • <PlatformTarget>x64</PlatformTarget>: Zadejte procesor cílové platformy 64bitové verze.

Pokud chcete aplikaci publikovat ze sady Visual Studio, můžete vytvořit profil publikování, který je trvalý. Profil publikování je založený na jazyce XML a má příponu souboru .pubxml . Visual Studio používá tento profil k implicitní publikování aplikace, zatímco pokud používáte rozhraní příkazového řádku .NET – musíte explicitně zadat profil publikování, který se má použít.

Klikněte pravým tlačítkem myši na projekt v Průzkumník řešení a vyberte Publikovat.... Pak vyberte Přidat profil publikování a vytvořte profil. V dialogovém okně Publikovat vyberte jako cíl složku.

The Visual Studio Publish dialog

Ponechte výchozí umístění a pak vyberte Dokončit. Po vytvoření profilu vyberte Zobrazit všechna nastavení a ověřte nastavení profilu.

The Visual Studio Profile settings

Ujistěte se, že jsou zadána následující nastavení:

  • Režim nasazení: Samostatné
  • Vytvoření jednoho souboru: zaškrtnuto
  • Povolit kompilaci ReadyToRun: zaškrtnuto
  • Oříznutí nepoužívaných sestavení (ve verzi Preview): Nezaškrtnuto

Nakonec vyberte Publikovat. Aplikace se zkompiluje a výsledný soubor .exe se publikuje do výstupního adresáře /publish .

Případně můžete k publikování aplikace použít rozhraní příkazového řádku .NET:

dotnet publish --output "C:\custom\publish\directory"

Další informace najdete na webu dotnet publish.

Důležité

Pokud se v .NET 6 pokusíte aplikaci ladit pomocí <PublishSingleFile>true</PublishSingleFile> nastavení, nebudete moct aplikaci ladit. Další informace naleznete v tématu Nejde připojit k CoreCLR při ladění aplikace PublishSingleFile .NET 6.

Vytvoření služby systému Windows

Pokud nejste zvyklí používat PowerShell a raději byste pro službu vytvořili instalační program, přečtěte si téma Vytvoření instalačního programu služby systému Windows. V opačném případě vytvořte službu Systému Windows pomocí nativního příkazu Správce řízení služeb systému Windows (sc.exe) create. Spusťte PowerShell jako správce.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe"

Tip

Pokud potřebujete změnit kořen obsahu konfigurace hostitele, můžete ho předat jako argument příkazového řádku při zadávání binpathpříkazu :

sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path"

Zobrazí se výstupní zpráva:

[SC] CreateService SUCCESS

Další informace naleznete v tématu sc.exe create.

Konfigurace služby systému Windows

Po vytvoření služby ji můžete volitelně nakonfigurovat. Pokud máte výchozí nastavení služby v pořádku, přejděte do části Ověření funkčnosti služby.

Služby Windows poskytují možnosti konfigurace obnovení. Aktuální konfiguraci můžete dotazovat pomocí sc.exe qfailure "<Service Name>" příkazu (where <Service Name> is your services' name) ke čtení aktuálních hodnot konfigurace obnovení:

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :

Příkaz zobrazí výstup konfigurace obnovení, což jsou výchozí hodnoty, protože ještě nejsou nakonfigurované.

The Windows Service recovery configuration properties dialog.

Pokud chcete nakonfigurovat obnovení, použijte sc.exe failure "<Service Name>" místo, kde <Service Name> je název vaší služby:

sc.exe failure ".NET Joke Service" reset=0 actions=restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS

Tip

Pokud chcete nakonfigurovat možnosti obnovení, musí vaše relace terminálu běžet jako Správa istrator.

Po úspěšné konfiguraci můžete znovu zadat dotaz na hodnoty pomocí sc.exe qfailure "<Service Name>" příkazu:

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :
        FAILURE_ACTIONS              : RESTART -- Delay = 60000 milliseconds.
                                       RESTART -- Delay = 60000 milliseconds.
                                       RUN PROCESS -- Delay = 1000 milliseconds.

Zobrazí se nakonfigurované hodnoty restartování.

The Windows Service recovery configuration properties dialog with restart enabled.

Možnosti obnovení služby a instance .NET BackgroundService

V rozhraní .NET 6 byly do .NET přidány nové chování zpracování výjimek hostování. Výčet BackgroundServiceExceptionBehavior byl přidán do Microsoft.Extensions.Hosting oboru názvů a slouží k určení chování služby při vyvolání výjimky. V následující tabulce jsou uvedené dostupné možnosti:

Možnost Popis
Ignore Ignorovat výjimky vyvolané v BackgroundService.
StopHost Při IHost vyvolání neošetřené výjimky se zastaví.

Výchozí chování před .NET 6 je Ignore, což způsobilo zombie procesy (spuštěný proces, který nic neudělal). U .NET 6 je StopHostvýchozí chování , což vede k zastavení hostitele při vyvolání výjimky. Ale zastaví se čistě, což znamená, že systém pro správu služby systému Windows nerestartuje službu. Chcete-li správně povolit restartování služby, můžete volat Environment.Exit nenulový ukončovací kód. Zvažte následující zvýrazněný catch blok:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

Ověření funkčnosti služby

Aplikaci vytvořenou jako službu Windows zobrazíte tak, že otevřete služby. Vyberte klávesu Windows (nebo Ctrl + Esc) a vyhledejte v části Služby. V aplikaci Services byste měli být schopni vaši službu najít podle jejího názvu.

Důležité

Ve výchozím nastavení nemůžou běžní uživatelé (bez oprávnění správce) spravovat služby systému Windows. Pokud chcete ověřit, že tato aplikace funguje podle očekávání, budete muset použít účet Správa.

The Services user interface.

Pokud chcete ověřit, že služba funguje podle očekávání, musíte:

  • Spuštění služby
  • Zobrazení protokolů
  • Zastavení služby

Důležité

Pokud chcete aplikaci ladit, ujistěte se, že se nepokoušejte ladit spustitelný soubor, který je aktivně spuštěný v rámci procesu služeb systému Windows.

Unable to start program.

Spuštění služby systému Windows

Ke spuštění služby systému Windows použijte sc.exe start příkaz:

sc.exe start ".NET Joke Service"

Zobrazí se výstup podobný následujícímu:

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 2  START_PENDING
                            (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x7d0
    PID                : 37636
    FLAGS

Stav služby bude mimo provozSTART_PENDING.

Zobrazení protokolů

Pokud chcete zobrazit protokoly, otevřete Prohlížeč událostí. Vyberte klávesu Windows (nebo Ctrl + Esc) a vyhledejte "Event Viewer". Vyberte uzel aplikace Prohlížeč událostí (místní)>Protokoly> systému Windows. Měla by se zobrazit položka na úrovni upozornění se zdrojem, který odpovídá oboru názvů aplikací. Poklikejte na položku nebo klikněte pravým tlačítkem myši a vyberte Vlastnosti události a zobrazte podrobnosti.

The Event Properties dialog, with details logged from the service

Po zobrazení protokolů v protokolu událostí byste měli službu zastavit. Je navržený tak, aby protokoloval náhodný vtip jednou za minutu. Jedná se o záměrné chování, ale není praktické pro produkční služby.

Zastavení služby systému Windows

Pokud chcete službu Systému Windows zastavit, použijte příkaz sc.exe stop :

sc.exe stop ".NET Joke Service"

Zobrazí se výstup podobný následujícímu:

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 3  STOP_PENDING
                            (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x0

Stav služby přejde z STOP_PENDING do stavu Zastaveno.

Odstranění služby systému Windows

Chcete-li odstranit službu systému Windows, použijte nativní příkaz Správce řízení služeb systému Windows (sc.exe) delete příkaz. Spusťte PowerShell jako správce.

Důležité

Pokud služba není ve stavu Zastaveno , neodstraní se okamžitě. Před vydáním příkazu delete se ujistěte, že je služba zastavená.

sc.exe delete ".NET Joke Service"

Zobrazí se výstupní zpráva:

[SC] DeleteService SUCCESS

Další informace naleznete v tématu sc.exe delete.

Viz také

Další