Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Windows Vista aware NT Service interacting with the desktop

4.90/5 (30 votes)
27 Mar 200735 min read 1   1.4K  
How to build Windows Vista aware Windows NT Service which can interact with the user

Contents

Introduction

Do you have kids, nieces or young sisters, brothers sitting in front of their boxes and spending a lifetime surfing the web and chatting to peers? Perhaps it would be nice to limit those activities in a soft and clear manner implementing negotiated family policies?

A couple of years ago I created a service for Windows XP watching specified processes, like the IE and Messenger and counting the running minutes of those programs. Exceeding the credits the user was informed that the time available have exceeded the negotiated limits and the application will shut down. Thereafter the service have killed the watched processes and all subsequently started processes were also forced to exit.

With Windows Vista on board the picture has slightly changed and in an unsafe manner written services running with the Flag "Allow service to interact with desktop" will not work at all. This is by design and detailed description you can find in the article "Services in Windows Vista". Also note you cannot do absolutely nothing about it except redesigning your service if you need interacting with the desktop.

In this article you will find a step-by-step guide how to create a Windows Vista aware service seamlessly interacting with the desktop which also works perfectly with earlier versions of Windows like XP and Server 2003. The way which I'm going to show you is just one possibility which I have found practicable. This article is not intended to discuss all possible solutions, and doubtlessly I believe you will find some more valuable ideas in this regard.

1. Design goals and requirements

Here is an arbitrary list of requirements which I have found practicable:

  1. The application is targeting Windows Vista and should be compatible to Windows OS supporting the .NET Framework 3.0
  2. The application should be configurable e.g. via an application configuration file (Figure 1)
  3. The configuration contains at least these read-only application settings:
    • The list of watched users, which represent the names of the watched users' computer-accounts. See attribute "UsersToWatch".
    • The list of watched applications, like iexplore, msnmsgr, firefox, etc. See attribute "ProcessesToWatch".
    • The daily allowance (limit in minutes) to use the listed above programs, e.g. 150 minutes (2,5 hours a day). This will apply to each watched user. See attribute "DailyCredit"
    • The list of weekdays to watch. See attribute "DaysToWatch". The listed numbers (1 trough 7) are encoded like 1 is Monday, 2 is Tuesday, and so on.
    • Exactly prohibited time intervals (e.g. between 4PM and 6PM) in which even if the daily allowance is not exceeded, the watched programs cannot be used. See the attribute "LockedTimeSpans" consists of pairs of figures. This means that between 10 a.m. and 12 a.m. and between 8 p.m. and noon the watched applications usage is prohibited.
  4. Logging off/on and restarting the computer should not initialize the counter and rather just continue according to the configuration policy. This also assumes, that stopping the service has to persist the counted data
  5. The user should be notified in a non-intrusive manner about daily minutes available and about prohibited time-spans. This assumes an User Interface (UI) displayed.
  6. Closing the UI should not close and exit the application, rather only hide it. The user should have the possibility to reactivate the UI e.g. via a notify icon in the system toolbar.
  7. The UI should run in the context of the interactively logged-on user
  8. Fast user switching hast to be supported
  9. Power events must not break the application
...
<configuration>
    <appSettings>
        <add key="ProcessesToWatch" value="iexplore,msnmsgr,firefox"/>
        <add key="DailyCredit" value="150"/>

        <add key="LogName" value="StefanoLog"/>
        <add key="SourceName" value="ProgramWatcher"/>
        <add key="DaysToWatch" value="1,2,3,4,5,6"/>

        <add key="LockedTimeSpans" value="10,12,20,24"/>
        <add key="UsersToWatch" value="kid1,kid2,kid3"/>
    </appSettings>

</configuration>
...

Figure 1 Application configuration file

2. In search of an appropriate solution

First of all note that the service we are talking about is a process-list level observer running silently in the background. Furthermore, the service has to be capable of scrutinizing the security tokens of the processes being watched. This has to be done periodically in terms of filtering the users running those processes according to the list of watched users.

Microsoft suggests to run services with the least possible privileged user. Here is the list you could choose of: LOCAL SERVICE, NETWORK SERVICE, LOCAL SYSTEM and a dedicated User Account. Considering all those possibilities starting with LOCAL SERVICE, you will stick to the LOCAL SYSTEM account. Let's explain why. Opening processes' security tokens is an operation which needs SE_SECURITY_NAME privilege. This privilege is however by default granted only for Administrators and the LOCAL SYSTEM account.

We learned that, in Windows Vista, only interactively logged on users are allowed to interact with the desktop. And here comes the challenge. As you know our service, silently watching all running processes, cannot display any UI and therefore this task has to be delegated to the currently logged on user in some magic way. Also note, that we do not want to be responsible for keeping passwords e.g. to use impersonation. The mandatory task is to provide a UI initiated by the running service. Please note that the service and the UI will obviously live in different sessions.

I was looking into the Windows Communication Foundation feature lists, and unfortunately did not find anything could help us to solve this dilemma. There is however an old and well understood technology still available and supported in Windows Vista. The talk is about Enterprise Services, or COM+ services or if you like the .NET brand ServicedComponents. Using an Out-of-Process COM+ Application can be configured to run in the security context of the interactively logged on user like the Figure 2 shows.

COM+ Application Identity

Figure 2 COM+ Application Identity

This looks like a highly preferable solution, as each time the NT Service will create a new UI Instance, it will automatically take the current interactively logged on user's identity. Technically, this means the system will start a surrogate process named like dllhost.exe running in the security context of that user. This UI process is not allowed to exit by the interactive user. Preferably it has to provide a notification icon in the system toolbar. The NT Service process can easily control the lifetime of the UI process, starting it while receiving OnStart(), closing it receiving OnStop() event, and periodically sending current updates while up-and-running.

There are few more problems to be resolved. How to handle user log-off/log-on events? Imagine an interactive user ends the session and a new user (who, according to the provided list, is also a watched one) logs on. Logging off the UI will exit whether the NT Service wants it to or not. Thus, the service has to be aware of the exiting UI. There is a windows system event WM_QUERYENDSESSION which is raised each time the user logs off. Unfortunately this event is intended for Windows Applications only and by default services are not aware of windows system events. Therefore, it seems like a good idea that the UI running as COM+ surrogate process has to have the ability to talk back to the NT Service at least in order to notify the NT Service about exiting and running states. For this reason, I decided to use Windows Communication Foundation (WCF). The NT Service can easily host a WCF Service providing a named pipe channel to the UI.

