Contents
IntroductionDo 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 requirementsHere is an arbitrary list of requirements which I have found practicable:
Collapse | Copy Code ... <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 solutionFirst 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 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 The NT Service
can detect the identity of the interactively
logged on user using WMI ( 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 InterfaceLet'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 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
Figure 3 Application Watcher Windows Form UI
If you decided to start with a Windows Application Template, rename the default
For a detailed explanation about the COM-Specific attributes read the CodeProject article "Exposing .NET Components to COM" from Nick Parker. Collapse | Copy Code ...
[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
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 The
Next, you are completely safe to delete the part of the Visual Studio generated
code which starts with something like Collapse | Copy Code ...
[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 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. Collapse | Copy Code ...
[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 3.2 COM+ Component DeploymentEssentially there are three ways of
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.
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
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
Collapse | Copy Code ...
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 ServiceThe 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 4.1 NT Service StructureThere 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 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 Note the "Do actual job here" comments in the Figures. This is the
place where our Collapse | Copy Code ...
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 Collapse | Copy Code ...
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 Collapse | Copy Code 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).
Collapse | Copy Code 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
4.2 NT Service DeploymentYou 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 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 Take a look at the Collapse | Copy Code 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 Somewhat more challenging is the implementation of the overridden
Collapse | Copy Code 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 ServiceAs 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:
5.1 Hosting WCF Services in the NT ServiceLet'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. Collapse | Copy Code [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 Next look at the Collapse | Copy Code <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 Collapse | Copy Code 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 UINow 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 Collapse | Copy Code 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
To handle system events, you have to override the
Collapse | Copy Code 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 moreThe 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:
ConclusionWe 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 |
|