Set Service Start Parameter using Topshelf
Asked Answered
Q

3

6

I have a service with multiple instances with different parameters for each instance, at the moment I'm setting these parameters manually (in another code to be exact) to Image Path of the service in Registry (e.g. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\MyService$i00). so our service installation is done in two steps.

I'm really interested to merge these steps in Topshelf installation for example like

MyService.exe install -instance "i00" -config "C:\i00Config.json"

First Try

I tried AddCommandLineDefinition from TopShelf but it seems it only works during installation and running through console not the service itself (will not add anything to service Image Path).

Second Try

I tried to see if its possible to do this with AfterInstall from Topshelf without any luck. here is a test code to see if it going to work or not (but unfortunately Topshelf overwrites the registry after AfterInstall call).

HostFactory.Run(x =>
        {
            x.UseNLog();
            x.Service<MyService>(sc =>
            {
                sc.ConstructUsing(hs => new MyService(hs));
                sc.WhenStarted((s, h) => s.Start(h));
                sc.WhenStopped((s, h) => s.Stop(h));
            });

            x.AfterInstall(s =>
            {
                using (var system = Registry.LocalMachine.OpenSubKey("SYSTEM"))
                using (var controlSet = system.OpenSubKey("CurrentControlSet"))
                using (var services = controlSet.OpenSubKey("services"))
                using (var service = services.OpenSubKey(string.IsNullOrEmpty(s.InstanceName)
                    ? s.ServiceName
                    : s.ServiceName + "$" + s.InstanceName, true))
                {

                    if (service == null)
                        return;

                    var imagePath = service.GetValue("ImagePath") as string;

                    if (string.IsNullOrEmpty(imagePath))
                        return;

                        var appendix = string.Format(" -{0} \"{1}\"", "config", "C:\i00config.json"); //only a test to see if it is possible at all or not
                        imagePath = imagePath + appendix;


                    service.SetValue("ImagePath", imagePath);
                }
            });

            x.SetServiceName("MyService");
            x.SetDisplayName("My Service");
            x.SetDescription("My Service Sample");
            x.StartAutomatically();
            x.RunAsLocalSystem();
            x.EnableServiceRecovery(r =>
            {
                r.OnCrashOnly();
                r.RestartService(1); //first
                r.RestartService(1); //second
                r.RestartService(1); //subsequents
                r.SetResetPeriod(0);
            });
        });

I couldn't find any relevant information about how it can be done using TopShelf so the question is, is it possible to do this with TopShelf?

Quadrivalent answered 1/10, 2015 at 16:45 Comment(0)
Q
9

Ok, as Travis mentioned, It seems there is no built-in feature or simple workaround for this problem. so I wrote a little extension for Topshelf based on a Custom Environment Builder (most of the code is borrowed form Topshelf project itself).

I posted the code on Github, in case others may find it useful, here is the Topshelf.StartParameters extension.

based on the extension my code would be like:

HostFactory.Run(x =>
    {
        x.EnableStartParameters();
        x.UseNLog();
        x.Service<MyService>(sc =>
        {
            sc.ConstructUsing(hs => new MyService(hs));
            sc.WhenStarted((s, h) => s.Start(h));
            sc.WhenStopped((s, h) => s.Stop(h));
        });

        x.WithStartParameter("config",a =>{/*we can use parameter here*/});

        x.SetServiceName("MyService");
        x.SetDisplayName("My Service");
        x.SetDescription("My Service Sample");
        x.StartAutomatically();
        x.RunAsLocalSystem();
        x.EnableServiceRecovery(r =>
        {
            r.OnCrashOnly();
            r.RestartService(1); //first
            r.RestartService(1); //second
            r.RestartService(1); //subsequents
            r.SetResetPeriod(0);
        });
    });

and I can simply set it with:

MyService.exe install -instance "i00" -config "C:\i00Config.json"
Quadrivalent answered 1/10, 2015 at 22:2 Comment(0)
L
4

To answer you question, no this isn't possible with Topshelf. I am excited you figured out how to manage the ImagePath. But that's the crux of the problem, there's been some discussion on the mailing list (https://groups.google.com/d/msg/topshelf-discuss/Xu4XR6wGWxw/8mAtyJFATq8J) on this topic and issues about it in the past.

The big problem is that managing expectations of behavior when applying custom arguments to the ImagePath will be unintuitive. For example, what happens when you call start with custom command line arguments? I'm open to implementing this or accepting a PR if we get something that doesn't confuse me just thinking about it, let alone trying to use. Right now, I strongly encourage you to use configuration, not command line arguments, to manage this, even if it means duplicating code on disk.

Laclair answered 1/10, 2015 at 19:22 Comment(3)
thanks for the info, unfortunately for me its not possible (don't want to open it up but its part of project requirements), so I end up writing a little extension on top of the Topshelf, Please check it (link is in my answer) if you can and see if it is reasonable enough, I can merge it with Topshelf code (it would get much cleaner and more concise) and send a pull request if its helpful.Quadrivalent
Just for thoughts, why multiple instances of the service? Why not 1 instance and multiple jobs with different job-data-maps? You can control them on basis of key anyway (sc.exe control 128..256), and now you only have 1 configuration.Jacques
I think you're underestimating the intuitive developer.Rothstein
R
0

The following work-around is nothing more than a registry update. The update operation expects the privileges the installer requires in order to write our extended arguments.

Basically, we're responding to the AfterInstall() event. As of Topshelf v4.0.3, calling the AppendImageArgs() work-around from within the event will cause your args to appear before the TS args. If the call is deferred, your args will appear after the TS args.

The work-around

private static void AppendImageArgs(string serviceName, IEnumerable<Tuple<string, object>> args)
{
  try
  {
    using (var service = Registry.LocalMachine.OpenSubKey($@"System\CurrentControlSet\Services\{serviceName}", true))
    {
      const string imagePath = "ImagePath";
      var value = service?.GetValue(imagePath) as string;
      if (value == null)
        return;
      foreach (var arg in args)
        if (arg.Item2 == null)
          value += $" -{arg.Item1}";
        else
          value += $" -{arg.Item1} \"{arg.Item2}\"";
      service.SetValue(imagePath, value);
    }
  }
  catch (Exception e)
  {
    Log.Error(e);
  }
}

An example call

private static void AppendImageArgs(string serviceName)
{
  var args = new[]
  {
    new Tuple<string, object>("param1", "Hello"),
    new Tuple<string, object>("param2", 1),
    new Tuple<string, object>("Color", ConsoleColor.Cyan),
  };
  AppendImageArgs(serviceName, args);
}

And the resulting args that would appear in the ImagePath:

 -displayname "MyService Display Name" -servicename "MyServiceName" -param1 "Hello" -param2 "1" -Color "Cyan"

Notice the args appeared after the TS args, -displayname & -servicename. In this example, the AppendImageArgs() call was invoked after TS finished its installation business.

Command line args can be specified normally using Topshelf methods such as AddCommandLineDefinition(). To force processing of the args, call ApplyCommandLine().

Rothstein answered 5/7, 2017 at 7:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.