How can I add an environment variable to a process launched via Shell­Execute­Ex or IContext­Menu?

Raymond Chen

The Shell­Execute­Ex function and IContext­Menu interface provide the caller a number of places to customize how the execution occurs by allowing the call to pass a “site” which Shell­Execute­Ex/IContext­Menu will access at various points in the execution process.

Today we’ll demonstrate this technique by adding environment variables to processes launched via Shell­Execute­Ex and IContext­Menu.

The ICreating­Process interface is used by Shell­Execute­Ex. and IContext­Menu to allow the caller to customize how processes are created. The extension point is obtained by querying the site for SID_Execute­Creating­Process and requesting an ICreating­Process. If one is produced, then the system calls the OnCreating method with an object that allows the creation to be customized.

Today’s C++ COM library is (rolls dice) C++/WinRT.

struct AddEnvironmentVariableSite :
    winrt::implements<AddEnvironmentVariableSite,
        ::IServiceProvider,
        ::ICreatingProcess>
{
    IFACEMETHOD(QueryService)
        (REFGUID service, REFIID riid, void** ppv)
    {
        if (service == SID_ExecuteCreatingProcess)
        {
            return this->QueryInterface(riid, ppv);
        }
        else
        {
            *ppv = nullptr;
            return E_NOTIMPL;
        }
    }

    IFACEMETHOD(OnCreating)(ICreateProcessInputs* inputs)
    {
        return inputs->SetEnvironmentVariable(
            L"EXTRAVARIABLE", L"Bonus");
    }
};

This site responds to SID_Execute­Creating­Process by returning itself, and it implements ICreating­Process by having its OnCreating method set an environment variable called EXTRAVARIABLE with a value of Bonus. Since that is the only thing we do, we can just return the result of Set­Environment­Variable() as our own return value. If you intend to add multiple environment variables, you would check the return value of each call to Set­Environment­Variable().

In general, your custom site would be part of a so-called “site chain” and forward any unhandled services to your own site, so that an outer site could respond to it.

Here’s an example of how to use this special environment variable site in conjunction with Shell­Execute­Ex:

BOOL Sample()
{
    SHELLEXECUTEINFO sei{ sizeof(sei) };
    sei.lpFile = LR"(C:\Windows\system32\charmap.exe)";
    sei.nShow = SW_SHOWNORMAL;
    auto site = winrt::make_self<AddEnvironmentVariableSite>();
    sei.hInstApp = reinterpret_cast<HINSTANCE>(site.get());    
    sei.fMask = SEE_MASK_FLAG_HINST_IS_SITE;                   
    return ShellExecuteEx(&sei);
}

To pass a site to Shell­Execute­Ex, we put it in the hInstApp member and set the SEE_MASK_FLAG_HINST_IS_SITE flag to say “If you look at the hInstApp, you’ll find a site!”¹

For context menus, we explicitly set our custom site as the context menu’s site. Building on our original sample for hosting an IContext­Menu:

void OnContextMenu(HWND hwnd, HWND hwndContext, UINT xPos, UINT yPos)
{
  IContextMenu *pcm;
  if (SUCCEEDED(GetUIObjectOfFile(hwnd, L"C:\\Windows\\clock.avi",
                   IID_IContextMenu, (void**)&pcm))) {
    HMENU hmenu = CreatePopupMenu();
    if (hmenu) {
      if (SUCCEEDED(pcm->QueryContextMenu(hmenu, 0,
                             SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST,
                             CMF_NORMAL))) {
        CMINVOKECOMMANDINFO info = { 0 };
        info.cbSize = sizeof(info);
        info.hwnd = hwnd;
        info.lpVerb = "play";
        auto site = winrt::make_self<AddEnvironmentVariableSite>();
        IUnknown_SetSite(pcm, site.get());                         
        pcm->InvokeCommand(&info);
        IUnknown_SetSite(pcm, nullptr);                            
      }
      DestroyMenu(hmenu);
    }
    pcm->Release();
  }
}

(I’m ignoring the fact that winrt::make_self can throw an exception which results in a memory leak in the sample code above.)

¹ This is admitted a rather ugly way to pass a site, but the ability to add a site was retrofitted onto the existing structure, so we had to hide it in an other-wise unused input member.

10 comments

Discussion is closed. Login to edit/delete existing comments.

  • Michael Taylor 0

    Even for Microsoft this seems like a bad hack. Reusing a field that doesn’t actually represent what it says it is just smells bad. This is especially bothersome since changing the environment variables of a new process is not that uncommon.

    The SHELLEXECUTEINFO type has the size as the first parameter. As has been done in several other places in Win32 over the years, adding new fields to the struct increases the struct size. Hence a new SHELLEXECUTEINFO2 struct could be defined that has new field(s) for the environment variable information. The implementation would use the size to determine which type it is working with. Existing code would continue to work as they are using the original type with the original size but new code could use the newer definition.

    I understand the concerns of backwards compatibility but that is a primary reason for passing the size as the first field, to handle these cases. The hack approach runs the risk of breaking existing code as well so I don’t see either approach guaranteeing compatibility. A caller may not be setting the mask properly and have the new mask set by accident. Just seems like this was a poor design decision when a built-in solution was already available.

  • GL 0

    MSDN (futuristically known as Microsoft Learn) says that the flag needs to be manually defined prior to Windows 8, but ICreatingProcess is documented to have minimum OS Vista. Does that mean the flag works in Vista and 7, just undocumented, or that SEE will fail in Vista and 7 with that flag, or that SEE succeeds without considering the flag in Vista and 7?

    Also, IContextMenu and SEE don’t always create processes (e.g., IDropTarget and a local server is already running). In that case, the interface will not affect anything if no process is launched, right?

    If the verb is IDropTarget, does SEE query its IObjectWithSite and pass the site chain in case the implementing object supports it, so that the drop target processes the site chain in case it ends up launching a new process (and potentially other, maybe proprietary services)?

    • Me Gusta 0

      The most likely thing that happened was that it wasn’t put into the public headers, but Windows supported it.
      It is also possible that this was functionality added into Windows via a patch, and this meant that it wasn’t defined in any of the public headers until the Windows 8 SDK. But in that kind of situation, you would expect the KB number to be listed which introduced the functionality.

    • Raymond ChenMicrosoft employee 1

      It’s a doc bug. SEE_MASK_FLAG_HINST_IS_SITE is not supported prior to Windows 8. I will submit a doc fix request.

  • Henke37 0

    This is the first I hear of ICreatingProcess, and I can’t seem to find any matches for it when searching online.

  • Joe Beans 0

    Why does IServiceProvider exist when we have IUnknown::QueryInterface?

    • Me Gusta 0

      To answer a question with a question, why does the object that implements IServiceProvider also have to implement ICreatingProcess? This did happen for the above sample, but this isn’t a requirement. IServiceProvider was designed for cases where an object didn’t implement all of the interfaces.

      • Raymond ChenMicrosoft employee 0

        IServiceProvider allows an object to say “I don’t implement that interface, but I know someone who does!” This is more general than QueryInterface, which requires the object to implement the interface directly. In our example, the service provider also implements ICreatingProcess, but in the general case, it could have delegated it to another object.

        • Me Gusta 0

          Oh, interestingly, the docs for IServiceProvider list the minimum client and server versions as Windows 11 (22000).

Feedback usabilla icon