Cristi
Cristi

Reputation: 1328

.NET Core application hanging when using Topshelf with IApplicationHostLifetime.StopApplication()

I have a .NET Core application that is running as a Windows service using Topshelf. What I want to do is for the service to be able to stop itself on some trigger (in my test app, a few seconds after startup). The problem is that my app seems to hang on startup and I'm not quite happy with the only solution I found so far.

This is the simplest code sample I could come up with. You might notice the slightly weird CreateAndStartHost(), that one is mostly copy/pasted from our actual app, so we can't really change that. The Run() extension method is basically just a wrapper over Task.Factory.StartNew().

The only thing that worked so far is to call and not wait _host.StopAsync() in ApplicationStopping.Register(() => { ... }) and then set some flag in ApplicationStopped.Register(() => { ... }) which will then be waited for in TestService.Stop(). The problem with this approach is that I would still like to be able to await StopAsync() inside TestService.Stop() and saving it in a Task feels a bit too hacky.

// Program.cs - Create and run Topshelf host
HostFactory.New(x =>
{
    x.Service<TestService>(sc =>
    {
        sc.ConstructUsing(() => new TestService());
        sc.WhenStarted((s, hostControl) => s.Start(hostControl));
        sc.WhenStopped((s, hostControl) => s.Stop(hostControl));
        sc.WhenShutdown((s, hostControl) =>
        {
            Log.Information("On TopShelf Shutdown");
            s.Stop(hostControl);
        });
    });
    x.SetDescription("TopshelfWithDotNetDi");
    x.SetDisplayName("TopshelfWithDotNetDi");
    x.SetServiceName("TopshelfWithDotNetDi");
    x.RunAsLocalSystem();
    x.StartAutomaticallyDelayed();
    x.EnableShutdown();
    x.UnhandledExceptionPolicy = UnhandledExceptionPolicyCode.LogErrorOnly;
    x.OnException(ex =>
    {
        Log.Information(ex.Message);
    });
}).Run();

// TestServices.cs
public class TestService : ServiceControl
{
    private IHost _host;

    public bool Start(HostControl hostControl)
    {
        Log.Information("Starting host...");

        _host = CreateAndStartHost().GetAwaiter().GetResult();

        var hostApplicationLifetime = _host.Services.GetService<IHostApplicationLifetime>();
        hostApplicationLifetime.ApplicationStopping.Register(() => { hostControl.Stop(); });
        hostApplicationLifetime.ApplicationStopped.Register(() => { Log.Information("Application stopped..."); });

        return true;
    }

    public bool Stop(HostControl hostControl)
    {
        Log.Information("Stopping host...");

        var stopTask = _host.StopAsync();
        while (!stopTask.Wait(1000)) // <-- PROBLEM: This will loop forever
        {
            hostControl.RequestAdditionalTime(TimeSpan.FromSeconds(1));
        }

        return true;
    }

    private static Task<IHost> CreateAndStartHost()
    {
        // Can't really change this bit, sorry
        var taskScheduler = new AsyncContextThread().Factory.Scheduler;
        return taskScheduler.Run(async () =>
        {
            var host = new HostBuilder()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<MyHostedService>();
                }).Build();

            await host.StartAsync();
            return host;
        });
    }
}

// MyHostedService will just call _applicationLifetimeHost.StopApplication()
// after a 5s loop, so I'm not including this bit in the sample

Upvotes: 0

Views: 393

Answers (1)

TheHvidsten
TheHvidsten

Reputation: 4418

This might not be the answer you're looking for, but I wouldn't use Topshelf. It seems like an abandoned project (latest commit three years ago).

You don't need Topshelf to be able to run .NET in a Windows service as this is supported natively:

Documentation

Upvotes: 1

Related Questions