WindowsNT Simple Service Manager (2)

Environment used: Windows NT Workstation version 4.0 with Service Pack 5, Microsoft Visual C++ 6.0.

Because many of the readers interested in the previous version have
compiling problems (especially on common controls extended styles),
the application class,
CServicesApp
, performs now some checks about
the system in
InitInstance
method.

1. The first is, of course, detection of operating system. The
required operating system IS Windows NT. Again I
have to mention that (the same ?) many readers asked me if I cannot
perform remote shutdown for a non-NT machine. The answer is no, if I want
to call InitiateSystemShutdown.
And, of course, the notion itself of service is valid only
for Windows NT. (If you can access the special keys for Windows ’95 and ’98,
like RunOnce, from an NT machine, please tell me how: if you
need to install Remote Registry Service on 9x machine or not, for example).

The member m_osverInfo is a OSVERSIONINFO structure. I
hope the names of strings IDs are quite self-explained. Also, the macros
as _MsgFatal
are in header file ErrStr.h
, in fact they are simple calls of
MessageBox
.


BOOL
CServicesApp::DetectWindowsNT()
{
m_osverInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
if(GetVersionEx(&m_osverInfo))
{
if(m_osverInfo.dwPlatformId != VER_PLATFORM_WIN32_NT)
{
CString strMessage;
strMessage.Format(IDS_ISNOTWINDOWSNT,
m_osverInfo.dwPlatformId == VER_PLATFORM_WIN32s
? _T(“Windows 3.1”) : (m_osverInfo.dwMinorVersion ? _T(“Windows ’98”) : _T(“Windows ’95”)));

_MsgFatal(NULL, strMessage);
return FALSE;
}
else
return TRUE;
}
else
{
CString strErr;
strErr.LoadString(IDS_ERR_OSVERSION);
_MsgFatal(NULL, strErr);
return FALSE;
}
}

2. The second needed check is for comctl32.dll version. The following routine, IsCommCtrl470_OrAbove, returns FALSE if common controls library has not the required version for this program (4.70). Again, string table may be supplied with some imagination.


BOOL
CServicesApp::IsCommCtrl470_OrAbove()
{
HINSTANCE _hCommCtl32Dll = 0;
DWORD _dwVersion = 0;
DWORD _dwRequired = MAKELONG(70, 4);

CString strMessage;

_hCommCtl32Dll = LoadLibrary(“comctl32.dll”);
if(_hCommCtl32Dll)
{
DLLGETVERSIONPROC pDllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(_hCommCtl32Dll, “DllGetVersion”);
if(pDllGetVersion)
{
DLLVERSIONINFO dvi;
HRESULT hr;

ZeroMemory(&dvi, sizeof(dvi));
dvi.cbSize = sizeof(dvi);

hr = (*pDllGetVersion)(&dvi);
if(SUCCEEDED(hr))
_dwVersion = MAKELONG(dvi.dwMinorVersion, dvi.dwMajorVersion);
else
goto _error_;
}
else
goto _error_;

goto _ok_;
}
else
goto _error_;

_error_:
strMessage.Format(IDS_OLDCOMMCTL32);
_MsgFatal(NULL, strMessage);

_ok_:
if(_hCommCtl32Dll)
FreeLibrary(_hCommCtl32Dll);

return (_dwVersion >= _dwRequired) ? TRUE : FALSE;
}

3. Another check is for netapi32.dll network library existence, performed by LoadNetDLL. The handle of this DLL is stored in the member variable m_hNetDLL and will be used in subsequent calls. The twin routine, UnloadNetDLL, is called in application’s class destructor.


bool
CServicesApp::LoadNetDLL()
{
m_hNetDLL = LoadLibrary(“netapi32.dll”);
return m_hNetDLL ? true : false;
}

bool
CServicesApp::UnloadNetDLL()
{
bool fFreed = FreeLibrary(m_hNetDLL) ? true : false;
m_hNetDLL = 0;
return fFreed;
}

4. Some words about using this program.

