A Network Monitor

One thing that becomes apparent as you create and use more server-based applications is that keeping track of what is running and what is not running becomes an ever-increasing part of your job. Your organization might have an operations staff responsible for monitoring the performance of server-based applications. Even with this support, however, failures can be missed and not caught until the worst possible time.

Network monitoring is a real-life problem that looks a lot like a common programming problem: poll vs. interrupt. Using an operations staff to monitor your servers is a lot like having a program that constantly checks on the state of a variable or a system settings often called polling. Asynchronous operations, or interrupts, often make more efficient use of system resources. Allowing your server-based applications to interrupt rather than having a human being constantly poll their status can be very useful.

What Is a Network Monitor?

A network monitor is a dedicated workstation or server that tests the state of equipment and services on the network. The system checks for failures and then uses available resources to notify the responsible person. These notifications can be sent via e-mail, pager, or any other notification method on hand.

Figure 14-1 The relationship between the network monitor, the monitor database, and the user.

Figure 14-1 shows the relationship between the network monitor, the monitor database, and end users. The network monitor periodically queries the monitor database to determine what tasks should be performed. If a failure occurs, an alert message is written back to the monitor database, and depending upon the criteria, a notification will be sent to the user. The monitor database could be something as simple as an INI file or as complex as a complete Microsoft SQL Server database. For this example I use a SQL Server database. The structure of the database is outlined in the SQL creation script presented in Listing 14-1.

Listing 14-1

CreateTables.sql

 CREATE TABLE [dbo].[TblAlerts] (    [AlertID] [int] IDENTITY (1, 1) NOT NULL ,     [TaskID] [int] NOT NULL ,     [DateTimeOfAlert] [datetime] NULL ,     [DateTimeReset] [datetime] NULL ,     [DateTimeIntervention] [datetime] NULL ,     [ResetTypeID] [int] NULL ,     [AlertComment] [varchar] (255) NULL,     [ResetComment] [varchar] (255) NULL) GO CREATE TABLE [dbo].[TblResetType] (    [ResetTypeID] [int] IDENTITY (1, 1) NOT NULL ,     [Description] [char] (50) NULL)  ON [PRIMARY] GO CREATE TABLE [dbo].[TblTasks] (    [TaskID] [int] IDENTITY (1, 1) NOT NULL ,     [Name] [char] (50) NOT NULL ,     [ExecuteType] [int] NULL ,     [Executable] [varchar] (50) NULL ,     [Function] [varchar] (255) NULL ,     [Parameter1] [varchar] (50) NULL ,     [Parameter2] [varchar] (50) NULL ,     [Parameter3] [varchar] (50) NULL ,     [Parameter4] [varchar] (50) NULL ,     [DateTimeLastRun] [datetime] NULL ,     [AtOrEvery] [char] (1) NULL ,     [EveryType] [char] (30) NULL ,     [RunAtTime] [datetime] NULL ,     [DateTimeNextRun] [datetime] NULL ,     [RunEveryInterval] [int] NULL     [Beeper] varchar(50) NULL,     [Email] varchar(128) NULL) ON [PRIMARY] GO ALTER TABLE [dbo].[TblAlerts] WITH NOCHECK ADD     CONSTRAINT [PK_TblAlerts] PRIMARY KEY  CLUSTERED     (        [AlertID]     )  ON [PRIMARY] GO ALTER TABLE [dbo].[TblResetType] WITH NOCHECK ADD     CONSTRAINT [PK_TblResetType] PRIMARY KEY  NONCLUSTERED     (        [ResetTypeID]     )  ON [PRIMARY] GO ALTER TABLE [dbo].[TblTasks] WITH NOCHECK ADD     CONSTRAINT [PK_TblTasks] PRIMARY KEY  NONCLUSTERED     (        [TaskID]     )  ON [PRIMARY] GO CREATE  INDEX [IX_TblAlerts] ON     [dbo].[TblAlerts]([TaskID]) ON [PRIMARY] GO CREATE  INDEX [IX_TblAlerts_1] ON     [dbo].[TblAlerts]([DateTimeOfAlert]) ON [PRIMARY] GO CREATE  INDEX [IX_TblTasks] ON     [dbo].[TblTasks]([Name]) ON [PRIMARY] GO CREATE  INDEX [IX_TblTasks_1] ON     [dbo].[TblTasks]([DateTimeNextRun]) ON [PRIMARY] GO 

The primary tables in the monitor database are as follows:

  • TblTasks. The list of tasks including the DLL or EXE file to run along with functional information, the date and time of the last run, and when the next run is due
  • TblAlerts. The list of alerts, with fields to hold the TaskID that the alert refers to, the date and time the alert occurred, and, if applicable, the date and time the alert was reset and the reset type
  • TblResetType. The type of reset, which can be a manual reset meaning someone intervened and reset it or a self-corrected reset, meaning that the next time the task was run it succeeded

In MonitorService , all alerts will be sent to a single person. The address information for that person is located in each record in TblTasks . In a real-world application, at least one additional table would contain responsible-party information. This table would be related to the task information table, allowing the failure of each task to result in alerts to a different person, as appropriate. We could also specify an amount of time to give the responsible person to reset the error or assign that value within the task. Different environments require different actions an obscure system used for nightly processing is quite different from a line of business system that affects an entire enterprise.

The Network Monitor's Structure

The network monitor's requirements fit nicely into the structure of a Microsoft Windows 2000 service. To see if we need to do processing, we can simply add a test to the processing loop contained in Run . The actual processing can take place in a separate thread. We do this test with an ODBC connection, using the classes I described in Chapter 8.

