diff --git a/openless-all/app/windows-ime/OpenLessIme.vcxproj b/openless-all/app/windows-ime/OpenLessIme.vcxproj index 32c1ba4f..af417141 100644 --- a/openless-all/app/windows-ime/OpenLessIme.vcxproj +++ b/openless-all/app/windows-ime/OpenLessIme.vcxproj @@ -77,6 +77,7 @@ + @@ -84,6 +85,7 @@ + diff --git a/openless-all/app/windows-ime/src/ipc_client.cpp b/openless-all/app/windows-ime/src/ipc_client.cpp new file mode 100644 index 00000000..d34b528e --- /dev/null +++ b/openless-all/app/windows-ime/src/ipc_client.cpp @@ -0,0 +1,486 @@ +#include "ipc_client.h" + +#include +#include + +#include "text_service.h" + +namespace { + +constexpr wchar_t kPipeName[] = L"\\\\.\\pipe\\OpenLessImeSubmit"; +constexpr DWORD kPipeBufferSize = 4096; +constexpr size_t kMaxJsonLineBytes = 64 * 1024; + +struct SubmitMessage { + std::wstring type; + std::wstring session_id; + std::wstring text; + int protocol_version = 0; + bool has_type = false; + bool has_session_id = false; + bool has_text = false; + bool has_protocol_version = false; +}; + +bool AppendUtf8AsWide(const char* data, + int length, + std::wstring* output) { + if (length == 0) { + return true; + } + + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, data, + length, nullptr, 0); + if (required <= 0) { + return false; + } + + const size_t old_size = output->size(); + output->resize(old_size + static_cast(required)); + return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, data, length, + output->data() + old_size, required) == required; +} + +bool WideToUtf8(const std::wstring& value, std::string* output) { + output->clear(); + if (value.empty()) { + return true; + } + + const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + value.c_str(), + static_cast(value.size()), + nullptr, 0, nullptr, nullptr); + if (required <= 0) { + return false; + } + + output->resize(static_cast(required)); + return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.c_str(), + static_cast(value.size()), output->data(), + required, nullptr, nullptr) == required; +} + +void SkipWhitespace(const std::string& json, size_t* pos) { + while (*pos < json.size()) { + const char c = json[*pos]; + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { + return; + } + ++(*pos); + } +} + +int HexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; +} + +bool ParseJsonString(const std::string& json, + size_t* pos, + std::wstring* value) { + value->clear(); + if (*pos >= json.size() || json[*pos] != '"') { + return false; + } + ++(*pos); + + size_t segment_start = *pos; + while (*pos < json.size()) { + const char c = json[*pos]; + if (static_cast(c) < 0x20) { + return false; + } + + if (c == '"') { + if (!AppendUtf8AsWide(json.data() + segment_start, + static_cast(*pos - segment_start), value)) { + return false; + } + ++(*pos); + return true; + } + + if (c != '\\') { + ++(*pos); + continue; + } + + if (!AppendUtf8AsWide(json.data() + segment_start, + static_cast(*pos - segment_start), value)) { + return false; + } + + ++(*pos); + if (*pos >= json.size()) { + return false; + } + + const char escaped = json[*pos]; + switch (escaped) { + case '"': + case '\\': + case '/': + value->push_back(static_cast(escaped)); + ++(*pos); + break; + case 'b': + value->push_back(L'\b'); + ++(*pos); + break; + case 'f': + value->push_back(L'\f'); + ++(*pos); + break; + case 'n': + value->push_back(L'\n'); + ++(*pos); + break; + case 'r': + value->push_back(L'\r'); + ++(*pos); + break; + case 't': + value->push_back(L'\t'); + ++(*pos); + break; + case 'u': { + if (*pos + 4 >= json.size()) { + return false; + } + uint32_t code_unit = 0; + for (int i = 1; i <= 4; ++i) { + const int digit = HexDigit(json[*pos + static_cast(i)]); + if (digit < 0) { + return false; + } + code_unit = (code_unit << 4) | static_cast(digit); + } + value->push_back(static_cast(code_unit)); + *pos += 5; + break; + } + default: + return false; + } + + segment_start = *pos; + } + + return false; +} + +bool ParseJsonInteger(const std::string& json, size_t* pos, int* value) { + if (*pos >= json.size() || json[*pos] < '0' || json[*pos] > '9') { + return false; + } + + int parsed = 0; + while (*pos < json.size() && json[*pos] >= '0' && json[*pos] <= '9') { + parsed = parsed * 10 + (json[*pos] - '0'); + ++(*pos); + } + + *value = parsed; + return true; +} + +bool SkipJsonValue(const std::string& json, size_t* pos) { + std::wstring ignored; + if (*pos >= json.size()) { + return false; + } + if (json[*pos] == '"') { + return ParseJsonString(json, pos, &ignored); + } + while (*pos < json.size() && json[*pos] != ',' && json[*pos] != '}') { + ++(*pos); + } + return true; +} + +bool ParseSubmitMessage(const std::string& json, SubmitMessage* message) { + size_t pos = 0; + SkipWhitespace(json, &pos); + if (pos >= json.size() || json[pos] != '{') { + return false; + } + ++pos; + + while (true) { + SkipWhitespace(json, &pos); + if (pos < json.size() && json[pos] == '}') { + ++pos; + break; + } + + std::wstring key; + if (!ParseJsonString(json, &pos, &key)) { + return false; + } + + SkipWhitespace(json, &pos); + if (pos >= json.size() || json[pos] != ':') { + return false; + } + ++pos; + SkipWhitespace(json, &pos); + + if (key == L"type") { + message->has_type = ParseJsonString(json, &pos, &message->type); + if (!message->has_type) { + return false; + } + } else if (key == L"sessionId") { + message->has_session_id = + ParseJsonString(json, &pos, &message->session_id); + if (!message->has_session_id) { + return false; + } + } else if (key == L"text") { + message->has_text = ParseJsonString(json, &pos, &message->text); + if (!message->has_text) { + return false; + } + } else if (key == L"protocolVersion") { + message->has_protocol_version = + ParseJsonInteger(json, &pos, &message->protocol_version); + if (!message->has_protocol_version) { + return false; + } + } else if (!SkipJsonValue(json, &pos)) { + return false; + } + + SkipWhitespace(json, &pos); + if (pos < json.size() && json[pos] == ',') { + ++pos; + continue; + } + if (pos < json.size() && json[pos] == '}') { + ++pos; + break; + } + return false; + } + + SkipWhitespace(json, &pos); + return pos == json.size(); +} + +std::wstring EscapeJsonString(const std::wstring& value) { + std::wstring escaped; + for (wchar_t ch : value) { + switch (ch) { + case L'"': + escaped += L"\\\""; + break; + case L'\\': + escaped += L"\\\\"; + break; + case L'\b': + escaped += L"\\b"; + break; + case L'\f': + escaped += L"\\f"; + break; + case L'\n': + escaped += L"\\n"; + break; + case L'\r': + escaped += L"\\r"; + break; + case L'\t': + escaped += L"\\t"; + break; + default: + if (ch < 0x20) { + wchar_t buffer[7] = {}; + swprintf_s(buffer, L"\\u%04X", static_cast(ch)); + escaped += buffer; + } else { + escaped.push_back(ch); + } + break; + } + } + return escaped; +} + +} // namespace + +OpenLessPipeServer::OpenLessPipeServer() = default; + +OpenLessPipeServer::~OpenLessPipeServer() { + Stop(); +} + +void OpenLessPipeServer::Start(OpenLessTextService* service) { + if (service == nullptr || thread_.joinable()) { + return; + } + + stop_requested_.store(false); + service_ = service; + service_->AddRef(); + thread_ = std::thread(&OpenLessPipeServer::Run, this); +} + +void OpenLessPipeServer::Stop() { + stop_requested_.store(true); + if (thread_.joinable()) { + CancelSynchronousIo(thread_.native_handle()); + WakePipe(); + thread_.join(); + } + + if (service_ != nullptr) { + service_->Release(); + service_ = nullptr; + } +} + +void OpenLessPipeServer::Run() { + while (!stop_requested_.load()) { + HANDLE pipe = CreateNamedPipeW( + kPipeName, PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, + kPipeBufferSize, kPipeBufferSize, 0, nullptr); + if (pipe == INVALID_HANDLE_VALUE) { + return; + } + + { + std::lock_guard lock(pipe_mutex_); + pipe_handle_ = pipe; + } + + const BOOL connected = + ConnectNamedPipe(pipe, nullptr) + ? TRUE + : (GetLastError() == ERROR_PIPE_CONNECTED ? TRUE : FALSE); + + if (connected && !stop_requested_.load()) { + std::string line; + if (ReadJsonLine(pipe, &line)) { + HandleSubmitLine(pipe, line); + } + } + + FlushFileBuffers(pipe); + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + + { + std::lock_guard lock(pipe_mutex_); + if (pipe_handle_ == pipe) { + pipe_handle_ = INVALID_HANDLE_VALUE; + } + } + } +} + +bool OpenLessPipeServer::ReadJsonLine(HANDLE pipe, std::string* line) { + line->clear(); + char buffer[1024] = {}; + + while (!stop_requested_.load() && line->size() < kMaxJsonLineBytes) { + DWORD bytes_read = 0; + if (!ReadFile(pipe, buffer, sizeof(buffer), &bytes_read, nullptr)) { + const DWORD error = GetLastError(); + if (error == ERROR_MORE_DATA && bytes_read > 0) { + line->append(buffer, buffer + bytes_read); + continue; + } + return !line->empty(); + } + + if (bytes_read == 0) { + return !line->empty(); + } + + for (DWORD i = 0; i < bytes_read; ++i) { + if (buffer[i] == '\n') { + return true; + } + line->push_back(buffer[i]); + if (line->size() >= kMaxJsonLineBytes) { + return true; + } + } + } + + return !line->empty(); +} + +void OpenLessPipeServer::HandleSubmitLine(HANDLE pipe, const std::string& line) { + SubmitMessage message; + if (!ParseSubmitMessage(line, &message) || !message.has_type || + !message.has_protocol_version || !message.has_session_id || + !message.has_text || message.protocol_version != 1 || + message.type != L"submitText") { + WriteResult(pipe, message.session_id, L"failed", L"protocolError"); + return; + } + + if (service_ == nullptr) { + WriteResult(pipe, message.session_id, L"failed", L"serviceUnavailable"); + return; + } + + const HRESULT hr = + service_->SubmitTextFromPipe(message.session_id, message.text); + if (SUCCEEDED(hr)) { + WriteResult(pipe, message.session_id, L"committed", nullptr); + } else { + WriteResult(pipe, message.session_id, L"rejected", L"commitRejected"); + } +} + +bool OpenLessPipeServer::WriteResult(HANDLE pipe, + const std::wstring& session_id, + const wchar_t* status, + const wchar_t* error_code) { + std::wstring response = L"{\"type\":\"submitResult\",\"protocolVersion\":1,"; + response += L"\"sessionId\":\""; + response += EscapeJsonString(session_id); + response += L"\",\"status\":\""; + response += status; + response += L"\",\"errorCode\":"; + if (error_code == nullptr) { + response += L"null"; + } else { + response += L"\""; + response += error_code; + response += L"\""; + } + response += L"}\n"; + + std::string utf8_response; + if (!WideToUtf8(response, &utf8_response)) { + return false; + } + + DWORD bytes_written = 0; + return WriteFile(pipe, utf8_response.data(), + static_cast(utf8_response.size()), &bytes_written, + nullptr) && + bytes_written == utf8_response.size(); +} + +void OpenLessPipeServer::WakePipe() { + HANDLE pipe = + CreateFileW(kPipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (pipe != INVALID_HANDLE_VALUE) { + CloseHandle(pipe); + } +} diff --git a/openless-all/app/windows-ime/src/ipc_client.h b/openless-all/app/windows-ime/src/ipc_client.h new file mode 100644 index 00000000..e0c0e799 --- /dev/null +++ b/openless-all/app/windows-ime/src/ipc_client.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +class OpenLessTextService; + +class OpenLessPipeServer { + public: + OpenLessPipeServer(); + OpenLessPipeServer(const OpenLessPipeServer&) = delete; + OpenLessPipeServer& operator=(const OpenLessPipeServer&) = delete; + ~OpenLessPipeServer(); + + void Start(OpenLessTextService* service); + void Stop(); + + private: + void Run(); + bool ReadJsonLine(HANDLE pipe, std::string* line); + void HandleSubmitLine(HANDLE pipe, const std::string& line); + bool WriteResult(HANDLE pipe, + const std::wstring& session_id, + const wchar_t* status, + const wchar_t* error_code); + void WakePipe(); + + std::atomic stop_requested_{false}; + std::thread thread_; + std::mutex pipe_mutex_; + HANDLE pipe_handle_ = INVALID_HANDLE_VALUE; + OpenLessTextService* service_ = nullptr; +}; diff --git a/openless-all/app/windows-ime/src/text_service.cpp b/openless-all/app/windows-ime/src/text_service.cpp index ed911832..7c4369e3 100644 --- a/openless-all/app/windows-ime/src/text_service.cpp +++ b/openless-all/app/windows-ime/src/text_service.cpp @@ -5,6 +5,21 @@ #include "edit_session.h" extern LONG g_object_count; +extern HINSTANCE g_module; + +namespace { + +constexpr wchar_t kMessageWindowClassName[] = L"OpenLessImeMessageWindow"; +constexpr UINT kSubmitTextMessage = WM_APP + 1; +constexpr UINT kSubmitTextTimeoutMs = 3000; + +struct SubmitTextRequest { + const std::wstring* session_id = nullptr; + const std::wstring* text = nullptr; + HRESULT result = E_UNEXPECTED; +}; + +} // namespace OpenLessTextService::OpenLessTextService() { InterlockedIncrement(&g_object_count); @@ -59,11 +74,19 @@ STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr, Deactivate(); + owner_thread_id_ = GetCurrentThreadId(); + thread_mgr_ = thread_mgr; thread_mgr_->AddRef(); client_id_ = client_id; - const HRESULT hr = StartIpcServer(); + HRESULT hr = EnsureMessageWindow(); + if (FAILED(hr)) { + Deactivate(); + return hr; + } + + hr = StartIpcServer(); if (FAILED(hr)) { Deactivate(); return hr; @@ -74,12 +97,14 @@ STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr, STDMETHODIMP OpenLessTextService::Deactivate() { StopIpcServer(); + DestroyMessageWindow(); if (thread_mgr_ != nullptr) { thread_mgr_->Release(); thread_mgr_ = nullptr; } client_id_ = TF_CLIENTID_NULL; + owner_thread_id_ = 0; return S_OK; } @@ -87,6 +112,74 @@ STDMETHODIMP OpenLessTextService::Deactivate() { HRESULT OpenLessTextService::SubmitTextFromPipe( const std::wstring& session_id, const std::wstring& text) { + if (GetCurrentThreadId() == owner_thread_id_) { + return CommitTextOnOwnerThread(session_id, text); + } + + if (message_window_ == nullptr) { + return E_UNEXPECTED; + } + + SubmitTextRequest request{&session_id, &text, E_UNEXPECTED}; + DWORD_PTR message_result = 0; + const LRESULT sent = SendMessageTimeoutW( + message_window_, kSubmitTextMessage, 0, + reinterpret_cast(&request), SMTO_ABORTIFHUNG, + kSubmitTextTimeoutMs, &message_result); + if (sent == 0) { + const DWORD error = GetLastError(); + return HRESULT_FROM_WIN32(error != ERROR_SUCCESS ? error : ERROR_TIMEOUT); + } + + return request.result; +} + +HRESULT OpenLessTextService::StartIpcServer() { + pipe_server_.Start(this); + return S_OK; +} + +void OpenLessTextService::StopIpcServer() { + pipe_server_.Stop(); +} + +HRESULT OpenLessTextService::EnsureMessageWindow() { + if (message_window_ != nullptr) { + return S_OK; + } + + WNDCLASSW window_class = {}; + window_class.lpfnWndProc = OpenLessTextService::MessageWindowProc; + window_class.hInstance = g_module; + window_class.lpszClassName = kMessageWindowClassName; + + if (!RegisterClassW(&window_class)) { + const DWORD error = GetLastError(); + if (error != ERROR_CLASS_ALREADY_EXISTS) { + return HRESULT_FROM_WIN32(error); + } + } + + message_window_ = + CreateWindowExW(0, kMessageWindowClassName, L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, g_module, this); + if (message_window_ == nullptr) { + return HRESULT_FROM_WIN32(GetLastError()); + } + + return S_OK; +} + +void OpenLessTextService::DestroyMessageWindow() { + if (message_window_ != nullptr) { + DestroyWindow(message_window_); + message_window_ = nullptr; + } +} + +HRESULT OpenLessTextService::CommitTextOnOwnerThread( + const std::wstring& session_id, + const std::wstring& text) { UNREFERENCED_PARAMETER(session_id); if (thread_mgr_ == nullptr || client_id_ == TF_CLIENTID_NULL) { @@ -131,8 +224,32 @@ HRESULT OpenLessTextService::SubmitTextFromPipe( return edit_result; } -HRESULT OpenLessTextService::StartIpcServer() { - return S_OK; -} +LRESULT CALLBACK OpenLessTextService::MessageWindowProc(HWND window, + UINT message, + WPARAM wparam, + LPARAM lparam) { + UNREFERENCED_PARAMETER(wparam); -void OpenLessTextService::StopIpcServer() {} + if (message == WM_NCCREATE) { + const auto* create = reinterpret_cast(lparam); + SetWindowLongPtrW(window, GWLP_USERDATA, + reinterpret_cast(create->lpCreateParams)); + return TRUE; + } + + auto* service = reinterpret_cast( + GetWindowLongPtrW(window, GWLP_USERDATA)); + if (message == kSubmitTextMessage && service != nullptr) { + auto* request = reinterpret_cast(lparam); + if (request == nullptr || request->session_id == nullptr || + request->text == nullptr) { + return 0; + } + + request->result = + service->CommitTextOnOwnerThread(*request->session_id, *request->text); + return 1; + } + + return DefWindowProcW(window, message, wparam, lparam); +} diff --git a/openless-all/app/windows-ime/src/text_service.h b/openless-all/app/windows-ime/src/text_service.h index 7ef32466..d6402a6a 100644 --- a/openless-all/app/windows-ime/src/text_service.h +++ b/openless-all/app/windows-ime/src/text_service.h @@ -4,6 +4,8 @@ #include #include +#include "ipc_client.h" + class OpenLessTextService final : public ITfTextInputProcessorEx { public: OpenLessTextService(); @@ -27,8 +29,20 @@ class OpenLessTextService final : public ITfTextInputProcessorEx { private: HRESULT StartIpcServer(); void StopIpcServer(); + HRESULT EnsureMessageWindow(); + void DestroyMessageWindow(); + HRESULT CommitTextOnOwnerThread(const std::wstring& session_id, + const std::wstring& text); + + static LRESULT CALLBACK MessageWindowProc(HWND window, + UINT message, + WPARAM wparam, + LPARAM lparam); LONG ref_count_ = 1; ITfThreadMgr* thread_mgr_ = nullptr; TfClientId client_id_ = TF_CLIENTID_NULL; + DWORD owner_thread_id_ = 0; + HWND message_window_ = nullptr; + OpenLessPipeServer pipe_server_; };