a) The tree control from left show workstation in the network. Right-click on a workstation will let you shutdown the selected machine, if you have rights to do this (SE_SHUTDOWN_PRIVILEGE, I think). The 2nd option is grayed and will generate a script to shutdown all services (especially on a server, services’ dependency chain is quite complicated and shutting down a server may take some time). For root, you can refresh workstations’ list. Again, there is a 2nd grayed option, shutdown network. Something is implemented, but is not quite correct. The menu item is disabled, but you can correct source at any time and after some tests (lookup at your colleagues, do this test in the night…) you can enable it.

b) The list control shows you services on selected workstation (if any). You can filter services selecting the type of service from the 1st listbox, Type, the state of service from the 2nd listbox, State, or entering some letters in the editbox Service name to filter the name (not display name). The filtering is case sensitive and only from the beginning (‘A’ will select ‘Alerter’, but not ‘Lanman’; an SQL-like filtering, with ‘%’, in the next version). Pressing F5 will refresh the list.

c) Move mouse over toolbar to read on the down status bar a short description of the buttons. Because this is a MFC-based toolbar (CToolBar), to use ON_UPDATE_COMMAND_UI in a dialog is a whole adventure (for me, at least). So, a button is not disabled (example: start service for a started service or no selection), but rather does nothing.

From the left:
1. Report. This will generate a report about all services on selected machine. You can choose 3 formats: Excel, Web Page and Text. The Word, Access and SQL Server Table are currently unfinished. Web Page and Text are implemented in code; Excel export resides in an external DLL, named msoentsv.dll in Release and msoentsvd.dll in Debug.

2. Print. Currently implementation is removed. the button does nothing.

3. Errors. Report error(s) during services’ listing.

4. Mail gestionary. Launch mail subsystem.

5. Explorer. Launch Windows Explorer.

6. Starts selected service.

7. Pauses/Continues selected service.

8. Stops selected service.

9. Currently logged user. The routine isn’t finished. Sometimes does not show correctly user.

10. Install new service. Shows a dialog which helps you to install a service.

11. Delete selected service. Mark selected service for removal.

12. Manage scheduled tasks. This feature is currently under construction, but you can edit and launch a scheduled task. You cannot remove or add. Resides in an external DLL, named ntsrvts.dll in Release and ntsrvtsd.dll in Debug.

5. Service installation is done via this dialog box. First button do the job, the 2nd dismiss dialog, and the 3rd browse for service executable file name. All the stuff is done by the CreateService system routine. The rest is history (tooltips, mouse move, etc.). It is not complete (load order group, dependencies, service start name, password are not considered at this time). But I think it’s a good starting point.

6. Logged user is managed by the following method, OnLoggedUser. As I already mentioned, it seems that will be our job (me and you) to determine the correct entry enumerated by the NetWkstaUserEnum at level 1. Pay some attention to USES_CONVERSION (it is in afxpriv.h header file) to avoid UNICODE problems. You will easily recognize the same technique used to gather information about a workstation (see my submission with the same name).


void
CServicesDlg::OnLoggedUser()
{
LPBYTE buf;
DWORD entriesread, total, hres = 0;

USES_CONVERSION;
LPWSTR lpwsrv = A2W(m_strWkstaName);

typedef NET_API_STATUS (NET_API_FUNCTION *NETWORKPROC_NetWkstaUserEnum)
(LPWSTR, DWORD, LPBYTE *, DWORD, LPDWORD, LPDWORD, LPDWORD);
NETWORKPROC_NetWkstaUserEnum _procNetWkstaUserEnum = (NETWORKPROC_NetWkstaUserEnum)
(GetProcAddress(theApp.m_hNetDLL, _T(“NetWkstaUserEnum”)));

if(_procNetWkstaUserEnum)
{
NET_API_STATUS nas = (*_procNetWkstaUserEnum)(lpwsrv, 1, (LPBYTE *)&buf,
MAX_PREFERRED_LENGTH, &entriesread, &total, &hres);
if(nas == NERR_Success)
{
WKSTA_USER_INFO_1 *pwui1 = (WKSTA_USER_INFO_1 *)buf;
CString str;
for(register DWORD x = 0; x < entriesread; x++) { str.Format(_T("Currently logged user on machine %s is %S (%S,%S,%S)."), m_strWkstaName, pwui1[x].wkui1_username, pwui1[x].wkui1_logon_domain, pwui1[x].wkui1_logon_server, pwui1[x].wkui1_oth_domains); } _MsgInfo(m_hWnd, str); } } }