Considering the design of the two piece application (NT Service & UI surrogate) I found one more problem while testing fast user switching. Imagine the first logged user will not log off, instead she clicks the "Switch User" menu. This will preserve the desktop of the first user and create a new session for the second one. At this time however the already running COM+ UI application will not experience any end-session notification and will keep running. In these terms even if the NT Service creates a new COM+ UI instance, this will materialize in the context of the first user's and the second user will not get displayed any UI at all. This is due to the fact that, the surrogate COM+ process will not end and keep running, sticking to the identity of the first user. This marriage has to be ended using a soft force and the NT Service has to shut down the COM+ Application while detecting a fresh new user.

You can shut down any COM+ Application programmatically using the COM + 1.0 Admin Type Library (COMAdmin) - the

 ICOMAdminCatalog.ShutdwonApplication(BSTR 
appIDOrName)
method does this. The question now, is how to determine the exact need discontinuing the COM+ Application?

The NT Service can detect the identity of the interactively logged on user using WMI (System.Management namespace). If the NT Service detects a new user arrives while the previous user's UI is still alive, it will close the previous user's UI and subsequently will shut down the COM+ Application. This initializes the COM+ Application and the NT Service can start from the scratch creating a new UI which at this time will be created in the new (active) user's context.

To be honest, you could use the same technique also for detecting a normal logoff. The NT Service will periodically keep trying to detect logged on users. Therefore, confirming no one is logged on, the NT Service could simply exit all UI instances even without the ability of the UI proactively notifying the NT Service. I found, however, that controlling the UI without actively listening to it, is a simple but somewhat rigid solution. To enrich the user experience and to extend features (which is discussed later on) the UI has to be armed with the ability actively talk to the NT Service. This the issue WCF will be integrated for.

3. Inside the User Interface

Let's take a closer look at the COM+ Out of Process being an UI. Beyond the questions of design and development we also have to consider the deployment phase. Sometimes it is a good enough solution to use xcopy deployment relying on the OS to do all the right things behind the scenes in order to register and start the requested ServicedComponent. Although this often works well good for In-Proc Components, in our scenario it turns out, this strategy will desperately fail with an AccessDeniedException thrown while instantiating the UI. To remedy this, we have to tell to the COM+ Application who exactly is eligible and trusted to instantiate it. This will be discussed in the Deployment chapter later on.

3.1 Out of Process COM+ Component Design

Start either with a classic Class Library Project Template in VS2005 or with a Windows Application Project Template. I would prefer the latter which gives you the immediate opportunity to design the UI (Figure 3). At this time you do not have to care too much whether the component will be run as an In-Proc Server (Library Application) or Out-of-Proc (Server). This will be decided at deployment time. The UI is a very simple Windows Form. It displays the remaining Minutes and an adjusted ProgressBar. The UI also will tell the user whether there is a generally forbidden timeframe (red background) or the applications are free to use (green background). These settings are configurable in the NT Service configuration as described above.

Windows Form

Figure 3 Application Watcher Windows Form UI

If you decided to start with a Windows Application Template, rename the default Form1 class to something more feasible like ComPlusForm, rename the Form's file to ComPlusForm.cs and then go to the Program.cs (where the Application.Run() lives) and rename it to e.g. ComPlusProgram.cs. Then open up this file and create an Interface which will hold all the method signatures the NT Service can use to control the UI (Figure 4).

For a detailed explanation about the COM-Specific attributes read the CodeProject article "Exposing .NET Components to COM" from Nick Parker.

C#
...
[ComVisible(true)]
[Description("Exposed public interface")]
[Guid("FF112233-111-4444-A3F5-999BBBFFFDCF")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IStefanoMessage
{
    [DispId(1)]
    void ShowMain();

    [DispId(2)]
    void HideMain();

    [DispId(3)]
    void ExitMain();

    [DispId(4)]
    void PingUpdate(int minutesLeft, int maxValue, bool forbiddenTimeSpan);

    [DispId(5)]
    void ExitProcess(string processToExit);

    [DispId(6)]
    void MessageBoxShow(string text, string caption, string icon);

    [DispId(7)]
    void SetGuid(string guid);
}
...

Figure 4 Component Interface

I will briefly explain the purpose of these methods. The first marked with the DispID(1) attribute will simply lead to pop-up the Form on the desktop. I decided to relocate it into the right bottom corner of the desktop. The next method will do the opposite, and conceal the Form. The ExitMain() will let the UI to kill himself and exit. The most important method is the DispID(4) marked, which will be used to periodically send updates to the UI about available time limits. The ExitProcess will ask the UI to exit the prohibited process. This is a nice solution, because the mighty NT Service will just let the UI know when it is time to exit the prohibited process. The NT Service will not kill any processes at all, instead the dirty task is delegated exactly into the security context of those user, who started the application. Next, the DispId(6) will display a regular DialogBox and the last one will send a unique identity to the UI in order to identify it.

The next step is to implement such an interface. For this reason go back to your ComPlusProgram.cs file and create a new class which derives from the ServicedComponent and implements the previously defined IStefanoMessage interface. Note (Figure 5), that next to the implemented interface signatures, there is a constructor and a public void WorkerMain method attributed as [STAThread]. The constructor starts a thread which will instantiate the Form.

The Application.Run(myForm) will not return at all until the Form lives (which is the session's whole lifetime). Therefore, you cannot insert that piece of code into the constructor. The NT Service calling new StefanoDisplay() would never return. Also note the ManualResetEvent which will be signaled as soon the new instance of the Form is created. This can take several seconds depending on a number of factors.

Next, you are completely safe to delete the part of the Visual Studio generated code which starts with something like static class Program. Our component will not use it at all. A static class would contradict to the considered design, which requires exactly distinguished instances for all users.

C#
...
    [ComVisible(true)]
    [Description("ServicedComponenet class to control User's Interface")]
    [Guid("423C6000-2222-CCCC-2222-281CE6BA756D")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("WinUIComPlus.StefanoDisplay")]
    public class StefanoDisplay : ServicedComponent, IStefanoMessage
    {
        ComPlusForm myForm;
        ManualResetEvent manualEvent = new ManualResetEvent(false);
        public StefanoDisplay()
        {
            Thread thread = new Thread(new ThreadStart(WorkerMain));
            thread.Start();
            manualEvent.WaitOne();
        }

        [STAThread]
        public void WorkerMain()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            myForm = new ComPlusForm();
            manualEvent.Set(); 
            // Signale, that the Form Instance exists and can be used;
            Application.Run(myForm);
        }

        #region IStefanoMessage Members

        void IStefanoMessage.ShowMain()
        {
            if (myForm != null)
                myForm.ShowMe();
        }

        ... and so on ...

        #endregion
    }
...

Figure 5 COM Visible Class implements the Interface

The ComPlusForm Class implementation contains only some code (download the code and take a look). The form's main job is to periodically refresh the UI using a System.Windows.Forms.Timer. The NT Service sends updated data concerning elapsed times and forbidden time-slots to the UI via PingUpdate() (Figure 4) method. At this stage we do not care talking back to the server yet.

To finish the task, you have to go to the AssemblyInfo.cs file and insert a few more lines of declarative code. This will address essential COM+ features (Figure 6) like the name of the application, the COM+ Application's Guid, the access control attributes and a security role defined as "AverageUser" (this is an arbitrary name). The SecurityRole-Attribute will enforce a new Role for the COM+ Application during deployment (Figure 7). See further details in the next chapter. Please also note that it is highly recommended to generate the guids you see in all figures using the guidgen.exe utility which is out-of-the-box available as part of the VS2005 installation.

Optionally, you could change the project's Output Type from "Windows Application" to "Class Library". This will change the extension of the produced assembly to .dll which is more appropriate. After a couple of changes it seems makes little sense to have an .exe which cannot be started like a real executable at all.

C#
...
[assembly: ApplicationName ("WinUserInterface")] 
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationID("FF974E3B-E3E6-4441-B7ED-C5111567EF01")]
[assembly: ApplicationAccessControl(Authentication = 
    AuthenticationOption.Packet, 
    ImpersonationLevel =  
    ImpersonationLevelOption.Identify,
    AccessChecksLevel = AccessChecksLevelOption.ApplicationComponent)]
