Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Remote Reboot Shell Extension


Remote Reboot Shell Extension

Download the code for this issue

As someone with more machines than monitors, I use an active keyboard/screen/mouse switcher. I'm also known to roam about the home with my laptop when the ambient noise from the kids exceeds a tolerable threshold, plugging into the various ports I've had installed. Whether you work in similar conditions or operate in a large distributed office, it can be a bit of a pain when you need to shutdown or reboot another machine. Rather than having to switch to an active monitor, or log on to pcAnywhere, or walk across the office, wouldn't a simple mouse click be nicer? (Of course, physiologists would say that a walk across the office is exactly what is needed to forestall our drooping statures, but that's another issue.)

A few years ago, I wrote a control panel applet that allows one to reboot/shutdown remote hosts, and that served its purpose nicely. However, since I am a big fan of shell extensions — and provide a number of them for free at http://shellext.com/ — I thought it might be nice to write one that provides shutdown/reboot of a remote host via an Explorer shortcut. This article will describe the main technical aspects of the solution — the Remote Reboot context menu handler shell extension — and highlight some issues one must consider when creating such shell extensions. The implementation is largely ATL, with various STLSoft (my project for bringing STL to the masses, located at http://stlsoft.org/) and WTL (see the online sidebar "WTL & ATL") components thrown in for good measure. The finished component is available for free along with the other Synesis Software Shell Extensions (from http://shellext.com/) from Version 1.5.1 onwards.

Rebooting a Remote Host

Rebooting a network server is pretty straightforward. You call the Win32 function InitiateSystemShutdown(), passing the host name, a timeout, and specifying whether to reboot rather than shutdown, and whether to force application closure. You also pass a message string that will be displayed on the remote host during the period between the start of the shutdown and the machine actually shutting down, as can be seen in Figure 1.

There are two issues we must face when using this function. First, InitiateSystemShutdown() is only supported on NT-family (NT4, 2000, XP) systems; on 95-family (95, 98, Me) systems it simply returns a failure code. Second, you must have appropriate permissions to affect the shutdown. Specifically, you need the SE_REMOTE_SHUTDOWN_NAME privilege on any remote hosts that you wish to close. If you don't have this, then the Remote Reboot shell extension is not going to work for you; it will report an Access Denied message box. Since the rights and privileges associated with your logon identity are written into your user token at log on, and do not change during the course of your user session, any changes made on your behalf by the system administrators will require you to log off. (In such circumstances it'll probably be less hassle to walk over to the machine and reboot it manually.) For those who administer their own systems, the remote shutdown right is set by adding the requisite user/group to the "Force shutdown from a remote system" right as shown in Figure 2, which shows an NT 4 server dialog.

Context Menu Handler Shell Extensions

Context menu handler shell extensions, like all active shell extensions, are in-process COM servers that implement certain interfaces and provide, upon registration, certain registry entries. When the user right-clicks on one or more items within Explorer, on the desktop, or within standard File dialogs (GetOpenFileName(), GetSaveFileName()), the registry entries for the particular file type(s) are consulted and the appropriate context menu handler shell extensions loaded and initialized. For example, when right-clicking on the file "kernel32.dll," the registry will be searched for at least the keys HKEY_CLASSES_ROOT\dllfile\shellex\ContextMenuHandlers and HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers. If either of these keys exist, and have subkeys, the GUIDs in the default values of the subkeys represent the CLSIDs of the context menu handlers to be loaded and activated.

