feat: receive OpenLess IME submissions over a named pipe

This commit is contained in:
millionart committed 2026-05-01 20:47:12 +08:00
1 parent e714fd2c7d
commit 52f7de5d85
5 files changed
+660 -5

No files matched your search

@@ -77,6 +77,7 @@
<ClCompile Include="src\class_factory.cpp" />
<ClCompile Include="src\dllmain.cpp" />
<ClCompile Include="src\edit_session.cpp" />
<ClCompile Include="src\ipc_client.cpp" />
<ClCompile Include="src\registry.cpp" />
<ClCompile Include="src\text_service.cpp" />
</ItemGroup>
@@ -84,6 +85,7 @@
<ClInclude Include="src\class_factory.h" />
<ClInclude Include="src\edit_session.h" />
<ClInclude Include="src\guids.h" />
<ClInclude Include="src\ipc_client.h" />
<ClInclude Include="src\registry.h" />
<ClInclude Include="src\text_service.h" />
</ItemGroup>
@@ -0,0 +1,486 @@
#include "ipc_client.h"
#include <cstdint>
#include <string>
#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<size_t>(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<int>(value.size()),
nullptr, 0, nullptr, nullptr);
if (required <= 0) {
return false;
}
output->resize(static_cast<size_t>(required));
return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.c_str(),
static_cast<int>(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<unsigned char>(c) < 0x20) {
return false;
}
if (c == '"') {
if (!AppendUtf8AsWide(json.data() + segment_start,
static_cast<int>(*pos - segment_start), value)) {
return false;
}
++(*pos);
return true;
}
if (c != '\\') {
++(*pos);
continue;
}
if (!AppendUtf8AsWide(json.data() + segment_start,
static_cast<int>(*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<wchar_t>(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<size_t>(i)]);
if (digit < 0) {
return false;
}
code_unit = (code_unit << 4) | static_cast<uint32_t>(digit);
}
value->push_back(static_cast<wchar_t>(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<unsigned int>(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<std::mutex> 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<std::mutex> 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<DWORD>(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);
}
}
@@ -0,0 +1,36 @@
#pragma once
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include <windows.h>
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<bool> stop_requested_{false};
std::thread thread_;
std::mutex pipe_mutex_;
HANDLE pipe_handle_ = INVALID_HANDLE_VALUE;
OpenLessTextService* service_ = nullptr;
};
@@ -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<LPARAM>(&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<CREATESTRUCTW*>(lparam);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
return TRUE;
}
auto* service = reinterpret_cast<OpenLessTextService*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == kSubmitTextMessage && service != nullptr) {
auto* request = reinterpret_cast<SubmitTextRequest*>(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);
}
@@ -4,6 +4,8 @@
#include <string>
#include <windows.h>
#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_;
};