Click here to Skip to main content
15,891,033 members
Articles / Programming Languages / C#

Self installing .NET service using the Win32 API

Rate me:
Please Sign up or sign in to vote.
4.73/5 (21 votes)
28 Oct 2005CPOL5 min read 177.9K   1.6K   90  
Sometimes the service classes provided by Visual Studio don't give you the control you need, so why not build your own? And while you're at it, why not make it self-installing? The base class provided gives you full control of the Win32 Services API from a convenient base class and attribute.
using System;
using System.Reflection;
using System.ServiceProcess;
using System.Runtime.InteropServices;
/*
 Copyright (c) 2005, 2006 David Hoyt
 
 A huge thank-you to C.V Anish for his article: http://www.codeproject.com/system/windows_nt_service.asp
 It paved the way for this class. Although I used his article to better understand the internals of Win32
 services, almost everything here is original except where noted.
*/
namespace HoytSoft.Service {
	public enum ServiceState : byte {
		Running,
		Stopped,
		Paused,
		ShuttingDown,
		Interrogating
	}

	///<summary>A base class for installing, starting, pausing, etc. a Windows service.</summary>
	public abstract class ServiceBase {
		#region Entry Point
		///<summary>Executes your service. If multiple services are defined in the assembly, it will run them all in separate threads.</summary>
		/// <param name="Args">The arguments passed in from the command line.</param>
		public static void Main(string[] Args) {
			//Reads in all the classes in the assembly and finds one that derives
			//from this class. If it finds one, it checks the attributes to see
			//if we should run it. If we should, we create an instance of it and 
			//start it on its way...
			Assembly a = Assembly.GetExecutingAssembly();
			if (a == null) throw new ServiceException("No currently executing assembly.");

			Type[] types = a.GetTypes();
			System.Collections.ArrayList alDispatchTables = new System.Collections.ArrayList();
			foreach(Type t in types) {
				if (t.IsClass && t.BaseType != null && t.BaseType.Equals(typeof(ServiceBase))) {
					//Gets all the custom attributes of type ServiceAttribute in the class.
					object[] attributes = t.GetCustomAttributes(typeof(Attributes.ServiceAttribute), true);
					foreach(Attributes.ServiceAttribute info in attributes) {
						if (info.Run) {
							ServiceBase s = (ServiceBase)Activator.CreateInstance(t);

							//Make sure we have a name set for this guy...
							if (s.Name == null || s.Name.Trim() == "")
								throw new ServiceRuntimeException("A service was created without a name.");

							if (Args.Length > 0 && (Args[0].ToLower() == "u" || Args[0].ToLower() == "uninstall")) {
								//Nothing to uninstall if it's not installed...
								if (!s.baseIsInstalled(info.Name)) break;
								if (!s.baseUninstall(info.Name))
									throw new ServiceUninstallException("Unable to remove service \"" + info.DisplayName + "\"");
								if (!s.Uninstall())
									throw new ServiceUninstallException("Service \"" + info.DisplayName + "\" was unable to uninstall itself correctly.");
							} else {
								//Always check to see if the service is installed and if it isn't,
								//then go ahead and install it...
								if (!s.baseIsInstalled(info.Name)) {
									string[] envArgs = Environment.GetCommandLineArgs();
									if (envArgs.Length > 0 && !s.baseInstall(envArgs[0], info.Name, info.DisplayName, info.Description, info.ServiceType, info.ServiceAccessType, info.ServiceStartType, info.ServiceErrorControl))
										throw new ServiceInstallException("Unable to install service \"" + info.DisplayName + "\"");
									if (!s.Install())
										throw new ServiceInstallException("Service was not able to install itself correctly.");
								}

								if (!s.debug) {
									ServicesAPI.SERVICE_TABLE_ENTRY entry = new HoytSoft.Service.ServicesAPI.SERVICE_TABLE_ENTRY();
									entry.lpServiceName = info.Name;
									entry.lpServiceProc = new HoytSoft.Service.ServicesAPI.ServiceMainProc(s.baseServiceMain);
									alDispatchTables.Add(entry);
								} else {
									s.args = Args;
								}
							}
						}
						break; //We can break b/c we only allow ONE instance of this attribute per object...
					}
				}
			}

			if (alDispatchTables.Count > 0) {
				//Add a null entry to tell the API it's the last entry in the table...
				ServicesAPI.SERVICE_TABLE_ENTRY entry = new HoytSoft.Service.ServicesAPI.SERVICE_TABLE_ENTRY();
				entry.lpServiceName = null;
				entry.lpServiceProc = null;
				alDispatchTables.Add(entry);

				ServicesAPI.SERVICE_TABLE_ENTRY[] table = (ServicesAPI.SERVICE_TABLE_ENTRY[])alDispatchTables.ToArray(typeof(ServicesAPI.SERVICE_TABLE_ENTRY));
				if (ServicesAPI.StartServiceCtrlDispatcher(table) == 0) {
					//There was an error. What was it?
					switch(Marshal.GetLastWin32Error()) {
						case ServicesAPI.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
							throw new ServiceStartupException("A service is being run as a console application. Try setting the Service attribute's \"Debug\" property to true if you're testing an application.");
						case ServicesAPI.ERROR_INVALID_DATA:
							throw new ServiceStartupException("The specified dispatch table contains entries that are not in the proper format.");
						case ServicesAPI.ERROR_SERVICE_ALREADY_RUNNING:
							throw new ServiceStartupException("A service is already running.");
						default:
							throw new ServiceStartupException("An unknown error occurred while starting up the service(s).");
					}
				}
			}
			alDispatchTables.Clear();
		}
		#endregion

