Single command line parameter to control Topshelf windows service - c#

I've made a topshelf windows service that starts three tasks. But since it might happen that one of those task might crash (yes, I know about EnableServiceRecovery), it would be better to use one program to create 3 services with different names and install them using command line parameters.
So in theory the code would look like:
static void Main(string[] args)
{
// *********************Below is a TopShelf code*****************************//
HostFactory.Run(hostConfigurator =>
{
hostConfigurator.Service<MyService>(serviceConfigurator =>
{
serviceConfigurator.ConstructUsing(() => new MyService(args[0])); //what service we are using
serviceConfigurator.WhenStarted(myService => myService.Start()); //what to run on start
serviceConfigurator.WhenStopped(myService => myService.Stop()); // and on stop
});
hostConfigurator.RunAsLocalSystem();
//****************Change those names for other services*******************************************//
hostConfigurator.SetDisplayName("CallForwardService"+args[0]);
hostConfigurator.SetDescription("CallForward using Topshelf"+args[0]);
hostConfigurator.SetServiceName("CallForwardService"+args[0]);
hostConfigurator.SetInstanceName(args[0]);
});
}
But of course it won't, because (from what I've read) you can't simply use args[] but apparently you can use something like
Callforward.exe install --servicename:CallForward --instancename:Workshop
I am still not sure how to pass the parameter to be used later in the program (in example above you can see it in new MyService(args[0]))
Can I use single parameter to set up all three elements (name, instance and internal use)?

Solved using help from How can I use CommandLine Arguments that is not recognized by TopShelf?
string department = null;
// *********************Below is a TopShelf code*****************************//
HostFactory.Run(hostConfigurator =>
{
hostConfigurator.AddCommandLineDefinition("department", f => { department = f; });
hostConfigurator.ApplyCommandLine();
hostConfigurator.Service<MyService>(serviceConfigurator =>
{
serviceConfigurator.ConstructUsing(() => new MyService(department)); //what service we are using
serviceConfigurator.WhenStarted(myService => myService.Start()); //what to run on start
serviceConfigurator.WhenStopped(myService => myService.Stop()); // and on stop
});
hostConfigurator.EnableServiceRecovery(r => //What to do when service crashes
{
r.RestartService(0); //First, second and consecutive times
r.RestartService(1);
r.RestartService(1);
r.SetResetPeriod(1); //Reset counter after 1 day
});
hostConfigurator.RunAsLocalSystem();
//****************Change those names for other services*******************************************//
string d = "CallForwardService_" + department;
hostConfigurator.SetDisplayName(d);
hostConfigurator.SetDescription("CallForward using Topshelf");
hostConfigurator.SetServiceName(d);
});
This answer was posted as an edit to the question Single command line parameter to control Topshelf windows service by the OP Yasskier under CC BY-SA 3.0.

Related

Removing a command from DiscordBot CommandService