[assembly: SecurityRole("AverageUser")]
...

Figure 6 AssemblyInfo.cs of the UI-Assembly

AverageUser Role

Figure 7 Server-Application's security tab

3.2 COM+ Component Deployment

Essentially there are three ways of ServicedComponent deployment. Whatever you choose, your assembly has to be strong named, for which you are forced to use a signature (e.g. a key file containing the public and private key pairs).

  1. Using the RegSvcs.exe command line utility
  2. Automatically using the .NET Framework automatic registration capabilities, named also as lazy registration
  3. Programmatically, using the System.EnterpriseServices.RegistrationHelper class

It turns out, however, that even if it seems to be like a lot of work, sometimes you will need additional adjustments of the deployed application.

  • Loads and registers an assembly.
  • Generates, registers, and installs a type library into a specified COM+ 1.0 application.
  • Configures services that you have added programmatically to your class.

Registering an assembly requires a fully trusted environment and installation will need elevated privileges. Let's talk about the what issues registration cannot tackle. It cannot add accounts to the created Role (remember the AverageUser - Role, Figure 7). Start the dcomcnfg.exe utility to display and interactively configure Component Services. You would right click on the Users folder (Figure 8) beneath the AverageUsers selecting "New User" and subsequently adding the particular user or security group to this Role. Adding the accounts to the Role however is not enough. The application could contain more than one component which has to be reconfigured in order to grant access to the users in the Role. For this reason the Check-Box in the Security tab shown in Figure 7 has to be checked, which by default after pure registration remains unchecked.

Enterprise Services Roles

Figure 8 Component Services Roles

To do all these things, you have a mighty friend in the face of the COM + 1.0 Admin Type Library. Let's go to your Windows Application Project and right clicking on References, choose the COM pane and search for the COM + 1.0 Admin Type Library. This will add COMAdmin to your References. Prior to unfolding the power of COMAdmin, you have to add an Installer to your current project.

Right click on your project in the solution explorer and choose "Add/New Item..." and select "Installer Class" from the list of available items. Rename the new file e.g. to ComPlusInstaller.cs. Open up the new file and rename the default class e.g. to ComPlusInstaller which inherits from the Installer class.

Next you have to change to the designer view and in the properties tab click on Events. This will show you all available event handler the Installer can implement. We need at least two of them AfterInstall and BeforeUninstall. In Figure 9 is the AfterInstall displayed which you have to read as follows:

  1. First using RegistrationHelper the component will be registered in a perfectly predictable and normal way
  2. The InstallAssembly method is void and will rather throw an exception if something goes wrong
  3. Using System.Activator an instance of the COMAdmin is created (see cac variable)
  4. The COMAdmin returns the collection of all Applications (see cacc variable)
  5. In the collection of Applications we have to find our own application (see strAppID) just registered a couple of steps ago and also the Roles collection belonging to this application (see caccRoles variable)
  6. Next looking for the particular role AverageUser in the returned Role collection
  7. If the role has been found, we need to identify the UsersInRole (see caccUsers variable) beneath the AverageUser role (see Figure 8)
  8. Now the NT AUTHORITY\LOCAL SYSTEM or simply "SYSTEM" account is added to the Users. Remember, this account will be used to run the NT Service. Changes will be saved. If you will run some test-application (e.g. a Console) against this COM+ Application, you will have to add also your interactive user account to the Role.
  9. Next the component identified by the Class-ID guid (see cacoComponent variable) is found in the collection of all components in order to grant access to the users in Role
  10. Lastly, the changes are persisted
C#
...
private void ComPlusInstaller_AfterInstall(object sender, InstallEventArgs e)
{
    if (!EventLog.SourceExists(comPlusEventSource))
    {
        EventLog.CreateEventSource(comPlusEventSource, "StefanoLog");
    }
    string assembly = GetType().Assembly.Location;
    string applicatioName = null;
    string typelibName = null;
    
    // Register first the component
    RegistrationHelper regHelper = new RegistrationHelper();
    try
    {
        regHelper.InstallAssembly(assembly,
                        ref applicatioName,
                        ref typelibName,
                        InstallationFlags.FindOrCreateTargetApplication);

        // If you want to debug simply uncomment the next line
        // System.Diagnostics.Debugger.Break(); 

        ICOMAdminCatalog cac = 
            (ICOMAdminCatalog)Activator.CreateInstance(Type.GetTypeFromProgID(
                "COMAdmin.COMAdminCatalog"));
        COMAdminCatalogCollection cacc = 
            (COMAdminCatalogCollection)cac.GetCollection("Applications");
        
        COMAdminCatalogCollection caccRoles =
             (COMAdminCatalogCollection)cacc.GetCollection("Roles", 
              strAppID);
        caccRoles.Populate();

        // Find Role
    COMAdminCatalogObject cacoRole = null;
    bool roleFound = false;
    foreach (COMAdminCatalogObject objectRole in caccRoles)
    {
        if (objectRole.Key.ToString().ToUpper() == strAppRole.ToUpper())
        {
            roleFound = true;
            cacoRole = objectRole;
            break;
        }
    }
        // Role Found?
        if (roleFound == true && cacoRole != null)
        {
            // Asign accounts to the found role
            COMAdminCatalogCollection caccUsers =
                 (COMAdminCatalogCollection)caccRoles.GetCollection(
                     "UsersInRole", cacoRole.Key);
            caccUsers.Populate();
            COMAdminCatalogObject cacoUser = null;
            cacoUser = (COMAdminCatalogObject)caccUsers.Add();
            cacoUser.set_Value("User", "SYSTEM");
            caccUsers.SaveChanges();

            // Reconfigure component to grant access to users in role.
            // *************************************************************
            COMAdminCatalogCollection caccComponents =
                 (COMAdminCatalogCollection)cacc.GetCollection("Components", 
                 strAppID);
            caccComponents.Populate();

            bool componenetFound = false;
            foreach (COMAdminCatalogObject cacoComponent in caccComponents)
            {
                if (cacoComponent.Key.ToString().ToUpper() == strCLSID)
                {
                    componenetFound = true;
                    break;
                }
            }

            if (componenetFound == true)
            {
                COMAdminCatalogCollection caccRolesForComponent =
                     (COMAdminCatalogCollection)caccComponents.GetCollection(
                          "RolesForComponent", strCLSID);
                COMAdminCatalogObject cacoRoleForComponent =
                     (COMAdminCatalogObject)caccRolesForComponent.Add();
               cacoRoleForComponent.set_Value("Name",
                   cacoRole.Name);
               caccRolesForComponent.SaveChanges();
            }
        }
    }
    catch (Exception)
    {
        throw;
    }
}
...

