Why does Windows keep showing the old indirect strings even after I update the binary?


If your application uses indirect localized string resources, and you update the application, you may find that Windows keeps using the old string from the previous version of the application.

For example, suppose that you set the localized name for a shortcut to @C:\Program Files\Contoso\Contoso.exe,-1, and in version 1 of your program, you have

LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Document Services"
END

LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Dokumentdienste"
END

For version 2, your marketing team decides that the program should really be called Contoso Document System, so you change the resource file to read

LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Document System"
END

LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Dokumentsystem"
END

The user upgrades to version 2 of your program, but the shortcut on the Start menu still reads Contoso Document Services. What's going on?

The shell keeps a cache of indirect localized strings because loading a DLL just to read a string out of it is pretty expensive. This cache is keyed by the string location specifier, and since your string location specifier hasn't changed from its previous value of @C:\Program Files\Contoso\Contoso.exe,-1, the shell continues using the value it stored away in its cache, which if the user had previously been using version 1 of your program, is the string Contoso Document Services.

Some people, having discovered this behavior, have tried to go in and tinker with the shell's internal cache of indirect localized strings, but such a technique is doomed to failure because the location of that cache changes pretty regularly, and besides, it's an internal implementation detail. (And even if you find it and manage to fiddle with it, you only fix the problem for the current user. Other users will still have the stale cache entry.)

The best solution is to treat indirect strings as locked: Once you decide what a string should say, you can't change it. When you issue version 2 of Contoso.exe, you can create a second string

LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Document Services" // shortcuts from version 1.0 use this
2 "Contoso Document System" // shortcuts from version 2.0 use this
END

LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
STRINGTABLE
BEGIN
1 "Contoso Dokumentdienste" // shortcuts from version 1.0 use this
2 "Contoso Dokumentsystem" // shortcuts from version 2.0 use this
END

and have the installer for version 2.0 create a shortcut whose indirect localized string specifier is

@C:\Program Files\Contoso\Contoso.exe,-2

I admit that this method is rather clumsy and requires more attention on the part of the developer. Everybody wants the "cheap" way out, where the definition of "cheap" is not "cheapest for the customer" but rather "cheapest for me, the developer, because there's a new episode of Doctor Who tonight and I don't want to miss it."

We saw last time that the format for indirect localized string resources has room for a comment. And it's the comment that we can take advantage of here. The shell uses the entire string location specifier as the key for its cache lookup, and that string includes the comment. Therefore, if you simply change the comment, that results in a cache miss, and the shell will go and re-fetch the string.

@C:\Program Files\Contoso\Contoso.exe,-1;v2

By appending a ;v2 to the string, you made it different from its predecessor, which means that the string cached by the predecessor won't be used.

As I noted, this is cheap for the developer, but not necessarily cheap for the customer. Suppose the customer copied the shortcut to Contoso version 1 to their desktop, then upgraded to version 2. The upgrade replaces the shortcut in the Start menu, but the copy on the desktop remains unchanged. You now have a shortcut on the desktop whose indirect string is

@C:\Program Files\Contoso\Contoso.exe,-1

and a shortcut on the Start menu whose indirect string is

@C:\Program Files\Contoso\Contoso.exe,-1;v2

Since the shortcut on the desktop was created while version 1 was still installed on the computer, its name will read Contoso Document Services because that was the contents of string 1. On the other hand, the shortcut on the Start menu will read Contoso Document System because its use of the ;v2 forced the shell to go back and look again, and this time it sees the revised string. So far so good.

But then the user does something which causes the cache to be pruned, like, say, changing their UI language to German. The shell says, "Okay, the UI language changed, I need to go reload all these indirect strings because MUI is going to change them to the new language." The shell sees the shortcut on the Start menu, reads string 1 out of Contoso.exe, and gets Contoso Dokumentsystem. The shell then sees the shortcut on the desktop, reads string 1 out of Contoso.exe, and gets... Contoso Dokumentsystem. Not Contoso Dokumentdienste.

Notice that the name of the shortcut on the desktop was silently upgraded to Contoso version 2.

Even if the user changes the language back to English in an attempt to get things back to the way they were, it won't work. The shell sees the shortcut on the Start menu, reads string 1 out of Contoso.exe, and gets Contoso Document System. The shell then sees the shortcut on the desktop, reads string 1 out of Contoso.exe, and gets Contoso Document System, not Contoso Document Service. The original string from the first version of Contoso.exe is already gone; the only way to get it back is to reinstall Contoso version 1.

But at least you didn't miss your TV show.

Bonus chatter: The one case I can think of where the cheap way out is acceptable is when you are issuing a prerelease version. For your prerelease versions, you can append ;prerelease build xxxxx to your string location specifier (where xxxxx is the build number), so that each time the user upgrades to a new build, the string is reloaded from scratch. This still has the same problem described above if the user has data left over from a previous build, but since it's a prerelease build, you can just declare that as not a supported configuration.