I am trying to figure out how to remove a command from the discord bot after it has been created. Here is how I create the command:
_commandService.CreateCommand("create").Parameter("message", ParameterType.Multiple).Do(async e =>
{
var message = e.Args.Skip(1).Aggregate("", (current, t) => current + (t + " "));;
_commandService.CreateCommand("hello").Do(async cc =>
{
await e.User.SendMessage(customCommand.Message);
});
});
The _commandService object is of type Discord.Commands.CommandService
Now, I want to be able to run:
_commandService.CreateCommand("delete").Parameter("message", ParameterType.Multiple).Do(async e =>
{
_commandService.DeleteCommand("hello");
});
However, no such method exists, nor am I able to access the commands inside _commandService object as everything is read only get;
Does anyone know how I can delete the command without having to restart the bot?
It's possible, but as of discord.net 1.0 you need to use the Modules system to do it. Unfortunately, it greatly complicates things. Hopefully they'll add a proper DeleteCommand(string commandName) in a future update.
Why you need to do this (this section not needed if you don't care about the discord.net source): The
class CommandMap (it stores the commands, unsurprisingly) exposes a method RemoveCommand that does what you're looking to do. The only reference to an object of this class in the source is in the private method RemoveModuleInternal in CommandService. This is exposed in one of two public methods: RemoveModuleAsync(ModuleInfo module) or RemoveModuleAsync<T>(). There is no other way to affect commands as of the 1.0 release.
How to do it: Get the ModuleInfo object for your module first. Preferably, the module you create will only contain the command you want to delete for reasons that should be obvious pretty soon. When you use CreateModuleAsync(string primaryAlias, Action<ModuleBuilder> buildFunc) (or one of the other methods used to add modules) you'll get the ModuleInfo object back. This does mean you need to use a ModuleBuilder instead of the simple commandService.CreateCommand method you use. Read up on how to do that here... if the process still confuses you, it's an excellent topic for another question.
You need to keep track of the ModuleInfo object that CreateModuleAsync returns in some manner (the method I would use is below) and then your second command becomes:
// private ModuleInfo createInfo
_commandService.CreateCommand("delete").Parameter("message", ParameterType.Multiple).Do(async e =>
{
if (createInfo != null)
{
await _commandService.DeleteModuleAsync(createInfo);
}
});
Do note that the entire module instance is getting deleted... that's why your "create" command should be the only thing in it.
An alternate solution (although significantly less elegant) if this whole Module business seems too complicated would be to store a boolean and simply toggle it to simulate the deletion of the command. That is:
// bool createNotDeleted = true;
_commandService.CreateCommand("create").Parameter("message", ParameterType.Multiple).Do(async e =>
{
if (createNotDeleted)
{
var message = e.Args.Skip(1).Aggregate("", (current, t) => current + (t + " "));;
_commandService.CreateCommand("hello").Do(async cc =>
{
await e.User.SendMessage(customCommand.Message);
});
}
else
{
// error handling
}
});
and
_commandService.CreateCommand("delete").Parameter("message", ParameterType.Multiple).Do(async e =>
{
if (createNotDeleted)
{
createNotDeleted = false
// return something indicating command is deleted
}
else
{
// error handling
}
});

Topshelf not hitting OnStart Method

I'm trying to build my first topshelf based service. I'm trying to follow the pattern in the quick start http://docs.topshelf-project.com/en/latest/configuration/quickstart.html
static void Main(string[] args)
{
var h = HostFactory.Run(x =>
{
ConfHost(x);
});
}
private static void ConfHost (Topshelf.HostConfigurators.HostConfigurator x )
{
x.Service<JobRunner>(s =>
{
ConfService(s);
});
x.RunAsLocalSystem();
x.StartAutomatically();
x.SetDescription("topshelf thing");
x.SetDisplayName("displayname ");
x.SetServiceName("svc name");
}
private static void ConfService(ServiceConfigurator<JobRunner> s)
{
s.ConstructUsing(name => new JobRunner());
s.WhenStarted(bt => bt.OnStart());
s.WhenStopped(bt => bt.OnStop());
}
This code runs straight through and terminates without ever hitting the Onstart method on JobRunner even if I put a break point on the first line.
In the hope that someone else can learn from my mistake, the console output actually gives the reason you need to step through carefully or put a Console.Read() at the end to see it
ConfigurationException: The service was not properly configured:
[Failure] Name must not contain whitespace, '/' or '\' characters
Windows does support spaces in service names:
x.SetDisplayName("displayname "); //Here is the space
Change it to another value without spaces and must work...

ReactiveUI: Why do I have to specify the scheduler explicitly in "...Throttle..."when using the TestScheduler

I'm new to ReactiveUI. I have the following simple setup: a path to a csv can be specified and the containing datapoints will be displayed to the user (using oxyplot).
Now I'm trying to test the following subscription:
public GraphViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
setupGraphFormatting();
// Data Loading if path is valid
this.WhenAnyValue(viewModel => viewModel.PathToDataCsv)
.ObserveOn(RxApp.MainThreadScheduler)
.Throttle(TimeSpan.FromMilliseconds(500), RxApp.TaskpoolScheduler)
.Select(csvPath => csvPath?.Trim('"'))
.Where(csvPath => !string.IsNullOrEmpty(csvPath) && File.Exists(csvPath))
.Subscribe(csvPath =>
{
csvPath = csvPath?.Trim('"');
updatePlotModel(csvPath);
}, exception => {});
/* additional Code*/
}
And that's the corresponding UnitTest:
[Test]
public void If_PathToDataCsv_has_a_valid_value()
{
new TestScheduler().With(scheduler =>
{
string pathToValidCsvFile = "data.log";
var viewModel = new GraphViewModel(null);
scheduler.AdvanceByMs(1000);
viewModel.PathToDataCsv = pathToValidCsvFile;
scheduler.AdvanceByMs(1000);
viewModel.PlotModel.Series.Count.Should().Be(6);
});
}
My first implementation of WhenAnyValue didn't set any of the Schedulers specifically ( in Throttle and lacking any ObserverOn ):
public GraphViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
setupGraphFormatting();
// Data Loading if path is valid
this.WhenAnyValue(viewModel => viewModel.PathToDataCsv)
.Throttle(TimeSpan.FromMilliseconds(500))
.Select(csvPath => csvPath?.Trim('"'))
.Where(csvPath => !string.IsNullOrEmpty(csvPath) && File.Exists(csvPath))
.Subscribe(csvPath =>
{
csvPath = csvPath?.Trim('"');
updatePlotModel(csvPath);
}, exception => {});
/* additional Code*/
}
But then my Unittest failed. My assumption was that TestScheduler was being used for Throttle behind the scenes and I didn't have to do anything. Am I doing something wrong or is this the right way: If I want to use TestScheduler/TimeTravelâ„¢ I have to specify the schedulers the way I did?
Edit in response to Glenn Watsons answer:
Ok, now it's clear: The methods in question (Throttle, ObserverOn) of course do not use ReactiveUI's Schedulers, because these are methods from the Reactive Extensions Framework. So they can't be replaced implicitly by ReactiveUI in case of a UnitTest except I tell the methods to use the RxApp Schedulers...
RxApp provides the ThreadPoolScheduler when you are in release mode, and the testing scheduler when you are in unit test mode.
By default the reactive extensions (separate to ReactiveUI) will use their own default schedulers which are unaware of unit tests.

