Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C++
Article

Shell extension to make executables as services

Rate me:
Please Sign up or sign in to vote.
3.22/5 (6 votes)
8 Apr 2000 89K   1.9K   41   10
A 'not-so-simple' shell extension allowing executable to be run as services
  • Download demo project - 38 Kb
  • Download source files - 36 Kb
  • Sample Image - mkexesvc.gif

    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:

    1. 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
      where CLSID_MkExeSvc comes from, as you probably guessed already, "Make Executable As Services".

    2. 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).

    3. 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;
    		//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.

    1. 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

    2. ...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:
    1. 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);
    2. 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;
    3. call OpenService for every service returned;
    4. 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;
    5. compare QUERY_SERVICE_CONFIG's lpBinaryPathName member with your executable name which always resides in m_ppszFileUserClickedOn[0].
    6. 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:

    New service

    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:

    Resolve services conflict: multiple services in the same executable

    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.
  • License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here


    Written By
    Web Developer
    Romania Romania
    This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

    Comments and Discussions

     
    JokeWoW This Is Old Now Pin
    easter_200720-Feb-06 19:52
    easter_200720-Feb-06 19:52 
    QuestionUninstall? Pin
    naveed22-Nov-04 6:40
    naveed22-Nov-04 6:40 
    GeneralDeveloping NT services Pin
    5-Apr-01 4:29
    suss5-Apr-01 4:29 
    GeneralRe: Developing NT services Pin
    Sardaukar5-Apr-01 18:29
    Sardaukar5-Apr-01 18:29 
    QuestionNot all exe can be services ? What are the requirements ? Pin
    Benjamin Mayrargue26-Oct-00 23:35
    Benjamin Mayrargue26-Oct-00 23:35 
    AnswerRe: Not all exe can be services ? What are the requirements ? Pin
    Sardaukar27-Oct-00 0:55
    Sardaukar27-Oct-00 0:55 
    GeneralNo, the acticle's name is not good. Pin
    Benjamin Mayrargue27-Oct-00 3:06
    Benjamin Mayrargue27-Oct-00 3:06 
    GeneralATL "Service" Wizard... Pin
    Benjamin Mayrargue27-Oct-00 6:48
    Benjamin Mayrargue27-Oct-00 6:48 
    GeneralMore Infos about environment! Pin
    Member 58585939-Apr-00 23:34
    Member 58585939-Apr-00 23:34 
    GeneralRe: More Infos about environment! Pin
    Sardaukar10-Apr-00 1:37
    Sardaukar10-Apr-00 1:37 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.