For network server shortcuts, the requisite key is NetServer, so your context menu handler must install under HKEY_CLASSES_ ROOT\NetServer\shellex\ContextMenuHandlers. At this point it is worth noting that we are talking about right-clicking on shortcuts to network servers. The extension here does not operate on the items within "Network Neighborhood"/"My Network Places," which I presume is another kind of shell extension — a namespace extension. (And that's another story...)

A context menu handler will, at minimum, support the interfaces IContextMenu and IShellExtInit. (There are additional interfaces IContextMenu2 and IContextMenu3 that help with custom drawing of the menu items.) The definitions of these interfaces (from ShlObj.h) are shown in Listing 1.

IShellExtInit::Initialize()

IShellExtInit::Initialize() is implemented by various shell extension types: property sheet handlers, drag-and-drop handlers, and context menu handlers, which are what we'll be talking about here.

Upon initialization, the shell extension is passed data from the shell via the IDataObject instance passed as the second parameter in IShellExtInit::Initialize(). Most of the shell extensions I've previously written have operated on filesystem types such that the shell provided data formats (in the IDataObject instance) that included the clipboard format CF_HDROP, which denotes a drop handle (HDROP), along with the shell-specific formats of Shell IDList Array, FileName, and FileNameW. The latter three are custom clipboard formats registered by the shell with the Win32 function RegisterClipboardFormat(). To use them you should call RegisterClipboardFormat() yourself, which will return a UINT representing the system-wide ID of the format. If it is not already registered, your call will register it (though this is unlikely since the shell itself registers them at startup).

Since working with a drop handle is very straightforward, this has been my preferred approach until now. A drop handle is an opaque handle that represents a system-managed set of paths. It is retrieved from an IDataObject instance using code such as that shown in Listing 2 (available online), which shows the implementation of a helper function I use for this purpose. (It resides in a C file, but is written to be compatible with C++ compilation if included into a C++ project file.) The function populates a FORMATETC structure describing the type of data required (CF_HDROP) and how it is to be received (TYMED_HGLOBAL), and passes a STGMEDIUM structure in which the data will be written.

Once you have a drop handle, you can access the paths it represents by calling DragQueryFile(), which places a specific path according to a given index into a caller-supplied buffer, or returns the number of files for the sentinel index value 0xFFFFFFFF. Once you've finished with the handle, you must call DragFinish() to release the resources.

For those who are comfortable with STL, the use of the handle can be simplified by using WinSTL's basic_drophandle_sequence class, as shown in Listing 3 (available online). (WinSTL is the Win32-related subproject of STLSoft, located at http://winstl.org/.)

Unfortunately, the CF_HDROP format is not provided for all filesystem types. The NetServer type, which is what we need to use for network servers, does not come with CF_HDROP. The MSDN documentation provides very little help on this (try a search for "+NetServer +shell"), so I resorted to some practical measures. IDataObject instances are able to report all their accessible formats in a COM enumerator implementing IEnumFORMATETC, which is retrieved via the EnumFormatEtc() method. Hence, I used a helper function DumpFormats() (Listing 4, available online) to trace all the formats for the data object. As you can see, the function uses a COMSTL template, enum_simple_sequence, which provides a parameterized mapping from a COM enumerator (something implementing one of the IEnumXXXX interfaces) to an STL-compliant sequence providing Input or Forward Iterator semantics. (COMSTL is the STLSoft subproject pertaining to COM, located at http://comstl.org/.) Combining this with the trace_FORMATETC function object provides a neat and simple mechanism of tracing the supported clipboard formats for our IDataObject instance.

Once I plugged this in, I was able to determine that the NetServer shell type provides the registered clipboard formats Shell IDList Array, FileName, FileNameW, and Net Resource. (Table 1 shows the various formats supported for the different types.)

Since the STGMEDIUM structure contains the union member lpszFileName (of type LPOLESTR), I decided to have a go at retrieving the filename as a (Unicode) string, rather than worry about the other formats. This worked well, but there are two (and a bit) problems. First, it transpires that the operating system on which I was working, XP, is the only one of the NT family that provides the FileName and FileNameW formats for NetServer types; as soon as I went to test on NT 4 or 2000, the shell extension silently failed to do anything. I think that such inconsistencies are quite inappropriate, but that's the world of shell extensions: There's a great deal of variation over the various Win32 incarnations. (The partial problem is that FileNameW is only provided on NT-family systems, albeit that's irrelevant for this shell extension, as we've seen that remote shutdowns can only be done from NT family machines.) The more serious problem is that FileNameW returns only a single filename, so if we had multiple selections only one would appear.

So FileNameW not being appropriate, and Net Resource documented to also return only a single path name, I decided to bite the bullet and deal with the Shell IDList Array format. Alas, it turns out that for NetServer types, even this format returns only one item.

When you have more than one server shortcut selected, it is the one with the caret whose name is passed through, whether that is in Shell IDList Array, Net Resource, or FileNameW formats. I guess this makes sense as far as it goes, and in this case is not contradictory; I would not want to write the extension to be able to shutdown multiple machines simultaneously as it is a very serious thing to be doing on one machine, never mind several at a time. However, it is conceivable that one may write extensions to do many useful (and benign) operations with network servers, in which case this restriction would be onerous. Moreover, despite not wanting to remotely reboot multiple machines, we still have a problem, as we do not want the reboot context menu items to appear when multiple machines are selected since we have no control over (and the user would have no idea as to) which of the selected machines would be operated on. Nasty.

I was able to find nothing in the shell extension documentation to help out here, so my somewhat hacked solution is to call GetFocus(), which retrieves the window in the current thread that has the focus. Since the shell extensions are in-process COM servers, they operate within the shell process (Explorer.exe) and of course, the window that has the focus is the one in which the selections have been made and right-clicked. Whether in one of Explorer's SDI tree-list windows or the desktop itself, the window concerned is a list view ("SysListView32"), so my solution is to send the focused window the LVM_GETSELECTEDCOUNT message. If the result is greater than one, then there are multiple items selected. If the result is 0, then the window is not a list view and the user is probably using a custom shell process, within which we're not going to be able to operate anyway. In either case, the E_FAIL code is returned from IShellExt::Initialize() and the shell will not then proceed to call the methods of IContextMenu and the menu items are not shown. Only when the selection count is one does the initialization proceed.

The remainder of the method (Listing 5) shows how to extract the path information from the Shell IDList Array format, which provides a memory block in the hGlobal member of the STGMEDIUM structure containing a CIDA structure, which is defined as

typedef struct _IDA {
    UINT cidl;
    UINT aoffset[1];
} CIDA, * LPIDA;

This innocent-looking structure definition belies a complex and troublesome nature. It is actually used to represent a contiguous layout of ITEMIDLISTs. aoffset is an array, of dimension 1 + cidl, of offsets into the block where the ITEMIDLISTs reside. The CIDA always contains an entry for the parent folder of the items concerned, so cidl represents only the number of child items. The parent ITEMIDLIST is located immediately aoffset[0] bytes from the start of the block. Each child item n is located at aoffset[1 + n] bytes from the start of the block. All this mind-numbing stuff can be more easily handled by using the macros HIDA_GetPIDLFolder() and HIDA_GetPIDLItem() suggested in the MSDN help (they do not appear in headers, hence their inclusion in the implementation file). Since we are dealing with only one child item, we just retrieve the parent folder and item 0.

When writing shell extensions (or other code that operates with shell structures), obtaining the filesystem path from ITEMIDLISTs is as simple as calling SHGetPathFromIDList(); passing in a pointer to the list and a pointer to a character buffer (of sufficient size to handle any valid path). Unfortunately, obtaining the information about a network server is not as simple. We need to call SHGetDataFromIDList() on the item's ITEMIDLIST and request a NETRESOURCE structure, but that function also requires the parent folder (as an IShellFolder instance) of the given item against which to bind the data. In order to get an IShellFolder instance from an ITEMIDLIST, we need to call IShellObject::BindToObject(). But what do we call it on? The answer is the IShellObject that represents the root of the desktop namespace; in other words, the desktop folder itself, which we obtain from SHGetDesktopFolder(). (All these interfaces follow COM rules in that they must be released when finished with.)

Once we have the folder object we can now call SHGetDataFromIDList(), passing the folder, the item we want to resolve, the format we require (SHGDFIL_NETRESOURCE ), and the buffer to receive the information. More contiguous memory complexities here, so we derive from the NETRESOURCE structure and thereby add the required 1024 bytes. (You'll note the interesting trick of deriving an anonymous structure from a named one — it looks weird but works fine on most compilers, even the very canny Metrowerks CodeWarrior, but it still may not be standards compliant.)

The final step is to test for, and then remove, the double backslash from the server name. This is for cosmetic purposes really, since most network functions work well with server names that are double-backslash prefixed just as well as those that are not. Then we save the name in the m_szHost member, and return.

IContextMenu::Query-ContextMenu()

After successful initialization, the shell will then ask the shell extension for its menu items. This is done by a call to IContextMenu::QueryContextMenu(). The shell passes in the following information: the menu handle, the index of the insertion point in the menu, a range of command identifiers, and flags.

If the flags specify the value CMF_DEFAULTONLY, then the method simply returns, since we do not affect the default menu item in this context menu handler. Otherwise, we step through our list of items and insert them into the menu. For each insertion, we use the current index point and the current command identifier, each of which is then incremented. Hence, on a third item, we'll be inserting at index indexMenu + 2 with ID idCmdFirst + 2. In terms of identifying commands, context menu handler shell extensions work on an indexed basis. Thus, when the shell later calls back because the user has either selected or clicked on an item from this shell extension, it passes the index of the item, not the command ID. Furthermore, it passes an index that is relative to the absolute index (indexMenu) that it passed in the call to QueryContextMenu(). So, when the user clicks on our third item, we will get a call back to say that item 2 has been clicked. This is actually the simplest and easiest way to do it, although at first it seems a little strange (and I recall my first shell extension failing to work at all as first I'd remembered the menu ID, and then later the absolute index, before I finally got it correct).

For simple context menu handlers, I use the WTLSTL template class SimpleContextMenuHandler and its associated macros, shown in Listing 6. (WTLSTL is another subproject of STLSoft, located at http://wtlstl.org/, pertaining to WTL — see the online sidebar "WTL & ATL.") The SIMPLE_CONTEXT_MENU_ ENTRY() macros associate two string resource identifiers (one for the menu item, one for the help string) with a handler method, as shown in the class definition for the Remote Reboot handler in Listing 7. This makes it easy to internationalize the menu and help strings, and also provides a simple and neat framework within which one can focus on operations rather than infrastructure.

IContextMenu::GetCommandString()

When the user moves the mouse over a menu item that was inserted by your context menu handler, the shell will call you back via the IContextMenu::GetCommandString() method to get a help string to display (in the status bar of the Explorer window). As mentioned earlier, the given index (the cmdOffset parameter) corresponds to the position in our list of SIMPLE_CONTEXT_MENU_MAP() entries. The implementation is very straightforward: Index the item and load the string.

There is, however, a small complication. The method is declared with the parameter pszName being of type LPSTR, but in order to support Unicode systems as well as ANSI, we must cast it to LPWSTR. Even though this shell extension will work only on Unicode systems, as a general rule I like to support both, and SimpleContextMenuHandler does so by calling either LoadStringA() or LoadStringW() depending on whether GCS_HELPTEXTA or GCS_HELPTEXTW is the command type passed to the method. Of the other command types, GCS_VALIDATEA/W are not sent to context menu handlers, so we can ignore them, and this context menu handler does not support verbs, so we can ignore them also.

IContextMenu::InvokeCommand()

This is the method where everything happens but, thanks to our index entry scheme, it is the simplest. If the high word of the lpVerb member of the CMINVOKECOMMANDINFO structure passed to the method is 0, then the low word is the index of the command. We validate the index and then call the appropriate method.

CRemoteReboot

So we've covered the basics of context menu handler shell extensions. We've seen how CRemoteReboot's IShellExtInit::Initialize() method dealt with getting the data from the shell, and also how the WTLSTL SimpleContextMenuHandler class can simplify the functionality of IContextMenu for us. Now it's time to focus on the specifics of Remote Reboot itself. This simply involves the implementation of the three handler methods, as shown in Listing 8 (available online), which handle the three menu items shown in Figure 3.

OnRebootServer() and OnShutdownServer() both call the helper method ShutdownServer(), which is where all the action happens, passing True and False, respectively, to stipulate whether to reboot or just to shutdown. The second parameter of ShutdownServer() is a Boolean stipulating whether to forcibly terminate the hosts. Both handlers call the WinSTL function IsKeyPressedAsync(VK_SHIFT), which means that the user can hold the shift key down when selecting the menu item rather than having to open the Remote Reboot dialog (see Figure 4, available online) in order to effect a forced termination. Forcing reboot/ shutdown simply means that if an application on the remote host does not shutdown cleanly (i.e., because it has a dialog open), then it will be terminated. Whether you are forcing or not, any users on the remote host are likely to lose their work, so don't think about using this tool maliciously in your office unless you are looking for a swift change of scenery!

ShutdownServer() is pretty straightforward. It loads the timeout and message values for the reboot/shutdown operation from the registry. If they are not yet present, then it uses default values. Then it calls the Win32 function InitiateSystemShutdown(), which commands the given host (m_szHost, elicited in the IShellExtInit::Initialize() method) to shutdown/reboot according to the given parameters. (Actually, InitiateSystemShutdown() is erroneously prototyped to take pointers to nonconst characters for the hostname and message strings, so what is called throughout the implementation is an inline overload, defined in stdafx.h, that takes const parameters.)

If the shutdown call fails, then GetLastError() is called, and the message text is sprintf()-ed into a dialog with the MessageBox_printf() function (which I described in a "Tech Tip" in the May 2003 issue), as shown in Figure 5 (available online).

If the function succeeds, then the pending dialog (see Figure 6, available online) is shown. It operates with a timer, and provides the progress of the timeout period as a countdown and an abort button to allow the shutdown/reboot to be cancelled. It is worth noting that it is appropriate (not to say necessary) to create a modal dialog here, because Explorer creates a new (user interface) thread within which to run any activated shell extensions.

The final handler, OnReboot(), invokes the CRebootDialog, as seen in Figure 5 (available online). There's no space here to discuss its implementation in detail. It's pretty standard fare for ATL dialogs although I do make use of various STLSoft and WTL control classes to simplify the manipulation of the dialog controls, the other dialogs, and the context-sensitive help (see the online sidebar "WTL & ATL").

Registering Shell Extensions

In order to be recognized and invoked by the shell, shell extensions must be registered. Registration of the Remote Reboot shell extension is effected via an ATL registry script, shown in Listing 9.

As for any in-proc COM server, there is an entry under HKEY_CLASSES_ROOT\ CLSID, providing the InprocServer32 subkey, and the associated threading model. However, there are two other keys. Under HKEY_ CLASSES_ROOT\NetServer\shellex\ContextMenuHandlers there is an entry providing the CLSID of the shell extension. It is this entry that allows the shell to determine that this shell extension is provided for network servers.

Shell extensions can be installed on Windows 95-family systems at any user's discretion, but installing on NT-family systems requires that you have rights to write to the registry. Furthermore, on these systems, the administrator can restrict the launch of shell extensions to those on the approved list, which reside in:

HKEY_LOCAL_MACHINE
  Software
    Microsoft
      Windows
        CurrentVersion
          Shell Extensions
            Approved

All of the Synesis Software Shell Extensions include entries for the Approved section (which are benignly ignored on 95-family systems), since I want them to be available on secure systems. Registering on NT-family requires sufficient rights to be able to write to HKEY_LOCAL_MACHINE, so it may require installation by the machine's administrator.

Debugging Shell Extensions

So we've looked at how to shutdown remote systems, learned about how context menu handler shell extensions interact with the shell, how simple ones can be implemented, and how to register them. The only thing that remains is how to debug them.

Since shell extensions are in-process COM servers, they have to be debugged within a host process. Unless you have written a fully functional custom test harness (which I doubt), the host process will be Explorer itself. If you're using Visual C++, you need to set c:\winnt\explorer.exe (or whatever the equivalent path is on your system) to be the "Executable for debug session." That's only half the picture, however, since Explorer is very likely already running. Running another instance of Explorer causes the first process to open up another window, and the second process to terminate quietly. (Only on systems experiencing some kind of problem are you likely to see more than one instance of the process running, and in such cases you're going to be crashing pretty soon anyway.)

We need to be able to start explorer.exe in the debugging session on a system where Explorer is not running. The answer to this is to kill the existing one. A crude method is to run up task manager and kill explorer.exe, but this can leave the system in an unstable state. The sophisticated way of doing it is to invoke the system shutdown dialog — either via Ctrl-Alt-Del, Shutdown or from Start, Shutdown — and then holding down Ctrl-Alt-Shift (left-hand keys) and clicking on the Cancel button. (You can also hit the Esc key rather than clicking on Cancel, but on my laptop this invokes system hibernation, which is somewhat inconvenient.)

As well as persuading Explorer to close itself down gracefully, this sequence tells the system not to try and restart the shell, which it otherwise may do. There are varying degrees of compliance, of course: XP never subsequently restarts Explorer without being asked, 2000 does it infrequently, and NT4 does it a lot.

So now that we've gotten rid of the shell, we can start debugging. Once the process is up, you can then right-click on the appropriate shell item and you'll hit any breakpoints you've set up. I usually have one on the entry of IShellExtInit::Initialize() and on the handler-specific interface methods, in this case the three methods of IContextMenu. You can then debug as you would any other DLL/COM component.

For context menu handlers, breaking within IShellExtInit::Initialize() and IContextMenu::QueryContextMenu() will move the focus to the debugger, so the menu will actually be cancelled — don't be misled by this into thinking that your shell extension is not working. Once you're satisfied that everything is OK with these two methods, it's best to disable the breakpoints therein, so that you can get on with the GetCommandString() and InvokeCommand() methods. One last tip: When debugging within GetCommandString() you can get the shell, indeed the whole system, in a weird state whereby your debugger can be hung. This is no doubt due to Windows' fundamental menu-handling logic — I've experienced similar behavior when debugging other menu functionality — but worry not. On NT-family systems you only need to hit Ctrl-Alt-Del and then hit cancel, and it all gets nicely cleared up (most of the time).

Don't forget to change the path for Explorer if you're testing on multiple boots on the same system. I can assure you that NT4's Explorer.exe will not execute on 2000, XP, and so on, and you may experience a few panicked moments, imagining you've trashed your system or the shell extension, before you realize your oversight.

The advice I've given about debugging has been all NT-family based. Alas, it is too many years since I did any debugging of any kind on 95-family operating systems, and I cannot remember whether I ever did shell extension debugging on them. I suspect I probably made do with OutputDebugString_printf()-style debugging on them. I'm not sure how many shell extension developers will be disenfranchised by this lack of advice, but judging from the hit-counts on http://shellext.com/, it is clear that the vast majority (>90 percent) of shell extension users are running NT-family machines.

Conclusion

I hope you've learned a little about shell extensions in general, and a lot about context menu handlers in particular. I also hope that I've sparked your interest in WTL and STLSoft (COMSTL, WinSTL, WTLSTL, and all the other little STLs), and I invite you to try both out, especially when developing small lightweight components. There's a lot more mileage in C++ as the primary development language for the Win32 platform than some quarters would have us believe, and there are still powerful and effort-reducing libraries being created that will support its position for a long time to come.

Acknowledgments

I'd like to thank Scott Patterson (http://www .gameframework.com/) for providing his usual constructive criticism while recovering from a nasty bout of the flu: above and beyond! I'd also like to thank the many users of the Synesis Software Shell Extensions for all your kind words, offers to buy, useful bug reports, and intriguing feature requests over the last couple of years. And, yes, they're going to continue to be free. Honest! w::d


Matthew Wilson holds a degree in Information Technology and a Ph.D. in Electrical Engineering, and is a software-development consultant for Synesis Software. Matthew's work interests are in writing bulletproof real-time, GUI, and software-analysis software in C, C++, C#, and Java. He has been working with C++ for over 10 years, and is currently bringing STLSoft.org and its offshoots into the public domain. Matthew can be contacted via matthew@synesis.com.au or at http://stlsoft.org/.


Related Reading


More Insights