		#region Private Variables
		private string name;
		private string displayName;
		private string description;
		private bool run;
		private bool debug;
		private string[] args;
		private ServiceState servState;
		private Attributes.ServiceType servType;
		private Attributes.ServiceAccessType servAccessType;
		private Attributes.ServiceStartType servStartType;
		private Attributes.ServiceErrorControl servErrorControl;
		private Attributes.ServiceControls servControls;
		private ServicesAPI.SERVICE_STATUS servStatus;
		private IntPtr servStatusHandle;
		private System.Diagnostics.EventLog log;
		#endregion

		#region Constructors
		public ServiceBase() {
			object[] attributes = this.GetType().GetCustomAttributes(typeof(Attributes.ServiceAttribute), true);
			foreach(Attributes.ServiceAttribute info in attributes) {
				this.name = info.Name;
				this.displayName = info.DisplayName;
				this.description = info.Description;
				this.run = info.Run;
				this.servType = info.ServiceType;
				this.servAccessType = info.ServiceAccessType;
				this.servStartType = info.ServiceStartType;
				this.servErrorControl = info.ServiceErrorControl;
				this.servControls = info.ServiceControls;
				this.debug = info.Debug;
			}
			this.servStatus = new HoytSoft.Service.ServicesAPI.SERVICE_STATUS();
			this.servState = ServiceState.Stopped;
			this.args = null;

			if (this.debug) {
				System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(baseDebugStart));
				t.Start();
			}
		}
		#endregion

		#region Properties
		///<summary>The name of the service used in the service database.</summary>
		public string Name { get { return this.name; } }
		///<summary>The name of the service that will be displayed in the services snap-in.</summary>
		public string DisplayName { get { return this.displayName; } }
		///<summary>The description of the service that will be displayed in the service snap-in.</summary>
		public string Description { get { return this.description; } }
		///<summary>Indicates if you want the service to run or not on program startup.</summary>
		public bool Run { get { return this.run; } }
		///<summary>Indicates the type of service you want to run.</summary>
		public Attributes.ServiceType ServiceType { get { return this.servType; } }
		///<summary>Access to the service. Before granting the requested access, the system checks the access token of the calling process.</summary>
		public Attributes.ServiceAccessType ServiceAccessType { get { return this.servAccessType; } }
		///<summary>Service start options.</summary>
		public Attributes.ServiceStartType ServiceStartType { get { return this.servStartType; } }
		///<summary>Severity of the error, and action taken, if this service fails to start.</summary>
		public Attributes.ServiceErrorControl ServiceErrorControl { get { return this.servErrorControl; } }
		///<summary>The controls or actions the service responds to.</summary>
		public Attributes.ServiceControls ServiceControls { get { return this.servControls; } }
		///<summary>The current state of the service.</summary>
		public ServiceState ServiceState { get { return this.servState; } }
		///<summary>Treats the service as a console application instead of a normal service.</summary>
		public bool Debug { get { return this.debug; } }
		#endregion

		#region Override Methods
		protected virtual bool Install() { return true; }
		protected virtual bool Uninstall() { return true; }
		protected virtual bool Initialize(string[] Arguments) { return true; }
		protected virtual void Start() { }
		protected virtual void Pause() { }
		protected virtual void Stop() { }
		protected virtual void Continue() { }
		protected virtual void Shutdown() { }
		protected virtual void Interrogate() { }
		#endregion

		#region API Methods
		[DllImport("advapi32.dll")]
		public static extern IntPtr CreateService(IntPtr scHandle, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpSvcName, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpDisplayName, 
			Attributes.ServiceAccessType dwDesiredAccess, 
			Attributes.ServiceType dwServiceType, 
			Attributes.ServiceStartType dwStartType, 
			Attributes.ServiceErrorControl dwErrorControl, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpPathName, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpLoadOrderGroup, 
			int lpdwTagId, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpDependencies, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpServiceStartName, 
			//[MarshalAs(UnmanagedType.LPTStr)]
			string lpPassword);

		[DllImport("advapi32.dll")]
		public static extern IntPtr OpenService(IntPtr hSCManager, 
			string lpServiceName, 
			Attributes.ServiceAccessType dwDesiredAccess);

		[DllImport("advapi32.dll")]
		public static extern IntPtr OpenService(IntPtr hSCManager, 
			string lpServiceName, 
			int dwDesiredAccess);

		[DllImport("advapi32.dll")]
		public static extern int DeleteService(IntPtr svHandle);
		#endregion

		#region Helper Methods
		public void Log(string Message) {
			Log(Message, System.Diagnostics.EventLogEntryType.Information);
		}
		public void Log(string Message, System.Diagnostics.EventLogEntryType EntryType) {
			if (this.log == null) {
				this.log = new System.Diagnostics.EventLog("Services");
				this.log.Source = this.displayName;
			}
			this.log.WriteEntry(Message, EntryType);
		}
		#endregion

		private void baseDebugStart() {
			if (this.Initialize(this.args)) {
				this.servState = ServiceState.Running;
				this.Start();
				this.Stop();
			}
		}

		private void baseServiceMain(int argc, string[] argv) {
			if (this.ServiceType != Attributes.ServiceType.FileSystemDriver && this.ServiceType != Attributes.ServiceType.KernelDriver)
				this.servStatus.dwServiceType = ServicesAPI.SERVICE_WIN32;
			else
				this.servStatus.dwServiceType = (int)this.ServiceType;
			this.servStatus.dwCurrentState = ServicesAPI.SERVICE_START_PENDING;
			this.servStatus.dwControlsAccepted = (int)this.ServiceControls;
			this.servStatus.dwWin32ExitCode = 0;
			this.servStatus.dwServiceSpecificExitCode = 0;
			this.servStatus.dwCheckPoint = 0;
			this.servStatus.dwWaitHint = 0;

			this.servStatusHandle = ServicesAPI.RegisterServiceCtrlHandler(this.Name, new ServicesAPI.ServiceCtrlHandlerProc(this.baseServiceControlHandler)); 
			if (servStatusHandle == IntPtr.Zero) return;
			this.servStatus.dwCurrentState = ServicesAPI.SERVICE_RUNNING;
			this.servStatus.dwCheckPoint = 0;
			this.servStatus.dwWaitHint = 0;
			if (ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus) == 0) {
				throw new ServiceRuntimeException("\"" + this.displayName + "\" threw an error. Error Number: " + Marshal.GetLastWin32Error());
			}