7. About Excel Automation used in MSOffice subsystem, all the job is done by _NTSrv_DoExcelExport exported routine. It is a classical use of a type library, excel9.olb. In this DLL I have selected only Application, Font, Range, Workbooks, Workbook, Worksheets and Worksheet objects. After this, the steps are: create an application, get application workbooks, add a new workbook, get a new sheet from sheets set, format 3 columns and fill with data enumerated with EnumServicesStatus. In fact it is the same stuff you can find on MSDN site, automation section (HOWTO: Use MFC to Automate Excel & Create/Format a New Workbook – Article ID: Q179706).

8. Finally, the task scheduling. In the known manner, _NTSrv_ManageScheduleTask exported routine will call a dialog box for us. This is another example of browsing, but this time using interfaces ITaskScheduler, ITask (keeped as LPARAM in list control), IEnumWorkItems. If you have already used IShellFolder browsing techniques, this will appear natural.

For tasks folder and log file, the routine seek local computer. Use RegConnectRegistry to get data from the remote machine.


void CTasksDlg::Init()
{
InitToolbar();

CImageList il;
il.Create(IDB_TASK, 16, 0, COLORREF(RGB(255, 255, 255)));
m_listTasks.SetImageList(&il, LVSIL_SMALL);
il.Detach();

m_listTasks.SetExtendedStyle(LVS_EX_GRIDLINES | LVS_EX_HEADERDRAGDROP | LVS_EX_SUBITEMIMAGES);

m_listTasks.SetBkColor(COLORREF(RGB(200, 200, 200)));
m_listTasks.SetTextBkColor(COLORREF(RGB(200, 200, 200)));

m_listTasks.InsertColumn(0, TEXT(“Name”) , LVCFMT_LEFT, 100);
m_listTasks.InsertColumn(1, TEXT(“Schedule”) , LVCFMT_LEFT, 150);
m_listTasks.InsertColumn(2, TEXT(“Next Run Time”) , LVCFMT_LEFT, 150);
m_listTasks.InsertColumn(3, TEXT(“Last Run Time”) , LVCFMT_LEFT, 150);
m_listTasks.InsertColumn(4, TEXT(“Status”) , LVCFMT_LEFT, 50);
m_listTasks.InsertColumn(5, TEXT(“Last Result”) , LVCFMT_RIGHT, 50);
m_listTasks.InsertColumn(6, TEXT(“Creator”) , LVCFMT_LEFT, 50);
m_listTasks.InsertColumn(7, TEXT(“Comment”) , LVCFMT_LEFT, 100);

HKEY hk = 0;
LONG l = RegOpenKeyEx(HKEY_LOCAL_MACHINE, g_lpcszRegPath, 0L, KEY_READ, &hk);

if(l == NO_ERROR)
{
DWORD dwType = 0, dwDataLen = 0;

TCHAR lpszTasksFolder[_MAX_PATH + 1];
TCHAR lpszTasksLog[_MAX_PATH + 1];

dwDataLen = _MAX_PATH + 1;
l = RegQueryValueEx(hk, TEXT(“tasksfolder”), NULL, &dwType, (LPBYTE)lpszTasksFolder, &dwDataLen);
if(l == NO_ERROR)
{
strcpy(g_lpszTasksFolder, lpszTasksFolder);
}

dwDataLen = _MAX_PATH + 1;
l = RegQueryValueEx(hk, TEXT(“logpath”), NULL, &dwType, (LPBYTE)lpszTasksLog, &dwDataLen);
if(l == NO_ERROR)
{
strcpy(g_lpszTasksLog, lpszTasksLog);
}

RegCloseKey(hk);
}
else
ShowLastError();

int count = m_listTasks.GetItemCount();

HRESULT hr = 0;

ULONG ulFetched = 0L;
IEnumWorkItems *pEWI = 0;

_com_error ce(hr);

USES_CONVERSION;
LPCWSTR lpw = A2W(TEXT(“\\\\”) + m_strWksta);
LPWSTR *lpwName = 0;

hr = CoInitialize(NULL);
if(FAILED(hr))
goto _out_;

hr =
CoCreateInstance
(
CLSID_CTaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskScheduler,
(LPVOID *)&m_pITS
);
if(FAILED(hr))
goto _out_;

hr = m_pITS->SetTargetComputer(lpw);
if(FAILED(hr))
goto _out_;

hr = m_pITS->Enum(&pEWI);
if(FAILED(hr))
goto _out_;

hr = pEWI->Reset();
if(FAILED(hr))
goto _out_;

while(S_OK == pEWI->Next(1, &lpwName, &ulFetched) && ulFetched)
{
for(register unsigned long x = 0; x < ulFetched; x++) { ITask *pT = 0; hr = m_pITS->Activate(*(lpwName + x), IID_ITask, (LPUNKNOWN *)&pT);
if(SUCCEEDED(hr))
{
m_listTasks.InsertItem(count, _T(“”));

LV_ITEM lvItem;
TCHAR lpszName[1024];

lstrcpy(lpszName, W2A(lpwName[x]));
lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iImage = 2;
lvItem.iItem = count;
lvItem.iSubItem = 0;

// Name
lvItem.lParam = (LPARAM)pT;
m_listTasks.SetItem(&lvItem);

LPWSTR lpwCreator = 0;
LPWSTR lpwComment = 0;
SYSTEMTIME st;
COleDateTime od;
CString str;

// schedule

// next run time
hr = pT->GetNextRunTime(&st);
od = COleDateTime(st);
if(od.m_status == COleDateTime::valid)
str = od.Format();
else
str = _T(“”);
lstrcpy(lpszName, (LPCTSTR)str);
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 2;
m_listTasks.SetItem(&lvItem);

// last run time
hr = pT->GetMostRecentRunTime(&st);
od = COleDateTime(st);
if(od.m_status == COleDateTime::valid)
str = od.Format();
else
str = _T(“”);
lstrcpy(lpszName, (LPCTSTR)str);
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 3;
m_listTasks.SetItem(&lvItem);

// status
HRESULT hrStatus = 0;
hr = pT->GetStatus(&hrStatus);
lstrcpy(lpszName, (LPCTSTR)(hrStatus == SCHED_S_TASK_READY ? “Ready” :
(hrStatus == SCHED_S_TASK_RUNNING ? “Running” : “Not scheduled.”)));
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 4;
m_listTasks.SetItem(&lvItem);

// exit code
DWORD dwExitCode = 0;
hr = pT->GetExitCode(&dwExitCode);
sprintf(lpszName, “%x”, dwExitCode);
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 5;
m_listTasks.SetItem(&lvItem);

// creator
hr = pT->GetCreator(&lpwCreator);
lstrcpy(lpszName, W2A(lpwCreator));
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 6;
m_listTasks.SetItem(&lvItem);

// comment
hr = pT->GetComment(&lpwComment);
lstrcpy(lpszName, W2A(lpwComment));
lvItem.mask = LVIF_TEXT;
lvItem.pszText = lpszName;
lvItem.cchTextMax = lstrlen(lpszName);
lvItem.iSubItem = 7;
m_listTasks.SetItem(&lvItem);

count++;
}

CoTaskMemFree(*(lpwName + x));
*(lpwName + x) = 0;
}

CoTaskMemFree(lpwName);
lpwName = 0;
}

goto _release_; // if everything is ok

_out_:
ce = _com_error(hr);
_MsgError(NULL, ce.ErrorMessage());

EndDialog(0);

_release_:
if(pEWI)
{
pEWI->Release();
pEWI = 0;
}

CoUninitialize();
}


