Click here to Skip to main content
Click here to Skip to main content

Self installing .NET service using the Win32 API

By , 28 Oct 2005
 

Test service in Service Manager

Introduction

The Windows service code that ships with the .NET framework and Visual Studio works just fine usually. However, sometimes it's just annoying to have to create an installer project just for a simple service you're writing. Furthermore, Microsoft tends to hide away the Win32 service infrastructure from us. I'm not saying that's a bad thing, but sometimes you just need something a little better. There're some advanced issues with services that make working with the API difficult and so wouldn't it be nice to have something that encapsulates all the functionality, but exposes it to those who need it? The code provides a base class and an attribute for you to use to define your own service.

Background

A thorough discussion of the intricacies of Win32 services is another subject. This code focuses on bringing it all together and then exposing it via an easy to use class and custom attribute.

Using the code

Using the code is as simple as deriving from the base class:

using System;
using HoytSoft.ServiceProcess.Attributes;

namespace MyServices {
    public class TestService : HoytSoft.ServiceProcess.ServiceBase {

        protected override bool Initialize(string[] Arguments) {
            this.Log("Test service initialized correctly, starting up...");
            return true;
        }
    }
}

And then defining an attribute:

using System;
using HoytSoft.ServiceProcess.Attributes;

namespace MyServices {
    [Service(
    "HoytSoft_TestService", 
    DisplayName         = "HoytSoft Test Service", 
    Description         = "Isn't this just absolutely amazing?",
    ServiceType         = ServiceType.Default,
    ServiceAccessType   = ServiceAccessType.AllAccess,
    ServiceStartType    = ServiceStartType.AutoStart,
    ServiceErrorControl = ServiceErrorControl.Normal,
    ServiceControls     = ServiceControls.Default
    )]
    public class TestService : HoytSoft.ServiceProcess.ServiceBase {

        protected override bool Initialize(string[] Arguments) {
            this.Log("Test service initialized correctly, starting up...");
            return true;
        }
    }
}

Test service in Service Manager

All the attributes correspond to their equivalent Win32 service descriptions. A lot of the code has been commented, so a glance at the intellisense should give you an idea of what each option is and how it modifies your Windows service.

It is important that you give a name to your service. It is a required parameter for the Service attribute. The attribute values specified here are used in the ServiceBase's Main() method. I suppose I should also note that your service should not define its own Main() method since ServiceBase uses its own that reads in Service attributes and then creates objects automatically for you. If the service has not been installed, it will auto-install it for you. To uninstall a service, simply pass in "u" or "uninstall" to the program. To manually do this, go to a DOS prompt and type in: sc delete "MyServiceName".

An interesting feature that hasn't been tested at all is the ability to use multiple services in a single application. To do this, be sure to set the ServiceType to ServiceType.ShareProcess or ServiceType.InteractiveProcessShare.

Now of most importance to you is probably developing, testing, and debugging your service. There is a property on the Service attribute named "Debug". Set this to true and the base class will automatically treat your program like a normal console program so you can develop the rest of the program. When you're ready to test it out as an actual service running on the machine, just switch this to false or take it out and it will work like a service. The program will install the service whether or not you're in debug mode when it's first run. If you try to run the program when it was compiled in Debug mode, you will get an error saying the process couldn't be started. This is by design since to gain all of the debug mode capabilities in Visual Studio, you can't have it start up as a real service. Simply switch the Debug mode, recompile, and it will run as normal.

You can write to the system event log by making a call to this.Log("My message here."):

The main meat of your code should be the overridden methods:

  • bool Initialize(string[] Arguments)

    "Arguments" are those passed in when called.

  • Start()
  • Stop()
  • Pause()
  • Continue()
  • Shutdown()
  • Interrogate()
  • Install()
  • Uninstall()

All of these should be fairly straightforward. Any questions?

