Service Binder object casting issue during service connection - service

I have an issue in service connection issue in that line var binder:TwentyFourHoursService.LocalBinder = service as TwentyFourHoursService.LocalBinder and could not find solution:
override fun onServiceConnected(
className: ComponentName,
service: IBinder
) { // cast the IBinder and get MyService instance
var binder:TwentyFourHoursService.LocalBinder = service as TwentyFourHoursService.LocalBinder
myService = binder.getService()
bound = true
// serviceCallbacks =this as ServiceCallbacks
myService!!.setCallbacks(mActivity) // register
}
Here is my service:
class TwentyFourHoursService : Service() {
private val mHandler: Handler = Handler() //run on another Thread to avoid crash
private var mTimer: Timer? = null //timer handling
// Binder given to clients
private val binder: IBinder = LocalBinder()
override fun onBind(intent: Intent): IBinder {
// throw UnsupportedOperationException("Not yet implemented")
return binder
}
override fun onCreate() {
if (mTimer != null) // Cancel if already existed
mTimer!!.cancel() else mTimer = Timer() //recreate new
mTimer!!.scheduleAtFixedRate(
TimeDisplay(),
0,
notify
) //Schedule task
//Timer().scheduleAtFixedRate(TimeDisplay(),0, notify)
}
fun setCallbacks(callbacks: ServiceCallbacks?) {
serviceCallbacks = callbacks
}
override fun onDestroy() {
super.onDestroy()
mTimer!!.cancel() //For Cancel Timer
Toast.makeText(this, "Service is Destroyed", Toast.LENGTH_SHORT).show()
}
//class TimeDisplay for handling task
internal inner class TimeDisplay : TimerTask() {
override fun run() { // run on another thread
mHandler.post(Runnable {
// display toast
/* if (serviceCallbacks!=null) {
serviceCallbacks!!.doSomething()
}*/
// Reload current fragment
// Reload current fragment
// startActivity(Intent(applicationContext, FitnessSlideMenuScreen::class.java))
// rFitnessSlideMenuScreen().displaySelectedFragment(HomeFragment())
Toast.makeText(applicationContext, "Service is running", Toast.LENGTH_SHORT).show()
})
}
}
Error:
***java.lang.ClassCastException: android.os.BinderProxy cannot be cast to com.example.beyahfitness.service.TwentyFourHoursService$LocalBinder***

The issue as I realised was as a result of the way had declared my service in Manifest,
<service
android:name=".MyService"
android:enabled="true"
android:process=":MyService" >
When I try getting the service at;
override fun onServiceConnected(name: ComponentName,service: IBinder)
I kept getting this error; java.lang.ClassCastException: android.os.BinderProxy cannot be cast to com.demo.MyService$LocalBinder
I resolved to refactoring my Service as below;
<service
android:name=".MyService" >
This apparently solves my problem, hope it helps you too

Related

Get data from Google Fit History Client when the app is in background or killed

I am working on a flutter plugin to calculate total steps in interval using Google Fit History Client (Fitness.getHistoryClient) which requires flutter activity and I want to call some of its methods in the background. I am using workmanager 0.5.0 to schedule a background job.
class MyPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
private lateinit var channel : MethodChannel
private val TAG = "MY TAG"
private var activity: Activity? = null
private var mResult: Result? = null
private lateinit var context: Context
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "my_channel_name")
channel.setMethodCallHandler(this)
context = flutterPluginBinding.applicationContext
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getTotalStepsInInterval" -> getTotalStepsInInterval(call, result)
else -> result.notImplemented()
}
}
private fun getTotalStepsInInterval(call: MethodCall, result: Result){
mResult = result
if (activity == null) {
mResult?.success(-1)
return
}
// val readRequest = ...
Fitness.getHistoryClient(activity!!, getGoogleAccount())
.readData(readRequest)
.addOnSuccessListener { response ->
var totalSteps = 0
// Calculate total steps from response
mResult?.success(totalSteps)
}
.addOnFailureListener { }
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
if (channel == null) {
return
}
binding.addActivityResultListener(this)
activity = binding.activity
}
// onDetachedFromActivityForConfigChanges + onReattachedToActivityForConfigChanges + onDetachedFromActivity
}
I am able to get the total steps when I invoke this method from a function in dart.
MyPlugin myPlugin = MyPlugin();
int res = await myPlugin.getTotalStepsInInterval();
print(res);
Activity is null when a method is invoked from the callback dispatcher of workmanager.
void callbackDispatcher() async {
workmanager.executeTask((task, inputData) async {
print('Running - Callback Dispatcher');
try {
MyPlugin myPlugin = MyPlugin();
int res = await myPlugin.getTotalStepsInInterval();
print(res);
} catch (err) {
throw Exception(err);
}
return Future.value(true);
});
}
// Output -------> -1
Since subscription is active using Recording API, I can fetch the data using readData when the user comes back to the app but I want to get the data at a specific time (For eg: 5:00 PM which I can schedule using workmanager) even when the app is not running. Duplicate Question
Still not possible?

