Managing windows services for a server via a website seems like a good option instead of having to remote desktop or remote accessing services.
A simple ASP.NET website should suffice this requirement, Is that correct ?
No, here is a simple scenario to prove that.
User wants to start a windows service using the website, now there are many things that can go wrong :
To fix this issue polling every 10(sec) can be done to find the correct state of the service. But what if this clients polling be replaced with service polling only when the state of the service is changed.
Now lets dig deep into and find out how this can be done. First lets look at the architecture of "OnlineServiceManagement"
OnlineServiceManagement has got 2 parts
First define contracts at least one that has dual binding enabled
Now implement contract behaviour
That's it main components of windows service are completed. Windows service exposes some operations for website consumption and also has "IWinServiceNotificationHandler" for implementing call back to clients.
Dlls
Scripts
Service references
We are using KnockoutJS that follows MVVM pattern in which Model comes first. So lets define our Model first
OnlineServiceManagement
OnlineServiceManagement
“We have to stop optimizing for programmers and start optimizing for users.” – Jeff Atwood
A simple ASP.NET website should suffice this requirement, Is that correct ?
No, here is a simple scenario to prove that.
User wants to start a windows service using the website, now there are many things that can go wrong :
- Service taking longer time to process the request
- Service was unable to start due to some failure
- Users display shows invalid service state
To fix this issue polling every 10(sec) can be done to find the correct state of the service. But what if this clients polling be replaced with service polling only when the state of the service is changed.
Now lets dig deep into and find out how this can be done. First lets look at the architecture of "OnlineServiceManagement"
OnlineServiceManagement has got 2 parts
- WCF Window service with Dual HTTP binding enabled
- ASP.NET MVC website with SignalR and KnockoutJS
WCF Window service with Dual HTTP binding enabled
Note - This application uses nlog for loggingFirst define contracts at least one that has dual binding enabled
[ServiceContract] public interface IWinServiceNotificationHandler { [OperationContract(IsOneWay = true)] void ServiceStatusChanged(String ServiceName); } [ServiceContract( Name = "WinServiceProviderService", SessionMode = SessionMode.Required, CallbackContract = typeof(IWinServiceNotificationHandler))] public interface IWinServiceProvider { [OperationContract] string Ping(); [OperationContract] ServiceControllerStatus GetServiceStatus(String Identification, String ServiceName); [OperationContract] ServiceControllerStatus StartService(String Identification, String ServiceName); [OperationContract] ServiceControllerStatus StopService(String Identification, String ServiceName); [OperationContract] ServiceControllerStatus RestartService(String Identification, String ServiceName); }
Now implement contract behaviour
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.PerCall)] public class WinServiceProvider : IWinServiceProvider { private static ListNow define endpoint configuration in app.config for windows service_callbackList = new List (); private static Dictionary _services = new Dictionary (); public static readonly object _locker_ = new object(); public string Ping() { return "I can ping"; } private void RegisterClient(String Identification) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "Identifiction as '" + Identification + "'"); // Subscribe the guest to the beer inventory IWinServiceNotificationHandler client = OperationContext.Current.GetCallbackChannel (); if (!_callbackList.Contains(client)) { _callbackList.Add(client); } ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); } private void RegisterServiceNames(String ServiceName, ServiceControllerStatus Status) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "for Service '" + ServiceName + "'"); if (!_services.ContainsKey(ServiceName)) { _services.Add(ServiceName, Status); } else { _services[ServiceName] = Status; } ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); } public static void ResetStatuses() { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); try { lock (_locker_) { string[] serviceNames = _services.Keys.ToArray(); foreach (string serviceName in serviceNames) { ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == serviceName).FirstOrDefault(); _services[serviceName] = sc.Status; } } } catch (Exception ex) { ServiceLogger.Logger.Error(ex); } ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); } public static void NotifyClients(String ServiceName) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); ServiceLogger.Logger.Info("Found {0} callback clients", _callbackList.Count); try { _callbackList.ForEach( delegate(IWinServiceNotificationHandler callback) { callback.ServiceStatusChanged(ServiceName); }); ResetStatuses(); } catch (Exception ex) { ServiceLogger.Logger.ErrorException("Exception", ex); } ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); } public static string HasServiceStatusChanged() { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); string retStatusChanged = string.Empty; try { lock (_locker_) { string[] serviceNames = _services.Keys.ToArray(); foreach (string serviceName in serviceNames) { ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == serviceName).FirstOrDefault(); if (sc != null) { if (_services[serviceName] != sc.Status) { ServiceLogger.Logger.Info("Status changed for '{0}'", serviceName); retStatusChanged = serviceName; break; } } } } } catch (Exception ex) { ServiceLogger.Logger.Error(ex); } ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retStatusChanged; } public ServiceControllerStatus GetServiceStatus(String Identification, String ServiceName) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "Identifiction as '" + Identification + "'"); RegisterClient(Identification); ServiceControllerStatus retStatus = ServiceControllerStatus.StopPending; ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == ServiceName).FirstOrDefault(); if (sc != null) { retStatus = sc.Status; } RegisterServiceNames(ServiceName, sc.Status); //NotifyClients(ServiceName); ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retStatus; } public ServiceControllerStatus StartService(String Identification, String ServiceName) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "Identifiction as '" + Identification + "'"); RegisterClient(Identification); ServiceControllerStatus retStatus = ServiceControllerStatus.StopPending; ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == ServiceName).FirstOrDefault(); if (sc != null) { sc.Start(); retStatus = sc.Status; //while (retStatus != ServiceControllerStatus.Running) //{ // System.Threading.Thread.Sleep(1000); //} } RegisterServiceNames(ServiceName, sc.Status); //NotifyClients(ServiceName); ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retStatus; } public ServiceControllerStatus StopService(String Identification, String ServiceName) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "Identifiction as '" + Identification + "'"); RegisterClient(Identification); ServiceControllerStatus retStatus = ServiceControllerStatus.StopPending; ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == ServiceName).FirstOrDefault(); if (sc != null) { sc.Stop(); retStatus = sc.Status; //while (retStatus != ServiceControllerStatus.Stopped) //{ // System.Threading.Thread.Sleep(1000); //} } RegisterServiceNames(ServiceName, sc.Status); //NotifyClients(ServiceName); ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retStatus; } public ServiceControllerStatus RestartService(String Identification, String ServiceName) { ServiceLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name + "Identifiction as '" + Identification + "'"); RegisterClient(Identification); ServiceControllerStatus retStatus = ServiceControllerStatus.StopPending; ServiceController sc = ServiceController.GetServices().Where(s => s.ServiceName == ServiceName).FirstOrDefault(); if (sc != null) { retStatus = StopService(Identification, ServiceName); retStatus = StartService(Identification, ServiceName); } RegisterServiceNames(ServiceName, sc.Status); //NotifyClients(ServiceName); ServiceLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retStatus; } }
endpoint address="" binding="wsDualHttpBinding" contract="WinServiceManger.Contract.IWinServiceProvider"
That's it main components of windows service are completed. Windows service exposes some operations for website consumption and also has "IWinServiceNotificationHandler" for implementing call back to clients.
ASP.NET MVC website with SignalR and KnockoutJS
Using the default MVC template for creating the project should be sufficient. Now lets look into the details of website. Include following dependencies :Dlls
- SignalR
- NLog
Scripts
- JQuery
- JQuery Templates Plugin
- KnockoutJS
- Json
Service references
- WinServiceManger
We are using KnockoutJS that follows MVVM pattern in which Model comes first. So lets define our Model first
public class WindowsServicesModel { public ListWe have our model ready now, so lets define our View to be a KnockoutJS template that display list of services available.Services { get; set; } public WindowsServicesModel(String Identification) { LoadServices(Identification); } private void LoadServices(String Identification) { try { Services = new List () { new WinService { Name = "ASP.NET State Service", ServiceName = "aspnet_state", Description="Provides support for out-of-process session states for ASP.NET. If this service is stopped, out-of-process requests will not be processed. If this service is disabled, any services that explicitly depend on it will fail to start.", CSSClass = "one", Status= "" }, new WinService { Name = "Background Intelligent Transfer Service", ServiceName = "BITS", Description="Transfers files in the background using idle network bandwidth. If the service is disabled, then any applications that depend on BITS, such as Windows Update or MSN Explorer, will be unable to automatically download programs and other information.", CSSClass = "two", Status= "" }, new WinService { Name = "Bluetooth Support Service", ServiceName = "bthserv", Description="The Bluetooth service supports discovery and association of remote Bluetooth devices. Stopping or disabling this service may cause already installed Bluetooth devices to fail to operate properly and prevent new devices from being discovered or associated.", CSSClass = "three", Status= "" } }; foreach (WinService winService in Services) { WinServiceManger.ServiceControllerStatus status = Global.WinServiceMangerClient.GetServiceStatus(Identification, winService.ServiceName); switch (status) { case WinServiceManger.ServiceControllerStatus.Running: winService.Status = "Running"; break; case WinServiceManger.ServiceControllerStatus.ContinuePending: winService.Status = "Continue Pending"; break; case WinServiceManger.ServiceControllerStatus.Paused: winService.Status = "Paused"; break; case WinServiceManger.ServiceControllerStatus.PausePending: winService.Status = "Pause Pending"; break; case WinServiceManger.ServiceControllerStatus.StartPending: winService.Status = "Start Pending"; break; case WinServiceManger.ServiceControllerStatus.Stopped: winService.Status = "Stopped"; break; default: winService.Status = "Unknown"; break; } } } catch (Exception ex) { WebLogger.Logger.ErrorException("WindowsServicesModel.LoadServices Exception : ", ex); } } }
Now write javascript code for updating View that is defined earlier
var koHelper = { ViewModel: {}, LoadViewModel: function (resultData) { //var strViewModel = document.getElementById(viewModelFieldId).value; //var clientViewModel = ko.utils.parseJson(resultData); var clientViewModel = resultData; this.ViewModel = clientViewModel; ko.applyBindings(this.ViewModel); }, getWindowsServices: function () { $.ajax({ url: "/Home/GetWindowsServices/", type: 'post', //data: "{'customerID':'1' }", contentType: 'application/json', success: function (result) { koHelper.LoadViewModel(result); var dt = new Date(); koHelper.UpdateStatus("Autoupdate service status by Signal-R"); }, error: function (jqXHR, textStatus, errorThrown) { var errorMessage = ''; $('#message').html(jqXHR.responseText); } }); }, StartService: function (data, event) { $.ajax({ url: "/Home/StartService/", type: 'post', data: JSON.stringify( { 'ServiceName': data.ServiceName } ), contentType: 'application/json', dataType: 'json', success: function (result) { if (result != null) { koHelper.LoadViewModel(result); var dt = new Date(); koHelper.UpdateStatus("StartService"); //$.windowsServiceManager.performedServiceStatusChange(data.ServiceName); } }, error: function (jqXHR, textStatus, errorThrown) { var errorMessage = ''; $('#message').html(jqXHR.responseText); } }); }, StopService: function (data, event) { $.ajax({ url: "/Home/StopService/", type: 'post', data: JSON.stringify({ 'ServiceName': data.ServiceName }), contentType: 'application/json', dataType: 'json', success: function (result) { if (result != null) { koHelper.LoadViewModel(result); var dt = new Date(); koHelper.UpdateStatus("StopService"); //$.windowsServiceManager.performedServiceStatusChange(data.ServiceName); } }, error: function (jqXHR, textStatus, errorThrown) { var errorMessage = ''; $('#message').html(jqXHR.responseText); } }); }, RestartService: function (data, event) { $.ajax({ url: "/Home/RestartService/", type: 'post', data: JSON.stringify({ 'ServiceName': data.ServiceName }), contentType: 'application/json', dataType: 'json', success: function (result) { if (result != null) { koHelper.LoadViewModel(result); var dt = new Date(); koHelper.UpdateStatus("RestartService"); //$.windowsServiceManager.performedServiceStatusChange(data.ServiceName); } }, error: function (jqXHR, textStatus, errorThrown) { var errorMessage = ''; $('#message').html(jqXHR.responseText); } }); }, UpdateStatus: function (statusData) { var content = "" + new Date() + "" + statusData + ""; content += $('#statusInfo').html() ; $('#statusInfo').html(content); } }We have used following controller actions:-
- /Home/GetWindowsServices/
- /Home/StartService/
- /Home/StopService/
- /Home/RestartService/
public JsonResult GetWindowsServices() { JsonResult retValue = null; WebLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); try { if (Session["Identification"] != null) { WindowsServicesModel wsm = new WindowsServicesModel(Session["Identification"].ToString()); retValue = new JsonResult() { Data = wsm }; } } catch (Exception ex) { WebLogger.Logger.ErrorException("Error ", ex); } WebLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retValue; } [AcceptVerbs(HttpVerbs.Post)] public JsonResult StartService(string ServiceName) { JsonResult retValue = null; WebLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); try { if (Session["Identification"] != null) { WinServiceManger.ServiceControllerStatus status = Global.WinServiceMangerClient.StartService(Session["Identification"].ToString(), ServiceName); //if (status == WinServiceManger.ServiceControllerStatus.Running) //{ WindowsServicesModel wsm = new WindowsServicesModel(Session["Identification"].ToString()); retValue = new JsonResult() { Data = wsm }; //} } } catch (Exception ex) { WebLogger.Logger.ErrorException("Error ", ex); } WebLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retValue; } [AcceptVerbs(HttpVerbs.Post)] public JsonResult StopService(string ServiceName) { JsonResult retValue = null; WebLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); try { WinServiceManger.ServiceControllerStatus status = Global.WinServiceMangerClient.StopService(Session["Identification"].ToString(), ServiceName); WindowsServicesModel wsm = new WindowsServicesModel(Session["Identification"].ToString()); retValue = new JsonResult() { Data = wsm }; } catch (Exception ex) { WebLogger.Logger.ErrorException("Error ", ex); } WebLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retValue; } [AcceptVerbs(HttpVerbs.Post)] public JsonResult RestartService(string ServiceName) { JsonResult retValue = null; WebLogger.Logger.Info("Started " + System.Reflection.MethodInfo.GetCurrentMethod().Name); try { WinServiceManger.ServiceControllerStatus status = Global.WinServiceMangerClient.RestartService(Session["Identification"].ToString(), ServiceName); //if (status == WinServiceManger.ServiceControllerStatus.Running) //{ WindowsServicesModel wsm = new WindowsServicesModel(Session["Identification"].ToString()); retValue = new JsonResult() { Data = wsm }; //} } catch (Exception ex) { WebLogger.Logger.ErrorException("Error ", ex); } WebLogger.Logger.Info("Completed " + System.Reflection.MethodInfo.GetCurrentMethod().Name); return retValue; }Thats it! we are done with main components required for implementing our requirement. Complete solution is available on GitHub at
“We have to stop optimizing for programmers and start optimizing for users.” – Jeff Atwood