Search code examples
c#windows-services.net-8.0servicecontrollerservice-control-manager

C# .NET 8 Where can my Windows Service access the arguments from sc.exe start?


In .NET Framework 4.6.2, you use this type of code for a Windows Service:

    public partial class ServiceMain : ServiceBase
    {
        public ServiceMain()
        {
            InitializeComponent();
        }

        protected override void OnStart( string[] args )
        {
            // I can do something with the args.

In .NET 8, every Windows Service example I've seen (and what I have written) is to have a Worker declared in your Main entry point (typically Program.cs)

        builder.Services.AddHostedService<Worker>();

and then that worker is just a background service:

public class Worker : BackgroundService

The args from Main are empty when starting via sc.exe. Is there some other way to grab those arguments?

I was expecting to see values in the Main args that were the args from the sc.exe call. I see no online examples of the .NET 8 equivalent of OnStart where there are service arguments.

This seems like an answer but I have confirmed that args are always empty.


Solution

  • I've created several asp core services myself and I thought you were mistaken - but you're correct, no accessible args.

    So I checked the source: https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src

    The WindowsServiceLifetime class has a virtual startup function that takes args.

    protected override void OnStart(string[] args)
    

    I couldn't find a public args member anywhere so I think the only way around this is to override this class and add your own as the implementation of IHostLifetime

    I copied the following from WindowsServiceLifetimeHostBuilderExtensions.cs

    I only made a small change to allow passing your own WindowsServiceLifetime based class.

    namespace Microsoft.Extensions.Hosting
    {
    
        public static class MyWindowsServiceLifetimeHostBuilderExtensions
        {
            public static void AddMyWindowsServiceLifetime<TImplementation>(this IServiceCollection services
                , Action<WindowsServiceLifetimeOptions> configure) where TImplementation : class, IHostLifetime
            {
                if (WindowsServiceHelpers.IsWindowsService())
                {
    
    #if !NETFRAMEWORK
                    Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
    #endif
    
                    services.AddLogging(logging =>
                    {
    #if !NETFRAMEWORK
                        Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
    #endif
                        logging.AddEventLog();
                    });
                    services.AddSingleton<IHostLifetime, TImplementation>();
                    services.AddSingleton<IConfigureOptions<EventLogSettings>, EventLogSettingsSetup>();
                    services.Configure(configure);
                }
            }
    
            private sealed class EventLogSettingsSetup : IConfigureOptions<EventLogSettings>
            {
                private readonly string _applicationName;
    
                public EventLogSettingsSetup(IHostEnvironment environment)
                {
                    _applicationName = environment.ApplicationName;
                }
    
                public void Configure(EventLogSettings settings)
                {
    #if !NETFRAMEWORK
                    Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
    #endif
    
                    if (string.IsNullOrEmpty(settings.SourceName))
                    {
                        settings.SourceName = _applicationName;
                    }
                }
            }
    
        }
    }
    

    My own WindowsServiceLifetime based class looks like this:

    [SupportedOSPlatform("windows")]
    public class MyWindowsServiceLifetime : WindowsServiceLifetime
    {
        public static string[] args { get; set; }
        public MyWindowsServiceLifetime(IHostEnvironment environment
            , IHostApplicationLifetime applicationLifetime
            , ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor)
            : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
        {
        }
        protected override void OnStart(string[] args)
        {
            MyWindowsServiceLifetime.args = args;
            base.OnStart(args);
        }
    }
    

    In my startup code I call the new extension method like this:

     builder.Services.AddMyWindowsServiceLifetime<MyWindowsServiceLifetime>(c => { });
    

    Now you can access the args from your own class, or in this case I made them public static.

    var _args = MyWindowsServiceLifetime.args;
    if (_args != null)
    {
        foreach (var arg in _args)
        {
            logger.LogInformation("arg= {arg}", arg);
        }
    }
    

    The args are not available in the Program.cs startup code, but you can access them from anywhere else.