Comments (30)
  1. Anonymous says:

    Can shortcuts even store indirect strings? Or are you talking about desktop.ini?

    [You can put indirect strings into shortcuts too. -Raymond]
  2. Anonymous says:

    I'm confused. Isn't it a GOOD thing that the desktop shortcut also got updated?

    [You truncated the question. Is it a good thing that the desktop shortcut got updated at some unpredictable point in the future. -Raymond]
  3. Presumably icons are cached by the shell in the same way, which explains a lot.

    The shell doesn't need to load the DLL to check if the cached strings are still likely to be valid. Checking the DLL's date would solve the problem in most cases (and it'd be forgiveable for caching to go wrong in cases where two DLL versions had identical datestamps).

    Of course, checking the date every time the string was used would be too expensive, but the shell could at least check it occasionally. Caches that never verify their validity are rather non-brilliant.

    ["Occasionally, Explorer stops responding for several minutes." Try debugging that. -Raymond]
  4. Anonymous says:

    Is there really a new episode of Dr. Who tonight?!

  5. Anonymous says:

    @Oh, no, not until this fall, unfortunately.

  6. Anonymous says:

    InvalidateIndirectStringCache(LPCTSTR lpModule) or even InvalidateShellCaches(LPCTSTR lpModule) would cause unacceptable missing of Doctor Who episodes for the MS developers, I take it? :-)

    I'm just teasing here, but the knife cuts both ways. If this is such a common scenario that it requires "careful attention" from the application developers, MS could choose to make it slightly less painful for end users as well by making it easy. I don't know where it would end up on the -100 points scale, mind you. In any case, it shouldn't take "several minutes" for a single module. It would be included in the installation/upgrade time, which to end users (sad to say) is an acceptable time to be unresponsive and slow anyway, if my personal experiences are any indication.

    [This imaginary Invalidate­Indirect­String­Cache wouldn't solve the problem, since it couldn't invalidate the caches of other users. -Raymond
  7. SimonRev says:

    But when Raymond queued this post Dr. Who was airing new episodes in the spring (if he had queued this for the end of April or May and Dr. Who hadn't moved to a fall schedule, he might well have gotten a lot of folks excited)

  8. Anonymous says:

    @Raymond: The docs don't say anything about indirect strings, only a user comment but that does not list the minimum OS. All the protocol specification ([MS-SHLLINK].pdf) says is: "NAME_STRING: An optional structure that specifies a description of the shortcut that is displayed to end users to identify the purpose of the shell link. This structure MUST be present if the HasName flag is set."

    It does work on WinXP, did not test on 2000…

  9. Anonymous says:

    @Raymond: if the user is upgrading the application for other users, they should also be able to invalidate shell caches for other users. If the user is not upgrading the application for other users, then those users' caches don't need invalidating.

    I don't actually develop multi-user apps, so I'm probably missing something?

    [And what if the other users' profiles are not accessible because they are on a roaming profile server you don't have access to? (And in a corporate environment "all other users" can run in the tens of thousands.) -Raymond]
  10. Anonymous says:

    Of course, if you put a gun to my head I can work around that issue too: defer the cache update by keeping a local list of updates-to-process. Implementing this so it gets tracked across profiles properly is of course not trivial.

    "And what if the application is stored on a network so it's not updated locally, and someone logs in from across the world to use the upgraded version of the application?"

    Then push the update to the domain controllers.

    "This feature started off at -100 points, care to guess how far it's come along since then?"

    Shut up, imaginary version of Raymond.

  11. Anonymous says:

    What about using the modified date on the shortcut as a key for the cache? When the binary is updated, the shortcut can also be refreshed by changing only its last modified date. Then, the cache would beinvalidated for this shortcut only. What am I missing?

    [You would have to find every copy of the shortcut the user ever made. What if the user copied it to a USB thumb drive? -Raymond]
  12. Anonymous says:

    This very rarely causes any issues….for explorer please always take the highest performing design! I would think even date check to validate cache would be too slow, some people have hundreds of shortcuts on their desktop/startmenu.

    [The date check can be very expensive if the target DLL is on slow media (DVD) or a slow network. -Raymond]
  13. This is shortsighted design for a cache. Thankfully (and partly because of me) there's a way in Win7 to do reliable MUI cache invalidation. I haven't tested it in a while and assume this is the subject of a future post, so I won't reveal the secret.

    Not that long ago I had to write code to enumerate user profiles in the registry, load the hive for that user, and clear the cached value (which is in different places on XP vs. Vista). We punted on the issue of roaming profiles. Look for the source in IECleanup.exe.

  14. > The shell keeps a cache of indirect localized strings

    Can't this cache be invalidated when the target .dll is written to?  Or is the shell not notified when this happens?

    [What if the target DLL is written to while you are logged off? And wouldn't this require the shell to monitor all DLLs which appear in the cache? -Raymond]
  15. Anonymous says:

    @Raymond: that's what I meant by "missing something".

    Of course, what you describe is pretty awful — with a design like that you can *never* invalidate any cache stored in a roaming profile, except on an ad-hoc basis (and you might as well not bother with that, viz. the "Explorer stops working for several minutes" issue). Someone clever once said that "a cache with a bad policy is another name for a memory leak". I suppose a cache that can't update is indistinguishable from a pain in the neck for developers. :-)

  16. asdbsd says:

    Why not have a second cache with DLL update times, common to all users? When indirect string gets cached, the DLL is registered there. The system monitors FS for changes to those DLLs. Then, when indirect string is taken from the cache, it's time of caching is compared to the corresponding DLL update time, and if it's different, the string's invalidated.

    In this common cache instead of DLL names you might store hashes. I wonder if this still qualifies as a privacy leak.

    [There are so many things wrong with this I don't know where to begin. At least two security flaws, possibly three depending on how you count. -Raymond]
  17. alexcohn says:

    [What if the user copied it to a USB thumb drive? -Raymond]

    That's a good one. If I plug in the portable disk into different computers, it will show up with very different text and icon, too?

  18. Anonymous says:

    Let's be real: how often do you update the indirect string without also modifying/replacing the shortcut?

    [Often enough that people ask this question because they updated the indirect string without also modifying the shortcut. -Raymond]
  19. Anonymous says:

    There's no need to retain the old index 1 string because that's a solved problem. Or else how does Windows cope with roaming your profile to a computer that doesn't actually have C:Program FilesContosoContoso.exe and then changing the UI language?

  20. Anonymous says:

    @Neil: It doesn't. That scenario is already broken anyway, so don't think about it.

  21. Anonymous says:

    | This imaginary InvalidateIndirectStringCache wouldn't solve the problem, since it couldn't invalidate the caches of other users.

    | And what if the other users' profiles are not accessible because they are on a roaming profile server you don't have access to?

    That's the task of the part of the installer that is run from

    [HKEY_LOCAL_MACHINESoftwareMicrosoftActive SetupInstalled Components…]

    "StubPath"=…

  22. Anonymous says:

    Are there any benefits to this feature that are worth all these problems?  Why was it implemented in the first place when it was obvious it would be broken for so many scenarios?

  23. Anonymous says:

    [Often enough that people ask this question because they updated the indirect string without also modifying the shortcut. -Raymond]

    I really should have known this would be the answer…

  24. cheong00 says:

    Emmm… I think if there's a setting in the Windows installer service to force cache update, it should be okay.

    Something like "if the user's last logon time to this computer is later than (the last update time of software that requires cache refresh), refresh the user's cache after logon".

    [And what if the other users' profiles are not accessible because they are on a roaming profile server you don't have access to? (And in a corporate environment "all other users" can run in the tens of thousands.) -Raymond]

    Somehow I think if roaming profile users don't get their cache update, their I.T. should understand, and most probably the users as well (known as common glitches… it's quite common for applications that not designed for roaming profile behave in some "funny" way, and the users have grown accustomed to that, AmIRight?). *Put troll cap on*

  25. Anonymous says:

    [There are so many things wrong with this I don't know where to begin. At least two security flaws, possibly three depending on how you count. -Raymond]

    I'd like to hear the explanation, if possible. The only flaws I can think of can be fixed with some more or less trivial workarounds.

  26. Anonymous says:

    To be completely fair, Doctor Who *does* rule and Karen Gillan is pretty hot, so I'm with the developer this time.

  27. > What if the target DLL is written to while you are logged off? And wouldn't this require the shell to monitor all DLLs which appear in the cache?

    So make that a requirement of the cache.  "The indirect string cache needs to be invalidated when the target .dll is written to, even if no user is logged on."  This just implies that the shell is a bad place to keep the cache.

    [This means that the cache must be system-wide, which is a problem because different users can be running different languages. (I guess you could monitor all satellite DLLs, but that quickly explodes into thousands of DLLs being monitored by the system.) Also, what if the file is modified while the operating system isn't even running? (E.g., you boot into another partition and then modify the file, or the file is on a removable drive and is modified while inserted into some other machine.) -Raymond]
  28. Hmmm… let's recap.

    Definition: a "perfect" caching system is one that creates a performance boost leaving no other evidence that it exists (no possibility of stale data being returned; no cache poisoning; no authorization bypasses, etc.)

    This blog post is a special case of the truism "some caching systems are imperfect."

    In this case, it looks like rubbing out the "OMG there's a cache here" evidence would either require reaching out to the .dll (to do a timestamp or hash comparison, losing a lot of the performance gain of the existing implementation) or pushing the problem down to the file system level (in which case it would only work on the new file system.)

  29. Anonymous says:

    .lnk-files are broken by design.

    The fix is, as usual, to reinstall windows.

  30. > which is a problem because different users can be running different languages

    Why is that a problem?  Because when different users log in you get cache collisions?

    Overcomeable; include the language information in the cache key (or equivalently, have a cache for each language.)

    [I'm still surprised you haven't freaked out over Windows having to register a change notification on every single DLL that has evern been seen (and then at every boot, reverifying the cache in case the file was modified while offline). It seems that you're over-engineering the problem. -Raymond]

Comments are closed.