diff --git a/server/AGENTS.md b/server/AGENTS.md index b1653af0..a7ac61a9 100644 --- a/server/AGENTS.md +++ b/server/AGENTS.md @@ -26,7 +26,7 @@ CI 现在会真的跑它:`.github/workflows/ci.yml` 在 Build 之后有 Provis 数据不全时的典型症状是候选查询返回空集,断言信息看起来像逻辑错误,实际是缺数据。排查测试失败前先确认数据根目录是齐的。 -`src/ipc/ipc.h` 里另有 14 条 `static_assert` 守着协议 ABI。那些是编译期的,随主工程一起检查,不依赖上面这些数据。 +IPC 线格式、opcode 和语音分帧的唯一实现位于 Engine 子模块的 `contracts/`;本仓的 `src/ipc/ipc.h` 只保留连接状态和 Server API。不要重新复制协议定义。共享头的 ABI 断言随主工程编译,主连接必须通过 `NegotiateMainPipeClient` 后才能激活;旧 DLL 的未版本化 hello 仍兼容,版本化客户端会收到关联 request_id 的协议确认。 改了协议或候选逻辑,正常构建并备好数据之后自己跑一次: diff --git a/server/MetasequoiaImeEngine b/server/MetasequoiaImeEngine index 55839c80..127019f2 160000 --- a/server/MetasequoiaImeEngine +++ b/server/MetasequoiaImeEngine @@ -1 +1 @@ -Subproject commit 55839c80c5b9be6323687b83b782d877c00d88e9 +Subproject commit 127019f252cab9146f7a4e184f77cd6ed13fcf56 diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index a34b207c..93be08dd 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -2478,6 +2478,10 @@ void MainPipeClientThread(HANDLE clientPipe, uint64_t handlerId) { break; } + if (!NegotiateMainPipeClient(pipeData, mainRegistrationId)) + { + break; + } if (!pipe_running || !SetPipeWaitMode(clientPipe, true)) { break; diff --git a/server/src/ipc/ipc.cpp b/server/src/ipc/ipc.cpp index ca566dbb..bde11145 100644 --- a/server/src/ipc/ipc.cpp +++ b/server/src/ipc/ipc.cpp @@ -17,6 +17,7 @@ #include #include #include "ipc.h" +#include "MetasequoiaImeEngine/contracts/ipc_negotiation.h" #include "ipc/active_client_state.h" #include "ipc/event_listener.h" #include "ipc/outbound_session_state.h" @@ -195,6 +196,7 @@ class PipeControlHandle struct PipeClientSession { + FanyImeProtocol::Negotiation protocol; HANDLE main_pipe = INVALID_HANDLE_VALUE; HANDLE main_control_pipe = INVALID_HANDLE_VALUE; uint64_t main_registration_id = 0; @@ -774,6 +776,28 @@ bool SharedMemoryAvailable() } } // namespace +bool NegotiateMainPipeClient(const FanyImeNamedpipeData &hello, uint64_t registration_id) +{ + const auto protocol = FanyImeProtocol::Negotiate(hello); + std::lock_guard lock(g_pipe_clients_mutex); + auto it = g_pipe_clients.find(hello.client_id); + if (it == g_pipe_clients.end() || registration_id == 0 || it->second.main_registration_id != registration_id) + return false; + auto &session = it->second; + session.protocol = protocol; + if (protocol.legacy) + return protocol.accepted; + if (!session.to_tsf_pipe || !session.to_tsf_ready) + return false; + const auto reply = FanyImeProtocol::Reply(hello, protocol); + DWORD written = 0; + DWORD error = ERROR_SUCCESS; + // Tie the ACK to this main registration while the registry lock excludes + // endpoint replacement. No activation or candidate result can precede it. + return session.to_tsf_pipe->Write(&reply, sizeof(reply), written, error) && written == sizeof(reply) && + protocol.accepted; +} + uint64_t RegisterToTsfPipeClient(uint64_t client_id, HANDLE pipe) { if (client_id == 0 || !pipe || pipe == INVALID_HANDLE_VALUE) diff --git a/server/src/ipc/ipc.h b/server/src/ipc/ipc.h index 75932bfc..32f4aedc 100644 --- a/server/src/ipc/ipc.h +++ b/server/src/ipc/ipc.h @@ -9,17 +9,8 @@ #include #include -inline const wchar_t *FANY_IME_SHARED_MEMORY = L"Local\\FanyImeSharedMemory"; -inline const int BUFFER_SIZE = 4096; +#include "MetasequoiaImeEngine/contracts/windows_ipc.h" -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 const wchar_t *FANY_IME_TSF_DIAGNOSTIC_NAMED_PIPE = L"\\\\.\\pipe\\FanyImeTsfDiagnosticNamedPipe"; -inline constexpr uint32_t FANY_IME_TSF_DIAGNOSTIC_MAGIC = 0x474F4C54; // "TLOG" -inline constexpr uint32_t FANY_IME_TSF_DIAGNOSTIC_VERSION = 1; -inline constexpr size_t FANY_IME_TSF_DIAGNOSTIC_MAX_FRAME_BYTES = 16 * 1024; inline constexpr DWORD FANY_IME_TO_TSF_PIPE_FRAME_CAPACITY = 64; inline constexpr DWORD FANY_IME_TO_TSF_WORKER_PIPE_FRAME_CAPACITY = 32; inline HANDLE hPipe = INVALID_HANDLE_VALUE; @@ -34,16 +25,6 @@ inline bool toTsfWorkerThreadConnected = false; inline HANDLE toTsfPipeThread = NULL; inline HANDLE toTsfWorkerThreadPipeThread = NULL; -// -// Events from tsf to server -// -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 - L"FanyHideCandidateWndEvent", // Event sent to UI process to notify time to hide candidate window - L"FanyShowCandidateWndEvent", // Event sent to UI process to notify time to show candidate window - L"FanyMoveCandidateWndEvent", // Event sent to UI process to notify time to move candidate window -}; - // // Events from server to tsf // @@ -70,175 +51,8 @@ inline std::vector hEvents(FANY_IME_EVENT_ARRAY.size()); inline std::vector hPipeEvents(FANY_IME_EVENT_PIPE_ARRAY.size()); inline std::vector hWorkerPipeEvents(FANY_IME_EVENT_PIPE_TO_TSF_WORKER_THREAD_ARRAY.size()); -struct FanyImeSharedMemoryData -{ - UINT keycode; - WCHAR wch; - UINT modifiers_down = 0; - int point[2] = {100, 100}; - int pinyin_length = 0; - wchar_t pinyin_string[128]; - wchar_t candidate_string[1024]; - wchar_t selected_candiate_string[128]; -}; - -// -// For uwp/metro apps, here we do not need candidate_string and selected_candiate_string, -// just let server process to handle them -// -// event_type -// 0: FanyImeKeyEvent -// 1: FanyHideCandidateWndEvent -// 2: FanyShowCandidateWndEvent -// 3: FanyMoveCandidateWndEvent -// 4: FanyLangbarRightClickEvent (legacy Main enum; tip sends via Aux text) -// 7: IMESwitch -// 8: PuncSwitch -// 9: DoubleSingleByteSwitch -// 10: ClientHello -// 11: ClientActivated -// 12: ClientDeactivated (terminal route reset; toolbar hidden) -// 13: StatusSnapshot (wch: CapsLock 1/0) -// 14: ClientSuspended (recoverable route reset; toolbar unchanged) -// 15: FocusRestored (StatusSnapshot payload + thread-focus ownership claim) -// -// modifiers_down: -// 0b00000001: Shift -// 0b00000010: Control -// 0b00000100: Alt -// TODO: Make it able to denote explicit modifiers, e.g. LShift, RShift, we could use left keys -// -// -struct FanyImeNamedpipeData -{ - UINT event_type; - uint64_t client_id = 0; - uint64_t request_id = 0; - UINT keycode; // VkCode - WCHAR wch; // Unicode character converted from vkcode - UINT modifiers_down = 0; - int point[2] = {100, 100}; - int pinyin_length = 0; - wchar_t pinyin_string[128]; -}; - -// -// Data sent to tsf end -// -// msg_type -// 0: success -// 1: candidate index out of range error -// -struct FanyImeNamedpipeDataToTsf -{ - UINT msg_type; - uint64_t request_id = 0; - wchar_t candidate_string[FanyImePipeLimits::CandidateTextCapacity]; -}; - -static_assert(offsetof(FanyImeNamedpipeData, request_id) == 16, - "FanyImeNamedpipeData request_id ABI must match the TSF client"); -static_assert(sizeof(WCHAR) == 2, "The IPC ABI requires 16-bit WCHAR"); -static_assert(offsetof(FanyImeNamedpipeData, client_id) == 8, - "FanyImeNamedpipeData client_id ABI must match the TSF client"); -static_assert(offsetof(FanyImeNamedpipeData, keycode) == 24, - "FanyImeNamedpipeData keycode ABI must match the TSF client"); -static_assert(offsetof(FanyImeNamedpipeData, pinyin_string) == 48, - "FanyImeNamedpipeData pinyin ABI must match the TSF client"); -static_assert(sizeof(FanyImeNamedpipeData) == 304, "FanyImeNamedpipeData ABI must match the TSF client"); -static_assert(offsetof(FanyImeNamedpipeDataToTsf, request_id) == 8, - "FanyImeNamedpipeDataToTsf request_id ABI must match the TSF client"); -static_assert(offsetof(FanyImeNamedpipeDataToTsf, candidate_string) == 16, - "FanyImeNamedpipeDataToTsf candidate ABI must match the TSF client"); -static_assert(sizeof(FanyImeNamedpipeDataToTsf) == 416, "FanyImeNamedpipeDataToTsf ABI must match the TSF client"); inline FanyImeNamedpipeData namedpipeData; -namespace FanyImePipeEventType -{ -constexpr UINT KeyEvent = 0; -constexpr UINT HideCandidateWnd = 1; -constexpr UINT ShowCandidateWnd = 2; -constexpr UINT MoveCandidateWnd = 3; -constexpr UINT LangbarRightClick = 4; -constexpr UINT ClientHello = 10; -constexpr UINT ClientActivated = 11; -constexpr UINT ClientDeactivated = 12; -constexpr UINT StatusSnapshot = 13; -// Rotate the active IPC route for an internal TSF focus-session reset. A -// suspension keeps floating-toolbar visibility; terminal deactivation hides it. -constexpr UINT ClientSuspended = 14; -// Same payload as StatusSnapshot, but additionally asserts that this client -// owns thread focus right now. Sent when document focus returns to a session -// the Server may have re-routed to another client meanwhile; without the -// ownership claim the Server would discard it as an inactive-client event. -constexpr UINT FocusRestored = 15; -constexpr UINT IMESwitch = 7; -constexpr UINT PuncSwitch = 8; -constexpr UINT DoubleSingleByteSwitch = 9; - -constexpr bool IsRouteDeactivation(UINT event_type) -{ - return event_type == ClientDeactivated || event_type == ClientSuspended; -} - -constexpr bool IsTerminalDeactivation(UINT event_type) -{ - return event_type == ClientDeactivated; -} -} // namespace FanyImePipeEventType - -// OR'd into modifiers_down. Must be stripped before key-modifier policy. -namespace FanyImePipeFlags -{ -constexpr UINT UiLess = 0x80000000u; -} // namespace FanyImePipeFlags - -namespace FanyImePipeRole -{ -constexpr UINT Main = 0; -constexpr UINT ToTsf = 1; -constexpr UINT ToTsfWorkerThread = 2; -} // namespace FanyImePipeRole - -struct FanyImePipeHello -{ - uint64_t client_id = 0; - UINT pipe_role = 0; -}; - -// -// Data sent to tsf worker thread -// -// msg_type -// 0: IME switch to EN -// 1: IME switch to CN -// -// data -// Not used now. -// -// -struct FanyImeNamedpipeDataToTsfWorkerThread -{ - UINT msg_type; - wchar_t data[200]; -}; - -struct FanyImeTsfDiagnosticBatchHeader -{ - uint32_t magic = FANY_IME_TSF_DIAGNOSTIC_MAGIC; - uint32_t version = FANY_IME_TSF_DIAGNOSTIC_VERSION; - uint32_t header_size = 28; - uint32_t payload_bytes = 0; - uint32_t record_count = 0; - uint32_t dropped_count = 0; - uint32_t source_process_id = 0; -}; - -static_assert(sizeof(FanyImePipeHello) == 16, "FanyImePipeHello ABI must match the TSF client"); -static_assert(sizeof(FanyImeNamedpipeDataToTsfWorkerThread) == 404, - "FanyImeNamedpipeDataToTsfWorkerThread ABI must match the TSF client"); -static_assert(sizeof(FanyImeTsfDiagnosticBatchHeader) == 28, - "FanyImeTsfDiagnosticBatchHeader ABI must match the TSF client"); static_assert(sizeof(FanyImeNamedpipeDataToTsf) * FANY_IME_TO_TSF_PIPE_FRAME_CAPACITY >= sizeof(FanyImeNamedpipeDataToTsf) * 64, "The TSF reply pipe must buffer at least 64 complete replies"); @@ -247,6 +61,7 @@ static_assert(sizeof(FanyImeNamedpipeDataToTsfWorkerThread) * FANY_IME_TO_TSF_WO "The TSF worker pipe must buffer at least 16 complete notifications"); int InitIpc(); +bool NegotiateMainPipeClient(const FanyImeNamedpipeData &hello, uint64_t registration_id); int CloseIpc(); int InitNamedPipe(); int CloseNamedPipe(); @@ -331,69 +146,10 @@ inline int PinyinLength = 0; inline std::wstring PinyinString = L""; inline std::wstring CandidateString = L""; -namespace DataFromServerMsgType -{ -constexpr UINT Normal = 0; -constexpr UINT OutofRange = 1; -constexpr UINT NeedToCreateWord = 2; -constexpr UINT Preedit = 3; -constexpr UINT NavigationIgnored = 4; -constexpr UINT MoveSelectionPrevious = 5; -constexpr UINT MoveSelectionNext = 6; -constexpr UINT MovePagePrevious = 7; -constexpr UINT MovePageNext = 8; -constexpr UINT PipeReady = 9; -// Commit candidate_string verbatim and finish the active composition. -constexpr UINT CommitExactText = 10; -// UILess hosts (games): candidate_string = preedit + L'\t' + cand1,cand2,... -constexpr UINT UiLessComposition = 11; -} // namespace DataFromServerMsgType +namespace DataFromServerMsgType = FanyImeReplyType; inline UINT MsgTypeToTsf = DataFromServerMsgType::Normal; // 默认为 Normal -namespace DataFromServerMsgTypeToTsfWorkerThread -{ -constexpr UINT SwitchToEn = 0; -constexpr UINT SwitchToCn = 1; -constexpr UINT SwitchToPuncEn = 2; -constexpr UINT SwitchToPuncCn = 3; -constexpr UINT SwitchToFullwidth = 4; -constexpr UINT SwitchToHalfwidth = 5; -constexpr UINT CommitCandidate = 6; -constexpr UINT PagingCommaPeriodChanged = 7; -// Ordered focus-session barrier. The data field echoes the nonzero request_id -// (focus token) from ClientActivated; implicit-key barriers reuse the nonzero -// token recorded for the current main registration. No token means no barrier. -// Because all worker packets share one endpoint/write lock, older notifications -// are written before this marker or rejected by their stale activation epoch. -constexpr UINT FocusSessionReady = 8; -// Worker reverse-pipe registration acknowledgement. This is always the first -// frame written to a newly accepted worker endpoint. -constexpr UINT PipeReady = 9; -// Unsolicited text insert (e.g. voice ASR). Independent of candidate finalize. -constexpr UINT InsertText = 10; -// Smart punctuation after ASCII letters/digits (',' '.' ':'). Payload "0"/"1". -constexpr UINT SmartPunctuationChanged = 11; -// Auto-complete opening paired punctuation and leave the caret inside. Payload "0"/"1". -constexpr UINT PairedPunctuationChanged = 12; -// Payload "1" when the active shuangpin profile uses ';' for the ing final. -constexpr UINT MicrosoftShuangpinChanged = 13; -// Replace a repeated smart ASCII punctuation with its Chinese mapping. Payload "0"/"1". -constexpr UINT SmartPunctuationRepeatToChineseChanged = 14; -// Streaming ASR: replace the inline composition with this full snapshot. -constexpr UINT UpdateVoiceComposition = 15; -// Streaming ASR: abort the voice composition without committing. -constexpr UINT CancelVoiceComposition = 16; -// Streaming ASR: replace the inline composition with this snapshot and commit. -constexpr UINT CommitVoiceComposition = 17; -// Payload "1" when input.mode is Japanese, otherwise "0". -constexpr UINT InputModeChanged = 18; -constexpr UINT CapsLockChanged = 19; -// Enables buffered TSF diagnostics sent to Server. Payload "0"/"1". -constexpr UINT TsfDiagnosticLogChanged = 20; -// Payload "0" follow IME, "1" always Chinese punctuation, "2" always English punctuation. -constexpr UINT PunctuationLockChanged = 21; -constexpr UINT MaxKnown = PunctuationLockChanged; -} // namespace DataFromServerMsgTypeToTsfWorkerThread +namespace DataFromServerMsgTypeToTsfWorkerThread = FanyImeWorkerReplyType; } // namespace Global diff --git a/server/src/ipc/ipc_protocol_limits.h b/server/src/ipc/ipc_protocol_limits.h index 1d494c52..5bb05283 100644 --- a/server/src/ipc/ipc_protocol_limits.h +++ b/server/src/ipc/ipc_protocol_limits.h @@ -1,9 +1,2 @@ #pragma once - -#include - -namespace FanyImePipeLimits -{ -constexpr std::size_t CandidateTextCapacity = 200; -constexpr std::size_t CandidateTextMaxLength = CandidateTextCapacity - 1; -} // namespace FanyImePipeLimits +#include "MetasequoiaImeEngine/contracts/ipc_protocol_limits.h" diff --git a/server/src/ipc/voice_composition_pipe.h b/server/src/ipc/voice_composition_pipe.h index 0c5cd622..56f2b428 100644 --- a/server/src/ipc/voice_composition_pipe.h +++ b/server/src/ipc/voice_composition_pipe.h @@ -1,143 +1,2 @@ #pragma once - -#include -#include -#include -#include - -// Framed voice-composition snapshots on the worker pipe. -// Keep in sync with MetasequoiaImeTsf/src/IPC/VoiceCompositionPipe.h. -// Each worker packet is still wchar_t[200]; a snapshot longer than one packet -// is sent as ordered frames and assembled before any TSF SetText. -namespace FanyImeVoiceCompositionPipe -{ -constexpr wchar_t kFlagFirst = 0x1; -constexpr wchar_t kFlagLast = 0x2; -constexpr std::size_t kPacketChars = 200; -constexpr std::size_t kHeaderChars = 2; -constexpr std::size_t kMaxChunkChars = kPacketChars - kHeaderChars - 1; -constexpr std::size_t kMaxSnapshotChars = 2048; - -struct Frame -{ - bool valid = false; - bool first = false; - bool last = false; - wchar_t generation = 0; - std::wstring chunk; -}; - -inline Frame ParseFrame(const wchar_t *data, std::size_t n = kPacketChars) -{ - Frame frame; - if (data == nullptr || n < kHeaderChars + 1) - { - return frame; - } - bool hasTerminator = false; - for (std::size_t i = 0; i < n; ++i) - { - if (data[i] == L'\0') - { - hasTerminator = true; - break; - } - } - if (!hasTerminator) - { - return frame; - } - const wchar_t flags = data[0]; - if ((flags & static_cast(~(kFlagFirst | kFlagLast))) != 0 || data[1] == 0) - { - return frame; - } - frame.valid = true; - frame.first = (flags & kFlagFirst) != 0; - frame.last = (flags & kFlagLast) != 0; - frame.generation = data[1]; - frame.chunk.assign(data + kHeaderChars); - return frame; -} - -inline std::vector EncodeSnapshot(std::wstring text, wchar_t generation) -{ - std::vector frames; - if (generation == 0) - { - return frames; - } - if (text.size() > kMaxSnapshotChars) - { - text.resize(kMaxSnapshotChars); - } - if (text.empty()) - { - std::wstring frame(kHeaderChars, L'\0'); - frame[0] = static_cast(kFlagFirst | kFlagLast); - frame[1] = generation; - frames.push_back(std::move(frame)); - return frames; - } - - std::size_t offset = 0; - while (offset < text.size()) - { - const std::size_t chunkLen = (std::min)(kMaxChunkChars, text.size() - offset); - std::wstring frame(kHeaderChars + chunkLen, L'\0'); - wchar_t flags = 0; - if (offset == 0) - { - flags |= kFlagFirst; - } - if (offset + chunkLen >= text.size()) - { - flags |= kFlagLast; - } - frame[0] = flags; - frame[1] = generation; - std::copy(text.begin() + static_cast(offset), - text.begin() + static_cast(offset + chunkLen), frame.begin() + kHeaderChars); - frames.push_back(std::move(frame)); - offset += chunkLen; - } - return frames; -} - -inline std::wstring AssembleFrames(const std::vector &payloads) -{ - std::wstring assembled; - bool started = false; - wchar_t generation = 0; - for (const std::wstring &payload : payloads) - { - wchar_t data[kPacketChars] = {}; - if (payload.size() >= kPacketChars) - { - return {}; - } - std::copy(payload.begin(), payload.end(), data); - const Frame frame = ParseFrame(data); - if (!frame.valid) - { - return {}; - } - if (frame.first) - { - assembled.clear(); - generation = frame.generation; - started = true; - } - else if (!started || frame.generation != generation) - { - return {}; - } - assembled += frame.chunk; - if (frame.last) - { - return assembled; - } - } - return {}; -} -} // namespace FanyImeVoiceCompositionPipe +#include "MetasequoiaImeEngine/contracts/voice_composition_pipe.h"