Updates

  • 10/4/05: After the default timeout of 30 seconds (this is a Windows-imposed standard), if the service is not being run by the service control manager (SCM), the program runs like a normal console/Windows program. This is done in the ServiceBase's main method which will detect when it's not being run as a service and executes the proper methods on the service.

    You just continue to use the service like you normally would through the Initialize() and Start() methods. If you want to test other methods, you can optionally use the following methods from inside Start():

    • TestPause()
    • TestContinue()
    • TestStop()
    • TestShutdown()
    • TestInterrogate()
    • TestInstall()
    • TestUninstall()

    ServiceBase will take care of calling your methods while attempting to simulate a real situation or user by calling your overridden methods asynchronously.

  • 10/12/05: A message was added to the CodeProject article informing me of missing copyright notices in the source code, so I've added them here along with some more updates. This version has a major fix involving running it from a referenced, external DLL. If you put the code inside another DLL and then try to use it, you may notice you can start, but can't pause or stop your service. The fix, actually, was just adding a strong name key (HoytSoft.snk) to the external assembly. After I did this, I was able to run the service normally.

    As a result, I have divided the solution into two projects - one containing the service code and the other is just a simple example console service app.

    Important! The namespace has changed! It was HoytSoft.Service, but it is now HoytSoft.ServiceProcess to more accurately reflect its replacement of the System.ServiceProcess namespace. Please keep in mind that to install/run the service you must have the proper permissions!

  • 10/21/05: There were some more problems with referencing the ServiceBase class from a class library. I think the main problem was a delegate that was going out of scope that rendered a callback useless. Lots of people got errors when trying to pause, stop, or do anything on the service when using it as a class library. I was able to reproduce the problem and after making the aforementioned fixes, it worked just fine.

    Also of note is the ability to explicitly and only install the service without having to run the rest of the service. To do this, just start the service and pass in "i" or "install" and it will quickly install and exit. If you start the app normally and it hasn't been installed yet, the app will work like before and install it before proceeding. You should still be able to run it as a console or service app.

Points of Interest

  • The service installs itself and can uninstall itself through a command line argument ("u" or "uninstall").
  • You can run, test, and debug your service from within Visual Studio without having to create a separate installer project or use the ServiceController classes.

History

  • Took out "Debug" property from the ServiceAttribute attribute since the code now auto-detects this. (See "Points of Interest" above).

License

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

About the Author

DavidHoyt
Software Developer (Senior) Lawrence Livermore National Laboratories
United States United States
Member
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!

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralVista Compatiblememberagates292 Apr '07 - 8:28 
BTW, this code works great under XP. Under Vista, an error occurs whenever the service is stopped: Error 1061: The service cannot accept control message at this time. Any advice on fixing this error?
 
thanks again for the good work
 
jgates

GeneralRe: Vista CompatiblememberDavid Hoyt11 Jul '08 - 17:02 
The latest version addresses this issue and it should be resolved. Please let me know if it persists. See my post "NEW RELEASE!" for more info.
QuestionOnCustomCommand ?memberbacgeek20 Oct '06 - 5:24 
Just wondering, is there a OnCustomCommand(int) Method exposed ? and if not can it be implemented
 
i have many services which use it for custome control of the services (like starting and stoping diffrent components of the service)
 

AnswerRe: OnCustomCommand ?memberKerem Kat30 May '07 - 23:55 
that would be great.
AnswerRe: OnCustomCommand ?memberkim.david.hauser16 Aug '07 - 22:51 
You can use the baseServiceControlHandler Function > All msgs go there > It's possible to forward all Messages >128 and <255 to a custom event / delegate
GeneralRe: OnCustomCommand ?memberDavid Hoyt11 Jul '08 - 17:03 
This was added in the lastest version. See my post titled "NEW RELEASE!" for more info.
GeneralInstalling the service description programmatically [modified]memberckitching22 Jul '06 - 13:17 
Your code uses registry APIs to set the description, but it's probably better to use the ChangeServiceConfig2 API instead. This snippet of code should be enough to make it work:
 
At around line 617 in ServiceBase.cs in baseInstall():
                    ServicesAPI.CloseServiceHandle(sc_handle);
                    return false;
                }
+               //Set the service's description
+               ServicesAPI.SERVICE_DESCRIPTION info = new ServicesAPI.SERVICE_DESCRIPTION();
+
+               info.lpDescription = Description;
+               if (Description == null)
+                   info.lpDescription = "";
+               ServicesAPI.ChangeServiceConfig2A(sv_handle, ServicesAPI.InfoLevel.SERVICE_CONFIG_DESCRIPTION, ref info);
                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 {
And in ServiceAPI.cs near line 48:
            int dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup,
            int lpdwTagId, string lpDependencies, string lpServiceStartName,
            string lpPassword, string lpDisplayName);
 