For executing a task, remember keeping of an ITask* in the LPARAM for column 0 of the list. So, we simply do the job with this pointer:

void CTasksDlg::OnRun()
{
int nSel = m_listTasks.GetNextItem(-1, LVNI_SELECTED);
if(nSel >= 0)
{
LVITEM lvItem;
lvItem.iItem = nSel;

if(m_listTasks.GetItem(&lvItem))
{
ITask *pT = (ITask *)(LPARAM)(lvItem.lParam);

if(pT)
{
IScheduledWorkItem *pSWI = 0;

HRESULT hr = pT->QueryInterface(IID_IScheduledWorkItem, (LPVOID *)&pSWI);
if(SUCCEEDED(hr) && pSWI)
{
hr = pSWI->Run();

if(SUCCEEDED(hr))
_MsgInfo(m_hWnd, _T(“Task launched succesfully.”));

pSWI->Release();
pSWI = 0;
}
}
}
}
}


Editing handled manually can be quite complicated (believe me, because I wrote manually a task scheduler and the configuration dialog has no less than 93 controls…). But we can use task property page directly, via IProvideTaskPage interface. And again, the rest is history. This sample provides all property pages (TASKPAGE_TASK, TASKPAGE_SCHEDULE, TASKPAGE_SETTINGS), but nobody forces you. Simply pass to the PROPSHEETHEADER structure the appropriate array of HPROPSHEETPAGE and the correct size. A special mention is for the lucky owners of NT 5.0: please test the blind code marked by #if (WINVER >= 0x0500), to check if security property page appears. I have only NT 4.0 and I can’t do this, so if someone could tell me if it worked…