			//Call the initialize method...
			if (!this.Initialize(argv)) {
				this.servStatus.dwCurrentState       = ServicesAPI.SERVICE_STOPPED; 
				this.servStatus.dwCheckPoint         = 0; 
				this.servStatus.dwWaitHint           = 0; 
				this.servStatus.dwWin32ExitCode      = 1; 
				this.servStatus.dwServiceSpecificExitCode = 1;
 
				ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus); 
				return; 
			}

			//Initialization complete - report running status. 
			this.servStatus.dwCurrentState       = ServicesAPI.SERVICE_RUNNING; 
			this.servStatus.dwCheckPoint         = 0; 
			this.servStatus.dwWaitHint           = 0; 
 
			if (ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus) == 0) {
				throw new ServiceRuntimeException("\"" + this.displayName + "\" threw an error. Error Number: " + Marshal.GetLastWin32Error());
			}

			this.servState = ServiceState.Running;
			this.Start();
		}

		//This is called whenever a service control event happens such as pausing, stopping, etc...
		private void baseServiceControlHandler(int Opcode) {
			switch(Opcode) {
				case ServicesAPI.SERVICE_CONTROL_PAUSE: 
					this.servState = ServiceState.Paused;
					this.servStatus.dwCurrentState = ServicesAPI.SERVICE_PAUSED;
					this.Pause();
					ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus);
					break;

				case ServicesAPI.SERVICE_CONTROL_CONTINUE:
					this.servState = ServiceState.Running;
					this.servStatus.dwCurrentState = ServicesAPI.SERVICE_RUNNING;
					this.Continue();
					ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus);
					break;

				case ServicesAPI.SERVICE_CONTROL_STOP:
					this.servState = ServiceState.Stopped;
					this.servStatus.dwWin32ExitCode = 0;
					this.servStatus.dwCurrentState = ServicesAPI.SERVICE_STOPPED;
					this.servStatus.dwCheckPoint = 0;
					this.servStatus.dwWaitHint = 0;

					this.Stop();
					ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus);
					break;

				case ServicesAPI.SERVICE_CONTROL_SHUTDOWN:
					this.servState = ServiceState.ShuttingDown;
					this.servStatus.dwCurrentState = ServicesAPI.SERVICE_STOPPED;
					this.Shutdown();
					ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus);
					break;

				case ServicesAPI.SERVICE_CONTROL_INTERROGATE:
					this.servState = ServiceState.Interrogating;
					this.servStatus.dwCurrentState = ServicesAPI.SERVICE_INTERROGATE;
					this.Interrogate();
					ServicesAPI.SetServiceStatus(this.servStatusHandle, ref this.servStatus);
					break; 
			}
		}

		private bool baseIsInstalled(string Name) {
			//Iterates through all the known services and looks for one
			//matching the "Name" parameter...
			ServiceController[] sc = ServiceController.GetServices();
			foreach(ServiceController s in sc) {
				if (s.ServiceName == Name) return true;
			}
			sc = ServiceController.GetDevices();
			foreach(ServiceController s in sc) {
				if (s.ServiceName == Name) return true;
			}
			return false;
		}

		//Got some help from: http://www.c-sharpcorner.com/Code/2003/Sept/InstallingWinServiceProgrammatically.asp
		private bool baseUninstall(string Name) {
			if (Name.Length > 256) throw new ServiceInstallException("The maximum length for a service name is 256 characters.");
			try {
				IntPtr sc_hndl = ServicesAPI.OpenSCManagerA(null, null, ServicesAPI.GENERIC_WRITE);
 
				if(sc_hndl != IntPtr.Zero) {
					IntPtr svc_hndl = OpenService(sc_hndl, Name, ServicesAPI.DELETE);
					if(svc_hndl != IntPtr.Zero) { 
						int i = DeleteService(svc_hndl);
						if (i != 0) {
							ServicesAPI.CloseServiceHandle(sc_hndl);
							return true;
						} else {
							ServicesAPI.CloseServiceHandle(sc_hndl);
							return false;
						}
					} else return false;
				} else return false;
			} catch {
				return false;
			}
		}

		//Got some help from: http://www.c-sharpcorner.com/Code/2003/Sept/InstallingWinServiceProgrammatically.asp
		private bool baseInstall(string ServicePath, string Name, string DisplayName, string Description, Attributes.ServiceType ServType, Attributes.ServiceAccessType ServAccessType, Attributes.ServiceStartType ServStartType, Attributes.ServiceErrorControl ServErrorControl) {
			if (Name.Length > 256) throw new ServiceInstallException("The maximum length for a service name is 256 characters.");
			if (Name.IndexOf(@"\") >= 0 || Name.IndexOf(@"/") >= 0) throw new ServiceInstallException(@"Service names cannot contain \ or / characters.");
			if (DisplayName.Length > 256) throw new ServiceInstallException("The maximum length for a display name is 256 characters.");
			//The spec says that if a service's path has a space in it, then we must quote it...
			//if (ServicePath.IndexOf(" ") >= 0)
			//	ServicePath = "\"" + ServicePath + "\"";
			//ServicePath = ServicePath.Replace(@"\", @"\\");

			try {
				IntPtr  sc_handle = ServicesAPI.OpenSCManagerA(null, null, ServicesAPI.ServiceControlManagerType.SC_MANAGER_CREATE_SERVICE);
				if (sc_handle == IntPtr.Zero) return false;

				IntPtr sv_handle = CreateService(sc_handle, Name, DisplayName, ServAccessType, ServType, ServStartType, ServErrorControl, ServicePath, null, 0, null, null, null);
				//IntPtr sv_handle = ServicesAPI.CreateService(sc_handle, Name, DisplayName, 0xF0000 | 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040 | 0x0080 | 0x0100, 0x00000010, 0x00000002, 0x00000001, ServicePath, null, 0, null, null, null);
				if (sv_handle == IntPtr.Zero) {
					ServicesAPI.CloseServiceHandle(sc_handle);
					return false;
				}
				ServicesAPI.CloseServiceHandle(sv_handle);
				ServicesAPI.CloseServiceHandle(sc_handle);

				//Sets a service's description by adding a registry entry for it.
				if (Description != null && Description != "") {
					try {
						using (Microsoft.Win32.RegistryKey serviceKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"System\CurrentControlSet\Services\" + Name, true)) {
							serviceKey.SetValue("Description", Description);
						}
					} catch {
						return false;
					}
				}

				return true;
			} catch {
				return false;
			}
		}
	}

	#region Helper Classes
	#region Exceptions
	public class ServiceRuntimeException : ServiceException {
		public ServiceRuntimeException(string Message) : base(Message) { }
		public ServiceRuntimeException(string Message, Exception InnerException) : base(Message, InnerException) { }
	}

	public class ServiceStartupException : ServiceException {
		public ServiceStartupException(string Message) : base(Message) { }
		public ServiceStartupException(string Message, Exception InnerException) : base(Message, InnerException) { }
	}

	public class ServiceUninstallException : ServiceException {
		public ServiceUninstallException(string Message) : base(Message) { }
		public ServiceUninstallException(string Message, Exception InnerException) : base(Message, InnerException) { }
	}

	public class ServiceInstallException : ServiceException {
		public ServiceInstallException(string Message) : base(Message) { }
		public ServiceInstallException(string Message, Exception InnerException) : base(Message, InnerException) { }
	}

	public class ServiceException : Exception {
		public ServiceException(string Message) : base(Message) { }
		public ServiceException(string Message, Exception InnerException) : base(Message, InnerException) { }
	}
	#endregion
	#endregion
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Lawrence Livermore National Laboratories
United States United States
I'm a recent graduate of Brigham Young University in Provo, UT and now working for Lawrence Livermore National Laboratories (LLNL). I've been programming since I was 14 and did the amazing Hoyt family website with an animated gif of a spinning globe. I've come a long way since then and now actually use pictures of people.

I've been interested in website development and Windows programming since and I haven't stopped except for two years spent in El Salvador as a religious representative for my church.

I've done lots of work with C#/C/C++/Java/Python/JavaScript/Scheme/T-SQL/PL-SQL/Visual Basic/etc., web services, windows apps, services, and web apps. It's been a lot of fun!

Comments and Discussions