!       [DllImport("advapi32.dll", CharSet = CharSet.Ansi, PreserveSig = true)]
        public static extern bool ChangeServiceConfig2A(
!           IntPtr hService, InfoLevel dwInfoLevel, 
            [MarshalAs(UnmanagedType.Struct)] ref SERVICE_DESCRIPTION lpInfo);
 
!       [DllImport("advapi32.dll", CharSet = CharSet.Ansi, PreserveSig = true)]
        public static extern bool ChangeServiceConfig2A(
!           IntPtr hService, InfoLevel dwInfoLevel, 
            [MarshalAs(UnmanagedType.Struct)] ref SERVICE_FAILURE_ACTIONS lpInfo);
 
        [DllImport("advapi32.dll")]
        public static extern int OpenServiceA(
            int hSCManager, string lpServiceName, ACCESS_TYPE dwDesiredAccess);
Not sure if specifying the charset specifically matters much, but the first parameter needs to be changed from int to IntPtr in the ChargeServiceConfig2 functions.
 
These changes appeared to work on my machine, but I have not tested it anywhere else.
 
 
-- modified at 19:18 Saturday 22nd July, 2006
GeneralRe: Installing the service description programmaticallymemberDavid Hoyt11 Jul '08 - 17:03 
This was fixed in the latest version. Please see my post titled "NEW RELEASE!" for more information.
QuestionBetter way to get the current process file name?membertimiscool99929 Apr '06 - 6:54 
I'm using SharpDevelop Beta 3 to run the project and I'm not sure if it's the HotySoft code or SharpDevelop not setting the command line arguments correctly, but there's a better way to get the full file path of a running process.
 
In ServiceBase.cs on lines 76-78 and 91-93 you may want to replace those lines with the following:
 