Figure 9 Installing the Serviced-Component

4. The Windows NT Service

The other half of the solution is the NT Service. Go to the solution which at this time contains only one single Windows Application project, and add a new project, selecting the Windows Service template. Rename the files e.g. to WatcherService.cs. Let's take a closer look at the WatcherService.cs which by this time contains the OnStop() and OnStart() event handlers. Essentially what you have to do is to think about what exactly the service is intended to do. In this particular case, the main business of the application watcher service is scrutinizing periodically all running processes and counting the elapsed time the watched processes have spent running in a context of the watched user. The word "periodically" is stressed. For this reason we have to start a dedicated thread, which will pause and resume continuously during the whole lifetime of the NT Service. Stopping the service reduces therefore to the task of stopping such a periodically activated thread.

4.1 NT Service Structure

There are two different possibilities to fulfill such a task. The first is the classic approach starting a dedicated thread which will do the job and after finishing a complete run falling asleep and awakening according to the predefined sleep-time (Figure 10). The second approach uses a server based timer e.g. the System.Windows.Forms.Timer which periodically restarts a worker thread (Figure 11).

I have decided to use the timer-based approach, which has the advantage of being more accurate in counting the elapsed time. If you schedule the timer to fire once a minute, this will be done exactly every 60 seconds of elapsed time. Using the Worker-Thread approach the frequency of the awakenings are not exact, and are defined as the sum of sleeping-time and thread's execution-time. The disadvantage of the timer-based approach is the danger of thread-overlapping. This can happen if for some reason the currently running timer is not yet finished while the next is being starting. This has to be prevented creating a boolean variable stillExecuting set to true while the thread is running (Figure 11). I found such situations happen while booting the computer and logging on the first time.

Note the "Do actual job here" comments in the Figures. This is the place where our ServicedComponent UI will be instantiated and the interface-methods will be called. On slow laptop computers sometimes it could takes minutes to establish the communication between NT Service and the UI Component.

C#
...
protected override void OnStart(string[] args)
{
    // Load settings
    InitializeOnStart();
    // Start a separate thread that does the actual work.
    if ((workerThread == null) ||
        ((workerThread.ThreadState & (System.Threading.ThreadState.Unstarted | 
        System.Threading.ThreadState.Stopped)) != 0))
        {
            serviceLog.WriteEntry("Starting the service worker thread.",  
                EventLogEntryType.Information, 10);
            workerThread = new Thread(new ThreadStart(ServiceWorkerMethod));
            goLoop = true;
            workerThread.Start();
        }
        if (workerThread != null)
        {
            serviceLog.WriteEntry("Worker thread state = " + 
                workerThread.ThreadState.ToString(),
                EventLogEntryType.Information, 11);
        }
}

protected override void OnStop()
{
    this.RequestAdditionalTime(5000);
    // Signal the worker thread to exit.
    if ((workerThread != null) && (workerThread.IsAlive))
    {
        serviceLog.WriteEntry("Stopping the service worker thread.", 
            EventLogEntryType.Information, 2);
        goLoop = false;
        Thread.Sleep(5000);
    }
    if (workerThread != null)
    {
        workerThread.Abort();
        judiLog.WriteEntry("OnStop Worker thread state = " + 
            workerThread.ThreadState.ToString(),
            EventLogEntryType.Information, 1);
    }
    // Save all the settings
    SaveSettings();
    // Indicate succesfull exit
    this.ExitCode = 0;
}

