Shell extension to make executables as services






3.22/5 (6 votes)
Apr 9, 2000

89978

1926
A 'not-so-simple' shell extension allowing executable to be run as services
I have seen that "Shell extensions" are something interesting for the readers. Following the excellent "The Complete Idiot's Guide to Write Shell Extensions" (Parts I, II, and III), I decided to support this guide (a good starting point for a complete reference of shell, I think...) presenting a shell extension growing from zero to an end. More precisely, it is about executables as services, a topic with more and more fans. There are some nice tools for allowing, in one way or another a way of running an executable before the logon screen, allowing boring tasks to be run with minimum of effort.
1. What we need for establishing a skeleton of our shell extension
Although covered in the 3 articles mentioned above, let's see them again:
- A GUID. Run guidgen from command line (if cmd.exe complains it
doesn't know where is guidgen, set the MSVC 6.0 environment variables running a
.bat file - mine is called vc.bat- contaning two
lines:
@echo off "C:\program Files\Microsoft Visual Studio\VC98\Bin\vcvars32.bat
(or whatever your MSVC binaries are located). After that, choose 2nd option, DEFINE_GUID, and you'll get something like// {ED7F189F-F6CA-4513-9356-8454B8017525} #ifndef __CLSID_MkExeSvc_Defined__ #define __CLSID_MkExeSvc_Defined__ DEFINE_GUID(CLSID_MkExeSvc, 0xed7f189f, 0xf6ca, 0x4513, 0x93, 0x56, 0x84, 0x54, 0xb8, 0x1, 0x75, 0x25); #endif
whereCLSID_MkExeSvc
comes from, as you probably guessed already, "Make Executable As Services".
- A
DllMain
entry point function.DWORD dwReason
parameter will indicates if we're in (DLL_PROCESS_ATTACH) or out (DLL_PROCESS_DETACH). My suggestion is to perform all initialization when ..._ATTACH and cleanup in ..._DETACH, as inextern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { UNREFERENCED_PARAMETER(lpReserved); if(dwReason == DLL_PROCESS_ATTACH) { g_hDll = hInstance; LoadResources(); } else if (dwReason == DLL_PROCESS_DETACH) { UnloadResources(); } return 1; }
(Loading resources from start and unloading at end is my habit - follow me if you like this).
- A .def file with the classical 4 routines (where you
export also other functions and/or variable). In this case, it's
;mkexesvc.def : Declares the module parameters for the DLL. LIBRARY mkexesvc DESCRIPTION 'Executable as service Shell Context Extension' EXPORTS DllRegisterServer PRIVATE ; COM server registration DllUnregisterServer PRIVATE ; COM server deregistration DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE
The last two are responsable with unloading (DllCanUnloadNow
- a simple test of theg_cDllRef
- DLL counter - global variable):STDAPI DllCanUnloadNow(void) { return g_cDllRef == 0 ? S_OK : S_FALSE; }
and the creation of the class factory object:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppvOut) { HRESULT hr = CLASS_E_CLASSNOTAVAILABLE; *ppvOut = NULL; if(IsEqualIID(rclsid, CLSID_MkExeSvc)) { ISvcExClassFactory *pcf = new ISvcExClassFactory; hr = pcf->QueryInterface(riid, ppvOut); } return hr; }
The other two twins (
DllRegisterServer
andDllUnregisterServer
) are inserting/deleting in theRegistry the necessary entries. The first entry to be inserted is the CLSID entry, and the second is the one who extends context menu for executables. Both are using the structure REGSTRUCTtypedef struct _REGSTRUCT { HKEY hRootKey; LPTSTR lpszSubKey; LPTSTR lpszValueName; LPTSTR lpszData; } REGSTRUCT, *LPREGSTRUCT;
to perform the operations. The CLSID array to insert/create is:REGSTRUCT ShExClsidEntries[] = { HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"), NULL, TEXT(DLLDESCNAME), HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InProcServer32"), NULL, TEXT("%s"), HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InProcServer32"), TEXT("ThreadingModel"), TEXT("Apartment"), NULL, NULL, NULL, NULL };
and for executables the HKEY_CLASSES_ROOT's exefile type has to be enhanced using a similar array:REGSTRUCT OtherShExEntries[] = { HKEY_LOCAL_MACHINE, TEXT("software\\classes\\clsid\\"DLLDESCNAME), NULL, TEXT("%s"), HKEY_CLASSES_ROOT, TEXT("exefile\\shellex\\ContextMenuHandlers\\"DLLDESCNAME), NULL, TEXT("%s"), NULL, NULL, NULL, NULL };
Of course, under WindowsNT/2000 we have to register the CLSID as 'Approved' extension, inserting a new value under the HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved key.
2. The interfaces
We mentioned the class factory above. Among the constructor, destructor,
QueryInterface
, AddRef
, Release
and
LockServer
, which are the common routines you see all the time, remains
the CreateInstance
, the gateway to our core object
delegated for manage the context menu and the associated handler: the interface
ISvcExShellExt
:
STDMETHODIMP ISvcExClassFactory::CreateInstance(LPUNKNOWN pUnkOuter, REFIID riid, LPVOID *ppvObject) { HRESULT hr = S_OK; LPISVCEXCSHELLEXT pShellExt = (LPISVCEXCSHELLEXT)NULL; *ppvObject = NULL; if(pUnkOuter ! = (LPUNKNOWN)NULL) hr = CLASS_E_NOAGGREGATION; else { pShellExt = new ISvcExShellExt; //here we are... if(pShellExt == (LPISVCEXCSHELLEXT)NULL) hr = E_OUTOFMEMORY; } if(hr == S_OK) hr = pShellExt->QueryInterface(riid, ppvObject); return hr; }
So, we manage to get the pShellExt
pointer to our...
ISvcExShellExt interface
This interface must implement, among IUnknown
methods, other two interfaces: IContextMenu
(or a version, let's say
IContextMenu2
, if you have
owner-draw menus involved, or even IContextMenu3
, for
WM_MENUCHAR processing ).
interface ISvcExShellExt : public IContextMenu, public IShellExtInit { // IUnknown STDMETHODIMP QueryInterface(REFIID riid, LPVOID FAR *ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IContextMenu STDMETHODIMP QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags); STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi); STDMETHODIMP GetCommandString(UINT idCmd, UINT uFlags, UINT FAR *reserved, LPSTR pszName, UINT cchMax); // IShellExtInit STDMETHODIMP Initialize(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj, HKEY hKeyID); ULONG m_cRef; LPDATAOBJECT m_pDataObj; UINT m_xFileCount; // count of selected files LPTSTR *m_ppszFileUserClickedOn; // [MAX_PATH] STDMETHODIMP Install(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd); STDMETHODIMP Uninstall(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd); STDMETHODIMP Open(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd); STDMETHODIMP About(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd); STDMETHODIMP GetFileNames(); void DeleteFileData(); BOOL GetServicesByPath(LPTSTR *ppSvcNames, int *pnIndex); ISvcExShellExt(); ~ISvcExShellExt(); }; typedef ISvcExShellExt *LPISVCEXCSHELLEXT;If the main purpose of
Initialize
is to initialize the m_pDataObj
member, passing the LPDATAOBJECT pDataObj
parameter,
the implementation of IContextMenu
is by far more interesting. There are three
methods to implement: QueryContextMenu
,
InvokeCommand
and GetCommandString
. Let's take a closer look.
QueryContextMenu
QueryContextMenu
inserts submenu items under the g_hSubMenu
popup menu constructed by LoadResources
in
DllMain
's DLL_PROCESS_ATTACH branch. All the procedure is, more or less, the same as in
Dll Registerer/Unregisterer,
except one thing: we have to enable Install menu if the selected executable is
not an installed service on the local computer (and disable Open
and Uninstall). And, of course, to disable Install and
enable the other two options if the selected item is an installed service.
For ensuring this menu validation task, we have to perform other two tasks. One is to establish the full path of the executable, and the other is to search in the services' database for a service (or more!) residing in it.
- Establishing the selected file is ensured by
GetFileNames
, also presented already in Dll Registerer/Unregisterer. The main role is played here by them_pDataObj
member variable: startsEnumFormatEtc
, get the item, set theCF_HDROP
format for theFORMATETC
and pass it toGetData
method ofIDataObject
to obtain, inSTGMEDIUM
'shGlobal
member, which is aHDROP
used to callDragQueryFile
. Although the interface keeps, inm_ppszFileUserClickedOn
, an array of executable filenames selected, we allow (for the moment) this context menu only if is selected exact ONE file (m_xFileCount == 1
).All of which we have to do to ensure the second job
- ...which is to search the entire services' database for services residing in this executable. The routine is called
GetServicesByPath
. Pass always the address of aLPTSTR
initialized with NULL and the address of an integer initialized with -1. The routine returns TRUE if at least a service resides inside the selected executable, and FALSE if there is no service installed with this executable path, or the SCM database operations failed. When you get a TRUE, the integer index is 0 if there is only one service inside, or more if there are many (for example, on Windows2000, check%WINDIR%\system32\services.exe
- where%WINDIR%
will be in most of the cases C:\WINNT - and you'll see that are no less that 17 services inside this executable!).
So, let's follow the idea:
- open SCM database with
OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS
) - maybe it's not necessary to have all access, but I suppose you are an Administrator (or run until the real one appears on your back); - enumerate services
(
EnumServicesStatus
); if you get ERROR_MORE_DATA error, reallocate the passed LPBYTE buffer and retry; if you get ERROR_NO_MORE_ITEMS, that was it; - call
OpenService
for every service returned; - call
QueryServiceConfig
to get aQUERY_SERVICE_CONFIG
pointer we're hunting; here I'm allocating more than necessary to ensure a succesful call from the allocation point; - compare
QUERY_SERVICE_CONFIG
'slpBinaryPathName
member with your executable name which always resides inm_ppszFileUserClickedOn[0]
. - you get a hit? add to your LPTSTR parameter and append a
'#'. This is what you receive returning from
GetServicesByPath
inppSvcNames
: a list of services' names separated by '#'. (I don't think '#' appears in the name of a service.).
Note. The parsing of this list is done using the totally multithreading-unrecommended_tcstok
(orstrtok
if you like ANSI) call. If you have more time to write a mtstrtok ("MultiThreaded String Tokenizer") I'll be happy to replace mine, which I don't like at all.
Let's suppose now our executable is not a service, but a simply .exe we
decided to honour with service rank choosing the Install option from our menu. A parameter passed to DialogBoxParam
.
We'll receive the management dialog box:
I suggest you read carefully the documentation for how to install a service. Reading
the CreateService
routine from MSDN is a good starting
point. Anyway, for a simple description, press the help button of the dialog (or
F1 ) and you'll receive a short
description, as in the image above.
The same dialog box you'll receive when you Open a service. The only
difference is that when you right-click such an executable as services.exe,
the DLL will find more than one service inside (the pnIndex parameter of GetServicesByPath
willl contain on return the value 16 - being 0-based - in this case) and
will ask you to resolve the conflict between services sharing the same exe:
Press Cancel if you want to dismiss operation, and OK if you choose a service to edit.
Notes
DeleteService
and replace it with _stOutputDebugString("DeleteService %s", pszService).