How to setup Topshelf with WhenCustomCommandReceived?

I'm using Topshelf to create a windows service (ServiceClass) and i'm thinking of sending custom commands using WhenCustomCommandReceived.
HostFactory.Run(x =>
{
x.EnablePauseAndContinue();
x.Service<ServiceClass>(s =>
{
s.ConstructUsing(name => new ServiceClass(path));
s.WhenStarted(tc => tc.Start());
s.WhenStopped(tc => tc.Stop());
s.WhenPaused(tc => tc.Pause());
s.WhenContinued(tc => tc.Resume());
s.WhenCustomCommandReceived(tc => tc.ExecuteCustomCommand());
});
x.RunAsLocalSystem();
x.SetDescription("Service Name");
x.SetDisplayName("Service Name");
x.SetServiceName("ServiceName");
x.StartAutomatically();
});
However, i'm getting an error on the WhenCustomCommandReceived line:
Delegate 'Action< ServiceClass, HostControl, int>' does not take 1 arguments
The signature is
ServiceConfigurator<ServiceClass>.WhenCustomCommandReceived(Action<ServiceClass, HostControl, int> customCommandReceived)
I already have methods for start, stop, pause in my ServiceClass: public void Start(), etc. Can anyone point me in the right direction on how to setup the Action? Thanks!
So, as you can see in the signature of the method, the Action takes three parameters, not just one. This means that you need to set it up like this:
s.WhenCustomCommandReceived((tc,hc,command) => tc.ExecuteCustomCommand());
The interesting parameter in this case would be command which is of type int. This is the command number that is sent to the service.
You might want to change the signature of the ExecuteCustomCommand method to accept such command like this:
s.WhenCustomCommandReceived((tc,hc,command) => tc.ExecuteCustomCommand(command));
And in the ServiceClass:
public void ExecuteCustomCommand(int command)
{
//Handle command
}
This allows you to act differently based on the command that you receive.
To test sending a command to the service (from another C# project) you can use the following code:
ServiceController sc = new ServiceController("ServiceName"); //ServiceName is the name of the windows service
sc.ExecuteCommand(255); //Send command number 255
According to this MSDN reference, the command value must be between 128 and 256.
Be sure to reference the System.ServiceProcess assembly in your test project.

Set Service Start Parameter using Topshelf

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?
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"
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.
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().

Categories