void CTasksDlg::OnOpen()
{
int nSel = m_listTasks.GetNextItem(-1, LVNI_SELECTED);
if(nSel >= 0)
{
LVITEM lvItem;
char lpszBuffer[256];
ITask *pT = 0;

ZeroMemory(&lvItem, sizeof(LVITEM));

lvItem.mask = LVIF_TEXT | LVIF_PARAM;
lvItem.iItem = nSel;
lvItem.iSubItem = 0;
lvItem.pszText = lpszBuffer;
lvItem.cchTextMax = 256;
lvItem.lParam = (LPARAM)(ITask *)pT;

char lpszName[256];

if(m_listTasks.GetItem(&lvItem))
{
sprintf(lpszName, “%s\\\”%s\””, (LPCTSTR)m_strWksta, lpszBuffer);

HRESULT hr = 0;
pT = (ITask *)(LPARAM)(lvItem.lParam);

if(pT)
{
IProvideTaskPage *pTP = 0;
PROPSHEETHEADER PropSheetHeader;
HPROPSHEETPAGE PSP_Task[4];

hr = pT->QueryInterface(IID_IProvideTaskPage, (LPVOID *)&pTP);
if(SUCCEEDED(hr) && pTP)
{
hr = pTP->GetPage(TASKPAGE_TASK , TRUE, &PSP_Task[0]);
hr = pTP->GetPage(TASKPAGE_SCHEDULE , TRUE, &PSP_Task[1]);
hr = pTP->GetPage(TASKPAGE_SETTINGS , TRUE, &PSP_Task[2]);

PropSheetHeader.dwSize = sizeof(PROPSHEETHEADER);
PropSheetHeader.dwFlags = PSH_DEFAULT;
PropSheetHeader.hwndParent = NULL;
PropSheetHeader.hInstance = NULL;
PropSheetHeader.pszCaption = lpszName;
PropSheetHeader.phpage = PSP_Task;
PropSheetHeader.nStartPage = 0;

#if (WINVER >= 0x0500)
ISecurityInformation *pSI = 0;
hr = pTP->QueryInterface(IID_ISecurityInformation, (LPVOID *)&pSI);
if(SUCCEEDED(hr))
{
pSI->CreateSecurityPage(pSI);
PropSheetHeader.nPages = 4;
}
#else
PropSheetHeader.nPages = 3;
#endif

PropertySheet(&PropSheetHeader);
}
}
}
}
}

Downloads

Download Windows NT Quick Service Manager (source code) – 193 KB
Download Task Scheduler Subsystem – 19 KB
Download MSOffice Excel Subsystem – 31 KB
Download Windows NT Quick Service Manager (zipped executable) – 69 KB

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read