string processPath = Process.GetCurrentProcess().MainModule.FileName;
if (processPath != null && processPath.Length > 0) {
System.IO.FileInfo fi = new System.IO.FileInfo(processPath);

AnswerRe: Better way to get the current process file name?memberDavidHoyt16 May '06 - 15:02 
That sounds odd since the OS usually passes in the path of the executable as the first argument every time you run a program. If it works, though, then that's all that matters! (c: Good job! Did you create a sharp develop project? If you haven't changed the namespaces, I'd be interested in putting up a version of it on the web. Thanks!
AnswerRe: Better way to get the current process file name?memberDavid Hoyt11 Jul '08 - 17:04 
This was fixed in the latest version. Please see my post titled "NEW RELEASE!" for more information.
QuestionDebugmodememberMivano29 Dec '05 - 2:21 
This looks like a very nice system to use. Debugging services is difficult when you have to reinstall the service everytime.
However I dont understand the debugmode used in you ServiceProcess. How can I run the application as a real service? The debug property is readonly and always returns true if I build and run the app using debug or release mode.
It will log the start and initialise events, but I cannot run the service (Error 1053: The service did not respond to the start or control request in a timely fashion.).
 
So how do I switch to a real service from a 'fake' console app?

AnswerRe: DebugmodememberDavidHoyt29 Dec '05 - 5:49 
"Debug mode" unfortunately was a poor name for that property. (c: Perhaps "RunningAsConsoleApp" might be more descriptive. That property will return false when it's being run as a service - that is, when it's started through the SCM and using start, stop, pause, etc. As for that error, I frequently got that message and others like it. The other messages left for this article describe similar problems and resolutions and might be beneficial. Please make sure you're using the most recent version by downloading it from my own website: http://www.hoytsoft.org/serviceBase.aspx[^]. That has frequently solved most people's issues.
 
Thanks for your interest and I hope it all works out for you!
GeneralRe: DebugmodememberMivano29 Dec '05 - 9:02 
Thanks for your reply. I am using the latest version from your website. Even your simple example application has this problem. Runs fine as console application, but cannot start as a service. I tried this in VS2003 and VS2005 with both debug and release modes but no luck.
I keep trying, I like the idea of a service which is still debugable.
Questionthis.Log not working ?memberMark Longin12 Dec '05 - 23:59 
First of all after reading the article and looking at the source files : thanks for sharing this great code !
 
I downloaded the latest version (Rev3) from the hoytsoft.org website and compiled it in Release version on XP Pro with VS2003. I then ran the HoytSoft.Example.exe from the command line with the i argument to install it as a service. Using the Service Control Panel, I was able to start, pause, resume and stop the service.
 
However, the service did not write any entry at all in the Windows Event Log (I used the Windows Event Viewer to check it). Looking at the source code of the example service, I was expecting to see a lot of entries ...
 
What could I have done wrong ? Thanks for helping me out here !
AnswerRe: this.Log not working ?memberDavidHoyt13 Dec '05 - 21:01 
Hi Mark - Thanks! (c:
 
There could be problems with your event log - it could be full, in which case the code (depending on your point of view) gracefully fails to log anything. If it's not full, then check your logs for one named "Services" since all log entries are placed there (this is configurable in the Service attribute using the LogName property). If you changed the name of the log after already installing the service and generating log entries, then Windows can be finicky about it. It seems to prefer applications to keep using the same log every time. (It can be remedied through the registry, though). Honestly, I am not entirely sure what's causing you the log entry errors (it simply uses the normal .NET framework's logging classes). Have you stepped through the code and found the specific point at which it fails? That would sure help me to pinpoint the problem. (c: Thanks for your interest!
GeneralRe: this.Log not working ?memberMark Longin14 Dec '05 - 4:09 
David,
 
Thanks for your quick reaction !
 
"Services" !!! I was looking into the "Application" log. I didn't even know you could force the Event Log to create other logs then the tree basic ones. Learned something new here !
 
I tried to add the LogName property with as value "Application" to the example service code but at run time (installing the service), that generated an exception. Looking to the ServiceBase source code, I think I cannot just add the attribute, but I will have to at least add a constructing method which takes it as an argument. Isn't it ?
 
In any case, thanks again for sharing this great code and responding so quickly to my post.
 
Mark
AnswerRe: this.Log not working ?memberDavidHoyt14 Dec '05 - 6:24 
This is what I was referring to in my previous post about Windows being finicky - once an application is associated with a log, Windows doesn't like you changing it. The attribute works as-is when the app first writes a log entry. That is, the app associates itself with a specific log and then windows expects it to be the same every time. If it changes, it will throw you that exception. The solution is to edit the registry at:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Services
You will see a string value called "Sources" - delete that or delete the contents of it and then restart the service with the "Applications" log name. Your attribute declaration should look something like:
 
[Service(
"HoytSoft_ExampleService", 
DisplayName         = "HoytSoft Example Service",
Description         = "Isn't this just absolutely amazing?",
ServiceType         = ServiceType.Default,
ServiceAccessType   = ServiceAccessType.AllAccess,
ServiceStartType    = ServiceStartType.AutoStart,
ServiceErrorControl = ServiceErrorControl.Normal,
ServiceControls	  = ServiceControls.Default, 
LogName             = "Application"
)]
public class ExampleService : HoytSoft.ServiceProcess.ServiceBase {
	...
}
 
After deleting the registry entry and re-running the service, you'll see log entries appear under "Application." If this was done from the very start (e.g. when first ran on a new system), you will not have any of these problems. Good luck - let me know how it turns out! (c:
 
-- modified at 12:26 Wednesday 14th December, 2005
GeneralRe: this.Log not working ?memberMark Longin14 Dec '05 - 9:18 
David,
 
Unfortunaltely, the registry key you are pointing me at does not contain a string value called Sources. Here is what it contains :
 
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Services]
 
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Services\HoytSoft Example Service]
"EventMessageFile"="C:\\WINDOWS\\Microsoft.NET\\Framework\\v1.1.4322\\EventLogMessages.dll"
 
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Services\Services]
"EventMessageFile"="C:\\WINDOWS\\Microsoft.NET\\Framework\\v1.1.4322\\EventLogMessages.dll"

 
I tried removing the whole Services key with its two subkeys and their string values. That removed the Services entry from the Event Viewer. I then installed the service as compiled like you mention. I started, paused, resumed and stopped it, but saw no entries appearing in the Application log. However, it created a "HoytSoft Example Service" key under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application, but that didn't seem to be enough ...
 
I then unistalled the service, removed the created registry key and re-installed the original service. And ... SURPRISE ... the Services log was created again into the Event Log Viewer and contained all entries created before by the service compiled with the LogName = "Application" set ....
 
So, my problem remains. Hopefully you can still help me solving this because the service I need to write has to absolutely log into the Application log.
 
Thanks in advance,
 
Mark
GeneralRe: this.Log not working ?memberDavidHoyt14 Dec '05 - 14:18 
If doing it manually isn't working, you might try to do it programatically. (c: Check out: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemdiagnosticseventlogclassdeleteeventsourcetopic.asp[^]
 
What version of Windows are you using, btw?
 
Thanks for your patience!
GeneralRe: this.Log not working ?memberMark Longin14 Dec '05 - 23:14 
David,
 
The result is the same, whether I remove it manually or programmatically.
 
My guess, without an indeep look into your code, is that, even with the LogName property set as you indicated, there are still some traces of a LogName = "Services" elsewhere in the code that force the use of a spooky event log. The entries made into that log only become visible in the event viewer after thruthfully creating the "Services" log.
 
I you can agree with this and find any solution to it, please post it. I maybe looking into it more deeply within a week or two.
 
PS : Development and testing is done on a XP Pro SP2 machine.
 
Regards,
 
Mark
NewsRe: this.Log not working ?memberCPP Basara26 Jun '06 - 0:02 
1.Before doing the following, make a change to ServiceAttribute.cs so that it support
'LogName' argument, then use like this : (This will then log into 'Application' log file)
  [Service(
	"HoytSoft_ExampleService", 
	DisplayName		= "HoytSoft Example Service",
	Description		= "Isn't this just absolutely amazing?",
	ServiceType		= ServiceType.Default,
	ServiceAccessType	= ServiceAccessType.AllAccess,
	ServiceStartType	= ServiceStartType.AutoStart,
	ServiceErrorControl	= ServiceErrorControl.Normal,
	ServiceControls		= ServiceControls.Default,
	LogName			= "Application"
  )]
2.Find out 'Helper Methods' region ( In ServiceBase.cs ),
where checklog() function has a line
this.log.Source = this.displayName;
replace it with
this.log.Source = this.name;
( Because 'Source' should be service name.),
then it will work fine.
 
Just try it. It works.
 
If you want to use custom EventLog file, just read MSDN... it helpsSmile | :) .
No matter how you use this, make sure that EventLog file exist first,
then you will log any messages as you wish.
 