Polly Retry with RX Observable.Interval

I'm new to Polly and I'm trying to apply the Retry policy, so that I can have it manually handling the retry connection in case of IBMMQ connection issue.
Please, consider the following code:
public class ReconnectException : Exception
{
}
public class QueueMonitor : IObservable<Message>, IDisposable
{
private readonly MQQueue mqQueue;
private readonly MQQueueManager queueManager;
private readonly string queueName;
private IDisposable timer;
private readonly object lockObj = new object();
private bool isChecking;
private readonly TimeSpan checkingFrequency;
private readonly List<IObserver<Message>> observers;
private TimeSpan reconnectInterval;
private readonly IScheduler scheduler;
private readonly int maxReconnectCount;
private static readonly ILog Logger = LogProvider.For<AonQueueManager>();
private readonly Policy pollyPolicy;
public QueueMonitor(IConfiguration configuration, string queueName, IScheduler scheduler = null)
{
this.queueManager = QueueFactory.GetIstance(configuration);
this.queueName = queueName;
this.scheduler = scheduler ?? Scheduler.Default;
checkingFrequency = configuration.GetValue("checkingFrequency", new TimeSpan(0, 0, 5));
reconnectInterval = configuration.GetValue("reconnectInterval", new TimeSpan(0, 0, 5));
maxReconnectCount = configuration.GetValue("maxReconnectCount", 3);
observers = new List<IObserver<Message>>();
pollyPolicy = Policy.Handle<ReconnectException>().WaitAndRetry(maxReconnectCount, _ => TimeSpan.FromSeconds(2));
mqQueue = queueManager.AccessQueue(queueName,
MQC.MQOO_INPUT_AS_Q_DEF // open queue for input
+ MQC.MQOO_FAIL_IF_QUIESCING); // but not if MQM stopping
}
public void Start()
{
var x = pollyPolicy.ExecuteAndCapture(CreateTimer);
}
private void CreateTimer()
{
Logger.DebugFormat("Repeating timer started, checking frequency: {checkingFrequency}", checkingFrequency);
timer = Observable.Interval(checkingFrequency, scheduler).Subscribe(_ =>
{
lock (lockObj)
{
if (isChecking) return;
Logger.Log(LogLevel.Debug, () => "Listening on queues for new messages");
isChecking = true;
var mqMsg = new MQMessage();
var mqGetMsgOpts = new MQGetMessageOptions { WaitInterval = checkingFrequency.Milliseconds };
// 15 second limit for waiting
mqGetMsgOpts.Options |= MQC.MQGMO_WAIT | MQC.MQGMO_FAIL_IF_QUIESCING |
MQC.MQCNO_RECONNECT_Q_MGR | MQC.MQOO_INPUT_AS_Q_DEF;
try
{
mqQueue.Get(mqMsg, mqGetMsgOpts);
if (mqMsg.Format.CompareTo(MQC.MQFMT_STRING) == 0)
{
var text = mqMsg.ReadString(mqMsg.MessageLength);
Logger.Debug($"Message received : [{text}]");
Message message = new Message { Content = text };
foreach (var observer in observers)
observer.OnNext(message);
}
else
{
Logger.Warn("Non-text message");
}
}
catch (MQException ex)
{
if (ex.Message == MQC.MQRC_NO_MSG_AVAILABLE.ToString())
{
Logger.Trace("No messages available");
//nothing to do, emtpy queue
}
else if (ex.Message == MQC.MQRC_CONNECTION_BROKEN.ToString())
{
Logger.ErrorException("MQ Exception, trying to recconect", ex);
throw new ReconnectException();
}
}
finally
{
isChecking = false;
}
}
});
}
public IDisposable Subscribe(IObserver<Message> observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
return new Unsubscriber(observers, observer);
}
public void Dispose()
{
((IDisposable)mqQueue)?.Dispose();
((IDisposable)queueManager)?.Dispose();
timer?.Dispose();
}
}
public class Unsubscriber : IDisposable
{
private readonly List<IObserver<Message>> observers;
private readonly IObserver<Message> observer;
public Unsubscriber(List<IObserver<Message>> observers, IObserver<Message> observer)
{
this.observers = observers;
this.observer = observer;
}
public void Dispose()
{
if (observer != null) observers.Remove(observer);
}
}
The problem I've is that when an exception is thrown inside the lamda ( throw new ReconnectException();), Polly doesn't catch it (and I understand why, since it's on another thread) and the application quits since it's on a different thread.
This code is a part of a library,so I don't know that if in every project the Global exceptions are correctly handed.
How do I get it "catched" by the Polly's code?
Thanks in advance
The code posted in the question applies the policy only to the act of creating the timer (the execution of CreateTimer()), not to the code executed by the timer (the lambda inside the .(Subscribe(_ => { }) call).
This is the same as the behaviour if the call to CreateTimer() was surrounded by a try { } catch { }. The catch would only cover the act of executing the CreateTimer() method, the creation of the timer.
For the Polly policy to govern exceptions thrown within the lambda, it needs to be applied within the lambda, to the relevant block/group of statements which are expected to throw the exception.
For example, you might code:
pollyPolicy.ExecuteAndCapture(() => mqQueue.Get(mqMsg, mqGetMsgOpts));
(with a policy configured to govern the particular MQException/s you want to handle).
Or you can apply the policy to a wider group of statements - just as with a try { } clause.
pollyPolicy.ExecuteAndCapture(() =>
{
// ...
mqQueue.Get(mqMsg, mqGetMsgOpts));
// ...
}

Unit Testing (xUnit) NLog logging service under .NET Core

.NET Core 2.1
NLog 4.6
xUnit 2.3.1
I have class library with xUnit that calls a separate library that contains REST-based APIs that is responsible for creating various logs for the system.
Since the unit test class library calls the REST-based API controller directly, the class's Startup class isn't loaded so I don't believe NLog is being configured. This will need to be done within the unit test class library but I cannot seem to figure that out.
I am able to load the REST-based API nlog configuration from the calling library and then execute NLogs directly from the LogManager but the NLog implementation explicitly used within the REST-based API does not log nor does any error occur.
If I use a soap client such as SOAPUI and call the REST's class library, the logs are created as expected. This means the unit test class library isn't configuring logging correctly.
// Unit Test's base class for wiring up DI and other configuration including Logging
public BaseTests()
{
// Configuration
string loggingServiceAPIPath = #"../../../../../../LoggingService/API/CM.LoggingService.API";
var builder = new ConfigurationBuilder().SetBasePath(Path.GetFullPath(loggingServiceAPIPath)).AddJsonFile("appsettings.json");
var configuration = builder.Build();
// Configure logging
LogManager.Configuration = new XmlLoggingConfiguration(Path.GetFullPath($"{ loggingServiceAPIPath }/nlog.config"));
// Application-Wide Services
IServiceCollection services = new ServiceCollection();
services.AddMvc();
services.AddLogging();
services.AddSingleton(configuration);
services.AddSingleton<ILoggerFactory, LoggerFactory>();
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddSingleton<ILoggingServiceController, LoggingServiceController>();
services.AddApplicationServices();
services.AddOptions();
services.ConfigureConfigServerClientOptions(configuration);
services.AddConfiguration(configuration);
services.Configure<ConfigServerData>(configuration);
this._serviceProvider = services.BuildServiceProvider();
// Persist configuration
IMemoryCache iMemoryCache = this._serviceProvider.GetService<IMemoryCache>();
IOptionsSnapshot<ConfigServerData> iConfigServerData = this._serviceProvider.GetService<IOptionsSnapshot<ConfigServerData>>();
if (iMemoryCache != null && iConfigServerData != null) { iMemoryCache.Set(CM.Common.Constants.ConfigKey, iConfigServerData.Value); }
}
// Unit Test being called from a class library
[Fact]
public async void Test_LogDebugSuccess()
{
LoggingServiceRequest request = new LoggingServiceRequest
{
ErrorException = new Exception(),
Message = System.Reflection.MethodBase.GetCurrentMethod().Name
};
// This is not capturing NLog probably due to not being called in a hosted environment.
var result = await
this._iLoggingServiceController.LogDebug(request);
// Assert
Assert.Null(result as NotFoundObjectResult);
var okObjectResult = result as OkObjectResult;
Assert.True((okObjectResult != null &&
okObjectResult.StatusCode.GetValueOrDefault(0) == Convert.ToInt32(System.Net.HttpStatusCode.OK)), "Log was not created.");
}
// LoggingService
public class Program
{
/// <summary>
/// Main
/// </summary>
/// <param name="args">Arguments</param>
public static void Main(string[] args)
{
// NLog: setup the logger first to catch all errors
var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
try
{
logger.Debug("Init Main");
Program.BuildWebHost(args).Run();
}
catch (Exception ex)
{
logger.Error(ex, $"Stopped program because of exception: { ex.Message }");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
}
/// <summary>
/// Build WebHost
/// </summary>
/// <param name="args">Arguments</param>
/// <returns>WebHost interface</returns>
public static IWebHost BuildWebHost(string[] args)
{
try
{
var config = WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(false)
.AddConfigServer()
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Trace);
})
.UseNLog() // NLog: setup NLog for Dependency injection
.Build();
return config;
}
catch (Exception ex)
{
throw ex;
}
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config =>
{
config.Filters.Add(typeof(CustomExceptionFilter));
}
);
// Add memory cache
services.AddMemoryCache();
services.AddMvc();
services.AddCors(o => o.AddPolicy("corspolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()));
services.AddSingleton(this.Configuration);
// Application-Wide Services
services.AddApplicationServices();
// Configuration
services.AddOptions();
services.ConfigureConfigServerClientOptions(this.Configuration);
services.AddConfiguration(this.Configuration);
services.Configure<ConfigServerData>(this.Configuration);
// Configure Swagger
services.AddSwaggerGen(s =>
{
s.SwaggerDoc("v1", new Info { Title = "CM LoggingService APIs", Version = "V1" });
});
}
public void Configure(IApplicationBuilder iApplicationBuilder, IHostingEnvironment iHostingEnvironment, IConfigurationManager iConfigurationManager, IMemoryCache iMemoryCache, IApplicationLifetime iApplicationLifetime, ILogger<LoggingServiceController> iLogger)
{
if (iHostingEnvironment.IsDevelopment() == true)
{
iApplicationBuilder.UseDeveloperExceptionPage();
iApplicationBuilder.UseStatusCodePages();
iApplicationBuilder.UseDatabaseErrorPage();
iApplicationBuilder.UseBrowserLink();
}
if (iHostingEnvironment.IsProduction() == false)
{
// Swagger - API Documentation
iApplicationBuilder.UseSwagger();
iApplicationBuilder.UseSwaggerUI(s =>
{
s.SwaggerEndpoint("./v1/swagger.json", "CM LoggingService APIs");
});
}
// Persist Steeltoe configuration
iConfigurationManager.Init();
if (iMemoryCache != null && iConfigurationManager != null) { iMemoryCache.Set(CM.Common.Constants.MEMORYCACHE_CONFIGURATIONMANAGER_KEY, iConfigurationManager); }
iConfigurationManager.LogConfiguration();
// Configure global exception handler
iApplicationBuilder.ConfigureExceptionHandler(iLogger);
iApplicationBuilder.UseMvc();
// Application Events
iApplicationLifetime.ApplicationStarted.Register(this.OnApplicationStarted);
iApplicationLifetime.ApplicationStopped.Register(this.OnApplicationStopping);
}
public class LoggingServiceController : Controller, ILoggingServiceController
{
private readonly ILogger<LoggingServiceController> _iLogger = null;
private readonly ILoggingServiceDomainController _iLoggingServiceDomainController = null;
public LoggingServiceController(ILogger<LoggingServiceController> iLogger, ILoggingServiceDomainController iLoggingServiceDomainController)
{
this._iLogger = iLogger;
this._iLoggingServiceDomainController = iLoggingServiceDomainController;
}
[HttpPost("LogError")]
public async Task<IActionResult> LogError([FromBody] LoggingServiceRequest request)
{
bool result = false;
try
{
// Validation
if (ModelState.IsValid == false)
{
this._iLogger.LogError($"{ CM.Common.ExceptionHandling.ExceptionTypes.VALIDATION }: { typeof(LoggingServiceRequest).Name } (request) is not valid.");
return BadRequest();
}
// Log
result = this._iLogger.LogError(request.ErrorException, request.Message, request.Args);
if (result == false) { return NotFound(); }
}
catch (Exception ex)
{
this._iLogger.LogError(ex, $"{ CM.Common.ExceptionHandling.ExceptionTypes.UNSPECIFIED }: { ex.Message }");
}
return Ok(result);
}
}

Vertx instance variable is null when trying to access it from it's method

Below is verticle
package com.api.redis.gateway.verticle;
import java.util.UUID;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.redis.RedisClient;
import io.vertx.redis.RedisOptions;
public class SimpleRestChild extends SimpleRestServer{
RedisClient client;
#Override
public void start() {
// TODO Auto-generated method stub
super.start();
client = RedisClient.create(vertx, new RedisOptions().setHost("127.0.0.1").setPort(6379));
client.subscribe("channelForServiceToPublish", handler -> {
if(handler.succeeded())
System.out.println("SimpleRestServer subscibed to the channel successfully");
});
}
public void handleSubscription(RoutingContext routingContext) {
JsonObject requestAsJson = routingContext.getBodyAsJson();
requestAsJson.put("uuid", getUUID());
// this client object is null.
client.set("request", requestAsJson.toString(), handler ->{
System.out.println("Simple server is setting value to redis client");
if(handler.succeeded()) {
System.out.println("Key and value is stored in Redis Server");
}else if(handler.failed()) {
System.out.println("Key and value is failed to be stored on Redis Server with cause : "+ handler.cause().getMessage());
}
});
client.publish("channelForServerToPublish", "ServiceOne", handler -> {
if(handler.succeeded()) {
System.out.println("Simple Server published message successfully");
}else if(handler.failed()) {
System.out.println("Simple Server failed to published message");
}
});
routingContext.vertx().eventBus().consumer("io.vertx.redis.channelForServiceToPublish", handler -> {
client.get("response", res ->{
if(res.succeeded()) {
JsonObject responseAsJson = new JsonObject(res.result());
if(responseAsJson.getString("uuid").equalsIgnoreCase(requestAsJson.getString("uuid"))) {
routingContext.response().setStatusCode(200).end(res.result());
}
}else if(res.failed()) {
System.out.println("Failed to get message from Redis Server");
routingContext.response().setStatusCode(500).end("Server Error ");
}
});
});
}
private String getUUID() {
UUID uid = UUID.randomUUID();
return uid.toString();
}
}
And below is the main verticle from where the above verticle is getting deployed and on any request to httpserver it's hanlder method is getting called.
package com.api.redis.gateway.verticle;
import io.vertx.core.AbstractVerticle;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.redis.RedisClient;
import io.vertx.redis.RedisOptions;
public class SimpleRestServer extends AbstractVerticle{
#Override
public void start(){
int http_port = 9001;
vertx.deployVerticle("com.api.redis.gateway.verticle.SimpleRestChild", handler -> {
if(handler.succeeded()) {
System.out.println(" SimpleRestChild deployed successfully");
}
});
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
SimpleRestChild child = null;
try {
child = (SimpleRestChild) Class.forName("com.api.redis.gateway.verticle.SimpleRestChild").newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
router.route("/subscription").handler(child::handleSubscription);
vertx.createHttpServer().requestHandler(router::accept).listen(http_port);
System.out.println("Server started at port : " + http_port);
}
}
When handleSubscription is getting called for any "/subscription" request. client object is coming as null.
As per my understanding two objects are getting created here. One with start() and other not having start().
I want to initialize Redisclient once.And use this object when handleSubscription() will get called for any request to "/subscription".
How to achieve this ?
How to fix this problem.
the requests may be coming in before the client initialization is actually complete.
AbstractVerticle has two variations of start():
start(), and
start(Future<Void> startFuture)
the overloaded version with the Future parameter should be used to perform potentially long-running initializations that are necessary to do before the Verticle can be considered deployed and ready. (there's a section dedicated to this topic in the docs).
so you might try changing your code as follows:
public class SimpleRestChild extends SimpleRestServer {
RedisClient client;
#Override
public void start(Future<Void> startFuture) {
client = ...
// important point below is that this Verticle's
// deployment status depends on whether or not
// the client initialization succeeds
client.subscribe("...", handler -> {
if(handler.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(handler.cause());
}
);
}
}
and:
public class SimpleRestServer extends AbstractVerticle {
#Override
public void start(Future<Void> startFuture) {
int http_port = 9001;
vertx.deployVerticle("...", handler -> {
// if the child Verticle is successfully deployed
// then move on to completing this Verticle's
// initialization
if(handler.succeeded()) {
Router router = ...
...
// if the server is successfully installed then
// invoke the Future to signal this Verticle
// is deployed
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(http_port, handler -> {
if(handler.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(handler.cause());
}
});
} else {
startFuture.fail(handler.cause());
}
}
using this type of approach, your Verticles will only service requests when all their dependent resources are fully initialized.

Stopping a Windows Service in the event of a critical error

I have a Windows Service which basically wraps a task:
public partial class Service : ServiceBase
{
private Task task;
private CancellationTokenSource cancelToken;
public Service()
{
InitializeComponent();
this.task = null;
this.cancelToken = null;
}
protected override void OnStart(string[] args)
{
var svc = new MyServiceTask();
this.cancelToken = new CancellationTokenSource();
this.task = svc.RunAsync(cancelToken.Token);
this.task.ContinueWith(t => this.OnUnhandledException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
protected override void OnStop()
{
if (this.task != null)
{
this.cancelToken.Cancel();
this.task.Wait();
}
}
private void OnUnhandledException(Exception ex)
{
this.EventLog.WriteEntry(string.Format("Unhandled exception: {0}", ex), EventLogEntryType.Error);
this.task = null;
this.Stop();
}
}
As you can see, the service can catch unhandled exceptions. If this happens, the exception is logged and the service is stopped. This has the effect of writing two messages to the event log - one error stating there was an unhandled exception, and another stating that the service was successfully stopped.
This may sound minor, but I'm hoping to be able to suppress the 'successfully stopped' message. I find it misleading - it suggests that the service stopping was a normal occurrence. Is there another way I can force the service to stop itself without this message being logged?