{"id":9493,"date":"2011-10-03T07:00:00","date_gmt":"2011-10-03T07:00:00","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/oldnewthing\/2011\/10\/03\/do-not-access-the-disk-in-your-icontextmenu-handler-no-really-dont-do-it\/"},"modified":"2011-10-03T07:00:00","modified_gmt":"2011-10-03T07:00:00","slug":"do-not-access-the-disk-in-your-icontextmenu-handler-no-really-dont-do-it","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20111003-00\/?p=9493","title":{"rendered":"Do not access the disk in your IContextMenu handler, no really, don&#039;t do it"},"content":{"rendered":"<p>\nWe saw some time ago that\n<a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2008\/05\/21\/8525411.aspx\">\nthe number one cause of crashes in Explorer is malware<\/a>.\n<\/p>\n<p>\nIt so happens that the number one cause of hangs in Explorer\nis disk access from context menu handlers\n(a special case of the more general principle,\n<a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2009\/04\/15\/9549682.aspx\">\nyou can&#8217;t open the file until the user tells you to open it<\/a>).\n<\/p>\n<p>\nThat&#8217;s why I was amused by Memet&#8217;s claim that\n<a HREF=\"http:\/\/blogs.msdn.com\/oldnewthing\/archive\/2009\/10\/05\/9903476.aspx#9904591\">\n&#8220;would hit the disk&#8221; is not acceptable for me<\/a>.\nThe feedback I see from customers, either directly\nfrom\n<a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2009\/05\/28\/9645162.aspx\">\nlarge multinational corporations with 500ms ping times<\/a>\nor indirectly\n<a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2010\/08\/04\/10045651.aspx\">\nfrom individual users who collectively click <i>Send Report<\/i>\nmillions of times a day<\/a>,\nis that &#8220;would hit the disk&#8221; ruins a lot of people&#8217;s days.\nIt may not be acceptable to you, but millions of other people\nwould beg to disagree.\n<\/p>\n<p>\nThe Windows team tries very hard to identify unwanted disk accesses\nin Explorer and get rid of them.\n<a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2005\/01\/11\/350628.aspx#350903\">\nWe don&#8217;t get them all<\/a>, but at least we try.\nBut if the unwanted disk access is coming from a third-party add-on,\nthere isn&#8217;t much that can be done aside from saying,\n&#8220;Don&#8217;t do that&#8221; and hoping the vendor listens.\n<\/p>\n<p>\nEvery so often, a vendor will come back and ask for advice on avoiding\ndisk access in their context menu handler.\nThere&#8217;s a lot of information packed into that data object that\ncontains information gathered from when the disk was accessed originally.\nYou can just retrieve that cached data instead of going off and\nhitting the disk again to recalculate it.\n<\/p>\n<p>\nI&#8217;m going to use a boring console application and the clipboard\nrather than building a full\n<code>IContext&shy;Menu<\/code>,\nsince the purpose here is to show how to get data from a data object\nwithout hitting the disk and not to delve into the details of\n<code>IContext&shy;Menu<\/code> implementation.\n<\/p>\n<pre>\n#define UNICODE\n#define <a HREF=\"http:\/\/blogs.msdn.com\/b\/oldnewthing\/archive\/2004\/02\/12\/71851.aspx\">_UNICODE<\/a>\n#include &lt;windows.h&gt;\n#include &lt;ole2.h&gt;\n#include &lt;shlobj.h&gt;\n#include &lt;propkey.h&gt;\n#include &lt;tchar.h&gt;\nvoid ProcessDataObject(IDataObject *pdto)\n{\n ... to be written ...\n}\nint __cdecl _tmain(int argc, PTSTR *argv)\n{\n if (SUCCEEDED(OleInitialize(NULL))) {\n  IDataObject *pdto;\n  if (SUCCEEDED(OleGetClipboard(&amp;pdto))) {\n   ProcessDataObject(pdto);\n   pdto-&gt;Release();\n  }\n  OleUninitialize();\n }\n}\n<\/pre>\n<p>\nOkay, let&#8217;s say that we want to check that all the items\non the clipboard are files and not directories.\nThe <code>HDROP<\/code> way of doing this would be to get\nthe path to each of the items in the data object,\nthen call <code>Get&shy;File&shy;Attributes<\/code> on each one to see\nif any of them has the\n<code>FILE_ATTRIBUTE_DIRECTORY<\/code> flag set.\nBut this hits the disk, which makes baby context menu host sad.\nFortunately, the <code>IShell&shy;Item&shy;Array<\/code> interface provides\nan easy way to check whether any or all the items in a data object\nhave a particular attribute.\n<\/p>\n<pre>\nvoid ProcessDataObject(IDataObject *pdto)\n{\n IShellItemArray *psia;\n HRESULT hr;\n hr = SHCreateShellItemArrayFromDataObject(pdto,\n                                          IID_PPV_ARGS(&amp;psia));\n if (SUCCEEDED(hr)) {\n  SFGAOF sfgaoResult;\n  hr = psia-&gt;GetAttributes(SIATTRIBFLAGS_OR, SFGAO_FOLDER,\n                                                 &amp;sfgaoResult);\n  if (hr == S_OK) {\n   _tprintf(TEXT(\"Contains a folder\\n\"));\n  } else if (hr == S_FALSE) {\n   _tprintf(TEXT(\"Contains no folders\\n\"));\n  }\n  psia-&gt;Release();\n }\n}\n<\/pre>\n<p>\nIn this case, we want to see if any item\n(<code>SI&shy;ATTRIB&shy;FLAGS_OR<\/code>)\nin the data object has\nthe <code>SFGAO_FOLDER<\/code> attribute.\nThe <code>IShell&shy;Item&shy;Array::Get&shy;Attributes<\/code>\nmethod returns\n<code>S_OK<\/code> if all of the attributes you requested\nare present in the result.\nSince we asked for only one attribute, and since we asked for\nthe result to be the logical <i>or<\/i> of the individual attributes,\nthis means that it returns <code>S_OK<\/code> if any item is a folder.\n<\/p>\n<p>\nOkay, fine, but what if the thing you want to know is not expressible\nas a <code>SFGAO<\/code> flag?\nWell, you can dig into each of the individual items.\nFor example, suppose we want to see the size of each item.\n<\/p>\n<pre>\n#include &lt;strsafe.h&gt;\nvoid ProcessDroppedObject(IDataObject *pdto)\n{\n IShellItemArray *psia;\n HRESULT hr;\n hr = SHCreateShellItemArrayFromDataObject(pdto,\n                                          IID_PPV_ARGS(&amp;psia));\n if (SUCCEEDED(hr)) {\n  <font COLOR=\"blue\">IEnumShellItems *pesi;\n  hr = psia-&gt;EnumItems(&amp;pesi);\n  if (SUCCEEDED(hr)) {\n   IShellItem *psi;\n   while (pesi-&gt;Next(1, &amp;psi, NULL) == S_OK) {\n    IShellItem2 *psi2;\n    hr = psi-&gt;QueryInterface(IID_PPV_ARGS(&amp;psi2));\n    if (SUCCEEDED(hr)) {\n     ULONGLONG ullSize;\n     hr = psi2-&gt;GetUInt64(PKEY_Size, &amp;ullSize);\n     if (SUCCEEDED(hr)) {\n      _tprintf(TEXT(\"Item size is %I64u\\n\"), ullSize);\n     }\n     psi2-&gt;Release();\n    }\n    psi-&gt;Release();\n   }\n  }<\/font>\n  psia-&gt;Release();\n }\n}\n<\/pre>\n<p>\nI went for <code>IEnum&shy;Shell&shy;Items<\/code> here,\neven though a <code>for<\/code> loop\nwith <code>IShell&shy;Item&shy;Array::Get&shy;Count<\/code> and\n<code>IShell&shy;Item&shy;Array::Get&shy;Item&shy;At<\/code>\nwould have worked, too.\n<\/p>\n<p>\nFile system items in data objects cache a bunch of useful pieces\nof information, such as the last-modified time, file creation time,\nlast-access time,\nthe file size,\nthe file attributes, and the file name (both long and short).\nOf course, all of these properties are subject to file system support.\nthe shell just takes what&#8217;s in the <code>WIN32_FIND_DATA<\/code>;\nif the values are incorrect\n(for example, if last-access time tracking is disabled),\nthen the shell is going to cache the incorrect value.\nBut don&#8217;t say, &#8220;Well, if the cache is no good, then I won&#8217;t use it;\nI&#8217;ll just go hit the disk&#8221;, because if you hit the disk,\nthe file system is going to give you the same incorrect value anyway!\n<\/p>\n<p>\nIf you just want to order the combo platter, you can ask for\n<a HREF=\"http:\/\/msdn.microsoft.com\/library\/bb760709.aspx\">\n<code>PKEY_Find&shy;Data<\/code><\/a>, and out will come a\n<code>WIN32_FIND_DATA<\/code>.\nThis might be the easiest way to convert your old-style context menu\nthat hits the disk into a new-style context menu that doesn&#8217;t hit the\ndisk:\nTake your calls to\n<code>Get&shy;File&shy;Attributes<\/code> and\n<code>Find&shy;First&shy;File<\/code>\nand convert them into calls into the property system,\nasking for <code>PKEY_File&shy;Attributes<\/code> or\n<code>PKEY_Find&shy;Data<\/code>.\n<\/p>\n<p>\nOkay, that&#8217;s the convenient modern way to get information that has\nbeen cached in the data object provided by the shell.\nWhat if you&#8217;re an old-school programmer?\nThen you get to roll up your sleeves and get your hands dirty with\nthe <code>CFSTR_SHELL&shy;ID&shy;LIST<\/code>\nclipboard format.\n(And if your target is Windows&nbsp;XP or earlier,\nyou have to do it this way since the\n<code>IShell&shy;Item&shy;Array<\/code> interface\nwas not introduced until Windows Vista.)\nIn fact, the <code>CFSTR_SHELL&shy;ID&shy;LIST<\/code>\nclipboard format will get your hands so dirty,\nI&#8217;m writing a helper class to manage it.\n<\/p>\n<p>\nFirst, go back and familiarize yourself with\n<a HREF=\"http:\/\/msdn.microsoft.com\/library\/bb773212.aspx\">\nthe <code>CIDA<\/code> structure<\/a>.\n<\/p>\n<pre>\n\/\/ these should look familiar\n#define HIDA_GetPIDLFolder(pida) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)-&gt;aoffset[0])\n#define HIDA_GetPIDLItem(pida, i) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)-&gt;aoffset[i+1])\nvoid ProcessDataObject(IDataObject *pdto)\n{\n FORMATETC fmte = {\n    (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST),\n    NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };\n STGMEDIUM stm = { 0 }; \/\/ defend against buggy data object\n HRESULT hr = pdto-&gt;GetData(&amp;fmte, &amp;stm);\n if (SUCCEEDED(hr) &amp;&amp; stm.hGlobal != NULL) {\n  LPIDA pida = (LPIDA)GlobalLock(stm.hGlobal);\n  if (pida != NULL) { \/\/ defend against buggy data object\n   IShellFolder *psfRoot;\n   hr = SHBindToObject(NULL, HIDA_GetPIDLFolder(pida), NULL,\n                       IID_PPV_ARGS(&amp;psfRoot));\n   if (SUCCEEDED(hr)) {\n    for (UINT i = 0; i &lt; pida-&gt;cidl; i++) {\n     IShellFolder2 *psf2;\n     PCUITEMID_CHILD pidl;\n     hr = SHBindToFolderIDListParent(psfRoot,\n                HIDA_GetPIDLItem(pida, i),\n                IID_PPV_ARGS(&amp;psf2), &amp;pidl);\n     if (SUCCEEDED(hr)) {\n      VARIANT vt;\n      if (SUCCEEDED(psf2-&gt;GetDetailsEx(pidl, &amp;PKEY_Size, &amp;vt))) {\n       if (SUCCEEDED(VariantChangeType(&amp;vt, &amp;vt, 0, VT_UI8))) {\n         _tprintf(TEXT(\"Item size is %I64u\\n\"), vt.ullVal);\n       }\n       VariantClear(&amp;vt);\n      }\n      psf2-&gt;Release();\n     }\n    }\n    psfRoot-&gt;Release();\n   }\n   GlobalUnlock(stm.hGlobal);\n  }\n  ReleaseStgMedium(&amp;stm);\n }\n}\n<\/pre>\n<p>\nI warned you it was going to be ugly.\n<\/p>\n<p>\nFirst, we retrieve the <code>CFSTR_SHELL&shy;ID&shy;LIST<\/code> clipboard\nformat from the data object.\nThis format takes the form of an <code>HGLOBAL<\/code>,\nwhich needs to be <code>Global&shy;Lock<\/code>&#8216;d\nlike all <code>HGLOBAL<\/code>s\nreturned by <code>IData&shy;Object::Get&shy;Data<\/code>.\nYou may notice two defensive measures here.\nFirst, there is a defense against data objects which return\nsuccess when they actually failed.\nTo detect this case, we zero out the <code>STG&shy;MEDIUM<\/code> and\nmake sure they returned something non-<code>NULL<\/code> in it.\nThe second defensive measure is against data objects which\nput an invalid <code>HGLOBAL<\/code> in the <code>STG&shy;MEDIUM<\/code>.\nOne of the nice things about doing things the\n<code>IShell&shy;Item&shy;Array<\/code> way is that the shell default\nimplementation of <code>IShell&shy;Item&shy;Array<\/code> has all these\ndefensive measures built-in so you don&#8217;t have to write them yourself.\n<\/p>\n<p>\nAnyway, once we get the <code>CIDA<\/code>,\nwe bind to the folder portion, walk through the items,\nand get the size of each item in order to print it.\nSame story, different words.\n<\/p>\n<p>\n<b>Exercise<\/b>: Why did we need a separate defensive measure\nfor data objects which returned success but left garbage in the\n<code>STG&shy;MEDIUM<\/code>?\nWhy doesn&#8217;t the <code>Global&shy;Lock<\/code> test cover that case, too?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We saw some time ago that the number one cause of crashes in Explorer is malware. It so happens that the number one cause of hangs in Explorer is disk access from context menu handlers (a special case of the more general principle, you can&#8217;t open the file until the user tells you to open [&hellip;]<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-9493","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>We saw some time ago that the number one cause of crashes in Explorer is malware. It so happens that the number one cause of hangs in Explorer is disk access from context menu handlers (a special case of the more general principle, you can&#8217;t open the file until the user tells you to open [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/9493","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=9493"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/9493\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=9493"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=9493"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=9493"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}