Windows TSF installer: same-process Profile/Category readback is stale after successful registration/unregistration
I am developing a native C++ Text Service using the Windows Text Services Framework (TSF).
This is a follow-up to an earlier Microsoft Q&A discussion in which I observed that a newly added or removed Language
Profile could temporarily be enumerated with the previous state from another fresh process.
In that earlier discussion, I was advised to treat registration mutation and cross-process visibility as separate phases, because the TSF profile catalog may converge asynchronously.
I am now seeing a related but more specific issue in an installer/uninstaller, involving both Language Profiles and TSF Categories.
Environment
- Native C++20
x64 Release build
COM initialized with:
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
The installer and the external verification process run as the same user, same Windows session, and elevated.
Exact Windows build was not captured in the saved evidence and can be provided in a follow-up if required.
Registration sequence
The installer currently uses the older ITfInputProcessorProfiles registration APIs:
profiles->Register(textServiceClsid);
profiles->AddLanguageProfile(
textServiceClsid,
0x0411,
profileGuid,
L"IEIP Windows",
...);
categories->RegisterCategory(
textServiceClsid,
GUID_TFCAT_CATEGORY_OF_TIP,
auxiliaryCategoryGuid);
categories->RegisterCategory(
textServiceClsid,
auxiliaryCategoryGuid,
textServiceClsid);
All executed calls reach the post-registration verification without returning a failed HRESULT.
The COM server is registered before these calls.
Same-process registration readback
After registration, the original registration objects are released.
For every verification attempt, the installer creates new objects with CoCreateInstance:
CLSID_TF_InputProcessorProfiles
CLSID_TF_CategoryMgr
It then checks:
SERVICE:
ITfInputProcessorProfiles::EnumInputProcessorInfo
PROFILE:
ITfInputProcessorProfiles::EnumLanguageProfiles(0x0411)
CATEGORY_OF_TIP:
ITfCategoryMgr::EnumItemsInCategory(GUID_TFCAT_CATEGORY_OF_TIP)
AUXILIARY CATEGORY:
ITfCategoryMgr::EnumItemsInCategory(auxiliaryCategoryGuid)
The installer retries three times with Sleep(100) between attempts.
All three attempts produced the same result:
SERVICE PASS
PROFILE NO_MATCH
CATEGORY_OF_TIP NO_MATCH
AUXILIARY CATEGORY PASS
For the two NO_MATCH cases:
enumerator creation: S_OK
Next(): S_FALSE
fetched: 0
So these were not API failures. The enumerators completed without finding the expected item.
The installer therefore returned failure.
Fresh-process observation after install
Approximately 2.2 seconds after the installer process exited, a separate read-only x64 process was started under the same user/session/admin context.
That fresh process observed all of the following as present:
Text Service present
Language Profile present
CATEGORY_OF_TIP present
Auxiliary Category present
The fresh verifier uses the same legacy enumeration APIs, and additionally checks:
ITfInputProcessorProfileMgr::EnumProfiles
ITfCategoryMgr::EnumCategoriesInItem
The forward and reverse Category observations were consistent.
This means that in this test:
same installer process:
PROFILE / CATEGORY_OF_TIP = not visible
fresh process ~2 seconds later:
PROFILE / CATEGORY_OF_TIP = visible
I cannot yet distinguish whether the important factor is the process boundary, the approximately two-second delay, or both.
Unregistration shows the opposite problem
The uninstall sequence is approximately:
// Deactivate if active
ITfInputProcessorProfileMgr::DeactivateProfile(...);
ITfInputProcessorProfiles::EnableLanguageProfile(..., FALSE);
ITfCategoryMgr::UnregisterCategory(... auxiliary ...);
ITfCategoryMgr::UnregisterCategory(... CATEGORY_OF_TIP ...);
ITfInputProcessorProfiles::RemoveLanguageProfile(...);
ITfInputProcessorProfiles::Unregister(...);
// remove COM registration
Immediately afterward, the same process verifies absence using the same ITfInputProcessorProfiles and ITfCategoryMgr instances that were used during unregistration.
That immediate same-process verification sometimes reports that something is still registered, so the installer returns:
Unregister verification failed
However, approximately 1.9 seconds later, a separate fresh process reports:
COM absent
Text Service absent
Language Profile absent
Categories absent
Active Profile false
So in practice the unregistration mutation appears to have succeeded even though the immediate same-process verification failed.
Previous finding
In my previous TSF investigation I observed:
AddLanguageProfile -> success
fresh enumeration shortly afterward -> profile absent
later -> profile present
RemoveLanguageProfile -> success
fresh enumeration shortly afterward -> profile still present
later -> profile absent
I was advised that Language Profile catalog visibility may converge asynchronously and that there is no documented notification meaning "registration is now globally visible."
The new observation is narrower:
it also involves ITfCategoryMgr, and
the installer may see a different state from a fresh process even after recreating the COM interfaces.
Questions
1. Does ITfCategoryMgr have the same eventual-consistency behavior?
Can the result of:
RegisterCategory(...)
UnregisterCategory(...)
legitimately remain temporarily invisible/stale to:
EnumItemsInCategory(...)
EnumCategoriesInItem(...)
even after the mutation API has successfully returned?
2. Can this stale catalog view be process-local?
If I release ITfInputProcessorProfiles and ITfCategoryMgr and then create new instances with CoCreateInstance in the same STA process, is it still possible for that process to retain an older TSF catalog view?
In other words, is a new COM object in the same process expected to refresh the catalog, or can the cache/session state survive for the process lifetime?
3. What is the recommended installer success contract?
Should an installer treat:
successful registration/unregistration HRESULTs
as the mutation result, and treat Profile/Category enumeration as a separate eventual-convergence check?
Should immediate same-process enumeration not be used as a hard success/failure condition?
Is there a documented method to force or refresh the TSF registration catalog before verifying it?
4. Is the STA message pump relevant here?
The Setup process uses COINIT_APARTMENTTHREADED, but during these short registration/readback retries it does not run an explicit message pump.
Could that affect registration/catalog visibility, or is message pumping relevant only to runtime activation/notification behavior and not to Register, AddLanguageProfile, RegisterCategory, etc.?
5. Should modern code use ITfInputProcessorProfileMgr::RegisterProfile / UnregisterProfile instead?
The current Microsoft documentation says that on Windows Vista and later, ITfInputProcessorProfileMgr is recommended instead of several older ITfInputProcessorProfiles methods, including:
Register
Unregister
AddLanguageProfile
RemoveLanguageProfile
EnumInputProcessorInfo
EnumLanguageProfiles
For a current Windows installer, should I replace the current registration path with:
ITfInputProcessorProfileMgr::RegisterProfile
ITfInputProcessorProfileMgr::UnregisterProfile
If so, is this simply the preferred modern API surface, or does it also provide different synchronization/visibility semantics that would avoid the behavior described above?
Goal
I am not trying to make TSF registration a runtime hot path.
I want the installer/uninstaller to have a correct success/failure contract and to avoid incorrectly reporting failure when TSF has actually completed the persistent mutation but its catalog view has not yet converged.
I would appreciate guidance on the intended TSF installer pattern, especially for Language Profile and Category verification.
Thank you.
Update – October 4, 2026
Additional reproduction result:
I have now repeated the installation using a newly assembled package located entirely under the intended E: drive project directory, with all product binaries unchanged.
The result was the same as before.
The installer performed one installation attempt only.
Its same-process readback produced the following result on all three attempts:
SERVICE PASS
PROFILE NO_MATCH
CATEGORY_OF_TIP NO_MATCH
AUXILIARY CATEGORY PASS
The Setup process exited with code 1 and did not reach PRODUCT_SETUP_COMPLETED.
Immediately afterward, a separate fresh read-only x64 observer reported all four registrations as present:
Text Service present
Language Profile present
CATEGORY_OF_TIP present
Auxiliary Category present
The owner record remained in PREPARED state, and both the owner path and COM server path pointed to the new E: drive package.
The text service was not active, the product remained OFF, no IEIP process was running, and neither the old C: drive DLL nor the new E: drive DLL was loaded in the observed processes.
No retry, resume, recovery, enable, activation, or second installation attempt was performed.
This reproduction suggests that the behavior is not specific to the previous C: drive workspace or package location.
The same discrepancy remains:
same Setup process:
PROFILE / CATEGORY_OF_TIP = not visible
fresh process immediately afterward:
PROFILE / CATEGORY_OF_TIP = visible
I am leaving the installation in this stopped state while waiting for guidance on the correct TSF installer verification contract.