At least two other threads run on the network monitor. The monitor uses the same mechanism for communicating with clients as does the CCommService example from Chapter 12. The service creates the requested number of worker communication threads in its constructor. There is also a thread running to follow up on alerts. This thread allows the service to track unhandled alerts, again notifying those involved if needed. TblAlerts contains the field DateTimeIntervention , which contains the date and the time the system last tried to notify the responsible party of the problem. The time of the last intervention, if any, is used to determine whether an alert should be escalated. In CMonitorService , escalation is a two-step process, moving from e-mailing to beeping. A more elaborate process could include backup people related to the task or responsible party.

The actual tests that probe the network are implemented as separate DLL or executable files. The database contains information on the type of file a DLL or an EXE file as well as the function name for DLLs. The task file, which contains information about who is responsible for each task, also contains parameters, which are later packaged to be sent to the DLL or passed directly to the EXE file. Any DLL function to be used by CMonitorService must accept a single parameter a pointer to a class that gives context to the DLL function and returns information on the success or failure of the operation. The network monitor supports EXE files because they allow us to use existing programs that cannot be modified. In this case, the only detail returned to CMonitorService is a return code from the executable. For this reason, custom DLL functions are the preferred way to call tests.

This structure allows the greatest flexibility possible. The sample DLL will ping a server using TCP/IP, determine the status of a service, test an ODBC data source, and perform a more complete test on a SQL Server database. We can add new functions as needed without requiring changes to the base executable.

The CMonitorService Class

