diff --git a/src/Compartment/Compartment.cpp b/src/Compartment/Compartment.cpp index faa631fe..5695e12c 100644 --- a/src/Compartment/Compartment.cpp +++ b/src/Compartment/Compartment.cpp @@ -190,8 +190,10 @@ HRESULT CCompartment::_ClearCompartment() // ctor //---------------------------------------------------------------------------- -CCompartmentEventSink::CCompartmentEventSink(_In_ CESCALLBACK pfnCallback, _In_ void *pv) -{ +CCompartmentEventSink::CCompartmentEventSink(_In_ CESCALLBACK pfnCallback, _In_ void *pv) +{ + _pCompartment = nullptr; + _dwCookie = TF_INVALID_COOKIE; _pfnCallback = pfnCallback; _pv = pv; _refCount = 1; @@ -280,8 +282,17 @@ STDAPI CCompartmentEventSink::OnChange(_In_ REFGUID guidCompartment) // //---------------------------------------------------------------------------- -HRESULT CCompartmentEventSink::_Advise(_In_ IUnknown *punk, _In_ REFGUID guidCompartment) -{ +HRESULT CCompartmentEventSink::_Advise(_In_ IUnknown *punk, _In_ REFGUID guidCompartment) +{ + if (!punk) + { + return E_INVALIDARG; + } + if (_pCompartment) + { + _Unadvise(); + } + HRESULT hr = S_OK; ITfCompartmentMgr *pCompartmentMgr = nullptr; ITfSource *pSource = nullptr; @@ -293,19 +304,28 @@ HRESULT CCompartmentEventSink::_Advise(_In_ IUnknown *punk, _In_ REFGUID guidCom } hr = pCompartmentMgr->GetCompartment(guidCompartment, &_pCompartment); - if (SUCCEEDED(hr)) + if (SUCCEEDED(hr)) { hr = _pCompartment->QueryInterface(IID_ITfSource, (void **)&pSource); if (SUCCEEDED(hr)) { - hr = pSource->AdviseSink(IID_ITfCompartmentEventSink, this, &_dwCookie); - pSource->Release(); - } - } + hr = pSource->AdviseSink(IID_ITfCompartmentEventSink, this, &_dwCookie); + pSource->Release(); + } + } pCompartmentMgr->Release(); - return hr; + if (FAILED(hr)) + { + if (_pCompartment) + { + _pCompartment->Release(); + _pCompartment = nullptr; + } + _dwCookie = TF_INVALID_COOKIE; + } + return hr; } //+--------------------------------------------------------------------------- @@ -314,21 +334,30 @@ HRESULT CCompartmentEventSink::_Advise(_In_ IUnknown *punk, _In_ REFGUID guidCom // //---------------------------------------------------------------------------- -HRESULT CCompartmentEventSink::_Unadvise() -{ +HRESULT CCompartmentEventSink::_Unadvise() +{ + if (!_pCompartment) + { + _dwCookie = TF_INVALID_COOKIE; + return S_OK; + } + HRESULT hr = S_OK; ITfSource *pSource = nullptr; - hr = _pCompartment->QueryInterface(IID_ITfSource, (void **)&pSource); - if (SUCCEEDED(hr)) - { - hr = pSource->UnadviseSink(_dwCookie); - pSource->Release(); + if (_dwCookie != TF_INVALID_COOKIE) + { + hr = _pCompartment->QueryInterface(IID_ITfSource, (void **)&pSource); + if (SUCCEEDED(hr)) + { + hr = pSource->UnadviseSink(_dwCookie); + pSource->Release(); + } } _pCompartment->Release(); _pCompartment = nullptr; - _dwCookie = 0; + _dwCookie = TF_INVALID_COOKIE; return hr; } diff --git a/src/Composition/Composition.cpp b/src/Composition/Composition.cpp index 2cd7196b..949fc03d 100644 --- a/src/Composition/Composition.cpp +++ b/src/Composition/Composition.cpp @@ -5,8 +5,9 @@ #include #include #include -#include "FanyDefines.h" -#include "../Utils/PerfTimer.h" +#include "FanyDefines.h" +#include "Ipc.h" +#include "../Utils/PerfTimer.h" //+--------------------------------------------------------------------------- // @@ -16,39 +17,66 @@ // someone other than this service ends a composition. //---------------------------------------------------------------------------- -STDAPI CMetasequoiaIME::OnCompositionTerminated(TfEditCookie ecWrite, _In_ ITfComposition *pComposition) -{ - PerfTimer timer; - // Clear dummy composition - PerfTimer removeDummyTimer; - _RemoveDummyCompositionForComposing(ecWrite, pComposition); - double removeDummyElapsedMs = removeDummyTimer.ElapsedMs(); - - // Clear display attribute and end composition, _EndComposition will release composition for us - ITfContext *pContext = _pContext; - const bool hadContext = (pContext != nullptr); - if (pContext) - { - pContext->AddRef(); - } - - PerfTimer endCompositionTimer; - _EndComposition(_pContext); - double endCompositionElapsedMs = endCompositionTimer.ElapsedMs(); - - PerfTimer deleteCandidateTimer; - _DeleteCandidateList(FALSE, pContext); - double deleteCandidateElapsedMs = deleteCandidateTimer.ElapsedMs(); - - if (pContext) - { - pContext->Release(); - pContext = nullptr; - } - - - return S_OK; -} +STDAPI CMetasequoiaIME::OnCompositionTerminated(TfEditCookie ecWrite, _In_ ITfComposition *pComposition) +{ + PerfTimer timer; + if (pComposition == nullptr || !_IsCompositionCurrent(pComposition)) + { + // A delayed termination callback for an older composition must never + // delete the current candidate presenter or release newer ownership. + return S_OK; + } + + // The callback already carries a write cookie and the host has already + // ended this exact composition. Detach ownership before making COM calls, + // so a re-entrant/stale callback cannot observe it as current or tear down + // a composition created later. + ITfComposition *terminatedComposition = pComposition; + terminatedComposition->AddRef(); + _pComposition->Release(); + _pComposition = nullptr; + + ITfContext *ownerContext = _pContext; + if (ownerContext) + { + ownerContext->AddRef(); + _pContext->Release(); + _pContext = nullptr; + } + + uint64_t nextEpoch = + _compositionEpoch.fetch_add(1, std::memory_order_acq_rel) + 1; + if (nextEpoch == 0) + { + _compositionEpoch.fetch_add(1, std::memory_order_acq_rel); + } + + // Detach and end the old candidate/session before the COM cleanup calls + // below can re-enter and create a presenter for a newer composition. + _DeleteCandidateList(FALSE, ownerContext); + if (Global::g_connected) + { + // EndCandidateUiSession is intentionally idempotent, but a presenter + // can exist before its UI session becomes active. Always send one + // exact routed clear so the Server cannot retain that composition. + SendHideCandidateWndEventToUIProcess(); + } + + _RemoveDummyCompositionForComposing(ecWrite, terminatedComposition); + if (ownerContext) + { + _ClearCompositionDisplayAttributes(ecWrite, ownerContext, + terminatedComposition); + } + terminatedComposition->Release(); + + if (ownerContext) + { + ownerContext->Release(); + } + + return S_OK; +} //+--------------------------------------------------------------------------- // @@ -67,10 +95,15 @@ BOOL CMetasequoiaIME::_IsComposing() // //---------------------------------------------------------------------------- -void CMetasequoiaIME::_SetComposition(_In_ ITfComposition *pComposition) -{ - _pComposition = pComposition; -} +void CMetasequoiaIME::_SetComposition(_In_ ITfComposition *pComposition) +{ + _pComposition = pComposition; + uint64_t nextEpoch = _compositionEpoch.fetch_add(1, std::memory_order_acq_rel) + 1; + if (nextEpoch == 0) + { + _compositionEpoch.fetch_add(1, std::memory_order_acq_rel); + } +} //+--------------------------------------------------------------------------- // diff --git a/src/Composition/CompositionProcessorEngine.cpp b/src/Composition/CompositionProcessorEngine.cpp index 86515162..4730858f 100644 --- a/src/Composition/CompositionProcessorEngine.cpp +++ b/src/Composition/CompositionProcessorEngine.cpp @@ -110,8 +110,10 @@ CCompositionProcessorEngine::CCompositionProcessorEngine() _pCompartmentConversion = nullptr; _pCompartmentKeyboardOpenEventSink = nullptr; _pCompartmentConversionEventSink = nullptr; - _pCompartmentDoubleSingleByteEventSink = nullptr; - _pCompartmentPunctuationEventSink = nullptr; + _pCompartmentDoubleSingleByteEventSink = nullptr; + _pCompartmentPunctuationEventSink = nullptr; + _pOwnerThreadMgr = nullptr; + _ownerMsgWndHandle = nullptr; _hasWildcardIncludedInKeystrokeBuffer = FALSE; @@ -176,12 +178,18 @@ CCompositionProcessorEngine::~CCompositionProcessorEngine() delete _pCompartmentDoubleSingleByteEventSink; _pCompartmentDoubleSingleByteEventSink = nullptr; } - if (_pCompartmentPunctuationEventSink) + if (_pCompartmentPunctuationEventSink) { _pCompartmentPunctuationEventSink->_Unadvise(); delete _pCompartmentPunctuationEventSink; - _pCompartmentPunctuationEventSink = nullptr; - } + _pCompartmentPunctuationEventSink = nullptr; + } + if (_pOwnerThreadMgr) + { + _pOwnerThreadMgr->Release(); + _pOwnerThreadMgr = nullptr; + } + _ownerMsgWndHandle = nullptr; } @@ -214,7 +222,17 @@ BOOL CCompositionProcessorEngine::SetupLanguageProfile(LANGID langid, REFGUID gu _isComLessMode = isComLessMode; _langid = langid; _guidProfile = guidLanguageProfile; - _tfClientId = tfClientId; + _tfClientId = tfClientId; + if (_pOwnerThreadMgr != pThreadMgr) + { + if (_pOwnerThreadMgr) + { + _pOwnerThreadMgr->Release(); + } + _pOwnerThreadMgr = pThreadMgr; + _pOwnerThreadMgr->AddRef(); + } + _ownerMsgWndHandle = Global::msgWndHandle; SetupPreserved(pThreadMgr, tfClientId); InitializeMetasequoiaIMECompartment(pThreadMgr, tfClientId); @@ -836,22 +854,64 @@ BOOL CCompositionProcessorEngine::CheckShiftKeyOnly(_In_ CMetasequoiaImeArray_pOwnerThreadMgr; + if (!pThreadMgr) + { + return E_UNEXPECTED; + } + pThreadMgr->AddRef(); + const HWND ownerWindow = fakeThis->_ownerMsgWndHandle; if (IsEqualGUID(guidCompartment, Global::MetasequoiaIMEGuidCompartmentDoubleSingleByte)) { @@ -1280,7 +1343,10 @@ HRESULT CCompositionProcessorEngine::CompartmentCallback(_In_ void *pv, REFGUID CompartmentDoubleSingleByte._GetCompartmentBOOL(isDoubleSingleByte); // 0: halfwidth, 1: fullwidth // SendDoubleSingleByteSwitchEventToUIProcessViaNamedPipe(isDoubleSingleByte ? 1 : 0); - PostMessage(Global::msgWndHandle, WM_UpdateDoubleSingleByte, (WPARAM)(isDoubleSingleByte ? 1 : 0), 0); + if (ownerWindow && IsWindow(ownerWindow)) + { + PostMessage(ownerWindow, WM_UpdateDoubleSingleByte, (WPARAM)(isDoubleSingleByte ? 1 : 0), 0); + } fakeThis->PrivateCompartmentsUpdated(pThreadMgr); } else if (IsEqualGUID(guidCompartment, Global::MetasequoiaIMEGuidCompartmentPunctuation)) @@ -1290,7 +1356,10 @@ HRESULT CCompositionProcessorEngine::CompartmentCallback(_In_ void *pv, REFGUID Global::MetasequoiaIMEGuidCompartmentPunctuation); CompartmentPunctuation._GetCompartmentBOOL(isPunctuation); // SendPuncSwitchEventToUIProcessViaNamedPipe(isPunctuation ? 1 : 0); - PostMessage(Global::msgWndHandle, WM_UpdatePuncMode, (WPARAM)(isPunctuation ? 1 : 0), 0); + if (ownerWindow && IsWindow(ownerWindow)) + { + PostMessage(ownerWindow, WM_UpdatePuncMode, (WPARAM)(isPunctuation ? 1 : 0), 0); + } fakeThis->PrivateCompartmentsUpdated(pThreadMgr); } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION) || @@ -1319,7 +1388,10 @@ HRESULT CCompositionProcessorEngine::CompartmentCallback(_In_ void *pv, REFGUID } // SendIMESwitchEventToUIProcessViaNamedPipe(isOpen ? 1 : 0); - PostMessage(Global::msgWndHandle, WM_UpdateIMEStatus, (WPARAM)(isOpen ? 1 : 0), 0); + if (ownerWindow && IsWindow(ownerWindow)) + { + PostMessage(ownerWindow, WM_UpdateIMEStatus, (WPARAM)(isOpen ? 1 : 0), 0); + } fakeThis->KeyboardOpenCompartmentUpdated(pThreadMgr); } @@ -1742,11 +1814,52 @@ void CCompositionProcessorEngine::SetDefaultCandidateTextFont() // // CCompositionProcessorEngine // -////////////////////////////////////////////////////////////////////// - -//+--------------------------------------------------------------------------- -// -// CCompositionProcessorEngine::IsVirtualKeyNeed +////////////////////////////////////////////////////////////////////// + +BOOL CCompositionProcessorEngine::IsVirtualKeyNeedForFreshComposition( + UINT uCode, _In_reads_(1) WCHAR *pwch, + _Out_opt_ _KEYSTROKE_STATE *pKeyState) +{ + if (pKeyState) + { + pKeyState->Category = CATEGORY_NONE; + pKeyState->Function = FUNCTION_NONE; + } + + // Classify against an actually empty composition. This path is used while + // the old focus session is still being cancelled, so none of its candidate + // mode, wildcard flags, or virtual-key buffer may affect the first key in + // the replacement session. + if (IsManualPinyinSeparatorInComposition(pwch ? *pwch : 0, FALSE, + CANDIDATE_NONE, 0)) + { + if (pKeyState) + { + pKeyState->Category = CATEGORY_COMPOSING; + pKeyState->Function = FUNCTION_INPUT; + } + return TRUE; + } + if (IsVirtualKeyKeystrokeComposition(uCode, pKeyState, FUNCTION_INPUT)) + { + return TRUE; + } + if (pwch && IsWildcard() && IsWildcardChar(*pwch) && + !IsDisableWildcardAtFirst()) + { + if (pKeyState) + { + pKeyState->Category = CATEGORY_COMPOSING; + pKeyState->Function = FUNCTION_INPUT; + } + return TRUE; + } + return FALSE; +} + +//+--------------------------------------------------------------------------- +// +// CCompositionProcessorEngine::IsVirtualKeyNeed // // Test virtual key code need to the Composition Processor Engine. // param diff --git a/src/Composition/CompositionProcessorEngine.h b/src/Composition/CompositionProcessorEngine.h index f83403ab..96f12cad 100644 --- a/src/Composition/CompositionProcessorEngine.h +++ b/src/Composition/CompositionProcessorEngine.h @@ -7,11 +7,19 @@ #include "Compartment.h" #include "define.h" -class CCompositionProcessorEngine -{ +class CCompositionProcessorEngine +{ friend class CMetasequoiaIME; - public: + public: + enum class PreservedKeyAction + { + None, + ToggleImeMode, + ToggleDoubleSingleByteMode, + TogglePunctuationMode + }; + CCompositionProcessorEngine(void); ~CCompositionProcessorEngine(void); @@ -30,8 +38,10 @@ class CCompositionProcessorEngine return MAKELCID(_langid, SORT_DEFAULT); } - BOOL IsVirtualKeyNeed(UINT uCode, _In_reads_(1) WCHAR *pwch, BOOL fComposing, CANDIDATE_MODE candidateMode, - BOOL hasCandidateWithWildcard, _Out_opt_ _KEYSTROKE_STATE *pKeyState); + BOOL IsVirtualKeyNeed(UINT uCode, _In_reads_(1) WCHAR *pwch, BOOL fComposing, CANDIDATE_MODE candidateMode, + BOOL hasCandidateWithWildcard, _Out_opt_ _KEYSTROKE_STATE *pKeyState); + BOOL IsVirtualKeyNeedForFreshComposition(UINT uCode, _In_reads_(1) WCHAR *pwch, + _Out_opt_ _KEYSTROKE_STATE *pKeyState); BOOL AddVirtualKey(WCHAR wch); void RemoveVirtualKey(DWORD_PTR dwIndex); @@ -64,9 +74,12 @@ class CCompositionProcessorEngine BOOL isIncrementalWordSearch, BOOL isWildcardSearch // ); - // Preserved key handler - void OnPreservedKey(ITfContext *pContext, REFGUID rguid, _Out_ BOOL *pIsEaten, _In_ ITfThreadMgr *pThreadMgr, - TfClientId tfClientId, BOOL *pNeedToggleIMEMode); + // Preserved key handler + BOOL IsPreservedKeyEligible(REFGUID rguid); + PreservedKeyAction GetPreservedKeyAction(REFGUID rguid) const; + void OnPreservedKey(ITfContext *pContext, REFGUID rguid, _Out_ BOOL *pIsEaten, _In_ ITfThreadMgr *pThreadMgr, + TfClientId tfClientId, BOOL *pNeedToggleIMEMode, + BOOL isPrevalidated = FALSE); // Toggle IME Mode void ToggleIMEMode(_In_ ITfThreadMgr *pThreadMgr, TfClientId tfClientId); @@ -228,8 +241,10 @@ class CCompositionProcessorEngine CCompartment *_pCompartmentConversion; CCompartmentEventSink *_pCompartmentConversionEventSink; CCompartmentEventSink *_pCompartmentKeyboardOpenEventSink; - CCompartmentEventSink *_pCompartmentDoubleSingleByteEventSink; - CCompartmentEventSink *_pCompartmentPunctuationEventSink; + CCompartmentEventSink *_pCompartmentDoubleSingleByteEventSink; + CCompartmentEventSink *_pCompartmentPunctuationEventSink; + ITfThreadMgr *_pOwnerThreadMgr; + HWND _ownerMsgWndHandle; // Configuration data BOOL _isWildcard : 1; diff --git a/src/Composition/EndComposition.cpp b/src/Composition/EndComposition.cpp index a7ddc361..edacc65e 100644 --- a/src/Composition/EndComposition.cpp +++ b/src/Composition/EndComposition.cpp @@ -23,17 +23,37 @@ class CEndCompositionEditSession : public CEditSessionBase { public: - CEndCompositionEditSession(_In_ CMetasequoiaIME *pTextService, _In_ ITfContext *pContext) - : CEditSessionBase(pTextService, pContext) + CEndCompositionEditSession(_In_ CMetasequoiaIME *pTextService, _In_ ITfContext *pContext, + _In_ ITfComposition *expectedComposition, uint64_t focusToken, + bool bypassFocusValidation) + : CEditSessionBase(pTextService, pContext), _expectedComposition(expectedComposition), + _focusToken(focusToken), _bypassFocusValidation(bypassFocusValidation) { + _expectedComposition->AddRef(); + } + + ~CEndCompositionEditSession() override + { + _expectedComposition->Release(); } // ITfEditSession STDMETHODIMP DoEditSession(TfEditCookie ec) { + if (!_pTextService->_IsCompositionCurrent(_expectedComposition) || + (!_bypassFocusValidation && + !_pTextService->_IsFocusSessionCurrent(_focusToken, _pContext))) + { + return S_FALSE; + } _pTextService->_TerminateComposition(ec, _pContext, TRUE); return S_OK; } + + private: + ITfComposition *_expectedComposition; + uint64_t _focusToken; + bool _bypassFocusValidation; }; ////////////////////////////////////////////////////////////////////// @@ -55,13 +75,27 @@ void CMetasequoiaIME::_TerminateComposition(TfEditCookie ec, _In_ ITfContext *pC if (_pComposition != nullptr) { + // Every operation below crosses COM and can re-enter + // OnCompositionTerminated. Hold exact local references and only clear + // service ownership if the same composition is still current after + // the call returns. + ITfComposition *terminatingComposition = _pComposition; + terminatingComposition->AddRef(); + ITfContext *ownerContext = _pContext; + if (ownerContext) + { + ownerContext->AddRef(); + } + // remove the display attribute from the composition range. PerfTimer clearDisplayAttrTimer; - _ClearCompositionDisplayAttributes(ec, pContext); + _ClearCompositionDisplayAttributes(ec, pContext, + terminatingComposition); double clearDisplayAttrElapsedMs = clearDisplayAttrTimer.ElapsedMs(); PerfTimer endCompositionTimer; - if (FAILED(_pComposition->EndComposition(ec))) + const HRESULT endResult = terminatingComposition->EndComposition(ec); + if (FAILED(endResult) && _pComposition == terminatingComposition) { // if we fail to EndComposition, then we need to close the reverse reading window. _DeleteCandidateList(TRUE, pContext); @@ -69,19 +103,37 @@ void CMetasequoiaIME::_TerminateComposition(TfEditCookie ec, _In_ ITfContext *pC double endCompositionElapsedMs = endCompositionTimer.ElapsedMs(); PerfTimer releaseCompositionTimer; - _pComposition->Release(); - _pComposition = nullptr; - double releaseCompositionElapsedMs = releaseCompositionTimer.ElapsedMs(); - - double releaseContextElapsedMs = 0; - if (_pContext) + if (_pComposition == terminatingComposition) { - PerfTimer releaseContextTimer; - _pContext->Release(); - _pContext = nullptr; - releaseContextElapsedMs = releaseContextTimer.ElapsedMs(); - } + _pComposition->Release(); + _pComposition = nullptr; + uint64_t nextEpoch = + _compositionEpoch.fetch_add(1, std::memory_order_acq_rel) + 1; + if (nextEpoch == 0) + { + _compositionEpoch.fetch_add(1, std::memory_order_acq_rel); + } + // Clear the context only as part of releasing this exact + // composition. A re-entrant callback may already have installed a + // newer composition on the same ITfContext pointer. + if (_pContext == ownerContext && _pContext) + { + PerfTimer releaseContextTimer; + _pContext->Release(); + _pContext = nullptr; + double releaseContextElapsedMs = + releaseContextTimer.ElapsedMs(); + } + } + double releaseCompositionElapsedMs = + releaseCompositionTimer.ElapsedMs(); + + if (ownerContext) + { + ownerContext->Release(); + } + terminatingComposition->Release(); } } @@ -91,17 +143,36 @@ void CMetasequoiaIME::_TerminateComposition(TfEditCookie ec, _In_ ITfContext *pC // //---------------------------------------------------------------------------- -void CMetasequoiaIME::_EndComposition(_In_opt_ ITfContext *pContext) +HRESULT CMetasequoiaIME::_EndComposition(_In_opt_ ITfContext *pContext, + _In_opt_ ITfComposition *expectedComposition, + bool bypassFocusValidation) { - CEndCompositionEditSession *pEditSession = new (std::nothrow) CEndCompositionEditSession(this, pContext); + ITfComposition *target = expectedComposition ? expectedComposition : _pComposition; + if (pContext == nullptr || target == nullptr) + { + return S_FALSE; + } + CEndCompositionEditSession *pEditSession = + new (std::nothrow) CEndCompositionEditSession(this, pContext, target, + _CaptureFocusSessionToken(), bypassFocusValidation); HRESULT hr = S_OK; PerfTimer timer; if (nullptr != pEditSession) { PerfTimer requestTimer; - pContext->RequestEditSession(_tfClientId, pEditSession, TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, &hr); + HRESULT editSessionHr = E_FAIL; + const HRESULT requestHr = + pContext->RequestEditSession(_tfClientId, pEditSession, + TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, + &editSessionHr); + hr = FAILED(requestHr) ? requestHr : editSessionHr; double requestElapsedMs = requestTimer.ElapsedMs(); pEditSession->Release(); } + else + { + hr = E_OUTOFMEMORY; + } + return hr; } diff --git a/src/DisplayAttribute/DisplayAttribute.cpp b/src/DisplayAttribute/DisplayAttribute.cpp index f9b64f77..a3291c18 100644 --- a/src/DisplayAttribute/DisplayAttribute.cpp +++ b/src/DisplayAttribute/DisplayAttribute.cpp @@ -8,13 +8,21 @@ // //---------------------------------------------------------------------------- -void CMetasequoiaIME::_ClearCompositionDisplayAttributes(TfEditCookie ec, _In_ ITfContext *pContext) -{ - ITfRange *pRangeComposition = nullptr; - ITfProperty *pDisplayAttributeProperty = nullptr; - - // get the compositon range. - if (FAILED(_pComposition->GetRange(&pRangeComposition))) +void CMetasequoiaIME::_ClearCompositionDisplayAttributes( + TfEditCookie ec, _In_ ITfContext *pContext, + _In_opt_ ITfComposition *expectedComposition) +{ + ITfRange *pRangeComposition = nullptr; + ITfProperty *pDisplayAttributeProperty = nullptr; + ITfComposition *composition = + expectedComposition ? expectedComposition : _pComposition; + if (composition == nullptr) + { + return; + } + + // get the compositon range. + if (FAILED(composition->GetRange(&pRangeComposition))) { return; } diff --git a/src/Edit/TextEditSink.cpp b/src/Edit/TextEditSink.cpp index a81269b2..3e712f80 100644 --- a/src/Edit/TextEditSink.cpp +++ b/src/Edit/TextEditSink.cpp @@ -1,6 +1,7 @@ #include "Private.h" -#include "Globals.h" -#include "MetasequoiaIME.h" +#include "Globals.h" +#include "MetasequoiaIME.h" +#include "Ipc.h" //+--------------------------------------------------------------------------- // @@ -9,10 +10,14 @@ // Called by the system whenever anyone releases a write-access document lock. //---------------------------------------------------------------------------- -STDAPI CMetasequoiaIME::OnEndEdit(__RPC__in_opt ITfContext *pContext, TfEditCookie ecReadOnly, - __RPC__in_opt ITfEditRecord *pEditRecord) -{ - BOOL isSelectionChanged; +STDAPI CMetasequoiaIME::OnEndEdit(__RPC__in_opt ITfContext *pContext, TfEditCookie ecReadOnly, + __RPC__in_opt ITfEditRecord *pEditRecord) +{ + if (!IsNamedpipeFocusStateOwner(this) || !Global::g_connected) + { + return S_OK; + } + BOOL isSelectionChanged; // // did the selection change? @@ -49,7 +54,10 @@ STDAPI CMetasequoiaIME::OnEndEdit(__RPC__in_opt ITfContext *pContext, TfEditCook { if (!_IsRangeCovered(ecReadOnly, tfSelection.range, pRangeComposition)) { - _EndComposition(pContext); + if (FAILED(_EndComposition(pContext))) + { + MarkNamedpipeSessionDirtyForOwner(this); + } } pRangeComposition->Release(); @@ -70,7 +78,7 @@ STDAPI CMetasequoiaIME::OnEndEdit(__RPC__in_opt ITfContext *pContext, TfEditCook // Always release any previous sink. //---------------------------------------------------------------------------- -BOOL CMetasequoiaIME::_InitTextEditSink(_In_ ITfDocumentMgr *pDocMgr) +BOOL CMetasequoiaIME::_InitTextEditSink(_In_opt_ ITfDocumentMgr *pDocMgr) { ITfSource *pSource = nullptr; BOOL ret = TRUE; diff --git a/src/Global/FanyDefines.h b/src/Global/FanyDefines.h index ca834962..b68ef970 100644 --- a/src/Global/FanyDefines.h +++ b/src/Global/FanyDefines.h @@ -9,7 +9,7 @@ inline std::wstring ZEN_BROWSER = L"zen.exe"; // inline std::unordered_set VSCodeSeries = {L"Code.exe", L"Code - Insiders.exe", L"VSCodium.exe"}; // inline bool IsVSCodeLike = false; inline LONG INVALID_Y = -100000; -inline float DpiScale = 1.0f; +inline thread_local float DpiScale = 1.0f; } // namespace Global namespace GlobalSettings @@ -27,25 +27,29 @@ constexpr std::string_view Pinyin = "pinyin"; constexpr std::string_view Cand = "cand"; } // namespace TsfPreeditStyle +inline std::string &tsfPreeditStyleStorage() +{ + static std::string style = std::string(TsfPreeditStyle::Raw); // 默认的原始按键序列 + return style; +} + inline const std::string &getTsfPreeditStyle() { - static const std::string style = std::string(TsfPreeditStyle::Raw); // 默认的原始按键序列 - return style; + return tsfPreeditStyleStorage(); } inline void setTsfPreeditStyle(std::string_view newStyle) { - static std::string currentStyle = std::string(TsfPreeditStyle::Raw); - currentStyle = std::string(newStyle); + tsfPreeditStyleStorage() = std::string(newStyle); } } // namespace GlobalSettings namespace GlobalIme { -inline std::wstring word_for_creating_word = L""; +inline thread_local std::wstring word_for_creating_word = L""; } namespace Global { -inline HWND msgWndHandle = nullptr; -} \ No newline at end of file +inline thread_local HWND msgWndHandle = nullptr; +} diff --git a/src/Global/Globals.cpp b/src/Global/Globals.cpp index d9e2ac23..6dfb3504 100644 --- a/src/Global/Globals.cpp +++ b/src/Global/Globals.cpp @@ -364,12 +364,12 @@ BOOL CheckModifiers(UINT modCurrent, UINT mod) // [31] Transition state //---------------------------------------------------------------------------- -USHORT ModifiersValue = 0; -BOOL IsShiftKeyDownOnly = FALSE; -BOOL IsControlKeyDownOnly = FALSE; -BOOL IsAltKeyDownOnly = FALSE; -BOOL PureShiftKeyDown = FALSE; -BOOL PureShiftKeyUp = FALSE; +thread_local USHORT ModifiersValue = 0; +thread_local BOOL IsShiftKeyDownOnly = FALSE; +thread_local BOOL IsControlKeyDownOnly = FALSE; +thread_local BOOL IsAltKeyDownOnly = FALSE; +thread_local BOOL PureShiftKeyDown = FALSE; +thread_local BOOL PureShiftKeyUp = FALSE; BOOL UpdateModifiers(WPARAM wParam, LPARAM lParam) { diff --git a/src/Global/Globals.h b/src/Global/Globals.h index b5545817..fd429ce7 100644 --- a/src/Global/Globals.h +++ b/src/Global/Globals.h @@ -114,12 +114,12 @@ LRESULT CALLBACK ThreadKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam); BOOL CheckModifiers(UINT uModCurrent, UINT uMod); BOOL UpdateModifiers(WPARAM wParam, LPARAM lParam); -extern USHORT ModifiersValue; -extern BOOL IsShiftKeyDownOnly; -extern BOOL IsControlKeyDownOnly; -extern BOOL IsAltKeyDownOnly; -extern BOOL PureShiftKeyDown; -extern BOOL PureShiftKeyUp; +extern thread_local USHORT ModifiersValue; +extern thread_local BOOL IsShiftKeyDownOnly; +extern thread_local BOOL IsControlKeyDownOnly; +extern thread_local BOOL IsAltKeyDownOnly; +extern thread_local BOOL PureShiftKeyDown; +extern thread_local BOOL PureShiftKeyUp; extern const GUID MetasequoiaIMEGuidCompartmentDoubleSingleByte; extern const GUID MetasequoiaIMEGuidCompartmentPunctuation; diff --git a/src/IME/MetasequoiaIME.cpp b/src/IME/MetasequoiaIME.cpp index 6952c37c..c075e5b9 100644 --- a/src/IME/MetasequoiaIME.cpp +++ b/src/IME/MetasequoiaIME.cpp @@ -26,17 +26,46 @@ namespace constexpr UINT_PTR TIMER_CONNECT_ALL_NAMEDPIPE = 1; constexpr UINT_PTR TIMER_CONNECT_TO_TSF_NAMEDPIPE = 2; constexpr UINT CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS = 50; -constexpr int CONNECT_ALL_NAMEDPIPE_MAX_RETRY = 3; -constexpr int CONNECT_TO_TSF_NAMEDPIPE_MAX_RETRY = 1; -int g_connectAllNamedpipeRetryCount = 0; -int g_connectToTsfNamedpipeRetryCount = 0; +constexpr UINT CONNECT_NAMEDPIPE_MAX_RETRY_INTERVAL_MS = 2000; +std::atomic nextWindowMessageToken{0}; + +UINT NextWindowMessageToken() +{ + UINT token = 0; + do + { + token = nextWindowMessageToken.fetch_add(1, std::memory_order_relaxed) + 1; + } while (token == 0); + return token; +} + +void SendCurrentImeStatusSnapshot(CMetasequoiaIME *pIME) +{ + if (!Global::g_connected || !pIME || !pIME->GetCompositionProcessorEngine() || !pIME->_GetThreadMgr()) + { + return; + } + + CCompositionProcessorEngine *engine = pIME->GetCompositionProcessorEngine(); + SendIMEStatusSnapshotToUIProcessViaNamedPipe(engine->GetIMEMode(pIME->_GetThreadMgr(), pIME->_GetClientId()), + engine->GetDoubleSingleByteMode(pIME->_GetThreadMgr(), + pIME->_GetClientId()), + engine->GetPunctuationMode(pIME->_GetThreadMgr(), + pIME->_GetClientId())); +} class CPunctuationCommitEditSession : public CEditSessionBase { public: CPunctuationCommitEditSession(CMetasequoiaIME *pTextService, ITfContext *pContext, UINT code, WCHAR wch, - LARGE_INTEGER requestStartQpc) - : CEditSessionBase(pTextService, pContext), _code(code), _wch(wch), _requestStartQpc(requestStartQpc) + uint64_t requestId, LARGE_INTEGER requestStartQpc, + std::wstring prefetchedText, uint64_t focusToken, + uint64_t compositionEpoch, + uint64_t deferredReplayToken) + : CEditSessionBase(pTextService, pContext), _code(code), _wch(wch), _requestId(requestId), + _requestStartQpc(requestStartQpc), _prefetchedText(std::move(prefetchedText)), + _focusToken(focusToken), _compositionEpoch(compositionEpoch), + _deferredReplayToken(deferredReplayToken) { if (_requestStartQpc.QuadPart == 0) { @@ -46,8 +75,36 @@ class CPunctuationCommitEditSession : public CEditSessionBase STDMETHODIMP DoEditSession(TfEditCookie ec) override { + struct Completion + { + CMetasequoiaIME *textService; + uint64_t token; + bool applied = false; + ~Completion() + { + if (textService && token != 0) + { + if (applied) + { + textService->_CompleteDeferredKeyReplay(token); + } + else + { + textService->_RetryDeferredKeyReplay(token); + } + } + } + } completion{_pTextService, _deferredReplayToken, false}; + + if (!_pTextService->_IsFocusSessionCurrent(_focusToken, _pContext) || + !_pTextService->_IsCompositionEpochCurrent(_compositionEpoch)) + { + return S_FALSE; + } PerfTimer timer; - HRESULT hr = _pTextService->_HandleCompositionPunctuation(ec, _pContext, _wch); + HRESULT hr = _pTextService->_HandleCompositionPunctuation(ec, _pContext, _wch, _requestId, + _prefetchedText); + completion.applied = hr == S_OK; LARGE_INTEGER freq = {}; LARGE_INTEGER nowQpc = {}; QueryPerformanceFrequency(&freq); @@ -60,7 +117,78 @@ class CPunctuationCommitEditSession : public CEditSessionBase private: UINT _code; WCHAR _wch; + uint64_t _requestId; LARGE_INTEGER _requestStartQpc; + std::wstring _prefetchedText; + uint64_t _focusToken; + uint64_t _compositionEpoch; + uint64_t _deferredReplayToken; +}; + +class CDeferredApplicationTextEditSession : public CEditSessionBase +{ + public: + CDeferredApplicationTextEditSession(CMetasequoiaIME *pTextService, + ITfContext *pContext, WCHAR wch, + uint64_t focusToken, + uint64_t focusGeneration, + uint64_t replayToken) + : CEditSessionBase(pTextService, pContext), _wch(wch), + _focusToken(focusToken), _focusGeneration(focusGeneration), + _replayToken(replayToken) + { + } + + STDMETHODIMP DoEditSession(TfEditCookie ec) override + { + if (!_pTextService->_IsDeferredKeyReplayCurrent( + _replayToken, _focusGeneration, _pContext) || + !_pTextService->_IsFocusSessionCurrent(_focusToken, _pContext)) + { + _pTextService->_RetryDeferredKeyReplay(_replayToken); + return S_FALSE; + } + + TF_SELECTION selection = {}; + ULONG fetched = 0; + bool textApplied = false; + HRESULT hr = _pContext->GetSelection( + ec, TF_DEFAULT_SELECTION, 1, &selection, &fetched); + if (SUCCEEDED(hr) && fetched == 1 && selection.range) + { + hr = selection.range->SetText(ec, 0, &_wch, 1); + if (SUCCEEDED(hr)) + { + // SetText is the irreversible operation. A subsequent + // selection-placement failure must not retry the character, + // otherwise the application receives it twice. + textApplied = true; + selection.range->Collapse(ec, TF_ANCHOR_END); + hr = _pContext->SetSelection(ec, 1, &selection); + } + selection.range->Release(); + } + else if (SUCCEEDED(hr)) + { + hr = E_FAIL; + } + + if (textApplied) + { + _pTextService->_CompleteDeferredKeyReplay(_replayToken); + } + else + { + _pTextService->_RetryDeferredKeyReplay(_replayToken); + } + return hr; + } + + private: + WCHAR _wch; + uint64_t _focusToken; + uint64_t _focusGeneration; + uint64_t _replayToken; }; } // namespace @@ -144,10 +272,35 @@ CMetasequoiaIME::CMetasequoiaIME() _msgWndHandle = nullptr; _pIpcThread = nullptr; - _hToTsfWorkerThreadPipe = nullptr; + _hToTsfWorkerThreadPipe.store(nullptr); + _workerPipeGeneration.store(0); + _ipcStopEvent = nullptr; _shouldStopIpcThread = false; + _ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + _workerCommitReady.store(false); + _expectedWorkerFocusToken.store(0); + _acknowledgedWorkerFocusToken.store(0); + _compositionEpoch.store(1); + _localSessionResetPending.store(false); + _localSessionResetToken.store(0); + _localResetEditSessionQueued = false; + _queuedLocalResetToken = 0; + _focusResetPending = false; + _activationRequired = false; + _focusLostToWindowsTextInputHost = false; _hasPendingServerCandidate = false; _pendingServerCandidateMsgType = Global::DataFromServerMsgType::OutofRange; + _hasDeferredKeyInFlight = false; + _deferredKeyReplayToken = 0; + _nextDeferredKeyReplayToken = 0; + _deferredKeyProjectionValid = false; + _deferredProjectedImeOpen = false; + _deferredProjectedPunctuationOpen = false; + _deferredProjectedDoubleSingleByteOpen = false; + _deferredProjectedInputLength = 0; + _deferredProjectedCandidateActive = false; + _deferredKeyFocusGeneration = 1; + _deferredKeyDrainPosted = false; } //+--------------------------------------------------------------------------- @@ -158,6 +311,11 @@ CMetasequoiaIME::CMetasequoiaIME() CMetasequoiaIME::~CMetasequoiaIME() { + _ClearDeferredKeyDowns(); + // Deactivate normally owns the unbind. Keep this owner-aware fallback for + // partial activation failures without allowing a delayed old service + // destructor to clear a newer service's TLS bindings. + UnbindNamedpipeFocusState(this); if (_pCandidateListUIPresenter) { delete _pCandidateListUIPresenter; @@ -169,6 +327,16 @@ CMetasequoiaIME::~CMetasequoiaIME() /* 处理线程的清理 */ if (_pIpcThread) { + _shouldStopIpcThread.store(true); + if (_ipcStopEvent) + { + SetEvent(_ipcStopEvent); + } + HANDLE workerPipe = _hToTsfWorkerThreadPipe.load(); + if (workerPipe && workerPipe != INVALID_HANDLE_VALUE) + { + CancelIoEx(workerPipe, nullptr); + } if (_pIpcThread->joinable()) { _pIpcThread->join(); @@ -176,37 +344,51 @@ CMetasequoiaIME::~CMetasequoiaIME() delete _pIpcThread; _pIpcThread = nullptr; } + if (_ipcStopEvent) + { + CloseHandle(_ipcStopEvent); + _ipcStopEvent = nullptr; + } } -void CMetasequoiaIME::_QueuePendingCommitCandidate(_In_z_ const WCHAR *pCommitString) +HRESULT CMetasequoiaIME::_RequestDeferredApplicationTextEditSession( + _In_ ITfContext *pContext, WCHAR wch, uint64_t expectedFocusToken, + uint64_t expectedFocusGeneration, uint64_t deferredReplayToken) { - std::lock_guard lock(_pendingCommitCandidateMutex); - _pendingCommitCandidate = pCommitString ? pCommitString : L""; + if (pContext == nullptr || wch == L'\0' || deferredReplayToken == 0) + { + _CompleteDeferredKeyReplay(deferredReplayToken); + return E_INVALIDARG; + } + + CDeferredApplicationTextEditSession *editSession = + new (std::nothrow) CDeferredApplicationTextEditSession( + this, pContext, wch, expectedFocusToken, + expectedFocusGeneration, deferredReplayToken); + if (editSession == nullptr) + { + _RetryDeferredKeyReplay(deferredReplayToken); + return E_OUTOFMEMORY; + } + + HRESULT editSessionHr = E_FAIL; + const HRESULT requestHr = pContext->RequestEditSession( + _tfClientId, editSession, TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, + &editSessionHr); + editSession->Release(); + if (FAILED(requestHr) || FAILED(editSessionHr)) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } + return FAILED(requestHr) ? requestHr : editSessionHr; } -std::wstring CMetasequoiaIME::_TakePendingCommitCandidate() -{ - std::lock_guard lock(_pendingCommitCandidateMutex); - std::wstring pendingCommitCandidate; - pendingCommitCandidate.swap(_pendingCommitCandidate); - return pendingCommitCandidate; -} - -void CMetasequoiaIME::_QueuePendingPunctuationCommitText(_In_z_ const WCHAR *pCommitString) -{ - std::lock_guard lock(_pendingCommitCandidateMutex); - _pendingPunctuationCommitText = pCommitString ? pCommitString : L""; -} - -std::wstring CMetasequoiaIME::_TakePendingPunctuationCommitText() -{ - std::lock_guard lock(_pendingCommitCandidateMutex); - std::wstring pendingPunctuationCommitText; - pendingPunctuationCommitText.swap(_pendingPunctuationCommitText); - return pendingPunctuationCommitText; -} - -HRESULT CMetasequoiaIME::_RequestDirectPunctuationEditSession(_In_ ITfContext *pContext, UINT code, WCHAR wch) +HRESULT CMetasequoiaIME::_RequestDirectPunctuationEditSession(_In_ ITfContext *pContext, UINT code, WCHAR wch, + uint64_t requestId, + std::wstring prefetchedText, + uint64_t expectedFocusToken, + uint64_t expectedCompositionEpoch, + uint64_t deferredReplayToken) { if (pContext == nullptr) { @@ -217,9 +399,18 @@ HRESULT CMetasequoiaIME::_RequestDirectPunctuationEditSession(_In_ ITfContext *p QueryPerformanceCounter(&requestStartQpc); CPunctuationCommitEditSession *pEditSession = - new (std::nothrow) CPunctuationCommitEditSession(this, pContext, code, wch, requestStartQpc); + new (std::nothrow) CPunctuationCommitEditSession(this, pContext, code, wch, requestId, + requestStartQpc, std::move(prefetchedText), + expectedFocusToken != 0 + ? expectedFocusToken + : _CaptureFocusSessionToken(), + expectedCompositionEpoch != 0 + ? expectedCompositionEpoch + : _CaptureCompositionEpoch(), + deferredReplayToken); if (pEditSession == nullptr) { + _RetryDeferredKeyReplay(deferredReplayToken); return E_OUTOFMEMORY; } @@ -228,7 +419,11 @@ HRESULT CMetasequoiaIME::_RequestDirectPunctuationEditSession(_In_ ITfContext *p HRESULT requestHr = pContext->RequestEditSession(_tfClientId, pEditSession, TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, &editSessionHr); pEditSession->Release(); - return requestHr; + if (FAILED(requestHr) || FAILED(editSessionHr)) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } + return FAILED(requestHr) ? requestHr : editSessionHr; } void CMetasequoiaIME::_QueuePendingServerCandidate(UINT msgType, _In_z_ const WCHAR *pCandidateString) @@ -259,6 +454,460 @@ bool CMetasequoiaIME::_TakePendingServerCandidate(_Out_ UINT *pMsgType, _Out_ st return true; } +bool CMetasequoiaIME::_PostAsyncKeyRequest(UINT message, UINT code, WCHAR wch, uint64_t requestId, + std::wstring prefetchedText, + uint64_t expectedFocusToken, + uint64_t expectedCompositionEpoch, + uint64_t deferredReplayToken) +{ + switch (message) + { + case WM_AsyncServerCandidateKey: + case WM_AsyncFinalizeCandidate: + case WM_AsyncPunctuationCommit: + case WM_AsyncNumberCandidateCommit: + break; + default: + _RetryDeferredKeyReplay(deferredReplayToken); + return false; + } + + constexpr size_t maxPendingAsyncKeys = 64; + const uint64_t focusToken = expectedFocusToken != 0 + ? expectedFocusToken + : _CaptureFocusSessionToken(); + const uint64_t compositionEpoch = expectedCompositionEpoch != 0 + ? expectedCompositionEpoch + : _CaptureCompositionEpoch(); + const HWND ownerWindow = _msgWndHandle; + if (focusToken == 0 || !ownerWindow || !IsWindow(ownerWindow)) + { + if (deferredReplayToken != 0) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } + else + { + MarkNamedpipeSessionDirtyForOwner(this); + } + return false; + } + + UINT token = 0; + { + std::lock_guard lock(_pendingCommitCandidateMutex); + if (_pendingAsyncKeyMessages.size() < maxPendingAsyncKeys) + { + do + { + token = NextWindowMessageToken(); + } while (token == 0 || _pendingAsyncKeyMessages.count(token) != 0); + _pendingAsyncKeyMessages.emplace( + token, AsyncKeyRequest{message, code, wch, requestId, focusToken, compositionEpoch, + deferredReplayToken, std::move(prefetchedText)}); + } + } + if (token == 0) + { + if (deferredReplayToken != 0) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } + else + { + MarkNamedpipeSessionDirtyForOwner(this); + } + return false; + } + if (!PostMessage(ownerWindow, message, static_cast(token), 0)) + { + { + std::lock_guard lock(_pendingCommitCandidateMutex); + _pendingAsyncKeyMessages.erase(token); + } + if (deferredReplayToken != 0) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } + else + { + MarkNamedpipeSessionDirtyForOwner(this); + } + return false; + } + return true; +} + +bool CMetasequoiaIME::_PostServerCandidateCommit(_In_z_ const WCHAR *candidateText) +{ + constexpr size_t maxPendingServerCommits = 64; + if (!_workerCommitReady.load(std::memory_order_acquire) || + _localSessionResetPending.load(std::memory_order_acquire) || + _acknowledgedWorkerFocusToken.load(std::memory_order_acquire) == 0) + { + return false; + } + const HWND ownerWindow = _msgWndHandle; + if (!ownerWindow || !IsWindow(ownerWindow)) + { + return false; + } + + UINT token = 0; + const uint64_t focusToken = _expectedWorkerFocusToken.load(std::memory_order_acquire); + const uint64_t compositionEpoch = _CaptureCompositionEpoch(); + { + std::lock_guard lock(_pendingCommitCandidateMutex); + if (!_workerCommitReady.load(std::memory_order_relaxed) || + _localSessionResetPending.load(std::memory_order_relaxed) || + focusToken == 0 || + _expectedWorkerFocusToken.load(std::memory_order_relaxed) != focusToken || + _acknowledgedWorkerFocusToken.load(std::memory_order_relaxed) != focusToken || + _pendingServerCommitMessages.size() >= maxPendingServerCommits) + { + return false; + } + do + { + token = NextWindowMessageToken(); + } while (token == 0 || _pendingServerCommitMessages.count(token) != 0); + _pendingServerCommitMessages.emplace( + token, WorkerCandidateCommit{candidateText ? candidateText : L"", focusToken, compositionEpoch}); + } + + if (!PostMessage(ownerWindow, WM_CommitCandidate, static_cast(token), 0)) + { + std::lock_guard lock(_pendingCommitCandidateMutex); + _pendingServerCommitMessages.erase(token); + PostMessage(ownerWindow, WM_IpcSessionDirty, 0, 0); + return false; + } + return true; +} + +bool CMetasequoiaIME::_TakeServerCandidateCommit(UINT token, _Out_ WorkerCandidateCommit &request) +{ + std::lock_guard lock(_pendingCommitCandidateMutex); + const auto entry = _pendingServerCommitMessages.find(token); + if (entry == _pendingServerCommitMessages.end()) + { + return false; + } + request = std::move(entry->second); + _pendingServerCommitMessages.erase(entry); + return true; +} + +bool CMetasequoiaIME::_PostWorkerCompartmentSwitch(UINT messageType, uint64_t focusToken) +{ + constexpr size_t maxPendingSwitches = 64; + if (messageType < Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish || + messageType > Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth || focusToken == 0 || + !_workerCommitReady.load(std::memory_order_acquire) || + _localSessionResetPending.load(std::memory_order_acquire) || + _expectedWorkerFocusToken.load(std::memory_order_acquire) != focusToken || + _acknowledgedWorkerFocusToken.load(std::memory_order_acquire) != focusToken) + { + return false; + } + + const HWND ownerWindow = _msgWndHandle; + if (!ownerWindow || !IsWindow(ownerWindow)) + { + return false; + } + + UINT token = 0; + { + std::lock_guard lock(_pendingCommitCandidateMutex); + if (!_workerCommitReady.load(std::memory_order_relaxed) || + _localSessionResetPending.load(std::memory_order_relaxed) || + _expectedWorkerFocusToken.load(std::memory_order_relaxed) != focusToken || + _acknowledgedWorkerFocusToken.load(std::memory_order_relaxed) != focusToken || + _pendingWorkerSwitchMessages.size() >= maxPendingSwitches) + { + return false; + } + do + { + token = NextWindowMessageToken(); + } while (token == 0 || _pendingWorkerSwitchMessages.count(token) != 0); + _pendingWorkerSwitchMessages.emplace( + token, WorkerCompartmentSwitch{messageType, focusToken, + _CaptureCompositionEpoch()}); + } + + if (!PostMessage(ownerWindow, WM_CheckGlobalCompartment, static_cast(token), 0)) + { + std::lock_guard lock(_pendingCommitCandidateMutex); + _pendingWorkerSwitchMessages.erase(token); + return false; + } + return true; +} + +bool CMetasequoiaIME::_TakeWorkerCompartmentSwitch(UINT token, _Out_ WorkerCompartmentSwitch &request) +{ + std::lock_guard lock(_pendingCommitCandidateMutex); + const auto entry = _pendingWorkerSwitchMessages.find(token); + if (entry == _pendingWorkerSwitchMessages.end()) + { + return false; + } + request = entry->second; + _pendingWorkerSwitchMessages.erase(entry); + return true; +} + +bool CMetasequoiaIME::_TakeAsyncKeyRequest(UINT message, UINT token, _Out_ AsyncKeyRequest &request) +{ + std::lock_guard lock(_pendingCommitCandidateMutex); + const auto entry = _pendingAsyncKeyMessages.find(token); + if (entry == _pendingAsyncKeyMessages.end() || entry->second.message != message) + { + return false; + } + request = std::move(entry->second); + _pendingAsyncKeyMessages.erase(entry); + return true; +} + +void CMetasequoiaIME::_ClearAsyncKeyRequests() +{ + std::vector deferredReplayTokens; + { + std::lock_guard lock(_pendingCommitCandidateMutex); + for (const auto &entry : _pendingAsyncKeyMessages) + { + if (entry.second.deferredReplayToken != 0) + { + deferredReplayTokens.push_back( + entry.second.deferredReplayToken); + } + } + _pendingAsyncKeyMessages.clear(); + } + for (uint64_t replayToken : deferredReplayTokens) + { + _RetryDeferredKeyReplay(replayToken); + } +} + +void CMetasequoiaIME::_ClearPendingIpcRequests() +{ + _ClearAsyncKeyRequests(); + ResetNamedpipeReplyState(); + + std::lock_guard lock(_pendingCommitCandidateMutex); + _pendingServerCommitMessages.clear(); + _pendingWorkerSwitchMessages.clear(); + _pendingServerCandidateString.clear(); + _hasPendingServerCandidate = false; + _pendingServerCandidateMsgType = Global::DataFromServerMsgType::OutofRange; +} + +uint64_t CMetasequoiaIME::_CaptureFocusSessionToken() const +{ + return _expectedWorkerFocusToken.load(std::memory_order_acquire); +} + +bool CMetasequoiaIME::_IsFocusSessionCurrent(uint64_t focusToken, _In_opt_ ITfContext *expectedContext) const +{ + if (focusToken == 0 || !Global::g_connected || + _expectedWorkerFocusToken.load(std::memory_order_acquire) != focusToken || + _acknowledgedWorkerFocusToken.load(std::memory_order_acquire) != focusToken || + !_workerCommitReady.load(std::memory_order_acquire) || + _localSessionResetPending.load(std::memory_order_acquire)) + { + return false; + } + if (expectedContext == nullptr) + { + return true; + } + if (_pThreadMgr == nullptr) + { + return false; + } + + ITfDocumentMgr *documentMgr = nullptr; + ITfContext *topContext = nullptr; + bool matches = false; + if (SUCCEEDED(_pThreadMgr->GetFocus(&documentMgr)) && documentMgr) + { + if (SUCCEEDED(documentMgr->GetTop(&topContext)) && topContext) + { + matches = topContext == expectedContext; + topContext->Release(); + } + documentMgr->Release(); + } + return matches; +} + +uint64_t CMetasequoiaIME::_CaptureCompositionEpoch() const +{ + return _compositionEpoch.load(std::memory_order_acquire); +} + +bool CMetasequoiaIME::_IsCompositionEpochCurrent(uint64_t compositionEpoch) const +{ + return compositionEpoch != 0 && + _compositionEpoch.load(std::memory_order_acquire) == compositionEpoch; +} + +bool CMetasequoiaIME::_IsCompositionCurrent(_In_opt_ ITfComposition *expectedComposition) const +{ + return expectedComposition != nullptr && _pComposition == expectedComposition; +} + +bool CMetasequoiaIME::_IsLocalSessionResetCurrent(UINT resetToken) const +{ + return resetToken != 0 && _localResetEditSessionQueued && + _queuedLocalResetToken == resetToken && + _localSessionResetPending.load(std::memory_order_acquire) && + _localSessionResetToken.load(std::memory_order_acquire) == resetToken; +} + +void CMetasequoiaIME::_CompleteLocalSessionReset(UINT resetToken) +{ + if (!_localResetEditSessionQueued || _queuedLocalResetToken != resetToken) + { + return; + } + _localResetEditSessionQueued = false; + _queuedLocalResetToken = 0; + + const UINT currentToken = _localSessionResetToken.load(std::memory_order_acquire); + if (_localSessionResetPending.load(std::memory_order_acquire) && currentToken != resetToken) + { + // A focus loss superseded an already-granted dirty reset. The old + // cancel has finished; process the exact newer token before reopening + // the key path. + _RequestLocalSessionReset(nullptr, currentToken); + return; + } + if (currentToken == resetToken && !_IsComposing() && + _pCandidateListUIPresenter == nullptr) + { + _localSessionResetPending.store(false, std::memory_order_release); + } + else if (currentToken == resetToken && + _localSessionResetPending.load(std::memory_order_acquire)) + { + // A granted edit session can still fail inside the key-state handler. + // Do not reopen the transport while the old local composition or + // presenter survives; retire this queue slot and retry the exact token. + if (Global::g_connected && _msgWndHandle && IsWindow(_msgWndHandle)) + { + _ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SetTimer(_msgWndHandle, TIMER_CONNECT_ALL_NAMEDPIPE, + _ipcReconnectDelayMs, nullptr); + } + return; + } + if (Global::g_connected && _msgWndHandle && IsWindow(_msgWndHandle)) + { + PostMessage(_msgWndHandle, WM_IpcReconnect, 0, 0); + _ScheduleDeferredKeyDownDrain(); + } +} + +void CMetasequoiaIME::_RequestLocalSessionReset(_In_opt_ ITfContext *preferredContext, UINT resetToken) +{ + if (resetToken == 0 || + _localSessionResetToken.load(std::memory_order_acquire) != resetToken || + !_localSessionResetPending.load(std::memory_order_acquire)) + { + return; + } + _workerCommitReady.store(false, std::memory_order_release); + _ClearPendingIpcRequests(); + + if (_localResetEditSessionQueued) + { + return; + } + + const bool needsCancel = _IsComposing() || _pCandidateListUIPresenter != nullptr; + if (!needsCancel) + { + if (_localSessionResetToken.load(std::memory_order_acquire) == resetToken) + { + _localSessionResetPending.store(false, std::memory_order_release); + } + if (Global::g_connected && _msgWndHandle && IsWindow(_msgWndHandle)) + { + PostMessage(_msgWndHandle, WM_IpcReconnect, 0, 0); + _ScheduleDeferredKeyDownDrain(); + } + return; + } + + ITfContext *resetContext = preferredContext; + if (resetContext) + { + resetContext->AddRef(); + } + else if (_pContext) + { + resetContext = _pContext; + resetContext->AddRef(); + } + else if (_pThreadMgr) + { + ITfDocumentMgr *documentMgr = nullptr; + if (SUCCEEDED(_pThreadMgr->GetFocus(&documentMgr)) && documentMgr) + { + documentMgr->GetTop(&resetContext); + documentMgr->Release(); + } + } + + if (resetContext) + { + _KEYSTROKE_STATE keyState = {}; + keyState.Category = CATEGORY_COMPOSING; + keyState.Function = FUNCTION_CANCEL; + _localResetEditSessionQueued = true; + _queuedLocalResetToken = resetToken; + const HRESULT resetRequestHr = + _InvokeKeyHandler(resetContext, 0, L'\0', 0, keyState, + FANY_IME_NO_REQUEST_ID, {}, resetToken); + if (FAILED(resetRequestHr) && _localResetEditSessionQueued && + _queuedLocalResetToken == resetToken) + { + // The edit session was not accepted and therefore cannot retire + // its queue slot. Keep the exact reset gate closed and retry from + // the reconnect timer; reopening IPC here could bind the old + // local composition to a new server focus token. + _localResetEditSessionQueued = false; + _queuedLocalResetToken = 0; + if (Global::g_connected && _msgWndHandle && IsWindow(_msgWndHandle)) + { + _ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SetTimer(_msgWndHandle, TIMER_CONNECT_ALL_NAMEDPIPE, + _ipcReconnectDelayMs, nullptr); + } + } + resetContext->Release(); + return; + } + + // No context remains capable of granting an edit session. At least tear + // down the local presenter so a stale candidate cannot commit later. + _DeleteCandidateList(TRUE, nullptr); + if (_localSessionResetToken.load(std::memory_order_acquire) == resetToken) + { + _localSessionResetPending.store(false, std::memory_order_release); + } + if (Global::g_connected && _msgWndHandle && IsWindow(_msgWndHandle)) + { + PostMessage(_msgWndHandle, WM_IpcReconnect, 0, 0); + _ScheduleDeferredKeyDownDrain(); + } +} + void CMetasequoiaIME::_ScheduleCandidatePresenterCleanup(_In_ CCandidateListUIPresenter *pPresenter) { if (pPresenter == nullptr) @@ -410,6 +1059,18 @@ STDAPI CMetasequoiaIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClient _tfClientId = tfClientId; _dwActivateFlags = dwFlags; + _focusResetPending = false; + _activationRequired = false; + _workerCommitReady.store(false, std::memory_order_release); + _acknowledgedWorkerFocusToken.store(0, std::memory_order_release); + _localSessionResetPending.store(false, std::memory_order_release); + _localResetEditSessionQueued = false; + _queuedLocalResetToken = 0; + BindNamedpipeFocusState(this, &_focusResetPending, &_activationRequired, + &_expectedWorkerFocusToken, &_localSessionResetPending, + &_localSessionResetToken, &_workerCommitReady, + &_acknowledgedWorkerFocusToken, + &_hToTsfWorkerThreadPipe, &_workerPipeGeneration); /* std::wstring processName = FanyUtils::GetCurrentProcessName(); @@ -450,17 +1111,38 @@ STDAPI CMetasequoiaIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClient nullptr, // Global::dllInstanceHandle, // this); + if (!_msgWndHandle) + { + goto ExitError; + } SetWindowLongPtr(_msgWndHandle, GWLP_USERDATA, (LONG_PTR)this); Global::msgWndHandle = _msgWndHandle; - /* 连接命名管道 */ - Global::g_connected = true; // 激活时也需要设置一下 - PostMessage(_msgWndHandle, WM_ConnectNamedpipe, 0, 0); + BOOL hasThreadFocus = FALSE; + const HRESULT threadFocusResult = _pThreadMgr->IsThreadFocus(&hasThreadFocus); + // Preserve the historical eager-connect behavior if a host cannot report + // focus. Otherwise an already-focused activation could remain offline + // forever because no later focus callback is guaranteed. + Global::g_connected = SUCCEEDED(threadFocusResult) ? hasThreadFocus != FALSE : true; + if (Global::g_connected) + { + RequireNamedpipeFocusActivation(); + } /* 创建 IPC 线程 */ - _shouldStopIpcThread = false; + _ipcStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!_ipcStopEvent) + { + goto ExitError; + } + _shouldStopIpcThread.store(false); _pIpcThread = new std::thread(IpcWorkerThread, this); + if (Global::g_connected) + { + PostMessage(_msgWndHandle, WM_ConnectNamedpipe, 0, 0); + } + ITfDocumentMgr *pDocMgrFocus = nullptr; if (SUCCEEDED(_pThreadMgr->GetFocus(&pDocMgrFocus)) && (pDocMgrFocus != nullptr)) { @@ -501,8 +1183,17 @@ STDAPI CMetasequoiaIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClient // Reset to Chinese mode whenever switch back to this IME _pCompositionProcessorEngine->InitializeMetasequoiaIMECompartment(pThreadMgr, tfClientId); - // 激活此输入法时,向 server 端发送一个激活的消息 - PostMessage(_msgWndHandle, WM_IMEActivation, 0, 0); + // The first connect timer can run before the engine exists. Re-arm after + // compartment initialization so an already-focused activation always + // replays both ownership and a complete status snapshot. + if (Global::g_connected) + { + PostMessage(_msgWndHandle, WM_ThreadFocus, 0, 0); + } + + // TSF lifecycle is still published on the epoch-checked Main pipe, but it + // deliberately does not control the floating toolbar. The Server keeps + // that toolbar resident and only applies real status changes to it. { HWND hwndTarget = GetFocus(); @@ -535,62 +1226,67 @@ ExitError: STDAPI CMetasequoiaIME::Deactivate() { + // Send this synchronously before destroying the message window. OnKill can + // queue the same lifecycle event, but that queued message may never run + // during a rapid TIP deactivation; the server treats duplicates as idempotent. + Global::g_connected = false; + _workerCommitReady.store(false, std::memory_order_release); + BeginNamedpipeLocalSessionReset(); // invalidate queued dirty-reset messages + _localSessionResetPending.store(false, std::memory_order_release); + _localResetEditSessionQueued = false; + _queuedLocalResetToken = 0; + _ClearDeferredKeyDowns(); + MarkNamedpipeFocusLost(); + FlushNamedpipeImeDeactivation(); + _ClearPendingIpcRequests(); + // 注销此输入法时,向 server 端发送一个注销的消息 // 注销不要给消息窗口发送消息,而是直接在这里处理 // SendIMEDeactivationEventToUIProcessViaNamedPipe(); - ITfInputProcessorProfileMgr *pProfileMgr = nullptr; - TF_INPUTPROCESSORPROFILE profile = {}; - HRESULT hrProfile = CoCreateInstance( // - CLSID_TF_InputProcessorProfiles, // - nullptr, // - CLSCTX_INPROC_SERVER, // - IID_ITfInputProcessorProfileMgr, // - (void **)&pProfileMgr); - - if (SUCCEEDED(hrProfile) && pProfileMgr != nullptr) - { - hrProfile = pProfileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); - if (SUCCEEDED(hrProfile) && !IsEqualCLSID(profile.clsid, Global::MetasequoiaIMECLSID)) - { -// 向 server 端发送一个输入法真的切换到了别的输入法的消息 -// 只有接收到这个消息,才可以隐藏 ftb - SendIMEDeactivationEventToUIProcessViaNamedPipe(); - } - - pProfileMgr->Release(); - } + // ClientDeactivated closes the Server-side input session and hides the + // toolbar because the TIP itself is going away. A temporary thread-focus + // loss uses ClientSuspended instead and deliberately keeps it visible. /* 清理 IPC 线程 */ - _shouldStopIpcThread = true; - if (_hToTsfWorkerThreadPipe && _hToTsfWorkerThreadPipe != INVALID_HANDLE_VALUE) + _shouldStopIpcThread.store(true); + if (_ipcStopEvent) { - CancelIoEx(_hToTsfWorkerThreadPipe, nullptr); + SetEvent(_ipcStopEvent); + } + HANDLE workerPipe = _hToTsfWorkerThreadPipe.load(); + if (workerPipe && workerPipe != INVALID_HANDLE_VALUE) + { + CancelIoEx(workerPipe, nullptr); } - // Clean IPC - /* 必须先 CloseIpc,再 join 线程 */ - CloseIpc(); - _hToTsfWorkerThreadPipe = nullptr; if (_pIpcThread && _pIpcThread->joinable()) { _pIpcThread->join(); delete _pIpcThread; _pIpcThread = nullptr; } + _hToTsfWorkerThreadPipe.store(nullptr); + InvalidateNamedpipeWorkerGeneration(); + // The worker no longer has an outstanding OVERLAPPED operation, so the + // UI thread can now close all three handles safely. + CloseIpc(); + if (_ipcStopEvent) + { + CloseHandle(_ipcStopEvent); + _ipcStopEvent = nullptr; + } + // Stop callbacks from the previously focused top context before tearing + // down the engine and thread manager. Context stack changes can otherwise + // re-enter a half-deactivated service. + _InitTextEditSink(nullptr); // TODO: 去掉共享内存,只保留命名管道 // CloseNamedpipe(); - if (_pCompositionProcessorEngine) - { - delete _pCompositionProcessorEngine; - _pCompositionProcessorEngine = nullptr; - } - ITfContext *pContext = _pContext; if (_pContext) { pContext->AddRef(); - _EndComposition(_pContext); + _EndComposition(_pContext, _pComposition, true); } if (_pCandidateListUIPresenter) @@ -609,6 +1305,12 @@ STDAPI CMetasequoiaIME::Deactivate() _DrainPendingCandidatePresenterCleanup(); + if (_pCompositionProcessorEngine) + { + delete _pCompositionProcessorEngine; + _pCompositionProcessorEngine = nullptr; + } + _UninitFunctionProviderSink(); _UninitThreadFocusSink(); @@ -627,11 +1329,12 @@ STDAPI CMetasequoiaIME::Deactivate() CompartmentDoubleSingleByte._ClearCompartment(); CCompartment CompartmentPunctuation(_pThreadMgr, _tfClientId, Global::MetasequoiaIMEGuidCompartmentPunctuation); - CompartmentDoubleSingleByte._ClearCompartment(); + CompartmentPunctuation._ClearCompartment(); if (_pThreadMgr != nullptr) { _pThreadMgr->Release(); + _pThreadMgr = nullptr; } _tfClientId = TF_CLIENTID_NULL; @@ -647,13 +1350,19 @@ STDAPI CMetasequoiaIME::Deactivate() { KillTimer(_msgWndHandle, TIMER_CONNECT_ALL_NAMEDPIPE); KillTimer(_msgWndHandle, TIMER_CONNECT_TO_TSF_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; - g_connectToTsfNamedpipeRetryCount = 0; DestroyWindow(_msgWndHandle); + if (Global::msgWndHandle == _msgWndHandle) + { + Global::msgWndHandle = nullptr; + } _msgWndHandle = nullptr; } UnregisterClass(L"MetasequoiaIMEWorkerWnd", Global::dllInstanceHandle); + UnbindNamedpipeFocusState(this); + _focusResetPending = false; + _activationRequired = false; + return S_OK; } @@ -665,92 +1374,167 @@ STDAPI CMetasequoiaIME::Deactivate() //---------------------------------------------------------------------------- void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) { - while (!pIME->_shouldStopIpcThread) - { - /* 阻塞读 */ - while (!pIME->_shouldStopIpcThread) + const auto notifyDisconnected = [pIME](HANDLE disconnectedPipe, UINT disconnectedGeneration) { + if (disconnectedGeneration == 0 || + pIME->_workerPipeGeneration.load(std::memory_order_acquire) != disconnectedGeneration) { - if (!pIME->_hToTsfWorkerThreadPipe || pIME->_hToTsfWorkerThreadPipe == INVALID_HANDLE_VALUE) + return; + } + // Close the first-key window immediately on the reader thread. The UI + // message may be delayed, fail to post, or race a replacement handle. + pIME->_workerCommitReady.store(false, std::memory_order_release); + pIME->_acknowledgedWorkerFocusToken.store(0, std::memory_order_release); + HANDLE expected = disconnectedPipe; + if (pIME->_hToTsfWorkerThreadPipe.compare_exchange_strong(expected, nullptr)) + { + const HWND ownerWindow = pIME->_msgWndHandle; + if (ownerWindow && IsWindow(ownerWindow)) { - Sleep(20); - continue; - } - - DWORD bytesRead = 0; - FanyImeNamedpipeDataToTsfWorkerThread buf; - BOOL readResult = ReadFile( // - pIME->_hToTsfWorkerThreadPipe, // - &buf, // - sizeof(buf), // - &bytesRead, // - nullptr // - ); - - if (!readResult || bytesRead == 0) - { - break; - } - - if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CommitCurCandidate) - { - pIME->_QueuePendingCommitCandidate(buf.data); - PostMessage(pIME->_msgWndHandle, WM_CommitCandidate, 0, 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToChinese) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToChinese, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncEn) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToPuncEn, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncCn) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToPuncCn, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToFullwidth) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToFullwidth, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth) - { - PostMessage( // - pIME->_msgWndHandle, // - WM_CheckGlobalCompartment, // - Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth, // - 0); - } - else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PagingCommaPeriodChanged) - { - const bool enabled = buf.data[0] == L'1'; - Global::PagingCommaPeriodEnabled.store(enabled, std::memory_order_relaxed); + PostMessage(ownerWindow, WM_IpcWorkerDisconnected, + reinterpret_cast(disconnectedPipe), + static_cast(disconnectedGeneration)); } } - Sleep(100); + }; + + while (!pIME->_shouldStopIpcThread.load()) + { + HANDLE workerPipe = pIME->_hToTsfWorkerThreadPipe.load(std::memory_order_acquire); + const UINT workerGeneration = pIME->_workerPipeGeneration.load(std::memory_order_acquire); + if (!workerPipe || workerPipe == INVALID_HANDLE_VALUE) + { + if (pIME->_ipcStopEvent && WaitForSingleObject(pIME->_ipcStopEvent, 50) == WAIT_OBJECT_0) + { + return; + } + continue; + } + + DWORD bytesRead = 0; + FanyImeNamedpipeDataToTsfWorkerThread buf = {}; + OVERLAPPED overlapped = {}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!overlapped.hEvent) + { + notifyDisconnected(workerPipe, workerGeneration); + continue; + } + + BOOL readResult = ReadFile(workerPipe, &buf, sizeof(buf), &bytesRead, &overlapped); + if (!readResult && GetLastError() == ERROR_IO_PENDING) + { + HANDLE waitHandles[] = {pIME->_ipcStopEvent, overlapped.hEvent}; + const DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + if (waitResult == WAIT_OBJECT_0) + { + CancelIoEx(workerPipe, &overlapped); + GetOverlappedResult(workerPipe, &overlapped, &bytesRead, TRUE); + CloseHandle(overlapped.hEvent); + return; + } + if (waitResult == WAIT_OBJECT_0 + 1) + { + readResult = GetOverlappedResult(workerPipe, &overlapped, &bytesRead, FALSE); + } + else + { + // WAIT_FAILED and every unexpected result must drain the + // pending operation before this stack OVERLAPPED is destroyed. + CancelIoEx(workerPipe, &overlapped); + GetOverlappedResult(workerPipe, &overlapped, &bytesRead, TRUE); + readResult = FALSE; + } + } + CloseHandle(overlapped.hEvent); + + bool validFrame = readResult && bytesRead == sizeof(buf) && + buf.msg_type <= Global::DataToTsfWorkerThreadMsgType::PipeReady; + if (validFrame && buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CommitCurCandidate) + { + bool hasTerminator = false; + for (const wchar_t ch : buf.data) + { + if (ch == L'\0') + { + hasTerminator = true; + break; + } + } + validFrame = hasTerminator; + } + if (validFrame && buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PagingCommaPeriodChanged) + { + validFrame = buf.data[0] == L'0' || buf.data[0] == L'1'; + } + if (validFrame && buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PipeReady) + { + // The registration ACK is normally consumed before this handle is + // published. Ignore a defensive duplicate only when well formed. + validFrame = buf.data[0] == L'\0'; + } + uint64_t focusToken = 0; + if (validFrame && buf.msg_type == Global::DataToTsfWorkerThreadMsgType::FocusSessionReady) + { + bool hasTerminator = false; + for (const wchar_t ch : buf.data) + { + if (ch == L'\0') + { + hasTerminator = true; + break; + } + } + wchar_t *end = nullptr; + if (hasTerminator && buf.data[0] != L'\0') + { + focusToken = _wcstoui64(buf.data, &end, 10); + } + // Token 0 is a syntactically valid legacy/dummy activation marker, + // but it can never satisfy the nonzero focus barrier below. Treat + // it as a harmless stale frame instead of tearing down the healthy + // worker pipe. + validFrame = hasTerminator && buf.data[0] != L'\0' && end && *end == L'\0'; + } + + if (!validFrame) + { + if (pIME->_shouldStopIpcThread.load()) + { + return; + } + notifyDisconnected(workerPipe, workerGeneration); + continue; + } + + if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::FocusSessionReady) + { + const uint64_t expectedToken = + pIME->_expectedWorkerFocusToken.load(std::memory_order_acquire); + if (expectedToken != 0 && focusToken == expectedToken) + { + pIME->_acknowledgedWorkerFocusToken.store(focusToken, std::memory_order_release); + pIME->_workerCommitReady.store(true, std::memory_order_release); + } + } + else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CommitCurCandidate) + { + if (pIME->_workerCommitReady.load(std::memory_order_acquire)) + { + pIME->_PostServerCandidateCommit(buf.data); + } + } + else if (buf.msg_type >= Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish && + buf.msg_type <= Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth) + { + const uint64_t focusToken = + pIME->_expectedWorkerFocusToken.load(std::memory_order_acquire); + pIME->_PostWorkerCompartmentSwitch(buf.msg_type, focusToken); + } + else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PagingCommaPeriodChanged) + { + const bool enabled = buf.data[0] == L'1'; + Global::PagingCommaPeriodEnabled.store(enabled, std::memory_order_relaxed); + } } } @@ -770,57 +1554,162 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar switch (message) { case WM_CheckGlobalCompartment: { - if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish) + CMetasequoiaIME::WorkerCompartmentSwitch request; + if (!pIME->_TakeWorkerCompartmentSwitch(static_cast(wParam), request) || + !pIME->_IsFocusSessionCurrent(request.focusToken)) { + break; + } + + if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToEnglish) + { + if (pIME->_IsComposing() || pIME->_pCandidateListUIPresenter != nullptr) + { + ITfDocumentMgr *documentMgr = nullptr; + ITfContext *context = nullptr; + if (SUCCEEDED(pIME->_GetThreadMgr()->GetFocus(&documentMgr)) && documentMgr) + { + if (SUCCEEDED(documentMgr->GetTop(&context)) && context) + { + _KEYSTROKE_STATE keyState = {}; + keyState.Category = CATEGORY_COMPOSING; + keyState.Function = FUNCTION_TOGGLE_IME_MODE; + if (FAILED(pIME->_InvokeKeyHandler(context, 0, L'\0', 0, keyState, + FANY_IME_NO_REQUEST_ID, {}, 0, + request.compositionEpoch, + request.focusToken))) + { + MarkNamedpipeSessionDirtyForOwner(pIME); + } + context->Release(); + } + documentMgr->Release(); + } + } pIME->GetCompositionProcessorEngine()->SetIMEMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), FALSE); } - else if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToChinese) + else if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToChinese) { pIME->GetCompositionProcessorEngine()->SetIMEMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), TRUE); } - else if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncEn) + else if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncEn) { pIME->GetCompositionProcessorEngine()->SetPunctuationMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), FALSE); } - else if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncCn) + else if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToPuncCn) { pIME->GetCompositionProcessorEngine()->SetPunctuationMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), TRUE); } - else if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToFullwidth) + else if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToFullwidth) { pIME->GetCompositionProcessorEngine()->SetDoubleSingleByteMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), TRUE); } - else if (wParam == Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth) + else if (request.messageType == Global::DataToTsfWorkerThreadMsgType::SwitchToHalfwidth) { pIME->GetCompositionProcessorEngine()->SetDoubleSingleByteMode(pIME->_GetThreadMgr(), pIME->_GetClientId(), FALSE); } + // Read all three compartments after applying the exact worker command. + // Sending a complete ordered snapshot prevents two queued status WMs + // from each observing/overwriting a different partial state. + SendCurrentImeStatusSnapshot(pIME); break; } case WM_ConnectNamedpipe: { - KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; - SetTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE, CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS, nullptr); + if (Global::g_connected) + { + PostMessage(hWnd, WM_IpcReconnect, 0, 0); + } break; } case WM_DisconnectNamedpipe: { + if (Global::g_connected) + { + // A newer OnSetThreadFocus already superseded this queued kill. + // Never let a stale focus-loss message deactivate the recovered + // client (the Win+. round-trip can produce exactly this ordering). + break; + } KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); KillTimer(hWnd, TIMER_CONNECT_TO_TSF_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; - g_connectToTsfNamedpipeRetryCount = 0; - SendClientDeactivatedEventToServerViaNamedPipe(); + FlushNamedpipeFocusSessionReset(); + pIME->_ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; break; } case WM_ConnectToTsfNamedpipe: { - KillTimer(hWnd, TIMER_CONNECT_TO_TSF_NAMEDPIPE); - g_connectToTsfNamedpipeRetryCount = 0; - // Keep old behavior: wait once before first connect try, but do it asynchronously. SetTimer(hWnd, TIMER_CONNECT_TO_TSF_NAMEDPIPE, CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS, nullptr); break; } + case WM_IpcWorkerDisconnected: { + // The worker posts this only after its OVERLAPPED read has completed, + // so it is now safe for the UI thread to close and recreate all pipes. + const HANDLE disconnectedPipe = reinterpret_cast(wParam); + const UINT disconnectedGeneration = static_cast(lParam); + if (!disconnectedPipe || disconnectedGeneration == 0 || + disconnectedGeneration != pIME->_workerPipeGeneration.load(std::memory_order_acquire) || + disconnectedPipe != GetToTsfWorkerThreadNamedpipe() || + pIME->_hToTsfWorkerThreadPipe.load(std::memory_order_acquire) != nullptr) + { + // A delayed notification from an older registration must not tear + // down handles opened by a newer reconnect attempt. + break; + } + pIME->_workerCommitReady.store(false, std::memory_order_release); + // Preserve the visible raw composition before the transport reset + // cancels the local TSF text store. If a key arrived after the reader + // thread closed Ready but before this message ran, its queued item + // remains behind this checkpoint. + pIME->_ArmDeferredRecoveryForTransport(pIME->_pContext); + MarkNamedpipeSessionDirtyForOwner(pIME); + CloseNamedpipe(); + if (Global::g_connected) + { + pIME->_ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SetTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE, pIME->_ipcReconnectDelayMs, nullptr); + } + break; + } + case WM_IpcSessionDirty: { + const UINT resetToken = static_cast(wParam); + if (resetToken == 0) + { + // Worker-thread callbacks cannot access the UI thread's TLS IPC + // binding. Let the owner thread allocate and post an exact reset + // token here. + MarkNamedpipeSessionDirtyForOwner(pIME); + } + else + { + pIME->_RequestLocalSessionReset(nullptr, resetToken); + } + break; + } + case WM_IpcReconnect: { + // Main and reply-pipe failures invalidate only their own TLS handle. + // Preserve a healthy worker read and let ConnectToAllNamedpipe reopen + // just the missing channel(s). + if (Global::g_connected) + { + const bool resetPending = + pIME->_localSessionResetPending.load(std::memory_order_acquire); + const bool activated = + !resetPending && EnsureNamedpipeFocusSessionActivated(); + if (activated) + { + KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); + pIME->_ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SendCurrentImeStatusSnapshot(pIME); + pIME->_ScheduleDeferredKeyDownDrain(); + break; + } + pIME->_ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SetTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE, pIME->_ipcReconnectDelayMs, nullptr); + } + break; + } case WM_TIMER: { if (wParam == TIMER_CONNECT_ALL_NAMEDPIPE) { @@ -828,25 +1717,38 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar if (!Global::g_connected) { KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; break; } - if (ConnectToAllNamedpipe()) + if (pIME->_localSessionResetPending.load(std::memory_order_acquire)) { - pIME->_hToTsfWorkerThreadPipe = GetToTsfWorkerThreadNamedpipe(); - KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; - SendClientActivatedEventToServerViaNamedPipe(); - break; + const UINT resetToken = + pIME->_localSessionResetToken.load(std::memory_order_acquire); + pIME->_RequestLocalSessionReset(nullptr, resetToken); + if (pIME->_localSessionResetPending.load(std::memory_order_acquire)) + { + pIME->_ipcReconnectDelayMs = + min(pIME->_ipcReconnectDelayMs * 2, + CONNECT_NAMEDPIPE_MAX_RETRY_INTERVAL_MS); + SetTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE, + pIME->_ipcReconnectDelayMs, nullptr); + break; + } } - g_connectAllNamedpipeRetryCount++; - if (g_connectAllNamedpipeRetryCount >= CONNECT_ALL_NAMEDPIPE_MAX_RETRY) + if (EnsureNamedpipeFocusSessionActivated()) { KillTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE); - g_connectAllNamedpipeRetryCount = 0; + pIME->_ipcReconnectDelayMs = CONNECT_NAMEDPIPE_RETRY_INTERVAL_MS; + SendCurrentImeStatusSnapshot(pIME); + pIME->_ScheduleDeferredKeyDownDrain(); + break; } + // Keep retrying while this TSF thread owns UI focus. The server + // can be restarted independently of the host application. + pIME->_ipcReconnectDelayMs = + min(pIME->_ipcReconnectDelayMs * 2, CONNECT_NAMEDPIPE_MAX_RETRY_INTERVAL_MS); + SetTimer(hWnd, TIMER_CONNECT_ALL_NAMEDPIPE, pIME->_ipcReconnectDelayMs, nullptr); break; } @@ -855,47 +1757,48 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar if (ConnectToTsfNamedpipe()) { KillTimer(hWnd, TIMER_CONNECT_TO_TSF_NAMEDPIPE); - g_connectToTsfNamedpipeRetryCount = 0; break; } - - g_connectToTsfNamedpipeRetryCount++; - if (g_connectToTsfNamedpipeRetryCount >= CONNECT_TO_TSF_NAMEDPIPE_MAX_RETRY) - { - KillTimer(hWnd, TIMER_CONNECT_TO_TSF_NAMEDPIPE); - g_connectToTsfNamedpipeRetryCount = 0; - } break; } break; } case WM_IMEActivation: { - SendIMEActivationEventToUIProcessViaNamedPipe(); + // Retained only for message-number compatibility. Main-pipe lifecycle + // messages no longer control floating-toolbar visibility. break; } case WM_ThreadFocus: { - /* */ - /* 通知 server 端更新一下 ftb 中英、全半角、标点的状态 */ - SendIMEStatusEventToUIProcessViaNamedPipe( // - pIME->GetCompositionProcessorEngine()->GetIMEMode(pIME->_GetThreadMgr(), pIME->_GetClientId()), // - pIME->GetCompositionProcessorEngine()->GetDoubleSingleByteMode(pIME->_GetThreadMgr(), - pIME->_GetClientId()), // - pIME->GetCompositionProcessorEngine()->GetPunctuationMode(pIME->_GetThreadMgr(), pIME->_GetClientId())); // + SendCurrentImeStatusSnapshot(pIME); + break; + } + case WM_DrainDeferredKeyDown: { + pIME->_DrainOneDeferredKeyDown(); break; } case WM_UpdateIMEStatus: { - SendIMESwitchEventToUIProcessViaNamedPipe((UINT)wParam); + SendCurrentImeStatusSnapshot(pIME); break; } case WM_UpdateDoubleSingleByte: { - SendDoubleSingleByteSwitchEventToUIProcessViaNamedPipe((BOOL)wParam); + SendCurrentImeStatusSnapshot(pIME); break; } case WM_UpdatePuncMode: { - SendPuncSwitchEventToUIProcessViaNamedPipe((BOOL)wParam); + SendCurrentImeStatusSnapshot(pIME); break; } case WM_CommitCandidate: { + CMetasequoiaIME::WorkerCandidateCommit request; + if (!pIME->_TakeServerCandidateCommit(static_cast(wParam), request)) + { + break; + } + if (!pIME->_IsFocusSessionCurrent(request.focusToken) || + !pIME->_IsCompositionEpochCurrent(request.compositionEpoch)) + { + break; + } ITfDocumentMgr *pDocMgrFocus = nullptr; ITfContext *pContext = nullptr; @@ -906,7 +1809,11 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar _KEYSTROKE_STATE KeystrokeState; KeystrokeState.Category = CATEGORY_CANDIDATE; KeystrokeState.Function = FUNCTION_FINALIZE_CANDIDATELIST; - pIME->_InvokeKeyHandler(pContext, 0, 0, 0, KeystrokeState); + pIME->_InvokeKeyHandler(pContext, 0, 0, 0, KeystrokeState, + FANY_IME_UNSOLICITED_REQUEST_ID, + std::move(request.text), 0, + request.compositionEpoch, + request.focusToken); pContext->Release(); } pDocMgrFocus->Release(); @@ -914,10 +1821,22 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar break; } case WM_AsyncFinalizeCandidate: { + CMetasequoiaIME::AsyncKeyRequest request; + if (!pIME->_TakeAsyncKeyRequest(WM_AsyncFinalizeCandidate, static_cast(wParam), request)) + { + break; + } + if (!pIME->_IsFocusSessionCurrent(request.focusToken) || + !pIME->_IsCompositionEpochCurrent(request.compositionEpoch)) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + break; + } PerfTimer timer; ITfDocumentMgr *pDocMgrFocus = nullptr; ITfContext *pContext = nullptr; + bool handedOffReplay = false; if (SUCCEEDED(pIME->_GetThreadMgr()->GetFocus(&pDocMgrFocus)) && pDocMgrFocus) { if (SUCCEEDED(pDocMgrFocus->GetTop(&pContext)) && pContext) @@ -925,20 +1844,41 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar _KEYSTROKE_STATE KeystrokeState; KeystrokeState.Category = CATEGORY_CANDIDATE; KeystrokeState.Function = FUNCTION_FINALIZE_CANDIDATELIST; - pIME->_InvokeKeyHandler(pContext, VK_SPACE, L' ', 0, KeystrokeState); + pIME->_InvokeKeyHandler(pContext, request.code, request.wch, 0, KeystrokeState, + request.requestId, {}, 0, + request.compositionEpoch, + request.focusToken, + request.deferredReplayToken); + handedOffReplay = true; pContext->Release(); } pDocMgrFocus->Release(); } + if (!handedOffReplay) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + } break; } case WM_AsyncPunctuationCommit: { + CMetasequoiaIME::AsyncKeyRequest request; + if (!pIME->_TakeAsyncKeyRequest(WM_AsyncPunctuationCommit, static_cast(wParam), request)) + { + break; + } + if (!pIME->_IsFocusSessionCurrent(request.focusToken) || + !pIME->_IsCompositionEpochCurrent(request.compositionEpoch)) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + break; + } PerfTimer timer; ITfDocumentMgr *pDocMgrFocus = nullptr; ITfContext *pContext = nullptr; - const UINT code = static_cast(wParam); - const WCHAR wch = static_cast(lParam); + const UINT code = request.code; + const WCHAR wch = request.wch; + bool handedOffReplay = false; if (SUCCEEDED(pIME->_GetThreadMgr()->GetFocus(&pDocMgrFocus)) && pDocMgrFocus) { if (SUCCEEDED(pDocMgrFocus->GetTop(&pContext)) && pContext) @@ -946,35 +1886,71 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar const bool useDirectPunctuationSession = pIME->_candidateMode == CANDIDATE_NONE && !pIME->_IsComposing(); if (useDirectPunctuationSession) { - pIME->_RequestDirectPunctuationEditSession(pContext, code, wch); + pIME->_RequestDirectPunctuationEditSession(pContext, code, wch, request.requestId, + std::move(request.prefetchedText), + request.focusToken, + request.compositionEpoch, + request.deferredReplayToken); + handedOffReplay = true; } else { _KEYSTROKE_STATE KeystrokeState; KeystrokeState.Category = CATEGORY_COMPOSING; KeystrokeState.Function = FUNCTION_PUNCTUATION; - pIME->_InvokeKeyHandler(pContext, code, wch, 0, KeystrokeState); + pIME->_InvokeKeyHandler(pContext, code, wch, 0, KeystrokeState, request.requestId, + std::move(request.prefetchedText), 0, + request.compositionEpoch, + request.focusToken, + request.deferredReplayToken); + handedOffReplay = true; } pContext->Release(); } pDocMgrFocus->Release(); } + if (!handedOffReplay) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + } break; } case WM_AsyncServerCandidateKey: { - const UINT code = static_cast(wParam); - const WCHAR wch = static_cast(lParam); - FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(); - if (receivedData->msg_type == Global::DataFromServerMsgType::Normal && - std::wstring(receivedData->candidate_string) == L"T") + CMetasequoiaIME::AsyncKeyRequest request; + if (!pIME->_TakeAsyncKeyRequest(WM_AsyncServerCandidateKey, static_cast(wParam), request)) { - receivedData = TryReadDataFromServerPipeWithTimeout(); + break; + } + if (!pIME->_IsFocusSessionCurrent(request.focusToken) || + !pIME->_IsCompositionEpochCurrent(request.compositionEpoch)) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + break; + } + const UINT code = request.code; + const WCHAR wch = request.wch; + FanyImeNamedpipeDataToTsf *receivedData = + TryReadDataFromServerPipeWithTimeout(request.requestId); + if (receivedData->msg_type == Global::DataFromServerMsgType::TransportUnavailable) + { + // Keep the existing composition intact. A transport failure is + // not text and must never be committed to the application. + if (request.deferredReplayToken != 0) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + } + else + { + MarkNamedpipeSessionDirtyForOwner(pIME); + } + break; } if (receivedData->msg_type == Global::DataFromServerMsgType::Normal) { if (wch == 0) { + pIME->_CompleteDeferredKeyReplay(request.deferredReplayToken); break; } const WCHAR *punctuation = pIME->_pCompositionProcessorEngine->GetPunctuation(wch); @@ -983,8 +1959,15 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar { commitText.append(punctuation); } - pIME->_QueuePendingPunctuationCommitText(commitText.c_str()); - PostMessage(hWnd, WM_AsyncPunctuationCommit, code, static_cast(wch)); + pIME->_PostAsyncKeyRequest(WM_AsyncPunctuationCommit, code, wch, + request.requestId, std::move(commitText), + request.focusToken, + request.compositionEpoch, + request.deferredReplayToken); + } + else + { + pIME->_CompleteDeferredKeyReplay(request.deferredReplayToken); } // Navigation responses are acknowledgements only. The Server owns and // refreshes candidate paging/selection state, so TSF must not apply the @@ -992,10 +1975,22 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar break; } case WM_AsyncNumberCandidateCommit: { + CMetasequoiaIME::AsyncKeyRequest request; + if (!pIME->_TakeAsyncKeyRequest(WM_AsyncNumberCandidateCommit, static_cast(wParam), request)) + { + break; + } + if (!pIME->_IsFocusSessionCurrent(request.focusToken) || + !pIME->_IsCompositionEpochCurrent(request.compositionEpoch)) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + break; + } PerfTimer timer; ITfDocumentMgr *pDocMgrFocus = nullptr; ITfContext *pContext = nullptr; + bool handedOffReplay = false; if (SUCCEEDED(pIME->_GetThreadMgr()->GetFocus(&pDocMgrFocus)) && pDocMgrFocus) { if (SUCCEEDED(pDocMgrFocus->GetTop(&pContext)) && pContext) @@ -1003,12 +1998,20 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar _KEYSTROKE_STATE KeystrokeState; KeystrokeState.Category = CATEGORY_CANDIDATE; KeystrokeState.Function = FUNCTION_SELECT_BY_NUMBER; - pIME->_InvokeKeyHandler(pContext, static_cast(wParam), static_cast(lParam), 0, - KeystrokeState); + pIME->_InvokeKeyHandler(pContext, request.code, request.wch, 0, KeystrokeState, + request.requestId, {}, 0, + request.compositionEpoch, + request.focusToken, + request.deferredReplayToken); + handedOffReplay = true; pContext->Release(); } pDocMgrFocus->Release(); } + if (!handedOffReplay) + { + pIME->_RetryDeferredKeyReplay(request.deferredReplayToken); + } break; } case WM_CleanupCandidatePresenter: { diff --git a/src/IME/MetasequoiaIME.h b/src/IME/MetasequoiaIME.h index cebe31fe..9239bac8 100644 --- a/src/IME/MetasequoiaIME.h +++ b/src/IME/MetasequoiaIME.h @@ -3,7 +3,9 @@ #include "KeyHandlerEditSession.h" #include "MetasequoiaIMEBaseStructure.h" #include +#include #include +#include #include #include #include @@ -27,6 +29,10 @@ const DWORD WM_AsyncFinalizeCandidate = WM_USER + 11; const DWORD WM_AsyncPunctuationCommit = WM_USER + 12; const DWORD WM_AsyncNumberCandidateCommit = WM_USER + 13; const DWORD WM_AsyncServerCandidateKey = WM_USER + 14; +const DWORD WM_IpcWorkerDisconnected = WM_USER + 15; +const DWORD WM_IpcReconnect = WM_USER + 16; +const DWORD WM_IpcSessionDirty = WM_USER + 17; +const DWORD WM_DrainDeferredKeyDown = WM_USER + 18; LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND wndHandle, UINT uMsg, WPARAM wParam, LPARAM lParam); class CMetasequoiaIME : public ITfTextInputProcessorEx, @@ -128,20 +134,24 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, HRESULT _HandleToogleIMEMode(TfEditCookie ec, _In_ ITfContext *pContext); // key event handlers for composition object. - HRESULT _HandleCompositionInput(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch); + HRESULT _HandleCompositionInput(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch, uint64_t requestId); HRESULT _HandleCompositionFinalize(TfEditCookie ec, _In_ ITfContext *pContext, BOOL fCandidateList); HRESULT _HandleCompositionConvert(TfEditCookie ec, _In_ ITfContext *pContext, BOOL isWildcardSearch); - HRESULT _HandleCompositionBackspace(TfEditCookie ec, _In_ ITfContext *pContext); + HRESULT _HandleCompositionBackspace(TfEditCookie ec, _In_ ITfContext *pContext, uint64_t requestId); HRESULT _HandleCompositionArrowKey(TfEditCookie ec, _In_ ITfContext *pContext, KEYSTROKE_FUNCTION keyFunction); - HRESULT _HandleCompositionPunctuation(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch); + HRESULT _HandleCompositionPunctuation(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch, uint64_t requestId, + const std::wstring &prefetchedText); HRESULT _HandleCompositionDoubleSingleByte(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch); // key event handlers for candidate object. - HRESULT _HandleCandidateFinalize(TfEditCookie ec, _In_ ITfContext *pContext); + HRESULT _HandleCandidateFinalize(TfEditCookie ec, _In_ ITfContext *pContext, uint64_t requestId, + const std::wstring &prefetchedText); HRESULT _HandleCandidateFinalizeForVKReturn(TfEditCookie ec, _In_ ITfContext *pContext); - HRESULT _HandleCandidateConvert(TfEditCookie ec, _In_ ITfContext *pContext); + HRESULT _HandleCandidateConvert(TfEditCookie ec, _In_ ITfContext *pContext, uint64_t requestId, + const std::wstring &prefetchedText); HRESULT _HandleCandidateArrowKey(TfEditCookie ec, _In_ ITfContext *pContext, _In_ KEYSTROKE_FUNCTION keyFunction); - HRESULT _HandleCandidateSelectByNumber(TfEditCookie ec, _In_ ITfContext *pContext, _In_ UINT uCode); + HRESULT _HandleCandidateSelectByNumber(TfEditCookie ec, _In_ ITfContext *pContext, _In_ UINT uCode, + uint64_t requestId, const std::wstring &prefetchedText); BOOL _IsSecureMode(void) { @@ -156,10 +166,26 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, return (_dwActivateFlags & TF_TMF_IMMERSIVEMODE) ? TRUE : FALSE; }; - CCompositionProcessorEngine *GetCompositionProcessorEngine() - { - return (_pCompositionProcessorEngine); - }; + CCompositionProcessorEngine *GetCompositionProcessorEngine() + { + return (_pCompositionProcessorEngine); + }; + + // Async TSF edit sessions may be granted after a focus transition. Bind + // every text-producing session to the exact focus token that scheduled it + // so an old worker/key message cannot commit into the next document. + uint64_t _CaptureFocusSessionToken() const; + bool _IsFocusSessionCurrent(uint64_t focusToken, _In_opt_ ITfContext *expectedContext = nullptr) const; + uint64_t _CaptureCompositionEpoch() const; + bool _IsCompositionEpochCurrent(uint64_t compositionEpoch) const; + bool _IsCompositionCurrent(_In_opt_ ITfComposition *expectedComposition) const; + bool _IsLocalSessionResetCurrent(UINT resetToken) const; + void _CompleteLocalSessionReset(UINT resetToken); + bool _IsDeferredKeyReplayCurrent(uint64_t replayToken, + uint64_t focusGeneration, + _In_opt_ ITfContext *expectedContext) const; + void _CompleteDeferredKeyReplay(uint64_t replayToken); + void _RetryDeferredKeyReplay(uint64_t replayToken); // comless helpers static HRESULT CMetasequoiaIME::CreateInstance(REFCLSID rclsid, REFIID riid, _Outptr_result_maybenull_ LPVOID *ppv, @@ -171,25 +197,126 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, DWORD cchPath); static void IpcWorkerThread(CMetasequoiaIME *pIME); - void _QueuePendingCommitCandidate(_In_z_ const WCHAR *pCommitString); - std::wstring _TakePendingCommitCandidate(); void _QueuePendingServerCandidate(UINT msgType, _In_z_ const WCHAR *pCandidateString); bool _TakePendingServerCandidate(_Out_ UINT *pMsgType, _Out_ std::wstring *pCandidateString); - void _QueuePendingPunctuationCommitText(_In_z_ const WCHAR *pCommitString); - std::wstring _TakePendingPunctuationCommitText(); void _ScheduleCandidatePresenterCleanup(_In_ CCandidateListUIPresenter *pPresenter); void _DrainPendingCandidatePresenterCleanup(); private: // functions for the composition object. - HRESULT _HandleCompositionInputWorker(_In_ CCompositionProcessorEngine *pCompositionProcessorEngine, - TfEditCookie ec, _In_ ITfContext *pContext); + HRESULT _HandleCompositionInputWorker(_In_ CCompositionProcessorEngine *pCompositionProcessorEngine, + TfEditCookie ec, _In_ ITfContext *pContext, uint64_t requestId); HRESULT _CreateAndStartCandidate(_In_ CCompositionProcessorEngine *pCompositionProcessorEngine, TfEditCookie ec, _In_ ITfContext *pContext); - HRESULT _HandleCandidateWorker(TfEditCookie ec, _In_ ITfContext *pContext); - - void _StartComposition(_In_ ITfContext *pContext); - void _EndComposition(_In_opt_ ITfContext *pContext); + HRESULT _HandleCandidateWorker(TfEditCookie ec, _In_ ITfContext *pContext, uint64_t requestId, + const std::wstring &prefetchedText); + + struct AsyncKeyRequest + { + UINT message = 0; + UINT code = 0; + WCHAR wch = L'\0'; + uint64_t requestId = UINT64_MAX; + uint64_t focusToken = 0; + uint64_t compositionEpoch = 0; + uint64_t deferredReplayToken = 0; + std::wstring prefetchedText; + }; + bool _PostAsyncKeyRequest(UINT message, UINT code, WCHAR wch, uint64_t requestId, + std::wstring prefetchedText = {}, + uint64_t expectedFocusToken = 0, + uint64_t expectedCompositionEpoch = 0, + uint64_t deferredReplayToken = 0); + bool _TakeAsyncKeyRequest(UINT message, UINT token, _Out_ AsyncKeyRequest &request); + struct WorkerCandidateCommit + { + std::wstring text; + uint64_t focusToken = 0; + uint64_t compositionEpoch = 0; + }; + bool _PostServerCandidateCommit(_In_z_ const WCHAR *candidateText); + bool _TakeServerCandidateCommit(UINT token, _Out_ WorkerCandidateCommit &request); + struct WorkerCompartmentSwitch + { + UINT messageType = 0; + uint64_t focusToken = 0; + uint64_t compositionEpoch = 0; + }; + bool _PostWorkerCompartmentSwitch(UINT messageType, uint64_t focusToken); + bool _TakeWorkerCompartmentSwitch(UINT token, _Out_ WorkerCompartmentSwitch &request); + void _ClearAsyncKeyRequests(); + void _ClearPendingIpcRequests(); + void _RequestLocalSessionReset(_In_opt_ ITfContext *preferredContext, UINT resetToken); + bool _CaptureWindowsTextInputHostFocusLoss(); + + struct DeferredKeyDown + { + enum class Kind + { + KeyDown, + PreservedKey, + ApplicationText + }; + + Kind kind = Kind::KeyDown; + ITfContext *context = nullptr; + WPARAM wParam = 0; + LPARAM lParam = 0; + WCHAR translatedWch = L'\0'; + UINT modifiersDown = 0; + _KEYSTROKE_STATE keyState = {}; + GUID preservedKey = {}; + uint64_t focusGeneration = 0; + UINT replayAttempts = 0; + bool preservedApplied = false; + }; + enum class KeyDownDispatchResult + { + Complete, + Retry, + AwaitingCompletion + }; + bool _HasDeferredKeyBarrier() const; + bool _DeferredKeyQueueHasCapacity() const; + void _EnsureDeferredKeyProjection(); + void _ApplyDeferredKeyProjection(const _KEYSTROKE_STATE &keyState); + void _ApplyDeferredPreservedKeyProjection(REFGUID preservedKey); + bool _RefreshDeferredRecoveryPrefix(_In_ ITfContext *pContext); + void _ArmDeferredRecoveryForTransport(_In_opt_ ITfContext *pContext); + bool _ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM wParam, + _In_opt_ const WCHAR *translatedWch, + _In_opt_ const UINT *modifiersDown, + _Out_ WCHAR *classifiedWch, + _Out_ UINT *classifiedCode, + _Out_ _KEYSTROKE_STATE *keyState); + bool _QueueDeferredKeyDown(_In_ ITfContext *pContext, WPARAM wParam, LPARAM lParam, + WCHAR translatedWch, UINT modifiersDown, + const _KEYSTROKE_STATE &keyState); + bool _QueueDeferredPreservedKey(_In_ ITfContext *pContext, REFGUID preservedKey); + void _ClearDeferredKeyDowns(); + void _ScheduleDeferredKeyDownDrain(); + void _DrainOneDeferredKeyDown(); + HRESULT _RequestDeferredApplicationTextEditSession( + _In_ ITfContext *pContext, WCHAR wch, uint64_t expectedFocusToken, + uint64_t expectedFocusGeneration, uint64_t deferredReplayToken); + KeyDownDispatchResult _DispatchKeyDown(_In_ ITfContext *pContext, WPARAM wParam, + LPARAM lParam, _Out_ BOOL *pIsEaten, + _In_opt_ const WCHAR *translatedWch, + _In_opt_ const UINT *modifiersDown, + _In_opt_ const _KEYSTROKE_STATE *prevalidatedKeyState, + bool canDefer, + uint64_t expectedFocusGeneration, + uint64_t deferredReplayToken = 0); + void _DispatchPreservedKey(_In_ ITfContext *pContext, REFGUID preservedKey, + _Out_ BOOL *pIsEaten, + uint64_t expectedFocusGeneration, + bool isPrevalidated, + uint64_t deferredReplayToken = 0); + + void _StartComposition(_In_ ITfContext *pContext); + HRESULT _EndComposition(_In_opt_ ITfContext *pContext, + _In_opt_ ITfComposition *expectedComposition = nullptr, + bool bypassFocusValidation = false); BOOL _IsComposing(); BOOL _IsKeyboardDisabled(); @@ -209,24 +336,36 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, HRESULT _RemoveDummyCompositionForComposing(TfEditCookie ec, _In_ ITfComposition *pComposition); // Invoke key handler edit session - HRESULT _InvokeKeyHandler(_In_ ITfContext *pContext, UINT code, WCHAR wch, DWORD flags, _KEYSTROKE_STATE keyState); - HRESULT _RequestDirectPunctuationEditSession(_In_ ITfContext *pContext, UINT code, WCHAR wch); + HRESULT _InvokeKeyHandler(_In_ ITfContext *pContext, UINT code, WCHAR wch, DWORD flags, + _KEYSTROKE_STATE keyState, uint64_t requestId, + std::wstring prefetchedText = {}, UINT localResetToken = 0, + uint64_t expectedCompositionEpoch = 0, + uint64_t expectedFocusToken = 0, + uint64_t deferredReplayToken = 0); + HRESULT _RequestDirectPunctuationEditSession(_In_ ITfContext *pContext, UINT code, WCHAR wch, + uint64_t requestId, std::wstring prefetchedText, + uint64_t expectedFocusToken = 0, + uint64_t expectedCompositionEpoch = 0, + uint64_t deferredReplayToken = 0); // function for the language property BOOL _SetCompositionLanguage(TfEditCookie ec, _In_ ITfContext *pContext); // function for the display attribute - void _ClearCompositionDisplayAttributes(TfEditCookie ec, _In_ ITfContext *pContext); + void _ClearCompositionDisplayAttributes( + TfEditCookie ec, _In_ ITfContext *pContext, + _In_opt_ ITfComposition *expectedComposition = nullptr); BOOL _SetCompositionDisplayAttributes(TfEditCookie ec, _In_ ITfContext *pContext, TfGuidAtom gaDisplayAttribute); BOOL _SetCompositionDisplayAttributesForRange(TfEditCookie ec, _In_ ITfContext *pContext, _In_ ITfRange *pRangeComposition, TfGuidAtom gaDisplayAttribute); BOOL _InitDisplayAttributeGuidAtom(); - BOOL _InitThreadMgrEventSink(); - void _UninitThreadMgrEventSink(); - - BOOL _InitTextEditSink(_In_ ITfDocumentMgr *pDocMgr); + BOOL _InitThreadMgrEventSink(); + void _UninitThreadMgrEventSink(); + void _HandleFocusedContextStackChange(_In_opt_ ITfContext *changedContext); + + BOOL _InitTextEditSink(_In_opt_ ITfDocumentMgr *pDocMgr); void _UpdateLanguageBarOnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus); @@ -236,8 +375,10 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, BOOL _InitActiveLanguageProfileNotifySink(); void _UninitActiveLanguageProfileNotifySink(); - BOOL _IsKeyEaten(_In_ ITfContext *pContext, UINT codeIn, _Out_ UINT *pCodeOut, _Out_writes_(1) WCHAR *pwch, - _Out_opt_ _KEYSTROKE_STATE *pKeyState); + BOOL _IsKeyEaten(_In_ ITfContext *pContext, UINT codeIn, _Out_ UINT *pCodeOut, _Out_writes_(1) WCHAR *pwch, + _Out_opt_ _KEYSTROKE_STATE *pKeyState, + _In_opt_ const WCHAR *translatedWch = nullptr, + bool freshCompositionState = false); BOOL _IsRangeCovered(TfEditCookie ec, _In_ ITfRange *pRangeTest, _In_ ITfRange *pRangeCover); VOID _DeleteCandidateList(BOOL fForce, _In_opt_ ITfContext *pContext); @@ -299,15 +440,44 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, HWND _msgWndHandle; std::thread *_pIpcThread; - HANDLE _hToTsfWorkerThreadPipe; + std::atomic _hToTsfWorkerThreadPipe; + std::atomic _workerPipeGeneration; + HANDLE _ipcStopEvent; std::atomic _shouldStopIpcThread; + UINT _ipcReconnectDelayMs; std::mutex _pendingCommitCandidateMutex; - std::wstring _pendingCommitCandidate; - std::wstring _pendingPunctuationCommitText; + std::map _pendingServerCommitMessages; + std::map _pendingWorkerSwitchMessages; + std::map _pendingAsyncKeyMessages; + std::atomic _workerCommitReady; + std::atomic _expectedWorkerFocusToken; + std::atomic _acknowledgedWorkerFocusToken; + std::atomic _compositionEpoch; + std::atomic _localSessionResetPending; + std::atomic _localSessionResetToken; + bool _localResetEditSessionQueued; + UINT _queuedLocalResetToken; + bool _focusResetPending; + bool _activationRequired; + bool _focusLostToWindowsTextInputHost; bool _hasPendingServerCandidate; UINT _pendingServerCandidateMsgType; std::wstring _pendingServerCandidateString; std::deque _pendingCandidatePresenterCleanup; + std::deque _deferredKeyDowns; + std::deque _deferredAppliedPrefix; + DeferredKeyDown _deferredKeyInFlight; + bool _hasDeferredKeyInFlight; + uint64_t _deferredKeyReplayToken; + uint64_t _nextDeferredKeyReplayToken; + bool _deferredKeyProjectionValid; + bool _deferredProjectedImeOpen; + bool _deferredProjectedPunctuationOpen; + bool _deferredProjectedDoubleSingleByteOpen; + size_t _deferredProjectedInputLength; + bool _deferredProjectedCandidateActive; + uint64_t _deferredKeyFocusGeneration; + bool _deferredKeyDrainPosted; LONG _refCount; diff --git a/src/IPC/Ipc.cpp b/src/IPC/Ipc.cpp index 7da0e5d0..6d22b555 100644 --- a/src/IPC/Ipc.cpp +++ b/src/IPC/Ipc.cpp @@ -1,6 +1,8 @@ #include "Ipc.h" #include +#include #include +#include #include #include #include @@ -11,23 +13,101 @@ #include -static HANDLE hMapFile = nullptr; -static void *pBuf; -static FanyImeSharedMemoryData *sharedData; -static bool canUseSharedMemory = false; +static thread_local HANDLE hMapFile = nullptr; +static thread_local void *pBuf = nullptr; +static thread_local FanyImeSharedMemoryData *sharedData = nullptr; +static thread_local bool canUseSharedMemory = false; -static HANDLE hPipe = nullptr; -static HANDLE hFromServerPipe = nullptr; -static HANDLE hToTsfWorkerThreadPipe = nullptr; +static thread_local HANDLE hPipe = nullptr; +static thread_local HANDLE hFromServerPipe = nullptr; +static thread_local HANDLE hToTsfWorkerThreadPipe = nullptr; -static FanyImeNamedpipeData namedpipeData = {}; -static FanyImeNamedpipeDataToTsf namedpipeDataFromServer = {}; +static thread_local FanyImeNamedpipeData namedpipeData = {}; +static thread_local FanyImeNamedpipeDataToTsf namedpipeDataFromServer = {}; +static thread_local FanyImeNamedpipeDataToTsf transportUnavailableReply = {}; +static thread_local std::map pendingReplies; +static thread_local std::deque unsolicitedReplies; +static thread_local uint64_t nextRequestId = 0; +static thread_local uint64_t nextFocusSessionToken = 0; +static thread_local const void *boundFocusStateOwner = nullptr; +static thread_local bool *boundFocusResetPending = nullptr; +static thread_local bool *boundActivationRequired = nullptr; +static thread_local std::atomic *boundExpectedWorkerFocusToken = nullptr; +static thread_local std::atomic *boundLocalSessionResetPending = nullptr; +static thread_local std::atomic *boundLocalSessionResetToken = nullptr; +static thread_local std::atomic *boundWorkerCommitReady = nullptr; +static thread_local std::atomic *boundAcknowledgedWorkerFocusToken = nullptr; +static thread_local std::atomic *boundWorkerPipeHandle = nullptr; +static thread_local std::atomic *boundWorkerPipeGeneration = nullptr; /* Data size transfered from Server process */ static const int ServerDtPipeDataSize = 512; namespace { +constexpr size_t MaxPendingReplyCount = 64; +// These tokens fence messages that can outlive a CMetasequoiaIME instance. +// Per-instance counters can collide after HWND/HANDLE reuse during a rapid +// deactivate/reactivate cycle. +std::atomic nextLocalSessionResetToken{0}; +std::atomic nextWorkerPipeGeneration{0}; + +uint64_t NextProtocolId(uint64_t &counter) +{ + do + { + ++counter; + } while (counter == FANY_IME_UNSOLICITED_REQUEST_ID || counter == FANY_IME_NO_REQUEST_ID); + return counter; +} + +UINT NextAtomicNonzeroToken(std::atomic &counter) +{ + UINT token = 0; + do + { + token = counter.fetch_add(1, std::memory_order_acq_rel) + 1; + } while (token == 0); + return token; +} + +bool IsLocalSessionResetPending() +{ + return boundLocalSessionResetPending && + boundLocalSessionResetPending->load(std::memory_order_acquire); +} + +bool IsExpectedWorkerFocusAcknowledged() +{ + if (!boundWorkerCommitReady) + { + return true; + } + if (!boundExpectedWorkerFocusToken || !boundAcknowledgedWorkerFocusToken) + { + return boundWorkerCommitReady->load(std::memory_order_acquire); + } + const uint64_t expected = boundExpectedWorkerFocusToken->load(std::memory_order_acquire); + return expected != 0 && boundWorkerCommitReady->load(std::memory_order_acquire) && + boundAcknowledgedWorkerFocusToken->load(std::memory_order_acquire) == expected; +} + +void PublishWorkerPipeHandleIfChanged(HANDLE workerPipe) +{ + if (!boundWorkerPipeHandle || + boundWorkerPipeHandle->load(std::memory_order_acquire) == workerPipe) + { + return; + } + if (boundWorkerPipeGeneration) + { + boundWorkerPipeGeneration->store( + NextAtomicNonzeroToken(nextWorkerPipeGeneration), + std::memory_order_release); + } + boundWorkerPipeHandle->store(workerPipe, std::memory_order_release); +} + inline bool IsValidPipeHandle(HANDLE hPipeHandle) { return hPipeHandle != nullptr && hPipeHandle != INVALID_HANDLE_VALUE; @@ -42,20 +122,11 @@ double GetElapsedMilliseconds(const LARGE_INTEGER &startCounter, const LARGE_INT uint64_t GetPipeClientId() { - static const uint64_t clientId = + thread_local const uint64_t clientId = (static_cast(GetCurrentProcessId()) << 32) | static_cast(GetCurrentThreadId()); return clientId; } - - - - - - - - - void ClosePipeHandleIfValid(HANDLE &hPipeHandle) { if (IsValidPipeHandle(hPipeHandle)) @@ -65,6 +136,177 @@ void ClosePipeHandleIfValid(HANDLE &hPipeHandle) hPipeHandle = nullptr; } +void RequestNamedpipeReconnect() +{ + MarkNamedpipeSessionDirty(); + if (Global::msgWndHandle && IsWindow(Global::msgWndHandle)) + { + PostMessage(Global::msgWndHandle, WM_IpcReconnect, 0, 0); + } +} + +bool IsValidServerReply(const FanyImeNamedpipeDataToTsf &reply) +{ + if (reply.msg_type > Global::DataFromServerMsgType::PipeReady) + { + return false; + } + + for (const wchar_t ch : reply.candidate_string) + { + if (ch == L'\0') + { + return true; + } + } + return false; +} + +void CachePendingReply(const FanyImeNamedpipeDataToTsf &reply) +{ + if (reply.msg_type == Global::DataFromServerMsgType::PipeReady) + { + return; + } + if (reply.request_id == FANY_IME_UNSOLICITED_REQUEST_ID) + { + unsolicitedReplies.push_back(reply); + while (unsolicitedReplies.size() > MaxPendingReplyCount) + { + unsolicitedReplies.pop_front(); + } + return; + } + pendingReplies[reply.request_id] = reply; + while (pendingReplies.size() > MaxPendingReplyCount) + { + pendingReplies.erase(pendingReplies.begin()); + } +} + +bool WriteOverlappedWithTimeout(HANDLE hPipeHandle, const void *data, DWORD size, DWORD &bytesWritten) +{ + OVERLAPPED overlapped = {}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!overlapped.hEvent) + { + return false; + } + + BOOL writeResult = WriteFile(hPipeHandle, data, size, &bytesWritten, &overlapped); + if (!writeResult && GetLastError() == ERROR_IO_PENDING) + { + const DWORD waitResult = WaitForSingleObject(overlapped.hEvent, 50); + if (waitResult == WAIT_OBJECT_0) + { + writeResult = GetOverlappedResult(hPipeHandle, &overlapped, &bytesWritten, FALSE); + } + else + { + CancelIoEx(hPipeHandle, &overlapped); + // Cancellation can lose a race with normal completion. Preserve a + // successfully completed hello instead of closing a healthy pipe. + writeResult = GetOverlappedResult(hPipeHandle, &overlapped, &bytesWritten, TRUE); + } + } + + CloseHandle(overlapped.hEvent); + return writeResult && bytesWritten == size; +} + +enum class OverlappedReadResult +{ + Completed, + TimedOut, + Failed, +}; + +OverlappedReadResult ReadOverlappedWithTimeout(HANDLE hPipeHandle, void *data, DWORD size, DWORD timeoutMs, + DWORD &bytesRead) +{ + bytesRead = 0; + OVERLAPPED overlapped = {}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!overlapped.hEvent) + { + return OverlappedReadResult::Failed; + } + + BOOL readResult = ReadFile(hPipeHandle, data, size, &bytesRead, &overlapped); + if (!readResult && GetLastError() == ERROR_IO_PENDING) + { + const DWORD waitResult = WaitForSingleObject(overlapped.hEvent, timeoutMs); + if (waitResult == WAIT_OBJECT_0) + { + readResult = GetOverlappedResult(hPipeHandle, &overlapped, &bytesRead, FALSE); + } + else + { + CancelIoEx(hPipeHandle, &overlapped); + const BOOL completed = GetOverlappedResult(hPipeHandle, &overlapped, &bytesRead, TRUE); + const DWORD completionError = completed ? ERROR_SUCCESS : GetLastError(); + CloseHandle(overlapped.hEvent); + if (completed) + { + return OverlappedReadResult::Completed; + } + return waitResult == WAIT_TIMEOUT && completionError == ERROR_OPERATION_ABORTED + ? OverlappedReadResult::TimedOut + : OverlappedReadResult::Failed; + } + } + + CloseHandle(overlapped.hEvent); + return readResult ? OverlappedReadResult::Completed : OverlappedReadResult::Failed; +} + +bool WaitForReplyPipeReady(HANDLE hPipeHandle) +{ + constexpr DWORD readyTimeoutMs = 250; + const ULONGLONG deadline = GetTickCount64() + readyTimeoutMs; + + while (true) + { + const ULONGLONG now = GetTickCount64(); + if (now >= deadline) + { + return false; + } + + FanyImeNamedpipeDataToTsf reply = {}; + DWORD bytesRead = 0; + const DWORD remainingMs = static_cast(deadline - now); + const OverlappedReadResult result = + ReadOverlappedWithTimeout(hPipeHandle, &reply, sizeof(reply), remainingMs, bytesRead); + if (result != OverlappedReadResult::Completed || bytesRead != sizeof(reply) || + !IsValidServerReply(reply)) + { + return false; + } + + if (reply.msg_type == Global::DataFromServerMsgType::PipeReady) + { + return reply.request_id == FANY_IME_UNSOLICITED_REQUEST_ID; + } + + // A reconnect can race with a reply already queued by the Server. + // Preserve it for its owning edit session and continue waiting for the + // explicit endpoint-registration acknowledgement. + CachePendingReply(reply); + } +} + +bool WaitForWorkerPipeReady(HANDLE hPipeHandle) +{ + constexpr DWORD readyTimeoutMs = 250; + FanyImeNamedpipeDataToTsfWorkerThread reply = {}; + DWORD bytesRead = 0; + const OverlappedReadResult result = + ReadOverlappedWithTimeout(hPipeHandle, &reply, sizeof(reply), readyTimeoutMs, bytesRead); + return result == OverlappedReadResult::Completed && bytesRead == sizeof(reply) && + reply.msg_type == Global::DataToTsfWorkerThreadMsgType::PipeReady && reply.data[0] == L'\0'; +} + bool WritePipeHello(HANDLE hPipeHandle, UINT pipeRole) { DWORD bytesWritten = 0; @@ -80,44 +322,286 @@ bool WritePipeHello(HANDLE hPipeHandle, UINT pipeRole) FanyImePipeHello hello = {}; hello.client_id = GetPipeClientId(); hello.pipe_role = pipeRole; - BOOL ret = WriteFile(hPipeHandle, &hello, sizeof(hello), &bytesWritten, NULL); - return ret && bytesWritten == sizeof(hello); + return WriteOverlappedWithTimeout(hPipeHandle, &hello, sizeof(hello), bytesWritten); } bool TryOpenClientPipe(HANDLE &hPipeHandle, const wchar_t *pipeName, UINT pipeRole) { if (IsValidPipeHandle(hPipeHandle)) { - return true; + if (pipeRole == FanyImePipeRole::ToTsfWorkerThread && boundWorkerPipeHandle && + boundWorkerPipeHandle->load(std::memory_order_acquire) != hPipeHandle) + { + // IpcWorkerThread clears the published atomic only after its + // OVERLAPPED read has completed. A still-nonnull TLS handle is + // therefore a stale endpoint and is now safe to close/re-register; + // do not publish it again before the queued disconnect message. + MarkNamedpipeSessionDirty(); + ClosePipeHandleIfValid(hPipeHandle); + } + else + { + if (pipeRole != FanyImePipeRole::ToTsf) + { + // Main is synchronous and is validated by its NOWAIT write. The + // worker pipe can have a read outstanding on another thread, which + // is responsible for reporting disconnection. + return true; + } + + // The reply pipe is overlapped and has no outstanding operation here, + // so PeekNamedPipe is a nonblocking liveness probe. A stale non-null + // HANDLE after Server restart must not authorize the first new key. + DWORD bytesAvailable = 0; + if (PeekNamedPipe(hPipeHandle, nullptr, 0, nullptr, &bytesAvailable, nullptr)) + { + return true; + } + + MarkNamedpipeSessionDirty(); + ClosePipeHandleIfValid(hPipeHandle); + ResetNamedpipeReplyState(); + } } + const DWORD flags = pipeRole == FanyImePipeRole::Main ? 0 : FILE_FLAG_OVERLAPPED; HANDLE openedPipe = CreateFile( // pipeName, // GENERIC_READ | GENERIC_WRITE, // 0, // nullptr, // OPEN_EXISTING, // - 0, // + flags, // nullptr // ); + if (openedPipe == INVALID_HANDLE_VALUE) + { + if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10)) + { + openedPipe = CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, flags, nullptr); + } + } + if (openedPipe == INVALID_HANDLE_VALUE) { return false; } hPipeHandle = openedPipe; + // Main writes are NOWAIT. Both receive pipes use true overlapped I/O with + // explicit deadlines, so neither can stall the host UI thread. + DWORD readMode = PIPE_READMODE_MESSAGE | (pipeRole == FanyImePipeRole::Main ? PIPE_NOWAIT : PIPE_WAIT); + if (!SetNamedPipeHandleState(hPipeHandle, &readMode, nullptr, nullptr)) + { + ClosePipeHandleIfValid(hPipeHandle); + return false; + } if (!WritePipeHello(hPipeHandle, pipeRole)) { ClosePipeHandleIfValid(hPipeHandle); return false; } + if (pipeRole == FanyImePipeRole::ToTsf && !WaitForReplyPipeReady(hPipeHandle)) + { + // A successful CreateFile/hello write does not mean the Server has + // registered this reverse endpoint yet. Do not allow a key request to + // overtake registration and lose its reply. + ClosePipeHandleIfValid(hPipeHandle); + return false; + } + if (pipeRole == FanyImePipeRole::ToTsfWorkerThread && !WaitForWorkerPipeReady(hPipeHandle)) + { + // Do not publish a worker handle until the Server has installed the + // exact endpoint. This prevents Main/Activated from overtaking the + // worker hello on cold start or under scheduler pressure. + ClosePipeHandleIfValid(hPipeHandle); + return false; + } + if (pipeRole == FanyImePipeRole::ToTsfWorkerThread) + { + // Every physical worker endpoint needs a new activation token. A + // registration ACK only proves that the endpoint exists; it does not + // authorize commits from the preceding endpoint/focus generation. + RequireNamedpipeFocusActivation(); + } return true; } } // namespace +void BindNamedpipeFocusState(const void *owner, bool *focusResetPending, bool *activationRequired, + std::atomic *expectedWorkerFocusToken, + std::atomic *localSessionResetPending, + std::atomic *localSessionResetToken, + std::atomic *workerCommitReady, + std::atomic *acknowledgedWorkerFocusToken, + std::atomic *workerPipeHandle, + std::atomic *workerPipeGeneration) +{ + boundFocusStateOwner = owner; + boundFocusResetPending = focusResetPending; + boundActivationRequired = activationRequired; + boundExpectedWorkerFocusToken = expectedWorkerFocusToken; + boundLocalSessionResetPending = localSessionResetPending; + boundLocalSessionResetToken = localSessionResetToken; + boundWorkerCommitReady = workerCommitReady; + boundAcknowledgedWorkerFocusToken = acknowledgedWorkerFocusToken; + boundWorkerPipeHandle = workerPipeHandle; + boundWorkerPipeGeneration = workerPipeGeneration; +} + +void UnbindNamedpipeFocusState(const void *owner) +{ + if (owner == nullptr || boundFocusStateOwner != owner) + { + return; + } + boundFocusStateOwner = nullptr; + boundFocusResetPending = nullptr; + boundActivationRequired = nullptr; + boundExpectedWorkerFocusToken = nullptr; + boundLocalSessionResetPending = nullptr; + boundLocalSessionResetToken = nullptr; + boundWorkerCommitReady = nullptr; + boundAcknowledgedWorkerFocusToken = nullptr; + boundWorkerPipeHandle = nullptr; + boundWorkerPipeGeneration = nullptr; +} + +bool IsNamedpipeFocusStateOwner(const void *owner) +{ + return owner != nullptr && boundFocusStateOwner == owner; +} + +UINT BeginNamedpipeLocalSessionReset() +{ + if (!boundLocalSessionResetPending || !boundLocalSessionResetToken) + { + return 0; + } + const UINT token = NextAtomicNonzeroToken(nextLocalSessionResetToken); + boundLocalSessionResetToken->store(token, std::memory_order_release); + boundLocalSessionResetPending->store(true, std::memory_order_release); + return token; +} + +void InvalidateNamedpipeWorkerGeneration() +{ + if (boundWorkerPipeGeneration) + { + boundWorkerPipeGeneration->store( + NextAtomicNonzeroToken(nextWorkerPipeGeneration), + std::memory_order_release); + } +} + +void RequireNamedpipeFocusActivation() +{ + if (boundActivationRequired) + { + *boundActivationRequired = true; + } + const uint64_t token = NextProtocolId(nextFocusSessionToken); + if (boundExpectedWorkerFocusToken) + { + boundExpectedWorkerFocusToken->store(token, std::memory_order_release); + } + if (boundWorkerCommitReady) + { + boundWorkerCommitReady->store(false, std::memory_order_release); + } + if (boundAcknowledgedWorkerFocusToken) + { + boundAcknowledgedWorkerFocusToken->store(0, std::memory_order_release); + } +} + +void MarkNamedpipeFocusLost() +{ + if (boundFocusResetPending) + { + *boundFocusResetPending = true; + } + if (boundActivationRequired) + { + *boundActivationRequired = false; + } + if (boundExpectedWorkerFocusToken) + { + boundExpectedWorkerFocusToken->store(0, std::memory_order_release); + } + if (boundWorkerCommitReady) + { + boundWorkerCommitReady->store(false, std::memory_order_release); + } + if (boundAcknowledgedWorkerFocusToken) + { + boundAcknowledgedWorkerFocusToken->store(0, std::memory_order_release); + } + // Frames from the focus epoch that just ended must not survive into the + // next explicit activation. This handle has no outstanding worker read. + ClosePipeHandleIfValid(hFromServerPipe); + ResetNamedpipeReplyState(); +} + +void MarkNamedpipeSessionDirty() +{ + // A transport failure must not manufacture a focus transition. Preserve + // a real OnKillThreadFocus suspension that is already pending, but leave + // a currently focused session's flag false. + RequireNamedpipeFocusActivation(); + ClosePipeHandleIfValid(hFromServerPipe); + ResetNamedpipeReplyState(); + + bool shouldPostReset = false; + UINT resetToken = 0; + if (boundLocalSessionResetPending && + !boundLocalSessionResetPending->exchange(true, std::memory_order_acq_rel)) + { + resetToken = BeginNamedpipeLocalSessionReset(); + shouldPostReset = resetToken != 0; + } + bool resetDelivered = !shouldPostReset; + const HWND ownerWindow = Global::msgWndHandle; + if (shouldPostReset && ownerWindow && IsWindow(ownerWindow)) + { + resetDelivered = PostMessage(ownerWindow, WM_IpcSessionDirty, + static_cast(resetToken), 0) != FALSE; + if (!resetDelivered && GetWindowThreadProcessId(ownerWindow, nullptr) == GetCurrentThreadId()) + { + SendMessage(ownerWindow, WM_IpcSessionDirty, static_cast(resetToken), 0); + resetDelivered = true; + } + } + if (shouldPostReset && !resetDelivered && boundLocalSessionResetPending && + boundLocalSessionResetToken && + boundLocalSessionResetToken->load(std::memory_order_acquire) == resetToken) + { + // No message can run the local cancel. Release only the exact token; + // the activation/transport dirty flags remain and the next key will + // retry the lifecycle handshake. + boundLocalSessionResetPending->store(false, std::memory_order_release); + } +} + +bool MarkNamedpipeSessionDirtyForOwner(const void *owner) +{ + if (!IsNamedpipeFocusStateOwner(owner)) + { + return false; + } + MarkNamedpipeSessionDirty(); + return true; +} + int InitIpc() { + // The live protocol is named-pipe-only. Keep the legacy mapping open for + // ABI compatibility, but never advertise or select it for new traffic. + canUseSharedMemory = false; + sharedData = nullptr; + pBuf = nullptr; + // // Shared memory, open here // @@ -142,8 +626,6 @@ int InitIpc() return 0; } - bool alreadyExists = (GetLastError() == ERROR_ALREADY_EXISTS); - pBuf = MapViewOfFile( // hMapFile, // FILE_MAP_ALL_ACCESS, // @@ -154,18 +636,14 @@ int InitIpc() if (!pBuf) { - // TODO: Error handling + CloseHandle(hMapFile); + hMapFile = nullptr; + sharedData = nullptr; + canUseSharedMemory = false; + return 0; } sharedData = static_cast(pBuf); - // Only initialize the shared memory when first created - if (!alreadyExists) - { - // Initialize - *sharedData = {}; - sharedData->point[0] = 100; - sharedData->point[1] = 100; - } return 0; } @@ -177,13 +655,33 @@ int InitNamedpipe() int ConnectToAllNamedpipe() { - bool mainPipeReady = TryOpenClientPipe(hPipe, FANY_IME_NAMED_PIPE, FanyImePipeRole::Main); - bool toTsfPipeReady = TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, FanyImePipeRole::ToTsf); - bool toTsfWorkerPipeReady = - TryOpenClientPipe(hToTsfWorkerThreadPipe, FANY_IME_TO_TSF_WORKER_THREAD_NAMED_PIPE, - FanyImePipeRole::ToTsfWorkerThread); + if (!TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, + FanyImePipeRole::ToTsf)) + { + return 0; + } + if (!TryOpenClientPipe(hToTsfWorkerThreadPipe, + FANY_IME_TO_TSF_WORKER_THREAD_NAMED_PIPE, + FanyImePipeRole::ToTsfWorkerThread)) + { + return 0; + } - return (mainPipeReady && toTsfPipeReady && toTsfWorkerPipeReady) ? 1 : 0; + // Publish the ACKed worker endpoint before attempting Main. If Main is + // unavailable, the next retry must keep this healthy endpoint instead of + // mistaking its still-private TLS handle for a stale registration. + PublishWorkerPipeHandleIfChanged(hToTsfWorkerThreadPipe); + if (IsLocalSessionResetPending()) + { + return 0; + } + + // Open Main last. Once it exists, a key may be sent immediately, so both + // reverse channels must already have completed their hello handshakes. + return TryOpenClientPipe(hPipe, FANY_IME_NAMED_PIPE, + FanyImePipeRole::Main) + ? 1 + : 0; } int ConnectToTsfNamedpipe() @@ -197,11 +695,7 @@ int CloseIpc() // Namedpipe // CloseNamedpipe(); - - if (!canUseSharedMemory) - { - return -1; - } + const int result = canUseSharedMemory ? 0 : -1; // // Shared memory @@ -211,12 +705,14 @@ int CloseIpc() UnmapViewOfFile(pBuf); pBuf = nullptr; } + sharedData = nullptr; if (hMapFile) { CloseHandle(hMapFile); hMapFile = nullptr; } + canUseSharedMemory = false; // // Events @@ -234,7 +730,7 @@ int CloseIpc() } } - return 0; + return result; } int CloseNamedpipe() @@ -242,9 +738,16 @@ int CloseNamedpipe() ClosePipeHandleIfValid(hPipe); ClosePipeHandleIfValid(hFromServerPipe); ClosePipeHandleIfValid(hToTsfWorkerThreadPipe); + ResetNamedpipeReplyState(); return 0; } +void ResetNamedpipeReplyState() +{ + pendingReplies.clear(); + unsolicitedReplies.clear(); +} + HANDLE GetToTsfWorkerThreadNamedpipe() { return hToTsfWorkerThreadPipe; @@ -260,44 +763,10 @@ int WriteDataToSharedMemory( // UINT write_flag // ) { - if (!canUseSharedMemory) - { - return WriteDataToNamedPipe(keycode, wch, modifiers_down, point, pinyin_length, pinyin_string, write_flag); - } - - if (write_flag >> 0 & 1u) - { - sharedData->keycode = keycode; - } - - if (write_flag >> 1 & 1u) - { - sharedData->wch = wch; - } - - if (write_flag >> 2 & 1u) - { - sharedData->modifiers_down = modifiers_down; - } - - if (write_flag >> 3 & 1u) - { - sharedData->point[0] = point[0]; - sharedData->point[1] = point[1]; - } - - if (write_flag >> 4 & 1u) - { - sharedData->pinyin_length = pinyin_length; - } - - if (write_flag >> 5 & 1u) - { - wcscpy_s(sharedData->pinyin_string, pinyin_string.c_str()); - sharedData->pinyin_string[pinyin_length] = L'\0'; - } - - return 0; + // The shared-memory protocol has no client_id/request_id and cannot be + // made safe in a multi-TSF-thread process. Keep the mapping code only for + // legacy compatibility; all live event payloads use the named-pipe ABI. + return WriteDataToNamedPipe(keycode, wch, modifiers_down, point, pinyin_length, pinyin_string, write_flag); } /** @@ -321,6 +790,10 @@ int WriteDataToNamedPipe( // UINT write_flag // ) { + // Every logical event starts from a clean packet so status/UI events can + // never leak key or pinyin data from the preceding request. + namedpipeData = {}; + if (write_flag >> 0 & 1u) { namedpipeData.keycode = keycode; @@ -344,193 +817,124 @@ int WriteDataToNamedPipe( // if (write_flag >> 4 & 1u) { - namedpipeData.pinyin_length = pinyin_length; + namedpipeData.pinyin_length = max(0, min(pinyin_length, 127)); } if (write_flag >> 5 & 1u) { - wcscpy_s(namedpipeData.pinyin_string, pinyin_string.c_str()); - namedpipeData.pinyin_string[pinyin_length] = L'\0'; + const size_t copyLength = min(pinyin_string.size(), _countof(namedpipeData.pinyin_string) - 1); + wmemcpy(namedpipeData.pinyin_string, pinyin_string.data(), copyLength); + namedpipeData.pinyin_string[copyLength] = L'\0'; + namedpipeData.pinyin_length = static_cast(copyLength); } return 0; } -int SendKeyEventToUIProcess() +KeyEventSendResult SendKeyEventToUIProcess(uint64_t *requestId) { - if (!canUseSharedMemory) - { - return SendKeyEventToUIProcessViaNamedPipe(); - } - - HANDLE hEvent = OpenEventW( // - EVENT_MODIFY_STATE, // - FALSE, // - FANY_IME_EVENT_ARRAY[0].c_str() // - ); // - - if (!hEvent) - { - // TODO: Error handling - } - - if (!SetEvent(hEvent)) - { - // TODO: Error handling - } - - CloseHandle(hEvent); - return 0; + return SendKeyEventToUIProcessViaNamedPipe(requestId); } int SendHideCandidateWndEventToUIProcess() { - if (!canUseSharedMemory) - { - return SendHideCandidateWndEventToUIProcessViaNamedPipe(); - } - - HANDLE hEvent = OpenEventW( // - EVENT_MODIFY_STATE, // - FALSE, // - FANY_IME_EVENT_ARRAY[1].c_str() // FanyHideCandidateWndEvent - ); // - - if (!hEvent) - { - // TODO: Error handling - } - - if (!SetEvent(hEvent)) - { - // TODO: Error handling - } - - CloseHandle(hEvent); - return 0; + return SendHideCandidateWndEventToUIProcessViaNamedPipe(); } int SendShowCandidateWndEventToUIProcess() { - if (!canUseSharedMemory) - { - return SendShowCandidateWndEventToUIProcessViaNamedPipe(); - } - - HANDLE hEvent = OpenEventW( // - EVENT_MODIFY_STATE, // - FALSE, // - FANY_IME_EVENT_ARRAY[2].c_str() // FanyShowCandidateWndEvent - ); // - - if (!hEvent) - { - // TODO: Error handling - } - - if (!SetEvent(hEvent)) - { - // TODO: Error handling - } - - CloseHandle(hEvent); - return 0; + return SendShowCandidateWndEventToUIProcessViaNamedPipe(); } int SendMoveCandidateWndEventToUIProcess() { - if (!canUseSharedMemory) - { - return SendMoveCandidateWndEventToUIProcessViaNamedPipe(); - } - - HANDLE hEvent = OpenEventW( // - EVENT_MODIFY_STATE, // - FALSE, // - FANY_IME_EVENT_ARRAY[3].c_str() // FanyMoveCandidateWndEvent - ); // - - if (!hEvent) - { - // TODO: Error handling - } - - if (!SetEvent(hEvent)) - { - // TODO: Error handling - } - - CloseHandle(hEvent); - return 0; + return SendMoveCandidateWndEventToUIProcessViaNamedPipe(); } int SendLangbarRightClickEventToUIProcess(const RECT *prcArea) { - if (!canUseSharedMemory) - { - return SendLangbarRightClickEventToUIProcessViaNamedPipe(prcArea); - } - return 0; + return SendLangbarRightClickEventToUIProcessViaNamedPipe(prcArea); } // // Named pipe // -void SendToNamedpipe() +bool SendToNamedpipe(bool *deliveryAmbiguous = nullptr) { - namedpipeData.client_id = GetPipeClientId(); + if (deliveryAmbiguous) + { + *deliveryAmbiguous = false; + } + FanyImeNamedpipeData packet = namedpipeData; + packet.client_id = GetPipeClientId(); + const bool isLifecyclePacket = + packet.event_type == FanyImePipeEventType::ClientHello || + packet.event_type == FanyImePipeEventType::ClientActivated || + packet.event_type == FanyImePipeEventType::ClientDeactivated || + packet.event_type == FanyImePipeEventType::ClientSuspended; + + // Lifecycle packets are used by Ensure... itself. Every other packet is + // authorized only after reverse registration, worker publication, exact + // token activation, and the worker's FocusSessionReady echo. Preserve the + // staged packet because Ensure... emits lifecycle packets through this + // same thread-local buffer. + if (!isLifecyclePacket) + { + if (!Global::g_connected || !EnsureNamedpipeFocusSessionActivated()) + { + namedpipeData = packet; + return false; + } + namedpipeData = packet; + } + + const bool mainPipeWasOpen = IsValidPipeHandle(hPipe); if (!TryOpenClientPipe(hPipe, FANY_IME_NAMED_PIPE, FanyImePipeRole::Main)) { - return; + RequestNamedpipeReconnect(); + return false; + } + + // A newly opened Main cannot authorize an ordinary packet by itself. In + // normal operation the barrier above already opened Main; reaching this + // branch means the transport changed underneath that barrier. + if (!isLifecyclePacket && !mainPipeWasOpen) + { + RequestNamedpipeReconnect(); + return false; + } + if (!isLifecyclePacket && + (IsLocalSessionResetPending() || + !IsExpectedWorkerFocusAcknowledged())) + { + // The worker reader can report a disconnect concurrently with the UI + // thread returning from Ensure. Recheck the exact acknowledgement at + // the final write boundary instead of sending into an epoch whose + // result channel has already disappeared. + RequestNamedpipeReconnect(); + return false; } DWORD bytesWritten = 0; - BOOL ret = WriteFile( // - hPipe, // - &namedpipeData, // - sizeof(namedpipeData), // - &bytesWritten, // - NULL // - ); - if (!ret || bytesWritten != sizeof(namedpipeData)) + if (WriteFile(hPipe, &packet, sizeof(packet), &bytesWritten, nullptr) && + bytesWritten == sizeof(packet)) { - /* Error handling: 必须将 hPipe 置为无效,否则将留下脏句柄,导致有些情况下无法再次连接 */ - ClosePipeHandleIfValid(hPipe); - for (int i = 0; i < 10; ++i) - { - Sleep(10); - if (TryOpenClientPipe(hPipe, FANY_IME_NAMED_PIPE, FanyImePipeRole::Main)) - { - break; - } - } - - if (hPipe == INVALID_HANDLE_VALUE) - { - return; - } - else - { - } - - namedpipeData.client_id = GetPipeClientId(); - DWORD bytesWritten = 0; - BOOL ret = WriteFile( // - hPipe, // - &namedpipeData, // - sizeof(namedpipeData), // - &bytesWritten, // - NULL // - ); - - if (!ret || bytesWritten != sizeof(namedpipeData)) - { - } - - return; + return true; } + + // Delivery is ambiguous after any failed write. Never replay an old + // activation token, key, status, or UI command on a replacement Main. + // The reconnect timer performs the complete reset/deactivate/activate + // transaction, followed by a fresh full status snapshot. + if (deliveryAmbiguous) + { + *deliveryAmbiguous = true; + } + ClosePipeHandleIfValid(hPipe); + RequestNamedpipeReconnect(); + return false; } /** @@ -540,62 +944,18 @@ void SendToNamedpipe() */ void ClearNamedpipeDataIfExists(bool force) { - bool isSpaceOrNumber = Global::Keycode == VK_SPACE || // - (Global::Keycode > '0' && Global::Keycode < '9') || // - (Global::CommitWithFirstCandPunc.count(Global::wch) > 0); - /* Only clear namedpipe data if keycode is space or number, for better performance */ - if (!force && !isSpaceOrNumber) - { - return; - } - - if (!hFromServerPipe || hFromServerPipe == INVALID_HANDLE_VALUE) // Try to reconnect - { - if (!TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, FanyImePipeRole::ToTsf)) - { - return; - } - } - - DWORD bytesAvailable = 0; - DWORD bytesRead = 0; - int clearedCount = 0; - while (true) - { - BOOL peekOk = PeekNamedPipe( // - hFromServerPipe, // - nullptr, // - 0, // - nullptr, // - &bytesAvailable, // - nullptr // - ); - - if (!peekOk || bytesAvailable == 0) - { - if (!peekOk) - { - } - break; // no more data or pipe error - } - - BOOL readOk = ReadFile( // - hFromServerPipe, // - &namedpipeDataFromServer, // - sizeof(namedpipeDataFromServer), // - &bytesRead, // - NULL // - ); - - if (!readOk || bytesRead == 0) - { - break; - } - clearedCount++; - } - + // Request IDs make destructive draining both unnecessary and incorrect: + // an async edit session may still own any frame currently in this pipe. + // Mismatched replies are retained by TryReadData... in pendingReplies. if (force) { + // Compatibility path for Server-initiated candidate clicks: legacy + // Server builds deliver the same candidate both on the worker pipe and + // as one unsolicited (request_id == 0) reply. The worker payload has + // already been consumed by the caller, so discard exactly that one + // duplicate. TryRead caches every nonzero request reply it encounters + // and ignores PipeReady, preserving all edit-session-owned frames. + (void)TryReadDataFromServerPipeWithTimeout(FANY_IME_UNSOLICITED_REQUEST_ID); } } @@ -604,64 +964,110 @@ void ClearNamedpipeDataIfExists(bool force) * * @return struct FanyImeNamedpipeDataToTsf* */ -struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout() +struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout(uint64_t expectedRequestId) { - std::pair ret = {0, L""}; - int timeoutMs = 50; // Default timeout 50ms + constexpr int timeoutMs = 50; - if (!hFromServerPipe || hFromServerPipe == INVALID_HANDLE_VALUE) // Try to reconnect + auto transportUnavailable = []() { + transportUnavailableReply = {}; + transportUnavailableReply.msg_type = Global::DataFromServerMsgType::TransportUnavailable; + return &transportUnavailableReply; + }; + + if (expectedRequestId == FANY_IME_NO_REQUEST_ID) + { + return transportUnavailable(); + } + + if (expectedRequestId == FANY_IME_UNSOLICITED_REQUEST_ID && !unsolicitedReplies.empty()) + { + namedpipeDataFromServer = unsolicitedReplies.front(); + unsolicitedReplies.pop_front(); + return &namedpipeDataFromServer; + } + const auto cached = pendingReplies.find(expectedRequestId); + if (expectedRequestId != FANY_IME_UNSOLICITED_REQUEST_ID && cached != pendingReplies.end()) + { + namedpipeDataFromServer = cached->second; + pendingReplies.erase(cached); + return &namedpipeDataFromServer; + } + + if (!IsValidPipeHandle(hFromServerPipe)) { if (!TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, FanyImePipeRole::ToTsf)) { - namedpipeDataFromServer.msg_type = Global::DataFromServerMsgType::Normal; - // wcscpy_s(namedpipeDataFromServer.candidate_string, L"PipeOpenError"); - wcscpy_s(namedpipeDataFromServer.candidate_string, L"X"); - return &namedpipeDataFromServer; + RequestNamedpipeReconnect(); + return transportUnavailable(); } } - DWORD bytesAvailable = 0; - DWORD lastPeekError = ERROR_SUCCESS; - int peekFailureCount = 0; - LARGE_INTEGER frequency = {}; - LARGE_INTEGER startCounter = {}; - LARGE_INTEGER nowCounter = {}; - QueryPerformanceFrequency(&frequency); - QueryPerformanceCounter(&startCounter); + const ULONGLONG deadline = GetTickCount64() + timeoutMs; while (true) { -// TODO: Do not log - BOOL peekOk = PeekNamedPipe(hFromServerPipe, nullptr, 0, nullptr, &bytesAvailable, nullptr); - if (!peekOk) + DWORD bytesRead = 0; + namedpipeDataFromServer = {}; + const ULONGLONG now = GetTickCount64(); + const DWORD remainingMs = now >= deadline ? 0 : static_cast(deadline - now); + const OverlappedReadResult readResult = ReadOverlappedWithTimeout( + hFromServerPipe, &namedpipeDataFromServer, sizeof(namedpipeDataFromServer), remainingMs, bytesRead); + if (readResult == OverlappedReadResult::TimedOut) { - lastPeekError = GetLastError(); - peekFailureCount++; + ClosePipeHandleIfValid(hFromServerPipe); + RequestNamedpipeReconnect(); + return transportUnavailable(); } - else if (bytesAvailable > 0) + if (readResult == OverlappedReadResult::Failed) { - auto ret = ReadDataFromServerViaNamedPipe(); - return ret; + ClosePipeHandleIfValid(hFromServerPipe); + pendingReplies.clear(); + unsolicitedReplies.clear(); + RequestNamedpipeReconnect(); + return transportUnavailable(); + } + if (bytesRead != sizeof(namedpipeDataFromServer)) + { + ClosePipeHandleIfValid(hFromServerPipe); + pendingReplies.clear(); + unsolicitedReplies.clear(); + RequestNamedpipeReconnect(); + return transportUnavailable(); + } + if (!IsValidServerReply(namedpipeDataFromServer)) + { + ClosePipeHandleIfValid(hFromServerPipe); + pendingReplies.clear(); + unsolicitedReplies.clear(); + RequestNamedpipeReconnect(); + return transportUnavailable(); } - QueryPerformanceCounter(&nowCounter); - if (GetElapsedMilliseconds(startCounter, nowCounter, frequency) >= timeoutMs) + // A delayed reply from a timed-out key must not be consumed as the + // reply for the next key. + if (namedpipeDataFromServer.msg_type != Global::DataFromServerMsgType::PipeReady && + namedpipeDataFromServer.request_id == expectedRequestId) + { + return &namedpipeDataFromServer; + } + + // Another async edit session owns this frame. Keep it bounded and let + // that session retrieve it by its explicit request ID later. + CachePendingReply(namedpipeDataFromServer); + + if (GetTickCount64() >= deadline) { break; } - - // Avoid Sleep(1), which often expands to a full scheduler tick (~15.6ms) - // in explorer.exe and makes space-commit latency visibly worse. - if (!SwitchToThread()) - { - YieldProcessor(); - } } - namedpipeDataFromServer.msg_type = Global::DataFromServerMsgType::Normal; - // Pipe timeout error - wcscpy_s(namedpipeDataFromServer.candidate_string, L"T"); - return &namedpipeDataFromServer; + // We consumed only other request IDs until this request's deadline. The + // Server may already have committed a selection, so preserving local + // composition would split the two state machines. Treat this exactly like + // a broken transport and schedule a tokenized reset plus local cancel. + ClosePipeHandleIfValid(hFromServerPipe); + RequestNamedpipeReconnect(); + return transportUnavailable(); } /** @@ -671,39 +1077,9 @@ struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout() * * @return struct FanyImeNamedpipeDataToTsf* */ -struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe() +struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe(uint64_t expectedRequestId) { - if (!hFromServerPipe || hFromServerPipe == INVALID_HANDLE_VALUE) // Try to reconnect - { - if (!TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, FanyImePipeRole::ToTsf)) - { - namedpipeDataFromServer.msg_type = 0; - // wcscpy_s(namedpipeDataFromServer.candidate_string, L"PipeOpenError"); - wcscpy_s(namedpipeDataFromServer.candidate_string, L"X"); - return &namedpipeDataFromServer; - } - } - - DWORD bytesRead = 0; - BOOL readResult = ReadFile( // - hFromServerPipe, // - &namedpipeDataFromServer, // - sizeof(namedpipeDataFromServer), // - &bytesRead, // - NULL // - ); - if (!readResult || bytesRead == 0) // Disconnected or error - { - ClosePipeHandleIfValid(hFromServerPipe); - } - else - { - return &namedpipeDataFromServer; - } - - namedpipeDataFromServer.msg_type = 0; - wcscpy_s(namedpipeDataFromServer.candidate_string, L"ReadDataError"); - return &namedpipeDataFromServer; + return TryReadDataFromServerPipeWithTimeout(expectedRequestId); } /** @@ -761,16 +1137,54 @@ void SendToAuxNamedpipe(std::wstring pipeData) * 2: FanyShowCandidateWndEvent * 3: FanyMoveCandidateWndEvent */ -int SendKeyEventToUIProcessViaNamedPipe() +KeyEventSendResult SendKeyEventToUIProcessViaNamedPipe(uint64_t *requestId) { - namedpipeData.event_type = FanyImePipeEventType::KeyEvent; - SendToNamedpipe(); + if (requestId) + { + *requestId = FANY_IME_NO_REQUEST_ID; + } - return 0; + // Lifecycle packets share the thread-local staging buffer with key data. + // Preserve the fully populated key packet while Ensure... emits the + // pending Deactivated/Activated handshake; otherwise the first key after + // a focus or transport reset is silently replaced with an empty packet. + const FanyImeNamedpipeData keyPacket = namedpipeData; + + // Focus changes and transport replacement are transactional: reverse and + // worker endpoints, Deactivated reset, and explicit tokenized Activated + // must all precede the first key. + if (!EnsureNamedpipeFocusSessionActivated()) + { + namedpipeData = keyPacket; + RequestNamedpipeReconnect(); + return KeyEventSendResult::DefinitelyNotSent; + } + if (IsLocalSessionResetPending()) + { + namedpipeData = keyPacket; + return KeyEventSendResult::DefinitelyNotSent; + } + + const uint64_t newRequestId = NextProtocolId(nextRequestId); + namedpipeData = keyPacket; + namedpipeData.request_id = newRequestId; + namedpipeData.event_type = FanyImePipeEventType::KeyEvent; + bool deliveryAmbiguous = false; + if (!SendToNamedpipe(&deliveryAmbiguous)) + { + return deliveryAmbiguous ? KeyEventSendResult::DeliveryAmbiguous + : KeyEventSendResult::DefinitelyNotSent; + } + if (requestId) + { + *requestId = newRequestId; + } + return KeyEventSendResult::Sent; } int SendHideCandidateWndEventToUIProcessViaNamedPipe() { + namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::HideCandidateWnd; SendToNamedpipe(); @@ -779,6 +1193,10 @@ int SendHideCandidateWndEventToUIProcessViaNamedPipe() int SendShowCandidateWndEventToUIProcessViaNamedPipe() { + // CandidateListUIPresenter stages the text/caret payload immediately + // before this call. Preserve that payload and overwrite only routing + // fields for this logical event. + namedpipeData.request_id = 0; namedpipeData.event_type = FanyImePipeEventType::ShowCandidateWnd; SendToNamedpipe(); @@ -787,6 +1205,8 @@ int SendShowCandidateWndEventToUIProcessViaNamedPipe() int SendMoveCandidateWndEventToUIProcessViaNamedPipe() { + // The caller has already staged the new caret point in namedpipeData. + namedpipeData.request_id = 0; namedpipeData.event_type = FanyImePipeEventType::MoveCandidateWnd; SendToNamedpipe(); @@ -795,6 +1215,7 @@ int SendMoveCandidateWndEventToUIProcessViaNamedPipe() int SendLangbarRightClickEventToUIProcessViaNamedPipe(const RECT *prcArea) { + namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::LangbarRightClick; /* 利用其他的字段,把图标的坐标传递过去 */ namedpipeData.point[0] = prcArea->left; @@ -813,22 +1234,147 @@ int SendIMEActivationEventToUIProcessViaNamedPipe() return 0; } -int SendClientActivatedEventToServerViaNamedPipe() +int SendClientActivatedEventToServerViaNamedPipe(uint64_t focusToken) { + if (focusToken == 0) + { + return -1; + } namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::ClientActivated; - SendToNamedpipe(); - - return 0; + namedpipeData.request_id = focusToken; + return SendToNamedpipe() ? 0 : -1; } int SendClientDeactivatedEventToServerViaNamedPipe() { namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::ClientDeactivated; - SendToNamedpipe(); + return SendToNamedpipe() ? 0 : -1; +} - return 0; +int SendClientSuspendedEventToServerViaNamedPipe() +{ + namedpipeData = {}; + namedpipeData.event_type = FanyImePipeEventType::ClientSuspended; + return SendToNamedpipe() ? 0 : -1; +} + +bool FlushNamedpipeFocusSessionReset() +{ + if (!boundFocusResetPending || !*boundFocusResetPending) + { + return true; + } + if (SendClientSuspendedEventToServerViaNamedPipe() != 0) + { + return false; + } + *boundFocusResetPending = false; + return true; +} + +bool FlushNamedpipeImeDeactivation() +{ + if (SendClientDeactivatedEventToServerViaNamedPipe() != 0) + { + return false; + } + if (boundFocusResetPending) + { + *boundFocusResetPending = false; + } + return true; +} + +bool EnsureNamedpipeFocusSessionActivated() +{ + if (IsLocalSessionResetPending()) + { + return false; + } + // Connect reverse channels before Main, then establish lifecycle ordering. + if (!ConnectToAllNamedpipe()) + { + return false; + } + if (IsLocalSessionResetPending()) + { + return false; + } + + // A lifecycle write can itself discover a stale Main handle and mark the + // session dirty. Iterate a small bounded number of times so the newly + // generated token is the one ultimately acknowledged by the worker pipe. + for (int attempt = 0; attempt < 3; ++attempt) + { + if (IsLocalSessionResetPending()) + { + return false; + } + if (!FlushNamedpipeFocusSessionReset()) + { + return false; + } + if (IsLocalSessionResetPending()) + { + return false; + } + if (!boundActivationRequired || !*boundActivationRequired) + { + if (IsExpectedWorkerFocusAcknowledged()) + { + return true; + } + break; + } + + const uint64_t token = boundExpectedWorkerFocusToken + ? boundExpectedWorkerFocusToken->load(std::memory_order_acquire) + : 0; + if (SendClientActivatedEventToServerViaNamedPipe(token) != 0) + { + return false; + } + if (IsLocalSessionResetPending()) + { + return false; + } + + const bool tokenStillCurrent = !boundExpectedWorkerFocusToken || + boundExpectedWorkerFocusToken->load(std::memory_order_acquire) == token; + if (tokenStillCurrent && boundActivationRequired) + { + *boundActivationRequired = false; + } + if ((!boundFocusResetPending || !*boundFocusResetPending) && + (!boundActivationRequired || !*boundActivationRequired)) + { + break; + } + } + + // ClientActivated is not complete until the worker channel echoes the + // exact focus token. This closes the first-key window and rejects stale + // markers that were buffered before OnKill. + if (boundWorkerCommitReady) + { + const ULONGLONG deadline = GetTickCount64() + 150; + while (GetTickCount64() < deadline) + { + if (IsLocalSessionResetPending()) + { + return false; + } + if (IsExpectedWorkerFocusAcknowledged()) + { + return true; + } + Sleep(1); + } + MarkNamedpipeSessionDirty(); + } + return !boundWorkerCommitReady; } /** @@ -841,33 +1387,22 @@ int SendClientDeactivatedEventToServerViaNamedPipe() */ int SendIMEStatusEventToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen) { - std::wstring status = L"ftbStatus"; - if (kbdIsOpen) + return SendIMEStatusSnapshotToUIProcessViaNamedPipe(kbdIsOpen, fullwidthIsOpen, puncIsOpen); +} + +int SendIMEStatusSnapshotToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen) +{ + if (!Global::g_connected) { - status += L"1"; + return -1; } - else - { - status += L"0"; - } - if (fullwidthIsOpen) - { - status += L"1"; - } - else - { - status += L"0"; - } - if (puncIsOpen) - { - status += L"1"; - } - else - { - status += L"0"; - } - SendToAuxNamedpipe(status); - return 0; + + namedpipeData = {}; + namedpipeData.event_type = FanyImePipeEventType::StatusSnapshot; + namedpipeData.keycode = kbdIsOpen ? 1u : 0u; + namedpipeData.modifiers_down = fullwidthIsOpen ? 1u : 0u; + namedpipeData.pinyin_length = puncIsOpen ? 1 : 0; + return SendToNamedpipe() ? 0 : -1; } int SendIMEDeactivationEventToUIProcessViaNamedPipe() @@ -879,6 +1414,7 @@ int SendIMEDeactivationEventToUIProcessViaNamedPipe() int SendIMESwitchEventToUIProcessViaNamedPipe(UINT uImeStatus) { + namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::IMESwitch; /* 利用其他的字段,把 IME 的中英状态传递过去 */ namedpipeData.keycode = uImeStatus; @@ -889,6 +1425,7 @@ int SendIMESwitchEventToUIProcessViaNamedPipe(UINT uImeStatus) int SendPuncSwitchEventToUIProcessViaNamedPipe(BOOL isPunc) { + namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::PuncSwitch; /* 利用其他的字段,把标点符号的中英状态传递过去 */ namedpipeData.keycode = isPunc; @@ -899,6 +1436,7 @@ int SendPuncSwitchEventToUIProcessViaNamedPipe(BOOL isPunc) int SendDoubleSingleByteSwitchEventToUIProcessViaNamedPipe(BOOL isDoubleSingleByte) { + namedpipeData = {}; namedpipeData.event_type = FanyImePipeEventType::DoubleSingleByteSwitch; /* 利用其他的字段,把全角/半角的状态传递过去 */ namedpipeData.keycode = isDoubleSingleByte; diff --git a/src/IPC/Ipc.h b/src/IPC/Ipc.h index 87263f25..34e3ce21 100644 --- a/src/IPC/Ipc.h +++ b/src/IPC/Ipc.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include #include @@ -12,6 +13,8 @@ inline const wchar_t *FANY_IME_NAMED_PIPE = L"\\\\.\\pipe\\FanyImeNamedPipe"; inline const wchar_t *FANY_IME_TO_TSF_NAMED_PIPE = L"\\\\.\\pipe\\FanyImeToTsfNamedPipe"; inline const wchar_t *FANY_IME_TO_TSF_WORKER_THREAD_NAMED_PIPE = L"\\\\.\\pipe\\FanyImeToTsfWorkerThreadNamedPipe"; inline const wchar_t *FANY_IME_AUX_NAMED_PIPE = L"\\\\.\\pipe\\FanyImeAuxNamedPipe"; +inline constexpr uint64_t FANY_IME_UNSOLICITED_REQUEST_ID = 0; +inline constexpr uint64_t FANY_IME_NO_REQUEST_ID = UINT64_MAX; inline const std::vector FANY_IME_EVENT_ARRAY = { L"FanyImeKeyEvent", // Event sent to UI process to notify time to update UI by new pinyin_string @@ -56,6 +59,7 @@ struct FanyImeNamedpipeData { UINT event_type; uint64_t client_id = 0; + uint64_t request_id = 0; UINT keycode; WCHAR wch; UINT modifiers_down = 0; @@ -73,7 +77,9 @@ constexpr UINT MoveCandidateWnd = 3; constexpr UINT LangbarRightClick = 4; constexpr UINT ClientHello = 10; constexpr UINT ClientActivated = 11; -constexpr UINT ClientDeactivated = 12; +constexpr UINT ClientDeactivated = 12; // terminal TIP switch; toolbar hidden +constexpr UINT StatusSnapshot = 13; +constexpr UINT ClientSuspended = 14; // temporary focus route reset; toolbar kept constexpr UINT IMESwitch = 7; constexpr UINT PuncSwitch = 8; constexpr UINT DoubleSingleByteSwitch = 9; @@ -102,6 +108,7 @@ struct FanyImePipeHello struct FanyImeNamedpipeDataToTsf { UINT msg_type; + uint64_t request_id = 0; wchar_t candidate_string[200]; // 200 chars at most }; @@ -122,13 +129,53 @@ struct FanyImeNamedpipeDataToTsfWorkerThread wchar_t data[200]; }; +static_assert(sizeof(WCHAR) == 2, "The IPC ABI requires 16-bit WCHAR."); +static_assert(offsetof(FanyImeNamedpipeData, client_id) == 8); +static_assert(offsetof(FanyImeNamedpipeData, request_id) == 16); +static_assert(offsetof(FanyImeNamedpipeData, keycode) == 24); +static_assert(offsetof(FanyImeNamedpipeData, pinyin_string) == 48); +static_assert(sizeof(FanyImeNamedpipeData) == 304); +static_assert(offsetof(FanyImeNamedpipeDataToTsf, request_id) == 8); +static_assert(offsetof(FanyImeNamedpipeDataToTsf, candidate_string) == 16); +static_assert(sizeof(FanyImeNamedpipeDataToTsf) == 416); +static_assert(sizeof(FanyImePipeHello) == 16); +static_assert(sizeof(FanyImeNamedpipeDataToTsfWorkerThread) == 404); + +enum class KeyEventSendResult +{ + Sent, + DefinitelyNotSent, + DeliveryAmbiguous, +}; + int InitIpc(); int InitNamedpipe(); int ConnectToAllNamedpipe(); int ConnectToTsfNamedpipe(); int CloseIpc(); int CloseNamedpipe(); +void ResetNamedpipeReplyState(); HANDLE GetToTsfWorkerThreadNamedpipe(); +void BindNamedpipeFocusState(_In_ const void *owner, + _In_opt_ bool *focusResetPending, _In_opt_ bool *activationRequired, + _In_opt_ std::atomic *expectedWorkerFocusToken, + _In_opt_ std::atomic *localSessionResetPending, + _In_opt_ std::atomic *localSessionResetToken, + _In_opt_ std::atomic *workerCommitReady, + _In_opt_ std::atomic *acknowledgedWorkerFocusToken, + _In_opt_ std::atomic *workerPipeHandle, + _In_opt_ std::atomic *workerPipeGeneration); +void UnbindNamedpipeFocusState(_In_ const void *owner); +bool IsNamedpipeFocusStateOwner(_In_ const void *owner); +UINT BeginNamedpipeLocalSessionReset(); +void InvalidateNamedpipeWorkerGeneration(); +void MarkNamedpipeFocusLost(); +void RequireNamedpipeFocusActivation(); +void MarkNamedpipeSessionDirty(); +bool MarkNamedpipeSessionDirtyForOwner(_In_ const void *owner); +bool EnsureNamedpipeFocusSessionActivated(); +bool FlushNamedpipeFocusSessionReset(); +bool FlushNamedpipeImeDeactivation(); // // For shared memory @@ -142,15 +189,17 @@ int WriteDataToSharedMemory( // const std::wstring &pinyin_string, // UINT write_flag // ); -int SendKeyEventToUIProcess(); +KeyEventSendResult SendKeyEventToUIProcess(_Out_opt_ uint64_t *requestId = nullptr); int SendHideCandidateWndEventToUIProcess(); int SendShowCandidateWndEventToUIProcess(); int SendMoveCandidateWndEventToUIProcess(); int SendLangbarRightClickEventToUIProcess(const RECT *prcArea); int SendIMEActivationEventToUIProcessViaNamedPipe(); int SendIMEDeactivationEventToUIProcessViaNamedPipe(); -int SendClientActivatedEventToServerViaNamedPipe(); +int SendClientActivatedEventToServerViaNamedPipe(uint64_t focusToken); int SendClientDeactivatedEventToServerViaNamedPipe(); +int SendClientSuspendedEventToServerViaNamedPipe(); +int SendIMEStatusSnapshotToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen); int SendIMEStatusEventToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen); int SendIMESwitchEventToUIProcessViaNamedPipe(UINT uImeStatus); int SendPuncSwitchEventToUIProcessViaNamedPipe(BOOL isPunc); @@ -170,14 +219,14 @@ int WriteDataToNamedPipe( // const std::wstring &pinyin_string, // UINT write_flag // ); -int SendKeyEventToUIProcessViaNamedPipe(); +KeyEventSendResult SendKeyEventToUIProcessViaNamedPipe(_Out_opt_ uint64_t *requestId = nullptr); int SendHideCandidateWndEventToUIProcessViaNamedPipe(); int SendShowCandidateWndEventToUIProcessViaNamedPipe(); int SendMoveCandidateWndEventToUIProcessViaNamedPipe(); int SendLangbarRightClickEventToUIProcessViaNamedPipe(const RECT *prcArea); void ClearNamedpipeDataIfExists(bool force = false); -struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout(); -struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe(); +struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout(uint64_t expectedRequestId); +struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe(uint64_t expectedRequestId); // // Modifiers: @@ -188,17 +237,17 @@ struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe(); // namespace Global { -inline UINT Keycode = 0; -inline WCHAR wch = L'\0'; -inline UINT ModifiersDown = 0; -inline int Point[2] = {100, 100}; -inline int PinyinLength = 0; -inline std::wstring PinyinString = L""; +inline thread_local UINT Keycode = 0; +inline thread_local WCHAR wch = L'\0'; +inline thread_local UINT ModifiersDown = 0; +inline thread_local int Point[2] = {100, 100}; +inline thread_local int PinyinLength = 0; +inline thread_local std::wstring PinyinString = L""; -inline int firefox_like_cnt = 0; // Apps like firefox, e.g. firefox, zen... -inline std::wstring current_process_name = L""; +inline thread_local int firefox_like_cnt = 0; // Apps like firefox, e.g. firefox, zen... +inline thread_local std::wstring current_process_name = L""; -inline wchar_t app_name[512] = {0}; +inline thread_local wchar_t app_name[512] = {0}; namespace DataFromServerMsgType { @@ -211,6 +260,12 @@ constexpr UINT MoveSelectionPrevious = 5; constexpr UINT MoveSelectionNext = 6; constexpr UINT MovePagePrevious = 7; constexpr UINT MovePageNext = 8; +// Reverse-pipe registration acknowledgement. This frame is consumed during +// the ToTsf pipe handshake and is never exposed as a key reply. +constexpr UINT PipeReady = 9; +// Local-only result. It is never sent over the pipe and must never be +// interpreted as candidate text to commit. +constexpr UINT TransportUnavailable = static_cast(-1); } // namespace DataFromServerMsgType namespace DataToTsfWorkerThreadMsgType @@ -223,9 +278,13 @@ constexpr UINT SwitchToFullwidth = 4; constexpr UINT SwitchToHalfwidth = 5; constexpr UINT CommitCurCandidate = 6; constexpr UINT PagingCommaPeriodChanged = 7; +constexpr UINT FocusSessionReady = 8; +// Worker-endpoint registration acknowledgement. It is consumed before the +// handle is published to IpcWorkerThread and before Main can be opened. +constexpr UINT PipeReady = 9; } // namespace DataToTsfWorkerThreadMsgType inline std::atomic_bool PagingCommaPeriodEnabled{false}; -inline bool g_connected = false; +inline thread_local bool g_connected = false; } // namespace Global diff --git a/src/Key/KeyEventSink.cpp b/src/Key/KeyEventSink.cpp index b6e9ce8f..475c845e 100644 --- a/src/Key/KeyEventSink.cpp +++ b/src/Key/KeyEventSink.cpp @@ -8,6 +8,7 @@ #include "MetasequoiaIMEBaseStructure.h" #include "fmt/xchar.h" #include +#include #include #include "Ipc.h" #include "FanyUtils.h" @@ -18,6 +19,155 @@ #define THIRDPARTY_NEXTPAGE static_cast(0xF003) #define THIRDPARTY_PREVPAGE static_cast(0xF004) +namespace +{ +// A recovery checkpoint may need one INPUT for every raw pinyin character and +// one MOVE_LEFT for every character to the right of the caret. Keep enough +// additional room for a short burst that arrives while the replacement IPC +// epoch is becoming ready. +constexpr size_t MAX_DEFERRED_KEY_DOWN_COUNT = + static_cast(MAX_PINYIN_LENGTH) * 2 + 32; + +struct DeferredShadowState +{ + bool imeOpen = false; + bool punctuationOpen = false; + bool doubleSingleByteOpen = false; + size_t inputLength = 0; + bool candidateActive = false; +}; + +bool IsBareModifierKey(UINT code) +{ + switch (code) + { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + return true; + default: + return false; + } +} + +void ApplyDeferredKeyState(DeferredShadowState &shadow, + const _KEYSTROKE_STATE &keyState) +{ + switch (keyState.Function) + { + case FUNCTION_INPUT: + if (shadow.inputLength < MAX_PINYIN_LENGTH) + { + ++shadow.inputLength; + } + shadow.candidateActive = false; + break; + case FUNCTION_FINALIZE_TEXTSTORE_AND_INPUT: + case FUNCTION_FINALIZE_CANDIDATELIST_AND_INPUT: + shadow.inputLength = 1; + shadow.candidateActive = false; + break; + case FUNCTION_BACKSPACE: + if (shadow.inputLength > 0) + { + --shadow.inputLength; + } + if (shadow.inputLength == 0) + { + shadow.candidateActive = false; + } + break; + case FUNCTION_CONVERT_WILDCARD: + shadow.candidateActive = shadow.inputLength > 0; + break; + case FUNCTION_CONVERT: + // This TIP routes Space+Convert to WM_AsyncFinalizeCandidate, which + // commits and ends the composition rather than merely opening a list. + shadow.inputLength = 0; + shadow.candidateActive = false; + break; + case FUNCTION_CANCEL: + shadow.inputLength = 0; + shadow.candidateActive = false; + break; + case FUNCTION_FINALIZE_TEXTSTORE: + case FUNCTION_FINALIZE_CANDIDATELIST: + case FUNCTION_FINALIZE_CANDIDATELISTForVKReturn: + case FUNCTION_SELECT_BY_NUMBER: + case FUNCTION_TOGGLE_IME_MODE: + case FUNCTION_PUNCTUATION: + case FUNCTION_DOUBLE_SINGLE_BYTE: + shadow.inputLength = 0; + shadow.candidateActive = false; + break; + default: + break; + } +} + +bool IsRecoverableDeferredPrefix(const _KEYSTROKE_STATE &keyState) +{ + switch (keyState.Function) + { + case FUNCTION_INPUT: + case FUNCTION_BACKSPACE: + case FUNCTION_MOVE_LEFT: + case FUNCTION_MOVE_RIGHT: + case FUNCTION_MOVE_UP: + case FUNCTION_MOVE_DOWN: + case FUNCTION_MOVE_PAGE_UP: + case FUNCTION_MOVE_PAGE_DOWN: + case FUNCTION_MOVE_PAGE_TOP: + case FUNCTION_MOVE_PAGE_BOTTOM: + case FUNCTION_CONVERT_WILDCARD: + case FUNCTION_SERVER_CANDIDATE_KEY: + return true; + default: + return false; + } +} + +bool StartsNewDeferredPrefix(const _KEYSTROKE_STATE &keyState) +{ + return keyState.Function == FUNCTION_FINALIZE_TEXTSTORE_AND_INPUT || + keyState.Function == FUNCTION_FINALIZE_CANDIDATELIST_AND_INPUT; +} + +UINT CaptureIpcModifiers() +{ + UINT modifiers = 0; + if ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) + modifiers |= 0b00000001; + if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) + modifiers |= 0b00000010; + if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) + modifiers |= 0b00000100; + return modifiers; +} + +void PostOwnerMessageWithSyncFallback(HWND window, UINT message, + WPARAM wParam = 0, LPARAM lParam = 0) +{ + if (!window || !IsWindow(window)) + { + return; + } + if (!PostMessage(window, message, wParam, lParam) && + GetWindowThreadProcessId(window, nullptr) == GetCurrentThreadId()) + { + SendMessage(window, message, wParam, lParam); + } +} +} // namespace + // Because the code mostly works with VKeys, here map a WCHAR back to a VKKey for certain // vkeys that the IME handles specially __inline UINT VKeyFromVKPacketAndWchar(UINT vk, WCHAR wch) @@ -64,7 +214,9 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // UINT codeIn, // _Out_ UINT *pCodeOut, // _Out_writes_(1) WCHAR *pwch, // - _Out_opt_ _KEYSTROKE_STATE *pKeyState // + _Out_opt_ _KEYSTROKE_STATE *pKeyState, // + _In_opt_ const WCHAR *translatedWch, // + bool freshCompositionState // ) { pContext; @@ -104,7 +256,7 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // // Map virtual key to character code // BOOL isTouchKeyboardSpecialKeys = FALSE; - WCHAR wch = ConvertVKey(codeIn); + WCHAR wch = translatedWch ? *translatedWch : ConvertVKey(codeIn); *pCodeOut = VKeyFromVKPacketAndWchar(codeIn, wch); if ((wch == THIRDPARTY_NEXTPAGE) || (wch == THIRDPARTY_PREVPAGE)) { @@ -135,12 +287,15 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // if (isOpen) // Chinese mode { - const bool isComposing = _IsComposing() ? true : false; + const bool isComposing = freshCompositionState ? false : _IsComposing() != FALSE; + const CANDIDATE_MODE candidateMode = + freshCompositionState ? CANDIDATE_NONE : _candidateMode; const bool isCapsLockOn = (GetKeyState(VK_CAPITAL) & 0x0001) != 0; const bool isUppercaseAlphabet = (wch >= L'A' && wch <= L'Z') && (*pCodeOut >= L'A' && *pCodeOut <= L'Z'); const bool isInputInProgress = - isComposing || (_candidateMode != CANDIDATE_NONE) || - (pCompositionProcessorEngine && pCompositionProcessorEngine->GetVirtualKeyLength() > 0); + !freshCompositionState && + (isComposing || (candidateMode != CANDIDATE_NONE) || + (pCompositionProcessorEngine && pCompositionProcessorEngine->GetVirtualKeyLength() > 0)); // CapsLock ON + uppercase(没有按 Shift) alphabet: // - start of input: don't eat @@ -155,14 +310,12 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // // // eat only keys that CKeyHandlerEditSession can handles. // - auto ret = pCompositionProcessorEngine->IsVirtualKeyNeed( // - *pCodeOut, // - pwch, // - isComposing, // - _candidateMode, // - _isCandidateWithWildcard, // - pKeyState // - ); + const BOOL ret = freshCompositionState + ? pCompositionProcessorEngine->IsVirtualKeyNeedForFreshComposition( + *pCodeOut, pwch, pKeyState) + : pCompositionProcessorEngine->IsVirtualKeyNeed( + *pCodeOut, pwch, isComposing, candidateMode, + _isCandidateWithWildcard, pKeyState); if (ret) { return TRUE; @@ -174,7 +327,9 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // // if (pCompositionProcessorEngine->IsPunctuation(wch)) { - if ((_candidateMode == CANDIDATE_NONE || _candidateMode == CANDIDATE_INCREMENTAL) && isPunctuation) + const CANDIDATE_MODE candidateMode = + freshCompositionState ? CANDIDATE_NONE : _candidateMode; + if ((candidateMode == CANDIDATE_NONE || candidateMode == CANDIDATE_INCREMENTAL) && isPunctuation) { if (pKeyState) { @@ -190,7 +345,7 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // // if (isDoubleSingleByte && pCompositionProcessorEngine->IsDoubleSingleByte(wch)) { - if (_candidateMode == CANDIDATE_NONE) + if ((freshCompositionState ? CANDIDATE_NONE : _candidateMode) == CANDIDATE_NONE) { if (pKeyState) { @@ -318,6 +473,403 @@ STDAPI CMetasequoiaIME::OnSetFocus(BOOL fForeground) return S_OK; } +bool CMetasequoiaIME::_HasDeferredKeyBarrier() const +{ + if (_localSessionResetPending.load(std::memory_order_acquire) || + _focusResetPending || _activationRequired || + _deferredKeyProjectionValid || !_deferredKeyDowns.empty() || + _hasDeferredKeyInFlight) + { + return true; + } + if (Global::g_connected) + { + const uint64_t expectedToken = + _expectedWorkerFocusToken.load(std::memory_order_acquire); + return expectedToken == 0 || + _acknowledgedWorkerFocusToken.load(std::memory_order_acquire) != + expectedToken || + !_workerCommitReady.load(std::memory_order_acquire); + } + return false; +} + +bool CMetasequoiaIME::_DeferredKeyQueueHasCapacity() const +{ + size_t deferredCount = + _deferredKeyDowns.size() + _deferredAppliedPrefix.size(); + if (_hasDeferredKeyInFlight) + { + ++deferredCount; + } + return deferredCount < MAX_DEFERRED_KEY_DOWN_COUNT; +} + +void CMetasequoiaIME::_EnsureDeferredKeyProjection() +{ + if (_deferredKeyProjectionValid) + { + return; + } + _deferredKeyProjectionValid = true; + _deferredProjectedImeOpen = _pCompositionProcessorEngine && _pThreadMgr && + _pCompositionProcessorEngine->GetIMEMode( + _pThreadMgr, _tfClientId) != FALSE; + _deferredProjectedPunctuationOpen = + _pCompositionProcessorEngine && _pThreadMgr && + _pCompositionProcessorEngine->GetPunctuationMode( + _pThreadMgr, _tfClientId) != FALSE; + _deferredProjectedDoubleSingleByteOpen = + _pCompositionProcessorEngine && _pThreadMgr && + _pCompositionProcessorEngine->GetDoubleSingleByteMode( + _pThreadMgr, _tfClientId) != FALSE; + // In the healthy path every IME-owned key enters the FIFO as well, so its + // first projection starts from the composition that is already visible. + // A transport-recovery checkpoint arms this projection before the local + // reset cancels that composition. + _deferredProjectedInputLength = + _pCompositionProcessorEngine + ? min(static_cast(MAX_PINYIN_LENGTH), + static_cast( + _pCompositionProcessorEngine->GetVirtualKeyLength())) + : 0; + // Incremental candidates are still the ordinary composing path: another + // letter extends the same raw input. Only an explicit/original candidate + // list makes the next input a finalize-and-start-new boundary. + _deferredProjectedCandidateActive = _candidateMode == CANDIDATE_ORIGINAL; +} + +void CMetasequoiaIME::_ApplyDeferredKeyProjection( + const _KEYSTROKE_STATE &keyState) +{ + _EnsureDeferredKeyProjection(); + DeferredShadowState shadow; + shadow.imeOpen = _deferredProjectedImeOpen; + shadow.punctuationOpen = _deferredProjectedPunctuationOpen; + shadow.doubleSingleByteOpen = _deferredProjectedDoubleSingleByteOpen; + shadow.inputLength = _deferredProjectedInputLength; + shadow.candidateActive = _deferredProjectedCandidateActive; + ApplyDeferredKeyState(shadow, keyState); + _deferredProjectedInputLength = shadow.inputLength; + _deferredProjectedCandidateActive = shadow.candidateActive; +} + +void CMetasequoiaIME::_ApplyDeferredPreservedKeyProjection( + REFGUID preservedKey) +{ + _EnsureDeferredKeyProjection(); + if (_pCompositionProcessorEngine == nullptr) + { + return; + } + switch (_pCompositionProcessorEngine->GetPreservedKeyAction(preservedKey)) + { + case CCompositionProcessorEngine::PreservedKeyAction::ToggleImeMode: + _deferredProjectedImeOpen = !_deferredProjectedImeOpen; + _deferredProjectedPunctuationOpen = _deferredProjectedImeOpen; + _deferredProjectedInputLength = 0; + _deferredProjectedCandidateActive = false; + break; + case CCompositionProcessorEngine::PreservedKeyAction::ToggleDoubleSingleByteMode: + _deferredProjectedDoubleSingleByteOpen = + !_deferredProjectedDoubleSingleByteOpen; + break; + case CCompositionProcessorEngine::PreservedKeyAction::TogglePunctuationMode: + _deferredProjectedPunctuationOpen = + !_deferredProjectedPunctuationOpen; + break; + default: + break; + } +} + +bool CMetasequoiaIME::_RefreshDeferredRecoveryPrefix( + _In_ ITfContext *pContext) +{ + while (!_deferredAppliedPrefix.empty()) + { + ITfContext *prefixContext = _deferredAppliedPrefix.front().context; + _deferredAppliedPrefix.pop_front(); + if (prefixContext) + { + prefixContext->Release(); + } + } + + if (pContext == nullptr || _pCompositionProcessorEngine == nullptr) + { + return false; + } + + CStringRange &raw = _pCompositionProcessorEngine->GetKeystrokeBuffer(); + const size_t rawLength = static_cast(raw.GetLength()); + const size_t caret = min( + rawLength, + static_cast(_pCompositionProcessorEngine->GetCaretPosition())); + const size_t moveLeftCount = rawLength - caret; + if (rawLength + moveLeftCount > MAX_DEFERRED_KEY_DOWN_COUNT) + { + return false; + } + + for (size_t index = 0; index < rawLength; ++index) + { + const WCHAR wch = raw.Get()[index]; + const SHORT virtualKey = VkKeyScanW(wch); + DeferredKeyDown recoveryKey; + recoveryKey.kind = DeferredKeyDown::Kind::KeyDown; + recoveryKey.context = pContext; + recoveryKey.wParam = virtualKey == -1 + ? static_cast(VK_PACKET) + : static_cast(LOBYTE(virtualKey)); + recoveryKey.translatedWch = wch; + recoveryKey.modifiersDown = + virtualKey != -1 && (HIBYTE(virtualKey) & 1) != 0 ? 1u : 0u; + recoveryKey.keyState.Category = CATEGORY_COMPOSING; + recoveryKey.keyState.Function = FUNCTION_INPUT; + recoveryKey.focusGeneration = _deferredKeyFocusGeneration; + pContext->AddRef(); + _deferredAppliedPrefix.push_back(recoveryKey); + } + for (size_t index = 0; index < moveLeftCount; ++index) + { + DeferredKeyDown recoveryKey; + recoveryKey.kind = DeferredKeyDown::Kind::KeyDown; + recoveryKey.context = pContext; + recoveryKey.wParam = VK_LEFT; + recoveryKey.keyState.Category = CATEGORY_COMPOSING; + recoveryKey.keyState.Function = FUNCTION_MOVE_LEFT; + recoveryKey.focusGeneration = _deferredKeyFocusGeneration; + pContext->AddRef(); + _deferredAppliedPrefix.push_back(recoveryKey); + } + return true; +} + +void CMetasequoiaIME::_ArmDeferredRecoveryForTransport( + _In_opt_ ITfContext *pContext) +{ + // An in-flight key owns its exact retry token. Its failure path moves the + // checkpoint in front of that key; moving it here as well would duplicate + // the prefix. + if (_hasDeferredKeyInFlight) + { + return; + } + + ITfContext *recoveryContext = pContext ? pContext : _pContext; + const bool activeDeferredState = + !_deferredKeyDowns.empty() || _deferredKeyProjectionValid; + if (!activeDeferredState) + { + // The dormant checkpoint may have been superseded by a non-eaten + // application key that finalized/cancelled the composition. Refresh + // from the engine at the transport boundary instead of trusting stale + // history. + if (recoveryContext) + { + (void)_RefreshDeferredRecoveryPrefix(recoveryContext); + } + } + else if (_deferredAppliedPrefix.empty()) + { + // A retry has already materialized the checkpoint in the FIFO. The + // real engine can still contain the same raw text until the queued + // reset edit session runs, so snapshotting it again would duplicate + // the whole prefix. + return; + } + if (_deferredAppliedPrefix.empty()) + { + return; + } + + // Capture the current real state before the local reset cancels it. The + // queued checkpoint then represents that same future state while reset is + // pending, so later key classification remains ordered. + _EnsureDeferredKeyProjection(); + while (!_deferredAppliedPrefix.empty()) + { + _deferredKeyDowns.push_front(_deferredAppliedPrefix.back()); + _deferredAppliedPrefix.pop_back(); + } + _ScheduleDeferredKeyDownDrain(); +} + +bool CMetasequoiaIME::_ClassifyDeferredKeyDown( + _In_ ITfContext *pContext, WPARAM wParam, + _In_opt_ const WCHAR *translatedWch, + _In_opt_ const UINT *modifiersDown, + _Out_ WCHAR *classifiedWch, _Out_ UINT *classifiedCode, + _Out_ _KEYSTROKE_STATE *keyState) +{ + if (pContext == nullptr || classifiedWch == nullptr || + classifiedCode == nullptr || keyState == nullptr || + _pCompositionProcessorEngine == nullptr || _pThreadMgr == nullptr) + { + return false; + } + + *classifiedWch = translatedWch ? *translatedWch : ConvertVKey(static_cast(wParam)); + *classifiedCode = VKeyFromVKPacketAndWchar(static_cast(wParam), *classifiedWch); + keyState->Category = CATEGORY_NONE; + keyState->Function = FUNCTION_NONE; + + const UINT capturedModifiers = modifiersDown ? *modifiersDown : CaptureIpcModifiers(); + // Ctrl/Alt/Windows combinations belong to the application. In particular, + // never turn a recovery FIFO into a shortcut sink. + if ((capturedModifiers & 0b00000110) != 0 || + (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0 || + (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0 || + IsBareModifierKey(*classifiedCode) || _IsKeyboardDisabled()) + { + return false; + } + + DeferredShadowState shadow; + if (_deferredKeyProjectionValid) + { + shadow.imeOpen = _deferredProjectedImeOpen; + shadow.punctuationOpen = _deferredProjectedPunctuationOpen; + shadow.doubleSingleByteOpen = + _deferredProjectedDoubleSingleByteOpen; + shadow.inputLength = _deferredProjectedInputLength; + shadow.candidateActive = _deferredProjectedCandidateActive; + } + else + { + shadow.imeOpen = + _pCompositionProcessorEngine->GetIMEMode(_pThreadMgr, _tfClientId) != FALSE; + shadow.punctuationOpen = + _pCompositionProcessorEngine->GetPunctuationMode(_pThreadMgr, _tfClientId) != FALSE; + shadow.doubleSingleByteOpen = + _pCompositionProcessorEngine->GetDoubleSingleByteMode( + _pThreadMgr, _tfClientId) != FALSE; + shadow.inputLength = min( + static_cast(MAX_PINYIN_LENGTH), + static_cast( + _pCompositionProcessorEngine->GetVirtualKeyLength())); + shadow.candidateActive = _candidateMode == CANDIDATE_ORIGINAL; + } + + const auto setKeyState = [keyState](KEYSTROKE_CATEGORY category, + KEYSTROKE_FUNCTION function) { + keyState->Category = category; + keyState->Function = function; + return true; + }; + + _KEYSTROKE_STATE inputState = {}; + WCHAR inputWch = *classifiedWch; + bool isInputKey = false; + if (shadow.imeOpen) + { + isInputKey = + _pCompositionProcessorEngine->IsVirtualKeyNeedForFreshComposition( + *classifiedCode, &inputWch, &inputState) != FALSE; + if (!isInputKey && shadow.inputLength > 0 && + ((*classifiedWch == L'\'') || + (_pCompositionProcessorEngine->IsWildcard() && + _pCompositionProcessorEngine->IsWildcardChar(*classifiedWch)))) + { + isInputKey = true; + } + if (shadow.inputLength == 0 && + (GetKeyState(VK_CAPITAL) & 0x0001) != 0 && + *classifiedWch >= L'A' && *classifiedWch <= L'Z' && + *classifiedCode >= L'A' && *classifiedCode <= L'Z') + { + // Match the normal fresh-composition path: CapsLock uppercase at + // the beginning belongs to the application. + isInputKey = false; + } + } + + if (shadow.candidateActive && isInputKey) + { + return setKeyState(CATEGORY_CANDIDATE, + FUNCTION_FINALIZE_CANDIDATELIST_AND_INPUT); + } + if (!shadow.candidateActive && isInputKey) + { + return setKeyState(CATEGORY_COMPOSING, FUNCTION_INPUT); + } + + if (shadow.imeOpen && shadow.inputLength > 0) + { + const bool candidateKey = shadow.candidateActive; + switch (*classifiedCode) + { + case VK_BACK: + return candidateKey + ? setKeyState(CATEGORY_CANDIDATE, FUNCTION_CANCEL) + : setKeyState(CATEGORY_COMPOSING, FUNCTION_BACKSPACE); + case VK_SPACE: + return setKeyState(CATEGORY_CANDIDATE, FUNCTION_CONVERT); + case VK_RETURN: + return candidateKey + ? setKeyState(CATEGORY_CANDIDATE, + FUNCTION_FINALIZE_CANDIDATELIST) + : setKeyState(CATEGORY_CANDIDATE, + FUNCTION_FINALIZE_CANDIDATELISTForVKReturn); + case VK_ESCAPE: + return setKeyState(CATEGORY_CANDIDATE, FUNCTION_CANCEL); + case VK_LEFT: + return setKeyState(CATEGORY_COMPOSING, FUNCTION_MOVE_LEFT); + case VK_RIGHT: + return setKeyState(CATEGORY_COMPOSING, FUNCTION_MOVE_RIGHT); + case VK_HOME: + return setKeyState(CATEGORY_CANDIDATE, FUNCTION_MOVE_PAGE_TOP); + case VK_END: + return setKeyState(CATEGORY_CANDIDATE, FUNCTION_MOVE_PAGE_BOTTOM); + case VK_OEM_MINUS: + case VK_OEM_PLUS: + case VK_OEM_COMMA: + case VK_OEM_PERIOD: + case VK_TAB: + case VK_PRIOR: + case VK_NEXT: + case VK_UP: + case VK_DOWN: + return setKeyState(CATEGORY_CANDIDATE, + FUNCTION_SERVER_CANDIDATE_KEY); + default: + break; + } + + if (*classifiedCode >= L'1' && *classifiedCode <= L'9') + { + return setKeyState(CATEGORY_CANDIDATE, FUNCTION_SELECT_BY_NUMBER); + } + if (shadow.punctuationOpen && + _pCompositionProcessorEngine->IsPunctuation(*classifiedWch)) + { + return setKeyState(CATEGORY_COMPOSING, FUNCTION_PUNCTUATION); + } + return false; + } + + if (shadow.punctuationOpen && + _pCompositionProcessorEngine->IsPunctuation(*classifiedWch)) + { + return setKeyState(CATEGORY_COMPOSING, FUNCTION_PUNCTUATION); + } + if (shadow.doubleSingleByteOpen && + _pCompositionProcessorEngine->IsDoubleSingleByte(*classifiedWch)) + { + return setKeyState(CATEGORY_COMPOSING, FUNCTION_DOUBLE_SINGLE_BYTE); + } + if (!shadow.imeOpen && *classifiedWch != L'\0' && + std::iswprint(static_cast(*classifiedWch)) != 0) + { + // A queued Shift may make this future key English. It still belongs + // behind the FIFO prefix; CATEGORY_NONE/FUNCTION_NONE marks a direct + // application-text replay instead of misclassifying it as Chinese. + return true; + } + return false; +} + //+--------------------------------------------------------------------------- // // ITfKeyEventSink::OnTestKeyDown @@ -327,13 +879,36 @@ STDAPI CMetasequoiaIME::OnSetFocus(BOOL fForeground) STDAPI CMetasequoiaIME::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten) { + if (pContext == nullptr || pIsEaten == nullptr) + { + return E_INVALIDARG; + } PerfTimer onTestKeyDownTimer; Global::UpdateModifiers(wParam, lParam); + if (_HasDeferredKeyBarrier()) + { + if (!_DeferredKeyQueueHasCapacity()) + { + *pIsEaten = FALSE; + return S_OK; + } + _KEYSTROKE_STATE deferredState = {}; + WCHAR deferredWch = L'\0'; + UINT deferredCode = 0; + *pIsEaten = _ClassifyDeferredKeyDown( + pContext, wParam, nullptr, nullptr, &deferredWch, + &deferredCode, &deferredState) + ? TRUE + : FALSE; + return S_OK; + } + _KEYSTROKE_STATE KeystrokeState; WCHAR wch = '\0'; UINT code = 0; - *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, &KeystrokeState); + *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, + &KeystrokeState); if (KeystrokeState.Category == CATEGORY_INVOKE_COMPOSITION_EDIT_SESSION) { @@ -342,13 +917,374 @@ STDAPI CMetasequoiaIME::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPARA // KeystrokeState.Category = CATEGORY_COMPOSING; - _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState); + _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState, + FANY_IME_NO_REQUEST_ID); } return S_OK; } +bool CMetasequoiaIME::_QueueDeferredKeyDown(_In_ ITfContext *pContext, WPARAM wParam, + LPARAM lParam, WCHAR translatedWch, + UINT modifiersDown, + const _KEYSTROKE_STATE &keyState) +{ + if (pContext == nullptr || !_DeferredKeyQueueHasCapacity()) + { + return false; + } + + pContext->AddRef(); + DeferredKeyDown key; + key.kind = keyState.Category == CATEGORY_NONE && + keyState.Function == FUNCTION_NONE + ? DeferredKeyDown::Kind::ApplicationText + : DeferredKeyDown::Kind::KeyDown; + key.context = pContext; + key.wParam = wParam; + key.lParam = lParam; + key.translatedWch = translatedWch; + key.modifiersDown = modifiersDown; + key.keyState = keyState; + key.focusGeneration = _deferredKeyFocusGeneration; + _deferredKeyDowns.push_back(key); + if (key.kind == DeferredKeyDown::Kind::KeyDown) + { + _ApplyDeferredKeyProjection(keyState); + } + else + { + _EnsureDeferredKeyProjection(); + } + _ScheduleDeferredKeyDownDrain(); + return true; +} + +bool CMetasequoiaIME::_QueueDeferredPreservedKey(_In_ ITfContext *pContext, + REFGUID preservedKey) +{ + if (pContext == nullptr || !_DeferredKeyQueueHasCapacity()) + { + return false; + } + + pContext->AddRef(); + DeferredKeyDown key; + key.kind = DeferredKeyDown::Kind::PreservedKey; + key.context = pContext; + key.preservedKey = preservedKey; + key.focusGeneration = _deferredKeyFocusGeneration; + _deferredKeyDowns.push_back(key); + _ApplyDeferredPreservedKeyProjection(preservedKey); + _ScheduleDeferredKeyDownDrain(); + return true; +} + +void CMetasequoiaIME::_ClearDeferredKeyDowns() +{ + // A posted drain belongs to the current message window/generation. It + // may never run if Deactivate destroys that window, so never carry this + // latch into a later Activate on the same TIP instance. + _deferredKeyDrainPosted = false; + if (++_deferredKeyFocusGeneration == 0) + { + ++_deferredKeyFocusGeneration; + } + while (!_deferredKeyDowns.empty()) + { + ITfContext *context = _deferredKeyDowns.front().context; + _deferredKeyDowns.pop_front(); + if (context) + { + context->Release(); + } + } + while (!_deferredAppliedPrefix.empty()) + { + ITfContext *context = _deferredAppliedPrefix.front().context; + _deferredAppliedPrefix.pop_front(); + if (context) + { + context->Release(); + } + } + if (_hasDeferredKeyInFlight) + { + ITfContext *context = _deferredKeyInFlight.context; + _hasDeferredKeyInFlight = false; + _deferredKeyReplayToken = 0; + _deferredKeyInFlight = {}; + if (context) + { + context->Release(); + } + } + _deferredKeyProjectionValid = false; + _deferredProjectedImeOpen = false; + _deferredProjectedPunctuationOpen = false; + _deferredProjectedDoubleSingleByteOpen = false; + _deferredProjectedInputLength = 0; + _deferredProjectedCandidateActive = false; +} + +void CMetasequoiaIME::_CompleteDeferredKeyReplay(uint64_t replayToken) +{ + if (replayToken == 0 || !_hasDeferredKeyInFlight || + _deferredKeyReplayToken != replayToken) + { + return; + } + + ITfContext *context = _deferredKeyInFlight.context; + if (_deferredKeyDowns.empty()) + { + // The exact final edit session has completed, so the real composition + // has caught up with the future projection. Compact the applied + // event history into a bounded raw-text/caret checkpoint. This + // checkpoint is dormant (not a barrier) until a transport reset needs + // to rebuild the composition. + _deferredKeyProjectionValid = false; + _deferredProjectedInputLength = 0; + _deferredProjectedCandidateActive = false; + (void)_RefreshDeferredRecoveryPrefix(context); + _deferredKeyInFlight = {}; + _hasDeferredKeyInFlight = false; + _deferredKeyReplayToken = 0; + if (context) + { + context->Release(); + } + _ScheduleDeferredKeyDownDrain(); + return; + } + + bool contextTransferredToPrefix = false; + const auto clearAppliedPrefix = [this]() { + while (!_deferredAppliedPrefix.empty()) + { + ITfContext *prefixContext = + _deferredAppliedPrefix.front().context; + _deferredAppliedPrefix.pop_front(); + if (prefixContext) + { + prefixContext->Release(); + } + } + }; + if (_deferredKeyInFlight.kind == DeferredKeyDown::Kind::KeyDown) + { + if (StartsNewDeferredPrefix(_deferredKeyInFlight.keyState)) + { + clearAppliedPrefix(); + // The old text/candidate side of this combined operation has + // already committed successfully. A replacement Server epoch + // must rebuild only the new raw character; replaying the original + // finalize-and-input function would try to finalize state that no + // longer exists. + _deferredKeyInFlight.keyState.Category = CATEGORY_COMPOSING; + _deferredKeyInFlight.keyState.Function = FUNCTION_INPUT; + _deferredAppliedPrefix.push_back(_deferredKeyInFlight); + contextTransferredToPrefix = true; + } + else if (IsRecoverableDeferredPrefix(_deferredKeyInFlight.keyState)) + { + _deferredAppliedPrefix.push_back(_deferredKeyInFlight); + contextTransferredToPrefix = true; + } + else + { + clearAppliedPrefix(); + } + } + else if (_deferredKeyInFlight.kind == + DeferredKeyDown::Kind::PreservedKey && + _pCompositionProcessorEngine && + _pCompositionProcessorEngine->GetPreservedKeyAction( + _deferredKeyInFlight.preservedKey) == + CCompositionProcessorEngine::PreservedKeyAction::ToggleImeMode) + { + clearAppliedPrefix(); + } + + _deferredKeyInFlight = {}; + _hasDeferredKeyInFlight = false; + _deferredKeyReplayToken = 0; + if (context && !contextTransferredToPrefix) + { + context->Release(); + } + _ScheduleDeferredKeyDownDrain(); +} + +bool CMetasequoiaIME::_IsDeferredKeyReplayCurrent( + uint64_t replayToken, uint64_t focusGeneration, + _In_opt_ ITfContext *expectedContext) const +{ + return replayToken != 0 && _hasDeferredKeyInFlight && + _deferredKeyReplayToken == replayToken && + focusGeneration == _deferredKeyFocusGeneration && + _deferredKeyInFlight.focusGeneration == focusGeneration && + (expectedContext == nullptr || + _deferredKeyInFlight.context == expectedContext); +} + +void CMetasequoiaIME::_RetryDeferredKeyReplay(uint64_t replayToken) +{ + if (replayToken == 0 || !_hasDeferredKeyInFlight || + _deferredKeyReplayToken != replayToken) + { + return; + } + if (_deferredKeyInFlight.focusGeneration != _deferredKeyFocusGeneration) + { + _CompleteDeferredKeyReplay(replayToken); + return; + } + + const bool needsBackoff = _deferredKeyInFlight.replayAttempts >= 2; + _deferredKeyDowns.push_front(_deferredKeyInFlight); + while (!_deferredAppliedPrefix.empty()) + { + _deferredKeyDowns.push_front(_deferredAppliedPrefix.back()); + _deferredAppliedPrefix.pop_back(); + } + _deferredKeyInFlight = {}; + _hasDeferredKeyInFlight = false; + _deferredKeyReplayToken = 0; + // Finish the ownership transfer before a dirty notification can fall + // back to synchronous window dispatch and re-enter reset bookkeeping. + MarkNamedpipeSessionDirtyForOwner(this); + if (!needsBackoff) + { + _ScheduleDeferredKeyDownDrain(); + } + else if (_msgWndHandle && IsWindow(_msgWndHandle)) + { + PostOwnerMessageWithSyncFallback(_msgWndHandle, WM_IpcReconnect); + } +} + +void CMetasequoiaIME::_ScheduleDeferredKeyDownDrain() +{ + if (!_deferredKeyDowns.empty() && !_hasDeferredKeyInFlight && + !_deferredKeyDrainPosted && + _msgWndHandle && IsWindow(_msgWndHandle)) + { + _deferredKeyDrainPosted = true; + if (!PostMessage(_msgWndHandle, WM_DrainDeferredKeyDown, 0, 0)) + { + // The owner window is thread-affine. A synchronous fallback keeps + // a transient queue-post failure from stranding an eaten key. + SendMessage(_msgWndHandle, WM_DrainDeferredKeyDown, 0, 0); + } + } +} + +void CMetasequoiaIME::_DrainOneDeferredKeyDown() +{ + _deferredKeyDrainPosted = false; + if (_hasDeferredKeyInFlight || _deferredKeyDowns.empty() || + !Global::g_connected) + { + return; + } + if (_localSessionResetPending.load(std::memory_order_acquire) || + !EnsureNamedpipeFocusSessionActivated()) + { + PostOwnerMessageWithSyncFallback(_msgWndHandle, WM_IpcReconnect); + return; + } + + _deferredKeyInFlight = _deferredKeyDowns.front(); + _deferredKeyDowns.pop_front(); + _hasDeferredKeyInFlight = true; + do + { + _deferredKeyReplayToken = ++_nextDeferredKeyReplayToken; + } while (_deferredKeyReplayToken == 0); + + DeferredKeyDown &key = _deferredKeyInFlight; + ++key.replayAttempts; + const uint64_t replayToken = _deferredKeyReplayToken; + const uint64_t focusToken = _CaptureFocusSessionToken(); + if (key.focusGeneration != _deferredKeyFocusGeneration) + { + // All queued keys belong to the focused top context that captured + // them. Never replay into another editor after another focus change. + _ClearDeferredKeyDowns(); + return; + } + if (!_IsFocusSessionCurrent(focusToken, key.context)) + { + // A token/Ready fence can close without a document focus change. + // Preserve the exact item for the replacement Server epoch; an actual + // focus-generation change is handled by _ClearDeferredKeyDowns above. + _RetryDeferredKeyReplay(replayToken); + return; + } + + BOOL eaten = FALSE; + if (key.kind == DeferredKeyDown::Kind::ApplicationText) + { + (void)_RequestDeferredApplicationTextEditSession( + key.context, key.translatedWch, focusToken, + key.focusGeneration, replayToken); + return; + } + if (key.kind == DeferredKeyDown::Kind::PreservedKey) + { + const auto preservedAction = _pCompositionProcessorEngine + ? _pCompositionProcessorEngine + ->GetPreservedKeyAction( + key.preservedKey) + : CCompositionProcessorEngine:: + PreservedKeyAction::None; + const bool awaitsEditSession = + preservedAction == CCompositionProcessorEngine:: + PreservedKeyAction::ToggleImeMode; + if (!key.preservedApplied) + { + // OnPreservedKey changes compartments synchronously. Mark that + // phase before entering COM so a failed async commit retries only + // the commit phase and never toggles twice. + key.preservedApplied = true; + _DispatchPreservedKey(key.context, key.preservedKey, &eaten, + key.focusGeneration, true, replayToken); + } + else if (awaitsEditSession) + { + _KEYSTROKE_STATE toggleState = {}; + toggleState.Category = CATEGORY_COMPOSING; + toggleState.Function = FUNCTION_TOGGLE_IME_MODE; + _InvokeKeyHandler(key.context, 0, L'\0', 0, toggleState, + FANY_IME_NO_REQUEST_ID, {}, 0, 0, 0, + replayToken); + } + if (!awaitsEditSession && _deferredKeyReplayToken == replayToken) + { + _CompleteDeferredKeyReplay(replayToken); + } + return; + } + + const KeyDownDispatchResult result = + _DispatchKeyDown(key.context, key.wParam, key.lParam, &eaten, + &key.translatedWch, &key.modifiersDown, &key.keyState, false, + key.focusGeneration, replayToken); + if (result == KeyDownDispatchResult::Retry) + { + _RetryDeferredKeyReplay(replayToken); + return; + } + + if (result == KeyDownDispatchResult::Complete && + _deferredKeyReplayToken == replayToken) + { + _CompleteDeferredKeyReplay(replayToken); + } +} + //+--------------------------------------------------------------------------- // // ITfKeyEventSink::OnKeyDown @@ -359,23 +1295,144 @@ STDAPI CMetasequoiaIME::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPARA STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten) { + if (pContext == nullptr || pIsEaten == nullptr) + { + return E_INVALIDARG; + } PerfTimer onKeyDownTimer; - const bool isPunctuationKey = _pCompositionProcessorEngine && _pCompositionProcessorEngine->IsPunctuation(ConvertVKey((UINT)wParam)); + const uint64_t focusGeneration = _deferredKeyFocusGeneration; + (void)_DispatchKeyDown(pContext, wParam, lParam, pIsEaten, nullptr, nullptr, + nullptr, true, focusGeneration); + return S_OK; +} - Global::UpdateModifiers(wParam, lParam); +CMetasequoiaIME::KeyDownDispatchResult CMetasequoiaIME::_DispatchKeyDown( + _In_ ITfContext *pContext, WPARAM wParam, LPARAM lParam, _Out_ BOOL *pIsEaten, + _In_opt_ const WCHAR *translatedWch, _In_opt_ const UINT *modifiersDown, + _In_opt_ const _KEYSTROKE_STATE *prevalidatedKeyState, + bool canDefer, uint64_t expectedFocusGeneration, + uint64_t deferredReplayToken) +{ + if (pContext == nullptr || pIsEaten == nullptr) + { + return KeyDownDispatchResult::Complete; + } - _KEYSTROKE_STATE KeystrokeState; + if (translatedWch == nullptr) + { + Global::UpdateModifiers(wParam, lParam); + } + + _KEYSTROKE_STATE KeystrokeState = {}; WCHAR wch = '\0'; UINT code = 0; + uint64_t requestId = FANY_IME_NO_REQUEST_ID; + const UINT capturedModifiers = modifiersDown ? *modifiersDown : CaptureIpcModifiers(); - PerfTimer isKeyEatenTimer; - *pIsEaten = _IsKeyEaten( // - pContext, // - (UINT)wParam, // - &code, // - &wch, // - &KeystrokeState // - ); + if (canDefer && _HasDeferredKeyBarrier()) + { + if (!_DeferredKeyQueueHasCapacity() || + !_ClassifyDeferredKeyDown(pContext, wParam, translatedWch, + &capturedModifiers, &wch, &code, + &KeystrokeState)) + { + *pIsEaten = FALSE; + return KeyDownDispatchResult::Complete; + } + if (expectedFocusGeneration == 0 || + expectedFocusGeneration != _deferredKeyFocusGeneration) + { + *pIsEaten = TRUE; + return KeyDownDispatchResult::Complete; + } + *pIsEaten = _QueueDeferredKeyDown( + pContext, wParam, lParam, wch, capturedModifiers, + KeystrokeState) + ? TRUE + : FALSE; + if (*pIsEaten && + _localSessionResetPending.load(std::memory_order_acquire)) + { + const UINT resetToken = + _localSessionResetToken.load(std::memory_order_acquire); + _RequestLocalSessionReset(pContext, resetToken); + } + return KeyDownDispatchResult::Complete; + } + + if (prevalidatedKeyState != nullptr) + { + KeystrokeState = *prevalidatedKeyState; + wch = translatedWch ? *translatedWch : ConvertVKey(static_cast(wParam)); + code = VKeyFromVKPacketAndWchar(static_cast(wParam), wch); + *pIsEaten = TRUE; + } + else + { + PerfTimer isKeyEatenTimer; + *pIsEaten = _IsKeyEaten( // + pContext, // + (UINT)wParam, // + &code, // + &wch, // + &KeystrokeState, // + translatedWch // + ); + } + + if (expectedFocusGeneration == 0 || + expectedFocusGeneration != _deferredKeyFocusGeneration) + { + // A COM callback inside key classification changed the focused + // topology. The old key must not enter the replacement Server epoch. + *pIsEaten = TRUE; + return KeyDownDispatchResult::Complete; + } + + const bool resetPending = + _localSessionResetPending.load(std::memory_order_acquire); + if (resetPending) + { + if (!canDefer) + { + return KeyDownDispatchResult::Retry; + } + + // The reset gate may have closed concurrently with normal + // classification. Reclassify against the FIFO's future state before + // retaining the key. + if (!_DeferredKeyQueueHasCapacity() || + !_ClassifyDeferredKeyDown(pContext, wParam, translatedWch, + &capturedModifiers, &wch, &code, + &KeystrokeState) || + !_QueueDeferredKeyDown(pContext, wParam, lParam, wch, + capturedModifiers, KeystrokeState)) + { + *pIsEaten = FALSE; + } + const UINT resetToken = _localSessionResetToken.load(std::memory_order_acquire); + _RequestLocalSessionReset(pContext, resetToken); + return KeyDownDispatchResult::Complete; + } + + if (canDefer && *pIsEaten && + (KeystrokeState.Category != CATEGORY_NONE || + KeystrokeState.Function != FUNCTION_NONE)) + { + // Give every IME-owned key a member-owned replay token before any IPC + // write or asynchronous TSF edit session is started. Consequently a + // write success followed by a reply/edit failure follows the same + // exact retry path as a key that arrived behind a reconnect barrier. + *pIsEaten = _QueueDeferredKeyDown( + pContext, wParam, lParam, wch, capturedModifiers, + KeystrokeState) + ? TRUE + : FALSE; + return KeyDownDispatchResult::Complete; + } + + const bool isPunctuationKey = _pCompositionProcessorEngine && + _pCompositionProcessorEngine->IsPunctuation(wch); Global::firefox_like_cnt = 0; @@ -390,41 +1447,66 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP { // 这个键仍然被吃掉(以防止它到达应用程序), // 但我们不把它发送到 server 端,也不进一步处理它。 - return S_OK; + return KeyDownDispatchResult::Complete; + } + + if (expectedFocusGeneration != _deferredKeyFocusGeneration) + { + return KeyDownDispatchResult::Complete; } Global::Keycode = code; Global::wch = wch; - if ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) - Global::ModifiersDown |= 0b00000001; - else - Global::ModifiersDown &= ~0b00000001; - - PerfTimer clearPipeTimer; - // Candidate-navigation responses share the main Server->TSF pipe with - // candidate commits. Drain any unconsumed response from an earlier key - // before issuing a new request so it cannot be mistaken for this key's - // acknowledgement. - ClearNamedpipeDataIfExists(KeystrokeState.Function == FUNCTION_SERVER_CANDIDATE_KEY); + Global::ModifiersDown = capturedModifiers; PerfTimer writeShmTimer; WriteDataToSharedMemory(Global::Keycode, wch, Global::ModifiersDown, nullptr, 0, L"", 0b000111); PerfTimer sendKeyEventTimer; - SendKeyEventToUIProcess(); + const KeyEventSendResult sendResult = SendKeyEventToUIProcess(&requestId); + if (sendResult != KeyEventSendResult::Sent) + { + if (!canDefer) + { + return KeyDownDispatchResult::Retry; + } + const bool queued = _QueueDeferredKeyDown( + pContext, wParam, lParam, wch, capturedModifiers, + KeystrokeState); + if (!queued) + { + // This path is defensive now that every normal eaten key is + // tokenized before dispatch. If it is ever reached, handing + // the key back is preferable to silently dropping an + // ambiguous delivery. + *pIsEaten = FALSE; + } + // A failed write dirties the session and forces a new activation + // token. If the old Server epoch did receive the ambiguous frame, + // that epoch is rejected/cleared before this queued key is replayed. + // Thus retrying after the exact FocusSessionReady fence cannot + // apply the same key twice to one Server composition. + return KeyDownDispatchResult::Complete; + } if (KeystrokeState.Function == FUNCTION_SERVER_CANDIDATE_KEY && _msgWndHandle) { - PostMessage(_msgWndHandle, WM_AsyncServerCandidateKey, code, static_cast(wch)); - return S_OK; + _PostAsyncKeyRequest(WM_AsyncServerCandidateKey, code, wch, requestId, + {}, 0, 0, deferredReplayToken); + return deferredReplayToken != 0 + ? KeyDownDispatchResult::AwaitingCompletion + : KeyDownDispatchResult::Complete; } if (code == VK_SPACE && KeystrokeState.Function == FUNCTION_CONVERT) { if (_msgWndHandle) { - PostMessage(_msgWndHandle, WM_AsyncFinalizeCandidate, 0, 0); - return S_OK; + _PostAsyncKeyRequest(WM_AsyncFinalizeCandidate, code, wch, requestId, + {}, 0, 0, deferredReplayToken); + return deferredReplayToken != 0 + ? KeyDownDispatchResult::AwaitingCompletion + : KeyDownDispatchResult::Complete; } } @@ -439,16 +1521,30 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP Global::CommitWithFirstCandPunc.count(wch) > 0; if (!shouldFinalizeFirstCandidateWithPunctuation) { - _QueuePendingPunctuationCommitText(punctuationCommitText.c_str()); + // The text is carried by this exact async request. No global + // single-slot cache can be overwritten by the next key. } - PostMessage(_msgWndHandle, WM_AsyncPunctuationCommit, code, static_cast(wch)); - return S_OK; + else + { + // Empty means the edit session must consume this request's + // candidate reply and append the punctuation derived from wch. + punctuationCommitText.clear(); + } + _PostAsyncKeyRequest(WM_AsyncPunctuationCommit, code, wch, requestId, + std::move(punctuationCommitText), 0, 0, + deferredReplayToken); + return deferredReplayToken != 0 + ? KeyDownDispatchResult::AwaitingCompletion + : KeyDownDispatchResult::Complete; } if (KeystrokeState.Function == FUNCTION_SELECT_BY_NUMBER && _msgWndHandle) { - PostMessage(_msgWndHandle, WM_AsyncNumberCandidateCommit, code, static_cast(wch)); - return S_OK; + _PostAsyncKeyRequest(WM_AsyncNumberCandidateCommit, code, wch, requestId, + {}, 0, 0, deferredReplayToken); + return deferredReplayToken != 0 + ? KeyDownDispatchResult::AwaitingCompletion + : KeyDownDispatchResult::Complete; } } @@ -471,7 +1567,13 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP if (needInvokeKeyHandler) { PerfTimer invokeTimer; - _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState); + _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState, + requestId, {}, 0, 0, 0, + deferredReplayToken); + if (deferredReplayToken != 0) + { + return KeyDownDispatchResult::AwaitingCompletion; + } } } else if (KeystrokeState.Category == CATEGORY_INVOKE_COMPOSITION_EDIT_SESSION) @@ -479,14 +1581,15 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP // Invoke key handler edit session KeystrokeState.Category = CATEGORY_COMPOSING; PerfTimer invokeTimer; - _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState); + _InvokeKeyHandler(pContext, code, wch, (DWORD)lParam, KeystrokeState, + FANY_IME_NO_REQUEST_ID); } if (isPunctuationKey) { } - return S_OK; + return KeyDownDispatchResult::Complete; } //+--------------------------------------------------------------------------- @@ -498,17 +1601,28 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP STDAPI CMetasequoiaIME::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten) { - if (pIsEaten == nullptr) + if (pContext == nullptr || pIsEaten == nullptr) { return E_INVALIDARG; } Global::UpdateModifiers(wParam, lParam); + if (_HasDeferredKeyBarrier()) + { + // A matching deferred key-down may or may not have fit in the bounded + // queue. Letting all key-ups through is harmless and guarantees the + // application never observes a down without its release. + *pIsEaten = FALSE; + return S_OK; + } + + _KEYSTROKE_STATE KeystrokeState = {}; WCHAR wch = '\0'; UINT code = 0; - *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, NULL); + *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, + &KeystrokeState); return S_OK; } @@ -523,12 +1637,23 @@ STDAPI CMetasequoiaIME::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM STDAPI CMetasequoiaIME::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten) { + if (pContext == nullptr || pIsEaten == nullptr) + { + return E_INVALIDARG; + } Global::UpdateModifiers(wParam, lParam); - _KEYSTROKE_STATE KeystrokeState; + if (_HasDeferredKeyBarrier()) + { + *pIsEaten = FALSE; + return S_OK; + } + + _KEYSTROKE_STATE KeystrokeState = {}; WCHAR wch = '\0'; UINT code = 0; - *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, &KeystrokeState); + *pIsEaten = _IsKeyEaten(pContext, (UINT)wParam, &code, &wch, + &KeystrokeState); // if (code == VK_SHIFT) // { @@ -552,33 +1677,76 @@ STDAPI CMetasequoiaIME::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lPar STDAPI CMetasequoiaIME::OnPreservedKey(ITfContext *pContext, REFGUID rguid, BOOL *pIsEaten) { - pContext; + if (pContext == nullptr || pIsEaten == nullptr) + { + return E_INVALIDARG; + } + const bool eligible = _pCompositionProcessorEngine && + _pCompositionProcessorEngine->IsPreservedKeyEligible(rguid) && + _DeferredKeyQueueHasCapacity(); + *pIsEaten = eligible && _QueueDeferredPreservedKey(pContext, rguid) + ? TRUE + : FALSE; + if (*pIsEaten && + _localSessionResetPending.load(std::memory_order_acquire)) + { + const UINT resetToken = + _localSessionResetToken.load(std::memory_order_acquire); + _RequestLocalSessionReset(pContext, resetToken); + } + return S_OK; +} - CCompositionProcessorEngine *pCompositionProcessorEngine; - pCompositionProcessorEngine = _pCompositionProcessorEngine; +void CMetasequoiaIME::_DispatchPreservedKey(_In_ ITfContext *pContext, + REFGUID preservedKey, + _Out_ BOOL *pIsEaten, + uint64_t expectedFocusGeneration, + bool isPrevalidated, + uint64_t deferredReplayToken) +{ + *pIsEaten = FALSE; + if (pContext == nullptr || _pCompositionProcessorEngine == nullptr || + expectedFocusGeneration == 0 || + expectedFocusGeneration != _deferredKeyFocusGeneration) + { + return; + } BOOL pNeedToggleIMEMode = FALSE; - pCompositionProcessorEngine->OnPreservedKey( // + _pCompositionProcessorEngine->OnPreservedKey( // pContext, // - rguid, // + preservedKey, // pIsEaten, // _GetThreadMgr(), // _GetClientId(), // - &pNeedToggleIMEMode // + &pNeedToggleIMEMode, // + isPrevalidated ? TRUE : FALSE // ); - if (pNeedToggleIMEMode) + if (pNeedToggleIMEMode && + expectedFocusGeneration == _deferredKeyFocusGeneration) { - _KEYSTROKE_STATE KeystrokeState; + // The preserved-key implementation also sends the Shift event to the + // Server. A failed/ambiguous send marks the local session dirty. Do + // not let the subsequent local edit falsely Complete this token; the + // replacement epoch receives the authoritative status snapshot and + // then retries only the already-applied local toggle phase. + if (deferredReplayToken != 0 && + _localSessionResetPending.load(std::memory_order_acquire)) + { + _RetryDeferredKeyReplay(deferredReplayToken); + return; + } + _KEYSTROKE_STATE KeystrokeState = {}; WCHAR wch = '\0'; UINT code = 0; KeystrokeState.Category = CATEGORY_COMPOSING; KeystrokeState.Function = FUNCTION_TOGGLE_IME_MODE; - _InvokeKeyHandler(pContext, code, wch, (DWORD)0, KeystrokeState); + _InvokeKeyHandler(pContext, code, wch, (DWORD)0, KeystrokeState, + FANY_IME_NO_REQUEST_ID, {}, 0, 0, 0, + deferredReplayToken); } - - return S_OK; } //+--------------------------------------------------------------------------- diff --git a/src/Key/KeyHandler.cpp b/src/Key/KeyHandler.cpp index 127c5e57..0fd8bf4f 100644 --- a/src/Key/KeyHandler.cpp +++ b/src/Key/KeyHandler.cpp @@ -16,13 +16,7 @@ namespace { -std::wstring g_toggleImeFallbackBuffer; - -bool IsTimeoutSentinelCandidate(UINT msgType, const WCHAR *pCandidateString) -{ - return msgType == Global::DataFromServerMsgType::Normal && pCandidateString != nullptr && - wcscmp(pCandidateString, L"T") == 0; -} +thread_local std::wstring g_toggleImeFallbackBuffer; DWORD_PTR MapRawCaretToPreedit(const CStringRange &raw, DWORD_PTR rawCaret, const std::wstring &preedit, size_t prefixLength) @@ -102,9 +96,12 @@ VOID CMetasequoiaIME::_DeleteCandidateList(BOOL isForce, _In_opt_ ITfContext *pC CCompositionProcessorEngine *pCompositionProcessorEngine = nullptr; pCompositionProcessorEngine = _pCompositionProcessorEngine; - PerfTimer purgeTimer; - pCompositionProcessorEngine->PurgeVirtualKey(); - double purgeElapsedMs = purgeTimer.ElapsedMs(); + if (pCompositionProcessorEngine) + { + PerfTimer purgeTimer; + pCompositionProcessorEngine->PurgeVirtualKey(); + double purgeElapsedMs = purgeTimer.ElapsedMs(); + } double endCandidateElapsedMs = 0; if (_pCandidateListUIPresenter) @@ -147,7 +144,6 @@ HRESULT CMetasequoiaIME::_HandleComplete(TfEditCookie ec, _In_ ITfContext *pCont _TerminateComposition(ec, pContext); double terminateElapsedMs = terminateTimer.ElapsedMs(); - return S_OK; } @@ -191,7 +187,6 @@ HRESULT CMetasequoiaIME::_HandleCancel(TfEditCookie ec, _In_ ITfContext *pContex _TerminateComposition(ec, pContext); double terminateElapsedMs = terminateTimer.ElapsedMs(); - return S_OK; } @@ -242,8 +237,10 @@ HRESULT CMetasequoiaIME::_HandleToogleIMEMode(TfEditCookie ec, _In_ ITfContext * // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCompositionInput(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch) +HRESULT CMetasequoiaIME::_HandleCompositionInput(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch, + uint64_t requestId) { + HRESULT workerResult = S_OK; ITfRange *pRangeComposition = nullptr; TF_SELECTION tfSelection; ULONG fetched = 0; @@ -286,11 +283,12 @@ HRESULT CMetasequoiaIME::_HandleCompositionInput(TfEditCookie ec, _In_ ITfContex pCompositionProcessorEngine->AddVirtualKey(wch); g_toggleImeFallbackBuffer.push_back(wch); - _HandleCompositionInputWorker(pCompositionProcessorEngine, ec, pContext); + workerResult = _HandleCompositionInputWorker( + pCompositionProcessorEngine, ec, pContext, requestId); Exit: tfSelection.range->Release(); - return S_OK; + return workerResult; } //+--------------------------------------------------------------------------- @@ -302,7 +300,8 @@ Exit: //---------------------------------------------------------------------------- HRESULT CMetasequoiaIME::_HandleCompositionInputWorker(_In_ CCompositionProcessorEngine *pCompositionProcessorEngine, - TfEditCookie ec, _In_ ITfContext *pContext) + TfEditCookie ec, _In_ ITfContext *pContext, + uint64_t requestId) { HRESULT hr = S_OK; PerfTimer timer; @@ -339,11 +338,17 @@ HRESULT CMetasequoiaIME::_HandleCompositionInputWorker(_In_ CCompositionProcesso // readingStr = L""; curReadingStr.Set(readingStr.c_str(), readingStr.length()); - if (GlobalSettings::getTsfPreeditStyle() == GlobalSettings::TsfPreeditStyle::Pinyin) + if (GlobalSettings::getTsfPreeditStyle() == GlobalSettings::TsfPreeditStyle::Pinyin && + requestId != FANY_IME_NO_REQUEST_ID) { PerfTimer preeditPipeTimer; - struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(); + struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(requestId); preeditPipeElapsedMs += preeditPipeTimer.ElapsedMs(); + if (receivedData->msg_type == + Global::DataFromServerMsgType::TransportUnavailable) + { + return HRESULT_FROM_WIN32(ERROR_BROKEN_PIPE); + } if (receivedData->msg_type == Global::DataFromServerMsgType::Preedit) { curReadingStr.Set(receivedData->candidate_string, wcslen(receivedData->candidate_string)); @@ -516,9 +521,6 @@ HRESULT CMetasequoiaIME::_HandleCompositionFinalize(TfEditCookie ec, _In_ ITfCon HRESULT hr = S_OK; PerfTimer timer; double finalizeCandidateElapsedMs = 0; - double getSelectionElapsedMs = 0; - double getRangeElapsedMs = 0; - double endCompositionElapsedMs = 0; if (isCandidateList && _pCandidateListUIPresenter) { @@ -543,39 +545,10 @@ HRESULT CMetasequoiaIME::_HandleCompositionFinalize(TfEditCookie ec, _In_ ITfCon } } } - else - { - // Finalize current text store strings - if (_IsComposing()) - { - ULONG fetched = 0; - TF_SELECTION tfSelection; - - PerfTimer getSelectionTimer; - if (FAILED(pContext->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &tfSelection, &fetched)) || fetched != 1) - { - return S_FALSE; - } - getSelectionElapsedMs = getSelectionTimer.ElapsedMs(); - - ITfRange *pRangeComposition = nullptr; - PerfTimer getRangeTimer; - if (SUCCEEDED(_pComposition->GetRange(&pRangeComposition))) - { - getRangeElapsedMs = getRangeTimer.ElapsedMs(); - if (_IsRangeCovered(ec, tfSelection.range, pRangeComposition)) - { - PerfTimer endCompositionTimer; - _EndComposition(pContext); - endCompositionElapsedMs = endCompositionTimer.ElapsedMs(); - } - - pRangeComposition->Release(); - } - - tfSelection.range->Release(); - } - } + // For the non-candidate path, the current composition text is already in + // the text store. _HandleCancel below owns the exact write cookie and + // terminates it synchronously; requesting a nested edit session here can + // legitimately fail with TF_E_SYNCHRONOUS and is redundant. PerfTimer cancelTimer; _HandleCancel(ec, pContext); @@ -682,8 +655,10 @@ HRESULT CMetasequoiaIME::_HandleCompositionConvert(TfEditCookie ec, _In_ ITfCont // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCompositionBackspace(TfEditCookie ec, _In_ ITfContext *pContext) +HRESULT CMetasequoiaIME::_HandleCompositionBackspace(TfEditCookie ec, _In_ ITfContext *pContext, + uint64_t requestId) { + HRESULT workerResult = S_OK; ITfRange *pRangeComposition = nullptr; TF_SELECTION tfSelection; ULONG fetched = 0; @@ -733,7 +708,8 @@ HRESULT CMetasequoiaIME::_HandleCompositionBackspace(TfEditCookie ec, _In_ ITfCo if (pCompositionProcessorEngine->GetVirtualKeyLength()) { - _HandleCompositionInputWorker(pCompositionProcessorEngine, ec, pContext); + workerResult = _HandleCompositionInputWorker( + pCompositionProcessorEngine, ec, pContext, requestId); } else { @@ -743,7 +719,7 @@ HRESULT CMetasequoiaIME::_HandleCompositionBackspace(TfEditCookie ec, _In_ ITfCo Exit: tfSelection.range->Release(); - return S_OK; + return workerResult; } //+--------------------------------------------------------------------------- @@ -836,7 +812,9 @@ Exit: // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch) +HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITfContext *pContext, WCHAR wch, + uint64_t requestId, + const std::wstring &prefetchedText) { HRESULT hr = S_OK; PerfTimer timer; @@ -846,7 +824,7 @@ HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITf CCompositionProcessorEngine *pCompositionProcessorEngine = nullptr; pCompositionProcessorEngine = _pCompositionProcessorEngine; - std::wstring pendingPunctuationCommitText = _TakePendingPunctuationCommitText(); + std::wstring pendingPunctuationCommitText = prefetchedText; const bool hasPendingPunctuationCommitText = !pendingPunctuationCommitText.empty(); std::wstring punctuationStr; if (hasPendingPunctuationCommitText) @@ -869,13 +847,13 @@ HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITf { /* 这里我们不需要考虑下标超出范围,因为我们总是可以取到第一个候选词 */ PerfTimer pipeReadTimer; - struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(); + struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(requestId); pipeReadElapsedMs = pipeReadTimer.ElapsedMs(); - if (IsTimeoutSentinelCandidate(receivedData->msg_type, receivedData->candidate_string)) + + if (receivedData->msg_type == + Global::DataFromServerMsgType::TransportUnavailable) { - PerfTimer retryTimer; - receivedData = TryReadDataFromServerPipeWithTimeout(); - pipeReadElapsedMs += retryTimer.ElapsedMs(); + return HRESULT_FROM_WIN32(ERROR_BROKEN_PIPE); } // The Server is authoritative for configurable candidate-navigation @@ -980,7 +958,11 @@ HRESULT CMetasequoiaIME::_HandleCompositionDoubleSingleByte(TfEditCookie ec, _In //---------------------------------------------------------------------------- HRESULT CMetasequoiaIME::_InvokeKeyHandler(_In_ ITfContext *pContext, UINT code, WCHAR wch, DWORD flags, - _KEYSTROKE_STATE keyState) + _KEYSTROKE_STATE keyState, uint64_t requestId, + std::wstring prefetchedText, UINT localResetToken, + uint64_t expectedCompositionEpoch, + uint64_t expectedFocusToken, + uint64_t deferredReplayToken) { flags; @@ -992,10 +974,22 @@ HRESULT CMetasequoiaIME::_InvokeKeyHandler(_In_ ITfContext *pContext, UINT code, LARGE_INTEGER requestStartQpc; QueryPerformanceCounter(&requestStartQpc); PerfTimer allocTimer; - pEditSession = new (std::nothrow) CKeyHandlerEditSession(this, pContext, code, wch, keyState, requestStartQpc); + pEditSession = new (std::nothrow) CKeyHandlerEditSession(this, pContext, code, wch, keyState, requestId, + requestStartQpc, std::move(prefetchedText), + localResetToken == 0 + ? (expectedFocusToken != 0 + ? expectedFocusToken + : _CaptureFocusSessionToken()) + : 0, + localResetToken, expectedCompositionEpoch, + deferredReplayToken); double allocElapsedMs = allocTimer.ElapsedMs(); if (pEditSession == nullptr) { + if (deferredReplayToken != 0) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } goto Exit; } @@ -1009,7 +1003,12 @@ HRESULT CMetasequoiaIME::_InvokeKeyHandler(_In_ ITfContext *pContext, UINT code, HRESULT requestHr = pContext->RequestEditSession(_tfClientId, pEditSession, TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, &editSessionHr); double requestCallElapsedMs = requestCallTimer.ElapsedMs(); - hr = requestHr; + hr = FAILED(requestHr) ? requestHr : editSessionHr; + if ((FAILED(requestHr) || FAILED(editSessionHr)) && + deferredReplayToken != 0) + { + _RetryDeferredKeyReplay(deferredReplayToken); + } PerfTimer releaseTimer; pEditSession->Release(); diff --git a/src/Key/KeyHandlerEditSession.cpp b/src/Key/KeyHandlerEditSession.cpp index 813931be..a6c8d361 100644 --- a/src/Key/KeyHandlerEditSession.cpp +++ b/src/Key/KeyHandlerEditSession.cpp @@ -23,6 +23,50 @@ STDAPI CKeyHandlerEditSession::DoEditSession(TfEditCookie ec) { + struct DeferredReplayCompletion + { + CMetasequoiaIME *textService; + uint64_t token; + bool applied = false; + ~DeferredReplayCompletion() + { + if (textService && token != 0) + { + if (applied) + { + textService->_CompleteDeferredKeyReplay(token); + } + else + { + textService->_RetryDeferredKeyReplay(token); + } + } + } + } deferredReplayCompletion{_pTextService, _deferredReplayToken, false}; + + const bool isLocalReset = _localResetToken != 0; + if (isLocalReset) + { + if (!_pTextService->_IsLocalSessionResetCurrent(_localResetToken)) + { + // A newer focus/reset token may supersede this granted session. + // It must still retire its exact queue slot so Complete can queue + // the newer token; returning without bookkeeping would gate every + // subsequent key forever. + _pTextService->_CompleteLocalSessionReset(_localResetToken); + return S_FALSE; + } + } + else if (!_pTextService->_IsFocusSessionCurrent(_focusToken, _pContext)) + { + return S_FALSE; + } + if (!isLocalReset && _expectedCompositionEpoch != 0 && + !_pTextService->_IsCompositionEpochCurrent(_expectedCompositionEpoch)) + { + return S_FALSE; + } + HRESULT hResult = S_OK; PerfTimer doEditSessionTimer; LARGE_INTEGER freq, nowQpc; @@ -32,18 +76,28 @@ STDAPI CKeyHandlerEditSession::DoEditSession(TfEditCookie ec) static_cast(freq.QuadPart); CKeyStateCategoryFactory *pKeyStateCategoryFactory = CKeyStateCategoryFactory::Instance(); - CKeyStateCategory *pKeyStateCategory = - pKeyStateCategoryFactory->MakeKeyStateCategory(_KeyState.Category, _pTextService); + CKeyStateCategory *pKeyStateCategory = pKeyStateCategoryFactory + ? pKeyStateCategoryFactory->MakeKeyStateCategory( + _KeyState.Category, _pTextService) + : nullptr; if (pKeyStateCategory) { - KeyHandlerEditSessionDTO keyHandlerEditSessioDTO(ec, _pContext, _uCode, _wch, _KeyState.Function); + KeyHandlerEditSessionDTO keyHandlerEditSessioDTO(ec, _pContext, _uCode, _wch, _KeyState.Function, + _requestId, _prefetchedText); hResult = pKeyStateCategory->KeyStateHandler(_KeyState.Function, keyHandlerEditSessioDTO); + deferredReplayCompletion.applied = hResult == S_OK; pKeyStateCategory->Release(); + } + if (pKeyStateCategoryFactory) + { pKeyStateCategoryFactory->Release(); } - + if (isLocalReset) + { + _pTextService->_CompleteLocalSessionReset(_localResetToken); + } return hResult; } diff --git a/src/Key/KeyHandlerEditSession.h b/src/Key/KeyHandlerEditSession.h index fd56901e..87875904 100644 --- a/src/Key/KeyHandlerEditSession.h +++ b/src/Key/KeyHandlerEditSession.h @@ -2,17 +2,29 @@ #include "EditSession.h" #include "Globals.h" +#include +#include +#include class CKeyHandlerEditSession : public CEditSessionBase { public: CKeyHandlerEditSession(CMetasequoiaIME *pTextService, ITfContext *pContext, UINT uCode, WCHAR wch, - _KEYSTROKE_STATE keyState, LARGE_INTEGER requestStartQpc = {}) + _KEYSTROKE_STATE keyState, uint64_t requestId, LARGE_INTEGER requestStartQpc = {}, + std::wstring prefetchedText = {}, uint64_t focusToken = 0, + UINT localResetToken = 0, uint64_t expectedCompositionEpoch = 0, + uint64_t deferredReplayToken = 0) : CEditSessionBase(pTextService, pContext) { _uCode = uCode; _wch = wch; _KeyState = keyState; + _requestId = requestId; + _focusToken = focusToken; + _localResetToken = localResetToken; + _expectedCompositionEpoch = expectedCompositionEpoch; + _deferredReplayToken = deferredReplayToken; + _prefetchedText = std::move(prefetchedText); _requestStartQpc = requestStartQpc; if (_requestStartQpc.QuadPart == 0) { @@ -27,5 +39,11 @@ class CKeyHandlerEditSession : public CEditSessionBase UINT _uCode; // virtual key code WCHAR _wch; // character code _KEYSTROKE_STATE _KeyState; // key function regarding virtual key + uint64_t _requestId; + uint64_t _focusToken; + UINT _localResetToken; + uint64_t _expectedCompositionEpoch; + uint64_t _deferredReplayToken; + std::wstring _prefetchedText; LARGE_INTEGER _requestStartQpc; }; diff --git a/src/Key/KeyStateCategory.cpp b/src/Key/KeyStateCategory.cpp index 0a2cec06..aed26f5b 100644 --- a/src/Key/KeyStateCategory.cpp +++ b/src/Key/KeyStateCategory.cpp @@ -4,7 +4,7 @@ #include "MetasequoiaIMEBaseStructure.h" #include -CKeyStateCategoryFactory *CKeyStateCategoryFactory::_instance; +thread_local CKeyStateCategoryFactory *CKeyStateCategoryFactory::_instance = nullptr; CKeyStateCategoryFactory::CKeyStateCategoryFactory() { @@ -248,13 +248,13 @@ CKeyStateComposing::CKeyStateComposing(_In_ CMetasequoiaIME *pTextService) : CKe HRESULT CKeyStateComposing::HandleKeyInput(KeyHandlerEditSessionDTO dto) { - return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch); + return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch, dto.requestId); } HRESULT CKeyStateComposing::HandleKeyFinalizeTextStoreAndInput(KeyHandlerEditSessionDTO dto) { _pTextService->_HandleCompositionFinalize(dto.ec, dto.pContext, FALSE); - return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch); + return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch, dto.requestId); } HRESULT CKeyStateComposing::HandleKeyFinalizeTextStore(KeyHandlerEditSessionDTO dto) @@ -265,7 +265,7 @@ HRESULT CKeyStateComposing::HandleKeyFinalizeTextStore(KeyHandlerEditSessionDTO HRESULT CKeyStateComposing::HandleKeyFinalizeCandidatelistAndInput(KeyHandlerEditSessionDTO dto) { _pTextService->_HandleCompositionFinalize(dto.ec, dto.pContext, TRUE); - return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch); + return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch, dto.requestId); } HRESULT CKeyStateComposing::HandleKeyFinalizeCandidatelist(KeyHandlerEditSessionDTO dto) @@ -275,9 +275,10 @@ HRESULT CKeyStateComposing::HandleKeyFinalizeCandidatelist(KeyHandlerEditSession HRESULT CKeyStateComposing::HandleKeyConvert(KeyHandlerEditSessionDTO dto) { - if (Global::Keycode == VK_SPACE) + if (dto.code == VK_SPACE) { - return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext); + return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext, dto.requestId, + dto.prefetchedText); } // VK_SPACE return _pTextService->_HandleCompositionConvert(dto.ec, dto.pContext, FALSE); @@ -300,7 +301,7 @@ HRESULT CKeyStateComposing::HandleKeyToogleIMEMode(KeyHandlerEditSessionDTO dto) HRESULT CKeyStateComposing::HandleKeyBackspace(KeyHandlerEditSessionDTO dto) { - return _pTextService->_HandleCompositionBackspace(dto.ec, dto.pContext); + return _pTextService->_HandleCompositionBackspace(dto.ec, dto.pContext, dto.requestId); } HRESULT CKeyStateComposing::HandleKeyArrow(KeyHandlerEditSessionDTO dto) @@ -315,7 +316,8 @@ HRESULT CKeyStateComposing::HandleKeyDoubleSingleByte(KeyHandlerEditSessionDTO d HRESULT CKeyStateComposing::HandleKeyPunctuation(KeyHandlerEditSessionDTO dto) { - return _pTextService->_HandleCompositionPunctuation(dto.ec, dto.pContext, dto.wch); + return _pTextService->_HandleCompositionPunctuation(dto.ec, dto.pContext, dto.wch, dto.requestId, + dto.prefetchedText); } /* @@ -328,7 +330,8 @@ CKeyStateCandidate::CKeyStateCandidate(_In_ CMetasequoiaIME *pTextService) : CKe // _HandleCandidateInput HRESULT CKeyStateCandidate::HandleKeyFinalizeCandidatelist(KeyHandlerEditSessionDTO dto) { - return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext); + return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext, dto.requestId, + dto.prefetchedText); } // _HandleCandidateInput @@ -340,19 +343,21 @@ HRESULT CKeyStateCandidate::HandleKeyFinalizeCandidatelistForVKReturn(KeyHandler // HandleKeyFinalizeCandidatelistAndInput HRESULT CKeyStateCandidate::HandleKeyFinalizeCandidatelistAndInput(KeyHandlerEditSessionDTO dto) { - _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext); - return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch); + _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext, dto.requestId, dto.prefetchedText); + return _pTextService->_HandleCompositionInput(dto.ec, dto.pContext, dto.wch, dto.requestId); } //_HandleCandidateConvert HRESULT CKeyStateCandidate::HandleKeyConvert(KeyHandlerEditSessionDTO dto) { - if (Global::Keycode == VK_SPACE) + if (dto.code == VK_SPACE) { - return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext); + return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext, dto.requestId, + dto.prefetchedText); } // Send candidate string to client when pressing VK_SPACE - return _pTextService->_HandleCandidateConvert(dto.ec, dto.pContext); + return _pTextService->_HandleCandidateConvert(dto.ec, dto.pContext, dto.requestId, + dto.prefetchedText); } //_HandleCancel @@ -371,6 +376,7 @@ HRESULT CKeyStateCandidate::HandleKeyArrow(KeyHandlerEditSessionDTO dto) HRESULT CKeyStateCandidate::HandleKeySelectByNumber(KeyHandlerEditSessionDTO dto) { // return _pTextService->_HandleCandidateSelectByNumber(dto.ec, dto.pContext, dto.code); - return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext); + return _pTextService->_HandleCandidateFinalize(dto.ec, dto.pContext, dto.requestId, + dto.prefetchedText); } diff --git a/src/Key/KeyStateCategory.h b/src/Key/KeyStateCategory.h index 49aa05ab..977bc178 100644 --- a/src/Key/KeyStateCategory.h +++ b/src/Key/KeyStateCategory.h @@ -1,7 +1,9 @@ #pragma once #include "Globals.h" #include "Private.h" -#include "MetasequoiaIME.h" +#include "MetasequoiaIME.h" +#include +#include class CKeyStateCategory; @@ -16,26 +18,32 @@ class CKeyStateCategoryFactory CKeyStateCategoryFactory(); private: - static CKeyStateCategoryFactory *_instance; + static thread_local CKeyStateCategoryFactory *_instance; }; typedef struct KeyHandlerEditSessionDTO { - KeyHandlerEditSessionDTO::KeyHandlerEditSessionDTO(TfEditCookie tFEC, _In_ ITfContext *pTfContext, UINT virualCode, - WCHAR inputChar, KEYSTROKE_FUNCTION arrowKeyFunction) + KeyHandlerEditSessionDTO::KeyHandlerEditSessionDTO(TfEditCookie tFEC, _In_ ITfContext *pTfContext, UINT virualCode, + WCHAR inputChar, KEYSTROKE_FUNCTION arrowKeyFunction, + uint64_t pipeRequestId, + const std::wstring &sessionPrefetchedText) { ec = tFEC; pContext = pTfContext; code = virualCode; wch = inputChar; - arrowKey = arrowKeyFunction; + arrowKey = arrowKeyFunction; + requestId = pipeRequestId; + prefetchedText = sessionPrefetchedText; } TfEditCookie ec; ITfContext *pContext; UINT code; WCHAR wch; - KEYSTROKE_FUNCTION arrowKey; + KEYSTROKE_FUNCTION arrowKey; + uint64_t requestId; + std::wstring prefetchedText; } KeyHandlerEditSessionDTO; class CKeyStateCategory diff --git a/src/Thread/ThreadFocusSink.cpp b/src/Thread/ThreadFocusSink.cpp index fcf07106..7f719f28 100644 --- a/src/Thread/ThreadFocusSink.cpp +++ b/src/Thread/ThreadFocusSink.cpp @@ -2,8 +2,57 @@ #include "Private.h" #include "MetasequoiaIME.h" #include "CandidateListUIPresenter.h" -#include -#include "fmt/xchar.h" +#include +#include +#include "fmt/xchar.h" + +namespace +{ +bool IsWindowsTextInputHostWindow(HWND window) +{ + if (!window) + { + return false; + } + + DWORD processId = 0; + GetWindowThreadProcessId(window, &processId); + if (processId == 0) + { + return false; + } + + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, + processId); + if (!process) + { + return false; + } + + WCHAR imagePath[32768] = {}; + DWORD imagePathLength = ARRAYSIZE(imagePath); + const bool queried = + QueryFullProcessImageNameW(process, 0, imagePath, &imagePathLength) != + FALSE; + CloseHandle(process); + if (!queried || imagePathLength == 0) + { + return false; + } + + const WCHAR *fileName = wcsrchr(imagePath, L'\\'); + fileName = fileName ? fileName + 1 : imagePath; + return _wcsicmp(fileName, L"TextInputHost.exe") == 0; +} +} // namespace + +bool CMetasequoiaIME::_CaptureWindowsTextInputHostFocusLoss() +{ + _focusLostToWindowsTextInputHost = + _focusLostToWindowsTextInputHost || + IsWindowsTextInputHostWindow(GetForegroundWindow()); + return _focusLostToWindowsTextInputHost; +} //+--------------------------------------------------------------------------- // @@ -13,7 +62,19 @@ STDAPI CMetasequoiaIME::OnSetThreadFocus() { - PostMessage(_msgWndHandle, WM_ThreadFocus, 0, 0); + if (!IsNamedpipeFocusStateOwner(this)) + { + return S_OK; + } + // Do not consume this marker here. TextInputHost owns a separate TSF + // client, so the authoritative document OnSetFocus must use it to force a + // new Server activation even when our local pipe handles still look live. + // Match bcaf34e: thread focus is observational. Document OnSetFocus owns + // disconnect/reconnect; this callback only refreshes status/presenter. + if (_msgWndHandle && IsWindow(_msgWndHandle)) + { + PostMessage(_msgWndHandle, WM_ThreadFocus, 0, 0); + } if (_pCandidateListUIPresenter) { ITfDocumentMgr *pCandidateListDocumentMgr = nullptr; @@ -26,11 +87,11 @@ STDAPI CMetasequoiaIME::OnSetThreadFocus() _pCandidateListUIPresenter->OnSetThreadFocus(); } - pCandidateListDocumentMgr->Release(); - } - } - return S_OK; -} + pCandidateListDocumentMgr->Release(); + } + } + return S_OK; +} //+--------------------------------------------------------------------------- // @@ -40,28 +101,42 @@ STDAPI CMetasequoiaIME::OnSetThreadFocus() STDAPI CMetasequoiaIME::OnKillThreadFocus() { + if (!IsNamedpipeFocusStateOwner(this) || !Global::g_connected) + { + return S_OK; + } + _focusLostToWindowsTextInputHost = false; + (void)_CaptureWindowsTextInputHostFocusLoss(); + + // ITfThreadFocusSink reports temporary auxiliary UI transitions as well + // as real application focus changes. The known-good implementation did + // not use this callback as an IPC/session boundary: document/top-context + // changes are already handled by ITfThreadMgrEventSink, and TIP switching + // is handled by Deactivate. Keep the current composition and pipe epoch + // intact here, especially for TextInputHost.exe / Win+.. if (_pCandidateListUIPresenter) { ITfDocumentMgr *pCandidateListDocumentMgr = nullptr; ITfContext *pTfContext = _pCandidateListUIPresenter->_GetContextDocument(); - if ((nullptr != pTfContext) && SUCCEEDED(pTfContext->GetDocumentMgr(&pCandidateListDocumentMgr))) - { + if ((nullptr != pTfContext) && SUCCEEDED(pTfContext->GetDocumentMgr(&pCandidateListDocumentMgr))) + { if (_pDocMgrLastFocused) { _pDocMgrLastFocused->Release(); _pDocMgrLastFocused = nullptr; - } - _pDocMgrLastFocused = pCandidateListDocumentMgr; - if (_pDocMgrLastFocused) - { - _pDocMgrLastFocused->AddRef(); - } - } - _pCandidateListUIPresenter->OnKillThreadFocus(); - } - return S_OK; -} + } + // GetDocumentMgr already returned one owned reference. Transfer it + // directly to _pDocMgrLastFocused; adding another reference here + // leaks one on every Win+. / thread-focus round trip. + _pDocMgrLastFocused = pCandidateListDocumentMgr; + pCandidateListDocumentMgr = nullptr; + + } + _pCandidateListUIPresenter->OnKillThreadFocus(); + } + return S_OK; +} BOOL CMetasequoiaIME::_InitThreadFocusSink() { diff --git a/src/Thread/ThreadMgrEventSink.cpp b/src/Thread/ThreadMgrEventSink.cpp index df2516e2..987ae5c1 100644 --- a/src/Thread/ThreadMgrEventSink.cpp +++ b/src/Thread/ThreadMgrEventSink.cpp @@ -42,22 +42,39 @@ STDAPI CMetasequoiaIME::OnUninitDocumentMgr(_In_ ITfDocumentMgr *pDocMgr) // focus document, or now no document holds the input focus. //---------------------------------------------------------------------------- -STDAPI CMetasequoiaIME::OnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus, _In_ ITfDocumentMgr *pDocMgrPrevFocus) -{ - pDocMgrPrevFocus; - - if (pDocMgrFocus && !Global::g_connected) - { - Global::g_connected = true; - PostMessage(_msgWndHandle, WM_ConnectNamedpipe, 0, 0); - } - else if (!pDocMgrFocus && Global::g_connected) - { - Global::g_connected = false; - PostMessage(_msgWndHandle, WM_DisconnectNamedpipe, 0, 0); - } - - _InitTextEditSink(pDocMgrFocus); +STDAPI CMetasequoiaIME::OnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus, _In_ ITfDocumentMgr *pDocMgrPrevFocus) +{ + if (!IsNamedpipeFocusStateOwner(this)) + { + return S_OK; + } + pDocMgrPrevFocus; + + const bool windowsTextInputHostTransition = + _focusLostToWindowsTextInputHost || + _CaptureWindowsTextInputHostFocusLoss(); + + // bcaf34e used document focus as the authoritative reconnect boundary. + // The modern protocol preserves that behavior with a new activation epoch + // on the existing healthy transport; EnsureNamedpipeFocusSessionActivated + // physically reopens a channel as well when its health check fails. + if (pDocMgrFocus && + (!Global::g_connected || windowsTextInputHostTransition)) + { + Global::g_connected = true; + _workerCommitReady.store(false, std::memory_order_release); + RequireNamedpipeFocusActivation(); + PostMessage(_msgWndHandle, WM_ConnectNamedpipe, 0, 0); + _focusLostToWindowsTextInputHost = false; + } + else if (!pDocMgrFocus && Global::g_connected) + { + MarkNamedpipeFocusLost(); + Global::g_connected = false; + PostMessage(_msgWndHandle, WM_DisconnectNamedpipe, 0, 0); + } + + _InitTextEditSink(pDocMgrFocus); _UpdateLanguageBarOnSetFocus(pDocMgrFocus); @@ -92,13 +109,13 @@ STDAPI CMetasequoiaIME::OnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus, _In_ ITfDo _pDocMgrLastFocused = pDocMgrFocus; - if (_pDocMgrLastFocused) - { - _pDocMgrLastFocused->AddRef(); - } - - return S_OK; -} + if (_pDocMgrLastFocused) + { + _pDocMgrLastFocused->AddRef(); + } + + return S_OK; +} //+--------------------------------------------------------------------------- // @@ -107,11 +124,10 @@ STDAPI CMetasequoiaIME::OnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus, _In_ ITfDo // Sink called by the framework when a context is pushed. //---------------------------------------------------------------------------- -STDAPI CMetasequoiaIME::OnPushContext(_In_ ITfContext *pContext) -{ - pContext; - - return E_NOTIMPL; +STDAPI CMetasequoiaIME::OnPushContext(_In_ ITfContext *pContext) +{ + _HandleFocusedContextStackChange(pContext); + return S_OK; } //+--------------------------------------------------------------------------- @@ -121,12 +137,78 @@ STDAPI CMetasequoiaIME::OnPushContext(_In_ ITfContext *pContext) // Sink called by the framework when a context is popped. //---------------------------------------------------------------------------- -STDAPI CMetasequoiaIME::OnPopContext(_In_ ITfContext *pContext) -{ - pContext; - - return E_NOTIMPL; -} +STDAPI CMetasequoiaIME::OnPopContext(_In_ ITfContext *pContext) +{ + _HandleFocusedContextStackChange(pContext); + return S_OK; +} + +void CMetasequoiaIME::_HandleFocusedContextStackChange(_In_opt_ ITfContext *changedContext) +{ + if (!IsNamedpipeFocusStateOwner(this) || changedContext == nullptr || + _pThreadMgr == nullptr) + { + return; + } + + ITfDocumentMgr *changedDocument = nullptr; + ITfDocumentMgr *focusedDocument = nullptr; + if (FAILED(changedContext->GetDocumentMgr(&changedDocument)) || + changedDocument == nullptr || + FAILED(_pThreadMgr->GetFocus(&focusedDocument)) || + focusedDocument == nullptr || changedDocument != focusedDocument) + { + if (changedDocument) + { + changedDocument->Release(); + } + if (focusedDocument) + { + focusedDocument->Release(); + } + return; + } + + ITfContext *newTopContext = nullptr; + if (FAILED(focusedDocument->GetTop(&newTopContext)) || newTopContext == nullptr) + { + focusedDocument->Release(); + changedDocument->Release(); + return; + } + const bool topContextChanged = newTopContext != _pTextEditSinkContext; + newTopContext->Release(); + if (!topContextChanged) + { + focusedDocument->Release(); + changedDocument->Release(); + return; + } + + // Capture the old composition owner before rebinding the text-edit sink. + // A pushed modal/context (or a popped editor context) must never inherit + // replies or queued edit sessions that belonged to the previous top. + ITfContext *resetContext = _pContext ? _pContext : _pTextEditSinkContext; + if (resetContext) + { + resetContext->AddRef(); + } + else + { + resetContext = changedContext; + resetContext->AddRef(); + } + + _ClearDeferredKeyDowns(); + MarkNamedpipeSessionDirtyForOwner(this); + const UINT resetToken = _localSessionResetToken.load(std::memory_order_acquire); + _RequestLocalSessionReset(resetContext, resetToken); + _InitTextEditSink(focusedDocument); + + resetContext->Release(); + focusedDocument->Release(); + changedDocument->Release(); +} //+--------------------------------------------------------------------------- // diff --git a/src/UI/CandidateListUIPresenter.cpp b/src/UI/CandidateListUIPresenter.cpp index adb15823..83e9f1ef 100644 --- a/src/UI/CandidateListUIPresenter.cpp +++ b/src/UI/CandidateListUIPresenter.cpp @@ -26,21 +26,15 @@ const int MOVEDOWN_ONE = 1; const int MOVETO_TOP = 0; const int MOVETO_BOTTOM = -1; -namespace -{ -bool IsTimeoutSentinelCandidate(UINT msgType, const std::wstring &candidateString) -{ - return msgType == Global::DataFromServerMsgType::Normal && candidateString == L"T"; -} -} - //+--------------------------------------------------------------------------- // // _HandleCandidateFinalize // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfContext *pContext) +HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfContext *pContext, + uint64_t requestId, + const std::wstring &prefetchedText) { HRESULT hr = S_OK; PerfTimer finalizeTimer; @@ -49,7 +43,7 @@ HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfConte DWORD_PTR keystrokeBufLen = keyStrokebuffer.GetLength(); DWORD_PTR candidateLen = keystrokeBufLen; CStringRange candidateString(keyStrokebuffer); - std::wstring pendingCommitCandidate = _TakePendingCommitCandidate(); + const std::wstring &pendingCommitCandidate = prefetchedText; // _pCandidateListUIPresenter would be null in uwp/metro apps @@ -62,7 +56,6 @@ HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfConte { if (!pendingCommitCandidate.empty()) { - ClearNamedpipeDataIfExists(true); candidateString.Set(pendingCommitCandidate.c_str(), pendingCommitCandidate.length()); PerfTimer insertTextTimer; hr = _InsertTextToComposition(ec, pContext, &candidateString); @@ -84,29 +77,21 @@ HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfConte std::wstring serverCandidateString; const bool hasPrefetchedServerCandidate = _TakePendingServerCandidate(&serverMsgType, &serverCandidateString); - if (hasPrefetchedServerCandidate) - { - if (IsTimeoutSentinelCandidate(serverMsgType, serverCandidateString)) - { - struct FanyImeNamedpipeDataToTsf *retryData = TryReadDataFromServerPipeWithTimeout(); - serverMsgType = retryData->msg_type; - serverCandidateString = retryData->candidate_string; - } - } - else + if (!hasPrefetchedServerCandidate) { PerfTimer pipeReadTimer; - struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(); + struct FanyImeNamedpipeDataToTsf *receivedData = TryReadDataFromServerPipeWithTimeout(requestId); serverMsgType = receivedData->msg_type; serverCandidateString = receivedData->candidate_string; - if (IsTimeoutSentinelCandidate(serverMsgType, serverCandidateString)) - { - struct FanyImeNamedpipeDataToTsf *retryData = TryReadDataFromServerPipeWithTimeout(); - serverMsgType = retryData->msg_type; - serverCandidateString = retryData->candidate_string; - } } + if (serverMsgType == Global::DataFromServerMsgType::TransportUnavailable) + { + // Transport state is never candidate text. Propagate failure so + // the exact deferred key remains owned and is replayed only after + // the replacement focus/session fence is ready. + return HRESULT_FROM_WIN32(ERROR_BROKEN_PIPE); + } if (serverMsgType == Global::DataFromServerMsgType::OutofRange) // Candidate index out of range { return hr; @@ -156,7 +141,8 @@ HRESULT CMetasequoiaIME::_HandleCandidateFinalize(TfEditCookie ec, _In_ ITfConte if (pCompositionProcessorEngine->GetVirtualKeyLength()) { - _HandleCompositionInputWorker(pCompositionProcessorEngine, ec, pContext); + _HandleCompositionInputWorker(pCompositionProcessorEngine, ec, pContext, + FANY_IME_NO_REQUEST_ID); } else { @@ -218,9 +204,11 @@ NoPresenter: // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCandidateConvert(TfEditCookie ec, _In_ ITfContext *pContext) +HRESULT CMetasequoiaIME::_HandleCandidateConvert(TfEditCookie ec, _In_ ITfContext *pContext, + uint64_t requestId, + const std::wstring &prefetchedText) { - return _HandleCandidateWorker(ec, pContext); + return _HandleCandidateWorker(ec, pContext, requestId, prefetchedText); } //+--------------------------------------------------------------------------- @@ -229,7 +217,9 @@ HRESULT CMetasequoiaIME::_HandleCandidateConvert(TfEditCookie ec, _In_ ITfContex // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCandidateWorker(TfEditCookie ec, _In_ ITfContext *pContext) +HRESULT CMetasequoiaIME::_HandleCandidateWorker(TfEditCookie ec, _In_ ITfContext *pContext, + uint64_t requestId, + const std::wstring &prefetchedText) { HRESULT hrReturn = E_FAIL; DWORD_PTR candidateLen = 0; @@ -250,7 +240,7 @@ HRESULT CMetasequoiaIME::_HandleCandidateWorker(TfEditCookie ec, _In_ ITfContext } candidateString.Set(pCandidateString, candidateLen); - hrReturn = _HandleCandidateFinalize(ec, pContext); + hrReturn = _HandleCandidateFinalize(ec, pContext, requestId, prefetchedText); Exit: return hrReturn; @@ -296,7 +286,9 @@ HRESULT CMetasequoiaIME::_HandleCandidateArrowKey( // // //---------------------------------------------------------------------------- -HRESULT CMetasequoiaIME::_HandleCandidateSelectByNumber(TfEditCookie ec, _In_ ITfContext *pContext, _In_ UINT uCode) +HRESULT CMetasequoiaIME::_HandleCandidateSelectByNumber(TfEditCookie ec, _In_ ITfContext *pContext, _In_ UINT uCode, + uint64_t requestId, + const std::wstring &prefetchedText) { int iSelectAsNumber = _pCompositionProcessorEngine->GetCandidateListIndexRange()->GetIndex(uCode); if (iSelectAsNumber == -1) @@ -308,7 +300,7 @@ HRESULT CMetasequoiaIME::_HandleCandidateSelectByNumber(TfEditCookie ec, _In_ IT { if (_pCandidateListUIPresenter->_SetSelectionInPage(iSelectAsNumber)) { - return _HandleCandidateConvert(ec, pContext); + return _HandleCandidateConvert(ec, pContext, requestId, prefetchedText); } } @@ -802,6 +794,10 @@ void CCandidateListUIPresenter::_EndCandidateList() void CCandidateListUIPresenter::_PrepareForAsyncCleanup() { _asyncCleanupPending = TRUE; + // Hide and clear the exact Server-side candidate session now. Deferring + // this until the presenter destructor lets an old cleanup message race a + // newly started composition and clear its candidates. + EndCandidateUiSession(); } void CCandidateListUIPresenter::_NotifyUI() @@ -1095,7 +1091,10 @@ HRESULT CCandidateListUIPresenter::_CandidateChangeNotification(_In_ enum CANDWN } CKeyHandlerEditSession *pEditSession = - new (std::nothrow) CKeyHandlerEditSession(_pTextService, pContext, 0, 0, KeyState); + new (std::nothrow) CKeyHandlerEditSession(_pTextService, pContext, 0, 0, KeyState, + FANY_IME_NO_REQUEST_ID, {}, {}, + _pTextService->_CaptureFocusSessionToken(), 0, + _pTextService->_CaptureCompositionEpoch()); if (nullptr != pEditSession) { HRESULT hrSession = S_OK; @@ -1279,7 +1278,7 @@ void CCandidateListUIPresenter::WriteCandidateUiPayload(_In_ UINT writeFlag) Global::ModifiersDown, // Global::Point, // Global::PinyinLength, // - Global::PinyinString, // + pinyinString, // writeFlag // ); }