Introduction
After couple of times searching for an appropriate solution, I have decided to write my own. All topics about self-installing services assume that the service will be installed once without prompting to enter service name. So, uninstall performs operation with executable assemblies not with service. Second issue is that all solutions offer install and start service by the System, not by User and that doesn't suit me. I have based my solution on one of many self-installs like solutions and just modify something.
Using the Code
First of all, we must create a service. Let's do it:
namespace Self_Installing_Service
{
class Program : ServiceBase
{
static void Main(string[] args)
{
bool debugMode = false;
if (args.Length > 0)
{
for (int ii = 0; ii < args.Length; ii++)
{
switch (args[ii].ToUpper())
{
case "/I":
InstallService();
return;
case "/U":
UninstallService();
return;
case "/D":
debugMode = true;
break;
default:
break;
}
}
}
if (debugMode)
{
Program service = new Program();
service.OnStart(null);
Console.WriteLine("Service Started...");
Console.WriteLine("<press any key to exit...>");
Console.Read();
}
else
{
System.ServiceProcess.ServiceBase.Run(new Program());
}
}
protected override void OnStart( string[] args ) { }
protected override void OnStop() { }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
private static void InstallService()
{
try
{
ManagedInstallerClass.InstallHelper( new string[]
{ Assembly.GetExecutingAssembly().Location } );
}
catch { }
}
private static void UninstallService()
{
ManagedInstallerClass.InstallHelper( new string[]
{ "/u", Assembly.GetExecutingAssembly().Location } );
}
}
}
No magic with it. Second, we need to create a service installer. All that you need is create another class derived from System.Configuration.Install.Installer
and add [RunInstaller(true)]
attribute to it:
[RunInstaller(true)]
public class CustomServiceInstaller : Installer
{
private ServiceProcessInstaller process;
private ServiceInstaller service;
public CustomServiceInstaller()
{
process = new ServiceProcessInstaller();
process.Account = ServiceAccount.User;
service = new ServiceInstaller();
Installers.Add( process );
Installers.Add( service );
}
}
Great! But there are two issuses: there is no service name and no same installed service check. Begin solving, I create a form like this:
"Service name" is install service name. "Password" is the current user password. So you must be an administrator to perform service install procedure. Next, override "OnBeforeInstall
" method:
private void RemoveIfExists( string serviceName )
{
if( ServiceController.GetServices().Any
( s => s.ServiceName.ToLower() == serviceName.ToLower() ) )
{
Uninstall( null );
}
}
protected override void OnBeforeInstall( IDictionary savedState )
{
AuthForm form = new AuthForm();
if ( form.ShowDialog() == DialogResult.OK )
{
process.Username = WindowsIdentity.GetCurrent().Name;
string sname = "";
string pwd = "";
form.Get( out sname, out pwd );
process.Password = pwd;
service.ServiceName = sname;
service.Description = Assembly.GetExecutingAssembly().GetName().Name;
RemoveIfExists( sname );
base.OnBeforeInstall( savedState );
}
else
{
throw new System.Exception( "Operation canceled by user" );
}
}
Summary
Now, we can create as many service instances as we can. And no more fear that named service already exists. Good luck. Regards.
Updates
24th December, 2014:
There is one major issue in my project. Installed services can`t be removed from system just by calling
ManagedInstallerClass.InstallHelper( new string[] { "/u", Assembly.GetExecutingAssembly().Location } );
it happens because we are changing ServiceName in runtime. It`s one way to delete a service, find all services, that spawned by our assembly and call uninstall method for each of. Thanks to Shmuli
and his solution A ServiceController Class that Contains the Path to Executable. According to it, i am made some changes:
In the service class append new method GetImagePath and modified uninstal method:
static Regex pathrx = new Regex("(?<=\").+(?=\")");
private static string GetImagePath(string servicename )
{
string registryPath = @"SYSTEM\CurrentControlSet\Services\" + servicename;
RegistryKey keyHKLM = Registry.LocalMachine;
RegistryKey key;
key = keyHKLM.OpenSubKey( registryPath );
string value = key.GetValue( "ImagePath" ).ToString();
key.Close();
var result = pathrx.Match(value);
if( result.Success )
return result.Value;
return value;
}
private static void UninstallService()
{
string binpath = Assembly.GetExecutingAssembly().Location;
string dir = binpath.Remove( binpath.LastIndexOf( "\\" ) );
var toBeRemoved = ServiceController.GetServices().Where( s => GetImagePath( s.ServiceName ) == binpath ).Select( x => x.ServiceName );
CustomServiceInstaller installer = new CustomServiceInstaller();
installer.Context = new InstallContext();
foreach ( var sname in toBeRemoved )
{
try
{
installer.Uninstall( sname );
}
catch { }
}
}
In the InstallerClass make overload method Uninstall, which takes a ServiceName to be uninstalled as param:
public void Uninstall( string serviceName )
{
service.ServiceName = serviceName;
base.Uninstall( null );
}
Source also updated.
History
- 19th December, 2014: Initial version
- In previous versions, I've forced debug mode:
bool debugMode = true;
So in that case, installed service never starts normally and you will receive an #1053 error. Just turn in to false
.
- 24th December, 2014: Uninstall issue fixed