-----------------------------------------------------------------------
I'm interested in any kinds of computer programming, so are you, right?
GeneralErrormemberQuinton Viljoen21 Oct '05 - 0:46 
Hi
 
First of all, 5 out of 5!!! The service base is awesome! To bad about my debug attribute not working, as I thought that is quite nice to have, atleast still works when you build in VS.
 
Whenever I try to pause or stop the service I get the following error.
 
Could not pasuse the service on your local computer. The service did not return an error. This could be an internal Windows error or an internal service error.
 
There's not much code in my servicer so far only logging to even viewer which is working.
 
When I click stop a second time it finally stops the service, only after it times out while stopping, but does stop.
 
Could you please lead me in the correct direction to resolve this issue.
 
Thanks
Quinton
AnswerRe: ErrormemberDavidHoyt21 Oct '05 - 10:47 
Hi Quinton,
 
This looks like another problem with hosting the ServiceBase in a class library instead of the executable itself. I did some more testing and believe I have it fixed. The code project article does not have the latest updates since I did it today. Please go to http://www.hoytsoft.org/serviceBase.aspx[^] to download the latest version (revision 3) and see the changes I've made. Apparently a callback from the service API was not working correctly, but should be fixed now. The delegate I was using for the service control handler callback was going out of scope and therefore garbage collected and then not around when the SCM tried to use the callback. So it should work as expected now!
GeneralRe: ErrormemberQuinton Viljoen23 Oct '05 - 18:25 
I will give it try using the new code.
 
Thanks.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 28 Oct 2005
Article Copyright 2005 by DavidHoyt
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid