Files
msime-windows/src/IPC/Ipc.cpp
T

1759 lines
56 KiB
C++

#include "Ipc.h"
#include <algorithm>
#include <cstring>
#include <debugapi.h>
#include <deque>
#include <handleapi.h>
#include <map>
#include <mutex>
#include <minwindef.h>
#include <utility>
#include <winnt.h>
#include <winuser.h>
#include "Globals.h"
#include "FanyDefines.h"
#include "MetasequoiaIME.h"
#include <fmt/xchar.h>
#include "../Utils/PerfTimer.h"
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 thread_local HANDLE hPipe = nullptr;
static thread_local HANDLE hFromServerPipe = nullptr;
static thread_local HANDLE hToTsfWorkerThreadPipe = nullptr;
static thread_local FanyImeNamedpipeData namedpipeData = {};
static thread_local FanyImeNamedpipeDataToTsf namedpipeDataFromServer = {};
static thread_local FanyImeNamedpipeDataToTsf transportUnavailableReply = {};
static thread_local std::map<uint64_t, FanyImeNamedpipeDataToTsf> pendingReplies;
static thread_local std::deque<FanyImeNamedpipeDataToTsf> 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<uint64_t> *boundExpectedWorkerFocusToken = nullptr;
static thread_local std::atomic<bool> *boundLocalSessionResetPending = nullptr;
static thread_local std::atomic<UINT> *boundLocalSessionResetToken = nullptr;
static thread_local std::atomic<bool> *boundWorkerCommitReady = nullptr;
static thread_local std::atomic<uint64_t> *boundAcknowledgedWorkerFocusToken = nullptr;
static thread_local std::atomic<HANDLE> *boundWorkerPipeHandle = nullptr;
static thread_local std::atomic<UINT> *boundWorkerPipeGeneration = nullptr;
/* Data size transfered from Server process */
static const int ServerDtPipeDataSize = 512;
namespace
{
constexpr size_t MaxPendingReplyCount = 64;
constexpr size_t MaxDiagnosticRecordCount = 256;
constexpr size_t MaxDiagnosticQueuedUnits = 32 * 1024;
constexpr DWORD DiagnosticFlushDelayMs = 250;
std::mutex diagnosticLogMutex;
std::deque<std::wstring> diagnosticLogRecords;
size_t diagnosticLogQueuedUnits = 0;
uint32_t diagnosticLogDroppedCount = 0;
bool diagnosticFlushScheduled = false;
void CALLBACK FlushTsfDiagnosticLogs(PTP_CALLBACK_INSTANCE, PVOID);
void ScheduleDiagnosticFlushLocked()
{
if (diagnosticFlushScheduled)
{
return;
}
diagnosticFlushScheduled = true;
DllAddRef();
if (!TrySubmitThreadpoolCallback(FlushTsfDiagnosticLogs, nullptr, nullptr))
{
diagnosticFlushScheduled = false;
DllRelease();
}
}
bool SendDiagnosticBatch(const FanyImeTsfDiagnosticBatchHeader &header, const std::wstring &payload)
{
HANDLE pipe = INVALID_HANDLE_VALUE;
for (int attempt = 0; attempt < 2; ++attempt)
{
pipe = CreateFileW(FANY_IME_TSF_DIAGNOSTIC_NAMED_PIPE, GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe != INVALID_HANDLE_VALUE)
{
break;
}
if (GetLastError() != ERROR_PIPE_BUSY || !WaitNamedPipeW(FANY_IME_TSF_DIAGNOSTIC_NAMED_PIPE, 20))
{
break;
}
}
if (pipe == INVALID_HANDLE_VALUE)
{
return false;
}
const size_t payloadBytes = payload.size() * sizeof(wchar_t);
std::vector<unsigned char> frame(sizeof(header) + payloadBytes);
memcpy(frame.data(), &header, sizeof(header));
if (payloadBytes != 0)
{
memcpy(frame.data() + sizeof(header), payload.data(), payloadBytes);
}
DWORD bytesWritten = 0;
const BOOL writeResult = WriteFile(pipe, frame.data(), static_cast<DWORD>(frame.size()), &bytesWritten, nullptr);
CloseHandle(pipe);
return writeResult && bytesWritten == frame.size();
}
void CALLBACK FlushTsfDiagnosticLogs(PTP_CALLBACK_INSTANCE, PVOID)
{
Sleep(DiagnosticFlushDelayMs);
FanyImeTsfDiagnosticBatchHeader header;
std::vector<std::wstring> batchRecords;
batchRecords.reserve(MaxDiagnosticRecordCount);
size_t batchUnits = 0;
std::wstring payload;
bool loggingDisabled = false;
{
std::lock_guard lock(diagnosticLogMutex);
if (!Global::TsfDiagnosticLogEnabled.load(std::memory_order_relaxed))
{
diagnosticLogRecords.clear();
diagnosticLogQueuedUnits = 0;
diagnosticLogDroppedCount = 0;
diagnosticFlushScheduled = false;
loggingDisabled = true;
}
else
{
const size_t maxPayloadUnits =
(FANY_IME_TSF_DIAGNOSTIC_MAX_FRAME_BYTES - sizeof(FanyImeTsfDiagnosticBatchHeader)) /
sizeof(wchar_t);
while (!diagnosticLogRecords.empty())
{
const std::wstring &record = diagnosticLogRecords.front();
if (!batchRecords.empty() && batchUnits + record.size() > maxPayloadUnits)
{
break;
}
batchUnits += (std::min)(record.size(), maxPayloadUnits - batchUnits);
diagnosticLogQueuedUnits -= record.size();
batchRecords.push_back(std::move(diagnosticLogRecords.front()));
diagnosticLogRecords.pop_front();
++header.record_count;
if (batchUnits == maxPayloadUnits)
{
break;
}
}
header.dropped_count = diagnosticLogDroppedCount;
header.source_process_id = GetCurrentProcessId();
diagnosticLogDroppedCount = 0;
}
}
if (loggingDisabled)
{
DllRelease();
return;
}
payload.reserve(batchUnits);
for (const std::wstring &record : batchRecords)
{
payload.append(record.data(), (std::min)(record.size(), batchUnits - payload.size()));
}
header.payload_bytes = static_cast<uint32_t>(payload.size() * sizeof(wchar_t));
const bool sent = header.record_count != 0 && SendDiagnosticBatch(header, payload);
{
std::lock_guard lock(diagnosticLogMutex);
if (!sent)
{
diagnosticLogDroppedCount += header.record_count;
}
diagnosticFlushScheduled = false;
if (!diagnosticLogRecords.empty())
{
ScheduleDiagnosticFlushLocked();
}
}
DllRelease();
}
// 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<UINT> nextLocalSessionResetToken{0};
std::atomic<UINT> 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<UINT> &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;
}
double GetElapsedMilliseconds(const LARGE_INTEGER &startCounter, const LARGE_INTEGER &endCounter,
const LARGE_INTEGER &frequency)
{
return static_cast<double>(endCounter.QuadPart - startCounter.QuadPart) * 1000.0 /
static_cast<double>(frequency.QuadPart);
}
uint64_t GetPipeClientId()
{
thread_local const uint64_t clientId =
(static_cast<uint64_t>(GetCurrentProcessId()) << 32) | static_cast<uint64_t>(GetCurrentThreadId());
return clientId;
}
void ClosePipeHandleIfValid(HANDLE &hPipeHandle)
{
if (IsValidPipeHandle(hPipeHandle))
{
CloseHandle(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::UiLessComposition)
{
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<DWORD>(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;
if (pipeRole == FanyImePipeRole::Main)
{
FanyImeNamedpipeData hello = {};
hello.event_type = FanyImePipeEventType::ClientHello;
hello.client_id = GetPipeClientId();
BOOL ret = WriteFile(hPipeHandle, &hello, sizeof(hello), &bytesWritten, NULL);
return ret && bytesWritten == sizeof(hello);
}
FanyImePipeHello hello = {};
hello.client_id = GetPipeClientId();
hello.pipe_role = pipeRole;
return WriteOverlappedWithTimeout(hPipeHandle, &hello, sizeof(hello), bytesWritten);
}
bool TryOpenClientPipe(HANDLE &hPipeHandle, const wchar_t *pipeName, UINT pipeRole)
{
if (IsValidPipeHandle(hPipeHandle))
{
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, //
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<uint64_t> *expectedWorkerFocusToken,
std::atomic<bool> *localSessionResetPending,
std::atomic<UINT> *localSessionResetToken,
std::atomic<bool> *workerCommitReady,
std::atomic<uint64_t> *acknowledgedWorkerFocusToken,
std::atomic<HANDLE> *workerPipeHandle,
std::atomic<UINT> *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<WPARAM>(resetToken), 0) != FALSE;
if (!resetDelivered && GetWindowThreadProcessId(ownerWindow, nullptr) == GetCurrentThreadId())
{
SendMessage(ownerWindow, WM_IpcSessionDirty, static_cast<WPARAM>(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
//
hMapFile = OpenFileMappingW( //
FILE_MAP_ALL_ACCESS, //
FALSE, //
FANY_IME_SHARED_MEMORY //
);
//
// Shared memory is not available, try to use namedpipe
//
InitNamedpipe();
if (!hMapFile)
{
// Error handling
canUseSharedMemory = false;
// TODO: Log error
return 0;
}
pBuf = MapViewOfFile( //
hMapFile, //
FILE_MAP_ALL_ACCESS, //
0, //
0, //
BUFFER_SIZE //
); //
if (!pBuf)
{
CloseHandle(hMapFile);
hMapFile = nullptr;
sharedData = nullptr;
canUseSharedMemory = false;
return 0;
}
sharedData = static_cast<FanyImeSharedMemoryData *>(pBuf);
return 0;
}
bool TryReadCandidatePageFromSharedMemory(std::wstring *candidatePage)
{
if (candidatePage == nullptr || sharedData == nullptr)
{
return false;
}
candidatePage->assign(sharedData->candidate_string);
return !candidatePage->empty();
}
int InitNamedpipe()
{
return ConnectToAllNamedpipe();
}
int ConnectToAllNamedpipe()
{
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;
}
// 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()
{
return TryOpenClientPipe(hFromServerPipe, FANY_IME_TO_TSF_NAMED_PIPE, FanyImePipeRole::ToTsf) ? 1 : 0;
}
int CloseIpc()
{
//
// Namedpipe
//
CloseNamedpipe();
const int result = canUseSharedMemory ? 0 : -1;
//
// Shared memory
//
if (pBuf)
{
UnmapViewOfFile(pBuf);
pBuf = nullptr;
}
sharedData = nullptr;
if (hMapFile)
{
CloseHandle(hMapFile);
hMapFile = nullptr;
}
canUseSharedMemory = false;
//
// Events
//
for (const auto &eventName : FANY_IME_EVENT_ARRAY)
{
HANDLE hEvent = OpenEventW( //
EVENT_ALL_ACCESS, //
FALSE, //
eventName.c_str() //
); //
if (hEvent)
{
CloseHandle(hEvent);
}
}
return result;
}
int CloseNamedpipe()
{
ClosePipeHandleIfValid(hPipe);
ClosePipeHandleIfValid(hFromServerPipe);
ClosePipeHandleIfValid(hToTsfWorkerThreadPipe);
ResetNamedpipeReplyState();
return 0;
}
void ResetNamedpipeReplyState()
{
pendingReplies.clear();
unsolicitedReplies.clear();
}
HANDLE GetToTsfWorkerThreadNamedpipe()
{
return hToTsfWorkerThreadPipe;
}
int WriteDataToSharedMemory( //
UINT keycode, //
WCHAR wch, //
UINT modifiers_down, //
const int point[2], //
int pinyin_length, //
const std::wstring &pinyin_string, //
UINT write_flag //
)
{
// 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);
}
/**
* @brief
*
* @param keycode
* @param modifiers_down
* @param point
* @param pinyin_length
* @param pinyin_string
* @param write_flag
* @return int
*/
int WriteDataToNamedPipe( //
UINT keycode, //
WCHAR wch, //
UINT modifiers_down, //
const int point[2], //
int pinyin_length, //
const std::wstring &pinyin_string, //
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;
}
if (write_flag >> 1 & 1u)
{
namedpipeData.wch = wch;
}
if (write_flag >> 2 & 1u)
{
namedpipeData.modifiers_down = modifiers_down;
}
if (write_flag >> 3 & 1u)
{
namedpipeData.point[0] = point[0];
namedpipeData.point[1] = point[1];
}
if (write_flag >> 4 & 1u)
{
namedpipeData.pinyin_length = max(0, min(pinyin_length, 127));
}
if (write_flag >> 5 & 1u)
{
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<int>(copyLength);
}
return 0;
}
KeyEventSendResult SendKeyEventToUIProcess(uint64_t *requestId)
{
return SendKeyEventToUIProcessViaNamedPipe(requestId);
}
void DebugTsfKeyLatency(const wchar_t *stage, uint64_t requestId, double elapsedMs, HRESULT result)
{
constexpr double kSlowStageThresholdMs = 8.0;
if (!Global::TsfDiagnosticLogEnabled.load(std::memory_order_relaxed) ||
(elapsedMs < kSlowStageThresholdMs && SUCCEEDED(result)))
{
return;
}
const std::wstring message = fmt::format(
L"[msime][key-latency] side=tsf stage={} request={} elapsed_ms={:.3f} result=0x{:08X} process={}\n",
stage, requestId, elapsedMs, static_cast<unsigned long>(result),
Global::current_process_name.empty() ? L"unknown" : Global::current_process_name);
QueueTsfDiagnosticLog(message);
}
void QueueTsfDiagnosticLog(const std::wstring &line)
{
if (!Global::TsfDiagnosticLogEnabled.load(std::memory_order_relaxed))
{
return;
}
std::wstring sanitized = line;
for (wchar_t &ch : sanitized)
{
if (ch == L'\r' || ch == L'\n')
{
ch = L' ';
}
}
while (!sanitized.empty() && sanitized.back() == L' ')
{
sanitized.pop_back();
}
constexpr size_t MaxSingleRecordUnits = 1024;
if (sanitized.size() > MaxSingleRecordUnits)
{
sanitized.resize(MaxSingleRecordUnits);
}
SYSTEMTIME sourceTime{};
GetLocalTime(&sourceTime);
sanitized = fmt::format(
L"[tsf source={:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03} p{}:t{} tick={}] {}\n",
sourceTime.wYear, sourceTime.wMonth, sourceTime.wDay, sourceTime.wHour, sourceTime.wMinute,
sourceTime.wSecond, sourceTime.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount64(),
sanitized);
std::lock_guard lock(diagnosticLogMutex);
while (!diagnosticLogRecords.empty() &&
(diagnosticLogRecords.size() >= MaxDiagnosticRecordCount ||
diagnosticLogQueuedUnits + sanitized.size() > MaxDiagnosticQueuedUnits))
{
diagnosticLogQueuedUnits -= diagnosticLogRecords.front().size();
diagnosticLogRecords.pop_front();
++diagnosticLogDroppedCount;
}
diagnosticLogQueuedUnits += sanitized.size();
diagnosticLogRecords.push_back(std::move(sanitized));
ScheduleDiagnosticFlushLocked();
}
int SendHideCandidateWndEventToUIProcess()
{
return SendHideCandidateWndEventToUIProcessViaNamedPipe();
}
int SendShowCandidateWndEventToUIProcess()
{
return SendShowCandidateWndEventToUIProcessViaNamedPipe();
}
int SendMoveCandidateWndEventToUIProcess()
{
return SendMoveCandidateWndEventToUIProcessViaNamedPipe();
}
int SendLangbarRightClickEventToUIProcess(const RECT *prcArea)
{
return SendLangbarRightClickEventToUIProcessViaNamedPipe(prcArea);
}
//
// Named pipe
//
bool SendToNamedpipe(bool *deliveryAmbiguous = nullptr)
{
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;
// Tell Server never to raise an IME HWND for UILess hosts (games / fullscreen).
if (Global::IsUiLessMode() &&
(packet.event_type == FanyImePipeEventType::KeyEvent ||
packet.event_type == FanyImePipeEventType::ShowCandidateWnd ||
packet.event_type == FanyImePipeEventType::MoveCandidateWnd ||
packet.event_type == FanyImePipeEventType::HideCandidateWnd))
{
packet.modifiers_down |= FanyImePipeFlags::UiLess;
}
if (packet.event_type == FanyImePipeEventType::ClientActivated && Global::HostUiLessMode)
{
packet.keycode = 1;
}
// 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))
{
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;
if (WriteFile(hPipe, &packet, sizeof(packet), &bytesWritten, nullptr) &&
bytesWritten == sizeof(packet))
{
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;
}
/**
* @brief Clear namedpipe data if exists, cause sometimes there may be some useless data sent by last key event from
* server
*
*/
void ClearNamedpipeDataIfExists(bool force)
{
// 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);
}
}
/**
* @brief Try to read selected candiate string data from server pipe with timeout
*
* @return struct FanyImeNamedpipeDataToTsf*
*/
struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout(uint64_t expectedRequestId)
{
return TryReadDataFromServerPipeWithTimeout(expectedRequestId, true);
}
struct FanyImeNamedpipeDataToTsf *TryReadDataFromServerPipeWithTimeout(uint64_t expectedRequestId,
bool abortTransportOnTimeout)
{
constexpr int timeoutMs = 50;
PerfTimer replyWaitTimer;
auto transportUnavailable = []() {
transportUnavailableReply = {};
transportUnavailableReply.msg_type = Global::DataFromServerMsgType::TransportUnavailable;
return &transportUnavailableReply;
};
auto softMiss = [](uint64_t requestId) {
// Deliberately not TransportUnavailable: preedit waiters fall back to
// the local reading string without tearing down the reply pipe.
namedpipeDataFromServer = {};
namedpipeDataFromServer.msg_type = Global::DataFromServerMsgType::Normal;
namedpipeDataFromServer.request_id = requestId;
return &namedpipeDataFromServer;
};
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))
{
RequestNamedpipeReconnect();
return transportUnavailable();
}
}
const ULONGLONG deadline = GetTickCount64() + timeoutMs;
while (true)
{
DWORD bytesRead = 0;
namedpipeDataFromServer = {};
const ULONGLONG now = GetTickCount64();
const DWORD remainingMs = now >= deadline ? 0 : static_cast<DWORD>(deadline - now);
const OverlappedReadResult readResult = ReadOverlappedWithTimeout(
hFromServerPipe, &namedpipeDataFromServer, sizeof(namedpipeDataFromServer), remainingMs, bytesRead);
if (readResult == OverlappedReadResult::TimedOut)
{
DebugTsfKeyLatency(L"reply-wait-timeout", expectedRequestId, replyWaitTimer.ElapsedMs(),
HRESULT_FROM_WIN32(ERROR_TIMEOUT));
if (!abortTransportOnTimeout)
{
return softMiss(expectedRequestId);
}
ClosePipeHandleIfValid(hFromServerPipe);
RequestNamedpipeReconnect();
return transportUnavailable();
}
if (readResult == OverlappedReadResult::Failed)
{
DebugTsfKeyLatency(L"reply-read-failed", expectedRequestId, replyWaitTimer.ElapsedMs(), E_FAIL);
ClosePipeHandleIfValid(hFromServerPipe);
pendingReplies.clear();
unsolicitedReplies.clear();
RequestNamedpipeReconnect();
return transportUnavailable();
}
if (bytesRead != sizeof(namedpipeDataFromServer))
{
DebugTsfKeyLatency(L"reply-size-invalid", expectedRequestId, replyWaitTimer.ElapsedMs(), E_FAIL);
ClosePipeHandleIfValid(hFromServerPipe);
pendingReplies.clear();
unsolicitedReplies.clear();
RequestNamedpipeReconnect();
return transportUnavailable();
}
if (!IsValidServerReply(namedpipeDataFromServer))
{
DebugTsfKeyLatency(L"reply-frame-invalid", expectedRequestId, replyWaitTimer.ElapsedMs(), E_FAIL);
ClosePipeHandleIfValid(hFromServerPipe);
pendingReplies.clear();
unsolicitedReplies.clear();
RequestNamedpipeReconnect();
return transportUnavailable();
}
// 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)
{
DebugTsfKeyLatency(L"reply-wait", expectedRequestId, replyWaitTimer.ElapsedMs(), S_OK);
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;
}
}
// 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.
if (!abortTransportOnTimeout)
{
DebugTsfKeyLatency(L"reply-soft-miss", expectedRequestId, replyWaitTimer.ElapsedMs(), S_FALSE);
return softMiss(expectedRequestId);
}
DebugTsfKeyLatency(L"reply-id-timeout", expectedRequestId, replyWaitTimer.ElapsedMs(),
HRESULT_FROM_WIN32(ERROR_TIMEOUT));
ClosePipeHandleIfValid(hFromServerPipe);
RequestNamedpipeReconnect();
return transportUnavailable();
}
/**
* @brief Read data sent by server
*
* TODO: Cancel when time exceed, we should set a timeout
*
* @return struct FanyImeNamedpipeDataToTsf*
*/
struct FanyImeNamedpipeDataToTsf *ReadDataFromServerViaNamedPipe(uint64_t expectedRequestId)
{
return TryReadDataFromServerPipeWithTimeout(expectedRequestId);
}
/**
* @brief 即用即断,不要求一直连接,但是,要让 Server 端的管道一直在 connect 状态阻塞住等待连接
*
* @param pipeData
*/
bool SendToAuxNamedpipe(const std::wstring &pipeData,
bool waitForAcknowledgement)
{
HANDLE hAuxPipe = INVALID_HANDLE_VALUE;
// 重试几次,等待 Server 准备好
for (int retry = 0; retry < 5; ++retry)
{
hAuxPipe = CreateFileW( //
FANY_IME_AUX_NAMED_PIPE, //
GENERIC_READ | GENERIC_WRITE, //
0, //
nullptr, //
OPEN_EXISTING, //
0, //
nullptr //
);
if (hAuxPipe && hAuxPipe != INVALID_HANDLE_VALUE)
{
break;
}
/* 为了解决 Windows Media Player 会在启动时调用两次 kill 并且第二次失败的问题
* TODO: 在 msg wnd proc 中去处理打开程序会调用两次 kill 的问题,将其优化成一次
*/
Sleep(20); // 等待 20ms 再重试
}
if (!hAuxPipe || hAuxPipe == INVALID_HANDLE_VALUE)
{
return false;
}
DWORD bytesWritten = 0;
BOOL ret = WriteFile( //
hAuxPipe, //
pipeData.c_str(), //
pipeData.length() * sizeof(wchar_t), //
&bytesWritten, //
NULL //
);
const bool sent =
ret && bytesWritten == pipeData.length() * sizeof(wchar_t);
if (!sent || !waitForAcknowledgement)
{
CloseHandle(hAuxPipe);
return sent;
}
DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_NOWAIT;
if (!SetNamedPipeHandleState(hAuxPipe, &pipeMode, nullptr, nullptr))
{
CloseHandle(hAuxPipe);
return false;
}
bool acknowledged = false;
const ULONGLONG deadline = GetTickCount64() + 150;
while (GetTickCount64() < deadline)
{
wchar_t acknowledgement[2] = {};
DWORD bytesRead = 0;
if (ReadFile(hAuxPipe, acknowledgement, sizeof(acknowledgement),
&bytesRead, nullptr))
{
acknowledged = bytesRead == sizeof(acknowledgement) &&
acknowledgement[0] == L'O' &&
acknowledgement[1] == L'K';
break;
}
if (GetLastError() != ERROR_NO_DATA)
{
break;
}
Sleep(1);
}
CloseHandle(hAuxPipe);
return acknowledged;
}
/**
* event_type
* 0: FanyImeKeyEvent
* 1: FanyHideCandidateWndEvent
* 2: FanyShowCandidateWndEvent
* 3: FanyMoveCandidateWndEvent
*/
KeyEventSendResult SendKeyEventToUIProcessViaNamedPipe(uint64_t *requestId)
{
if (requestId)
{
*requestId = FANY_IME_NO_REQUEST_ID;
}
// 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();
return 0;
}
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();
return 0;
}
int SendMoveCandidateWndEventToUIProcessViaNamedPipe()
{
// The caller has already staged the new caret point in namedpipeData.
namedpipeData.request_id = 0;
namedpipeData.event_type = FanyImePipeEventType::MoveCandidateWnd;
SendToNamedpipe();
return 0;
}
int SendLangbarRightClickEventToUIProcessViaNamedPipe(const RECT *prcArea)
{
// Menu is global UI: send on the session-less Aux pipe so a suspended /
// unfocused TIP can still ask Server to show it. Do not use Main / focus
// session activation — that path is intentionally down after focus loss.
if (!prcArea)
{
return -1;
}
SendToAuxNamedpipe(fmt::format(L"LangbarRightClick|{}|{}|{}|{}", prcArea->left, prcArea->top, prcArea->right,
prcArea->bottom));
return 0;
}
int SendIMEActivationEventToUIProcessViaNamedPipe()
{
SendToAuxNamedpipe(L"IMEActivation");
return 0;
}
int SendClientActivatedEventToServerViaNamedPipe(uint64_t focusToken)
{
if (focusToken == 0)
{
return -1;
}
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::ClientActivated;
namedpipeData.request_id = focusToken;
return SendToNamedpipe() ? 0 : -1;
}
int SendClientDeactivatedEventToServerViaNamedPipe(uint64_t focusToken)
{
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::ClientDeactivated;
namedpipeData.request_id = focusToken;
return SendToNamedpipe() ? 0 : -1;
}
int SendClientSuspendedEventToServerViaNamedPipe()
{
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::ClientSuspended;
const bool sent = SendToNamedpipe();
return sent ? 0 : -1;
}
bool FlushNamedpipeFocusSessionReset()
{
if (!boundFocusResetPending || !*boundFocusResetPending)
{
return true;
}
if (SendClientSuspendedEventToServerViaNamedPipe() != 0)
{
return false;
}
*boundFocusResetPending = false;
return true;
}
bool FlushNamedpipeImeDeactivation(uint64_t focusToken)
{
if (focusToken == 0 && boundExpectedWorkerFocusToken)
{
focusToken =
boundExpectedWorkerFocusToken->load(std::memory_order_acquire);
}
if (SendClientDeactivatedEventToServerViaNamedPipe(focusToken) != 0)
{
// Deactivate is about to destroy the message window and all persistent
// pipes, so its normal reconnect path cannot repair this final write.
// Use a PID-validated, focus-token-checked Aux request and wait briefly
// for the Server to confirm that it fenced the exact old session.
if (focusToken == 0 ||
!SendToAuxNamedpipe(
fmt::format(L"TerminalDeactivation|{}|{}",
GetPipeClientId(), focusToken),
true))
{
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;
}
/**
* @brief Send IME status to UI process via named pipe
*
* @param kbdIsOpen
* @param fullwidthIsOpen
* @param puncIsOpen
* @return int
*/
int SendIMEStatusEventToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen)
{
return SendIMEStatusSnapshotToUIProcessViaNamedPipe(kbdIsOpen, fullwidthIsOpen, puncIsOpen);
}
int SendIMEStatusSnapshotToUIProcessViaNamedPipe(bool kbdIsOpen, bool fullwidthIsOpen, bool puncIsOpen,
bool assertsFocusOwnership)
{
if (!Global::g_connected)
{
return -1;
}
namedpipeData = {};
namedpipeData.event_type =
assertsFocusOwnership ? FanyImePipeEventType::FocusRestored : FanyImePipeEventType::StatusSnapshot;
namedpipeData.keycode = kbdIsOpen ? 1u : 0u;
namedpipeData.modifiers_down = fullwidthIsOpen ? 1u : 0u;
namedpipeData.pinyin_length = puncIsOpen ? 1 : 0;
namedpipeData.wch = (GetKeyState(VK_CAPITAL) & 0x0001) != 0 ? 1 : 0;
const bool sent = SendToNamedpipe();
return sent ? 0 : -1;
}
int SendIMEDeactivationEventToUIProcessViaNamedPipe()
{
SendToAuxNamedpipe(L"IMEDeactivation");
return 0;
}
int SendIMESwitchEventToUIProcessViaNamedPipe(UINT uImeStatus)
{
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::IMESwitch;
/* 利用其他的字段,把 IME 的中英状态传递过去 */
namedpipeData.keycode = uImeStatus;
SendToNamedpipe();
return 0;
}
int SendPuncSwitchEventToUIProcessViaNamedPipe(BOOL isPunc)
{
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::PuncSwitch;
/* 利用其他的字段,把标点符号的中英状态传递过去 */
namedpipeData.keycode = isPunc;
SendToNamedpipe();
return 0;
}
int SendDoubleSingleByteSwitchEventToUIProcessViaNamedPipe(BOOL isDoubleSingleByte)
{
namedpipeData = {};
namedpipeData.event_type = FanyImePipeEventType::DoubleSingleByteSwitch;
/* 利用其他的字段,把全角/半角的状态传递过去 */
namedpipeData.keycode = isDoubleSingleByte;
SendToNamedpipe();
return 0;
}