Download demo project - 38 KbDownload source files - 36 Kb
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
#ifndef __CLSID_MkExeSvc_Defined__
#define __CLSID_MkExeSvc_Defined__
DEFINE_GUID(CLSID_MkExeSvc, 0xed7f189f, 0xf6ca, 0x4513, 0x93, 0x56, 0x84, 0x54, 0xb8, 0x1, 0x75, 0x25);
#endif
where CLSID_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 in
extern "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 the g_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
and DllUnregisterServer
) 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 REGSTRUCT
typedef 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;
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
{
STDMETHODIMP QueryInterface(REFIID riid, LPVOID FAR *ppv);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
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);
STDMETHODIMP Initialize(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj, HKEY hKeyID);
ULONG m_cRef;
LPDATAOBJECT m_pDataObj;
UINT m_xFileCount;
LPTSTR *m_ppszFileUserClickedOn;
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 the m_pDataObj
member variable: starts EnumFormatEtc
, get the item, set the
CF_HDROP
format for the FORMATETC
and pass it to GetData
method of IDataObject
to obtain, in STGMEDIUM
's
hGlobal
member, which is a HDROP
used to call DragQueryFile
. Although the interface keeps, in m_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 a LPTSTR
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 a QUERY_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
's lpBinaryPathName
member with your executable name which always resides in m_ppszFileUserClickedOn[0]
.
- you get a hit? add to your LPTSTR parameter and append a
'#'. This is what you receive returning from
GetServicesByPath
in ppSvcNames
: 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
(or strtok
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
I expect serious enhancements from you. Remember: this is only a sample.
This code is freeware, but, although I don't believe I'm one of the essentials that you must mention, it would be nice to remaind my
contribution, if exists. Also me I'm borrowing code/ideas all the time, rewriting/modifying routines to enhance existing ones. That's the reason of Codeproject,
isn't it?
Don't take my code as it is. I already mentioned the word 'gateway'. So this article can be a starting point,
but I'm sure that code has bugs and can seriously damage your machine if you delete a vital service. So, I'm assuming that you won't blame me if something happens. If you are not
sure about what a service is, it's better to comment, for example, DeleteService
and replace it with _stOutputDebugString("DeleteService %s", pszService).
The views expressed above are those of the author alone.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.