.net-coretopshelf

dotnet core TopShelf Windows Service fails to start


I have a dotnet core console application build to connect to a Sql Service Broker instance to monitor table changes.

The app monitors one table that is updated from an ERP system and then publishes messages to our bus.

It runs fine when running as a console application, or debugging in my IDE.

I am having an issue when using TopShelf to configure it as a windows service.

Here is the entry point:

        private static void Main(string[] args)
       {
        RegisterComponents();

        var serviceHost = HostFactory.Run(sc =>
        {
            sc.Service<ISalesOrderMonitorService>(s =>
            {
                var sqlListener = _container.ResolveNamed<SqlDependencyEx>(ListenerKey.SalesOrder);
                var changeHandler = _container.Resolve<ISalesOrderChangeHandler>();
                var listenerConfig = _container.ResolveNamed<ListenerConfiguration>(ListenerKey.SalesOrder);
                var logger = _container.Resolve<ILogger<SalesOrder>>();

                s.ConstructUsing(f =>
                    new SalesOrderMonitorService(sqlListener, changeHandler, listenerConfig, logger));

                s.WhenStarted(tc => tc.Start());
                s.WhenStopped(tc => tc.Stop());
            });
        });

        var exitCode = (int) Convert.ChangeType(serviceHost, serviceHost.GetType());

        Environment.ExitCode = exitCode;
    }

The "worker" class:

    public abstract class ServiceBase<T, TZ> : IService<T>
    where T : IChangeHandler
{
    protected readonly IChangeHandler ChangeHandler;
    protected readonly SqlDependencyEx Listener;
    protected readonly ListenerConfiguration ListenerConfiguration;
    protected readonly ILogger<TZ> Logger;

    protected ServiceBase(SqlDependencyEx listener, IChangeHandler changeHandler,
        ListenerConfiguration listenerConfiguration, ILogger<TZ> logger)
    {
        Logger = logger;
        ListenerConfiguration = listenerConfiguration;
        Listener = listener;
        ChangeHandler = changeHandler;
    }

    public virtual void Start()
    {
        try
        {
            Listener.TableChanged += (o, e) => ChangeHandler.Process(e);

            Listener.Start();

            Logger.LogDebug(
                $"Listening to changes on the {ListenerConfiguration.Table} table in the {ListenerConfiguration.Database} database");
        }
        catch (Exception e)
        {
            Logger.LogError(e, e.Message);
            throw;
        }

    }

    public virtual void Stop()
    {
        Listener.Stop();
    }

Install through TopShelf is no problem:

c:>{ServiceName}.exe install -username "serviceAccount" -password "superSecret" -servicename "ServiceName" -servicedescription "Description" -displayname "Service DisplayName" --autostart

When I go to start the service - I get this:

enter image description here

This is misleading because the event viewer shows this:

enter image description here

This is happening way faster than 30 seconds. This is definitely related to how I am configuring TopShelf.

As stated - the application works just fine when run "debug" or even as just an exe console.


Solution

  • I got it figured out. Actually both comments from @DotNetPadawan and @Lex Li indirectly got me there.

    For starters - enabling the remote debugger clued me in that my appsetting.json was not being read into my IConfiguration. That was really confusing because everything works fine running locally with a debugger or even just starting the exe.

    The link Lex Li points out did not provide the answer - however that article had this reference:

    Host and Deploy aspnetcore as a Windows Service

    It was here that I found this little nugget:

    The current working directory returned by calling GetCurrentDirectory for a Windows Service is the C:\WINDOWS\system32 folder. The system32 folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files.

    The link explains how to conditionally set the current directory if the app is running as a service.

            var isConsole = args.Contains("-mode:console");
    
            if (!isConsole)
            {
                var pathToExe = Process.GetCurrentProcess().MainModule?.FileName;
                var pathToContentRoot = Path.GetDirectoryName(pathToExe);
                Directory.SetCurrentDirectory(pathToContentRoot);
            }
    

    Putting this out there for anyone else that runs into this problem.

    Admittedly - netcore 3.0 is likely the better way to go - but I don't have the bandwidth to upgrade everything is this repo (lots of shared stuff) to 3.0. I needed to get this working.