Contents
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.
Here is an arbitrary list of requirements which I have found practicable:
- The application is targeting Windows Vista and should be compatible to
Windows OS supporting the .NET Framework 3.0
- The application should be configurable e.g. via an application
configuration file (Figure 1)
- 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.
- 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
- 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.
- 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.
- The UI should run in the context of the interactively logged-on
user
- Fast user switching hast to be supported
- 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
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.
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.
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.
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.
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.
...
[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.
...
[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();
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.
...
[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
Figure 7 Server-Application's security tab
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).
- Using the RegSvcs.exe command line utility
- Automatically using the .NET Framework automatic registration
capabilities, named also as lazy registration
- 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.
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:
- First using
RegistrationHelper
the component will be registered in a
perfectly predictable and normal way - The
InstallAssembly
method is void and will
rather throw an exception if something goes wrong - Using
System.Activator
an instance of the COMAdmin is created (see cac
variable) - The COMAdmin returns the collection of all Applications (see
cacc
variable) - 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) - Next looking for the particular role AverageUser in the returned Role
collection
- If the role has been found, we need to identify the UsersInRole (see
caccUsers
variable) beneath the AverageUser role (see Figure
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.
- 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 - Lastly, the changes are persisted
...
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;
RegistrationHelper regHelper = new RegistrationHelper();
try
{
regHelper.InstallAssembly(assembly,
ref applicatioName,
ref typelibName,
InstallationFlags.FindOrCreateTargetApplication);
ICOMAdminCatalog cac =
(ICOMAdminCatalog)Activator.CreateInstance(Type.GetTypeFromProgID(
"COMAdmin.COMAdminCatalog"));
COMAdminCatalogCollection cacc =
(COMAdminCatalogCollection)cac.GetCollection("Applications");
COMAdminCatalogCollection caccRoles =
(COMAdminCatalogCollection)cacc.GetCollection("Roles",
strAppID);
caccRoles.Populate();
COMAdminCatalogObject cacoRole = null;
bool roleFound = false;
foreach (COMAdminCatalogObject objectRole in caccRoles)
{
if (objectRole.Key.ToString().ToUpper() == strAppRole.ToUpper())
{
roleFound = true;
cacoRole = objectRole;
break;
}
}
if (roleFound == true && cacoRole != null)
{
COMAdminCatalogCollection caccUsers =
(COMAdminCatalogCollection)caccRoles.GetCollection(
"UsersInRole", cacoRole.Key);
caccUsers.Populate();
COMAdminCatalogObject cacoUser = null;
cacoUser = (COMAdminCatalogObject)caccUsers.Add();
cacoUser.set_Value("User", "SYSTEM");
caccUsers.SaveChanges();
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
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.
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.
...
protected override void OnStart(string[] args)
{
InitializeOnStart();
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);
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);
}
SaveSettings();
this.ExitCode = 0;
}
public void ServiceWorkerMethod()
{
serviceLog.WriteEntry("The service worker thread has been succesfully
started.");
try
{
do
{
Thread.Sleep(timeInterval);
....
}
while (goLoop);
}
catch (ThreadAbortException)
{
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
...
protected override void OnStart(string[] args)
{
InitializeOnStart();
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);
SaveSettings();
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;
}
try
{
....
}
catch (ThreadAbortException)
{
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.
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()
{
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)
{
}
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).
- 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. - 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.
- 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. - The service now can start creating a new UI calling
WatchedUser.CreateUserInterface()
(Figure 12). - 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. - 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).
private bool IsUserLoggedOn(out string userDomain, out string userName)
{
userDomain = "";
userName = "";
ObjectQuery oQuery = new ObjectQuery("select * from
win32_computersystem");
ManagementObjectSearcher oSearcher = new
ManagementObjectSearcher(oQuery);
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();
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.
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.
public ProjectInstaller()
{
InitializeComponent();
processInstaller.Account = ServiceAccount.LocalSystem;
serviceInstaller.StartType = ServiceStartMode.Automatic;
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:
- Open the Service Control Manager's Database
- Lock the SCM's Database for the time we put the described above
dependency in it
- Open the particular service using the service's name
- Change the service's configuration (while leaving all other
configuration details unchanged!)
- Unlock the SCM's database and close the service's handle
protected override void OnAfterInstall(IDictionary savedState)
{
base.OnAfterInstall(savedState);
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).
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.
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.
[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
protected override void OnStart(string[] args)
{
InitializeOnStart();
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);
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);
#region SAVE SETTINGS
if (serviceHost != null)
serviceHost.Close();
this.ExitCode = 0;
}
Figure 18 Extended with WCF hosting OnStart and OnStop
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.
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.
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.
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
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.
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.