The CMonitorService class is based upon the CPPService class. It uses some of the same functions as CTAPIMAPIService , which we created in Chapter 7. I made only one change to the base class. You can see the change to the CPPService class at the bottom of the ParseArguments method shown in the following code fragment:

 BOOL CPPService::ParseArguments(int argc, char **argv) {     BOOL result = false;     if (argc > 1 && !(_strnicmp(argv[1], "-i", 2)))     {         if ((Install()))         {             printf("\n%s installed!", ServiceName());         }         else         {             printf("\n%s NOT installed! Last error %d",             ServiceName(), GetLastError());         }         result = true;     }     else if (argc > 1 && !(_strnicmp(argv[1], "-u", 2)))     {         if ((UnInstall()))         {             printf("\n%s uninstalled!", ServiceName());         }         else         {             printf(                "\n%s NOT uninstalled! Last error "                 "%d", ServiceName(), GetLastError());         }         result = true;     }     // Added for Chapter 14     else if (argc > 1 && !(_strnicmp(argv[1], "-d", 2)))     {         m_isRunning = true;         Run();         result = true;     }     return result; } 

This change adds support for a - d parameter. This parameter allows the program to run as a standard console mode application. This is tremendously useful for debugging. Of course, not all problems can be diagnosed using the - d mode because some problems can be caused by the security context under which the service operates.

NOTE
MonitorService must be run under the context of a specific user rather than under the context of the System account. If you run the service under the System account, you will find no mail profile. This will cause the service to not run.

One other possible problem is the use of the default mail profile. If there is no default mail profile, or if the default mail profile is not appropriate, mail might not be sent. You can modify the code, passing an actual mail profile name rather than NULL to accept the default. Making the mail profile a parameter would make the service even more flexible.

Listing 14-2 contains the StdAfx.h file, the standard header for CMonitorService . This header includes files to support the base class, TAPI, MAPI, and ODBC.

Listing 14-2

StdAfx.h

 // StdAfx.h : include file for standard system include files, //  or project-specific include files that are used frequently, but //  are changed infrequently // #if !defined(AFX_STDAFX_H__7327D38D_5E17_11D3_B4B6_00C04F79B510__ INCLUDED_) #define AFX_STDAFX_H__7327D38D_5E17_11D3_B4B6_00C04F79B510__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define WS_VERSION_REQD    0x0202 // Exclude rarely used stuff from Windows headers. #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include <stdlib.h> #include <winsock2.h> #include <string.h> #include <tapi.h> #include <mapi.h> #include <mapix.h> #include "mapiinit.h" #include <initguid.h> #include <mapitags.h> #include "SQL.H" #include "sqlext.h" #include "odbclass.h" #include "time.h" typedef DWORD (WINAPI *THREADPROC)(LPVOID lpParameter); #include "CppSvcMsg.h" #include "CPPService.h" #include "MonitorService.h" // TODO: Reference additional headers your program requires here. //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional //  declarations immediately before the previous line. #endif //!defined(AFX_STDAFX_H__7327D38D_5E17_11D3_B4B6_00C04F79B510__ INCLUDED_) 

Listing 14-3 shows MonitorService.h, the header for the CMonitorService class. First note that the number of worker threads specified by the NUM_WORKERS #define is two rather than four as in some previous examples. This service will interact less with remote clients and do more dynamic thread creation. Two enums are defined for the type of executable the monitor runs to perform network tests. The MONITOR_CONTEXT structure is a general-purpose structure that allows information on a current task to be passed from thread to thread and to the testing functions called in an outlying DLL. Many of the other methods of the class are similar to earlier examples, such as CTAPIMAPIService from Chapter 7. Additional data members of the class include the name, user name, and password used to connect to the monitoring database. These default to "Monitoring", "sa", and " ", respectively, and can be changed after the class is constructed.

Listing 14-3

MonitorService.h

 #define NUM_WORKERS 2 enum ExecutableType {     ExecutableFile = 1,     DLLFile = 2 }; enum OperationType {     ProcessClient = 1,     ProcessTask = 2 }; typedef struct _MONITOR_CONTEXT {     OperationType Operation;     ExecutableType exeType;     LPVOID CommHandle;     char szExecutable[255];     char szFunction[255];     char szParameters[4][132];     char szTaskName[255];     DWORD dwTaskID;     DWORD dwRunEveryInterval;     char atOrEvery;     char szReturnMsg[512];     long returnCode;     bool fSuccess; } MONITOR_CONTEXT; typedef DWORD (FAR PASCAL *PFNTESTFUNC)     (MONITOR_CONTEXT *pContext); class CMonitorService : public CPPService { private:     static DWORD __stdcall DoTask(LPVOID v);     static DWORD __stdcall WSCommDispatcher(LPVOID v);     static DWORD __stdcall MonitorAlerts(LPVOID v);     int CallBeeper(char *beeperNumber, char *beepTo);     int SendMapiMessage(char *szTo, char *szSubject,         char *szNoteText);     bool GetRootFolder(void);     bool GetFoldersHierarchy(char *name,         IMAPIFolder *pfolder, short int depth);     void ReportTAPIError(LONG ret, char *op);     void SetLastSocket(SOCKET s);     int InitMAPI();     int DeInitMAPI();     SOCKET GetLastSocket();     HANDLE m_hGotContext;     HANDLE m_hEvent;     HANDLE m_hThreads[NUM_WORKERS];     DWORD m_dwThreadIDs[NUM_WORKERS];     SOCKET LastSocket;     int m_ServerPort;     CRITICAL_SECTION csMapi; public:     char m_dbPassword[255];     char m_dbUser[255];     char m_database[255];     static DWORD __stdcall Thread(LPVOID t);     virtual void DoWork(SOCKET s, WORD ThreadNum);     int GetServerPort();     void SetServerPort(int tServerPort);     CMonitorService(char *tsvcName, char *tdispName,         DWORD tsvcStart = SERVICE_DEMAND_START,         bool CreateMsgPump = true);     ~CMonitorService();    virtual void Run();    virtual void OnInstall(); }; 

Listing 14-4 contains MonitorService.cpp, which defines most methods of the CMonitorService class. The TAPI- and MAPI-related functions are located in TAPIMAPISupport . cpp in the companion CD-ROM. I made modest changes to the MAPI functions because of the relatively high volume of e-mail that could be sent by this application. Most of the setup and breakdown of the MAPI environment is contained in InitMapi and DeInitMapi , respectively. In earlier examples these functions did no more than load and unload the MAPI DLL.

Listing 14-4

MonitorService.cpp

 // MonitorService.cpp : Communications service, using TCP/IP //  and Events. // #include "stdafx.h" #include <mapiguid.h> //------------------------------------------------------------------- // Example simple main routine. int main(int argc, char* argv[]) {     CMonitorService MyMonitorService("MonitorService",         "Monitor Test Service");     // We could do something like the following if we needed to.     // MyCommService.SetServerPort(5555);     // This example uses the default.     // Have we handled the arguments and done an install     //  or an uninstall?     if (MyMonitorService.ParseArguments(argc, argv) == false)     {         // If not, start the service running.         MyMonitorService.StartService();     }     return 0; } //------------------------------------------------------------------- // Utility function. LPSTR DeleteTrsp(LPSTR s) {     int loop;     loop = strlen(s);     loop--;     while (loop && (s[loop] == ' '  s[loop] == '\t'))     {         s[loop] = '\0';         loop--;     }     return(s); } //------------------------------------------------------------------- // The heart of the class, the WinSock dispatcher DWORD __stdcall CMonitorService::WSCommDispatcher(LPVOID v) {     // Declare and initialize.     BOOL   fConnected = FALSE;     DWORD   dwThreadID = (DWORD)0;     HANDLE  hThread = INVALID_HANDLE_VALUE;     int     addrLen;     int     error;     struct  sockaddr_in srvSocketAddr;     struct  sockaddr_in cliSocketAddr;     char    diag[255];     WSADATA wsaData;     SOCKET  srvSocket;     SOCKET  cliSocket;     CMonitorService *p_this;     p_this = (CMonitorService *)m_this;     error = WSAStartup(WS_VERSION_REQD, &wsaData);     if (error != 0)     {         return 0;     }     if (LOBYTE(wsaData.wVersion) < 2)     {         sprintf(diag, "\nVersion %d.%d not supported",             LOBYTE(wsaData.wVersion),             HIBYTE(wsaData.wVersion));         p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_DEBUG, diag);         return 0;     }     srvSocket = socket(AF_INET, SOCK_STREAM, 0);     if (srvSocket == INVALID_SOCKET)     {         sprintf(diag, "SERVER: Windows Socket Error %d - "             "Couldn't create socket",             WSAGetLastError());         p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_DEBUG, diag);         WSACleanup();         return 0;     }     srvSocketAddr.sin_family = AF_INET;     srvSocketAddr.sin_addr.s_addr = INADDR_ANY;     srvSocketAddr.sin_port = p_this->GetServerPort();     if (bind(srvSocket, (LPSOCKADDR)&srvSocketAddr,         sizeof(srvSocketAddr)) == SOCKET_ERROR)     {         sprintf(diag, "SERVER: Windows Socket Error %d - "             "Couldn't bind socket",             WSAGetLastError());         p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_DEBUG, diag);         WSACleanup();         return 0;     }     if (listen(srvSocket, SOMAXCONN) == SOCKET_ERROR)     {         sprintf(diag, "SERVER: Windows Socket Error %d - "             "Couldn't listen on socket",             WSAGetLastError());         p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_DEBUG, diag);         WSACleanup();         return 0;     }     while ((p_this->IsRunning()))     {         addrLen = (sizeof(cliSocketAddr));         cliSocket = accept(srvSocket,             (LPSOCKADDR)&cliSocketAddr, &addrLen);         if (cliSocket == INVALID_SOCKET)         {             wsprintf(diag,                 "SERVER: Windows Socket Error %d - "                 "accept() got invalid socket",                 WSAGetLastError());             p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_DEBUG, diag);             WSACleanup();             return 0;         }         p_this->SetLastSocket(cliSocket);         SetEvent(p_this->m_hEvent);         while (WaitForSingleObject(p_this->m_hGotContext,             1000) == WAIT_TIMEOUT)         {             if (!p_this->IsRunning())             {                 WSACleanup();                 wsprintf(diag, "SERVER: Exiting");                 p_this->LogEvent(EVENTLOG_INFORMATION_TYPE,                     EVMSG_DEBUG, diag);                 return 0;             }         }         ResetEvent(p_this->m_hGotContext);     }     WSACleanup();     sprintf(diag, "SERVER: Exiting");     p_this->LogEvent(EVENTLOG_INFORMATION_TYPE, EVMSG_DEBUG, diag);     return 0; } //------------------------------------------------------------------- // The constructor. CMonitorService::CMonitorService(char *tsvcName,                                  char *tdispName,                                  DWORD tsvcStart,                                  bool CreateMsgPump) :CPPService(tsvcName, tdispName, tsvcStart, CreateMsgPump) {     DWORD dwCreationFlags = 0;     // Create the event, default security,     //  NOT manual Reset,     //  NON signaled originally,     //  NOT named, since just used by this process.     m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);     if (m_hEvent == NULL)     {         LogEvent(EVENTLOG_ERROR_TYPE,             EVMSG_DEBUG,             "Can't create event!");     }     // Create the event, default security,     //  Manual Reset,     //  NON signaled originally,     //  NOT named, since just used by this process.     m_hGotContext = CreateEvent(NULL, TRUE, FALSE, NULL);     if (m_hEvent == NULL)     {         LogEvent(EVENTLOG_ERROR_TYPE,             EVMSG_DEBUG,             "Can't create Got Socket event!");     }     m_ServerPort = 5043;                 // A default port     strcpy(m_database, "Monitoring");    // Default values     strcpy(m_dbUser, "sa");     strcpy(m_dbPassword, "");     for (WORD loop = 0 ; loop < NUM_WORKERS ; loop++)     {         if ((m_hThreads[loop] = CreateThread(NULL,                 0, Thread, (LPVOID)loop,                 dwCreationFlags,                 &m_dwThreadIDs[loop])) == NULL)         {             LogEvent(EVENTLOG_ERROR_TYPE,                 EVMSG_DEBUG,                 "Can't create thread!");         }     } } CMonitorService::~CMonitorService() {     if (m_hEvent != NULL)     {         CloseHandle(m_hEvent);     }     if (m_hGotContext != NULL)     {         CloseHandle(m_hGotContext);     }     for (WORD loop = 0 ; loop < NUM_WORKERS ; loop++)     {         if (m_hThreads[loop] != NULL)         {             CloseHandle(m_hThreads[loop]);         }     } } //------------------------------------------------------------------- // Used to pass a socket from the dispatcher to the worker thread SOCKET CMonitorService::GetLastSocket() {     return LastSocket; } //------------------------------------------------------------------- // Used to pass a socket from the dispatcher to the worker thread void CMonitorService::SetLastSocket(SOCKET s) {     LastSocket = s; } //------------------------------------------------------------------- // Set the port number the server should use. void CMonitorService::SetServerPort(int tServerPort) {     m_ServerPort = tServerPort; } //------------------------------------------------------------------- // Get the port number the server is using. int CMonitorService::GetServerPort() {     return m_ServerPort; } //------------------------------------------------------------------- // The routine called to run the service void CMonitorService::Run() {     DWORD dwThreadID;     DWORD dwSleepCount = 0;     HANDLE hThread;     RETCODE rc;     MONITOR_CONTEXT Context;     ::InitializeCriticalSection(&csMapi);     ::EnterCriticalSection(&csMapi);     InitMAPI();     ::LeaveCriticalSection(&csMapi);     CODBCDatabase::InitCriticalSection();     CODBCDatabase *db;     db = new CODBCDatabase(m_database, m_dbUser, m_dbPassword);     if ((hThread = CreateThread(NULL, 0, WSCommDispatcher,         (LPVOID)0, 0, &dwThreadID)) != NULL)     {         CloseHandle(hThread);     }     Sleep(1);     if ((hThread = CreateThread(NULL, 0, MonitorAlerts,         (LPVOID)0, 0, &dwThreadID)) != NULL)     {         CloseHandle(hThread);     }     while (m_isRunning)     {         if (!(dwSleepCount%30))         {             UWORD col = 1;             CODBCCursor *curs;             SQL_TIMESTAMP_STRUCT Date;             SQL_TIMESTAMP_STRUCT tsNow;             char szDate[255];             char szDateNext[255];             char szBuffer[255];             char szSql[1024];             curs = new CODBCCursor(db);             curs->setTables("tblTasks");             curs->bindColumn(col, "TaskID",                 255, SQL_C_ULONG);             curs->bindColumn(col, "Executable", 255);             curs->bindColumn(col, "Function", 255);             curs->bindColumn(col, "ExecuteType",                 255, SQL_C_ULONG);             curs->bindColumn(col, "DateTimeLastRun",                 255, SQL_C_TYPE_TIMESTAMP);             curs->bindColumn(col, "Parameter1", 255);             curs->bindColumn(col, "Parameter2", 255);             curs->bindColumn(col, "Parameter3", 255);             curs->bindColumn(col, "Parameter4", 255);             curs->bindColumn(col, "Name", 255);             curs->bindColumn(col, "atOrEvery", 2);             curs->bindColumn(col, "RunEveryInterval",                 255, SQL_C_ULONG);             curs->BuildSQLTimestamp(&tsNow, szDate);             wsprintf(szBuffer,                 " WHERE DateTimeNextRun<='%04d-%02d-%02d %02d:%02d'",                 tsNow.year, tsNow.month, tsNow.day,                 tsNow.hour, tsNow.minute);             curs->setRestrict(szBuffer);             curs->setOrderBy(" DateTimeNextRun ");             curs->doSelect();             if ((rc = curs->fetch()) == SQL_SUCCESS                   (rc == SQL_SUCCESS_WITH_INFO))             {                 memcpy((void *)&Date,                     (SQL_TIMESTAMP_STRUCT *)                     (*curs)["DateTimeLastRun"],                     sizeof(SQL_TIMESTAMP_STRUCT));                 Context.dwTaskID = *(int *)(*curs)[1];                 Context.exeType =                     *(ExecutableType *)(*curs)["ExecuteType"];                 Context.dwRunEveryInterval =                     *(int *)(*curs)["RunEveryInterval"];                 strcpy(Context.szExecutable,                     (char *)(*curs)["Executable"]);                 strcpy(Context.szFunction,                     (char *)(*curs)["Function"]);                 strcpy(Context.szParameters[0],                     (char *)(*curs)["Parameter1"]);                 strcpy(Context.szParameters[1],                     (char *)(*curs)["Parameter2"]);                 strcpy(Context.szParameters[2],                     (char *)(*curs)["Parameter3"]);                 strcpy(Context.szParameters[3],                     (char *)(*curs)["Parameter4"]);                 strcpy(Context.szTaskName,                     (char *)(*curs)["Name"]);                 Context.atOrEvery = *(char *)(*curs)["atOrEvery"];                 curs->BuildSQLTimestamp(&tsNow, szDate);                 if (Context.atOrEvery == 'E')                 {                     curs->IncrementSQLTimestamp(&tsNow,                         Context.dwRunEveryInterval);                 }                 else                 {                     curs->IncrementSQLTimestamp(&tsNow,                         (60*24));                 }                 wsprintf(szDateNext, "%04d-%02d-%02d %02d:%02d",                     tsNow.year,                     tsNow.month,                     tsNow.day,                     tsNow.hour,                     tsNow.minute);                 // Delete this cursor so that the outstanding                  //  statement is cleared and the update can take place.                 //  This limits us to one event per minute,                 //  not an unreasonable limit for an example.                 delete curs;                 wsprintf(szSql, "UPDATE tblTasks SET "                     "DateTimeLastRun='%s', DateTimeNextRun='%s'"                     " WHERE TaskID=%d",                     szDate, szDateNext, Context.dwTaskID);                 db->ExecuteSQL(szSql);                 hThread = CreateThread(NULL, 0, DoTask,                     (LPVOID)&Context, 0,                     &dwThreadID);                 if (hThread != NULL)                 {                     CloseHandle(hThread);                 }                 else                 {                     LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_ERROR,                         DeleteTrsp(Context.szTaskName),                         "Could not create thread");                     wsprintf(szSql,                         "INSERT TblAlerts(DateTimeOfAlert,"                         "TaskID,AlertComment)"                         "VALUES('%s',%d,'Unable to create thread')",                         szDate, Context.dwTaskID);                     db->ExecuteSQL(szSql);                 }             }             else             {                 delete curs;             }             dwSleepCount = 0;         }         dwSleepCount++;         Sleep(1000);     }     m_isRunning = false;     // Give threads a chance to die.     Sleep(2000);     delete db;     DeInitMAPI();     CODBCDatabase::DelCriticalSection();     ::DeleteCriticalSection(&csMapi);     return; } //------------------------------------------------------------------- // Start the service upon installation. void CMonitorService::OnInstall() {     ::StartThisService(ServiceName()); } //------------------------------------------------------------------- // The function that does the actual work. This should be overridden. void CMonitorService::DoWork(SOCKET s, WORD ThreadNum) {     char diag[255];     char data[1024];     BOOL fSuccess;     int len;     static int blksSent = 0;     int bytesRead;     int maxData = 512;     bytesRead = 0L;     do {         memset((void *)data, '\0', 1024);         if ((len = recv(s,             ((char *)data)+bytesRead,             maxData, 0)) == SOCKET_ERROR)         {             fSuccess = FALSE;             wsprintf(diag, "SOCKET ERROR", data);             LogEvent(EVENTLOG_ERROR_TYPE,                 EVMSG_DEBUG, diag);         }         else         {             if (len)             {                 fSuccess = TRUE;                 bytesRead += (DWORD)len;                 blksSent++;                 data[200] = '\0';        // Only try to log the                                 // first 200 characters or fewer.                 wsprintf(diag,                     "Client says: %s to Thread %d",                     data, ThreadNum);                 LogEvent(EVENTLOG_INFORMATION_TYPE,                     EVMSG_DEBUG, diag);             }         }     } while (IsRunning() && len != SOCKET_ERROR && len != 0);     closesocket(s); } //------------------------------------------------------------------- // The thread routine;  used to call DoWork correctly DWORD __stdcall CMonitorService::Thread(LPVOID t) {     DWORD ret;     WORD ThreadNum;     SOCKET s;     CMonitorService *p_this;     p_this = (CMonitorService *)m_this;     ThreadNum = (WORD)(t);     do {         while ((ret = WaitForSingleObject(p_this->m_hEvent,                 1000)) == WAIT_TIMEOUT &&                 p_this->IsRunning() == TRUE)         {             ;         }         if (p_this->IsRunning() == FALSE)         {             break;         }         if (ret == WAIT_OBJECT_0)         {             s = p_this->GetLastSocket();             SetEvent(p_this->m_hGotContext);             p_this->DoWork(s, ThreadNum);         }     } while (ret == WAIT_TIMEOUT  ret == WAIT_OBJECT_0);     return(0); } DWORD __stdcall CMonitorService::DoTask(LPVOID v) {     MONITOR_CONTEXT *Context;     STARTUPINFO StartupInfo;     PROCESS_INFORMATION ProcessInfo;     BOOL RetVal;     DWORD dw;     SQL_TIMESTAMP_STRUCT tsNow;     char szCommandLine[512];     char szDate[255];     char szSql[1024];     HMODULE hLibrary;     PFNTESTFUNC pfnTestFunc;     CODBCDatabase *db;     CODBCCursor *curs;     CMonitorService *p_this;     p_this = (CMonitorService *)m_this;     Context = (MONITOR_CONTEXT *)v;     db = new CODBCDatabase("Monitoring", "sa", "");     if (!(db->isConnected()))     {         return(0);     }     curs = new CODBCCursor(db);     curs->BuildSQLTimestamp(&tsNow, szDate);     // Now we do the loadlibrary or createprocess.     switch (Context->exeType)     {     case DLLFile:         Context->fSuccess = false;         wsprintf(Context->szReturnMsg,             "Function %s in Library %s not loaded",             DeleteTrsp(Context->szFunction),             DeleteTrsp(Context->szExecutable));         hLibrary = LoadLibrary(Context->szExecutable);         if (hLibrary != NULL)         {             pfnTestFunc = (PFNTESTFUNC)GetProcAddress(hLibrary,                 Context->szFunction);             if ((pfnTestFunc != NULL))             {                 pfnTestFunc(Context);             }             FreeLibrary(hLibrary);         }         if (!Context->fSuccess)         {             LPVOID szMsgBuf;             FormatMessage(                FORMAT_MESSAGE_ALLOCATE_BUFFER                  FORMAT_MESSAGE_FROM_SYSTEM                  FORMAT_MESSAGE_IGNORE_INSERTS,                 NULL,                 GetLastError(),                 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                 (LPTSTR) &szMsgBuf,                 0,                 NULL);             strcat(Context->szReturnMsg, "  ");             strcat(Context->szReturnMsg, (const char *)szMsgBuf);             // Free the buffer.             LocalFree(szMsgBuf);         }         break;     case ExecutableFile:         memset((void *)&StartupInfo, '\0', sizeof(StartupInfo));         memset((void *)&ProcessInfo, '\0', sizeof(ProcessInfo));         StartupInfo.cb = sizeof(StartupInfo);         StartupInfo.cbReserved2 = 0;         StartupInfo.dwFillAttribute = 0;         StartupInfo.dwFlags = 0;         StartupInfo.dwX = 0;         StartupInfo.dwXCountChars = 0;         StartupInfo.dwY = 0;         StartupInfo.dwYCountChars = 0;         StartupInfo.lpDesktop = NULL;         StartupInfo.lpReserved = NULL;         StartupInfo.lpReserved2 = NULL;         StartupInfo.lpTitle = "";         StartupInfo.wShowWindow = SW_HIDE;         sprintf(szCommandLine, "%s %s %s %s %s",             Context->szExecutable,             Context->szParameters[0],             Context->szParameters[1],             Context->szParameters[2],             Context->szParameters[3]);         RetVal = CreateProcess(NULL, szCommandLine, NULL, NULL,             FALSE, 0, NULL, NULL,             &StartupInfo,             &ProcessInfo);         if (!RetVal)         {             LPVOID szMsgBuf;             FormatMessage(                FORMAT_MESSAGE_ALLOCATE_BUFFER                  FORMAT_MESSAGE_FROM_SYSTEM                  FORMAT_MESSAGE_IGNORE_INSERTS,                 NULL,                 GetLastError(),                 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                 (LPTSTR) &szMsgBuf,                 0,                 NULL);             wsprintf(Context->szReturnMsg,                 "Error creating process: %s ", szMsgBuf);             LocalFree(szMsgBuf);         }         else         {             DWORD dwExitCode;             do {                 dw = WaitForSingleObject(ProcessInfo.hProcess, 1000);             } while (dw == WAIT_TIMEOUT && p_this->IsRunning());             GetExitCodeProcess(ProcessInfo.hProcess, &dwExitCode);             wsprintf(Context->szReturnMsg,                 "Process %s Returned %d",                 szCommandLine, dwExitCode);             Context->fSuccess = (dwExitCode == 0);             CloseHandle(ProcessInfo.hProcess);             CloseHandle(ProcessInfo.hThread);         }         break;     }     if (!Context->fSuccess)     {         curs->BuildSQLTimestamp(&tsNow, szDate);         p_this->LogEvent(EVENTLOG_ERROR_TYPE, EVMSG_ALERT,             DeleteTrsp(Context->szTaskName),             DeleteTrsp(Context->szReturnMsg));         wsprintf(szSql,             "INSERT TblAlerts(DateTimeOfAlert,"             "TaskID,alertComment)"             "VALUES('%s',%d,'%s')",             szDate,Context->dwTaskID,             Context->szReturnMsg);         db->ExecuteSQL(szSql);     }     else     {         // Clear any alert.         wsprintf(szSql,             "UPDATE TblAlerts SET DateTimeReset='%s',"             "ResetTypeID="             "(SELECT resetTypeId FROM TblResetType "             "WHERE Description LIKE 'Self Corrected%%') "             "WHERE TaskID=%d",             szDate, Context->dwTaskID);         db->ExecuteSQL(szSql);     }     delete curs;     delete db;     return(0); } //------------------------------------------------------------------- // This is used to monitor alerts. DWORD __stdcall CMonitorService::MonitorAlerts(LPVOID t) {     DWORD dwSleepCount = 0;     RETCODE rc;     char szRestrict[1024];     char szDate[255];     char szBuffer[255];     CMonitorService *p_this;     CODBCDatabase *db;     CODBCCursor *curs;     SQL_TIMESTAMP_STRUCT ts;     p_this = (CMonitorService *)m_this;     db = new CODBCDatabase(p_this->m_database,         p_this->m_dbUser,         p_this->m_dbPassword);     if (!db->isConnected())     {         char diag[255];         wsprintf(diag,             "Cannot Connect to %s database",             p_this->m_database);         p_this->LogEvent(EVENTLOG_ERROR_TYPE,             EVMSG_DEBUG, diag);         dwSleepCount = 0;         return 0;     }     while (p_this->IsRunning())     {         if ((dwSleepCount%180) == 0)         {             UWORD col;             do {                 curs = new CODBCCursor(db);                 curs->setTables("tblAlerts");                 curs->bindColumn(col, "AlertID", 255,                     SQL_C_ULONG);                 curs->bindColumn(col, "TaskID", 255,                     SQL_C_ULONG);                 curs->bindColumn(col, "DateTimeReset", 255,                     SQL_C_TYPE_TIMESTAMP);                 curs->bindColumn(col, "DateTimeIntervention", 255,                     SQL_C_TYPE_TIMESTAMP);                 curs->bindColumn(col, "AlertComment", 255);                 curs->BuildSQLTimestamp(&ts, szDate);                 curs->DecrementSQLTimestamp(&ts, 15);                 wsprintf(szRestrict,                     " WHERE DateTimeReset IS NULL "                     "AND (DateTimeIntervention IS NULL OR "                     "DateTimeIntervention<"                     "'%04d-%02d-%02d %02d:%02d')",                     ts.year, ts.month, ts.day, ts.hour, ts.day);                 curs->setRestrict(szRestrict);                 curs->doSelect();                 rc = curs->fetch();                 if (rc == SQL_SUCCESS                      rc == SQL_SUCCESS_WITH_INFO)                 {                     DWORD dwAlertID;                     DWORD dwTaskID;                     char szDate[255];                     char szAlertComment[512];                     // We declare these in here to                     //  allow us to use a separate                     //  connection to the db.                     CODBCCursor *taskCurs;                     CODBCDatabase *db;                     dwAlertID = *(int *)(*curs)["AlertID"];                     dwTaskID = *(int *)(*curs)["TaskID"];                     strcpy(szAlertComment,                         (char *)(*curs)["AlertComment"]);                     SQL_TIMESTAMP_STRUCT tsNotify;                     memset((void *)&tsNotify, '\0', sizeof(tsNotify));                     memcpy((void *)&tsNotify,                         (SQL_TIMESTAMP_STRUCT *)                         (*curs)["DateTimeIntervention"],                         sizeof(SQL_TIMESTAMP_STRUCT));                     db = new CODBCDatabase(p_this->m_database,                         p_this->m_dbUser,                         p_this->m_dbPassword);                     taskCurs = new CODBCCursor(db);                     taskCurs->setTables("tblTasks");                     taskCurs->bindColumn(col, "Name", 255);                     taskCurs->bindColumn(col, "EMail", 255);                     taskCurs->bindColumn(col, "Beeper", 255);                     wsprintf(szRestrict, " WHERE TaskID=%d", dwTaskID);                     taskCurs->setRestrict(szRestrict);                     taskCurs->doSelect();                     taskCurs->fetch();                     // Notify the responsible party.                     if (tsNotify.year == 0)                     {                         // First time? Use e-mail.                         sprintf(szBuffer,                             "Error executing task number %d (%s). "                             "\nMessage: %s",                             dwTaskID,                             DeleteTrsp(                                (char *)(*taskCurs)["Name"]),                             DeleteTrsp(szAlertComment));                         EnterCriticalSection(&p_this->csMapi);                         if ((p_this->SendMapiMessage(                            (char *)(*taskCurs)["EMail"],                             "Monitor error!",                             szBuffer)) == -1)                         {                             char diag[255];                             wsprintf(diag,                                 "Cannot send E-Mail to %s",                                 (char *)(*taskCurs)["EMail"]);                             p_this->LogEvent(                                EVENTLOG_ERROR_TYPE,                                 EVMSG_DEBUG, diag);                         }                         LeaveCriticalSection(&p_this->csMapi);                     }                     else                     {                         // Second time? Beep'em.                         wsprintf(szBuffer, "%04d",                             dwTaskID);                         // Uncomment this code if you want to beep                         //  the person in tblTasks. This can be                         //  quite annoying during testing. /*                      p_this->CallBeeper(                            (char *)(*taskCurs)["Beeper"],                             szBuffer);*/                     }                     delete taskCurs;                     taskCurs = NULL;                     // Change intervention time.                     curs->BuildSQLTimestamp(&tsNotify, szDate);                     delete curs;                     curs = 0;                     wsprintf(szRestrict, "UPDATE tblAlerts "                         "SET DateTimeIntervention='%s' "                         "WHERE alertID=%d",                         szDate, dwAlertID);                     db->ExecuteSQL(szRestrict);                     delete db;                     db = NULL;                 }                 else                 {                     delete curs;                     curs = NULL;                 }             } while (p_this->IsRunning() &&                 (rc == SQL_SUCCESS                  rc == SQL_SUCCESS_WITH_INFO));         }         Sleep(1000);         dwSleepCount++;     }     if (db)     {         delete db;         db = NULL;     }     if (curs)     {         delete curs;         curs = NULL;     }     return 0; } 

To describe the way that CMonitorService works, I will step through the code starting at the Run method, describing the functions used by Run as they occur.

The Run method of CMonitorService first declares some variables, including two variables from the ODBC classes we first saw in Chapter 8, CODBCDatabase and CODBCCursor . I create a critical section to serialize access to the MAPI functions, which use some global variables. I call the modified version of InitMapi that is now a method of CMonitorService , which loads the MAPI DLL and sets up the MAPI session. Next I create two threads. First I use CreateThread to create a thread running WSCommDispatcher . This thread dispatches client requests; we first saw it in the CCommService example in Chapter 12. Next I create a thread to run the MonitorAlerts static method of the class.

The MonitorAlerts method polls the TblAlerts file periodically every 3 minutes in CMonitorService . I create local copies of the CODBCDatabase and CODBCCursor objects to allow MonitorAlerts to search the alerts table for unresolved alerts. Next I create a while loop controlled by the IsRunning method of CMonitorService . This method will manage alerts for the duration of the operation of the service.

A variable named dwSleepCount keeps track of the number of calls to Sleep with an argument specifying a 1000-millisecond sleep. This allows MonitorAlert to enter a section of code used to read the alerts in the TblAlerts file. I create a cursor pointing to the TblAlerts table. The two fields only get alerts with no date in the DateTimeReset field (meaning that they are current alerts) and that have been outstanding for at least 15 minutes, based on the value in DateTimeOfAlert . To select these alerts, I use one of several new methods added to CODBCCursor designed to work with ODBC's Timestamp type. These types are returned in a SQL_TIMESTAMP_STRUCT that contains members for year, month, day, hour, minute, second, and fraction of a second. For instance, the BuildSQLTimestamp method of CODBCCursor creates a SQL_TIMESTAMP_STRUCT and a string suitable to be placed in a SQL statement and places in each respective variables pointed to by the two arguments. IncrementSQLTimestamp increments the timestamp by the specified number of minutes, both values passed as parameters. These methods handle timestamp changes that cause the day to change. There are also methods to decrement timestamps; I use them here to create a timestamp for 15 minutes prior to the current system time.

For each alert that meets the specified criteria, I gather information about the task that failed, using a separate cursor and connection so that the existing connection remains at the proper position. Using information contained in the task record, MonitorAlerts either e-mails the responsible person with details of the problem or beeps the user with the task ID referred to by the alert. Finally the intervention time is stored in the alert's record. At the bottom of the loop, Sleep is called for 1000 milliseconds and dwSleepCount is incremented.

Let's go back to Run in CMonitorService . Once the MonitorAlerts thread begins, Run enters a loop controlled by IsRunning . As in MonitorAlerts , a variable named dwSleepCount allows processing to occur only after some number of calls to Sleep  in this case, 30 calls, meaning that the processing occurs for only 30 seconds. Times in the system are kept only to the minute, so this resolution is adequate. The number of times the thread calls Sleep is another item that might be stored in the registry and thus be configurable in a production application.

Each record in TblTasks contains a field called DateTimeNextRun . This field allows a relatively simple query to handle tasks that should be run every n minutes or each day at a given time. This is the field I used in the query in Run to determine which tasks should be run. This implementation has an artificial limit that allows only one task to be run per minute. This limit could be eliminated in one of two ways: either establish two connections to the database so that changes on one cursor would not disturb the results from the initial query, or cache the task ID of all tasks waiting to run in an array of DWORDs and then process the ID.

For records in TblTasks that meet the criteria, information from the record is placed into the appropriate fields in a MONITOR_CONTEXT structure. The elements stored there include the name of the DLL or executable program to run, the function name, and parameters to pass. Depending upon the type of task one performed every n minutes or one performed at a certain time  DateTimeNextRun and DateTimeLastRun are set. Of course, at this point it is uncertain that the program will actually be run, but failures will be logged in the Windows 2000 event log and in TblAlerts . Finally I create a thread to run DoTask , the function that actually runs the task specified in TblTasks. I fill in a MONITOR_CONTEXT structure based upon information in TblTask and send a pointer to the structure as the argument to the thread.

DoTask opens some ODBC connections and then enters a switch statement controlled by the exeType member of the context pointer passed to DoTask . If a DLL is specified and the DLL can be loaded using LoadLibrary , and if the function specified in TblTasks can be located using GetProcAddress , I call the specified function and pass it the same pointer to MONITOR_CONTEXT that was passed to DoTask . If an error occurs either in loading the DLL or in the operation of the function in the DLL an alert is inserted into TblAlerts .

If an EXE file is specified, I call CreateProcess and pass the parameters found in TblTasks on the command line. I check the return code from the newly created process to determine success or failure; 0 indicating success and any other value indicating failure. In a production system, the parameters might allow keywords that would be replaced at run time. For instance, a parameter of @today might be replaced with today's date and @now might be replaced with the current time.



Inside Server-Based Applications
Inside Server-Based Applications (DV-MPS General)
ISBN: 1572318171
EAN: 2147483647
Year: 1999
Pages: 91

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net