Position Changer Add-In updated for Windows XP Media Center Edition 2005 Update Rollup 2
Today, the Media Center team released Update Rollup 2 for Windows XP Media Center Edition 2005 (you may have heard of the update referred to previously as "Emerald"). Update Rollup 2 includes a plethora of modifications to improve the stability, reliability, and functionality of Windows XP Media Center Edition 2005. A new SDK to match was also released, available at http://www.microsoft.com/downloads/details.aspx?FamilyId=1D836C29-ABD5-4FDD-90C5-5C5ABAE97DB4&displaylang=en.
One of the problems users of MCE 2005 experience is poorly written add-ins and HTML applications hosted by MCE. In MCE 2005, add-ins are hosted in-process in ehShell.exe (the main MCE application), so when something goes wrong in one of these add-ins, the entire MCE experience can be affected.
To address this reliability issue in Update Rollup 2, MCE has moved to an out-of-process hosting model. When an add-in is run, a new hosting application, ehExtHost.exe, loads the add-in rather than ehShell.exe loading it (take a look in task manager after an add-in loads and you'll see an instance of the application listed). That way, if MCE detects that the add-in is being a bad citizen, it can tear down the whole hosting process. A named pipe remoting channel is used to provide communication between ehShell and the add-in loaded in ehExtHost. Since ehShell previously loaded add-ins into a separate application domain, remoting was already necessary for add-ins to communicate with the hosting code in the primary application domain, so this change for Update Rollup 2 shouldn't affect the functionality of any existing add-ins. Or, at least not those that play by the rules...
As I mentioned in my article Time Travel with Windows XP Media Center, while developing the Position Changer Add-In, I made several assumptions about Media Center and the environment that would be hosting the add-in. Most of these assumptions are still valid for Update Rollup 2 (for example, I had assumed that, while watching a recorded video, the arrow keys served no purpose, and that remains the case in this update). However, if you recall from the article, my add-in uses a keyboard hook in order to intercept and handle certain remote control interactions. The keyboard hook requires that the add-in be running in the same process as the GUI thread handling the keyboard commands issued by the remote. But as I've just outlined, with Update Rollup 2, add-ins are no longer hosted in ehShell, and thus the version of the add-in I posted with my article no longer works once you upgrade to Update Rollup 2. It won't cause anything to crash; rather, the add-in will just fail to load, a message will be written to the event log detailing that the keyboard hook couldn't be installed, and the add-in will exit silently.
Have no fear, though. If you've come to rely on the add-in (as I have), you'll be comforted to know that I've updated the code so that it is compatible with the new version. You can download the source code and installers for both versions here. Just uninstall the version you currently have installed, install the new version, and you should be good to go.
For those of you who are just interested in getting this functionality back, you can stop reading now. For those of you who are interested in the changes I had to make to get this to work, keep reading.
First and foremost, I had to abandon the type of keyboard hook I was using in the previous version. My add-in is now running in a separate process from the thread that I want to hook, and thus I would need to use a global hook. However, if you read Knowledge Base article 318804, you'll discover that global hooks are not supported in the .NET Framework:
Except for the WH_KEYBOARD_LL low-level hook and the WH_MOUSE_LL low-level hook, you cannot implement global hooks in the Microsoft .NET Framework. To install a global hook, a hook must have a native DLL export to inject itself in another process that requires a valid, consistent function to call into. This behavior requires a DLL export. The .NET Framework does not support DLL exports. Managed code has no concept of a consistent value for a function pointer because these function pointers are proxies that are built dynamically."
Ah, but there's a glimmer of hope. The KB article mentions that WH_KEYBOARD_LL can be used to implement global hooks. What is WH_KEYBOARD_LL? From the documentation:
The LowLevelKeyboardProc hook procedure is an application-defined or library-defined callback function used with the SetWindowsHookEx function. The system calls this function every time a new keyboard input event is about to be posted into a thread input queue. The keyboard input can come from the local keyboard driver or from calls to the keybd_event function. If the input comes from a call to keybd_event, the input was "injected". However, the WH_KEYBOARD_LL hook is not injected into another process. Instead, the context switches back to the process that installed the hook and it is called in its original context. Then the context switches back to the application that generated the event."
Perfect! This means that from the hosting process, I can use a low-level keyboard hook instead of a standard keyboard hook to intercept the remote control commands routed to ehShell. Doing so required only minor modifications to my KeyboardHook class, which you can examine by downloading the source code I've made available. There is a downside to this approach, of course. This is now a global hook, which means that every keystroke to be handled by any application on the machine will cause a context switch to the hosting process so that my add-in can determine whether it wants to handle it. To minimize this impact, when the add-in starts up, it retrieves the process ID of ehShell.exe and caches it away. When the hook callback executes to process keyboard input, the handler first checks to see if the process responsible for this input is ehShell (it make an educated guess about what process is responsible by retrieving the ID of the process that owns the current foreground window). If it's not, it immediately returns in order to minimize the amount of time spent processing in the hook (of course, we also only want to handle ehShell input, so this is necessary anyway).
With that change in place, the new version of the add-in is almost fully functional. However, if you attempt to use the bookmarking functionality, you'll find that it is partially broken as it's unable to associate bookmarks with a particular DVR-MS file (if you read the article, you'll remember that this was one of my goals, to be able to set a bookmark per file). The reason for this becomes clear if you look at the event log after attempting to set or jump to a bookmark, as you'll see a message like the following in the log:
Unable to access filter graph for current show. System.Runtime.InteropServices.COMException (0x8001010D): An outgoing call cannot be made since the application is dispatching an input-synchronous call. at System.Runtime.InteropServices.UCOMIRunningObjectTable.GetObject( UCOMIMoniker pmkObjectName, Object& ppunkObject) at Toub.MediaCenter.AddIns.MceGraph.GetGraphForProcess(Int32 pid) at Toub.MediaCenter.AddIns.MceGraph.GetCurrentMediaInfo(Int32 pid)
In order to figure out the current DVR-MS file, I access the running object table (ROT) in order to get at the current playback filter graph used by MCE. This allows me to iterate through the filters in the graph looking for the source filter that will reveal to me the path to the current DVR-MS. In order to retrieve a filter graph from the ROT, I need to make a call to the ROT's GetObject method. However, this is a call on a COM component. A quick look at Knowledge Base article 131056 reveals the following:
A synchronous OLE call made by the recipient of an inter-process/ inter-thread SendMessage fails with RPC_E_CANTCALLOUT_ININPUTSYNCCALL(0x8001010D).
It explains why if you're interested in the nitty gritty. A slightly more relevant explanation is available from Knowledge Base article 198996:
COM does not allow normal outgoing COM method calls from a thread that is currently servicing a SendMessage request.
And another look at the documentation for LowLevelKeyboardProc reveals this:
This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.
Aha. "The call is made by sending a message". Thus, our hook callback is the result a message being sent. Our hook then attempts to make a normal outbound COM call from the same thread. And boom: RPC_E_CANTCALLOUT_ININPUTSYNCCALL.
To get around this, I simply do the work to access the filter graph on a separate thread. And with that, Position Changer Add-In should work just like it did prior to Update Rollup 2. The only thing you might notice is a slightly sluggish response to keystrokes, as each one requires that the hosting process handle it.
I hope you enjoy it.