public void ServiceWorkerMethod()
{
    serviceLog.WriteEntry("The service worker thread has been succesfully 
        started.");
    try
    {
        do
        {
            // Wait defined time-delay (1 minute) each time
            Thread.Sleep(timeInterval);

            // Do the actual job now and here
                    ....
        }
        while (goLoop); // This becomes false stopping the service
    }
    catch (ThreadAbortException)
    {
        // Another thread has signalled that this worker
        // thread must terminate.  Typically, this occurs when
        // the main service thread receives a service stop
        // command.
        serviceLog.WriteEntry("Worker-Thread aborted while stopping!", 
            EventLogEntryType.Information, 3);
    }
    serviceLog.WriteEntry("Exiting the service worker thread as the 
        Stop-Event is signaled.", EventLogEntryType.Information, 4);
}
...

Figure 10 Worker-Thread driven NT Service Skeleton

C#
...
protected override void OnStart(string[] args)
{
    // Load settings
    InitializeOnStart();
    // Start a separate timer that does the actual work.
    serviceLog.WriteEntry("Starting the service worker thread.",
        EventLogEntryType.Information, 10);
    theWorkerTimer = new System.Timers.Timer();
    theWorkerTimer.Elapsed += new ElapsedEventHandler(ServiceTimerTick);
    theWorkerTimer.Interval = timeInterval;
    theWorkerTimer.Enabled = true;
    GC.KeepAlive(theWorkerTimer);
    serviceLog.WriteEntry("Started the service worker thread.",
        EventLogEntryType.Information, 12);            
}

protected override void OnStop()
{
    this.RequestAdditionalTime(5000);
    serviceLog.WriteEntry("OnStop stopping the Timer",
        EventLogEntryType.Information, 2);
    theWorkerTimer.Stop();
    theWorkerTimer.Dispose();
    serviceLog.WriteEntry("OnStop Timer has been disposed",
        EventLogEntryType.Information, 1);  
    // Save all the settings
    SaveSettings();  
    // Indicate succesfull exit   
    this.ExitCode = 0;
}
        
public void ServiceTimerTick(Object sender, ElapsedEventArgs e)
{
    if (stillRunning == true)
    {
        serviceLog.WriteEntry("Previous intance of ServiceTimerTick not 
            yet finished!", EventLogEntryType.Information, 113);
        return; // previous call not yet finshed
    }
      
    try
    {
        // Do the actual job now and here
        ....
    }
    catch (ThreadAbortException)
    {
        // Another thread has signalled that this worker
        // thread must terminate.  Typically, this occurs when
        // the main service thread receives a service stop command.
        serviceLog.WriteEntry("Worker-Thread aborted while 
            stopping!", EventLogEntryType.Information,3);
     }
     finally
     {
         stillRunning = false;
     }
}
...

Figure 11 Timer driven NT Service Skeleton

Obviously the next step while proceeding with the Windows Service project is to add a project reference to the Windows Application in terms of consuming the Interface declared in that assembly (WinUIComPlus).

Furthermore, let's create an additional class WatchedUser, which represents a particular watched user like Figure 12 shows. Note the methods CreateUserInterface() and DeleteUserInterface(). The constructor takes the name of the watched user and the current counter of elapsed minutes. For brevity the list of all properties is discarded.

C#
public class WatchedUser 
{

    public WatchedUser(string userName, int userCounter)
    {
        counter = userCounter;
        alive = false;
        name = userName;
        guid = System.Guid.NewGuid().ToString();
        myUI = null;
        exiting = false;
    }

    #region Properties
    private WinUIComPlus.IStefanoMessage myUI;
    public WinUIComPlus.IStefanoMessage ComPlusObject
    {
        get { return myUI; }
    }
    private int counter;
    ...
        
    #endregion

    public WinUIComPlus.IStefanoMessage CreateUserInterface()
    {        
        // this will instantiate the Form 
        myUI = new WinUIComPlus.StefanoDisplay(); 
        if (myUI != null)
        { 
            myUI.SetGuid(guid);
            return myUI;
        } 
        return null;
    }

    public void DeleteUserInterface()
    {
        alive = false;
        try
        {
            if (myUI != null)
                Marshal.ReleaseComObject(myUI);
            catch (Exception)
            {
                // just proceed, we are exiting anyway;
            }
            finally
            {
                myUI = null;
            }
        }
    }
}

Figure 12 The WatchedUser class

Let's summarize what exactly the service has to do in the section marked as "// Do the actual job now and here" (Figure 11).

  1. The service will first of all try to detect who is logged on and if anybody is out of there, the service will try to identify the user as a watched user. For this purpose we will use WMI via System.Management namespace classes querying win32_computersystem (Figure 13). The IsUserLoggedOn() method will return true if the logged on user is also a watched one and additionally returns the domain name and the exact username.
  2. Next the service will look whether the identified logged on user has already an existing and alive UI instantiated. If this does not not apply, the service's thread will proceed with the next step at No.3 otherwise will take step No.5.
  3. The service will check for all alive UI instances created for all watched users. If the service detects an alive UI not belonging to the currently logged on interactive user, then the current user is definitely recognized as a new user. All other alive UI instances have to be shut down. This strategy cures the limitation of COM+ Services during fast user switching. If e.g. user X1 do not log off and a new user X2 logs in, COM+ Out-of-process surrogates will keep running for a couple of minutes in the context of the original user X1. Any attempts to create a new WinUIComPlus.StefanoDisplay() instance, will end with a new instance in the session of the user X1. Therefore, the NT Service has to make sure, the UI of user X1 is closed and the COM+ Application is shut down in order to handle user X2.
  4. The service now can start creating a new UI calling WatchedUser.CreateUserInterface() (Figure 12).
  5. The NT Service will now create a list of watched processes being run and filter the users accessing to the security tokens of the processes. This is the highly privileged operation for which reason the NT Service has to run as LOCAL SYSTEM. The code essentially consists of pure P/Invoke calls and is not shown here in the article. However, if you download the code, look for the
    private bool 
        LookupUserAccount(Process process, out string domain, out string user)
    method, which will return true if the operation succeeds and also returns the exact username and domain got from the security token of the prcocess.
  6. Lastly the NT Service has to send a status update to the UI instance. This message will contain data about the current counter and effective forbidden time-spans. The status update is executed via IStefanoDisplay.PingUpdate() (Figure 4).
C#
private bool IsUserLoggedOn(out string userDomain, out string userName)
{
    userDomain = "";
    userName = "";

    ObjectQuery oQuery = new ObjectQuery("select * from 
        win32_computersystem");
    //Execute the query  
    ManagementObjectSearcher oSearcher = new 
        ManagementObjectSearcher(oQuery);
    //Get the result
    ManagementObjectCollection oReturnCollection = oSearcher.Get();
    if (oReturnCollection.Count >= 1)
    {
        foreach (ManagementObject oReturn in oReturnCollection)
        {
            string name = oReturn["UserName"].ToString();
            if (name.Length > 0)
            {
                int index = name.IndexOf('\\');
                userDomain = name.Substring(0, index).ToLower();
                userName = name.Substring(index + 1).ToLower();

                // Look whether currently logged on user is on the watched 
                // users list
                foreach (string usr in watchedUserList)
                {
                    if (usr.ToLower() == userName)
                    {
                        return true;
                    }
                }
            }
        } 
    }
    return false;
}

Figure 13 Using WMI query to detect logged on users

Completing this chapter adds a configuration file to the Windows Service project (Add New Item /Application Configuration File). The contents you can take from Figure 1.

Furthermore remember about the need to save counted data while the service is shutting down. This data will be read at service start time and the initial values initialized. The application configuration file created above is only appropriate for read-only data. To preserve dynamically changing settings open the Windows Service Project properties (right mouse click on the project) and go to the settings pane. Click on the displayed "This project does not contain a default settings file..." link which will create a new Settings.settings entry beneath the Project Properties (see the solution explorer) and will also open-up a settings table-view. The table contains yet nothing but a single empty line. Rename the default Settings in the column Name to something like "timestamp" and choose the System.DateTime data type. Leave the value empty. This will preserve the exact date-time of the service stop event. Next add one more settings line named like stringCollection and select the type System.Collections.Specialized.StringCollection. This property is intended to preserve the counters (one item for each watched user). For implementation details take the source code.

4.2 NT Service Deployment

You can deploy the NT Service via the installutil.exe utility available in the .NET Framework redistributable package. However, in order to do so, you have to add an Installer into the Windows Service Project like we did that in the ServicedComponenet (Windows Application) Project. The difference is, that in this case the Installer will be consumed by the installutil.exe and in the previous case by the RegSvcs.exe utility.

Go and double-click now in the solution explorer on the WatcherService.cs which will activate the designer view. Then navigate into the designer-view (which is grayed containing a text beginning like: "To add components to your class....". Right mouse click in this area and select "Add Installer". This will immediately add a ProjectInstaller.cs into your project and Visual Studio 2005 IDE will activate this class in the designer mode view containing two new control-like instances named serviceInstaller1 and serviceProcessInstaller1. What you have to know about these instances is the fact, that the ServicePocessInstaller class represents the NT Service Process itself and has to be unique. The ServiceInstaller class represents the currently developing service (not the whole process). The NT Service Process can contain more than one service hosted in one single process. So theoretically you could have more than one ServiceInstaller class instances in your project, which is however not the case in this project.

Take a look at the ProjectInstaller's constructor (Figure 14). Note that it is set explicitly, that the process has to run in the context of the local system. The service process is set to start automatically. The service's display name is also set next along with the description you will see in the opened services management console.

C#
public ProjectInstaller()
{
    InitializeComponent();
            
    // The services run under the system account.
    processInstaller.Account = ServiceAccount.LocalSystem;

    // The service is started automatically.
    serviceInstaller.StartType = ServiceStartMode.Automatic;

    // ServiceName must equal those on ServiceBase derived classes.            
    serviceInstaller.ServiceName = "ApplProcessWatcher";
    serviceInstaller.Description = "This service is intended to watch 
        applications according configuration details";
}

Figure 14 NT Service ProjectInstaller

Now let's handle the events raised during service installation. Essentially you could do that in a three different ways. Per ServiceInstaller instance, per ServicePocessInstaller instance and also per the ProjectInstaller. The ProjectInstaller inherits form the System.Configuration.Install.Installer class and the preferred way is to use this class. You have to switch to the code-view of the ProjectInstaller and override at least the OnAfterInstall() and OnBeforeUninstall() methods. The protected override void OnBeforeUninstall(...) is implemented only with a single line of code calling the base.OnBeforeUninstall(...) Method. At this point there is nothing more you would need to worry about.

Somewhat more challenging is the implementation of the overridden OnAfterInstall() Method (Figure 15). The first you have to note is the call of the base.OnAfterInstall(...) method and next some message written into the Application Event-Log. The confusing stuff which follows this last line of well-known code consists of a series of P/Invoke calls into the advapi32.dll. Unfortunately in the System.Configuration.Install namespace there is no support yet for changing the default service configuration settings. Why do we need such change? While starting the service, especially during computer boot-time, there is by default no way to determine the services' start order. This could be a painful experience as the WatcherService could start earlier as some vital services do (e.g. the COM+ System or the RPC Services). In order to prevent crashes caused by wrong service start order, we have to define a service dependency e.g. on the COM+ Event System (see EventSystem). The EventSystem itself has dependency on the RPC Service which therefore looks like an appropriate decision. In terms of fixing the service dependency, the following operations have to be done:

  1. Open the Service Control Manager's Database
  2. Lock the SCM's Database for the time we put the described above dependency in it
  3. Open the particular service using the service's name
  4. Change the service's configuration (while leaving all other configuration details unchanged!)
  5. Unlock the SCM's database and close the service's handle
C#
protected override void OnAfterInstall(IDictionary savedState)
{
    base.OnAfterInstall(savedState);

    // Add steps to be done after the installation is over.
    EventLog.WriteEntry(projectInstallerSource, "OnAfterInstall 
        called");

    IntPtr databaseHandle = OpenSCManager(null, null,
        (uint)(SERVICE_ACCESS.SERVICE_QUERY_CONFIG |
        SERVICE_ACCESS.SERVICE_CHANGE_CONFIG |
        SERVICE_ACCESS.SERVICE_QUERY_STATUS |
        SERVICE_ACCESS.STANDARD_RIGHTS_REQUIRED |
        SERVICE_ACCESS.SERVICE_ENUMERATE_DEPENDENTS));
    if (databaseHandle == IntPtr.Zero)
        throw new System.Runtime.InteropServices.ExternalException(
            "Open Service Manager Error");

    IntPtr serviceDbLock = LockServiceDatabase(databaseHandle);
        if (IntPtr.Zero == serviceDbLock)
        {
            int nError = Marshal.GetLastWin32Error();
            Win32Exception win32Exception = new Win32Exception(nError);
            throw new System.Runtime.InteropServices.ExternalException(
                "Failed to lock the Service Control Manager: " + 
                win32Exception.Message);
        }

    IntPtr serviceHandle = OpenService(databaseHandle,
        serviceInstaller.ServiceName,
        SERVICE_ACCESS.SERVICE_QUERY_CONFIG | 
        SERVICE_ACCESS.SERVICE_CHANGE_CONFIG);
    if (serviceHandle == IntPtr.Zero)
        throw new System.Runtime.InteropServices.ExternalException(
            "Open Service Error");

    if (!ChangeServiceConfig(serviceHandle,
        SERVICE_NO_CHANGE,
        SERVICE_NO_CHANGE,
        SERVICE_NO_CHANGE,
        null,
        null,
        IntPtr.Zero,
        "EventSystem",
        null,
        null,
        null))
    {
        int nError = Marshal.GetLastWin32Error();
        Win32Exception win32Exception = new Win32Exception(nError);
        throw new System.Runtime.InteropServices.ExternalException(
            "Could not change to interactive process : " + 
            win32Exception.Message);
    }

    EventLog.WriteEntry(projectInstallerSource,
        "OnAfterInstall succeeded do change service dependency");

    UnlockServiceDatabase(serviceDbLock);
    CloseServiceHandle(serviceHandle);
}

Figure 15 Altering service configuration in the SCM database

Please take the specified P/Invoke declarations from the code you can download and scrutinize. At this point you can build the solution and deploy manually both the COM+ Component (via RegSvcs.exe utility) and also the NT Service (via installutil.exe).

5. UI talking back to the NT Service

As previously mentioned, the ability of the UI to communicate with the NT Service without being asked is an impressive capability. This is especially useful for two reasons:

  • Periodically notifying the NT Service UI is alive. As the UI component has a timer implemented, each time the tick event is raised, the UI could sent back a "Hello I'm alive and doing well" message.
  • While the user logs off, the UI running in the context of the interactively logged on user, will be shut down by system. The NT Service of course has the instrumentation to check periodically the logged on users and appropriately react in case of any change, however it seems to be a more suitable to have an immediate coupling to the UI-Exit event.
  • Thirdly (which is not yet implemented) using the UI the logged on user could request additional or extended allowance to use forbidden processes while the daily limit expires. This could be handled by clicking e.g. on a Button "Request more" on the UI's surface and typing in the username and password of the dedicated user who is in the role of a supervisor.

5.1 Hosting WCF Services in the NT Service

Let's go back to the Windows Service Project (WatcherService). The door to utilize WCF-Services leads trough two additional library references, which are the System.ServiceModel.dll and the System.Runtime.Serialization.dll (both of them part of the .NET Framework 3.0 Version). Next if you have installed the "Visual Studio 2005 Extensions for .NET Framework 3.0 (WCF & WPF), November 2006 CTP" add to the project simply a "New Item/ WCF Service". If you have not installed the aforementioned extension, do not worry and add a new Class instead (note: the .NET Framework 3.0 redistributable installed is required, except Windows Vista which OS has already out-of-the-box this library on board).

Whether you added a new WCF Service or simply a Class, you have to end with a public interface (e.g. IWCFServiceClass) attributed using the ServiceContractAttribute class. The interface has two method signatures declared as bool Exiting() and bool Alive(). Note that the methods are receiving a string parameter, which is the UI's identity (this is generated and sent by the NT Service to the UI). Furthermore you will need a public class (e.g. WCFServiceClass) which implements this interface (Figure 16). Also note, the declared methods signatures are attributed with the OperationContractAttribute class. That's it. The further implementation details have absolutely nothing to do with WCF, and are dealing with completely innocent stuff like iterating through the list of all watched users' objects and looking for the specific key (guid), then setting some properties (Existing, Alive) to true and calling a method which will actively delete/release the instantiated UI.

C#
[ServiceContract]
public interface IWCFServiceClass
{
    [OperationContract]
    bool Exiting(string id);

    [OperationContract]
    bool Alive(string id);
}

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,
    ConcurrencyMode=ConcurrencyMode.Multiple)]
public class WCFServiceClass : IWCFServiceClass
{
    #region IWCFServiceClass Members
    
    bool IWCFServiceClass.Exiting(string id)
    {
        bool retcode = false;
        foreach (string key in WatcherService.usersObjects.Keys)
        {
            if (WatcherService.usersObjects[key].Guid == id)
            {
                WatcherService.usersObjects[key].Exiting = true;
                WatcherService.usersObjects[key].DeleteUserInterface();
                retcode = true;
                break;
            }
        }
        return retcode;
    }

    bool IWCFServiceClass.Alive(string id)
    {
        bool retcode = false;
        foreach (string key in WatcherService.usersObjects.Keys)
        {
            if (WatcherService.usersObjects[key].Guid == id)
            {
                WatcherService.usersObjects[key].Alive = true;
                retcode = true;
                break;
            }
        }
        return retcode;
    }

    #endregion
}

Figure 16 Service Contract details

Completing the Service-Contract which of course will be used by the UI Component, we have to do two more steps in order to succeed. The first is to extend the application configuration file with the stuff related to the WCF (Figure 17) and the second is to open the WCF Service Host while starting the NT Service (Figure 18) and closing it while stopping the NT Service.

If you are new in WCF you probably do not mind an explanation of the WCF related stuff in a little bit more detail. Note in the configuration file the baseAddress attribute inside the <baseAddress> element. This instructs the WCF Service Host to listen via http to service metadata requests. Bear in mind this http port does not represent the working channel intended for the UI to talk to the NT Service. This is rather for servicing design-time requests for the WSDL (WCF Service Contract) and after the design has completed, it is even safe to discard it.

Next look at the <endpoint> element, which defines the ABC of the WCF: Address, Binding and Contract. The binding uses named pipes between the processes. The address attribute also reflects named pipe usage, however you are perfectly safe to rename the value of the address while leaving the first part "net.pipe://localhost/" unchanged. The contract attribute's exact value has to be match the interface definition (Figure 16).

<system.serviceModel>
    <services>
        <service name="WindowsNTService.WCFServiceClass"
            behaviorConfiguration="NTBehavior">
            <host>

                <baseAddresses>
                    <add baseAddress="http://localhost:9002/
                        NTWatcherService/"/> 
                </baseAddresses> 
            </host>
            <endpoint name="WindowsNTService"
                address="net.pipe://localhost/NTWatcherService/pipe"

                binding="netNamedPipeBinding"
                bindingConfiguration="NTPipeConfiguration"
                contract="WindowsNTService.IWCFServiceClass"/>
        </service> 
    </services>

    <bindings>
        <netNamedPipeBinding>
            <binding name="NTPipeConfiguration"></binding>
        </netNamedPipeBinding>
    </bindings>

    <behaviors>
        <serviceBehaviors>
            <behavior name="NTBehavior">
                <serviceMetadata httpGetEnabled="true" />
            </behavior>

        </serviceBehaviors>
    </behaviors>
</system.serviceModel>

Figure 17 WCF Configuration for hosting in the NT Service

C#
protected override void OnStart(string[] args)
{
    // Load settings
    InitializeOnStart();
            
    // Start service host
    // **************************************************
    serviceHost = new ServiceHost(typeof(WindowsNTService.WCFServiceClass));
    eventLog.WriteEntry("Created the ServiceHost of type 
       WindowsNTService.WCFServiceClass.",
       EventLogEntryType.Information, 13);
    if (serviceHost != null)
        serviceHost.Open();
        eventLog.WriteEntry("Opened the ServiceHost of type 
            WindowsNTService.WCFServiceClass.",
            EventLogEntryType.Information, 14);

    // Start a separate timer that does the actual work.
    // ***************************************************
    eventLog.WriteEntry("Starting the service worker thread.",
        EventLogEntryType.Information, 10);
    theWorkerTimer = new System.Timers.Timer();
    theWorkerTimer.Elapsed += new ElapsedEventHandler(ServiceTimerTick);
    theWorkerTimer.Interval = timeInterval;
    theWorkerTimer.Enabled = true;
    GC.KeepAlive(theWorkerTimer);
    eventLog.WriteEntry("Started the service worker thread.",
        EventLogEntryType.Information, 12);           
}
         
protected override void OnStop()
{
    this.RequestAdditionalTime(5000);

    eventLog.WriteEntry("OnStop stopping the Timer",
        EventLogEntryType.Information, 2);
    theWorkerTimer.Stop();
    theWorkerTimer.Dispose();
    eventLog.WriteEntry("OnStop Timer has been disposed",
        EventLogEntryType.Information, 1);

    // Start saving current counters state
    #region SAVE SETTINGS
    // Finished saving current state

    // Shut down the WCF Service Host
    if (serviceHost != null)
        serviceHost.Close();

    this.ExitCode = 0;
}

Figure 18 Extended with WCF hosting OnStart and OnStop

5.2 Consuming WCF Services in the UI

Now the NT Service is ready to start. Unfortunately to get the WCF metadata for the UI Component, the NT Service has to be up-and-running and listening to the configured 9002 http port. For this reason you have to install and start the NT Service using the installutil.exe utility like mentioned earlier. Do not forget prior to starting to comment out all the lines of code in the NT Service, which are dealing with the instantiated UI-Component! This is because the UI is not yet completed.

If you succeeded to install and start the NT Service, open up your Internet Explorer and type the baseAddress into the address list, which is "http://localhost:9002/NTWatcherService/". This is in accordance with the configuration file discussed in the previous chapter (Figure 17). If you got the right response (Figure 19), the WCF Service Host is alive you can proceed. Go next to the Windows Application project. In the Solution Explorer right click the project's References and select "Add Service Reference...". Please note do not confuse this with "Add Reference..." and also leave untouched the "Add Web Reference..."! Feed the dialog box which pops up with the very same address like in the Internet Explorer. This will add a Service Reference by default named like "localhost.map" to your project. Also be aware of the fact that also a brand new application configuration file is created which contains the Service Reference's configuration details.

Internet Explorer consuming WCF

Figure 19 Consuming WCF-Metadata in Internet Explorer

It turns out that an application configuration file in the Windows Application Project causes an unwelcomed problem. Wonder why? Remember, the project containing the UI is not a simple .NET Executable. The project's assembly will live rather as a registered COM+ Application (dllhost.exe). Therefore the application configuration file by default will not apply at all - that's it. The best way to remedy is completely abandoning the configuration file and instead importing the declarative WCF settings into the applications code.

Take a look at Figure 20 which shows a piece of code after discard application configuration file. Just remove it from the project. In the FormLoad event handler a new NetNamedPipeBinding and EndpointAddress classes have to be instantiated. Next instantiate the WCFServiceClassClient using the binding and address instances as input parameters. Please note the difference. Using the originally created application configuration you could simply instantiate this class via the default constructor without the binding and address parameters. The .NET Framework would look for the information needed directly into the configuration file.

C#
private void Form1_Load(object sender, EventArgs e)
{
    SetFormPostion();
    this.notifyIcon1.Visible = true;
    this.Opacity = 0;
           
    exactClose = false;
    timer1.Interval = 2000;

    NetNamedPipeBinding myBinding = new NetNamedPipeBinding();
    string netPipe = "net.pipe://localhost/NTWatcherService/pipe";
    EndpointAddress myEndpoint = new EndpointAddress(netPipe);
    server = new WCFServiceClassClient(myBinding, myEndpoint);
    if (OpenWCFServer() == true)
        timer1.Start();
}

private bool OpenWCFServer()
{
    bool retcode = false;
    if (server != null)
    {
        try
        {
            server.Open();
            retcode = true;
        }
        catch (TimeoutException ex)
        {
            server.Abort();
            server = null;
            EventLog.WriteEntry(comPlusEventSource, ex.Message, 
                EventLogEntryType.Error);
        }
        catch (CommunicationException ex)
        {
            server.Abort();
            server = null;
            EventLog.WriteEntry(comPlusEventSource, ex.Message, 
                EventLogEntryType.Error);
        }
    }
    return retcode;
}

Figure 20 Making the initial handshake with the WCF Server

The only remaining task to be complete is to make sure that:

  • The timer will call server.Alive() in order to notify the NT Service being alive
  • Exiting the application the UI must call server.Exiting() method in the FormClosing event handler.

The FormClosing event handler is a little bit tricky. This, in terms of requirements, should not allow the user to close and exit the application. The application can only exit if the NT Service asks or the user is logging off. The first is easy to handle, as the server has to explicitly call the ExitMain() method (Figure 4) which can set a boolean indicator like exactClose to true. But how do you catch a use logging off?

To handle system events, you have to override the System.Windows.Form.Control.WndProc() method which is a way for hooking windows system events. Next look for the WM_QUERYENDSESSION message which occurs exactly while the user decides to log off.

C#
private const int WM_QUERYENDSESSION = 0x11;

protected override void WndProc(ref System.Windows.Forms.Message m)
{
    if (m.Msg == WM_QUERYENDSESSION)
    {
        exactClose = true;
        EventLog.WriteEntry(comPlusEventSource,
            "WM_QUERYENDSESSION: this is a logoff, 
            shutdown, or reboot");
    }
    base.WndProc(ref m);
} 
        
private void SendExitMessageToServer()
{
    if (server != null)
    {
        try
        {
            bool b = server.Exiting(guid);
            EventLog.WriteEntry(comPlusEventSource, "server.Exiting() 
                called; result: " + b.ToString());
            server.Close();
            EventLog.WriteEntry(comPlusEventSource, "server.Close() 
                called");
            server = null;
        }
        catch (CommunicationException ex)
        {
            EventLog.WriteEntry(comPlusEventSource, ex.ToString(), 
                EventLogEntryType.Error);
            server.Abort();
            server = null;
        }
    }
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (exactClose == true)
        SendExitMessageToServer();
        else
            this.Hide();

            EventLog.WriteEntry(comPlusEventSource, "Form1_FormClosing: 
                !" + exactClose.ToString());
            e.Cancel = !exactClose;
}

Figure 21 Hooking system events in Windows Forms and closing the Form

6. Deployment and something more

The last recommended thing to do is to wrap up all produced assemblies into a single windows installer package. You have to go to your solution and add a third project which is of type "Other Project Types/ Setup and Deployment /Setup Project". The details regarding this type of project please take directly from the code which you can download and scrutinize. The Visual Studio 2005 created MSI Package hast to be deployed using elevated privileges. Use the setup.exe which you can start in Vista right clicking on it an selecting "Run as Administrator".

The demo is a tested application and runs fine on many of my computers at home. You can download and use it however without any warranty. If you downloaded and successfully installed the application (leave all the settings in defaults) in order to use it, you have to remember and few more things:

  • Install the application using the default "Just for Me" option in your administrative account (who is presumably the supervisor).
  • Navigate to the %programfiles% folder and in the "...\Stefano\ApplicationWatcher\NTService" change the default configuration settings (WatcherService.exe.config - see Figure 1) according to your home-policy.
  • After you changed the configuration, start the service manually. Thereafter the service will start automatically each boot-time.
  • Log off and let your kids use the box. Please be aware there is absolutely no need to let them run an administrative account while surfing in the WEB - this is even more true when running Windows Vista.
  • Do not solely rely on this Application! Watch the kids while they are using the internet also in person and start be familiar also with the Internet Explorer Content Advisory features.

Conclusion

We learned how to build a windows NT Service application interacting seamlessly with the user, which runs fine on Windows Vista and also supports previous Windows OS versions like Windows XP and Windows Server 2003. Furthermore, we learned how to expose .NET Components as COM+ Application and have gained some understanding around COM+ Application deployment pitfalls. We have integrated Windows Communication Foundation services into the NT Service Application and we made the User Interface capable to talk to the NT Service using named pipes.

The application is power event aware, and can handle suspending and resuming power-events. For detailed instructions how to do that please take the source code.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here