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


Comments

  • Remote process listing ?

    Posted by Legacy on 01/15/2004 12:00am

    Originally posted by: kiran

    hi,
    Good , it enumerates services on local machine, but i need some way to enumerate services of remote machine
    kiran

    • thanks

      Posted by catarrsys on 12/27/2007 03:36am

      thanks

      Reply
    Reply
  • Access PROPSHEETPAGE by HPROPSHEETPAGE

    Posted by Legacy on 02/27/2002 12:00am

    Originally posted by: Yi Qin

    Any one know if there is a way to access the PROPSHEETPAGE identified by the HPROPSHEETPAGE, or any way to modify the pages before the PROPSHEETHEADER structure is passed to PropertySheet.

    • My solution

      Posted by bird_alone on 08/15/2010 08:29am

      I had a same problem.You can use this little code:
      // Get the page.
            LPPROPSHEETPAGE pPage0 = (LPPROPSHEETPAGE)rSheet.IndexToPage( nCurrentPage );
      	  LPPROPSHEETPAGE pPage = (LPPROPSHEETPAGE)(pPage0->pcRefParent+8);
      
      pPage is correct pointer to LPPROPSHEETPAGE.
      ;)

      Reply
    Reply
  • Why not recursively ??!!

    Posted by Legacy on 12/18/2001 12:00am

    Originally posted by: WeberMenschi

    He!

    After you detected ERROR_DEPENDENT_SERVICES_RUNNING, why don't you call the function DoControlOperation recursively?

    greetings
    webermenschi

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: September 16, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you starting an on-premise-to-cloud data migration project? Have you thought about how much space you might need for your online platform or how to handle data that might be related to users who no longer exist? If these questions or any other concerns have been plaguing you about your migration project, check out this eSeminar. Join our speakers Betsy Bilhorn, VP, Product Management at Scribe, Mike Virnig, PowerSucess Manager and Michele …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds