diff --git a/.gitignore b/.gitignore index e46b9242f..8411fccca 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,12 @@ build msbuild compile_commands.json +/x64/ +/Win32/ +/ARM64/ +/test/*/x64/ +/test/*/Win32/ +/test/*/ARM64/ /output/Win32/*.exe /output/Win32/rime.dll /output/Win32/*.pdb @@ -27,6 +33,11 @@ compile_commands.json /output/data/essay.txt /output/data/opencc/ /output/data/*.yaml +/output/data/lua/ +/output/data/rime.lua +/output/data/weasel-custom-data.txt +/output/data/weasel-visible-schemas.txt +/output/data/01 */ /output/archives /include/glog /include/rime*.h @@ -55,4 +66,4 @@ arm64x_wrapper/*.o arm64x_wrapper/*.dll arm64x_wrapper/*.ime arm64x_wrapper/*.lib -arm64x_wrapper/*.exp \ No newline at end of file +arm64x_wrapper/*.exp diff --git a/RimeWithWeasel/RimeWithWeasel.cpp b/RimeWithWeasel/RimeWithWeasel.cpp index c4d3a5e22..2a0ca6f5c 100644 --- a/RimeWithWeasel/RimeWithWeasel.cpp +++ b/RimeWithWeasel/RimeWithWeasel.cpp @@ -1,1511 +1,1615 @@ -#include "stdafx.h" -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#define TRANSPARENT_COLOR 0x00000000 -#define ARGB2ABGR(value) \ - ((value & 0xff000000) | ((value & 0x000000ff) << 16) | \ - (value & 0x0000ff00) | ((value & 0x00ff0000) >> 16)) -#define RGBA2ABGR(value) \ - (((value & 0xff) << 24) | ((value & 0xff000000) >> 24) | \ - ((value & 0x00ff0000) >> 8) | ((value & 0x0000ff00) << 8)) -typedef enum { COLOR_ABGR = 0, COLOR_ARGB, COLOR_RGBA } ColorFormat; - -using namespace weasel; - -static RimeApi* rime_api; -WeaselSessionId _GenerateNewWeaselSessionId(SessionStatusMap sm, DWORD pid) { - if (sm.empty()) - return (WeaselSessionId)(pid + 1); - return (WeaselSessionId)(sm.rbegin()->first + 1); -} - -int expand_ibus_modifier(int m) { - return (m & 0xff) | ((m & 0xff00) << 16); -} - -RimeWithWeaselHandler::RimeWithWeaselHandler(UI* ui) - : m_ui(ui), - m_active_session(0), - m_disabled(true), - m_current_dark_mode(false), - m_global_ascii_mode(false), - m_show_notifications_time(1200), - _UpdateUICallback(NULL) { - m_ui->InServer() = true; - rime_api = rime_get_api(); - assert(rime_api); - m_pid = GetCurrentProcessId(); - uint16_t msbit = 0; - for (auto i = 31; i >= 0; i--) { - if (m_pid & (1 << i)) { - msbit = i; - break; - } - } - m_pid = (m_pid << (31 - msbit)); - _Setup(); -} - -RimeWithWeaselHandler::~RimeWithWeaselHandler() { - m_show_notifications.clear(); - m_session_status_map.clear(); - m_app_options.clear(); -} - -bool add_session = false; -void _UpdateUIStyle(RimeConfig* config, UI* ui, bool initialize); -bool _UpdateUIStyleColor(RimeConfig* config, - UIStyle& style, - const std::string& color = std::string()); -void _LoadAppOptions(RimeConfig* config, AppOptionsByAppName& app_options); - -void _RefreshTrayIcon(const RimeSessionId session_id, - const std::function _UpdateUICallback) { - // Dangerous, don't touch - static char app_name[256] = {0}; - auto ret = rime_api->get_property(session_id, "client_app", app_name, - sizeof(app_name) - 1); - if (!ret || u8tow(app_name) == std::wstring(L"explorer.exe")) - boost::thread th([=]() { - ::Sleep(100); - if (_UpdateUICallback) - _UpdateUICallback(); - }); - else if (_UpdateUICallback) - _UpdateUICallback(); -} - -void RimeWithWeaselHandler::_Setup() { - RIME_STRUCT(RimeTraits, weasel_traits); - std::string shared_dir = wtou8(WeaselSharedDataPath().wstring()); - std::string user_dir = wtou8(WeaselUserDataPath().wstring()); - weasel_traits.shared_data_dir = shared_dir.c_str(); - weasel_traits.user_data_dir = user_dir.c_str(); - weasel_traits.prebuilt_data_dir = weasel_traits.shared_data_dir; - std::string distribution_name = wtou8(get_weasel_ime_name()); - weasel_traits.distribution_name = distribution_name.c_str(); - weasel_traits.distribution_code_name = WEASEL_CODE_NAME; - weasel_traits.distribution_version = WEASEL_VERSION; - weasel_traits.app_name = "rime.weasel"; - std::string log_dir = WeaselLogPath().u8string(); - weasel_traits.log_dir = log_dir.c_str(); - rime_api->setup(&weasel_traits); - rime_api->set_notification_handler(&RimeWithWeaselHandler::OnNotify, this); -} - -void RimeWithWeaselHandler::Initialize() { - m_disabled = _IsDeployerRunning(); - if (m_disabled) { - return; - } - - LOG(INFO) << "Initializing la rime."; - rime_api->initialize(NULL); - if (rime_api->start_maintenance(/*full_check = */ False)) { - m_disabled = true; - rime_api->join_maintenance_thread(); - } - - RimeConfig config = {NULL}; - if (rime_api->config_open("weasel", &config)) { - if (m_ui) { - _UpdateUIStyle(&config, m_ui, true); - _UpdateShowNotifications(&config, true); - m_current_dark_mode = IsUserDarkMode(); - if (m_current_dark_mode) { - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - if (rime_api->config_get_string(&config, "style/color_scheme_dark", - buffer, BUF_SIZE)) { - std::string color_name(buffer); - _UpdateUIStyleColor(&config, m_ui->style(), color_name); - } - } - m_base_style = m_ui->style(); - } - Bool global_ascii = false; - if (rime_api->config_get_bool(&config, "global_ascii", &global_ascii)) - m_global_ascii_mode = !!global_ascii; - if (!rime_api->config_get_int(&config, "show_notifications_time", - &m_show_notifications_time)) - m_show_notifications_time = 1200; - _LoadAppOptions(&config, m_app_options); - rime_api->config_close(&config); - } - m_last_schema_id.clear(); -} - -void RimeWithWeaselHandler::Finalize() { - m_active_session = 0; - m_disabled = true; - m_session_status_map.clear(); - LOG(INFO) << "Finalizing la rime."; - rime_api->finalize(); -} - -DWORD RimeWithWeaselHandler::FindSession(WeaselSessionId ipc_id) { - if (m_disabled) - return 0; - Bool found = rime_api->find_session(to_session_id(ipc_id)); - DLOG(INFO) << "Find session: session_id = " << to_session_id(ipc_id) - << ", found = " << found; - return found ? (ipc_id) : 0; -} - -DWORD RimeWithWeaselHandler::AddSession(LPWSTR buffer, EatLine eat) { - if (m_disabled) { - DLOG(INFO) << "Trying to resume service."; - EndMaintenance(); - if (m_disabled) - return 0; - } - RimeSessionId session_id = (RimeSessionId)rime_api->create_session(); - if (m_global_ascii_mode) { - for (const auto& pair : m_session_status_map) { - if (pair.first) { - rime_api->set_option(session_id, "ascii_mode", - !!pair.second.status.is_ascii_mode); - break; - } - } - } - - WeaselSessionId ipc_id = - _GenerateNewWeaselSessionId(m_session_status_map, m_pid); - DLOG(INFO) << "Add session: created session_id = " << session_id - << ", ipc_id = " << ipc_id; - SessionStatus& session_status = new_session_status(ipc_id); - session_status.style = m_base_style; - session_status.session_id = session_id; - _ReadClientInfo(ipc_id, buffer); - - RIME_STRUCT(RimeStatus, status); - if (rime_api->get_status(session_id, &status)) { - std::string schema_id = status.schema_id; - m_last_schema_id = schema_id; - _LoadSchemaSpecificSettings(ipc_id, schema_id); - _LoadAppInlinePreeditSet(ipc_id, true); - _UpdateInlinePreeditStatus(ipc_id); - _RefreshTrayIcon(session_id, _UpdateUICallback); - session_status.status = status; - session_status.__synced = false; - rime_api->free_status(&status); - } - m_ui->style() = session_status.style; - // show session's welcome message :-) if any - if (eat) { - _Respond(ipc_id, eat); - } - add_session = true; - _UpdateUI(ipc_id); - add_session = false; - m_active_session = ipc_id; - return ipc_id; -} - -DWORD RimeWithWeaselHandler::RemoveSession(WeaselSessionId ipc_id) { - if (m_ui) - m_ui->Hide(); - if (m_disabled) - return 0; - DLOG(INFO) << "Remove session: session_id = " << to_session_id(ipc_id); - // TODO: force committing? otherwise current composition would be lost - rime_api->destroy_session(to_session_id(ipc_id)); - m_session_status_map.erase(ipc_id); - m_active_session = 0; - return 0; -} - -void RimeWithWeaselHandler::UpdateColorTheme(BOOL darkMode) { - RimeConfig config = {NULL}; - if (rime_api->config_open("weasel", &config)) { - if (m_ui) { - _UpdateUIStyle(&config, m_ui, true); - m_current_dark_mode = darkMode; - if (darkMode) { - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - if (rime_api->config_get_string(&config, "style/color_scheme_dark", - buffer, BUF_SIZE)) { - std::string color_name(buffer); - _UpdateUIStyleColor(&config, m_ui->style(), color_name); - } - } - m_base_style = m_ui->style(); - } - rime_api->config_close(&config); - } - - for (auto& pair : m_session_status_map) { - RIME_STRUCT(RimeStatus, status); - if (rime_api->get_status(to_session_id(pair.first), &status)) { - _LoadSchemaSpecificSettings(pair.first, std::string(status.schema_id)); - _LoadAppInlinePreeditSet(pair.first, true); - _UpdateInlinePreeditStatus(pair.first); - pair.second.status = status; - pair.second.__synced = false; - rime_api->free_status(&status); - } - } - m_ui->style() = get_session_status(m_active_session).style; -} - -BOOL RimeWithWeaselHandler::ProcessKeyEvent(KeyEvent keyEvent, - WeaselSessionId ipc_id, - EatLine eat) { - DLOG(INFO) << "Process key event: keycode = " << keyEvent.keycode - << ", mask = " << keyEvent.mask << ", ipc_id = " << ipc_id; - if (m_disabled) - return FALSE; - RimeSessionId session_id = to_session_id(ipc_id); - Bool handled = rime_api->process_key(session_id, keyEvent.keycode, - expand_ibus_modifier(keyEvent.mask)); - // vim_mode when keydown only - if (!handled && !(keyEvent.mask & ibus::Modifier::RELEASE_MASK)) { - bool isVimBackInCommandMode = - (keyEvent.keycode == ibus::Keycode::Escape) || - ((keyEvent.mask & (1 << 2)) && - (keyEvent.keycode == ibus::Keycode::XK_c || - keyEvent.keycode == ibus::Keycode::XK_C || - keyEvent.keycode == ibus::Keycode::XK_bracketleft)); - if (isVimBackInCommandMode && - rime_api->get_option(session_id, "vim_mode") && - !rime_api->get_option(session_id, "ascii_mode")) { - rime_api->set_option(session_id, "ascii_mode", True); - } - } - _Respond(ipc_id, eat); - _UpdateUI(ipc_id); - m_active_session = ipc_id; - return (BOOL)handled; -} - -void RimeWithWeaselHandler::CommitComposition(WeaselSessionId ipc_id) { - DLOG(INFO) << "Commit composition: ipc_id = " << ipc_id; - if (m_disabled) - return; - rime_api->commit_composition(to_session_id(ipc_id)); - _UpdateUI(ipc_id); - m_active_session = ipc_id; -} - -void RimeWithWeaselHandler::ClearComposition(WeaselSessionId ipc_id) { - DLOG(INFO) << "Clear composition: ipc_id = " << ipc_id; - if (m_disabled) - return; - rime_api->clear_composition(to_session_id(ipc_id)); - _UpdateUI(ipc_id); - m_active_session = ipc_id; -} - -void RimeWithWeaselHandler::SelectCandidateOnCurrentPage( - size_t index, - WeaselSessionId ipc_id) { - DLOG(INFO) << "select candidate on current page, ipc_id = " << ipc_id - << ", index = " << index; - if (m_disabled) - return; - rime_api->select_candidate_on_current_page(to_session_id(ipc_id), index); -} - -bool RimeWithWeaselHandler::HighlightCandidateOnCurrentPage( - size_t index, - WeaselSessionId ipc_id, - EatLine eat) { - DLOG(INFO) << "highlight candidate on current page, ipc_id = " << ipc_id - << ", index = " << index; - bool res = rime_api->highlight_candidate_on_current_page( - to_session_id(ipc_id), index); - _Respond(ipc_id, eat); - _UpdateUI(ipc_id); - return res; -} - -bool RimeWithWeaselHandler::ChangePage(bool backward, - WeaselSessionId ipc_id, - EatLine eat) { - DLOG(INFO) << "change page, ipc_id = " << ipc_id - << (backward ? "backward" : "foreward"); - bool res = rime_api->change_page(to_session_id(ipc_id), backward); - _Respond(ipc_id, eat); - _UpdateUI(ipc_id); - return res; -} - -void RimeWithWeaselHandler::FocusIn(DWORD client_caps, WeaselSessionId ipc_id) { - DLOG(INFO) << "Focus in: ipc_id = " << ipc_id - << ", client_caps = " << client_caps; - if (m_disabled) - return; - _UpdateUI(ipc_id); - m_active_session = ipc_id; -} - -void RimeWithWeaselHandler::FocusOut(DWORD param, WeaselSessionId ipc_id) { - DLOG(INFO) << "Focus out: ipc_id = " << ipc_id; - if (m_ui) - m_ui->Hide(); - m_active_session = 0; -} - -void RimeWithWeaselHandler::UpdateInputPosition(RECT const& rc, - WeaselSessionId ipc_id) { - DLOG(INFO) << "Update input position: (" << rc.left << ", " << rc.top - << "), ipc_id = " << ipc_id - << ", m_active_session = " << m_active_session; - if (m_ui) - m_ui->UpdateInputPosition(rc); - if (m_disabled) - return; - if (m_active_session != ipc_id) { - _UpdateUI(ipc_id); - m_active_session = ipc_id; - } -} - -std::string RimeWithWeaselHandler::m_message_type; -std::string RimeWithWeaselHandler::m_message_value; -std::string RimeWithWeaselHandler::m_message_label; -std::string RimeWithWeaselHandler::m_option_name; -std::mutex RimeWithWeaselHandler::m_notifier_mutex; - -void RimeWithWeaselHandler::OnNotify(void* context_object, - uintptr_t session_id, - const char* message_type, - const char* message_value) { - // may be running in a thread when deploying rime - RimeWithWeaselHandler* self = - reinterpret_cast(context_object); - if (!self || !message_type || !message_value) - return; - std::lock_guard lock(m_notifier_mutex); - m_message_type = message_type; - m_message_value = message_value; - if (RIME_API_AVAILABLE(rime_api, get_state_label) && - !strcmp(message_type, "option")) { - Bool state = message_value[0] != '!'; - const char* option_name = message_value + !state; - m_option_name = option_name; - const char* state_label = - rime_api->get_state_label(session_id, option_name, state); - if (state_label) { - m_message_label = std::string(state_label); - } - } -} - -void RimeWithWeaselHandler::_ReadClientInfo(WeaselSessionId ipc_id, - LPWSTR buffer) { - std::string app_name; - // parse request text - wbufferstream bs(buffer, WEASEL_IPC_BUFFER_LENGTH); - std::wstring line; - while (bs.good()) { - std::getline(bs, line); - if (!bs.good()) - break; - // file ends - if (line == L".") - break; - const std::wstring kClientAppKey = L"session.client_app="; - if (starts_with(line, kClientAppKey)) { - std::wstring lwr = line; - to_lower(lwr); - app_name = wtou8(lwr.substr(kClientAppKey.length())); - } - } - SessionStatus& session_status = get_session_status(ipc_id); - RimeSessionId session_id = session_status.session_id; - // set app specific options - if (!app_name.empty()) { - rime_api->set_property(session_id, "client_app", app_name.c_str()); - - auto it = m_app_options.find(app_name); - if (it != m_app_options.end()) { - AppOptions& options(m_app_options[it->first]); - for (const auto& pair : options) { - DLOG(INFO) << "set app option: " << pair.first << " = " << pair.second; - rime_api->set_option(session_id, pair.first.c_str(), Bool(pair.second)); - } - } - } - // inline preedit - bool inline_preedit = session_status.style.inline_preedit; - rime_api->set_option(session_id, "inline_preedit", Bool(inline_preedit)); - // show soft cursor on weasel panel but not inline - rime_api->set_option(session_id, "soft_cursor", Bool(!inline_preedit)); -} - -void RimeWithWeaselHandler::_GetCandidateInfo(CandidateInfo& cinfo, - RimeContext& ctx) { - cinfo.candies.resize(ctx.menu.num_candidates); - cinfo.comments.resize(ctx.menu.num_candidates); - cinfo.labels.resize(ctx.menu.num_candidates); - for (int i = 0; i < ctx.menu.num_candidates; ++i) { - cinfo.candies[i].str = escape_string(u8tow(ctx.menu.candidates[i].text)); - if (ctx.menu.candidates[i].comment) { - cinfo.comments[i].str = - escape_string(u8tow(ctx.menu.candidates[i].comment)); - } - if (RIME_STRUCT_HAS_MEMBER(ctx, ctx.select_labels) && ctx.select_labels) { - cinfo.labels[i].str = escape_string(u8tow(ctx.select_labels[i])); - } else if (ctx.menu.select_keys) { - cinfo.labels[i].str = - escape_string(std::wstring(1, ctx.menu.select_keys[i])); - } else { - cinfo.labels[i].str = std::to_wstring((i + 1) % 10); - } - } - cinfo.highlighted = ctx.menu.highlighted_candidate_index; - cinfo.currentPage = ctx.menu.page_no; - cinfo.is_last_page = ctx.menu.is_last_page; -} - -void RimeWithWeaselHandler::StartMaintenance() { - m_session_status_map.clear(); - Finalize(); - _UpdateUI(0); -} - -void RimeWithWeaselHandler::EndMaintenance() { - if (m_disabled) { - Initialize(); - _UpdateUI(0); - } - m_session_status_map.clear(); -} - -void RimeWithWeaselHandler::SetOption(WeaselSessionId ipc_id, - const std::string& opt, - bool val) { - // from no-session client, not actual typing session - if (!ipc_id) { - if (m_global_ascii_mode && opt == "ascii_mode") { - for (auto& pair : m_session_status_map) - rime_api->set_option(to_session_id(pair.first), "ascii_mode", val); - } else { - rime_api->set_option(to_session_id(m_active_session), opt.c_str(), val); - } - } else { - rime_api->set_option(to_session_id(ipc_id), opt.c_str(), val); - } -} - -void RimeWithWeaselHandler::OnUpdateUI(std::function const& cb) { - _UpdateUICallback = cb; -} - -bool RimeWithWeaselHandler::_IsDeployerRunning() { - HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); - bool deployer_detected = hMutex && GetLastError() == ERROR_ALREADY_EXISTS; - if (hMutex) { - CloseHandle(hMutex); - } - return deployer_detected; -} - -void RimeWithWeaselHandler::_UpdateUI(WeaselSessionId ipc_id) { - // if m_ui nullptr, _UpdateUI meaningless - if (!m_ui) - return; - - Status& weasel_status = m_ui->status(); - Context weasel_context; - - RimeSessionId session_id = to_session_id(ipc_id); - - if (ipc_id == 0) - weasel_status.disabled = m_disabled; - - _GetStatus(weasel_status, ipc_id, weasel_context); - - SessionStatus& session_status = get_session_status(ipc_id); - if (rime_api->get_option(session_id, "inline_preedit")) - session_status.style.client_caps |= INLINE_PREEDIT_CAPABLE; - else - session_status.style.client_caps &= ~INLINE_PREEDIT_CAPABLE; - - if (!_ShowMessage(weasel_context, weasel_status)) { - m_ui->Hide(); - m_ui->Update(weasel_context, weasel_status); - } - - _RefreshTrayIcon(session_id, _UpdateUICallback); - - { - std::lock_guard lock(m_notifier_mutex); - m_message_type.clear(); - m_message_value.clear(); - m_message_label.clear(); - m_option_name.clear(); - } -} - -void RimeWithWeaselHandler::_LoadSchemaSpecificSettings( - WeaselSessionId ipc_id, - const std::string& schema_id) { - if (!m_ui) - return; - RimeConfig config; - if (!rime_api->schema_open(schema_id.c_str(), &config)) - return; - _UpdateShowNotifications(&config); - m_ui->style() = m_base_style; - _UpdateUIStyle(&config, m_ui, false); - SessionStatus& session_status = get_session_status(ipc_id); - session_status.style = m_ui->style(); - UIStyle& style = session_status.style; - // load schema color style config - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - const auto update_color_scheme = [&]() { - std::string color_name(buffer); - RimeConfigIterator preset = {0}; - if (rime_api->config_begin_map( - &preset, &config, ("preset_color_schemes/" + color_name).c_str())) { - _UpdateUIStyleColor(&config, style, color_name); - rime_api->config_end(&preset); - } else { - RimeConfig weaselconfig; - if (rime_api->config_open("weasel", &weaselconfig)) { - _UpdateUIStyleColor(&weaselconfig, style, color_name); - rime_api->config_close(&weaselconfig); - } - } - }; - const char* key = - m_current_dark_mode ? "style/color_scheme_dark" : "style/color_scheme"; - if (rime_api->config_get_string(&config, key, buffer, BUF_SIZE)) - update_color_scheme(); - // load schema icon start - { - const auto load_icon = [](RimeConfig& config, const char* key1, - const char* key2) { - const auto user_dir = WeaselUserDataPath(); - const auto shared_dir = WeaselSharedDataPath(); - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - if (rime_api->config_get_string(&config, key1, buffer, BUF_SIZE) || - (key2 != NULL && - rime_api->config_get_string(&config, key2, buffer, BUF_SIZE))) { - auto resource = u8tow(buffer); - if (fs::is_regular_file(user_dir / resource)) - return (user_dir / resource).wstring(); - else if (fs::is_regular_file(shared_dir / resource)) - return (shared_dir / resource).wstring(); - } - return std::wstring(); - }; - style.current_zhung_icon = - load_icon(config, "schema/icon", "schema/zhung_icon"); - style.current_ascii_icon = load_icon(config, "schema/ascii_icon", NULL); - style.current_full_icon = load_icon(config, "schema/full_icon", NULL); - style.current_half_icon = load_icon(config, "schema/half_icon", NULL); - } - // load schema icon end - rime_api->config_close(&config); -} - -void RimeWithWeaselHandler::_LoadAppInlinePreeditSet(WeaselSessionId ipc_id, - bool ignore_app_name) { - SessionStatus& session_status = get_session_status(ipc_id); - RimeSessionId session_id = session_status.session_id; - static char _app_name[50]; - rime_api->get_property(session_id, "client_app", _app_name, - sizeof(_app_name) - 1); - std::string app_name(_app_name); - if (!ignore_app_name && m_last_app_name == app_name) - return; - m_last_app_name = app_name; - bool inline_preedit = session_status.style.inline_preedit; - bool found = false; - if (!app_name.empty()) { - auto it = m_app_options.find(app_name); - if (it != m_app_options.end()) { - AppOptions& options(m_app_options[it->first]); - for (const auto& pair : options) { - if (pair.first == "inline_preedit") { - rime_api->set_option(session_id, pair.first.c_str(), - Bool(pair.second)); - session_status.style.inline_preedit = Bool(pair.second); - found = true; - break; - } - } - } - } - if (!found) { - session_status.style.inline_preedit = m_base_style.inline_preedit; - // load from schema. - RIME_STRUCT(RimeStatus, status); - if (rime_api->get_status(session_id, &status)) { - std::string schema_id = status.schema_id; - RimeConfig config; - if (rime_api->schema_open(schema_id.c_str(), &config)) { - Bool value = False; - if (rime_api->config_get_bool(&config, "style/inline_preedit", - &value)) { - session_status.style.inline_preedit = value; - } - rime_api->config_close(&config); - } - rime_api->free_status(&status); - } - } - if (session_status.style.inline_preedit != inline_preedit) - _UpdateInlinePreeditStatus(ipc_id); -} - -bool RimeWithWeaselHandler::_ShowMessage(Context& ctx, Status& status) { - std::lock_guard lock(m_notifier_mutex); - if (m_message_type.empty() || m_message_value.empty()) - return m_ui->IsCountingDown(); - // show as auxiliary string - std::wstring& tips(ctx.aux.str); - bool show_icon = false; - if (m_message_type == "deploy") { - if (m_message_value == "start") - if (GetThreadUILanguage() == MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)) - tips = L"Deploying RIME"; - else - tips = L"正在部署 RIME"; - else if (m_message_value == "success") - if (GetThreadUILanguage() == MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)) - tips = L"Deployed"; - else - tips = L"部署完成"; - else if (m_message_value == "failure") { - if (GetThreadUILanguage() == - MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL)) - tips = L"有錯誤,請查看日誌 %TEMP%\\rime.weasel\\rime.weasel.*.INFO"; - else if (GetThreadUILanguage() == - MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) - tips = L"有错误,请查看日志 %TEMP%\\rime.weasel\\rime.weasel.*.INFO"; - else - tips = - L"There is an error, please check the logs " - L"%TEMP%\\rime.weasel\\rime.weasel.*.INFO"; - } - } else if (m_message_type == "schema") { - tips = /*L"【" + */ status.schema_name /* + L"】"*/; - } else if (m_message_type == "option") { - status.type = SCHEMA; - if (m_message_value == "!ascii_mode") { - show_icon = true; - } else if (m_message_value == "ascii_mode") { - show_icon = true; - } else - tips = u8tow(m_message_label); - - if (m_message_value == "full_shape" || m_message_value == "!full_shape") - status.type = FULL_SHAPE; - } - auto counter = m_ui->IsCountingDown(); - if (!show_icon && counter) - return counter; - auto foption = m_show_notifications.find(m_option_name); - auto falways = m_show_notifications.find("always"); - if ((!add_session && (foption != m_show_notifications.end() || - falways != m_show_notifications.end())) || - m_message_type == "deploy") { - m_ui->Update(ctx, status); - if (m_show_notifications_time) - m_ui->ShowWithTimeout(m_show_notifications_time); - return true; - } else { - return m_ui->IsCountingDown(); - } -} -inline std::string _GetLabelText(const std::vector& labels, - int id, - const wchar_t* format) { - wchar_t buffer[128]; - swprintf_s<128>(buffer, format, labels.at(id).str.c_str()); - return wtou8(std::wstring(buffer)); -} - -bool RimeWithWeaselHandler::_Respond(WeaselSessionId ipc_id, EatLine eat) { - std::wstring body; - body.reserve(4096); - std::vector actions; - actions.reserve(8); - - SessionStatus& session_status = get_session_status(ipc_id); - RimeSessionId session_id = session_status.session_id; - RIME_STRUCT(RimeCommit, commit); - if (rime_api->get_commit(session_id, &commit)) { - actions.push_back("commit"); - std::wstring commit_text_w = escape_string(u8tow(commit.text)); - body.append(L"commit=").append(commit_text_w).append(L"\n"); - rime_api->free_commit(&commit); - } - - bool is_composing = false; - RIME_STRUCT(RimeStatus, status); - static const std::wstring Bool_wstring[] = {L"0", L"1"}; - if (rime_api->get_status(session_id, &status)) { - is_composing = !!status.is_composing; - actions.push_back("status"); - body.append(L"status.ascii_mode=") - .append(Bool_wstring[!!status.is_ascii_mode]) - .append(L"\n") - .append(L"status.composing=") - .append(Bool_wstring[!!status.is_composing]) - .append(L"\n") - .append(L"status.disabled=") - .append(Bool_wstring[!!status.is_disabled]) - .append(L"\n") - .append(L"status.full_shape=") - .append(Bool_wstring[!!status.is_full_shape]) - .append(L"\n") - .append(L"status.schema_id=") - .append(status.schema_id ? u8tow(status.schema_id) : std::wstring()) - .append(L"\n"); - if (m_global_ascii_mode && - (session_status.status.is_ascii_mode != status.is_ascii_mode)) { - for (auto& pair : m_session_status_map) { - if (pair.first != ipc_id) - rime_api->set_option(to_session_id(pair.first), "ascii_mode", - !!status.is_ascii_mode); - } - } - session_status.status = status; - rime_api->free_status(&status); - } - - RIME_STRUCT(RimeContext, ctx); - if (rime_api->get_context(session_id, &ctx)) { - bool has_candidates = ctx.menu.num_candidates > 0; - CandidateInfo cinfo; - if (has_candidates) { - _GetCandidateInfo(cinfo, ctx); - } - if (is_composing) { - const auto& preedit = ctx.composition.preedit; - const auto& start = ctx.composition.sel_start; - const auto& end = ctx.composition.sel_end; - const auto& cursor = ctx.composition.cursor_pos; - static const auto u8towstring = [](const char* u8str, int len = 0) { - return std::to_wstring(utf8towcslen(u8str, len)); - }; - actions.push_back("ctx"); - switch (session_status.style.preedit_type) { - case UIStyle::PREVIEW: { - if (ctx.commit_text_preview) { - const char* first_utf8 = ctx.commit_text_preview; - const size_t first_len = std::strlen(first_utf8); - const std::wstring first_w = escape_string(u8tow(first_utf8)); - const std::wstring tmp = u8towstring(first_utf8, (int)first_len); - body.append(L"ctx.preedit=") - .append(first_w) - .append(L"\n") - .append(L"ctx.preedit.cursor=") - .append(u8towstring(first_utf8, 0)) - .append(L",") - .append(tmp) - .append(L",") - .append(tmp) - .append(L"\n"); - break; - } - // no preview, fall back to composition - } - case UIStyle::COMPOSITION: { - body.append(L"ctx.preedit=") - .append(escape_string(u8tow(preedit))) - .append(L"\n"); - if (start <= end) { - body.append(L"ctx.preedit.cursor=") - .append(u8towstring(preedit, start)) - .append(L",") - .append(u8towstring(preedit, end)) - .append(L",") - .append(u8towstring(preedit, cursor)) - .append(L"\n"); - } - break; - } - case UIStyle::PREVIEW_ALL: { - body.append(L"ctx.preedit=") - .append(escape_string(u8tow(preedit))) - .append(L" ["); - auto label_valid = session_status.style.label_font_point > 0; - auto comment_valid = session_status.style.comment_font_point > 0; - const std::wstring mark_text_w = - session_status.style.mark_text.empty() - ? std::wstring(L"*") - : session_status.style.mark_text; - for (auto i = 0; i < ctx.menu.num_candidates; i++) { - std::wstring label_w; - if (label_valid) { - wchar_t buf_lbl[128]; - swprintf_s<128>(buf_lbl, - session_status.style.label_text_format.c_str(), - cinfo.labels.at(i).str.c_str()); - label_w = std::wstring(buf_lbl); - } - std::wstring comment_w = - comment_valid ? cinfo.comments.at(i).str : std::wstring(); - std::wstring prefix_w = (i != ctx.menu.highlighted_candidate_index) - ? std::wstring() - : mark_text_w; - body.append(L" ") - .append(prefix_w) - .append(escape_string(label_w)) - .append(escape_string(u8tow(ctx.menu.candidates[i].text))) - .append(L" ") - .append(escape_string(comment_w)); - } - body.append(L" ]\n"); - if (start <= end) { - body.append(L"ctx.preedit.cursor=") - .append(u8towstring(preedit, start)) - .append(L",") - .append(u8towstring(preedit, end)) - .append(L",") - .append(u8towstring(preedit, cursor)) - .append(L"\n"); - } - break; - } - } - } - if (has_candidates) { - std::wstringstream ss; - boost::archive::text_woarchive oa(ss); - - oa << cinfo; - - auto s = ss.str(); - body.append(L"ctx.cand=").append(std::move(s)).append(L"\n"); - } - rime_api->free_context(&ctx); - } - - // configuration information - actions.push_back("config"); - body.append(L"config.inline_preedit=") - .append(std::to_wstring((int)session_status.style.inline_preedit)) - .append(L"\n"); - - // style - if (!session_status.__synced) { - std::wstringstream ss; - boost::archive::text_woarchive oa(ss); - oa << session_status.style; - - actions.push_back("style"); - body.append(L"style=").append(ss.str()).append(L"\n"); - session_status.__synced = true; - } - - // summarize: send header first to avoid vector head-insert cost - std::wstring header; - if (actions.empty()) { - header = L"action=noop\n"; - } else { - std::string actionList; - actionList.reserve(64); - for (size_t i = 0; i < actions.size(); ++i) { - if (i > 0) - actionList += ','; - actionList += actions[i]; - } - header = std::wstring(L"action=") + u8tow(actionList) + L"\n"; - } - if (!eat(header)) - return false; - - body.append(L".\n"); - if (!eat(body)) - return false; - - return true; -} - -// Blend foreground and background ARGB colors taking alpha into account. -// Returns an ABGR COLORREF with premultiplied alpha blended result. -static inline COLORREF blend_colors(COLORREF fcolor, COLORREF bcolor) { - // Extract ARGB channels from both colors. - BYTE fA = (fcolor >> 24) & 0xFF; - BYTE fB = (fcolor >> 16) & 0xFF; - BYTE fG = (fcolor >> 8) & 0xFF; - BYTE fR = fcolor & 0xFF; - BYTE bA = (bcolor >> 24) & 0xFF; - BYTE bB = (bcolor >> 16) & 0xFF; - BYTE bG = (bcolor >> 8) & 0xFF; - BYTE bR = bcolor & 0xFF; - // Convert alpha to [0,1] - float fAlpha = fA / 255.0f; - float bAlpha = bA / 255.0f; - // Result alpha - float retAlpha = fAlpha + (1 - fAlpha) * bAlpha; - if (retAlpha <= 1e-6f) { - // Fully transparent result — return background unchanged as fallback. - return bcolor; - } - auto mix = [&](float fc, float bc) -> BYTE { - return static_cast((fc * fAlpha + bc * bAlpha * (1 - fAlpha)) / - retAlpha); - }; - BYTE retR = mix(fR, bR); - BYTE retG = mix(fG, bG); - BYTE retB = mix(fB, bB); - BYTE outA = static_cast(retAlpha * 255.0f); - return (static_cast(outA) << 24) | (retB << 16) | (retG << 8) | - retR; -} -// parse color value, with fallback value -static Bool _RimeGetColor(RimeConfig* config, - const std::string& key, - int& value, - const ColorFormat& fmt, - const unsigned int& fallback) { - char color[256] = {0}; - if (!rime_api->config_get_string(config, key.c_str(), color, 256)) { - value = fallback; - return False; - } - const auto color_str = std::string(color); - // adjudge if str is 0x 0X # hex color format, return trimmed hex part - // out part is 6 or 8 length hex string without white space - const auto parse_color_code = [](const std::string& str, std::string& out) { - if (str.empty()) - return false; - size_t start = 0; - if (str[0] == '#') { - start = 1; - } else if (str.size() >= 2 && - (str.compare(0, 2, "0x") == 0 || str.compare(0, 2, "0X") == 0)) { - start = 2; - } else { - return false; - } - const std::string hex_part = str.substr(start); - if (hex_part.empty()) - return false; - if ((start == 1 || start == 2) && hex_part.length() != 3 && - hex_part.length() != 4 && hex_part.length() != 6 && - hex_part.length() != 8) { - return false; - } - for (char c : hex_part) { - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || - (c >= 'A' && c <= 'F'))) - return false; - } - out = str.substr(start).substr(0, 8); -#define _2C(c) std::string(2, c) - if (out.size() == 3) - out = _2C(out[0]) + _2C(out[1]) + _2C(out[2]); - else if (out.size() == 4) - out = _2C(out[0]) + _2C(out[1]) + _2C(out[2]) + _2C(out[3]); -#undef _2C - return true; - }; - auto hex_color = std::string(); - if (parse_color_code(color_str, hex_color)) { - value = std::stoul(hex_color, 0, 16); - if (hex_color.length() == 6) - value = (fmt != COLOR_RGBA) ? (value | 0xff000000) - : (((unsigned int)value << 8) | 0x000000ff); - } else { - if (!rime_api->config_get_int(config, key.c_str(), &value)) { - value = fallback; - return False; - } - if (value <= 0xffffff) - value = (fmt != COLOR_RGBA) ? (value | 0xff000000) - : (((unsigned int)value << 8) | 0x000000ff); - else if (value > 0xffffffff) - value &= 0xffffffff; - } - if (fmt == COLOR_ARGB) - value = ARGB2ABGR(value); - else if (fmt == COLOR_RGBA) - value = RGBA2ABGR(value); - value &= 0xffffffff; - return True; -} - -template -using Array = std::array, N>; - -// parset bool type configuration to T type value trueValue / falseValue -template -void _RimeGetBool(RimeConfig* config, - const char* key, - bool cond, - T& value, - const T& trueValue = true, - const T& falseValue = false) { - Bool tempb = False; - if (rime_api->config_get_bool(config, key, &tempb) || cond) - value = (!!tempb) ? trueValue : falseValue; -} -// parse string option to T type value, with fallback -template -void _RimeParseStringOptWithFallback(RimeConfig* config, - const char* key, - T& value, - const Array& arr, - const T& fallback) { - char str_buff[256] = {0}; - if (rime_api->config_get_string(config, key, str_buff, 255)) { - for (size_t i = 0; i < N; ++i) { - if (strcmp(arr[i].first, str_buff) == 0) { - value = arr[i].second; - return; - } - } - } - value = fallback; -} - -template -void _RimeGetIntStr(RimeConfig* config, - const char* key, - T& value, - const char* fb_key = nullptr, - const void* fb_value = nullptr, - const std::function& func = nullptr) { - if constexpr (std::is_same::value) { - if (!rime_api->config_get_int(config, key, &value) && fb_key != 0) - rime_api->config_get_int(config, fb_key, &value); - } else if constexpr (std::is_same::value) { - const int BUF_SIZE = 2047; - char buffer[BUF_SIZE + 1] = {0}; - if (rime_api->config_get_string(config, key, buffer, BUF_SIZE) || - rime_api->config_get_string(config, fb_key, buffer, BUF_SIZE)) { - value = u8tow(buffer); - } else if (fb_value) { - value = *(T*)fb_value; - } - } - if (func) - func(value); -} - -// Helper to iterate a Rime map and invoke callback with key/path -static void ForEachRimeMap( - RimeConfig* config, - const std::string& path, - const std::function& cb) { - RimeConfigIterator iter; - if (!rime_api->config_begin_map(&iter, config, path.c_str())) - return; - while (rime_api->config_next(&iter)) { - cb(iter.key, iter.path); - } - rime_api->config_end(&iter); -} - -// Helper to iterate a Rime list and invoke callback with item path -static void ForEachRimeList( - RimeConfig* config, - const std::string& path, - const std::function& cb) { - RimeConfigIterator iter; - if (!rime_api->config_begin_list(&iter, config, path.c_str())) - return; - while (rime_api->config_next(&iter)) { - cb(iter.path); - } - rime_api->config_end(&iter); -} - -void RimeWithWeaselHandler::_UpdateShowNotifications(RimeConfig* config, - bool initialize) { - Bool show_notifications = true; - if (initialize) - m_show_notifications_base.clear(); - m_show_notifications.clear(); - - if (rime_api->config_get_bool(config, "show_notifications", - &show_notifications)) { - // config read as bool, for global all on or off - if (show_notifications) - m_show_notifications["always"] = true; - if (initialize) - m_show_notifications_base = m_show_notifications; - } else { - // read as list using helper - ForEachRimeList(config, "show_notifications", [&](const char* item_path) { - char buffer[256] = {0}; - if (rime_api->config_get_string(config, item_path, buffer, 256)) - m_show_notifications[std::string(buffer)] = true; - }); - if (initialize) - m_show_notifications_base = m_show_notifications; - if (m_show_notifications.empty()) { - // not configured, or incorrect type - if (initialize) - m_show_notifications_base["always"] = true; - m_show_notifications = m_show_notifications_base; - } - } -} - -// update ui's style parameters, ui has been check before referenced -static void _UpdateUIStyle(RimeConfig* config, UI* ui, bool initialize) { - UIStyle& style(ui->style()); - const std::function rmspace = [](std::wstring& str) { - str = std::regex_replace(str, std::wregex(L"\\s*(,|:|^|$)\\s*"), L"$1"); - }; - const std::function _abs = [](int& value) { value = abs(value); }; - // get font faces - _RimeGetIntStr(config, "style/font_face", style.font_face, 0, 0, rmspace); - std::wstring* const pFallbackFontFace = initialize ? &style.font_face : NULL; - _RimeGetIntStr(config, "style/label_font_face", style.label_font_face, 0, - pFallbackFontFace, rmspace); - _RimeGetIntStr(config, "style/comment_font_face", style.comment_font_face, 0, - pFallbackFontFace, rmspace); - // able to set label font/comment font empty, force fallback to font face. - if (style.label_font_face.empty()) - style.label_font_face = style.font_face; - if (style.comment_font_face.empty()) - style.comment_font_face = style.font_face; - // get font points - _RimeGetIntStr(config, "style/font_point", style.font_point); - if (style.font_point <= 0) - style.font_point = 12; - _RimeGetIntStr(config, "style/label_font_point", style.label_font_point, - "style/font_point", 0, _abs); - _RimeGetIntStr(config, "style/comment_font_point", style.comment_font_point, - "style/font_point", 0, _abs); - _RimeGetIntStr(config, "style/candidate_abbreviate_length", - style.candidate_abbreviate_length, 0, 0, _abs); - _RimeGetBool(config, "style/inline_preedit", initialize, - style.inline_preedit); - _RimeGetBool(config, "style/vertical_auto_reverse", initialize, - style.vertical_auto_reverse); - static constexpr Array _preeditArr = { - {{"composition", UIStyle::COMPOSITION}, - {"preview", UIStyle::PREVIEW}, - {"preview_all", UIStyle::PREVIEW_ALL}}}; - _RimeParseStringOptWithFallback(config, "style/preedit_type", - style.preedit_type, _preeditArr, - style.preedit_type); - static constexpr Array _aliasModeArr = { - {{"force_dword", UIStyle::FORCE_DWORD}, - {"cleartype", UIStyle::CLEARTYPE}, - {"grayscale", UIStyle::GRAYSCALE}, - {"aliased", UIStyle::ALIASED}, - {"default", UIStyle::DEFAULT}}}; - _RimeParseStringOptWithFallback(config, "style/antialias_mode", - style.antialias_mode, _aliasModeArr, - style.antialias_mode); - static constexpr Array _hoverTypeArr = { - {{"none", UIStyle::HoverType::NONE}, - {"semi_hilite", UIStyle::HoverType::SEMI_HILITE}, - {"hilite", UIStyle::HoverType::HILITE}}}; - _RimeParseStringOptWithFallback(config, "style/hover_type", style.hover_type, - _hoverTypeArr, style.hover_type); - static constexpr Array _alignType = { - {{"top", UIStyle::ALIGN_TOP}, - {"center", UIStyle::ALIGN_CENTER}, - {"bottom", UIStyle::ALIGN_BOTTOM}}}; - _RimeParseStringOptWithFallback(config, "style/layout/align_type", - style.align_type, _alignType, - style.align_type); - _RimeGetBool(config, "style/display_tray_icon", initialize, - style.display_tray_icon); - _RimeGetBool(config, "style/ascii_tip_follow_cursor", initialize, - style.ascii_tip_follow_cursor); - _RimeGetBool(config, "style/horizontal", initialize, style.layout_type, - UIStyle::LAYOUT_HORIZONTAL, UIStyle::LAYOUT_VERTICAL); - _RimeGetBool(config, "style/paging_on_scroll", initialize, - style.paging_on_scroll); - _RimeGetBool(config, "style/click_to_capture", initialize, - style.click_to_capture, true, false); - _RimeGetBool(config, "style/fullscreen", false, style.layout_type, - ((style.layout_type == UIStyle::LAYOUT_HORIZONTAL) - ? UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN - : UIStyle::LAYOUT_VERTICAL_FULLSCREEN), - style.layout_type); - _RimeGetBool(config, "style/vertical_text", false, style.layout_type, - UIStyle::LAYOUT_VERTICAL_TEXT, style.layout_type); - _RimeGetBool(config, "style/vertical_text_left_to_right", false, - style.vertical_text_left_to_right); - _RimeGetBool(config, "style/vertical_text_with_wrap", false, - style.vertical_text_with_wrap); - static constexpr Array _text_orientation = { - {{"horizontal", false}, {"vertical", true}}}; - bool _text_orientation_bool = false; - _RimeParseStringOptWithFallback(config, "style/text_orientation", - _text_orientation_bool, _text_orientation, - _text_orientation_bool); - if (_text_orientation_bool) - style.layout_type = UIStyle::LAYOUT_VERTICAL_TEXT; - _RimeGetIntStr(config, "style/label_format", style.label_text_format); - _RimeGetIntStr(config, "style/mark_text", style.mark_text); - _RimeGetIntStr(config, "style/layout/baseline", style.baseline, 0, 0, _abs); - _RimeGetIntStr(config, "style/layout/linespacing", style.linespacing, 0, 0, - _abs); - _RimeGetIntStr(config, "style/layout/min_width", style.min_width, 0, 0, _abs); - _RimeGetIntStr(config, "style/layout/max_width", style.max_width, 0, 0, _abs); - _RimeGetIntStr(config, "style/layout/min_height", style.min_height, 0, 0, - _abs); - _RimeGetIntStr(config, "style/layout/max_height", style.max_height, 0, 0, - _abs); - // layout (alternative to style/horizontal) - static constexpr Array _layoutArr = { - {{"vertical", UIStyle::LAYOUT_VERTICAL}, - {"horizontal", UIStyle::LAYOUT_HORIZONTAL}, - {"vertical_text", UIStyle::LAYOUT_VERTICAL_TEXT}, - {"vertical+fullscreen", UIStyle::LAYOUT_VERTICAL_FULLSCREEN}, - {"horizontal+fullscreen", UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN}}}; - _RimeParseStringOptWithFallback(config, "style/layout/type", - style.layout_type, _layoutArr, - style.layout_type); - // disable max_width when full screen - if (style.layout_type == UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN || - style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN) { - style.max_width = 0; - style.inline_preedit = false; - } - _RimeGetIntStr(config, "style/layout/border", style.border, - "style/layout/border_width", 0, _abs); - _RimeGetIntStr(config, "style/layout/margin_x", style.margin_x); - _RimeGetIntStr(config, "style/layout/margin_y", style.margin_y); - _RimeGetIntStr(config, "style/layout/spacing", style.spacing, 0, 0, _abs); - _RimeGetIntStr(config, "style/layout/candidate_spacing", - style.candidate_spacing, 0, 0, _abs); - _RimeGetIntStr(config, "style/layout/hilite_spacing", style.hilite_spacing, 0, - 0, _abs); - _RimeGetIntStr(config, "style/layout/hilite_padding_x", - style.hilite_padding_x, "style/layout/hilite_padding", 0, - _abs); - _RimeGetIntStr(config, "style/layout/hilite_padding_y", - style.hilite_padding_y, "style/layout/hilite_padding", 0, - _abs); - _RimeGetIntStr(config, "style/layout/shadow_radius", style.shadow_radius, 0, - 0, _abs); - // disable shadow for fullscreen layout - style.shadow_radius *= - (!(style.layout_type == UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN || - style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN)); - _RimeGetIntStr(config, "style/layout/shadow_offset_x", style.shadow_offset_x); - _RimeGetIntStr(config, "style/layout/shadow_offset_y", style.shadow_offset_y); - // round_corner as alias of hilited_corner_radius - _RimeGetIntStr(config, "style/layout/hilited_corner_radius", - style.round_corner, "style/layout/round_corner", 0, _abs); - // corner_radius not set, fallback to round_corner - _RimeGetIntStr(config, "style/layout/corner_radius", style.round_corner_ex, - "style/layout/round_corner", 0, _abs); - // fix padding and spacing settings - if (style.layout_type != UIStyle::LAYOUT_VERTICAL_TEXT) { - // hilite_padding vs spacing - // if hilite_padding over spacing, increase spacing - style.spacing = max(style.spacing, style.hilite_padding_y * 2); - // hilite_padding vs candidate_spacing - if (style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN || - style.layout_type == UIStyle::LAYOUT_VERTICAL) { - // vertical, if hilite_padding_y over candidate spacing, - // increase candidate spacing - style.candidate_spacing = - max(style.candidate_spacing, style.hilite_padding_y * 2); - } else { - // horizontal, if hilite_padding_x over candidate - // spacing, increase candidate spacing - style.candidate_spacing = - max(style.candidate_spacing, style.hilite_padding_x * 2); - } - // hilite_padding_x vs hilite_spacing - if (!style.inline_preedit) - style.hilite_spacing = max(style.hilite_spacing, style.hilite_padding_x); - } else // LAYOUT_VERTICAL_TEXT - { - // hilite_padding_x vs spacing - // if hilite_padding over spacing, increase spacing - style.spacing = max(style.spacing, style.hilite_padding_x * 2); - // hilite_padding vs candidate_spacing - // if hilite_padding_x over candidate - // spacing, increase candidate spacing - style.candidate_spacing = - max(style.candidate_spacing, style.hilite_padding_x * 2); - // vertical_text_with_wrap and hilite_padding_y over candidate_spacing - if (style.vertical_text_with_wrap) - style.candidate_spacing = - max(style.candidate_spacing, style.hilite_padding_y * 2); - // hilite_padding_y vs hilite_spacing - if (!style.inline_preedit) - style.hilite_spacing = max(style.hilite_spacing, style.hilite_padding_y); - } - // fix padding and margin settings - int scale = style.margin_x < 0 ? -1 : 1; - style.margin_x = scale * max(style.hilite_padding_x, abs(style.margin_x)); - scale = style.margin_y < 0 ? -1 : 1; - style.margin_y = scale * max(style.hilite_padding_y, abs(style.margin_y)); - // get enhanced_position - _RimeGetBool(config, "style/enhanced_position", initialize, - style.enhanced_position, true, false); - // get color scheme - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - if (initialize && rime_api->config_get_string(config, "style/color_scheme", - buffer, BUF_SIZE)) - _UpdateUIStyleColor(config, style); -} -// load color configs to style, by "style/color_scheme" or specific scheme name -// "color" which is default empty -static bool _UpdateUIStyleColor(RimeConfig* config, - UIStyle& style, - const std::string& color) { - const int BUF_SIZE = 255; - char buffer[BUF_SIZE + 1] = {0}; - std::string color_mark = "style/color_scheme"; - // color scheme - if (rime_api->config_get_string(config, color_mark.c_str(), buffer, - BUF_SIZE) || - !color.empty()) { - std::string prefix("preset_color_schemes/"); - prefix += (color.empty()) ? buffer : color; - // define color format, default abgr if not set - ColorFormat fmt = COLOR_ABGR; - static constexpr Array _colorFmt = { - {{"argb", COLOR_ARGB}, {"rgba", COLOR_RGBA}, {"abgr", COLOR_ABGR}}}; - _RimeParseStringOptWithFallback(config, (prefix + "/color_format").c_str(), - fmt, _colorFmt, COLOR_ABGR); -#define COLOR(key, value, fallback) \ - _RimeGetColor(config, (prefix + "/" + key), value, fmt, fallback) - COLOR("back_color", style.back_color, 0xffffffff); - COLOR("shadow_color", style.shadow_color, 0); - COLOR("prevpage_color", style.prevpage_color, 0); - COLOR("nextpage_color", style.nextpage_color, 0); - COLOR("text_color", style.text_color, 0xff000000); - COLOR("candidate_text_color", style.candidate_text_color, style.text_color); - COLOR("candidate_back_color", style.candidate_back_color, 0); - COLOR("border_color", style.border_color, style.text_color); - COLOR("hilited_text_color", style.hilited_text_color, style.text_color); - COLOR("hilited_back_color", style.hilited_back_color, style.back_color); - COLOR("hilited_candidate_text_color", style.hilited_candidate_text_color, - style.hilited_text_color); - COLOR("hilited_candidate_back_color", style.hilited_candidate_back_color, - style.hilited_back_color); - COLOR("hilited_candidate_shadow_color", - style.hilited_candidate_shadow_color, 0); - COLOR("hilited_shadow_color", style.hilited_shadow_color, 0); - COLOR("candidate_shadow_color", style.candidate_shadow_color, 0); - COLOR("candidate_border_color", style.candidate_border_color, 0); - COLOR("hilited_candidate_border_color", - style.hilited_candidate_border_color, 0); - COLOR("label_color", style.label_text_color, - blend_colors(style.candidate_text_color, style.candidate_back_color)); - COLOR("hilited_label_color", style.hilited_label_text_color, - blend_colors(style.hilited_candidate_text_color, - style.hilited_candidate_back_color)); - COLOR("comment_text_color", style.comment_text_color, - style.label_text_color); - COLOR("hilited_comment_text_color", style.hilited_comment_text_color, - style.hilited_label_text_color); - COLOR("hilited_mark_color", style.hilited_mark_color, 0); -#undef COLOR - return true; - } - return false; -} -static void _LoadAppOptions(RimeConfig* config, - AppOptionsByAppName& app_options) { - app_options.clear(); - ForEachRimeMap( - config, "app_options", [&](const char* app_key, const char* app_path) { - AppOptions& options(app_options[app_key]); - ForEachRimeMap( - config, app_path, [&](const char* opt_key, const char* opt_path) { - Bool value = False; - if (rime_api->config_get_bool(config, opt_path, &value)) { - options[opt_key] = !!value; - } - }); - }); -} - -void RimeWithWeaselHandler::_GetStatus(Status& stat, - WeaselSessionId ipc_id, - Context& ctx) { - SessionStatus& session_status = get_session_status(ipc_id); - RimeSessionId session_id = session_status.session_id; - RIME_STRUCT(RimeStatus, status); - if (rime_api->get_status(session_id, &status)) { - std::string schema_id = ""; - if (status.schema_id) - schema_id = status.schema_id; - stat.schema_name = u8tow(status.schema_name); - stat.schema_id = u8tow(status.schema_id); - stat.ascii_mode = !!status.is_ascii_mode; - stat.composing = !!status.is_composing; - stat.disabled = !!status.is_disabled; - stat.full_shape = !!status.is_full_shape; - if (schema_id != m_last_schema_id) { - session_status.__synced = false; - m_last_schema_id = schema_id; - if (schema_id != ".default") { // don't load for schema select menu - bool inline_preedit = session_status.style.inline_preedit; - _LoadSchemaSpecificSettings(ipc_id, schema_id); - _LoadAppInlinePreeditSet(ipc_id, true); - if (session_status.style.inline_preedit != inline_preedit) - // in case of inline_preedit set in schema - _UpdateInlinePreeditStatus(ipc_id); - // refresh icon after schema changed - _RefreshTrayIcon(session_id, _UpdateUICallback); - m_ui->style() = session_status.style; - if (m_show_notifications.find("schema") != m_show_notifications.end() && - m_show_notifications_time > 0) { - ctx.aux.str = stat.schema_name; - m_ui->Update(ctx, stat); - m_ui->ShowWithTimeout(m_show_notifications_time); - } - } - } - rime_api->free_status(&status); - } -} - -void RimeWithWeaselHandler::_GetContext(Context& weasel_context, - RimeSessionId session_id) { - RIME_STRUCT(RimeContext, ctx); - if (rime_api->get_context(session_id, &ctx)) { - if (ctx.composition.length > 0) { - weasel_context.preedit.str = u8tow(ctx.composition.preedit); - if (ctx.composition.sel_start < ctx.composition.sel_end) { - TextAttribute attr; - attr.type = HIGHLIGHTED; - attr.range.start = - utf8towcslen(ctx.composition.preedit, ctx.composition.sel_start); - attr.range.end = - utf8towcslen(ctx.composition.preedit, ctx.composition.sel_end); - - weasel_context.preedit.attributes.push_back(attr); - } - } - if (ctx.menu.num_candidates) { - CandidateInfo& cinfo(weasel_context.cinfo); - _GetCandidateInfo(cinfo, ctx); - } - rime_api->free_context(&ctx); - } -} - -void RimeWithWeaselHandler::_UpdateInlinePreeditStatus(WeaselSessionId ipc_id) { - if (!m_ui) - return; - SessionStatus& session_status = get_session_status(ipc_id); - RimeSessionId session_id = session_status.session_id; - // set inline_preedit option - bool inline_preedit = session_status.style.inline_preedit; - rime_api->set_option(session_id, "inline_preedit", Bool(inline_preedit)); - // show soft cursor on weasel panel but not inline - rime_api->set_option(session_id, "soft_cursor", Bool(!inline_preedit)); -} +#include "stdafx.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TRANSPARENT_COLOR 0x00000000 +#define ARGB2ABGR(value) \ + ((value & 0xff000000) | ((value & 0x000000ff) << 16) | \ + (value & 0x0000ff00) | ((value & 0x00ff0000) >> 16)) +#define RGBA2ABGR(value) \ + (((value & 0xff) << 24) | ((value & 0xff000000) >> 24) | \ + ((value & 0x00ff0000) >> 8) | ((value & 0x0000ff00) << 8)) +typedef enum { COLOR_ABGR = 0, COLOR_ARGB, COLOR_RGBA } ColorFormat; + +using namespace weasel; + +static RimeApi* rime_api; +WeaselSessionId _GenerateNewWeaselSessionId(SessionStatusMap sm, DWORD pid) { + if (sm.empty()) + return (WeaselSessionId)(pid + 1); + return (WeaselSessionId)(sm.rbegin()->first + 1); +} + +int expand_ibus_modifier(int m) { + return (m & 0xff) | ((m & 0xff00) << 16); +} + +RimeWithWeaselHandler::RimeWithWeaselHandler(UI* ui) + : m_ui(ui), + m_active_session(0), + m_disabled(true), + m_current_dark_mode(false), + m_global_ascii_mode(false), + m_show_notifications_time(1200), + _UpdateUICallback(NULL), + _ServiceNotificationCallback(NULL), + m_tray_icon_signature() { + m_ui->InServer() = true; + rime_api = rime_get_api(); + assert(rime_api); + m_pid = GetCurrentProcessId(); + uint16_t msbit = 0; + for (auto i = 31; i >= 0; i--) { + if (m_pid & (1 << i)) { + msbit = i; + break; + } + } + m_pid = (m_pid << (31 - msbit)); + _Setup(); +} + +RimeWithWeaselHandler::~RimeWithWeaselHandler() { + m_show_notifications.clear(); + m_session_status_map.clear(); + m_app_options.clear(); +} + +bool add_session = false; +void _UpdateUIStyle(RimeConfig* config, UI* ui, bool initialize); +bool _UpdateUIStyleColor(RimeConfig* config, + UIStyle& style, + const std::string& color = std::string()); +void _LoadAppOptions(RimeConfig* config, AppOptionsByAppName& app_options); + +void _RefreshTrayIcon(const RimeSessionId session_id, + const std::function _UpdateUICallback) { + // Dangerous, don't touch + static char app_name[256] = {0}; + auto ret = rime_api->get_property(session_id, "client_app", app_name, + sizeof(app_name) - 1); + if (!ret || u8tow(app_name) == std::wstring(L"explorer.exe")) + boost::thread th([=]() { + ::Sleep(100); + if (_UpdateUICallback) + _UpdateUICallback(); + }); + else if (_UpdateUICallback) + _UpdateUICallback(); +} + +void RimeWithWeaselHandler::_Setup() { + RIME_STRUCT(RimeTraits, weasel_traits); + std::string shared_dir = wtou8(WeaselSharedDataPath().wstring()); + std::string user_dir = wtou8(WeaselUserDataPath().wstring()); + weasel_traits.shared_data_dir = shared_dir.c_str(); + weasel_traits.user_data_dir = user_dir.c_str(); + weasel_traits.prebuilt_data_dir = weasel_traits.shared_data_dir; + std::string distribution_name = wtou8(get_weasel_ime_name()); + weasel_traits.distribution_name = distribution_name.c_str(); + weasel_traits.distribution_code_name = WEASEL_CODE_NAME; + weasel_traits.distribution_version = WEASEL_VERSION; + weasel_traits.app_name = "rime.weasel"; + std::string log_dir = WeaselLogPath().u8string(); + weasel_traits.log_dir = log_dir.c_str(); + rime_api->setup(&weasel_traits); + rime_api->set_notification_handler(&RimeWithWeaselHandler::OnNotify, this); +} + +void RimeWithWeaselHandler::Initialize() { + m_disabled = _IsDeployerRunning(); + if (m_disabled) { + return; + } + + LOG(INFO) << "Initializing la rime."; + rime_api->initialize(NULL); + if (rime_api->start_maintenance(/*full_check = */ False)) { + m_disabled = true; + rime_api->join_maintenance_thread(); + } + + RimeConfig config = {NULL}; + if (rime_api->config_open("weasel", &config)) { + if (m_ui) { + _UpdateUIStyle(&config, m_ui, true); + _UpdateShowNotifications(&config, true); + m_current_dark_mode = IsUserDarkMode(); + if (m_current_dark_mode) { + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + if (rime_api->config_get_string(&config, "style/color_scheme_dark", + buffer, BUF_SIZE)) { + std::string color_name(buffer); + _UpdateUIStyleColor(&config, m_ui->style(), color_name); + } + } + m_base_style = m_ui->style(); + } + Bool global_ascii = false; + if (rime_api->config_get_bool(&config, "global_ascii", &global_ascii)) + m_global_ascii_mode = !!global_ascii; + if (!rime_api->config_get_int(&config, "show_notifications_time", + &m_show_notifications_time)) + m_show_notifications_time = 1200; + _LoadAppOptions(&config, m_app_options); + rime_api->config_close(&config); + } + m_last_schema_id.clear(); +} + +void RimeWithWeaselHandler::Finalize() { + m_active_session = 0; + m_disabled = true; + m_session_status_map.clear(); + LOG(INFO) << "Finalizing la rime."; + rime_api->finalize(); +} + +DWORD RimeWithWeaselHandler::FindSession(WeaselSessionId ipc_id) { + if (m_disabled) + return 0; + auto it = m_session_status_map.find(ipc_id); + if (it == m_session_status_map.end()) + return 0; + RimeSessionId session_id = it->second.session_id; + Bool found = session_id && rime_api->find_session(session_id); + DLOG(INFO) << "Find session: session_id = " << session_id + << ", found = " << found; + return found ? (ipc_id) : 0; +} + +DWORD RimeWithWeaselHandler::AddSession(LPWSTR buffer, EatLine eat) { + if (m_disabled) { + DLOG(INFO) << "Trying to resume service."; + EndMaintenance(); + if (m_disabled) + return 0; + } + RimeSessionId session_id = (RimeSessionId)rime_api->create_session(); + if (m_global_ascii_mode) { + for (const auto& pair : m_session_status_map) { + if (pair.first) { + rime_api->set_option(session_id, "ascii_mode", + !!pair.second.status.is_ascii_mode); + break; + } + } + } + + WeaselSessionId ipc_id = + _GenerateNewWeaselSessionId(m_session_status_map, m_pid); + DLOG(INFO) << "Add session: created session_id = " << session_id + << ", ipc_id = " << ipc_id; + SessionStatus& session_status = new_session_status(ipc_id); + session_status.style = m_base_style; + session_status.session_id = session_id; + _ReadClientInfo(ipc_id, buffer); + + RIME_STRUCT(RimeStatus, status); + if (rime_api->get_status(session_id, &status)) { + std::string schema_id = status.schema_id; + m_last_schema_id = schema_id; + _LoadSchemaSpecificSettings(ipc_id, schema_id); + _LoadAppInlinePreeditSet(ipc_id, true); + _UpdateInlinePreeditStatus(ipc_id); + session_status.status = status; + session_status.__synced = false; + rime_api->free_status(&status); + } + m_ui->style() = session_status.style; + // show session's welcome message :-) if any + if (eat) { + _Respond(ipc_id, eat); + } + add_session = true; + _UpdateUI(ipc_id); + add_session = false; + m_active_session = ipc_id; + return ipc_id; +} + +DWORD RimeWithWeaselHandler::RemoveSession(WeaselSessionId ipc_id) { + if (m_ui) + m_ui->Hide(); + if (m_disabled) + return 0; + DLOG(INFO) << "Remove session: session_id = " << to_session_id(ipc_id); + // TODO: force committing? otherwise current composition would be lost + rime_api->destroy_session(to_session_id(ipc_id)); + m_session_status_map.erase(ipc_id); + m_active_session = 0; + return 0; +} + +void RimeWithWeaselHandler::UpdateColorTheme(BOOL darkMode) { + RimeConfig config = {NULL}; + if (rime_api->config_open("weasel", &config)) { + if (m_ui) { + _UpdateUIStyle(&config, m_ui, true); + m_current_dark_mode = darkMode; + if (darkMode) { + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + if (rime_api->config_get_string(&config, "style/color_scheme_dark", + buffer, BUF_SIZE)) { + std::string color_name(buffer); + _UpdateUIStyleColor(&config, m_ui->style(), color_name); + } + } + m_base_style = m_ui->style(); + } + rime_api->config_close(&config); + } + + for (auto& pair : m_session_status_map) { + RIME_STRUCT(RimeStatus, status); + if (rime_api->get_status(to_session_id(pair.first), &status)) { + _LoadSchemaSpecificSettings(pair.first, std::string(status.schema_id)); + _LoadAppInlinePreeditSet(pair.first, true); + _UpdateInlinePreeditStatus(pair.first); + pair.second.status = status; + pair.second.__synced = false; + rime_api->free_status(&status); + } + } + m_ui->style() = get_session_status(m_active_session).style; +} + +BOOL RimeWithWeaselHandler::ProcessKeyEvent(KeyEvent keyEvent, + WeaselSessionId ipc_id, + EatLine eat) { + DLOG(INFO) << "Process key event: keycode = " << keyEvent.keycode + << ", mask = " << keyEvent.mask << ", ipc_id = " << ipc_id; + if (m_disabled) + return FALSE; + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + bool was_composing = !!session_status.status.is_composing; + Bool handled = rime_api->process_key(session_id, keyEvent.keycode, + expand_ibus_modifier(keyEvent.mask)); + // vim_mode when keydown only + if (!handled && !(keyEvent.mask & ibus::Modifier::RELEASE_MASK)) { + bool isVimBackInCommandMode = + (keyEvent.keycode == ibus::Keycode::Escape) || + ((keyEvent.mask & (1 << 2)) && + (keyEvent.keycode == ibus::Keycode::XK_c || + keyEvent.keycode == ibus::Keycode::XK_C || + keyEvent.keycode == ibus::Keycode::XK_bracketleft)); + if (isVimBackInCommandMode && + rime_api->get_option(session_id, "vim_mode") && + !rime_api->get_option(session_id, "ascii_mode")) { + rime_api->set_option(session_id, "ascii_mode", True); + } + } + RimeUiStatusSnapshot status_snapshot; + bool has_commit = false; + _Respond(ipc_id, eat, &status_snapshot, &has_commit); + _UpdateUI(ipc_id, &status_snapshot); + m_active_session = ipc_id; + bool is_composing = status_snapshot.has_status + ? status_snapshot.status.composing + : was_composing; + bool should_eat = + ShouldEatKeyEvent(handled, has_commit, was_composing, is_composing); + if (ShouldTraceKeyEvents() && !handled && has_commit) { + WeaselDebugLog(L"RimeWithWeasel", + L"force key eaten because response has commit keycode=" + + std::to_wstring(keyEvent.keycode) + L" mask=" + + std::to_wstring(keyEvent.mask) + L" ipc_id=" + + std::to_wstring(ipc_id)); + } + return (BOOL)should_eat; +} + +void RimeWithWeaselHandler::CommitComposition(WeaselSessionId ipc_id) { + DLOG(INFO) << "Commit composition: ipc_id = " << ipc_id; + if (m_disabled) + return; + rime_api->commit_composition(to_session_id(ipc_id)); + _UpdateUI(ipc_id); + m_active_session = ipc_id; +} + +void RimeWithWeaselHandler::ClearComposition(WeaselSessionId ipc_id) { + DLOG(INFO) << "Clear composition: ipc_id = " << ipc_id; + if (m_disabled) + return; + rime_api->clear_composition(to_session_id(ipc_id)); + _UpdateUI(ipc_id); + m_active_session = ipc_id; +} + +void RimeWithWeaselHandler::SelectCandidateOnCurrentPage( + size_t index, + WeaselSessionId ipc_id) { + DLOG(INFO) << "select candidate on current page, ipc_id = " << ipc_id + << ", index = " << index; + if (m_disabled) + return; + rime_api->select_candidate_on_current_page(to_session_id(ipc_id), index); +} + +bool RimeWithWeaselHandler::HighlightCandidateOnCurrentPage( + size_t index, + WeaselSessionId ipc_id, + EatLine eat) { + DLOG(INFO) << "highlight candidate on current page, ipc_id = " << ipc_id + << ", index = " << index; + bool res = rime_api->highlight_candidate_on_current_page( + to_session_id(ipc_id), index); + RimeUiStatusSnapshot status_snapshot; + _Respond(ipc_id, eat, &status_snapshot); + _UpdateUI(ipc_id, &status_snapshot); + return res; +} + +bool RimeWithWeaselHandler::ChangePage(bool backward, + WeaselSessionId ipc_id, + EatLine eat) { + DLOG(INFO) << "change page, ipc_id = " << ipc_id + << (backward ? "backward" : "foreward"); + bool res = rime_api->change_page(to_session_id(ipc_id), backward); + RimeUiStatusSnapshot status_snapshot; + _Respond(ipc_id, eat, &status_snapshot); + _UpdateUI(ipc_id, &status_snapshot); + return res; +} + +void RimeWithWeaselHandler::FocusIn(DWORD client_caps, WeaselSessionId ipc_id) { + DLOG(INFO) << "Focus in: ipc_id = " << ipc_id + << ", client_caps = " << client_caps; + if (m_disabled) + return; + _UpdateUI(ipc_id); + m_active_session = ipc_id; +} + +void RimeWithWeaselHandler::FocusOut(DWORD param, WeaselSessionId ipc_id) { + DLOG(INFO) << "Focus out: ipc_id = " << ipc_id; + if (m_ui) + m_ui->Hide(); + m_active_session = 0; +} + +void RimeWithWeaselHandler::UpdateInputPosition(RECT const& rc, + WeaselSessionId ipc_id) { + DLOG(INFO) << "Update input position: (" << rc.left << ", " << rc.top + << "), ipc_id = " << ipc_id + << ", m_active_session = " << m_active_session; + if (m_ui) + m_ui->UpdateInputPosition(rc); + if (m_disabled) + return; + if (m_active_session != ipc_id) { + _UpdateUI(ipc_id); + m_active_session = ipc_id; + } +} + +std::string RimeWithWeaselHandler::m_message_type; +std::string RimeWithWeaselHandler::m_message_value; +std::string RimeWithWeaselHandler::m_message_label; +std::string RimeWithWeaselHandler::m_option_name; +std::mutex RimeWithWeaselHandler::m_notifier_mutex; + +void RimeWithWeaselHandler::OnNotify(void* context_object, + uintptr_t session_id, + const char* message_type, + const char* message_value) { + // may be running in a thread when deploying rime + RimeWithWeaselHandler* self = + reinterpret_cast(context_object); + if (!self || !message_type || !message_value) + return; + std::lock_guard lock(m_notifier_mutex); + m_message_type = message_type; + m_message_value = message_value; + if (RIME_API_AVAILABLE(rime_api, get_state_label) && + !strcmp(message_type, "option")) { + Bool state = message_value[0] != '!'; + const char* option_name = message_value + !state; + m_option_name = option_name; + const char* state_label = + rime_api->get_state_label(session_id, option_name, state); + if (state_label) { + m_message_label = std::string(state_label); + } + } +} + +void RimeWithWeaselHandler::_ReadClientInfo(WeaselSessionId ipc_id, + LPWSTR buffer) { + std::string app_name; + // parse request text + wbufferstream bs(buffer, WEASEL_IPC_BUFFER_LENGTH); + std::wstring line; + while (bs.good()) { + std::getline(bs, line); + if (!bs.good()) + break; + // file ends + if (line == L".") + break; + const std::wstring kClientAppKey = L"session.client_app="; + if (starts_with(line, kClientAppKey)) { + std::wstring lwr = line; + to_lower(lwr); + app_name = wtou8(lwr.substr(kClientAppKey.length())); + } + } + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + // set app specific options + if (!app_name.empty()) { + rime_api->set_property(session_id, "client_app", app_name.c_str()); + + auto it = m_app_options.find(app_name); + if (it != m_app_options.end()) { + AppOptions& options(m_app_options[it->first]); + for (const auto& pair : options) { + DLOG(INFO) << "set app option: " << pair.first << " = " << pair.second; + rime_api->set_option(session_id, pair.first.c_str(), Bool(pair.second)); + } + } + } + // inline preedit + bool inline_preedit = session_status.style.inline_preedit; + rime_api->set_option(session_id, "inline_preedit", Bool(inline_preedit)); + // show soft cursor on weasel panel but not inline + rime_api->set_option(session_id, "soft_cursor", Bool(!inline_preedit)); +} + +void RimeWithWeaselHandler::_GetCandidateInfo(CandidateInfo& cinfo, + RimeContext& ctx) { + cinfo.candies.resize(ctx.menu.num_candidates); + cinfo.comments.resize(ctx.menu.num_candidates); + cinfo.labels.resize(ctx.menu.num_candidates); + for (int i = 0; i < ctx.menu.num_candidates; ++i) { + cinfo.candies[i].str = escape_string(u8tow(ctx.menu.candidates[i].text)); + if (ctx.menu.candidates[i].comment) { + cinfo.comments[i].str = + escape_string(u8tow(ctx.menu.candidates[i].comment)); + } + if (RIME_STRUCT_HAS_MEMBER(ctx, ctx.select_labels) && ctx.select_labels) { + cinfo.labels[i].str = escape_string(u8tow(ctx.select_labels[i])); + } else if (ctx.menu.select_keys) { + cinfo.labels[i].str = + escape_string(std::wstring(1, ctx.menu.select_keys[i])); + } else { + cinfo.labels[i].str = std::to_wstring((i + 1) % 10); + } + } + cinfo.highlighted = ctx.menu.highlighted_candidate_index; + cinfo.currentPage = ctx.menu.page_no; + cinfo.is_last_page = ctx.menu.is_last_page; +} + +void RimeWithWeaselHandler::StartMaintenance() { + m_session_status_map.clear(); + Finalize(); + _InvalidateTrayIconSignature(); + _UpdateUI(0); +} + +void RimeWithWeaselHandler::EndMaintenance(DWORD result) { + bool deploy_result = IsMaintenanceDeployResult(result); + if (deploy_result) { + _SetDeployMessage(result); + } + if (m_disabled) { + Initialize(); + _InvalidateTrayIconSignature(); + _UpdateUI(0); + } + if (deploy_result) { + NotifyService(result); + } + m_session_status_map.clear(); +} + +void RimeWithWeaselHandler::_SetDeployMessage(DWORD result) { + std::lock_guard lock(m_notifier_mutex); + m_message_type = "deploy"; + m_message_value = MaintenanceDeployMessageValue(result); + m_message_label.clear(); + m_option_name.clear(); +} + +void RimeWithWeaselHandler::NotifyService(DWORD notification) { + if (_ServiceNotificationCallback) + _ServiceNotificationCallback(notification); +} + +void RimeWithWeaselHandler::SetOption(WeaselSessionId ipc_id, + const std::string& opt, + bool val) { + // from no-session client, not actual typing session + if (!ipc_id) { + if (m_global_ascii_mode && opt == "ascii_mode") { + for (auto& pair : m_session_status_map) + rime_api->set_option(to_session_id(pair.first), "ascii_mode", val); + } else { + rime_api->set_option(to_session_id(m_active_session), opt.c_str(), val); + } + } else { + rime_api->set_option(to_session_id(ipc_id), opt.c_str(), val); + } +} + +void RimeWithWeaselHandler::OnUpdateUI(std::function const& cb) { + _UpdateUICallback = cb; +} + +void RimeWithWeaselHandler::OnMaintenanceResult( + std::function const& cb) { + OnServiceNotification(cb); +} + +void RimeWithWeaselHandler::OnServiceNotification( + std::function const& cb) { + _ServiceNotificationCallback = cb; +} + +bool RimeWithWeaselHandler::_IsDeployerRunning() { + HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); + bool deployer_detected = hMutex && GetLastError() == ERROR_ALREADY_EXISTS; + if (hMutex) { + CloseHandle(hMutex); + } + return deployer_detected; +} + +void RimeWithWeaselHandler::_UpdateUI( + WeaselSessionId ipc_id, + const RimeUiStatusSnapshot* status_snapshot) { + // if m_ui nullptr, _UpdateUI meaningless + if (!m_ui) + return; + + Status weasel_status = m_ui->status(); + Context weasel_context; + + RimeSessionId session_id = ipc_id ? to_session_id(ipc_id) : 0; + + if (ipc_id == 0) { + weasel_status.disabled = m_disabled; + } else if (status_snapshot && status_snapshot->has_status) { + _ApplyStatusSnapshot(weasel_status, ipc_id, weasel_context, + *status_snapshot); + } else { + _GetStatus(weasel_status, ipc_id, weasel_context); + } + + if (ipc_id != 0) { + SessionStatus& session_status = get_session_status(ipc_id); + if (rime_api->get_option(session_id, "inline_preedit")) + session_status.style.client_caps |= INLINE_PREEDIT_CAPABLE; + else + session_status.style.client_caps &= ~INLINE_PREEDIT_CAPABLE; + } + + if (!_ShowMessage(weasel_context, weasel_status) && + RimeUiNeedsUpdate(m_ui->ctx(), m_ui->status(), weasel_context, + weasel_status)) { + m_ui->Hide(); + m_ui->Update(weasel_context, weasel_status); + } + + _RefreshTrayIconIfNeeded(session_id); + + { + std::lock_guard lock(m_notifier_mutex); + m_message_type.clear(); + m_message_value.clear(); + m_message_label.clear(); + m_option_name.clear(); + } +} + +void RimeWithWeaselHandler::_RefreshTrayIconIfNeeded(RimeSessionId session_id) { + if (!m_ui) + return; + RimeTrayIconSignature signature = + RimeTrayIconSignature::From(m_ui->style(), m_ui->status()); + if (signature == m_tray_icon_signature) + return; + m_tray_icon_signature = signature; + _RefreshTrayIcon(session_id, _UpdateUICallback); +} + +void RimeWithWeaselHandler::_InvalidateTrayIconSignature() { + m_tray_icon_signature = RimeTrayIconSignature(); +} + +void RimeWithWeaselHandler::_LoadSchemaSpecificSettings( + WeaselSessionId ipc_id, + const std::string& schema_id) { + if (!m_ui) + return; + RimeConfig config; + if (!rime_api->schema_open(schema_id.c_str(), &config)) + return; + _UpdateShowNotifications(&config); + m_ui->style() = m_base_style; + _UpdateUIStyle(&config, m_ui, false); + SessionStatus& session_status = get_session_status(ipc_id); + session_status.style = m_ui->style(); + UIStyle& style = session_status.style; + // load schema color style config + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + const auto update_color_scheme = [&]() { + std::string color_name(buffer); + RimeConfigIterator preset = {0}; + if (rime_api->config_begin_map( + &preset, &config, ("preset_color_schemes/" + color_name).c_str())) { + _UpdateUIStyleColor(&config, style, color_name); + rime_api->config_end(&preset); + } else { + RimeConfig weaselconfig; + if (rime_api->config_open("weasel", &weaselconfig)) { + _UpdateUIStyleColor(&weaselconfig, style, color_name); + rime_api->config_close(&weaselconfig); + } + } + }; + const char* key = + m_current_dark_mode ? "style/color_scheme_dark" : "style/color_scheme"; + if (rime_api->config_get_string(&config, key, buffer, BUF_SIZE)) + update_color_scheme(); + // load schema icon start + { + const auto load_icon = [](RimeConfig& config, const char* key1, + const char* key2) { + const auto user_dir = WeaselUserDataPath(); + const auto shared_dir = WeaselSharedDataPath(); + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + if (rime_api->config_get_string(&config, key1, buffer, BUF_SIZE) || + (key2 != NULL && + rime_api->config_get_string(&config, key2, buffer, BUF_SIZE))) { + auto resource = u8tow(buffer); + if (fs::is_regular_file(user_dir / resource)) + return (user_dir / resource).wstring(); + else if (fs::is_regular_file(shared_dir / resource)) + return (shared_dir / resource).wstring(); + } + return std::wstring(); + }; + style.current_zhung_icon = + load_icon(config, "schema/icon", "schema/zhung_icon"); + style.current_ascii_icon = load_icon(config, "schema/ascii_icon", NULL); + style.current_full_icon = load_icon(config, "schema/full_icon", NULL); + style.current_half_icon = load_icon(config, "schema/half_icon", NULL); + } + // load schema icon end + rime_api->config_close(&config); +} + +void RimeWithWeaselHandler::_LoadAppInlinePreeditSet(WeaselSessionId ipc_id, + bool ignore_app_name) { + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + static char _app_name[50]; + rime_api->get_property(session_id, "client_app", _app_name, + sizeof(_app_name) - 1); + std::string app_name(_app_name); + if (!ignore_app_name && m_last_app_name == app_name) + return; + m_last_app_name = app_name; + bool inline_preedit = session_status.style.inline_preedit; + bool found = false; + if (!app_name.empty()) { + auto it = m_app_options.find(app_name); + if (it != m_app_options.end()) { + AppOptions& options(m_app_options[it->first]); + for (const auto& pair : options) { + if (pair.first == "inline_preedit") { + rime_api->set_option(session_id, pair.first.c_str(), + Bool(pair.second)); + session_status.style.inline_preedit = Bool(pair.second); + found = true; + break; + } + } + } + } + if (!found) { + session_status.style.inline_preedit = m_base_style.inline_preedit; + // load from schema. + RIME_STRUCT(RimeStatus, status); + if (rime_api->get_status(session_id, &status)) { + std::string schema_id = status.schema_id; + RimeConfig config; + if (rime_api->schema_open(schema_id.c_str(), &config)) { + Bool value = False; + if (rime_api->config_get_bool(&config, "style/inline_preedit", + &value)) { + session_status.style.inline_preedit = value; + } + rime_api->config_close(&config); + } + rime_api->free_status(&status); + } + } + if (session_status.style.inline_preedit != inline_preedit) + _UpdateInlinePreeditStatus(ipc_id); +} + +bool RimeWithWeaselHandler::_ShowMessage(Context& ctx, Status& status) { + std::lock_guard lock(m_notifier_mutex); + if (m_message_type.empty() || m_message_value.empty()) + return m_ui->IsCountingDown(); + if (ShouldSuppressInlineOptionNotification(m_message_type, m_message_value)) { + status.type = SCHEMA; + return m_ui->IsCountingDown(); + } + // show as auxiliary string + std::wstring& tips(ctx.aux.str); + bool show_icon = false; + if (m_message_type == "deploy") { + if (m_message_value == "start") + if (GetThreadUILanguage() == MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)) + tips = L"Deploying RIME"; + else + tips = L"正在部署 RIME"; + else if (m_message_value == "success") + if (GetThreadUILanguage() == MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)) + tips = L"Deployed"; + else + tips = L"部署完成"; + else if (m_message_value == "failure") { + if (GetThreadUILanguage() == + MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL)) + tips = L"有錯誤,請查看日誌 %TEMP%\\rime.weasel\\rime.weasel.*.INFO"; + else if (GetThreadUILanguage() == + MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)) + tips = L"有错误,请查看日志 %TEMP%\\rime.weasel\\rime.weasel.*.INFO"; + else + tips = + L"There is an error, please check the logs " + L"%TEMP%\\rime.weasel\\rime.weasel.*.INFO"; + } + } else if (m_message_type == "schema") { + tips = /*L"【" + */ status.schema_name /* + L"】"*/; + } else if (m_message_type == "option") { + status.type = SCHEMA; + if (m_message_value == "!ascii_mode") { + show_icon = true; + } else if (m_message_value == "ascii_mode") { + show_icon = true; + } else + tips = u8tow(m_message_label); + + if (m_message_value == "full_shape" || m_message_value == "!full_shape") + status.type = FULL_SHAPE; + } + auto counter = m_ui->IsCountingDown(); + if (!show_icon && counter) + return counter; + auto foption = m_show_notifications.find(m_option_name); + auto falways = m_show_notifications.find("always"); + if ((!add_session && (foption != m_show_notifications.end() || + falways != m_show_notifications.end())) || + m_message_type == "deploy") { + m_ui->Update(ctx, status); + if (m_show_notifications_time) + m_ui->ShowWithTimeout(m_show_notifications_time); + return true; + } else { + return m_ui->IsCountingDown(); + } +} +inline std::string _GetLabelText(const std::vector& labels, + int id, + const wchar_t* format) { + wchar_t buffer[128]; + swprintf_s<128>(buffer, format, labels.at(id).str.c_str()); + return wtou8(std::wstring(buffer)); +} + +bool RimeWithWeaselHandler::_Respond(WeaselSessionId ipc_id, + EatLine eat, + RimeUiStatusSnapshot* status_snapshot, + bool* has_commit) { + std::wstring body; + body.reserve(4096); + std::vector actions; + actions.reserve(8); + if (has_commit) + *has_commit = false; + + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + RIME_STRUCT(RimeCommit, commit); + if (rime_api->get_commit(session_id, &commit)) { + if (has_commit) + *has_commit = true; + actions.push_back("commit"); + std::wstring commit_text_w = escape_string(u8tow(commit.text)); + body.append(L"commit=").append(commit_text_w).append(L"\n"); + rime_api->free_commit(&commit); + } + + bool is_composing = false; + RIME_STRUCT(RimeStatus, status); + static const std::wstring Bool_wstring[] = {L"0", L"1"}; + if (rime_api->get_status(session_id, &status)) { + RimeUiStatusSnapshot snapshot = RimeUiStatusSnapshot::From(status); + if (status_snapshot) + *status_snapshot = snapshot; + is_composing = !!status.is_composing; + actions.push_back("status"); + body.append(L"status.ascii_mode=") + .append(Bool_wstring[!!status.is_ascii_mode]) + .append(L"\n") + .append(L"status.composing=") + .append(Bool_wstring[!!status.is_composing]) + .append(L"\n") + .append(L"status.disabled=") + .append(Bool_wstring[!!status.is_disabled]) + .append(L"\n") + .append(L"status.full_shape=") + .append(Bool_wstring[!!status.is_full_shape]) + .append(L"\n") + .append(L"status.schema_id=") + .append(status.schema_id ? u8tow(status.schema_id) : std::wstring()) + .append(L"\n"); + if (m_global_ascii_mode && + (session_status.status.is_ascii_mode != status.is_ascii_mode)) { + for (auto& pair : m_session_status_map) { + if (pair.first != ipc_id) + rime_api->set_option(to_session_id(pair.first), "ascii_mode", + !!status.is_ascii_mode); + } + } + session_status.status = status; + rime_api->free_status(&status); + } + + RIME_STRUCT(RimeContext, ctx); + if (rime_api->get_context(session_id, &ctx)) { + bool has_candidates = ctx.menu.num_candidates > 0; + CandidateInfo cinfo; + if (has_candidates) { + _GetCandidateInfo(cinfo, ctx); + } + if (is_composing) { + const auto& preedit = ctx.composition.preedit; + const auto& start = ctx.composition.sel_start; + const auto& end = ctx.composition.sel_end; + const auto& cursor = ctx.composition.cursor_pos; + static const auto u8towstring = [](const char* u8str, int len = 0) { + return std::to_wstring(utf8towcslen(u8str, len)); + }; + actions.push_back("ctx"); + switch (session_status.style.preedit_type) { + case UIStyle::PREVIEW: { + if (ctx.commit_text_preview) { + const char* first_utf8 = ctx.commit_text_preview; + const size_t first_len = std::strlen(first_utf8); + const std::wstring first_w = escape_string(u8tow(first_utf8)); + const std::wstring tmp = u8towstring(first_utf8, (int)first_len); + body.append(L"ctx.preedit=") + .append(first_w) + .append(L"\n") + .append(L"ctx.preedit.cursor=") + .append(u8towstring(first_utf8, 0)) + .append(L",") + .append(tmp) + .append(L",") + .append(tmp) + .append(L"\n"); + break; + } + // no preview, fall back to composition + } + case UIStyle::COMPOSITION: { + body.append(L"ctx.preedit=") + .append(escape_string(u8tow(preedit))) + .append(L"\n"); + if (start <= end) { + body.append(L"ctx.preedit.cursor=") + .append(u8towstring(preedit, start)) + .append(L",") + .append(u8towstring(preedit, end)) + .append(L",") + .append(u8towstring(preedit, cursor)) + .append(L"\n"); + } + break; + } + case UIStyle::PREVIEW_ALL: { + body.append(L"ctx.preedit=") + .append(escape_string(u8tow(preedit))) + .append(L" ["); + auto label_valid = session_status.style.label_font_point > 0; + auto comment_valid = session_status.style.comment_font_point > 0; + const std::wstring mark_text_w = + session_status.style.mark_text.empty() + ? std::wstring(L"*") + : session_status.style.mark_text; + for (auto i = 0; i < ctx.menu.num_candidates; i++) { + std::wstring label_w; + if (label_valid) { + wchar_t buf_lbl[128]; + swprintf_s<128>(buf_lbl, + session_status.style.label_text_format.c_str(), + cinfo.labels.at(i).str.c_str()); + label_w = std::wstring(buf_lbl); + } + std::wstring comment_w = + comment_valid ? cinfo.comments.at(i).str : std::wstring(); + std::wstring prefix_w = (i != ctx.menu.highlighted_candidate_index) + ? std::wstring() + : mark_text_w; + body.append(L" ") + .append(prefix_w) + .append(escape_string(label_w)) + .append(escape_string(u8tow(ctx.menu.candidates[i].text))) + .append(L" ") + .append(escape_string(comment_w)); + } + body.append(L" ]\n"); + if (start <= end) { + body.append(L"ctx.preedit.cursor=") + .append(u8towstring(preedit, start)) + .append(L",") + .append(u8towstring(preedit, end)) + .append(L",") + .append(u8towstring(preedit, cursor)) + .append(L"\n"); + } + break; + } + } + } + if (has_candidates) { + std::wstringstream ss; + boost::archive::text_woarchive oa(ss); + + oa << cinfo; + + auto s = ss.str(); + body.append(L"ctx.cand=").append(std::move(s)).append(L"\n"); + } + rime_api->free_context(&ctx); + } + + // configuration information + actions.push_back("config"); + body.append(L"config.inline_preedit=") + .append(std::to_wstring((int)session_status.style.inline_preedit)) + .append(L"\n"); + + // style + if (!session_status.__synced) { + std::wstringstream ss; + boost::archive::text_woarchive oa(ss); + oa << session_status.style; + + actions.push_back("style"); + body.append(L"style=").append(ss.str()).append(L"\n"); + session_status.__synced = true; + } + + // summarize: send header first to avoid vector head-insert cost + std::wstring header; + if (actions.empty()) { + header = L"action=noop\n"; + } else { + std::string actionList; + actionList.reserve(64); + for (size_t i = 0; i < actions.size(); ++i) { + if (i > 0) + actionList += ','; + actionList += actions[i]; + } + header = std::wstring(L"action=") + u8tow(actionList) + L"\n"; + } + if (!eat(header)) + return false; + + body.append(L".\n"); + if (!eat(body)) + return false; + + return true; +} + +// Blend foreground and background ARGB colors taking alpha into account. +// Returns an ABGR COLORREF with premultiplied alpha blended result. +static inline COLORREF blend_colors(COLORREF fcolor, COLORREF bcolor) { + // Extract ARGB channels from both colors. + BYTE fA = (fcolor >> 24) & 0xFF; + BYTE fB = (fcolor >> 16) & 0xFF; + BYTE fG = (fcolor >> 8) & 0xFF; + BYTE fR = fcolor & 0xFF; + BYTE bA = (bcolor >> 24) & 0xFF; + BYTE bB = (bcolor >> 16) & 0xFF; + BYTE bG = (bcolor >> 8) & 0xFF; + BYTE bR = bcolor & 0xFF; + // Convert alpha to [0,1] + float fAlpha = fA / 255.0f; + float bAlpha = bA / 255.0f; + // Result alpha + float retAlpha = fAlpha + (1 - fAlpha) * bAlpha; + if (retAlpha <= 1e-6f) { + // Fully transparent result — return background unchanged as fallback. + return bcolor; + } + auto mix = [&](float fc, float bc) -> BYTE { + return static_cast((fc * fAlpha + bc * bAlpha * (1 - fAlpha)) / + retAlpha); + }; + BYTE retR = mix(fR, bR); + BYTE retG = mix(fG, bG); + BYTE retB = mix(fB, bB); + BYTE outA = static_cast(retAlpha * 255.0f); + return (static_cast(outA) << 24) | (retB << 16) | (retG << 8) | + retR; +} +// parse color value, with fallback value +static Bool _RimeGetColor(RimeConfig* config, + const std::string& key, + int& value, + const ColorFormat& fmt, + const unsigned int& fallback) { + char color[256] = {0}; + if (!rime_api->config_get_string(config, key.c_str(), color, 256)) { + value = fallback; + return False; + } + const auto color_str = std::string(color); + // adjudge if str is 0x 0X # hex color format, return trimmed hex part + // out part is 6 or 8 length hex string without white space + const auto parse_color_code = [](const std::string& str, std::string& out) { + if (str.empty()) + return false; + size_t start = 0; + if (str[0] == '#') { + start = 1; + } else if (str.size() >= 2 && + (str.compare(0, 2, "0x") == 0 || str.compare(0, 2, "0X") == 0)) { + start = 2; + } else { + return false; + } + const std::string hex_part = str.substr(start); + if (hex_part.empty()) + return false; + if ((start == 1 || start == 2) && hex_part.length() != 3 && + hex_part.length() != 4 && hex_part.length() != 6 && + hex_part.length() != 8) { + return false; + } + for (char c : hex_part) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) + return false; + } + out = str.substr(start).substr(0, 8); +#define _2C(c) std::string(2, c) + if (out.size() == 3) + out = _2C(out[0]) + _2C(out[1]) + _2C(out[2]); + else if (out.size() == 4) + out = _2C(out[0]) + _2C(out[1]) + _2C(out[2]) + _2C(out[3]); +#undef _2C + return true; + }; + auto hex_color = std::string(); + if (parse_color_code(color_str, hex_color)) { + value = std::stoul(hex_color, 0, 16); + if (hex_color.length() == 6) + value = (fmt != COLOR_RGBA) ? (value | 0xff000000) + : (((unsigned int)value << 8) | 0x000000ff); + } else { + if (!rime_api->config_get_int(config, key.c_str(), &value)) { + value = fallback; + return False; + } + if (value <= 0xffffff) + value = (fmt != COLOR_RGBA) ? (value | 0xff000000) + : (((unsigned int)value << 8) | 0x000000ff); + else if (value > 0xffffffff) + value &= 0xffffffff; + } + if (fmt == COLOR_ARGB) + value = ARGB2ABGR(value); + else if (fmt == COLOR_RGBA) + value = RGBA2ABGR(value); + value &= 0xffffffff; + return True; +} + +template +using Array = std::array, N>; + +// parset bool type configuration to T type value trueValue / falseValue +template +void _RimeGetBool(RimeConfig* config, + const char* key, + bool cond, + T& value, + const T& trueValue = true, + const T& falseValue = false) { + Bool tempb = False; + if (rime_api->config_get_bool(config, key, &tempb) || cond) + value = (!!tempb) ? trueValue : falseValue; +} +// parse string option to T type value, with fallback +template +void _RimeParseStringOptWithFallback(RimeConfig* config, + const char* key, + T& value, + const Array& arr, + const T& fallback) { + char str_buff[256] = {0}; + if (rime_api->config_get_string(config, key, str_buff, 255)) { + for (size_t i = 0; i < N; ++i) { + if (strcmp(arr[i].first, str_buff) == 0) { + value = arr[i].second; + return; + } + } + } + value = fallback; +} + +template +void _RimeGetIntStr(RimeConfig* config, + const char* key, + T& value, + const char* fb_key = nullptr, + const void* fb_value = nullptr, + const std::function& func = nullptr) { + if constexpr (std::is_same::value) { + if (!rime_api->config_get_int(config, key, &value) && fb_key != 0) + rime_api->config_get_int(config, fb_key, &value); + } else if constexpr (std::is_same::value) { + const int BUF_SIZE = 2047; + char buffer[BUF_SIZE + 1] = {0}; + if (rime_api->config_get_string(config, key, buffer, BUF_SIZE) || + rime_api->config_get_string(config, fb_key, buffer, BUF_SIZE)) { + value = u8tow(buffer); + } else if (fb_value) { + value = *(T*)fb_value; + } + } + if (func) + func(value); +} + +// Helper to iterate a Rime map and invoke callback with key/path +static void ForEachRimeMap( + RimeConfig* config, + const std::string& path, + const std::function& cb) { + RimeConfigIterator iter; + if (!rime_api->config_begin_map(&iter, config, path.c_str())) + return; + while (rime_api->config_next(&iter)) { + cb(iter.key, iter.path); + } + rime_api->config_end(&iter); +} + +// Helper to iterate a Rime list and invoke callback with item path +static void ForEachRimeList( + RimeConfig* config, + const std::string& path, + const std::function& cb) { + RimeConfigIterator iter; + if (!rime_api->config_begin_list(&iter, config, path.c_str())) + return; + while (rime_api->config_next(&iter)) { + cb(iter.path); + } + rime_api->config_end(&iter); +} + +void RimeWithWeaselHandler::_UpdateShowNotifications(RimeConfig* config, + bool initialize) { + Bool show_notifications = true; + if (initialize) + m_show_notifications_base.clear(); + m_show_notifications.clear(); + + if (rime_api->config_get_bool(config, "show_notifications", + &show_notifications)) { + // config read as bool, for global all on or off + if (show_notifications) + m_show_notifications["always"] = true; + if (initialize) + m_show_notifications_base = m_show_notifications; + } else { + // read as list using helper + ForEachRimeList(config, "show_notifications", [&](const char* item_path) { + char buffer[256] = {0}; + if (rime_api->config_get_string(config, item_path, buffer, 256)) + m_show_notifications[std::string(buffer)] = true; + }); + if (initialize) + m_show_notifications_base = m_show_notifications; + if (m_show_notifications.empty()) { + // not configured, or incorrect type + if (initialize) + m_show_notifications_base["always"] = true; + m_show_notifications = m_show_notifications_base; + } + } +} + +// update ui's style parameters, ui has been check before referenced +static void _UpdateUIStyle(RimeConfig* config, UI* ui, bool initialize) { + UIStyle& style(ui->style()); + const std::function rmspace = [](std::wstring& str) { + str = std::regex_replace(str, std::wregex(L"\\s*(,|:|^|$)\\s*"), L"$1"); + }; + const std::function _abs = [](int& value) { value = abs(value); }; + // get font faces + _RimeGetIntStr(config, "style/font_face", style.font_face, 0, 0, rmspace); + std::wstring* const pFallbackFontFace = initialize ? &style.font_face : NULL; + _RimeGetIntStr(config, "style/label_font_face", style.label_font_face, 0, + pFallbackFontFace, rmspace); + _RimeGetIntStr(config, "style/comment_font_face", style.comment_font_face, 0, + pFallbackFontFace, rmspace); + // able to set label font/comment font empty, force fallback to font face. + if (style.label_font_face.empty()) + style.label_font_face = style.font_face; + if (style.comment_font_face.empty()) + style.comment_font_face = style.font_face; + // get font points + _RimeGetIntStr(config, "style/font_point", style.font_point); + if (style.font_point <= 0) + style.font_point = 12; + _RimeGetIntStr(config, "style/label_font_point", style.label_font_point, + "style/font_point", 0, _abs); + _RimeGetIntStr(config, "style/comment_font_point", style.comment_font_point, + "style/font_point", 0, _abs); + _RimeGetIntStr(config, "style/candidate_abbreviate_length", + style.candidate_abbreviate_length, 0, 0, _abs); + _RimeGetBool(config, "style/inline_preedit", initialize, + style.inline_preedit); + _RimeGetBool(config, "style/vertical_auto_reverse", initialize, + style.vertical_auto_reverse); + static constexpr Array _preeditArr = { + {{"composition", UIStyle::COMPOSITION}, + {"preview", UIStyle::PREVIEW}, + {"preview_all", UIStyle::PREVIEW_ALL}}}; + _RimeParseStringOptWithFallback(config, "style/preedit_type", + style.preedit_type, _preeditArr, + style.preedit_type); + static constexpr Array _aliasModeArr = { + {{"force_dword", UIStyle::FORCE_DWORD}, + {"cleartype", UIStyle::CLEARTYPE}, + {"grayscale", UIStyle::GRAYSCALE}, + {"aliased", UIStyle::ALIASED}, + {"default", UIStyle::DEFAULT}}}; + _RimeParseStringOptWithFallback(config, "style/antialias_mode", + style.antialias_mode, _aliasModeArr, + style.antialias_mode); + static constexpr Array _hoverTypeArr = { + {{"none", UIStyle::HoverType::NONE}, + {"semi_hilite", UIStyle::HoverType::SEMI_HILITE}, + {"hilite", UIStyle::HoverType::HILITE}}}; + _RimeParseStringOptWithFallback(config, "style/hover_type", style.hover_type, + _hoverTypeArr, style.hover_type); + static constexpr Array _alignType = { + {{"top", UIStyle::ALIGN_TOP}, + {"center", UIStyle::ALIGN_CENTER}, + {"bottom", UIStyle::ALIGN_BOTTOM}}}; + _RimeParseStringOptWithFallback(config, "style/layout/align_type", + style.align_type, _alignType, + style.align_type); + _RimeGetBool(config, "style/display_tray_icon", initialize, + style.display_tray_icon); + _RimeGetBool(config, "style/ascii_tip_follow_cursor", initialize, + style.ascii_tip_follow_cursor); + _RimeGetBool(config, "style/horizontal", initialize, style.layout_type, + UIStyle::LAYOUT_HORIZONTAL, UIStyle::LAYOUT_VERTICAL); + _RimeGetBool(config, "style/paging_on_scroll", initialize, + style.paging_on_scroll); + _RimeGetBool(config, "style/click_to_capture", initialize, + style.click_to_capture, true, false); + _RimeGetBool(config, "style/fullscreen", false, style.layout_type, + ((style.layout_type == UIStyle::LAYOUT_HORIZONTAL) + ? UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN + : UIStyle::LAYOUT_VERTICAL_FULLSCREEN), + style.layout_type); + _RimeGetBool(config, "style/vertical_text", false, style.layout_type, + UIStyle::LAYOUT_VERTICAL_TEXT, style.layout_type); + _RimeGetBool(config, "style/vertical_text_left_to_right", false, + style.vertical_text_left_to_right); + _RimeGetBool(config, "style/vertical_text_with_wrap", false, + style.vertical_text_with_wrap); + static constexpr Array _text_orientation = { + {{"horizontal", false}, {"vertical", true}}}; + bool _text_orientation_bool = false; + _RimeParseStringOptWithFallback(config, "style/text_orientation", + _text_orientation_bool, _text_orientation, + _text_orientation_bool); + if (_text_orientation_bool) + style.layout_type = UIStyle::LAYOUT_VERTICAL_TEXT; + _RimeGetIntStr(config, "style/label_format", style.label_text_format); + _RimeGetIntStr(config, "style/mark_text", style.mark_text); + _RimeGetIntStr(config, "style/layout/baseline", style.baseline, 0, 0, _abs); + _RimeGetIntStr(config, "style/layout/linespacing", style.linespacing, 0, 0, + _abs); + _RimeGetIntStr(config, "style/layout/min_width", style.min_width, 0, 0, _abs); + _RimeGetIntStr(config, "style/layout/max_width", style.max_width, 0, 0, _abs); + _RimeGetIntStr(config, "style/layout/min_height", style.min_height, 0, 0, + _abs); + _RimeGetIntStr(config, "style/layout/max_height", style.max_height, 0, 0, + _abs); + // layout (alternative to style/horizontal) + static constexpr Array _layoutArr = { + {{"vertical", UIStyle::LAYOUT_VERTICAL}, + {"horizontal", UIStyle::LAYOUT_HORIZONTAL}, + {"vertical_text", UIStyle::LAYOUT_VERTICAL_TEXT}, + {"vertical+fullscreen", UIStyle::LAYOUT_VERTICAL_FULLSCREEN}, + {"horizontal+fullscreen", UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN}}}; + _RimeParseStringOptWithFallback(config, "style/layout/type", + style.layout_type, _layoutArr, + style.layout_type); + // disable max_width when full screen + if (style.layout_type == UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN || + style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN) { + style.max_width = 0; + style.inline_preedit = false; + } + _RimeGetIntStr(config, "style/layout/border", style.border, + "style/layout/border_width", 0, _abs); + _RimeGetIntStr(config, "style/layout/margin_x", style.margin_x); + _RimeGetIntStr(config, "style/layout/margin_y", style.margin_y); + _RimeGetIntStr(config, "style/layout/spacing", style.spacing, 0, 0, _abs); + _RimeGetIntStr(config, "style/layout/candidate_spacing", + style.candidate_spacing, 0, 0, _abs); + _RimeGetIntStr(config, "style/layout/hilite_spacing", style.hilite_spacing, 0, + 0, _abs); + _RimeGetIntStr(config, "style/layout/hilite_padding_x", + style.hilite_padding_x, "style/layout/hilite_padding", 0, + _abs); + _RimeGetIntStr(config, "style/layout/hilite_padding_y", + style.hilite_padding_y, "style/layout/hilite_padding", 0, + _abs); + _RimeGetIntStr(config, "style/layout/shadow_radius", style.shadow_radius, 0, + 0, _abs); + // disable shadow for fullscreen layout + style.shadow_radius *= + (!(style.layout_type == UIStyle::LAYOUT_HORIZONTAL_FULLSCREEN || + style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN)); + _RimeGetIntStr(config, "style/layout/shadow_offset_x", style.shadow_offset_x); + _RimeGetIntStr(config, "style/layout/shadow_offset_y", style.shadow_offset_y); + // round_corner as alias of hilited_corner_radius + _RimeGetIntStr(config, "style/layout/hilited_corner_radius", + style.round_corner, "style/layout/round_corner", 0, _abs); + // corner_radius not set, fallback to round_corner + _RimeGetIntStr(config, "style/layout/corner_radius", style.round_corner_ex, + "style/layout/round_corner", 0, _abs); + // fix padding and spacing settings + if (style.layout_type != UIStyle::LAYOUT_VERTICAL_TEXT) { + // hilite_padding vs spacing + // if hilite_padding over spacing, increase spacing + style.spacing = max(style.spacing, style.hilite_padding_y * 2); + // hilite_padding vs candidate_spacing + if (style.layout_type == UIStyle::LAYOUT_VERTICAL_FULLSCREEN || + style.layout_type == UIStyle::LAYOUT_VERTICAL) { + // vertical, if hilite_padding_y over candidate spacing, + // increase candidate spacing + style.candidate_spacing = + max(style.candidate_spacing, style.hilite_padding_y * 2); + } else { + // horizontal, if hilite_padding_x over candidate + // spacing, increase candidate spacing + style.candidate_spacing = + max(style.candidate_spacing, style.hilite_padding_x * 2); + } + // hilite_padding_x vs hilite_spacing + if (!style.inline_preedit) + style.hilite_spacing = max(style.hilite_spacing, style.hilite_padding_x); + } else // LAYOUT_VERTICAL_TEXT + { + // hilite_padding_x vs spacing + // if hilite_padding over spacing, increase spacing + style.spacing = max(style.spacing, style.hilite_padding_x * 2); + // hilite_padding vs candidate_spacing + // if hilite_padding_x over candidate + // spacing, increase candidate spacing + style.candidate_spacing = + max(style.candidate_spacing, style.hilite_padding_x * 2); + // vertical_text_with_wrap and hilite_padding_y over candidate_spacing + if (style.vertical_text_with_wrap) + style.candidate_spacing = + max(style.candidate_spacing, style.hilite_padding_y * 2); + // hilite_padding_y vs hilite_spacing + if (!style.inline_preedit) + style.hilite_spacing = max(style.hilite_spacing, style.hilite_padding_y); + } + // fix padding and margin settings + int scale = style.margin_x < 0 ? -1 : 1; + style.margin_x = scale * max(style.hilite_padding_x, abs(style.margin_x)); + scale = style.margin_y < 0 ? -1 : 1; + style.margin_y = scale * max(style.hilite_padding_y, abs(style.margin_y)); + // get enhanced_position + _RimeGetBool(config, "style/enhanced_position", initialize, + style.enhanced_position, true, false); + // get color scheme + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + if (initialize && rime_api->config_get_string(config, "style/color_scheme", + buffer, BUF_SIZE)) + _UpdateUIStyleColor(config, style); +} +// load color configs to style, by "style/color_scheme" or specific scheme name +// "color" which is default empty +static bool _UpdateUIStyleColor(RimeConfig* config, + UIStyle& style, + const std::string& color) { + const int BUF_SIZE = 255; + char buffer[BUF_SIZE + 1] = {0}; + std::string color_mark = "style/color_scheme"; + // color scheme + if (rime_api->config_get_string(config, color_mark.c_str(), buffer, + BUF_SIZE) || + !color.empty()) { + std::string prefix("preset_color_schemes/"); + prefix += (color.empty()) ? buffer : color; + // define color format, default abgr if not set + ColorFormat fmt = COLOR_ABGR; + static constexpr Array _colorFmt = { + {{"argb", COLOR_ARGB}, {"rgba", COLOR_RGBA}, {"abgr", COLOR_ABGR}}}; + _RimeParseStringOptWithFallback(config, (prefix + "/color_format").c_str(), + fmt, _colorFmt, COLOR_ABGR); +#define COLOR(key, value, fallback) \ + _RimeGetColor(config, (prefix + "/" + key), value, fmt, fallback) + COLOR("back_color", style.back_color, 0xffffffff); + COLOR("shadow_color", style.shadow_color, 0); + COLOR("prevpage_color", style.prevpage_color, 0); + COLOR("nextpage_color", style.nextpage_color, 0); + COLOR("text_color", style.text_color, 0xff000000); + COLOR("candidate_text_color", style.candidate_text_color, style.text_color); + COLOR("candidate_back_color", style.candidate_back_color, 0); + COLOR("border_color", style.border_color, style.text_color); + COLOR("hilited_text_color", style.hilited_text_color, style.text_color); + COLOR("hilited_back_color", style.hilited_back_color, style.back_color); + COLOR("hilited_candidate_text_color", style.hilited_candidate_text_color, + style.hilited_text_color); + COLOR("hilited_candidate_back_color", style.hilited_candidate_back_color, + style.hilited_back_color); + COLOR("hilited_candidate_shadow_color", + style.hilited_candidate_shadow_color, 0); + COLOR("hilited_shadow_color", style.hilited_shadow_color, 0); + COLOR("candidate_shadow_color", style.candidate_shadow_color, 0); + COLOR("candidate_border_color", style.candidate_border_color, 0); + COLOR("hilited_candidate_border_color", + style.hilited_candidate_border_color, 0); + COLOR("label_color", style.label_text_color, + blend_colors(style.candidate_text_color, style.candidate_back_color)); + COLOR("hilited_label_color", style.hilited_label_text_color, + blend_colors(style.hilited_candidate_text_color, + style.hilited_candidate_back_color)); + COLOR("comment_text_color", style.comment_text_color, + style.label_text_color); + COLOR("hilited_comment_text_color", style.hilited_comment_text_color, + style.hilited_label_text_color); + COLOR("hilited_mark_color", style.hilited_mark_color, 0); +#undef COLOR + return true; + } + return false; +} +static void _LoadAppOptions(RimeConfig* config, + AppOptionsByAppName& app_options) { + app_options.clear(); + ForEachRimeMap( + config, "app_options", [&](const char* app_key, const char* app_path) { + AppOptions& options(app_options[app_key]); + ForEachRimeMap( + config, app_path, [&](const char* opt_key, const char* opt_path) { + Bool value = False; + if (rime_api->config_get_bool(config, opt_path, &value)) { + options[opt_key] = !!value; + } + }); + }); +} + +void RimeWithWeaselHandler::_GetStatus(Status& stat, + WeaselSessionId ipc_id, + Context& ctx) { + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + RIME_STRUCT(RimeStatus, status); + if (rime_api->get_status(session_id, &status)) { + RimeUiStatusSnapshot snapshot = RimeUiStatusSnapshot::From(status); + _ApplyStatusSnapshot(stat, ipc_id, ctx, snapshot); + rime_api->free_status(&status); + } +} + +void RimeWithWeaselHandler::_ApplyStatusSnapshot( + Status& stat, + WeaselSessionId ipc_id, + Context& ctx, + const RimeUiStatusSnapshot& snapshot) { + if (!snapshot.has_status) + return; + + SessionStatus& session_status = get_session_status(ipc_id); + stat.schema_name = snapshot.status.schema_name; + stat.schema_id = snapshot.status.schema_id; + stat.ascii_mode = snapshot.status.ascii_mode; + stat.composing = snapshot.status.composing; + stat.disabled = snapshot.status.disabled; + stat.full_shape = snapshot.status.full_shape; + + if (snapshot.schema_id != m_last_schema_id) { + session_status.__synced = false; + m_last_schema_id = snapshot.schema_id; + if (snapshot.schema_id != + ".default") { // don't load for schema select menu + bool inline_preedit = session_status.style.inline_preedit; + _LoadSchemaSpecificSettings(ipc_id, snapshot.schema_id); + _LoadAppInlinePreeditSet(ipc_id, true); + if (session_status.style.inline_preedit != inline_preedit) + // in case of inline_preedit set in schema + _UpdateInlinePreeditStatus(ipc_id); + m_ui->style() = session_status.style; + if (m_show_notifications.find("schema") != m_show_notifications.end() && + m_show_notifications_time > 0) { + ctx.aux.str = stat.schema_name; + m_ui->Update(ctx, stat); + m_ui->ShowWithTimeout(m_show_notifications_time); + } + } + } +} + +void RimeWithWeaselHandler::_GetContext(Context& weasel_context, + RimeSessionId session_id) { + RIME_STRUCT(RimeContext, ctx); + if (rime_api->get_context(session_id, &ctx)) { + if (ctx.composition.length > 0) { + weasel_context.preedit.str = u8tow(ctx.composition.preedit); + if (ctx.composition.sel_start < ctx.composition.sel_end) { + TextAttribute attr; + attr.type = HIGHLIGHTED; + attr.range.start = + utf8towcslen(ctx.composition.preedit, ctx.composition.sel_start); + attr.range.end = + utf8towcslen(ctx.composition.preedit, ctx.composition.sel_end); + + weasel_context.preedit.attributes.push_back(attr); + } + } + if (ctx.menu.num_candidates) { + CandidateInfo& cinfo(weasel_context.cinfo); + _GetCandidateInfo(cinfo, ctx); + } + rime_api->free_context(&ctx); + } +} + +void RimeWithWeaselHandler::_UpdateInlinePreeditStatus(WeaselSessionId ipc_id) { + if (!m_ui) + return; + SessionStatus& session_status = get_session_status(ipc_id); + RimeSessionId session_id = session_status.session_id; + // set inline_preedit option + bool inline_preedit = session_status.style.inline_preedit; + rime_api->set_option(session_id, "inline_preedit", Bool(inline_preedit)); + // show soft cursor on weasel panel but not inline + rime_api->set_option(session_id, "soft_cursor", Bool(!inline_preedit)); +} diff --git a/WeaselDeployer/Configurator.cpp b/WeaselDeployer/Configurator.cpp index 150232267..64209e49e 100644 --- a/WeaselDeployer/Configurator.cpp +++ b/WeaselDeployer/Configurator.cpp @@ -1,237 +1,250 @@ -#include "stdafx.h" -#include "WeaselDeployer.h" -#include "Configurator.h" -#include "SwitcherSettingsDialog.h" -#include "UIStyleSettings.h" -#include "UIStyleSettingsDialog.h" -#include "DictManagementDialog.h" -#include -#include -#include -#include -#pragma warning(disable : 4005) -#include -#include -#pragma warning(default : 4005) -#include -#include -#include "WeaselDeployer.h" - -static void CreateFileIfNotExist(std::string filename) { - std::filesystem::path file_path = WeaselUserDataPath() / u8tow(filename); - DWORD dwAttrib = GetFileAttributes(file_path.c_str()); - if (!(INVALID_FILE_ATTRIBUTES != dwAttrib && - 0 == (dwAttrib & FILE_ATTRIBUTE_DIRECTORY))) { - std::wofstream o(file_path.c_str(), std::ios::app); - o.close(); - } -} -Configurator::Configurator() { - CreateFileIfNotExist("default.custom.yaml"); - CreateFileIfNotExist("weasel.custom.yaml"); -} - -void Configurator::Initialize() { - RIME_STRUCT(RimeTraits, weasel_traits); - std::string shared_dir = wtou8(WeaselSharedDataPath().wstring()); - std::string user_dir = wtou8(WeaselUserDataPath().wstring()); - weasel_traits.shared_data_dir = shared_dir.c_str(); - weasel_traits.user_data_dir = user_dir.c_str(); - weasel_traits.prebuilt_data_dir = weasel_traits.shared_data_dir; - std::string distribution_name = wtou8(get_weasel_ime_name()); - weasel_traits.distribution_name = distribution_name.c_str(); - weasel_traits.distribution_code_name = WEASEL_CODE_NAME; - weasel_traits.distribution_version = WEASEL_VERSION; - weasel_traits.app_name = "rime.weasel"; - std::string log_dir = WeaselLogPath().u8string(); - weasel_traits.log_dir = log_dir.c_str(); - RimeApi* rime_api = rime_get_api(); - assert(rime_api); - rime_api->setup(&weasel_traits); - LOG(INFO) << "WeaselDeployer reporting."; - rime_api->deployer_initialize(NULL); -} - -static bool configure_switcher(RimeLeversApi* api, - RimeSwitcherSettings* switchcer_settings, - bool* reconfigured) { - RimeCustomSettings* settings = (RimeCustomSettings*)switchcer_settings; - if (!api->load_settings(settings)) - return false; - SwitcherSettingsDialog dialog(switchcer_settings); - if (dialog.DoModal() == IDOK) { - if (api->save_settings(settings)) - *reconfigured = true; - return true; - } - return false; -} - -static bool configure_ui(RimeLeversApi* api, - UIStyleSettings* ui_style_settings, - bool* reconfigured) { - RimeCustomSettings* settings = ui_style_settings->settings(); - if (!api->load_settings(settings)) - return false; - UIStyleSettingsDialog dialog(ui_style_settings); - if (dialog.DoModal() == IDOK) { - if (api->save_settings(settings)) - *reconfigured = true; - return true; - } - return false; -} - -int Configurator::Run(bool installing) { - RimeModule* levers = rime_get_api()->find_module("levers"); - if (!levers) - return 1; - RimeLeversApi* api = (RimeLeversApi*)levers->get_api(); - if (!api) - return 1; - - bool reconfigured = false; - - RimeSwitcherSettings* switcher_settings = api->switcher_settings_init(); - UIStyleSettings ui_style_settings; - - bool skip_switcher_settings = - installing && !api->is_first_run((RimeCustomSettings*)switcher_settings); - bool skip_ui_style_settings = - installing && !api->is_first_run(ui_style_settings.settings()); - - (skip_switcher_settings || - configure_switcher(api, switcher_settings, &reconfigured)) && - (skip_ui_style_settings || - configure_ui(api, &ui_style_settings, &reconfigured)); - - api->custom_settings_destroy((RimeCustomSettings*)switcher_settings); - - if (installing || reconfigured) { - return UpdateWorkspace(reconfigured); - } - return 0; -} - -int Configurator::UpdateWorkspace(bool report_errors) { - HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); - if (!hMutex) { - LOG(ERROR) << "Error creating WeaselDeployerMutex."; - return 1; - } - if (GetLastError() == ERROR_ALREADY_EXISTS) { - LOG(WARNING) << "another deployer process is running; aborting operation."; - CloseHandle(hMutex); - if (report_errors) { - // MessageBox(NULL, - // L"正在執行另一項部署任務,方纔所做的修改將在輸入法再次啓動後生效。", - // L"【小狼毫】", MB_OK | MB_ICONINFORMATION); - MSG_BY_IDS(IDS_STR_DEPLOYING_RESTARTREQ, IDS_STR_WEASEL, - MB_OK | MB_ICONINFORMATION); - } - return 1; - } - - weasel::Client client; - if (client.Connect()) { - LOG(INFO) << "Turning WeaselServer into maintenance mode."; - client.StartMaintenance(); - } - - { - RimeApi* rime = rime_get_api(); - // initialize default config, preset schemas - rime->deploy(); - // initialize weasel config - rime->deploy_config_file("weasel.yaml", "config_version"); - } - - CloseHandle(hMutex); // should be closed before resuming service. - - if (client.Connect()) { - LOG(INFO) << "Resuming service."; - client.EndMaintenance(); - } - return 0; -} - -int Configurator::DictManagement() { - HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); - if (!hMutex) { - LOG(ERROR) << "Error creating WeaselDeployerMutex."; - return 1; - } - if (GetLastError() == ERROR_ALREADY_EXISTS) { - LOG(WARNING) << "another deployer process is running; aborting operation."; - CloseHandle(hMutex); - // MessageBox(NULL, L"正在執行另一項部署任務,請稍候再試。", L"【小狼毫】", - // MB_OK | MB_ICONINFORMATION); - MSG_BY_IDS(IDS_STR_DEPLOYING_WAIT, IDS_STR_WEASEL, - MB_OK | MB_ICONINFORMATION); - return 1; - } - - weasel::Client client; - if (client.Connect()) { - LOG(INFO) << "Turning WeaselServer into maintenance mode."; - client.StartMaintenance(); - } - - { - RimeApi* rime = rime_get_api(); - if (RIME_API_AVAILABLE(rime, run_task)) { - rime->run_task("installation_update"); // setup user data sync dir - } - DictManagementDialog dlg; - dlg.DoModal(); - } - - CloseHandle(hMutex); // should be closed before resuming service. - - if (client.Connect()) { - LOG(INFO) << "Resuming service."; - client.EndMaintenance(); - } - return 0; -} - -int Configurator::SyncUserData() { - HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); - if (!hMutex) { - LOG(ERROR) << "Error creating WeaselDeployerMutex."; - return 1; - } - if (GetLastError() == ERROR_ALREADY_EXISTS) { - LOG(WARNING) << "another deployer process is running; aborting operation."; - CloseHandle(hMutex); - // MessageBox(NULL, L"正在執行另一項部署任務,請稍候再試。", L"【小狼毫】", - // MB_OK | MB_ICONINFORMATION); - MSG_BY_IDS(IDS_STR_DEPLOYING_WAIT, IDS_STR_WEASEL, - MB_OK | MB_ICONINFORMATION); - return 1; - } - - weasel::Client client; - if (client.Connect()) { - LOG(INFO) << "Turning WeaselServer into maintenance mode."; - client.StartMaintenance(); - } - - { - RimeApi* rime = rime_get_api(); - if (!rime->sync_user_data()) { - LOG(ERROR) << "Error synching user data."; - CloseHandle(hMutex); - return 1; - } - rime->join_maintenance_thread(); - } - - CloseHandle(hMutex); // should be closed before resuming service. - - if (client.Connect()) { - LOG(INFO) << "Resuming service."; - client.EndMaintenance(); - } - return 0; -} +#include "stdafx.h" +#include "WeaselDeployer.h" +#include "Configurator.h" +#include "SwitcherSettingsDialog.h" +#include "UIStyleSettings.h" +#include "UIStyleSettingsDialog.h" +#include "DictManagementDialog.h" +#include +#include +#include +#include +#pragma warning(disable : 4005) +#include +#include +#pragma warning(default : 4005) +#include +#include +#include "WeaselDeployer.h" + +static void CreateFileIfNotExist(std::string filename) { + std::filesystem::path file_path = WeaselUserDataPath() / u8tow(filename); + DWORD dwAttrib = GetFileAttributes(file_path.c_str()); + if (!(INVALID_FILE_ATTRIBUTES != dwAttrib && + 0 == (dwAttrib & FILE_ATTRIBUTE_DIRECTORY))) { + std::wofstream o(file_path.c_str(), std::ios::app); + o.close(); + } +} +Configurator::Configurator() { + CreateFileIfNotExist("default.custom.yaml"); + CreateFileIfNotExist("weasel.custom.yaml"); +} + +void Configurator::Initialize() { + RIME_STRUCT(RimeTraits, weasel_traits); + std::string shared_dir = wtou8(WeaselSharedDataPath().wstring()); + std::string user_dir = wtou8(WeaselUserDataPath().wstring()); + weasel_traits.shared_data_dir = shared_dir.c_str(); + weasel_traits.user_data_dir = user_dir.c_str(); + weasel_traits.prebuilt_data_dir = weasel_traits.shared_data_dir; + std::string distribution_name = wtou8(get_weasel_ime_name()); + weasel_traits.distribution_name = distribution_name.c_str(); + weasel_traits.distribution_code_name = WEASEL_CODE_NAME; + weasel_traits.distribution_version = WEASEL_VERSION; + weasel_traits.app_name = "rime.weasel"; + std::string log_dir = WeaselLogPath().u8string(); + weasel_traits.log_dir = log_dir.c_str(); + RimeApi* rime_api = rime_get_api(); + assert(rime_api); + rime_api->setup(&weasel_traits); + LOG(INFO) << "WeaselDeployer reporting."; + rime_api->deployer_initialize(NULL); +} + +static bool configure_switcher(RimeLeversApi* api, + RimeSwitcherSettings* switchcer_settings, + bool* reconfigured) { + RimeCustomSettings* settings = (RimeCustomSettings*)switchcer_settings; + if (!api->load_settings(settings)) + return false; + SwitcherSettingsDialog dialog(switchcer_settings); + if (dialog.DoModal() == IDOK) { + if (api->save_settings(settings)) + *reconfigured = true; + return true; + } + return false; +} + +static bool configure_ui(RimeLeversApi* api, + UIStyleSettings* ui_style_settings, + bool* reconfigured) { + RimeCustomSettings* settings = ui_style_settings->settings(); + if (!api->load_settings(settings)) + return false; + UIStyleSettingsDialog dialog(ui_style_settings); + if (dialog.DoModal() == IDOK) { + if (api->save_settings(settings)) + *reconfigured = true; + return true; + } + return false; +} + +int Configurator::Run(bool installing) { + RimeModule* levers = rime_get_api()->find_module("levers"); + if (!levers) + return 1; + RimeLeversApi* api = (RimeLeversApi*)levers->get_api(); + if (!api) + return 1; + + bool reconfigured = false; + + RimeSwitcherSettings* switcher_settings = api->switcher_settings_init(); + UIStyleSettings ui_style_settings; + + bool skip_switcher_settings = + installing && !api->is_first_run((RimeCustomSettings*)switcher_settings); + bool skip_ui_style_settings = + installing && !api->is_first_run(ui_style_settings.settings()); + + (skip_switcher_settings || + configure_switcher(api, switcher_settings, &reconfigured)) && + (skip_ui_style_settings || + configure_ui(api, &ui_style_settings, &reconfigured)); + + api->custom_settings_destroy((RimeCustomSettings*)switcher_settings); + + if (installing || reconfigured) { + return UpdateWorkspace(reconfigured); + } + return 0; +} + +int Configurator::UpdateWorkspace(bool report_errors) { + HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); + if (!hMutex) { + LOG(ERROR) << "Error creating WeaselDeployerMutex."; + return 1; + } + if (GetLastError() == ERROR_ALREADY_EXISTS) { + LOG(WARNING) << "another deployer process is running; aborting operation."; + CloseHandle(hMutex); + if (report_errors) { + // MessageBox(NULL, + // L"正在執行另一項部署任務,方纔所做的修改將在輸入法再次啓動後生效。", + // L"【小狼毫】", MB_OK | MB_ICONINFORMATION); + MSG_BY_IDS(IDS_STR_DEPLOYING_RESTARTREQ, IDS_STR_WEASEL, + MB_OK | MB_ICONINFORMATION); + } + return 1; + } + + weasel::Client client; + if (client.TryConnect()) { + LOG(INFO) << "Turning WeaselServer into maintenance mode."; + client.StartMaintenance(); + } + + bool deployed = false; + { + RimeApi* rime = rime_get_api(); + // initialize default config, preset schemas + deployed = !!rime->deploy(); + // initialize weasel config + deployed = + !!rime->deploy_config_file("weasel.yaml", "config_version") && deployed; + } + + CloseHandle(hMutex); // should be closed before resuming service. + + if (client.TryConnect()) { + LOG(INFO) << "Resuming service."; + client.EndMaintenance(deployed + ? weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS + : weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE); + } + return deployed ? 0 : 1; +} + +int Configurator::DictManagement() { + HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); + if (!hMutex) { + LOG(ERROR) << "Error creating WeaselDeployerMutex."; + return 1; + } + if (GetLastError() == ERROR_ALREADY_EXISTS) { + LOG(WARNING) << "another deployer process is running; aborting operation."; + CloseHandle(hMutex); + // MessageBox(NULL, L"正在執行另一項部署任務,請稍候再試。", L"【小狼毫】", + // MB_OK | MB_ICONINFORMATION); + MSG_BY_IDS(IDS_STR_DEPLOYING_WAIT, IDS_STR_WEASEL, + MB_OK | MB_ICONINFORMATION); + return 1; + } + + weasel::Client client; + if (client.TryConnect()) { + LOG(INFO) << "Turning WeaselServer into maintenance mode."; + client.StartMaintenance(); + } + + { + RimeApi* rime = rime_get_api(); + if (RIME_API_AVAILABLE(rime, run_task)) { + rime->run_task("installation_update"); // setup user data sync dir + } + DictManagementDialog dlg; + dlg.DoModal(); + } + + CloseHandle(hMutex); // should be closed before resuming service. + + if (client.TryConnect()) { + LOG(INFO) << "Resuming service."; + client.EndMaintenance(); + } + return 0; +} + +int Configurator::SyncUserData() { + HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerMutex"); + if (!hMutex) { + LOG(ERROR) << "Error creating WeaselDeployerMutex."; + return 1; + } + if (GetLastError() == ERROR_ALREADY_EXISTS) { + LOG(WARNING) << "another deployer process is running; aborting operation."; + CloseHandle(hMutex); + // MessageBox(NULL, L"正在執行另一項部署任務,請稍候再試。", L"【小狼毫】", + // MB_OK | MB_ICONINFORMATION); + MSG_BY_IDS(IDS_STR_DEPLOYING_WAIT, IDS_STR_WEASEL, + MB_OK | MB_ICONINFORMATION); + return 1; + } + + weasel::Client client; + bool maintenance_started = false; + if (client.TryConnect()) { + LOG(INFO) << "Turning WeaselServer into maintenance mode."; + client.StartMaintenance(); + maintenance_started = true; + } + auto resume_service = [&]() { + if (!maintenance_started) + return; + if (client.TryConnect()) { + LOG(INFO) << "Resuming service."; + client.EndMaintenance(); + } + maintenance_started = false; + }; + + { + RimeApi* rime = rime_get_api(); + if (!rime->sync_user_data()) { + LOG(ERROR) << "Error synching user data."; + CloseHandle(hMutex); + resume_service(); + return 1; + } + rime->join_maintenance_thread(); + } + + CloseHandle(hMutex); // should be closed before resuming service. + + resume_service(); + return 0; +} diff --git a/WeaselIPC/PipeChannel.cpp b/WeaselIPC/PipeChannel.cpp index 8687e87a2..0412f80d2 100644 --- a/WeaselIPC/PipeChannel.cpp +++ b/WeaselIPC/PipeChannel.cpp @@ -38,12 +38,34 @@ bool PipeChannelBase::_Ensure() { return true; } +bool PipeChannelBase::_EnsureOnce() { + try { + HANDLE* phandle = _GetPipeHandle(); + if (_Invalid(*phandle)) { + HANDLE pipe = _TryConnect(); + if (_Invalid(pipe)) + return false; + DWORD mode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(pipe, &mode, NULL, NULL)) { + _FinalizePipe(pipe); + return false; + } + *phandle = pipe; + } + } catch (...) { + return false; + } + + return true; +} + HANDLE PipeChannelBase::_Connect(const wchar_t* name) { HANDLE pipe = INVALID_HANDLE_VALUE; while (_Invalid(pipe = _TryConnect())) ::WaitNamedPipe(name, 500); DWORD mode = PIPE_READMODE_MESSAGE; if (!SetNamedPipeHandleState(pipe, &mode, NULL, NULL)) { + _FinalizePipe(pipe); _ThrowLastError; } return pipe; @@ -68,12 +90,13 @@ HANDLE PipeChannelBase::_TryConnect() { return INVALID_HANDLE_VALUE; } -size_t PipeChannelBase::_WritePipe(HANDLE pipe, size_t s, char* b) { +size_t PipeChannelBase::_WritePipe(HANDLE pipe, size_t s, char* b, bool flush) { DWORD lwritten; if (!::WriteFile(pipe, b, s, &lwritten, NULL) || lwritten <= 0) { _ThrowLastError; } - ::FlushFileBuffers(pipe); + if (flush) + ::FlushFileBuffers(pipe); return lwritten; } diff --git a/WeaselIPC/WeaselClientImpl.cpp b/WeaselIPC/WeaselClientImpl.cpp index 26122ad50..f5a43bead 100644 --- a/WeaselIPC/WeaselClientImpl.cpp +++ b/WeaselIPC/WeaselClientImpl.cpp @@ -1,285 +1,502 @@ -#include "stdafx.h" -#include "WeaselClientImpl.h" -#include - -using namespace weasel; - -ClientImpl::ClientImpl() - : session_id(0), channel(GetPipeName()), is_ime(false) { - _InitializeClientInfo(); -} - -ClientImpl::~ClientImpl() { - if (channel.Connected()) - Disconnect(); -} - -// http://stackoverflow.com/questions/557081/how-do-i-get-the-hmodule-for-the-currently-executing-code -HMODULE GetCurrentModule() { // NB: XP+ solution! - HMODULE hModule = NULL; - GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, - (LPCTSTR)GetCurrentModule, &hModule); - - return hModule; -} - -void ClientImpl::_InitializeClientInfo() { - // get app name - WCHAR exe_path[MAX_PATH] = {0}; - GetModuleFileName(NULL, exe_path, MAX_PATH); - std::wstring path = exe_path; - size_t separator_pos = path.find_last_of(L"\\/"); - if (separator_pos < path.size()) - app_name = path.substr(separator_pos + 1); - else - app_name = path; - to_lower(app_name); - // determine client type - GetModuleFileName(GetCurrentModule(), exe_path, MAX_PATH); - path = exe_path; - to_lower(path); - is_ime = ends_with(path, L".ime"); -} - -bool ClientImpl::Connect(ServerLauncher const& launcher) { - return channel.Connect(); -} - -void ClientImpl::Disconnect() { - if (_Active()) - EndSession(); - channel.Disconnect(); -} - -void ClientImpl::ShutdownServer() { - _SendMessage(WEASEL_IPC_SHUTDOWN_SERVER, 0, 0); -} - -bool ClientImpl::ProcessKeyEvent(KeyEvent const& keyEvent) { - if (!_Active()) - return false; - - LRESULT ret = - _SendMessage(WEASEL_IPC_PROCESS_KEY_EVENT, keyEvent, session_id); - return ret != 0; -} - -bool ClientImpl::CommitComposition() { - if (!_Active()) - return false; - - LRESULT ret = _SendMessage(WEASEL_IPC_COMMIT_COMPOSITION, 0, session_id); - return ret != 0; -} - -bool ClientImpl::ClearComposition() { - if (!_Active()) - return false; - - LRESULT ret = _SendMessage(WEASEL_IPC_CLEAR_COMPOSITION, 0, session_id); - return ret != 0; -} - -bool ClientImpl::SelectCandidateOnCurrentPage(size_t index) { - if (!_Active()) - return false; - LRESULT ret = _SendMessage(WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, index, - session_id); - return ret != 0; -} - -bool ClientImpl::HighlightCandidateOnCurrentPage(size_t index) { - if (!_Active()) - return false; - LRESULT ret = _SendMessage(WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, - index, session_id); - return ret != 0; -} - -bool ClientImpl::ChangePage(bool backward) { - if (!_Active()) - return false; - LRESULT ret = _SendMessage(WEASEL_IPC_CHANGE_PAGE, backward, session_id); - return ret != 0; -} - -void ClientImpl::UpdateInputPosition(RECT const& rc) { - if (!_Active()) - return; - /* - 移位标志 = 1bit == 0 - height:0~127 = 7bit - top:-2048~2047 = 12bit(有符号) - left:-2048~2047 = 12bit(有符号) - - 高解析度下: - 移位标志 = 1bit == 1 - height:0~254 = 7bit(舍弃低1位) - top:-4096~4094 = 12bit(有符号,舍弃低1位) - left:-4096~4094 = 12bit(有符号,舍弃低1位) - */ - int hi_res = - static_cast(rc.bottom - rc.top >= 128 || rc.left < -2048 || - rc.left >= 2048 || rc.top < -2048 || rc.top >= 2048); - int left = max(-2048, min(2047, rc.left >> hi_res)); - int top = max(-2048, min(2047, rc.top >> hi_res)); - int height = max(0, min(127, (rc.bottom - rc.top) >> hi_res)); - DWORD compressed_rect = ((hi_res & 0x01) << 31) | ((height & 0x7f) << 24) | - ((top & 0xfff) << 12) | (left & 0xfff); - _SendMessage(WEASEL_IPC_UPDATE_INPUT_POS, compressed_rect, session_id); -} - -void ClientImpl::FocusIn() { - DWORD client_caps = 0; /* TODO */ - _SendMessage(WEASEL_IPC_FOCUS_IN, client_caps, session_id); -} - -void ClientImpl::FocusOut() { - _SendMessage(WEASEL_IPC_FOCUS_OUT, 0, session_id); -} - -void ClientImpl::TrayCommand(UINT menuId) { - _SendMessage(WEASEL_IPC_TRAY_COMMAND, menuId, session_id); -} - -void ClientImpl::StartSession() { - if (_Active() && Echo()) - return; - - _WriteClientInfo(); - UINT ret = _SendMessage(WEASEL_IPC_START_SESSION, 0, 0); - session_id = ret; -} - -void ClientImpl::EndSession() { - _SendMessage(WEASEL_IPC_END_SESSION, 0, session_id); - session_id = 0; -} - -void ClientImpl::StartMaintenance() { - _SendMessage(WEASEL_IPC_START_MAINTENANCE, 0, 0); - session_id = 0; -} - -void ClientImpl::EndMaintenance() { - _SendMessage(WEASEL_IPC_END_MAINTENANCE, 0, 0); - session_id = 0; -} - -bool ClientImpl::Echo() { - if (!_Active()) - return false; - - UINT serverEcho = _SendMessage(WEASEL_IPC_ECHO, 0, session_id); - return (serverEcho == session_id); -} - -bool ClientImpl::GetResponseData(ResponseHandler const& handler) { - if (!handler) { - return false; - } - - return channel.HandleResponseData(handler); -} - -bool ClientImpl::_WriteClientInfo() { - channel << L"action=session\n"; - channel << L"session.client_app=" << app_name.c_str() << L"\n"; - channel << L"session.client_type=" << (is_ime ? L"ime" : L"tsf") << L"\n"; - channel << L".\n"; - return true; -} - -LRESULT ClientImpl::_SendMessage(WEASEL_IPC_COMMAND Msg, - DWORD wParam, - DWORD lParam) { - try { - PipeMessage req{Msg, wParam, lParam}; - return channel.Transact(req); - } catch (DWORD /* ex */) { - return 0; - } -} - -Client::Client() : m_pImpl(new ClientImpl()) {} - -Client::~Client() { - if (m_pImpl) - delete m_pImpl; -} - -bool Client::Connect(ServerLauncher launcher) { - return m_pImpl->Connect(launcher); -} - -void Client::Disconnect() { - m_pImpl->Disconnect(); -} - -void Client::ShutdownServer() { - m_pImpl->ShutdownServer(); -} - -bool Client::ProcessKeyEvent(KeyEvent const& keyEvent) { - return m_pImpl->ProcessKeyEvent(keyEvent); -} - -bool Client::CommitComposition() { - return m_pImpl->CommitComposition(); -} - -bool Client::ClearComposition() { - return m_pImpl->ClearComposition(); -} - -bool Client::SelectCandidateOnCurrentPage(size_t index) { - return m_pImpl->SelectCandidateOnCurrentPage(index); -} - -bool Client::HighlightCandidateOnCurrentPage(size_t index) { - return m_pImpl->HighlightCandidateOnCurrentPage(index); -} - -bool Client::ChangePage(bool backward) { - return m_pImpl->ChangePage(backward); -} - -void Client::UpdateInputPosition(RECT const& rc) { - m_pImpl->UpdateInputPosition(rc); -} - -void Client::FocusIn() { - m_pImpl->FocusIn(); -} - -void Client::FocusOut() { - m_pImpl->FocusOut(); -} - -void Client::StartSession() { - m_pImpl->StartSession(); -} - -void Client::EndSession() { - m_pImpl->EndSession(); -} - -void Client::StartMaintenance() { - m_pImpl->StartMaintenance(); -} - -void Client::EndMaintenance() { - m_pImpl->EndMaintenance(); -} - -void Client::TrayCommand(UINT menuId) { - m_pImpl->TrayCommand(menuId); -} - -bool Client::Echo() { - return m_pImpl->Echo(); -} - -bool Client::GetResponseData(ResponseHandler handler) { - return m_pImpl->GetResponseData(handler); -} +#include "stdafx.h" +#include "WeaselClientImpl.h" +#include +#include + +using namespace weasel; + +ClientImpl::ClientImpl(std::wstring pipe_name) + : session_id(0), + channel(std::move(pipe_name)), + is_ime(false), + input_position_cache() { + _InitializeClientInfo(); +} + +ClientImpl::~ClientImpl() { + if (channel.Connected()) + Disconnect(); +} + +// http://stackoverflow.com/questions/557081/how-do-i-get-the-hmodule-for-the-currently-executing-code +HMODULE GetCurrentModule() { // NB: XP+ solution! + HMODULE hModule = NULL; + GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (LPCTSTR)GetCurrentModule, &hModule); + + return hModule; +} + +void ClientImpl::_InitializeClientInfo() { + // get app name + WCHAR exe_path[MAX_PATH] = {0}; + GetModuleFileName(NULL, exe_path, MAX_PATH); + std::wstring path = exe_path; + size_t separator_pos = path.find_last_of(L"\\/"); + if (separator_pos < path.size()) + app_name = path.substr(separator_pos + 1); + else + app_name = path; + to_lower(app_name); + // determine client type + GetModuleFileName(GetCurrentModule(), exe_path, MAX_PATH); + path = exe_path; + to_lower(path); + is_ime = ends_with(path, L".ime"); +} + +bool ClientImpl::Connect(ServerLauncher const& launcher) { + std::lock_guard lock(client_mutex); + if (channel.Connected()) + return true; + if (channel.TryConnect()) + return true; + if (launcher) + launcher(); + return channel.Connect(); +} + +bool ClientImpl::TryConnect() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (channel.Connected()) + return true; + return channel.TryConnect(); +} + +bool ClientImpl::Reconnect(ServerLauncher const& launcher, bool wait_for_pipe) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + + if (_Active()) { + DWORD ignored = 0; + _TrySendMessage(WEASEL_IPC_END_SESSION, 0, session_id, &ignored); + } + session_id = 0; + input_position_cache.Reset(); + channel.Disconnect(); + + if (wait_for_pipe) { + if (!channel.TryConnect() && launcher) + launcher(); + if (!channel.Connect()) + return false; + } else if (!channel.TryConnect()) { + return false; + } + + return _StartSessionLocked(wait_for_pipe); +} + +void ClientImpl::Disconnect() { + std::lock_guard lock(client_mutex); + if (_Active()) + EndSession(); + channel.Disconnect(); + input_position_cache.Reset(); +} + +void ClientImpl::ShutdownServer(DWORD reason) { + std::lock_guard lock(client_mutex); + _SendMessage(WEASEL_IPC_SHUTDOWN_SERVER, reason, 0); +} + +void ClientImpl::NotifyService(DWORD notification) { + std::lock_guard lock(client_mutex); + _SendMessage(WEASEL_IPC_NOTIFY_SERVICE, notification, 0); +} + +bool ClientImpl::ProcessKeyEvent(KeyEvent const& keyEvent) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_PROCESS_KEY_EVENT, keyEvent, session_id, + &result) && + result != 0; +} + +bool ClientImpl::ProcessKeyEvent(KeyEvent const& keyEvent, bool* eaten) { + if (eaten) + *eaten = false; + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (session_id == 0) + return false; + + DWORD result = 0; + if (!_TrySendMessage(WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS, keyEvent, + session_id, &result)) + return false; + if (!IsKeyEventResultProcessed(result)) + return false; + if (eaten) + *eaten = IsKeyEventResultEaten(result); + return true; +} + +bool ClientImpl::CommitComposition() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_COMMIT_COMPOSITION, 0, session_id, + &result) && + result != 0; +} + +bool ClientImpl::ClearComposition() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_CLEAR_COMPOSITION, 0, session_id, + &result) && + result != 0; +} + +bool ClientImpl::SelectCandidateOnCurrentPage(size_t index) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, + static_cast(index), session_id, &result) && + result != 0; +} + +bool ClientImpl::HighlightCandidateOnCurrentPage(size_t index) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, + static_cast(index), session_id, &result) && + result != 0; +} + +bool ClientImpl::ChangePage(bool backward) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + DWORD result = 0; + return _TrySendMessage(WEASEL_IPC_CHANGE_PAGE, backward, session_id, + &result) && + result != 0; +} + +void ClientImpl::UpdateInputPosition(RECT const& rc) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + if (!_Active()) + return; + /* + 移位标志 = 1bit == 0 + height:0~127 = 7bit + top:-2048~2047 = 12bit(有符号) + left:-2048~2047 = 12bit(有符号) + + 高解析度下: + 移位标志 = 1bit == 1 + height:0~254 = 7bit(舍弃低1位) + top:-4096~4094 = 12bit(有符号,舍弃低1位) + left:-4096~4094 = 12bit(有符号,舍弃低1位) + */ + int hi_res = + static_cast(rc.bottom - rc.top >= 128 || rc.left < -2048 || + rc.left >= 2048 || rc.top < -2048 || rc.top >= 2048); + int left = max(-2048, min(2047, rc.left >> hi_res)); + int top = max(-2048, min(2047, rc.top >> hi_res)); + int height = max(0, min(127, (rc.bottom - rc.top) >> hi_res)); + DWORD compressed_rect = ((hi_res & 0x01) << 31) | ((height & 0x7f) << 24) | + ((top & 0xfff) << 12) | (left & 0xfff); + if (!input_position_cache.ShouldSend(compressed_rect)) + return; + + DWORD result = 0; + if (_TrySendMessage(WEASEL_IPC_UPDATE_INPUT_POS, compressed_rect, session_id, + &result)) { + input_position_cache.MarkSent(compressed_rect); + } +} + +void ClientImpl::FocusIn() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + DWORD client_caps = 0; /* TODO */ + DWORD result = 0; + _TrySendMessage(WEASEL_IPC_FOCUS_IN, client_caps, session_id, &result); +} + +void ClientImpl::FocusOut() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + DWORD result = 0; + _TrySendMessage(WEASEL_IPC_FOCUS_OUT, 0, session_id, &result); +} + +void ClientImpl::TrayCommand(UINT menuId) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) { + WeaselDebugLog(L"WeaselClient", L"TrayCommand skipped: busy menuId=" + + std::to_wstring(menuId)); + return; + } + DWORD result = 0; + bool ok = + _TrySendMessage(WEASEL_IPC_TRAY_COMMAND, menuId, session_id, &result); + WeaselDebugLog(L"WeaselClient", + L"TrayCommand menuId=" + std::to_wstring(menuId) + + L" session_id=" + std::to_wstring(session_id) + + L" connected=" + std::to_wstring(channel.Connected()) + + L" ok=" + std::to_wstring(ok) + L" result=" + + std::to_wstring(result)); +} + +bool ClientImpl::TrayCommandSync(UINT menuId) { + std::lock_guard lock(client_mutex); + DWORD result = 0; + bool ok = + _TrySendMessage(WEASEL_IPC_TRAY_COMMAND, menuId, session_id, &result); + if (!ok) { + channel.Disconnect(); + if (channel.TryConnect()) + ok = + _TrySendMessage(WEASEL_IPC_TRAY_COMMAND, menuId, session_id, &result); + } + WeaselDebugLog(L"WeaselClient", + L"TrayCommandSync menuId=" + std::to_wstring(menuId) + + L" session_id=" + std::to_wstring(session_id) + + L" connected=" + std::to_wstring(channel.Connected()) + + L" ok=" + std::to_wstring(ok) + L" result=" + + std::to_wstring(result)); + return ok; +} + +void ClientImpl::StartSession() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + if (_Active() && Echo()) + return; + + _StartSessionLocked(false); +} + +void ClientImpl::EndSession() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + DWORD result = 0; + _TrySendMessage(WEASEL_IPC_END_SESSION, 0, session_id, &result); + session_id = 0; + input_position_cache.Reset(); +} + +void ClientImpl::StartMaintenance() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + _SendMessage(WEASEL_IPC_START_MAINTENANCE, 0, 0); + session_id = 0; + input_position_cache.Reset(); +} + +void ClientImpl::EndMaintenance(DWORD result) { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + _SendMessage(WEASEL_IPC_END_MAINTENANCE, result, 0); + session_id = 0; + input_position_cache.Reset(); +} + +bool ClientImpl::Echo() { + std::unique_lock lock(client_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return false; + if (!_Active()) + return false; + + DWORD serverEcho = 0; + if (!_TrySendMessage(WEASEL_IPC_ECHO, 0, session_id, &serverEcho)) + return false; + return (serverEcho == session_id); +} + +bool ClientImpl::GetResponseData(ResponseHandler const& handler) { + if (!handler) { + return false; + } + + // The response buffer is thread-local in PipeChannel. Parsing it must not be + // blocked by a background recovery thread that uses another thread's pipe. + return channel.HandleResponseData(handler); +} + +bool ClientImpl::_StartSessionLocked(bool wait_for_pipe) { + input_position_cache.Reset(); + _WriteClientInfo(); + DWORD ret = 0; + if (wait_for_pipe) { + ret = static_cast(_SendMessage(WEASEL_IPC_START_SESSION, 0, 0)); + } else if (!_TrySendMessage(WEASEL_IPC_START_SESSION, 0, 0, &ret)) { + session_id = 0; + return false; + } + session_id = ret; + return session_id != 0; +} + +bool ClientImpl::_WriteClientInfo() { + channel << L"action=session\n"; + channel << L"session.client_app=" << app_name.c_str() << L"\n"; + channel << L"session.client_type=" << (is_ime ? L"ime" : L"tsf") << L"\n"; + channel << L".\n"; + return true; +} + +LRESULT ClientImpl::_SendMessage(WEASEL_IPC_COMMAND Msg, + DWORD wParam, + DWORD lParam) { + try { + PipeMessage req{Msg, wParam, lParam}; + return channel.Transact(req); + } catch (DWORD /* ex */) { + return 0; + } +} + +bool ClientImpl::_TrySendMessage(WEASEL_IPC_COMMAND Msg, + DWORD wParam, + DWORD lParam, + DWORD* result) { + if (!result) + return false; + PipeMessage req{Msg, wParam, lParam}; + return channel.TryTransact(req, result); +} + +Client::Client() : m_pImpl(new ClientImpl()) {} + +Client::~Client() { + if (m_pImpl) + delete m_pImpl; +} + +bool Client::Connect(ServerLauncher launcher) { + return m_pImpl->Connect(launcher); +} + +bool Client::TryConnect() { + return m_pImpl->TryConnect(); +} + +bool Client::Reconnect(ServerLauncher launcher, bool wait_for_pipe) { + return m_pImpl->Reconnect(launcher, wait_for_pipe); +} + +void Client::Disconnect() { + m_pImpl->Disconnect(); +} + +void Client::ShutdownServer(DWORD reason) { + m_pImpl->ShutdownServer(reason); +} + +void Client::NotifyService(DWORD notification) { + m_pImpl->NotifyService(notification); +} + +bool Client::ProcessKeyEvent(KeyEvent const& keyEvent) { + return m_pImpl->ProcessKeyEvent(keyEvent); +} + +bool Client::ProcessKeyEvent(KeyEvent const& keyEvent, bool* eaten) { + return m_pImpl->ProcessKeyEvent(keyEvent, eaten); +} + +bool Client::CommitComposition() { + return m_pImpl->CommitComposition(); +} + +bool Client::ClearComposition() { + return m_pImpl->ClearComposition(); +} + +bool Client::SelectCandidateOnCurrentPage(size_t index) { + return m_pImpl->SelectCandidateOnCurrentPage(index); +} + +bool Client::HighlightCandidateOnCurrentPage(size_t index) { + return m_pImpl->HighlightCandidateOnCurrentPage(index); +} + +bool Client::ChangePage(bool backward) { + return m_pImpl->ChangePage(backward); +} + +void Client::UpdateInputPosition(RECT const& rc) { + m_pImpl->UpdateInputPosition(rc); +} + +void Client::FocusIn() { + m_pImpl->FocusIn(); +} + +void Client::FocusOut() { + m_pImpl->FocusOut(); +} + +void Client::StartSession() { + m_pImpl->StartSession(); +} + +void Client::EndSession() { + m_pImpl->EndSession(); +} + +void Client::StartMaintenance() { + m_pImpl->StartMaintenance(); +} + +void Client::EndMaintenance(DWORD result) { + m_pImpl->EndMaintenance(result); +} + +void Client::TrayCommand(UINT menuId) { + m_pImpl->TrayCommand(menuId); +} + +bool Client::TrayCommandSync(UINT menuId) { + return m_pImpl->TrayCommandSync(menuId); +} + +bool Client::Echo() { + return m_pImpl->Echo(); +} + +bool Client::GetResponseData(ResponseHandler handler) { + return m_pImpl->GetResponseData(handler); +} diff --git a/WeaselIPC/WeaselClientImpl.h b/WeaselIPC/WeaselClientImpl.h index 760279286..a8e05e3ae 100644 --- a/WeaselIPC/WeaselClientImpl.h +++ b/WeaselIPC/WeaselClientImpl.h @@ -1,49 +1,82 @@ -#pragma once -#include -#include - -namespace weasel { - -class ClientImpl { - public: - ClientImpl(); - ~ClientImpl(); - - bool Connect(ServerLauncher const& launcher); - void Disconnect(); - void ShutdownServer(); - void StartSession(); - void EndSession(); - void StartMaintenance(); - void EndMaintenance(); - bool Echo(); - bool ProcessKeyEvent(KeyEvent const& keyEvent); - bool CommitComposition(); - bool ClearComposition(); - bool SelectCandidateOnCurrentPage(size_t index); - bool HighlightCandidateOnCurrentPage(size_t index); - bool ChangePage(bool backward); - void UpdateInputPosition(RECT const& rc); - void FocusIn(); - void FocusOut(); - void TrayCommand(UINT menuId); - bool GetResponseData(ResponseHandler const& handler); - - protected: - void _InitializeClientInfo(); - bool _WriteClientInfo(); - - LRESULT _SendMessage(WEASEL_IPC_COMMAND Msg, DWORD wParam, DWORD lParam); - - bool _Connected() const { return channel.Connected(); } - bool _Active() const { return channel.Connected() && session_id != 0; } - - private: - UINT session_id; - std::wstring app_name; - bool is_ime; - - PipeChannel channel; -}; - -} // namespace weasel \ No newline at end of file +#pragma once +#include +#include +#include + +namespace weasel { + +struct InputPositionCache { + InputPositionCache() : has_position(false), last_position(0) {} + + bool ShouldSend(DWORD position) const { + return !has_position || last_position != position; + } + + void MarkSent(DWORD position) { + has_position = true; + last_position = position; + } + + void Reset() { + has_position = false; + last_position = 0; + } + + bool has_position; + DWORD last_position; +}; + +class ClientImpl { + public: + explicit ClientImpl(std::wstring pipe_name = GetPipeName()); + ~ClientImpl(); + + bool Connect(ServerLauncher const& launcher); + bool TryConnect(); + bool Reconnect(ServerLauncher const& launcher, bool wait_for_pipe); + void Disconnect(); + void ShutdownServer(DWORD reason); + void NotifyService(DWORD notification); + void StartSession(); + void EndSession(); + void StartMaintenance(); + void EndMaintenance(DWORD result = WEASEL_IPC_MAINTENANCE_RESULT_NONE); + bool Echo(); + bool ProcessKeyEvent(KeyEvent const& keyEvent); + bool ProcessKeyEvent(KeyEvent const& keyEvent, bool* eaten); + bool CommitComposition(); + bool ClearComposition(); + bool SelectCandidateOnCurrentPage(size_t index); + bool HighlightCandidateOnCurrentPage(size_t index); + bool ChangePage(bool backward); + void UpdateInputPosition(RECT const& rc); + void FocusIn(); + void FocusOut(); + void TrayCommand(UINT menuId); + bool TrayCommandSync(UINT menuId); + bool GetResponseData(ResponseHandler const& handler); + + protected: + void _InitializeClientInfo(); + bool _WriteClientInfo(); + + LRESULT _SendMessage(WEASEL_IPC_COMMAND Msg, DWORD wParam, DWORD lParam); + bool _TrySendMessage(WEASEL_IPC_COMMAND Msg, + DWORD wParam, + DWORD lParam, + DWORD* result); + bool _StartSessionLocked(bool wait_for_pipe); + + bool _Connected() const { return channel.Connected(); } + bool _Active() const { return channel.Connected() && session_id != 0; } + + private: + UINT session_id; + std::wstring app_name; + bool is_ime; + InputPositionCache input_position_cache; + std::recursive_mutex client_mutex; + + PipeChannel channel; +}; +} // namespace weasel diff --git a/WeaselIPCServer/WeaselServerImpl.cpp b/WeaselIPCServer/WeaselServerImpl.cpp index 1131e41e7..8c30b9abb 100644 --- a/WeaselIPCServer/WeaselServerImpl.cpp +++ b/WeaselIPCServer/WeaselServerImpl.cpp @@ -1,471 +1,597 @@ -#include "stdafx.h" -#include "WeaselServerImpl.h" -#include -#include -#include -#include - -namespace weasel { -class PipeServer : public PipeChannel { - public: - using ServerRunner = std::function; - using Respond = std::function; - using ServerHandler = std::function; - - PipeServer(std::wstring&& pn_cmd, SECURITY_ATTRIBUTES* s); - - public: - void Listen(ServerHandler const& handler); - /* Get a server runner */ - ServerRunner GetServerRunner(ServerHandler const& handler); - - private: - void _ProcessPipeThread(HANDLE pipe, ServerHandler const& handler); -}; -} // namespace weasel - -using namespace weasel; - -extern CAppModule _Module; - -ServerImpl::ServerImpl() - : m_pRequestHandler(NULL), - m_darkMode(IsUserDarkMode()), - channel(std::make_unique(GetPipeName(), sa.get_attr())) { - m_hUser32Module = GetModuleHandle(_T("user32.dll")); -} - -ServerImpl::~ServerImpl() { - _Finailize(); -} - -void ServerImpl::_Finailize() { - if (pipeThread != nullptr) { - pipeThread->interrupt(); - pipeThread = nullptr; - } else { - // avoid finalize again - return; - } - - if (IsWindow()) { - DestroyWindow(); - } -} - -LRESULT ServerImpl::OnColorChange(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - if (IsUserDarkMode() != m_darkMode) { - m_darkMode = IsUserDarkMode(); - m_pRequestHandler->UpdateColorTheme(m_darkMode); - } - return 0; -} - -LRESULT ServerImpl::OnCreate(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - // not neccessary... - ::SetWindowText(m_hWnd, WEASEL_IPC_WINDOW); - return 0; -} - -LRESULT ServerImpl::OnClose(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - Stop(); - return 0; -} - -LRESULT ServerImpl::OnDestroy(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - bHandled = FALSE; - return 1; -} - -LRESULT ServerImpl::OnQueryEndSystemSession(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - return TRUE; -} - -LRESULT ServerImpl::OnEndSystemSession(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - if (m_pRequestHandler) { - m_pRequestHandler->Finalize(); - m_pRequestHandler = nullptr; - } - return 0; -} - -LRESULT ServerImpl::OnCommand(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled) { - UINT uID = LOWORD(wParam); - switch (uID) { - case ID_WEASELTRAY_ENABLE_ASCII: - m_pRequestHandler->SetOption(lParam, "ascii_mode", true); - return 0; - case ID_WEASELTRAY_DISABLE_ASCII: - m_pRequestHandler->SetOption(lParam, "ascii_mode", false); - return 0; - default:; - } - - std::map::iterator it = m_MenuHandlers.find(uID); - if (it == m_MenuHandlers.end()) { - bHandled = FALSE; - return 0; - } - it->second(); // execute command - return 0; -} - -DWORD ServerImpl::OnCommand(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - BOOL handled = TRUE; - OnCommand(uMsg, wParam, lParam, handled); - return handled; -} - -HWND ServerImpl::Start() { - std::wstring instanceName = L"(WEASEL)Furandōru-Sukāretto-"; - instanceName += getUsername(); - HANDLE hMutexOneInstance = ::CreateMutex(NULL, FALSE, instanceName.c_str()); - bool areYouOK = (::GetLastError() == ERROR_ALREADY_EXISTS || - ::GetLastError() == ERROR_ACCESS_DENIED); - - if (areYouOK) { - return 0; // assure single instance - } - - HWND hwnd = Create(NULL); - - return hwnd; -} - -int ServerImpl::Stop() { - // DO NOT exit process or finalize here - // Let WeaselServer handle this - PostMessage(WM_QUIT); - return 0; -} - -static std::mutex g_api_mutex; - -int ServerImpl::Run() { - // This workaround causes a VC internal error: - // void PipeServer::Listen(ServerHandler handler); - // - // auto handler = boost::bind(&ServerImpl::HandlePipeMessage, this); - // auto listener = boost::bind(&PipeServer::Listen, channel.get(), handler); - // - auto listener = [this](PipeMessage msg, PipeServer::Respond resp) -> void { - std::lock_guard guard(g_api_mutex); - HandlePipeMessage(msg, resp); - }; - pipeThread = std::make_unique( - [this, &listener]() { channel->Listen(listener); }); - - CMessageLoop theLoop; - _Module.AddMessageLoop(&theLoop); - int nRet = theLoop.Run(); - _Module.RemoveMessageLoop(); - return nRet; -} - -DWORD ServerImpl::OnEcho(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam) { - if (!m_pRequestHandler) - return 0; - return m_pRequestHandler->FindSession(lParam); -} - -DWORD ServerImpl::OnStartSession(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler) - return 0; - return m_pRequestHandler->AddSession( - reinterpret_cast(channel->ReceiveBuffer()), - [this](std::wstring& msg) -> bool { - *channel << msg; - return true; - }); -} - -DWORD ServerImpl::OnEndSession(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler) - return 0; - return m_pRequestHandler->RemoveSession(lParam); -} - -DWORD ServerImpl::OnKeyEvent(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler /* || !m_pSharedMemory*/) - return 0; - - auto eat = [this](std::wstring& msg) -> bool { - *channel << msg; - return true; - }; - return m_pRequestHandler->ProcessKeyEvent(KeyEvent(wParam), lParam, eat); -} - -DWORD ServerImpl::OnShutdownServer(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - Stop(); - return 0; -} - -DWORD ServerImpl::OnFocusIn(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler) - return 0; - m_pRequestHandler->FocusIn(wParam, lParam); - return 0; -} - -DWORD ServerImpl::OnFocusOut(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler) - return 0; - m_pRequestHandler->FocusOut(wParam, lParam); - return 0; -} - -DWORD ServerImpl::OnUpdateInputPosition(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (!m_pRequestHandler) - return 0; - /* - * 移位标志 = 1bit == 0 - * height: 0~127 = 7bit - * top:-2048~2047 = 12bit(有符号) - * left:-2048~2047 = 12bit(有符号) - * - * 高解析度下: - * 移位标志 = 1bit == 1 - * height: 0~254 = 7bit(舍弃低1位) - * top: -4096~4094 = 12bit(有符号,舍弃低1位) - * left: -4096~4094 = 12bit(有符号,舍弃低1位) - */ - RECT rc; - int hi_res = (wParam >> 31) & 0x01; - rc.left = ((wParam & 0x7ff) - (wParam & 0x800)) << hi_res; - rc.top = (((wParam >> 12) & 0x7ff) - ((wParam >> 12) & 0x800)) << hi_res; - const int width = 6; - int height = ((wParam >> 24) & 0x7f) << hi_res; - rc.right = rc.left + width; - rc.bottom = rc.top + height; - - { - using PPTLPFPMDPI = BOOL(WINAPI*)(HWND, LPPOINT); - PPTLPFPMDPI PhysicalToLogicalPointForPerMonitorDPI = - (PPTLPFPMDPI)::GetProcAddress(m_hUser32Module, - "PhysicalToLogicalPointForPerMonitorDPI"); - POINT lt = {rc.left, rc.top}; - POINT rb = {rc.right, rc.bottom}; - PhysicalToLogicalPointForPerMonitorDPI(NULL, <); - PhysicalToLogicalPointForPerMonitorDPI(NULL, &rb); - rc = {lt.x, lt.y, rb.x, rb.y}; - } - - m_pRequestHandler->UpdateInputPosition(rc, lParam); - return 0; -} - -DWORD ServerImpl::OnStartMaintenance(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) - m_pRequestHandler->StartMaintenance(); - return 0; -} - -DWORD ServerImpl::OnEndMaintenance(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) - m_pRequestHandler->EndMaintenance(); - return 0; -} - -DWORD ServerImpl::OnCommitComposition(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) - m_pRequestHandler->CommitComposition(lParam); - return 0; -} - -DWORD ServerImpl::OnClearComposition(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) - m_pRequestHandler->ClearComposition(lParam); - return 0; -} - -DWORD ServerImpl::OnSelectCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) - m_pRequestHandler->SelectCandidateOnCurrentPage(wParam, lParam); - return 0; -} - -DWORD ServerImpl::OnHighlightCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) { - auto eat = [this](std::wstring& msg) -> bool { - *channel << msg; - return true; - }; - m_pRequestHandler->HighlightCandidateOnCurrentPage(wParam, lParam, eat); - } - return 0; -} - -DWORD ServerImpl::OnChangePage(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam) { - if (m_pRequestHandler) { - auto eat = [this](std::wstring& msg) -> bool { - *channel << msg; - return true; - }; - m_pRequestHandler->ChangePage(wParam, lParam, eat); - } - return 0; -} - -#define MAP_PIPE_MSG_HANDLE(__msg, __wParam, __lParam) \ - { \ - auto lParam = __lParam; \ - auto wParam = __wParam; \ - LRESULT _result = 0; \ - switch (__msg) { -#define PIPE_MSG_HANDLE(__msg, __func) \ - case __msg: \ - _result = __func(__msg, wParam, lParam); \ - break; - -#define END_MAP_PIPE_MSG_HANDLE(__result) \ - } \ - __result = _result; \ - } - -template -void ServerImpl::HandlePipeMessage(PipeMessage pipe_msg, _Resp resp) { - DWORD result; - - MAP_PIPE_MSG_HANDLE(pipe_msg.Msg, pipe_msg.wParam, pipe_msg.lParam) - PIPE_MSG_HANDLE(WEASEL_IPC_ECHO, OnEcho) - PIPE_MSG_HANDLE(WEASEL_IPC_START_SESSION, OnStartSession) - PIPE_MSG_HANDLE(WEASEL_IPC_END_SESSION, OnEndSession) - PIPE_MSG_HANDLE(WEASEL_IPC_PROCESS_KEY_EVENT, OnKeyEvent) - PIPE_MSG_HANDLE(WEASEL_IPC_SHUTDOWN_SERVER, OnShutdownServer) - PIPE_MSG_HANDLE(WEASEL_IPC_FOCUS_IN, OnFocusIn) - PIPE_MSG_HANDLE(WEASEL_IPC_FOCUS_OUT, OnFocusOut) - PIPE_MSG_HANDLE(WEASEL_IPC_UPDATE_INPUT_POS, OnUpdateInputPosition) - PIPE_MSG_HANDLE(WEASEL_IPC_START_MAINTENANCE, OnStartMaintenance) - PIPE_MSG_HANDLE(WEASEL_IPC_END_MAINTENANCE, OnEndMaintenance) - PIPE_MSG_HANDLE(WEASEL_IPC_COMMIT_COMPOSITION, OnCommitComposition) - PIPE_MSG_HANDLE(WEASEL_IPC_CLEAR_COMPOSITION, OnClearComposition); - PIPE_MSG_HANDLE(WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, - OnSelectCandidateOnCurrentPage); - PIPE_MSG_HANDLE(WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, - OnHighlightCandidateOnCurrentPage); - PIPE_MSG_HANDLE(WEASEL_IPC_CHANGE_PAGE, OnChangePage); - PIPE_MSG_HANDLE(WEASEL_IPC_TRAY_COMMAND, OnCommand); - END_MAP_PIPE_MSG_HANDLE(result); - - resp(result); -} - -PipeServer::PipeServer(std::wstring&& pn_cmd, SECURITY_ATTRIBUTES* s) - : PipeChannel(std::move(pn_cmd), s) {} - -void PipeServer::Listen(ServerHandler const& handler) { - for (;;) { - HANDLE pipe = INVALID_HANDLE_VALUE; - try { - boost::this_thread::interruption_point(); - pipe = _ConnectServerPipe(pname); - boost::thread th( - [&handler, pipe, this] { _ProcessPipeThread(pipe, handler); }); - } catch (DWORD ex) { - _FinalizePipe(pipe); - } - boost::this_thread::interruption_point(); - } -} - -PipeServer::ServerRunner PipeServer::GetServerRunner( - ServerHandler const& handler) { - return [&handler, this]() { Listen(handler); }; -} - -void PipeServer::_ProcessPipeThread(HANDLE pipe, ServerHandler const& handler) { - try { - for (;;) { - Res msg; - _Receive(pipe, &msg, sizeof(msg)); - handler(msg, [this, pipe](Msg resp) { _Send(pipe, resp); }); - } - } catch (...) { - _FinalizePipe(pipe); - } -} - -// weasel::Server - -Server::Server() : m_pImpl(new ServerImpl) {} - -Server::~Server() { - if (m_pImpl) - delete m_pImpl; -} - -HWND Server::Start() { - return m_pImpl->Start(); -} - -int Server::Stop() { - return m_pImpl->Stop(); -} - -int Server::Run() { - return m_pImpl->Run(); -} - -void Server::SetRequestHandler(RequestHandler* pHandler) { - m_pImpl->SetRequestHandler(pHandler); -} - -void Server::AddMenuHandler(UINT uID, CommandHandler handler) { - m_pImpl->AddMenuHandler(uID, handler); -} - -HWND Server::GetHWnd() { - return m_pImpl->m_hWnd; -} +#include "stdafx.h" +#include "WeaselServerImpl.h" +#include +#include +#include +#include + +namespace weasel { +class PipeServer : public PipeChannel { + public: + using ServerRunner = std::function; + using Respond = std::function; + using ServerHandler = std::function; + + PipeServer(std::wstring&& pn_cmd, SECURITY_ATTRIBUTES* s); + + public: + void Listen(ServerHandler const& handler); + /* Get a server runner */ + ServerRunner GetServerRunner(ServerHandler const& handler); + + private: + void _ProcessPipeThread(HANDLE pipe, ServerHandler const& handler); +}; +} // namespace weasel + +using namespace weasel; + +extern CAppModule _Module; +static std::mutex g_api_mutex; + +static bool PipeMessageUsesTryOuterApiLock(PipeMessage pipe_msg) { + return pipe_msg.Msg == WEASEL_IPC_PROCESS_KEY_EVENT || + pipe_msg.Msg == WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS; +} + +ServerImpl::ServerImpl() + : m_pRequestHandler(NULL), + m_darkMode(IsUserDarkMode()), + channel(std::make_unique(GetPipeName(), sa.get_attr())) { + m_hUser32Module = GetModuleHandle(_T("user32.dll")); +} + +ServerImpl::~ServerImpl() { + _Finailize(); +} + +void ServerImpl::_Finailize() { + if (pipeThread != nullptr) { + pipeThread->interrupt(); + pipeThread = nullptr; + } else { + // avoid finalize again + return; + } + + if (IsWindow()) { + DestroyWindow(); + } +} + +LRESULT ServerImpl::OnColorChange(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + if (IsUserDarkMode() != m_darkMode) { + m_darkMode = IsUserDarkMode(); + std::lock_guard guard(g_api_mutex); + if (m_pRequestHandler) + m_pRequestHandler->UpdateColorTheme(m_darkMode); + } + return 0; +} + +LRESULT ServerImpl::OnCreate(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + // not neccessary... + ::SetWindowText(m_hWnd, WEASEL_IPC_WINDOW); + return 0; +} + +LRESULT ServerImpl::OnClose(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + Stop(); + return 0; +} + +LRESULT ServerImpl::OnDestroy(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + bHandled = FALSE; + return 1; +} + +LRESULT ServerImpl::OnQueryEndSystemSession(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + return TRUE; +} + +LRESULT ServerImpl::OnEndSystemSession(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + std::lock_guard guard(g_api_mutex); + if (m_pRequestHandler) { + m_pRequestHandler->Finalize(); + m_pRequestHandler = nullptr; + } + return 0; +} + +LRESULT ServerImpl::OnServiceNotifyMessage(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + std::lock_guard guard(g_api_mutex); + if (m_pRequestHandler) + m_pRequestHandler->NotifyService(static_cast(wParam)); + return 0; +} + +LRESULT ServerImpl::OnCommand(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled) { + UINT uID = LOWORD(wParam); + switch (uID) { + case ID_WEASELTRAY_ENABLE_ASCII: { + std::lock_guard guard(g_api_mutex); + if (m_pRequestHandler) + m_pRequestHandler->SetOption(lParam, "ascii_mode", true); + return 0; + } + case ID_WEASELTRAY_DISABLE_ASCII: { + std::lock_guard guard(g_api_mutex); + if (m_pRequestHandler) + m_pRequestHandler->SetOption(lParam, "ascii_mode", false); + return 0; + } + default:; + } + + std::map::iterator it = m_MenuHandlers.find(uID); + if (it == m_MenuHandlers.end()) { + bHandled = FALSE; + return 0; + } + it->second(); // execute command + return 0; +} + +DWORD ServerImpl::OnCommand(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + BOOL handled = TRUE; + OnCommand(uMsg, wParam, lParam, handled); + return handled; +} + +HWND ServerImpl::Start() { + std::wstring instanceName = ServiceInstanceMutexName(); + HANDLE hMutexOneInstance = ::CreateMutex(NULL, FALSE, instanceName.c_str()); + DWORD mutex_error = ::GetLastError(); + bool areYouOK = (mutex_error == ERROR_ALREADY_EXISTS || + mutex_error == ERROR_ACCESS_DENIED); + WeaselDebugLog( + L"WeaselIPCServer", + L"Start mutex=" + instanceName + L" handle=" + + std::to_wstring(reinterpret_cast(hMutexOneInstance)) + + L" error=" + std::to_wstring(mutex_error) + L" already_running=" + + std::to_wstring(areYouOK)); + + if (areYouOK) { + WeaselDebugLog(L"WeaselIPCServer", L"Start failed: single instance guard"); + return 0; // assure single instance + } + + HWND hwnd = Create(NULL); + WeaselDebugLog(L"WeaselIPCServer", + L"Create hidden window hwnd=" + + std::to_wstring(reinterpret_cast(hwnd)) + + L" last_error=" + std::to_wstring(GetLastError())); + + return hwnd; +} + +int ServerImpl::Stop() { + WeaselDebugLog(L"WeaselIPCServer", L"Stop requested"); + // DO NOT exit process or finalize here + // Let WeaselServer handle this + PostMessage(WM_QUIT); + return 0; +} + +bool ServerImpl::PipeMessageNeedsOuterApiLock(PipeMessage pipe_msg) { + switch (pipe_msg.Msg) { + case WEASEL_IPC_PROCESS_KEY_EVENT: + case WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS: + case WEASEL_IPC_SHUTDOWN_SERVER: + case WEASEL_IPC_NOTIFY_SERVICE: + case WEASEL_IPC_UPDATE_INPUT_POS: + case WEASEL_IPC_TRAY_COMMAND: + return false; + default: + return true; + } +} + +void ServerImpl::PostServiceNotification(DWORD notification) { + if (!notification) + return; + PostMessage(WM_WEASEL_SERVICE_NOTIFY, static_cast(notification), 0); +} + +int ServerImpl::Run() { + // This workaround causes a VC internal error: + // void PipeServer::Listen(ServerHandler handler); + // + // auto handler = boost::bind(&ServerImpl::HandlePipeMessage, this); + // auto listener = boost::bind(&PipeServer::Listen, channel.get(), handler); + // + auto listener = [this](PipeMessage msg, PipeServer::Respond resp) -> void { + HandlePipeMessage(msg, resp); + }; + pipeThread = std::make_unique( + [this, &listener]() { channel->Listen(listener); }); + WeaselDebugLog(L"WeaselIPCServer", L"pipe listener thread started"); + + CMessageLoop theLoop; + _Module.AddMessageLoop(&theLoop); + int nRet = theLoop.Run(); + _Module.RemoveMessageLoop(); + return nRet; +} + +DWORD ServerImpl::OnEcho(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam) { + if (!m_pRequestHandler) + return 0; + return m_pRequestHandler->FindSession(lParam); +} + +DWORD ServerImpl::OnStartSession(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler) + return 0; + return m_pRequestHandler->AddSession( + reinterpret_cast(channel->ReceiveBuffer()), + [this](std::wstring& msg) -> bool { + *channel << msg; + return true; + }); +} + +DWORD ServerImpl::OnEndSession(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler) + return 0; + return m_pRequestHandler->RemoveSession(lParam); +} + +DWORD ServerImpl::OnKeyEvent(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler || !m_pRequestHandler->FindSession(lParam)) + return 0; + + auto eat = [this](std::wstring& msg) -> bool { + *channel << msg; + return true; + }; + return m_pRequestHandler->ProcessKeyEvent(KeyEvent(wParam), lParam, eat); +} + +DWORD ServerImpl::OnKeyEventWithStatus(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler || !m_pRequestHandler->FindSession(lParam)) + return 0; + + auto eat = [this](std::wstring& msg) -> bool { + *channel << msg; + return true; + }; + BOOL eaten = + m_pRequestHandler->ProcessKeyEvent(KeyEvent(wParam), lParam, eat); + return MakeKeyEventResult(eaten); +} + +DWORD ServerImpl::OnShutdownServer(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + DWORD reason = WEASEL_IPC_SHUTDOWN_REASON_EXIT; + if (wParam == WEASEL_IPC_SHUTDOWN_REASON_RESTART) + reason = WEASEL_IPC_SHUTDOWN_REASON_RESTART; + else if (wParam == WEASEL_IPC_SHUTDOWN_REASON_STOP) + reason = WEASEL_IPC_SHUTDOWN_REASON_STOP; + if (reason == WEASEL_IPC_SHUTDOWN_REASON_EXIT) + MarkServiceManualExit(); + else if (reason == WEASEL_IPC_SHUTDOWN_REASON_RESTART) + ClearServiceManualExit(); + if (reason == WEASEL_IPC_SHUTDOWN_REASON_EXIT) + UnregisterApplicationRestart(); + + DWORD notification = WEASEL_IPC_SERVICE_NOTIFICATION_EXITING; + if (reason == WEASEL_IPC_SHUTDOWN_REASON_RESTART) + notification = WEASEL_IPC_SERVICE_NOTIFICATION_RESTARTING; + PostServiceNotification(notification); + boost::thread([this] { + Sleep(1200); + Stop(); + }).detach(); + return 0; +} + +DWORD ServerImpl::OnServiceNotification(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + PostServiceNotification(wParam); + return 0; +} + +DWORD ServerImpl::OnFocusIn(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler) + return 0; + m_pRequestHandler->FocusIn(wParam, lParam); + return 0; +} + +DWORD ServerImpl::OnFocusOut(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (!m_pRequestHandler) + return 0; + m_pRequestHandler->FocusOut(wParam, lParam); + return 0; +} + +DWORD ServerImpl::OnUpdateInputPosition(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + /* + * 移位标志 = 1bit == 0 + * height: 0~127 = 7bit + * top:-2048~2047 = 12bit(有符号) + * left:-2048~2047 = 12bit(有符号) + * + * 高解析度下: + * 移位标志 = 1bit == 1 + * height: 0~254 = 7bit(舍弃低1位) + * top: -4096~4094 = 12bit(有符号,舍弃低1位) + * left: -4096~4094 = 12bit(有符号,舍弃低1位) + */ + RECT rc; + int hi_res = (wParam >> 31) & 0x01; + rc.left = ((wParam & 0x7ff) - (wParam & 0x800)) << hi_res; + rc.top = (((wParam >> 12) & 0x7ff) - ((wParam >> 12) & 0x800)) << hi_res; + const int width = 6; + int height = ((wParam >> 24) & 0x7f) << hi_res; + rc.right = rc.left + width; + rc.bottom = rc.top + height; + + { + using PPTLPFPMDPI = BOOL(WINAPI*)(HWND, LPPOINT); + PPTLPFPMDPI PhysicalToLogicalPointForPerMonitorDPI = + (PPTLPFPMDPI)::GetProcAddress(m_hUser32Module, + "PhysicalToLogicalPointForPerMonitorDPI"); + POINT lt = {rc.left, rc.top}; + POINT rb = {rc.right, rc.bottom}; + PhysicalToLogicalPointForPerMonitorDPI(NULL, <); + PhysicalToLogicalPointForPerMonitorDPI(NULL, &rb); + rc = {lt.x, lt.y, rb.x, rb.y}; + } + + std::lock_guard guard(g_api_mutex); + if (!m_pRequestHandler) + return 0; + m_pRequestHandler->UpdateInputPosition(rc, lParam); + return 0; +} + +DWORD ServerImpl::OnStartMaintenance(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) + m_pRequestHandler->StartMaintenance(); + return 0; +} + +DWORD ServerImpl::OnEndMaintenance(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) + m_pRequestHandler->EndMaintenance(wParam); + return 0; +} + +DWORD ServerImpl::OnCommitComposition(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) + m_pRequestHandler->CommitComposition(lParam); + return 0; +} + +DWORD ServerImpl::OnClearComposition(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) + m_pRequestHandler->ClearComposition(lParam); + return 0; +} + +DWORD ServerImpl::OnSelectCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) + m_pRequestHandler->SelectCandidateOnCurrentPage(wParam, lParam); + return 0; +} + +DWORD ServerImpl::OnHighlightCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) { + auto eat = [this](std::wstring& msg) -> bool { + *channel << msg; + return true; + }; + m_pRequestHandler->HighlightCandidateOnCurrentPage(wParam, lParam, eat); + } + return 0; +} + +DWORD ServerImpl::OnChangePage(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + if (m_pRequestHandler) { + auto eat = [this](std::wstring& msg) -> bool { + *channel << msg; + return true; + }; + m_pRequestHandler->ChangePage(wParam, lParam, eat); + } + return 0; +} + +#define MAP_PIPE_MSG_HANDLE(__msg, __wParam, __lParam) \ + { \ + auto lParam = __lParam; \ + auto wParam = __wParam; \ + LRESULT _result = 0; \ + switch (__msg) { +#define PIPE_MSG_HANDLE(__msg, __func) \ + case __msg: \ + _result = __func(__msg, wParam, lParam); \ + break; + +#define END_MAP_PIPE_MSG_HANDLE(__result) \ + } \ + __result = _result; \ + } + +DWORD ServerImpl::DispatchPipeMessage(PipeMessage pipe_msg) { + DWORD result; + + MAP_PIPE_MSG_HANDLE(pipe_msg.Msg, pipe_msg.wParam, pipe_msg.lParam) + PIPE_MSG_HANDLE(WEASEL_IPC_ECHO, OnEcho) + PIPE_MSG_HANDLE(WEASEL_IPC_START_SESSION, OnStartSession) + PIPE_MSG_HANDLE(WEASEL_IPC_END_SESSION, OnEndSession) + PIPE_MSG_HANDLE(WEASEL_IPC_PROCESS_KEY_EVENT, OnKeyEvent) + PIPE_MSG_HANDLE(WEASEL_IPC_SHUTDOWN_SERVER, OnShutdownServer) + PIPE_MSG_HANDLE(WEASEL_IPC_NOTIFY_SERVICE, OnServiceNotification) + PIPE_MSG_HANDLE(WEASEL_IPC_FOCUS_IN, OnFocusIn) + PIPE_MSG_HANDLE(WEASEL_IPC_FOCUS_OUT, OnFocusOut) + PIPE_MSG_HANDLE(WEASEL_IPC_UPDATE_INPUT_POS, OnUpdateInputPosition) + PIPE_MSG_HANDLE(WEASEL_IPC_START_MAINTENANCE, OnStartMaintenance) + PIPE_MSG_HANDLE(WEASEL_IPC_END_MAINTENANCE, OnEndMaintenance) + PIPE_MSG_HANDLE(WEASEL_IPC_COMMIT_COMPOSITION, OnCommitComposition) + PIPE_MSG_HANDLE(WEASEL_IPC_CLEAR_COMPOSITION, OnClearComposition); + PIPE_MSG_HANDLE(WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, + OnSelectCandidateOnCurrentPage); + PIPE_MSG_HANDLE(WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, + OnHighlightCandidateOnCurrentPage); + PIPE_MSG_HANDLE(WEASEL_IPC_CHANGE_PAGE, OnChangePage); + PIPE_MSG_HANDLE(WEASEL_IPC_TRAY_COMMAND, OnCommand); + PIPE_MSG_HANDLE(WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS, + OnKeyEventWithStatus); + END_MAP_PIPE_MSG_HANDLE(result); + + return result; +} + +template +void ServerImpl::HandlePipeMessage(PipeMessage pipe_msg, _Resp resp) { + DWORD result; + if (PipeMessageUsesTryOuterApiLock(pipe_msg)) { + std::unique_lock guard(g_api_mutex, std::try_to_lock); + if (!guard.owns_lock()) { + result = 0; + } else { + result = DispatchPipeMessage(pipe_msg); + } + } else if (PipeMessageNeedsOuterApiLock(pipe_msg)) { + std::lock_guard guard(g_api_mutex); + result = DispatchPipeMessage(pipe_msg); + } else { + result = DispatchPipeMessage(pipe_msg); + } + + resp(result); +} + +PipeServer::PipeServer(std::wstring&& pn_cmd, SECURITY_ATTRIBUTES* s) + : PipeChannel(std::move(pn_cmd), s) {} + +void PipeServer::Listen(ServerHandler const& handler) { + for (;;) { + HANDLE pipe = INVALID_HANDLE_VALUE; + try { + boost::this_thread::interruption_point(); + WeaselDebugLog(L"WeaselIPCServer", L"waiting for pipe connection"); + pipe = _ConnectServerPipe(pname); + WeaselDebugLog(L"WeaselIPCServer", + L"pipe connected handle=" + + std::to_wstring(reinterpret_cast(pipe))); + boost::thread th( + [&handler, pipe, this] { _ProcessPipeThread(pipe, handler); }); + th.detach(); + } catch (DWORD ex) { + WeaselDebugLog(L"WeaselIPCServer", + L"pipe listen exception=" + std::to_wstring(ex)); + _FinalizePipe(pipe); + } + boost::this_thread::interruption_point(); + } +} + +PipeServer::ServerRunner PipeServer::GetServerRunner( + ServerHandler const& handler) { + return [&handler, this]() { Listen(handler); }; +} + +void PipeServer::_ProcessPipeThread(HANDLE pipe, ServerHandler const& handler) { + try { + for (;;) { + Res msg; + _Receive(pipe, &msg, sizeof(msg)); + handler(msg, [this, pipe](Msg resp) { _Send(pipe, resp); }); + } + } catch (...) { + _FinalizePipe(pipe); + } +} + +// weasel::Server + +Server::Server() : m_pImpl(new ServerImpl) {} + +Server::~Server() { + if (m_pImpl) + delete m_pImpl; +} + +HWND Server::Start() { + return m_pImpl->Start(); +} + +int Server::Stop() { + return m_pImpl->Stop(); +} + +int Server::Run() { + return m_pImpl->Run(); +} + +void Server::SetRequestHandler(RequestHandler* pHandler) { + m_pImpl->SetRequestHandler(pHandler); +} + +void Server::AddMenuHandler(UINT uID, CommandHandler handler) { + m_pImpl->AddMenuHandler(uID, handler); +} + +HWND Server::GetHWnd() { + return m_pImpl->m_hWnd; +} diff --git a/WeaselIPCServer/WeaselServerImpl.h b/WeaselIPCServer/WeaselServerImpl.h index 5a2e3ab07..005617fb3 100644 --- a/WeaselIPCServer/WeaselServerImpl.h +++ b/WeaselIPCServer/WeaselServerImpl.h @@ -1,103 +1,119 @@ -#pragma once -#include -#include -#include // for security attributes constants -#include // for ACL -#include -#include - -#include "SecurityAttribute.h" - -namespace weasel { -class PipeServer; - -typedef CWinTraits ServerWinTraits; - -class ServerImpl : public CWindowImpl -// class ServerImpl -{ - public: - DECLARE_WND_CLASS(WEASEL_IPC_WINDOW) - - BEGIN_MSG_MAP(WEASEL_IPC_WINDOW) - MESSAGE_HANDLER(WM_CREATE, OnCreate) - MESSAGE_HANDLER(WM_DESTROY, OnDestroy) - MESSAGE_HANDLER(WM_CLOSE, OnClose) - MESSAGE_HANDLER(WM_QUERYENDSESSION, OnQueryEndSystemSession) - MESSAGE_HANDLER(WM_ENDSESSION, OnEndSystemSession) - MESSAGE_HANDLER(WM_DWMCOLORIZATIONCOLORCHANGED, OnColorChange) - MESSAGE_HANDLER(WM_SETTINGCHANGE, OnColorChange) - MESSAGE_HANDLER(WM_COMMAND, OnCommand) - END_MSG_MAP() - - LRESULT OnColorChange(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled); - LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); - LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); - LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); - LRESULT OnQueryEndSystemSession(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled); - LRESULT OnEndSystemSession(UINT uMsg, - WPARAM wParam, - LPARAM lParam, - BOOL& bHandled); - LRESULT OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); - DWORD OnCommand(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnEcho(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnStartSession(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnEndSession(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnKeyEvent(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnShutdownServer(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnFocusIn(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnFocusOut(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnUpdateInputPosition(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam); - DWORD OnStartMaintenance(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnEndMaintenance(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnCommitComposition(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam); - DWORD OnClearComposition(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - DWORD OnSelectCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam); - DWORD OnHighlightCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, - DWORD wParam, - DWORD lParam); - DWORD OnChangePage(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); - - public: - ServerImpl(); - ~ServerImpl(); - - HWND Start(); - int Stop(); - int Run(); - - void SetRequestHandler(RequestHandler* pHandler) { - m_pRequestHandler = pHandler; - } - void AddMenuHandler(UINT uID, CommandHandler& handler) { - m_MenuHandlers[uID] = handler; - } - - private: - void _Finailize(); - template - void HandlePipeMessage(PipeMessage pipe_msg, _Resp resp); - - std::unique_ptr channel; - std::unique_ptr pipeThread; - RequestHandler* m_pRequestHandler; // reference - std::map m_MenuHandlers; - HMODULE m_hUser32Module; - SecurityAttribute sa; - BOOL m_darkMode; -}; - -} // namespace weasel +#pragma once +#include +#include +#include // for security attributes constants +#include // for ACL +#include +#include + +#include "SecurityAttribute.h" + +#define WM_WEASEL_SERVICE_NOTIFY (WEASEL_IPC_LAST_COMMAND + 200) + +namespace weasel { +class PipeServer; + +typedef CWinTraits ServerWinTraits; + +class ServerImpl : public CWindowImpl +// class ServerImpl +{ + public: + DECLARE_WND_CLASS(WEASEL_IPC_WINDOW) + + BEGIN_MSG_MAP(WEASEL_IPC_WINDOW) + MESSAGE_HANDLER(WM_CREATE, OnCreate) + MESSAGE_HANDLER(WM_DESTROY, OnDestroy) + MESSAGE_HANDLER(WM_CLOSE, OnClose) + MESSAGE_HANDLER(WM_QUERYENDSESSION, OnQueryEndSystemSession) + MESSAGE_HANDLER(WM_ENDSESSION, OnEndSystemSession) + MESSAGE_HANDLER(WM_DWMCOLORIZATIONCOLORCHANGED, OnColorChange) + MESSAGE_HANDLER(WM_SETTINGCHANGE, OnColorChange) + MESSAGE_HANDLER(WM_COMMAND, OnCommand) + MESSAGE_HANDLER(WM_WEASEL_SERVICE_NOTIFY, OnServiceNotifyMessage) + END_MSG_MAP() + + LRESULT OnColorChange(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled); + LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnQueryEndSystemSession(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled); + LRESULT OnEndSystemSession(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled); + LRESULT OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnServiceNotifyMessage(UINT uMsg, + WPARAM wParam, + LPARAM lParam, + BOOL& bHandled); + DWORD OnCommand(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnEcho(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnStartSession(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnEndSession(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnKeyEvent(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnKeyEventWithStatus(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnShutdownServer(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnServiceNotification(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnFocusIn(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnFocusOut(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnUpdateInputPosition(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnStartMaintenance(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnEndMaintenance(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnCommitComposition(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnClearComposition(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + DWORD OnSelectCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnHighlightCandidateOnCurrentPage(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam); + DWORD OnChangePage(WEASEL_IPC_COMMAND uMsg, DWORD wParam, DWORD lParam); + + public: + ServerImpl(); + ~ServerImpl(); + + HWND Start(); + int Stop(); + int Run(); + static bool PipeMessageNeedsOuterApiLock(PipeMessage pipe_msg); + + void SetRequestHandler(RequestHandler* pHandler) { + m_pRequestHandler = pHandler; + } + void AddMenuHandler(UINT uID, CommandHandler& handler) { + m_MenuHandlers[uID] = handler; + } + + private: + void _Finailize(); + void PostServiceNotification(DWORD notification); + DWORD DispatchPipeMessage(PipeMessage pipe_msg); + template + void HandlePipeMessage(PipeMessage pipe_msg, _Resp resp); + + std::unique_ptr channel; + std::unique_ptr pipeThread; + RequestHandler* m_pRequestHandler; // reference + std::map m_MenuHandlers; + HMODULE m_hUser32Module; + SecurityAttribute sa; + BOOL m_darkMode; +}; + +} // namespace weasel diff --git a/WeaselServer/WeaselServer.cpp b/WeaselServer/WeaselServer.cpp index b4cf8b2f1..9c79c5b09 100644 --- a/WeaselServer/WeaselServer.cpp +++ b/WeaselServer/WeaselServer.cpp @@ -1,130 +1,317 @@ -// WeaselServer.cpp : main source file for WeaselServer.exe -// -// WTL MessageLoop 封装了消息循环. 实现了 getmessage/dispatchmessage.... - -#include "stdafx.h" -#include "resource.h" -#include "WeaselService.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#pragma comment(lib, "Shcore.lib") -CAppModule _Module; - -int WINAPI _tWinMain(HINSTANCE hInstance, - HINSTANCE /*hPrevInstance*/, - LPTSTR lpstrCmdLine, - int nCmdShow) { - LANGID langId = get_language_id(); - SetThreadUILanguage(langId); - SetThreadLocale(langId); - - if (!IsWindowsBlueOrLaterEx()) { - CString info, cap; - info.LoadStringW(IDS_STR_SYSTEM_VERSION_WARNING); - cap.LoadStringW(IDS_STR_SYSTEM_VERSION_WARNING_CAPTION); - MessageBoxExW(NULL, info, cap, MB_ICONERROR, langId); - return 0; - } - SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); - - // 防止服务进程开启输入法 - ImmDisableIME(-1); - - WCHAR user_name[20] = {0}; - DWORD size = _countof(user_name); - GetUserName(user_name, &size); - if (!_wcsicmp(user_name, L"SYSTEM")) { - return 1; - } - - HRESULT hRes = ::CoInitialize(NULL); - // If you are running on NT 4.0 or higher you can use the following call - // instead to make the EXE free threaded. This means that calls come in on a - // random RPC thread. - // HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); - ATLASSERT(SUCCEEDED(hRes)); - - // this resolves ATL window thunking problem when Microsoft Layer for Unicode - // (MSLU) is used - ::DefWindowProc(NULL, 0, 0, 0L); - - AtlInitCommonControls( - ICC_BAR_CLASSES); // add flags to support other controls - - hRes = _Module.Init(NULL, hInstance); - ATLASSERT(SUCCEEDED(hRes)); - - if (!wcscmp(L"/userdir", lpstrCmdLine)) { - CreateDirectory(WeaselUserDataPath().c_str(), NULL); - WeaselServerApp::explore(WeaselUserDataPath()); - return 0; - } - if (!wcscmp(L"/weaseldir", lpstrCmdLine)) { - WeaselServerApp::explore(WeaselServerApp::install_dir()); - return 0; - } - if (!wcscmp(L"/ascii", lpstrCmdLine) || !wcscmp(L"/nascii", lpstrCmdLine)) { - weasel::Client client; - bool ascii = !wcscmp(L"/ascii", lpstrCmdLine); - if (client.Connect()) // try to connect to running server - { - if (ascii) - client.TrayCommand(ID_WEASELTRAY_ENABLE_ASCII); - else - client.TrayCommand(ID_WEASELTRAY_DISABLE_ASCII); - } - return 0; - } - - // command line option /q stops the running server - bool quit = !wcscmp(L"/q", lpstrCmdLine) || !wcscmp(L"/quit", lpstrCmdLine); - // restart if already running - { - weasel::Client client; - if (client.Connect()) // try to connect to running server - { - client.ShutdownServer(); - if (quit) - return 0; - int retry = 0; - while (client.Connect() && retry < 10) { - client.ShutdownServer(); - retry++; - Sleep(50); - } - if (retry >= 10) - return 0; - } else if (quit) - return 0; - } - - bool check_updates = !wcscmp(L"/update", lpstrCmdLine); - if (check_updates) { - WeaselServerApp::check_update(); - } - - CreateDirectory(WeaselUserDataPath().c_str(), NULL); - - int nRet = 0; - try { - WeaselServerApp app; - RegisterApplicationRestart(NULL, 0); - nRet = app.Run(); - } catch (...) { - // bad luck... - nRet = -1; - } - - _Module.Term(); - ::CoUninitialize(); - - return nRet; -} +// WeaselServer.cpp : main source file for WeaselServer.exe +// +// WTL MessageLoop 封装了消息循环. 实现了 getmessage/dispatchmessage.... + +#include "stdafx.h" +#include "resource.h" +#include "WeaselService.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#pragma comment(lib, "Shcore.lib") +CAppModule _Module; + +struct ParentProcessInfo { + DWORD pid = 0; + std::wstring name; + std::wstring path; + + std::wstring Description() const { + std::wstring description = L"parent_pid=" + std::to_wstring(pid); + if (!name.empty()) + description += L" parent_name=" + name; + if (!path.empty()) + description += L" parent_path=" + path; + return description; + } +}; + +static DWORD GetParentProcessId(DWORD pid, + std::wstring* parent_name = nullptr) { + DWORD parent_pid = 0; + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) + return 0; + + PROCESSENTRY32 entry = {0}; + entry.dwSize = sizeof(entry); + if (Process32First(snapshot, &entry)) { + do { + if (entry.th32ProcessID == pid) { + parent_pid = entry.th32ParentProcessID; + break; + } + } while (Process32Next(snapshot, &entry)); + } + if (parent_pid && parent_name && Process32First(snapshot, &entry)) { + do { + if (entry.th32ProcessID == parent_pid) { + *parent_name = entry.szExeFile; + break; + } + } while (Process32Next(snapshot, &entry)); + } + CloseHandle(snapshot); + return parent_pid; +} + +static std::wstring GetProcessImagePath(DWORD pid) { + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!process) + return L""; + + WCHAR path[MAX_PATH] = {0}; + DWORD length = _countof(path); + std::wstring result; + if (QueryFullProcessImageNameW(process, 0, path, &length)) + result = path; + CloseHandle(process); + return result; +} + +static ParentProcessInfo CurrentParentProcessInfo() { + ParentProcessInfo info; + info.pid = GetParentProcessId(GetCurrentProcessId(), &info.name); + if (info.pid) + info.path = GetProcessImagePath(info.pid); + return info; +} + +static bool TryConnectService(weasel::Client& client, bool wait_for_control) { + int attempts = wait_for_control ? 20 : 1; + for (int retry = 0; retry < attempts; ++retry) { + if (client.TryConnect()) + return true; + if (!wait_for_control) + break; + if (!weasel::IsServiceInstanceMutexPresent() && retry > 0) + break; + Sleep(100); + } + return false; +} + +int WINAPI _tWinMain(HINSTANCE hInstance, + HINSTANCE /*hPrevInstance*/, + LPTSTR lpstrCmdLine, + int nCmdShow) { + LANGID langId = get_language_id(); + SetThreadUILanguage(langId); + SetThreadLocale(langId); + ParentProcessInfo parent_process = CurrentParentProcessInfo(); + WeaselDebugLog(L"WeaselServer", L"entry cmdline=" + + std::wstring(lpstrCmdLine) + L" " + + parent_process.Description()); + + if (!IsWindowsBlueOrLaterEx()) { + WeaselDebugLog(L"WeaselServer", L"exit: unsupported Windows version"); + CString info, cap; + info.LoadStringW(IDS_STR_SYSTEM_VERSION_WARNING); + cap.LoadStringW(IDS_STR_SYSTEM_VERSION_WARNING_CAPTION); + MessageBoxExW(NULL, info, cap, MB_ICONERROR, langId); + return 0; + } + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + + // 防止服务进程开启输入法 + ImmDisableIME(-1); + + WCHAR user_name[20] = {0}; + DWORD size = _countof(user_name); + GetUserName(user_name, &size); + if (!_wcsicmp(user_name, L"SYSTEM")) { + WeaselDebugLog(L"WeaselServer", L"exit: running as SYSTEM"); + return 1; + } + + HRESULT hRes = ::CoInitialize(NULL); + // If you are running on NT 4.0 or higher you can use the following call + // instead to make the EXE free threaded. This means that calls come in on a + // random RPC thread. + // HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); + ATLASSERT(SUCCEEDED(hRes)); + + // this resolves ATL window thunking problem when Microsoft Layer for Unicode + // (MSLU) is used + ::DefWindowProc(NULL, 0, 0, 0L); + + AtlInitCommonControls( + ICC_BAR_CLASSES); // add flags to support other controls + + hRes = _Module.Init(NULL, hInstance); + ATLASSERT(SUCCEEDED(hRes)); + + if (!wcscmp(L"/userdir", lpstrCmdLine)) { + WeaselDebugLog(L"WeaselServer", L"command /userdir"); + CreateDirectory(WeaselUserDataPath().c_str(), NULL); + WeaselServerApp::explore(WeaselUserDataPath()); + return 0; + } + if (!wcscmp(L"/weaseldir", lpstrCmdLine)) { + WeaselDebugLog(L"WeaselServer", L"command /weaseldir"); + WeaselServerApp::explore(WeaselServerApp::install_dir()); + return 0; + } + if (!wcscmp(L"/ascii", lpstrCmdLine) || !wcscmp(L"/nascii", lpstrCmdLine)) { + weasel::Client client; + bool ascii = !wcscmp(L"/ascii", lpstrCmdLine); + bool connected = TryConnectService(client, false); + WeaselDebugLog(L"WeaselServer", + L"command ascii connected=" + std::to_wstring(connected)); + if (connected) // try to connect to running server + { + if (ascii) + client.TrayCommand(ID_WEASELTRAY_ENABLE_ASCII); + else + client.TrayCommand(ID_WEASELTRAY_DISABLE_ASCII); + } + return 0; + } + + // command line option /q stops the running server + bool quit = weasel::IsServiceQuitCommandLine(lpstrCmdLine); + bool stop_requested = weasel::IsServiceStopCommandLine(lpstrCmdLine); + bool restart_requested = weasel::IsServiceRestartCommandLine(lpstrCmdLine); + bool recovery_requested = weasel::IsServiceRecoveryCommandLine(lpstrCmdLine); + bool startup_requested = weasel::IsServiceStartupCommandLine(lpstrCmdLine); + bool implicit_start = weasel::IsImplicitServiceStartCommandLine(lpstrCmdLine); + bool check_updates = weasel::IsServiceUpdateCommandLine(lpstrCmdLine); + bool legacy_restart_from_interactive_parent = + weasel::ShouldTreatLegacyRestartAsManual( + lpstrCmdLine, parent_process.name, parent_process.path); + bool restarted_existing_server = false; + if (quit) { + weasel::MarkServiceManualExit(); + } else if (legacy_restart_from_interactive_parent || + weasel::ShouldClearServiceManualExit(lpstrCmdLine)) { + weasel::ClearServiceManualExit(); + } + WeaselDebugLog( + L"WeaselServer", + L"service command quit=" + std::to_wstring(quit) + L" stop_requested=" + + std::to_wstring(stop_requested) + L" restart_requested=" + + std::to_wstring(restart_requested) + L" recovery_requested=" + + std::to_wstring(recovery_requested) + L" startup_requested=" + + std::to_wstring(startup_requested) + L" implicit_start=" + + std::to_wstring(implicit_start) + L" check_updates=" + + std::to_wstring(check_updates) + L" legacy_manual_restart=" + + std::to_wstring(legacy_restart_from_interactive_parent) + + L" manual_exit_marked=" + + std::to_wstring(weasel::IsServiceManualExitMarked())); + if (!quit && !legacy_restart_from_interactive_parent && + weasel::ShouldSuppressServiceStartAfterManualExit(lpstrCmdLine)) { + WeaselDebugLog( + L"WeaselServer", + L"exit: service start suppressed because manual exit is marked"); + return 0; + } + // restart if already running + { + weasel::Client client; + bool connected = client.TryConnect(); + WeaselDebugLog(L"WeaselServer", L"initial TryConnect connected=" + + std::to_wstring(connected)); + if (connected) // try to connect to running server + { + if (check_updates) { + WeaselDebugLog(L"WeaselServer", + L"forwarding update check to existing server"); + client.TrayCommand(ID_WEASELTRAY_CHECKUPDATE); + return 0; + } + if (!weasel::ShouldShutdownExistingService(lpstrCmdLine)) { + WeaselDebugLog( + L"WeaselServer", + L"exit: service already running and command does not request " + L"shutdown"); + return 0; + } + WeaselDebugLog(L"WeaselServer", L"sending shutdown to existing server"); + DWORD shutdown_reason = weasel::WEASEL_IPC_SHUTDOWN_REASON_RESTART; + if (quit) + shutdown_reason = weasel::WEASEL_IPC_SHUTDOWN_REASON_EXIT; + else if (stop_requested) + shutdown_reason = weasel::WEASEL_IPC_SHUTDOWN_REASON_STOP; + client.ShutdownServer(shutdown_reason); + if (quit || stop_requested) + return 0; + restarted_existing_server = true; + client.Disconnect(); + int retry = 0; + bool still_connected = true; + bool mutex_present = true; + for (; retry < 50; retry++) { + Sleep(100); + weasel::Client probe; + still_connected = probe.TryConnect(); + mutex_present = weasel::IsServiceInstanceMutexPresent(); + WeaselDebugLog(L"WeaselServer", + L"restart wait retry=" + std::to_wstring(retry) + + L" still_connected=" + + std::to_wstring(still_connected) + + L" mutex_present=" + std::to_wstring(mutex_present)); + if (!still_connected && !mutex_present) + break; + if (still_connected && retry == 24) + probe.ShutdownServer(weasel::WEASEL_IPC_SHUTDOWN_REASON_RESTART); + } + if (retry >= 50) { + weasel::Client probe; + if (probe.TryConnect()) + probe.NotifyService( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE); + WeaselDebugLog(L"WeaselServer", + L"exit: existing server did not stop in time"); + return 0; + } + } else if (quit || stop_requested) { + WeaselDebugLog(L"WeaselServer", L"exit: stop requested but no server"); + return 0; + } + } + + if (recovery_requested && weasel::IsServiceManualExitMarked()) { + WeaselDebugLog( + L"WeaselServer", + L"exit: recovery suppressed by manual exit before app start"); + return 0; + } + + if (check_updates) { + WeaselDebugLog(L"WeaselServer", L"command /update"); + WeaselServerApp::check_update(); + } + + CreateDirectory(WeaselUserDataPath().c_str(), NULL); + + int nRet = 0; + try { + WeaselDebugLog(L"WeaselServer", + L"starting app startup_notification=" + + std::to_wstring(weasel::ServiceStartupNotification( + restart_requested, restarted_existing_server))); + WeaselServerApp app(weasel::ServiceStartupNotification( + restart_requested, restarted_existing_server)); + RegisterApplicationRestart(weasel::ServiceRecoveryArgument(), 0); + nRet = app.Run(); + WeaselDebugLog(L"WeaselServer", + L"app.Run returned " + std::to_wstring(nRet)); + } catch (...) { + // bad luck... + WeaselDebugLog(L"WeaselServer", L"exception while running app"); + nRet = -1; + } + + _Module.Term(); + ::CoUninitialize(); + + return nRet; +} diff --git a/WeaselServer/WeaselServer.rc b/WeaselServer/WeaselServer.rc index 53fcadd45..0d90051eb 100644 Binary files a/WeaselServer/WeaselServer.rc and b/WeaselServer/WeaselServer.rc differ diff --git a/WeaselServer/WeaselServerApp.cpp b/WeaselServer/WeaselServerApp.cpp index e09c4c5ed..9f0dedef9 100644 --- a/WeaselServer/WeaselServerApp.cpp +++ b/WeaselServer/WeaselServerApp.cpp @@ -1,10 +1,13 @@ #include "stdafx.h" #include "WeaselServerApp.h" +#include #include +#include -WeaselServerApp::WeaselServerApp() +WeaselServerApp::WeaselServerApp(DWORD startup_notification) : m_handler(std::make_unique(&m_ui)), - tray_icon(m_ui) { + tray_icon(m_ui), + m_startup_notification(startup_notification) { // m_handler.reset(new RimeWithWeaselHandler(&m_ui)); m_server.SetRequestHandler(m_handler.get()); SetupMenuHandlers(); @@ -13,8 +16,14 @@ WeaselServerApp::WeaselServerApp() WeaselServerApp::~WeaselServerApp() {} int WeaselServerApp::Run() { - if (!m_server.Start()) + WeaselDebugLog(L"WeaselServerApp", + L"Run start startup_notification=" + + std::to_wstring(m_startup_notification)); + if (!m_server.Start()) { + WeaselDebugLog(L"WeaselServerApp", L"m_server.Start failed"); return -1; + } + WeaselDebugLog(L"WeaselServerApp", L"m_server.Start ok"); // win_sparkle_set_appcast_url("http://localhost:8000/weasel/update/appcast.xml"); win_sparkle_set_registry_path("Software\\Rime\\Weasel\\Updates"); @@ -33,9 +42,16 @@ int WeaselServerApp::Run() { m_handler->OnUpdateUI([this]() { tray_icon.Refresh(); }); tray_icon.Create(m_server.GetHWnd()); + m_handler->OnServiceNotification([this](DWORD notification) { + tray_icon.ShowServiceNotification(notification); + }); tray_icon.Refresh(); + tray_icon.ShowServiceNotification(m_startup_notification); + WeaselDebugLog(L"WeaselServerApp", L"tray initialized"); int ret = m_server.Run(); + WeaselDebugLog(L"WeaselServerApp", + L"m_server.Run returned " + std::to_wstring(ret)); m_handler->Finalize(); m_ui.Destroy(); @@ -47,8 +63,18 @@ int WeaselServerApp::Run() { void WeaselServerApp::SetupMenuHandlers() { std::filesystem::path dir = install_dir(); - m_server.AddMenuHandler(ID_WEASELTRAY_QUIT, - [this] { return m_server.Stop() == 0; }); + m_server.AddMenuHandler(ID_WEASELTRAY_QUIT, [this] { + WeaselDebugLog(L"WeaselServerApp", L"tray quit requested"); + weasel::MarkServiceManualExit(); + UnregisterApplicationRestart(); + tray_icon.ShowServiceNotification( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_EXITING); + std::thread([this] { + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + m_server.Stop(); + }).detach(); + return true; + }); m_server.AddMenuHandler(ID_WEASELTRAY_DEPLOY, std::bind(execute, dir / L"WeaselDeployer.exe", std::wstring(L"/deploy"))); diff --git a/WeaselServer/WeaselServerApp.h b/WeaselServer/WeaselServerApp.h index d5e32e006..02c64a045 100644 --- a/WeaselServer/WeaselServerApp.h +++ b/WeaselServer/WeaselServerApp.h @@ -56,7 +56,8 @@ class WeaselServerApp { } public: - WeaselServerApp(); + explicit WeaselServerApp(DWORD startup_notification = + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_NONE); ~WeaselServerApp(); int Run(); @@ -67,4 +68,5 @@ class WeaselServerApp { weasel::UI m_ui; WeaselTrayIcon tray_icon; std::unique_ptr m_handler; + DWORD m_startup_notification; }; diff --git a/WeaselServer/WeaselService.cpp b/WeaselServer/WeaselService.cpp index 1545a45d8..48c77637d 100644 --- a/WeaselServer/WeaselService.cpp +++ b/WeaselServer/WeaselService.cpp @@ -66,7 +66,7 @@ void WeaselService::Start(DWORD dwArgc = 0, PWSTR* pszArgv = NULL) { // Perform service-specific initialization. // if (IsWindowsVistaOrGreater()) - { RegisterApplicationRestart(NULL, 0); } + { RegisterApplicationRestart(weasel::ServiceRecoveryArgument(), 0); } boost::thread{[this] { app.Run(); }}; // Tell SCM that the service is started. SetServiceStatus(SERVICE_RUNNING); @@ -86,11 +86,11 @@ void WeaselService::Stop() { // Tell SCM that the service is stopping. SetServiceStatus(SERVICE_STOP_PENDING); - // Perform service-specific stop operations. + // SCM stop is not a user tray quit; do not mark manual-exit here. weasel::Client client; - if (client.Connect()) // try to connect to running server + if (client.TryConnect()) // try to connect to running server { - client.ShutdownServer(); + client.ShutdownServer(weasel::WEASEL_IPC_SHUTDOWN_REASON_STOP); } // Tell SCM that the service is stopped. diff --git a/WeaselServer/WeaselTrayIcon.cpp b/WeaselServer/WeaselTrayIcon.cpp index 119111aff..7df3d0b2a 100644 --- a/WeaselServer/WeaselTrayIcon.cpp +++ b/WeaselServer/WeaselTrayIcon.cpp @@ -91,3 +91,23 @@ void WeaselTrayIcon::Refresh() { ShowIcon(); } } + +void WeaselTrayIcon::ShowMaintenanceResult(DWORD result) { + ShowServiceNotification(result); +} + +void WeaselTrayIcon::ShowServiceNotification(DWORD notification) { + UINT string_id = ServiceNotificationStringId(notification); + if (!string_id) + return; + + CString info; + if (!info.LoadStringW(string_id)) + return; + + if (!Visible()) + ShowIcon(); + + ShowBalloon(info, get_weasel_ime_name().c_str(), + ServiceNotificationBalloonIcon(notification)); +} diff --git a/WeaselServer/WeaselTrayIcon.h b/WeaselServer/WeaselTrayIcon.h index 42d407aaa..750b55ab5 100644 --- a/WeaselServer/WeaselTrayIcon.h +++ b/WeaselServer/WeaselTrayIcon.h @@ -2,9 +2,42 @@ #include #include #include "SystemTraySDK.h" +#include "resource.h" #define WM_WEASEL_TRAY_NOTIFY (WEASEL_IPC_LAST_COMMAND + 100) +inline UINT ServiceNotificationStringId(DWORD notification) { + if (notification == weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS) + return IDS_STR_DEPLOY_SUCCESS; + if (notification == weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE) + return IDS_STR_DEPLOY_FAILURE; + if (notification == weasel::WEASEL_IPC_SERVICE_NOTIFICATION_EXITING) + return IDS_STR_SERVICE_EXITING; + if (notification == weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTARTING) + return IDS_STR_SERVICE_RESTARTING; + if (notification == weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS) + return IDS_STR_SERVICE_RESTART_SUCCESS; + if (notification == weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE) + return IDS_STR_SERVICE_RESTART_FAILURE; + return 0; +} + +inline DWORD ServiceNotificationBalloonIcon(DWORD notification) { + return notification == weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE || + notification == + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE + ? NIIF_ERROR + : NIIF_INFO; +} + +inline UINT MaintenanceDeployResultStringId(DWORD result) { + return ServiceNotificationStringId(result); +} + +inline DWORD MaintenanceDeployResultBalloonIcon(DWORD result) { + return ServiceNotificationBalloonIcon(result); +} + class WeaselTrayIcon : public CSystemTray { public: enum WeaselTrayMode { @@ -18,6 +51,8 @@ class WeaselTrayIcon : public CSystemTray { BOOL Create(HWND hTargetWnd); void Refresh(); + void ShowMaintenanceResult(DWORD result); + void ShowServiceNotification(DWORD notification); protected: virtual void CustomizeMenu(HMENU hMenu); diff --git a/WeaselServer/resource.h b/WeaselServer/resource.h index c6655b8c8..576aa9451 100644 --- a/WeaselServer/resource.h +++ b/WeaselServer/resource.h @@ -9,10 +9,16 @@ #define IDR_MENU_POPUP 105 #define IDI_FULL_SHAPE 106 #define IDI_HALF_SHAPE 107 -#define IDS_STR_SYSTEM_VERSION_WARNING_CAPTION 300 -#define IDS_STR_SYSTEM_VERSION_WARNING 301 -#define IDS_STR_UNDER_MAINTENANCE 302 -#define ID_WEASELTRAY_QUIT 40001 +#define IDS_STR_SYSTEM_VERSION_WARNING_CAPTION 300 +#define IDS_STR_SYSTEM_VERSION_WARNING 301 +#define IDS_STR_UNDER_MAINTENANCE 302 +#define IDS_STR_DEPLOY_SUCCESS 303 +#define IDS_STR_DEPLOY_FAILURE 304 +#define IDS_STR_SERVICE_EXITING 305 +#define IDS_STR_SERVICE_RESTARTING 306 +#define IDS_STR_SERVICE_RESTART_SUCCESS 307 +#define IDS_STR_SERVICE_RESTART_FAILURE 308 +#define ID_WEASELTRAY_QUIT 40001 #define ID_WEASELTRAY_DEPLOY 40002 #define ID_WEASELTRAY_CHECKUPDATE 40003 #define ID_WEASELTRAY_FORUM 40004 diff --git a/WeaselSetup/WeaselSetup.cpp b/WeaselSetup/WeaselSetup.cpp index ccaf86f3d..7f9537509 100644 --- a/WeaselSetup/WeaselSetup.cpp +++ b/WeaselSetup/WeaselSetup.cpp @@ -1,283 +1,292 @@ -// WeaselSetup.cpp : main source file for WeaselSetup.exe -// - -#include "stdafx.h" - -#include "resource.h" -#include "WeaselUtility.h" -#include - -#include "InstallOptionsDlg.h" - -#include -#pragma comment(lib, "Shcore.lib") -CAppModule _Module; - -static int Run(LPTSTR lpCmdLine); -static bool IsProcAdmin(); -static int RestartAsAdmin(LPTSTR lpCmdLine); - -int WINAPI _tWinMain(HINSTANCE hInstance, - HINSTANCE /*hPrevInstance*/, - LPTSTR lpstrCmdLine, - int /*nCmdShow*/) { - HRESULT hRes = ::CoInitialize(NULL); - ATLASSERT(SUCCEEDED(hRes)); - - AtlInitCommonControls( - ICC_BAR_CLASSES); // add flags to support other controls - - hRes = _Module.Init(NULL, hInstance); - ATLASSERT(SUCCEEDED(hRes)); - - LANGID langId = get_language_id(); - SetThreadUILanguage(langId); - SetThreadLocale(langId); - - int nRet = Run(lpstrCmdLine); - - _Module.Term(); - ::CoUninitialize(); - - return nRet; -} -int install(bool hant, bool silent); -int uninstall(bool silent); -bool has_installed(); - -static std::wstring install_dir() { - WCHAR exe_path[MAX_PATH] = {0}; - GetModuleFileNameW(GetModuleHandle(NULL), exe_path, _countof(exe_path)); - std::wstring dir(exe_path); - size_t pos = dir.find_last_of(L"\\"); - dir.resize(pos); - return dir; -} - -static int CustomInstall(bool installing) { - bool hant = false; - bool silent = false; - std::wstring user_dir; - - const WCHAR KEY[] = L"Software\\Rime\\Weasel"; - HKEY hKey; - LSTATUS ret = RegOpenKey(HKEY_CURRENT_USER, KEY, &hKey); - if (ret == ERROR_SUCCESS) { - WCHAR value[MAX_PATH]; - DWORD len = sizeof(value); - DWORD type = 0; - DWORD data = 0; - ret = - RegQueryValueEx(hKey, L"RimeUserDir", NULL, &type, (LPBYTE)value, &len); - if (ret == ERROR_SUCCESS && type == REG_SZ) { - user_dir = value; - } - len = sizeof(data); - ret = RegQueryValueEx(hKey, L"Hant", NULL, &type, (LPBYTE)&data, &len); - if (ret == ERROR_SUCCESS && type == REG_DWORD) { - hant = (data != 0); - if (installing) - silent = true; - } - RegCloseKey(hKey); - } - bool _has_installed = has_installed(); - if (!silent) { - InstallOptionsDialog dlg; - dlg.installed = _has_installed; - dlg.hant = hant; - dlg.user_dir = user_dir; - if (IDOK != dlg.DoModal()) { - if (!installing) - return 1; // aborted by user - } else { - hant = dlg.hant; - user_dir = dlg.user_dir; - _has_installed = dlg.installed; - } - } - if (!_has_installed) - if (0 != install(hant, silent)) - return 1; - - if (user_dir.empty()) { - // default user dir %APPDATA%\Rime - WCHAR _path[MAX_PATH] = {0}; - ExpandEnvironmentStringsW(L"%APPDATA%\\Rime", _path, _countof(_path)); - user_dir = std::wstring(_path); - } - ret = SetRegKeyValue(HKEY_CURRENT_USER, KEY, L"RimeUserDir", user_dir.c_str(), - REG_SZ, false); - if (FAILED(HRESULT_FROM_WIN32(ret))) { - MSG_BY_IDS(IDS_STR_ERR_WRITE_USER_DIR, IDS_STR_INSTALL_FAILED, - MB_ICONERROR | MB_OK); - return 1; - } - ret = SetRegKeyValue(HKEY_CURRENT_USER, KEY, L"Hant", (hant ? 1 : 0), - REG_DWORD, false); - if (FAILED(HRESULT_FROM_WIN32(ret))) { - MSG_BY_IDS(IDS_STR_ERR_WRITE_HANT, IDS_STR_INSTALL_FAILED, - MB_ICONERROR | MB_OK); - return 1; - } - if (_has_installed) { - std::wstring dir(install_dir()); - std::thread th([dir]() { - ShellExecuteW(NULL, NULL, (dir + L"\\WeaselServer.exe").c_str(), L"/q", - NULL, SW_SHOWNORMAL); - Sleep(500); - ShellExecuteW(NULL, NULL, (dir + L"\\WeaselServer.exe").c_str(), L"", - NULL, SW_SHOWNORMAL); - Sleep(500); - ShellExecuteW(NULL, NULL, (dir + L"\\WeaselDeployer.exe").c_str(), - L"/deploy", NULL, SW_SHOWNORMAL); - }); - th.detach(); - MSG_BY_IDS(IDS_STR_MODIFY_SUCCESS_INFO, IDS_STR_MODIFY_SUCCESS_CAP, - MB_ICONINFORMATION | MB_OK); - } - - return 0; -} - -LPCTSTR GetParamByPrefix(LPCTSTR lpCmdLine, LPCTSTR prefix) { - return (wcsncmp(lpCmdLine, prefix, wcslen(prefix)) == 0) - ? (lpCmdLine + wcslen(prefix)) - : 0; -} - -static int Run(LPTSTR lpCmdLine) { - constexpr bool silent = true; - // parameter /? or /help to show commandline args - if (!wcscmp(L"/?", lpCmdLine) || !wcscmp(L"/help", lpCmdLine)) { - WCHAR msg[1024] = {0}; - if (LoadString(GetModuleHandle(NULL), IDS_STR_HELP, msg, - sizeof(msg) / sizeof(TCHAR))) { - MessageBox(NULL, msg, L"WeaselSetup", MB_ICONINFORMATION | MB_OK); - } else { - MessageBox( - NULL, - L"Usage: WeaselSetup.exe [options]\n" - L"/? or /help - Show this help message\n" - L"/u - Uninstall Weasel\n" - L"/i - Install Weasel\n" - L"/s - Install Weasel (Simplified Chinese)\n" - L"/t - Install Weasel (Traditional Chinese)\n" - L"/ls - Set Weasel language to Simplified Chinese\n" - L"/lt - Set Weasel language to Traditional Chinese\n" - L"/le - Set Weasel language to English\n" - L"/eu - Enable automatic update check\n" - L"/du - Disable automatic update check\n" - L"/toggleime - Toggle IME on open/close(ctrl+space)\n" - L"/toggleascii - Toggle ASCII on open/close(ctrl+space)\n" - L"/testing - Set update channel to testing\n" - L"/release - Set update channel to release\n" - L"/userdir: - Set user directory\n", - L"WeaselSetup", MB_ICONINFORMATION | MB_OK); - } - return 0; - } - bool uninstalling = !wcscmp(L"/u", lpCmdLine); - if (uninstalling) { - if (IsProcAdmin()) - return uninstall(silent); - else - return RestartAsAdmin(lpCmdLine); - } - - if (auto res = GetParamByPrefix(lpCmdLine, L"/userdir:")) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"RimeUserDir", res, REG_SZ); - } - - if (!wcscmp(L"/ls", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"Language", L"chs", REG_SZ); - } else if (!wcscmp(L"/lt", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"Language", L"cht", REG_SZ); - } else if (!wcscmp(L"/le", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"Language", L"eng", REG_SZ); - } - - if (!wcscmp(L"/eu", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel\\Updates", - L"CheckForUpdates", L"1", REG_SZ); - } - if (!wcscmp(L"/du", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel\\Updates", - L"CheckForUpdates", L"0", REG_SZ); - } - - if (!wcscmp(L"/toggleime", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"ToggleImeOnOpenClose", L"yes", REG_SZ); - } - if (!wcscmp(L"/toggleascii", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"ToggleImeOnOpenClose", L"no", REG_SZ); - } - if (!wcscmp(L"/testing", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"UpdateChannel", L"testing", REG_SZ); - } - if (!wcscmp(L"/release", lpCmdLine)) { - return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"UpdateChannel", L"release", REG_SZ); - } - - if (!IsProcAdmin()) { - return RestartAsAdmin(lpCmdLine); - } - - bool hans = !wcscmp(L"/s", lpCmdLine); - if (hans) - return install(false, silent); - bool hant = !wcscmp(L"/t", lpCmdLine); - if (hant) - return install(true, silent); - bool installing = !wcscmp(L"/i", lpCmdLine); - return CustomInstall(installing); -} - -// https://learn.microsoft.com/zh-cn/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership -bool IsProcAdmin() { - BOOL b = FALSE; - SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; - PSID AdministratorsGroup; - b = AllocateAndInitializeSid(&NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, - DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, - &AdministratorsGroup); - - if (b) { - if (!CheckTokenMembership(NULL, AdministratorsGroup, &b)) { - b = FALSE; - } - FreeSid(AdministratorsGroup); - } - - return (b); -} - -int RestartAsAdmin(LPTSTR lpCmdLine) { - SHELLEXECUTEINFO execInfo{0}; - TCHAR path[MAX_PATH]; - GetModuleFileName(GetModuleHandle(NULL), path, _countof(path)); - execInfo.lpFile = path; - execInfo.lpParameters = lpCmdLine; - execInfo.lpVerb = _T("runas"); - execInfo.cbSize = sizeof(execInfo); - execInfo.nShow = SW_SHOWNORMAL; - execInfo.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; - execInfo.hwnd = NULL; - execInfo.hProcess = NULL; - if (::ShellExecuteEx(&execInfo) && execInfo.hProcess != NULL) { - ::WaitForSingleObject(execInfo.hProcess, INFINITE); - DWORD dwExitCode = 0; - ::GetExitCodeProcess(execInfo.hProcess, &dwExitCode); - ::CloseHandle(execInfo.hProcess); - return dwExitCode; - } - return -1; -} +// WeaselSetup.cpp : main source file for WeaselSetup.exe +// + +#include "stdafx.h" + +#include "resource.h" +#include "WeaselIPC.h" +#include "WeaselUtility.h" +#include + +#include "InstallOptionsDlg.h" + +#include +#pragma comment(lib, "Shcore.lib") +CAppModule _Module; + +static int Run(LPTSTR lpCmdLine); +static bool IsProcAdmin(); +static int RestartAsAdmin(LPTSTR lpCmdLine); + +int WINAPI _tWinMain(HINSTANCE hInstance, + HINSTANCE /*hPrevInstance*/, + LPTSTR lpstrCmdLine, + int /*nCmdShow*/) { + HRESULT hRes = ::CoInitialize(NULL); + ATLASSERT(SUCCEEDED(hRes)); + + AtlInitCommonControls( + ICC_BAR_CLASSES); // add flags to support other controls + + hRes = _Module.Init(NULL, hInstance); + ATLASSERT(SUCCEEDED(hRes)); + + LANGID langId = get_language_id(); + SetThreadUILanguage(langId); + SetThreadLocale(langId); + + int nRet = Run(lpstrCmdLine); + + _Module.Term(); + ::CoUninitialize(); + + return nRet; +} +int install(bool hant, bool silent); +int uninstall(bool silent); +bool has_installed(); + +static std::wstring install_dir() { + WCHAR exe_path[MAX_PATH] = {0}; + GetModuleFileNameW(GetModuleHandle(NULL), exe_path, _countof(exe_path)); + std::wstring dir(exe_path); + size_t pos = dir.find_last_of(L"\\"); + dir.resize(pos); + return dir; +} + +static int CustomInstall(bool installing) { + bool hant = false; + bool silent = false; + std::wstring user_dir; + + const WCHAR KEY[] = L"Software\\Rime\\Weasel"; + HKEY hKey; + LSTATUS ret = RegOpenKey(HKEY_CURRENT_USER, KEY, &hKey); + if (ret == ERROR_SUCCESS) { + WCHAR value[MAX_PATH]; + DWORD len = sizeof(value); + DWORD type = 0; + DWORD data = 0; + ret = + RegQueryValueEx(hKey, L"RimeUserDir", NULL, &type, (LPBYTE)value, &len); + if (ret == ERROR_SUCCESS && type == REG_SZ) { + user_dir = value; + } + len = sizeof(data); + ret = RegQueryValueEx(hKey, L"Hant", NULL, &type, (LPBYTE)&data, &len); + if (ret == ERROR_SUCCESS && type == REG_DWORD) { + hant = (data != 0); + if (installing) + silent = true; + } + RegCloseKey(hKey); + } + bool _has_installed = has_installed(); + bool install_tsf = installing || !_has_installed; + if (!silent) { + InstallOptionsDialog dlg; + dlg.installed = _has_installed; + dlg.hant = hant; + dlg.user_dir = user_dir; + if (IDOK != dlg.DoModal()) { + if (!installing) + return 1; // aborted by user + } else { + hant = dlg.hant; + user_dir = dlg.user_dir; + _has_installed = dlg.installed; + install_tsf = installing || !_has_installed; + } + } + WeaselDebugLog(L"WeaselSetup", + L"CustomInstall installing=" + std::to_wstring(installing) + + L" has_installed=" + std::to_wstring(_has_installed) + + L" silent=" + std::to_wstring(silent) + L" install_tsf=" + + std::to_wstring(install_tsf)); + if (install_tsf) + if (0 != install(hant, silent)) + return 1; + + if (user_dir.empty()) { + // default user dir %APPDATA%\Rime + WCHAR _path[MAX_PATH] = {0}; + ExpandEnvironmentStringsW(L"%APPDATA%\\Rime", _path, _countof(_path)); + user_dir = std::wstring(_path); + } + ret = SetRegKeyValue(HKEY_CURRENT_USER, KEY, L"RimeUserDir", user_dir.c_str(), + REG_SZ, false); + if (FAILED(HRESULT_FROM_WIN32(ret))) { + MSG_BY_IDS(IDS_STR_ERR_WRITE_USER_DIR, IDS_STR_INSTALL_FAILED, + MB_ICONERROR | MB_OK); + return 1; + } + ret = SetRegKeyValue(HKEY_CURRENT_USER, KEY, L"Hant", (hant ? 1 : 0), + REG_DWORD, false); + if (FAILED(HRESULT_FROM_WIN32(ret))) { + MSG_BY_IDS(IDS_STR_ERR_WRITE_HANT, IDS_STR_INSTALL_FAILED, + MB_ICONERROR | MB_OK); + return 1; + } + if (_has_installed) { + std::wstring dir(install_dir()); + std::thread th([dir]() { + ShellExecuteW(NULL, NULL, (dir + L"\\WeaselServer.exe").c_str(), + weasel::ServiceStopArgument(), NULL, SW_SHOWNORMAL); + Sleep(500); + ShellExecuteW(NULL, NULL, (dir + L"\\WeaselServer.exe").c_str(), + weasel::ServiceManualRestartArgument(), NULL, + SW_SHOWNORMAL); + Sleep(500); + ShellExecuteW(NULL, NULL, (dir + L"\\WeaselDeployer.exe").c_str(), + L"/deploy", NULL, SW_SHOWNORMAL); + }); + th.detach(); + MSG_BY_IDS(IDS_STR_MODIFY_SUCCESS_INFO, IDS_STR_MODIFY_SUCCESS_CAP, + MB_ICONINFORMATION | MB_OK); + } + + return 0; +} + +LPCTSTR GetParamByPrefix(LPCTSTR lpCmdLine, LPCTSTR prefix) { + return (wcsncmp(lpCmdLine, prefix, wcslen(prefix)) == 0) + ? (lpCmdLine + wcslen(prefix)) + : 0; +} + +static int Run(LPTSTR lpCmdLine) { + constexpr bool silent = true; + // parameter /? or /help to show commandline args + if (!wcscmp(L"/?", lpCmdLine) || !wcscmp(L"/help", lpCmdLine)) { + WCHAR msg[1024] = {0}; + if (LoadString(GetModuleHandle(NULL), IDS_STR_HELP, msg, + sizeof(msg) / sizeof(TCHAR))) { + MessageBox(NULL, msg, L"WeaselSetup", MB_ICONINFORMATION | MB_OK); + } else { + MessageBox( + NULL, + L"Usage: WeaselSetup.exe [options]\n" + L"/? or /help - Show this help message\n" + L"/u - Uninstall Weasel\n" + L"/i - Install Weasel\n" + L"/s - Install Weasel (Simplified Chinese)\n" + L"/t - Install Weasel (Traditional Chinese)\n" + L"/ls - Set Weasel language to Simplified Chinese\n" + L"/lt - Set Weasel language to Traditional Chinese\n" + L"/le - Set Weasel language to English\n" + L"/eu - Enable automatic update check\n" + L"/du - Disable automatic update check\n" + L"/toggleime - Toggle IME on open/close(ctrl+space)\n" + L"/toggleascii - Toggle ASCII on open/close(ctrl+space)\n" + L"/testing - Set update channel to testing\n" + L"/release - Set update channel to release\n" + L"/userdir: - Set user directory\n", + L"WeaselSetup", MB_ICONINFORMATION | MB_OK); + } + return 0; + } + bool uninstalling = !wcscmp(L"/u", lpCmdLine); + if (uninstalling) { + if (IsProcAdmin()) + return uninstall(silent); + else + return RestartAsAdmin(lpCmdLine); + } + + if (auto res = GetParamByPrefix(lpCmdLine, L"/userdir:")) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"RimeUserDir", res, REG_SZ); + } + + if (!wcscmp(L"/ls", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"Language", L"chs", REG_SZ); + } else if (!wcscmp(L"/lt", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"Language", L"cht", REG_SZ); + } else if (!wcscmp(L"/le", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"Language", L"eng", REG_SZ); + } + + if (!wcscmp(L"/eu", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel\\Updates", + L"CheckForUpdates", L"1", REG_SZ); + } + if (!wcscmp(L"/du", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel\\Updates", + L"CheckForUpdates", L"0", REG_SZ); + } + + if (!wcscmp(L"/toggleime", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"ToggleImeOnOpenClose", L"yes", REG_SZ); + } + if (!wcscmp(L"/toggleascii", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"ToggleImeOnOpenClose", L"no", REG_SZ); + } + if (!wcscmp(L"/testing", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"UpdateChannel", L"testing", REG_SZ); + } + if (!wcscmp(L"/release", lpCmdLine)) { + return SetRegKeyValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"UpdateChannel", L"release", REG_SZ); + } + + if (!IsProcAdmin()) { + return RestartAsAdmin(lpCmdLine); + } + + bool hans = !wcscmp(L"/s", lpCmdLine); + if (hans) + return install(false, silent); + bool hant = !wcscmp(L"/t", lpCmdLine); + if (hant) + return install(true, silent); + bool installing = !wcscmp(L"/i", lpCmdLine); + return CustomInstall(installing); +} + +// https://learn.microsoft.com/zh-cn/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership +bool IsProcAdmin() { + BOOL b = FALSE; + SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; + PSID AdministratorsGroup; + b = AllocateAndInitializeSid(&NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, + &AdministratorsGroup); + + if (b) { + if (!CheckTokenMembership(NULL, AdministratorsGroup, &b)) { + b = FALSE; + } + FreeSid(AdministratorsGroup); + } + + return (b); +} + +int RestartAsAdmin(LPTSTR lpCmdLine) { + SHELLEXECUTEINFO execInfo{0}; + TCHAR path[MAX_PATH]; + GetModuleFileName(GetModuleHandle(NULL), path, _countof(path)); + execInfo.lpFile = path; + execInfo.lpParameters = lpCmdLine; + execInfo.lpVerb = _T("runas"); + execInfo.cbSize = sizeof(execInfo); + execInfo.nShow = SW_SHOWNORMAL; + execInfo.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; + execInfo.hwnd = NULL; + execInfo.hProcess = NULL; + if (::ShellExecuteEx(&execInfo) && execInfo.hProcess != NULL) { + ::WaitForSingleObject(execInfo.hProcess, INFINITE); + DWORD dwExitCode = 0; + ::GetExitCodeProcess(execInfo.hProcess, &dwExitCode); + ::CloseHandle(execInfo.hProcess); + return dwExitCode; + } + return -1; +} diff --git a/WeaselSetup/imesetup.cpp b/WeaselSetup/imesetup.cpp index 0880e7ff0..82905ddff 100644 --- a/WeaselSetup/imesetup.cpp +++ b/WeaselSetup/imesetup.cpp @@ -1,494 +1,520 @@ -#include "stdafx.h" -#include -#include -#include -#include -#include -#include -#include -#include "InstallOptionsDlg.h" - -// {A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A} -static const GUID c_clsidTextService = { - 0xa3f4cded, - 0xb1e9, - 0x41ee, - {0x9c, 0xa6, 0x7b, 0x4d, 0xd, 0xe6, 0xcb, 0xa}}; - -// {3D02CAB6-2B8E-4781-BA20-1C9267529467} -static const GUID c_guidProfile = { - 0x3d02cab6, - 0x2b8e, - 0x4781, - {0xba, 0x20, 0x1c, 0x92, 0x67, 0x52, 0x94, 0x67}}; - -// if in the future, option hant is extended, maybe a function to generate this -// info is required -#define PSZTITLE_HANS \ - L"0804:{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}{3D02CAB6-2B8E-4781-BA20-" \ - L"1C9267529467}" -#define PSZTITLE_HANT \ - L"0404:{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}{3D02CAB6-2B8E-4781-BA20-" \ - L"1C9267529467}" -#define ILOT_UNINSTALL 0x00000001 -typedef HRESULT(WINAPI* PTF_INSTALLLAYOUTORTIP)(LPCWSTR psz, DWORD dwFlags); - -#define WEASEL_WER_KEY \ - L"SOFTWARE\\Microsoft\\Windows\\Windows Error " \ - L"Reporting\\LocalDumps\\WeaselServer.exe" - -BOOL copy_file(const std::wstring& src, const std::wstring& dest) { - BOOL ret = CopyFile(src.c_str(), dest.c_str(), FALSE); - if (!ret) { - for (int i = 0; i < 10; ++i) { - std::wstring old = dest + L".old." + std::to_wstring(i); - if (MoveFileEx(dest.c_str(), old.c_str(), MOVEFILE_REPLACE_EXISTING)) { - MoveFileEx(old.c_str(), NULL, MOVEFILE_DELAY_UNTIL_REBOOT); - break; - } - } - ret = CopyFile(src.c_str(), dest.c_str(), FALSE); - } - return ret; -} - -BOOL delete_file(const std::wstring& file) { - BOOL ret = DeleteFile(file.c_str()); - if (!ret) { - for (int i = 0; i < 10; ++i) { - std::wstring old = file + L".old." + std::to_wstring(i); - if (MoveFileEx(file.c_str(), old.c_str(), MOVEFILE_REPLACE_EXISTING)) { - MoveFileEx(old.c_str(), NULL, MOVEFILE_DELAY_UNTIL_REBOOT); - return TRUE; - } - } - } - return ret; -} - -typedef BOOL(WINAPI* PISWOW64P2)(HANDLE, USHORT*, USHORT*); -BOOL is_arm64_machine() { - PISWOW64P2 fnIsWow64Process2 = (PISWOW64P2)GetProcAddress( - GetModuleHandle(_T("kernel32.dll")), "IsWow64Process2"); - - if (fnIsWow64Process2 == NULL) { - return FALSE; - } - - USHORT processMachine; - USHORT nativeMachine; - - if (!fnIsWow64Process2(GetCurrentProcess(), &processMachine, - &nativeMachine)) { - return FALSE; - } - return nativeMachine == IMAGE_FILE_MACHINE_ARM64; -} - -typedef HRESULT(WINAPI* PISWOWGMS)(USHORT, BOOL*); -typedef UINT(WINAPI* PGSW64DIR2)(LPWSTR, UINT, WORD); -INT get_wow_arm32_system_dir(LPWSTR lpBuffer, UINT uSize) { - PISWOWGMS fnIsWow64GuestMachineSupported = (PISWOWGMS)GetProcAddress( - GetModuleHandle(_T("kernel32.dll")), "IsWow64GuestMachineSupported"); - PGSW64DIR2 fnGetSystemWow64Directory2W = (PGSW64DIR2)GetProcAddress( - GetModuleHandle(_T("kernelbase.dll")), "GetSystemWow64Directory2W"); - - if (fnIsWow64GuestMachineSupported == NULL || - fnGetSystemWow64Directory2W == NULL) { - return 0; - } - - BOOL supported; - if (fnIsWow64GuestMachineSupported(IMAGE_FILE_MACHINE_ARMNT, &supported) != - S_OK) { - return 0; - } - - if (!supported) { - return 0; - } - - return fnGetSystemWow64Directory2W(lpBuffer, uSize, IMAGE_FILE_MACHINE_ARMNT); -} - -typedef int (*ime_register_func)(const std::wstring& ime_path, - bool register_ime, - bool is_wow64, - bool is_wowarm, - bool hant, - bool silent); - -int install_ime_file(std::wstring& srcPath, - const std::wstring& ext, - bool hant, - bool silent, - ime_register_func func) { - WCHAR path[MAX_PATH]; - GetModuleFileNameW(GetModuleHandle(NULL), path, _countof(path)); - - std::wstring srcFileName = L"weasel"; - - srcFileName += ext; - WCHAR drive[_MAX_DRIVE]; - WCHAR dir[_MAX_DIR]; - _wsplitpath_s(path, drive, _countof(drive), dir, _countof(dir), NULL, 0, NULL, - 0); - srcPath = std::wstring(drive) + dir + srcFileName; - - GetSystemDirectoryW(path, _countof(path)); - std::wstring destPath = std::wstring(path) + L"\\weasel" + ext; - - int retval = 0; - // 复制 .dll/.ime 到系统目录 - if (!copy_file(srcPath, destPath)) { - MSG_NOT_SILENT_ID_CAP(silent, destPath.c_str(), IDS_STR_INSTALL_FAILED, - MB_ICONERROR | MB_OK); - return 1; - } - retval += func(destPath, true, false, false, hant, silent); - if (is_wow64()) { - PVOID OldValue = NULL; - // PW64DW64FR fnWow64DisableWow64FsRedirection = - // (PW64DW64FR)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), - // "Wow64DisableWow64FsRedirection"); PW64RW64FR - // fnWow64RevertWow64FsRedirection = - // (PW64RW64FR)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), - // "Wow64RevertWow64FsRedirection"); - if (Wow64DisableWow64FsRedirection(&OldValue) == FALSE) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRCANCELFSREDIRECT, - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - if (is_arm64_machine()) { - WCHAR sysarm32[MAX_PATH]; - if (get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)) > 0) { - // Install the ARM32 version if ARM32 WOW is supported (lower than - // Windows 11 24H2). - std::wstring srcPathARM32 = srcPath; - ireplace_last(srcPathARM32, ext, L"ARM" + ext); - - std::wstring destPathARM32 = std::wstring(sysarm32) + L"\\weasel" + ext; - if (!copy_file(srcPathARM32, destPathARM32)) { - MSG_NOT_SILENT_ID_CAP(silent, destPathARM32.c_str(), - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - retval += func(destPathARM32, true, true, true, hant, silent); - } - - // Then install the ARM64 (and x64) version. - // On ARM64 weasel.dll(ime) is an ARM64X redirection DLL (weaselARM64X). - // When loaded, it will be redirected to weaselARM64.dll(ime) on ARM64 - // processes, and weaselx64.dll(ime) on x64 processes. So we need a total - // of three files. - - std::wstring srcPathX64 = srcPath; - std::wstring destPathX64 = destPath; - ireplace_last(srcPathX64, ext, L"x64" + ext); - ireplace_last(destPathX64, ext, L"x64" + ext); - if (!copy_file(srcPathX64, destPathX64)) { - MSG_NOT_SILENT_ID_CAP(silent, destPathX64.c_str(), - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - std::wstring srcPathARM64 = srcPath; - std::wstring destPathARM64 = destPath; - ireplace_last(srcPathARM64, ext, L"ARM64" + ext); - ireplace_last(destPathARM64, ext, L"ARM64" + ext); - if (!copy_file(srcPathARM64, destPathARM64)) { - MSG_NOT_SILENT_ID_CAP(silent, destPathARM64.c_str(), - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - // Since weaselARM64X is just a redirector we don't have separate - // HANS and HANT variants. - srcPath = std::wstring(drive) + dir + L"weaselARM64X" + ext; - } else { - ireplace_last(srcPath, ext, L"x64" + ext); - } - - if (!copy_file(srcPath, destPath)) { - MSG_NOT_SILENT_ID_CAP(silent, destPath.c_str(), IDS_STR_INSTALL_FAILED, - MB_ICONERROR | MB_OK); - return 1; - } - retval += func(destPath, true, true, false, hant, silent); - if (Wow64RevertWow64FsRedirection(OldValue) == FALSE) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRRECOVERFSREDIRECT, - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - } - return retval; -} - -int uninstall_ime_file(const std::wstring& ext, - bool silent, - ime_register_func func) { - int retval = 0; - WCHAR path[MAX_PATH]; - GetSystemDirectoryW(path, _countof(path)); - std::wstring imePath(path); - imePath += L"\\weasel" + ext; - retval += func(imePath, false, false, false, false, silent); - delete_file(imePath); - if (is_wow64()) { - retval += func(imePath, false, true, false, false, silent); - PVOID OldValue = NULL; - if (Wow64DisableWow64FsRedirection(&OldValue) == FALSE) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRCANCELFSREDIRECT, - IDS_STR_UNINSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - if (is_arm64_machine()) { - WCHAR sysarm32[MAX_PATH]; - if (get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)) > 0) { - std::wstring imePathARM32 = std::wstring(sysarm32) + L"\\weasel" + ext; - retval += func(imePathARM32, false, true, true, false, silent); - delete_file(imePathARM32); - } - - std::wstring imePathX64 = imePath; - ireplace_last(imePathX64, ext, L"x64" + ext); - delete_file(imePathX64); - - std::wstring imePathARM64 = imePath; - ireplace_last(imePathARM64, ext, L"ARM64" + ext); - delete_file(imePathARM64); - } - - delete_file(imePath); - if (Wow64RevertWow64FsRedirection(OldValue) == FALSE) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRRECOVERFSREDIRECT, - IDS_STR_UNINSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - } - return retval; -} - -// 注册IME输入法 -// `register_ime` (IMM/.ime) support removed — TSF-only build - -void enable_profile(BOOL fEnable, bool hant) { - HRESULT hr; - ITfInputProcessorProfiles* pProfiles = NULL; - - hr = CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, - CLSCTX_INPROC_SERVER, IID_ITfInputProcessorProfiles, - (LPVOID*)&pProfiles); - - if (SUCCEEDED(hr)) { - LANGID lang_id = hant ? 0x0404 : 0x0804; - if (fEnable) { - pProfiles->EnableLanguageProfile(c_clsidTextService, lang_id, - c_guidProfile, fEnable); - pProfiles->EnableLanguageProfileByDefault(c_clsidTextService, lang_id, - c_guidProfile, fEnable); - } else { - pProfiles->RemoveLanguageProfile(c_clsidTextService, lang_id, - c_guidProfile); - } - - pProfiles->Release(); - } -} - -// 注册TSF输入法 -int register_text_service(const std::wstring& tsf_path, - bool register_ime, - bool is_wow64, - bool is_wowarm32, - bool hant, - bool silent) { - using RegisterServerFunction = HRESULT(STDAPICALLTYPE*)(); - - if (!register_ime) - enable_profile(FALSE, hant); - - std::wstring params = L" \"" + tsf_path + L"\""; - if (!register_ime) { - params = L" /u " + params; // unregister - } - // if (silent) // always silent - { params = L" /s " + params; } - - if (!SetEnvironmentVariable(L"TEXTSERVICE_PROFILE", - hant ? L"hant" : L"hans")) { - throw std::runtime_error("SetEnvironmentVariable failed"); - } - - std::wstring app = L"regsvr32.exe"; - if (is_wowarm32) { - WCHAR sysarm32[MAX_PATH]; - get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)); - - app = std::wstring(sysarm32) + L"\\" + app; - } - - SHELLEXECUTEINFOW shExInfo = {0}; - shExInfo.cbSize = sizeof(shExInfo); - shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS; - shExInfo.hwnd = 0; - shExInfo.lpVerb = L"open"; // Operation to perform - shExInfo.lpFile = app.c_str(); // Application to start - shExInfo.lpParameters = params.c_str(); // Additional parameters - shExInfo.lpDirectory = 0; - shExInfo.nShow = SW_SHOW; - shExInfo.hInstApp = 0; - if (ShellExecuteExW(&shExInfo)) { - WaitForSingleObject(shExInfo.hProcess, INFINITE); - CloseHandle(shExInfo.hProcess); - } else { - WCHAR msg[100]; - CString str; - str.LoadStringW(IDS_STR_ERRREGTSF); - StringCchPrintfW(msg, _countof(msg), str, params.c_str()); - MSG_NOT_SILENT_ID_CAP(silent, msg, IDS_STR_INORUN_FAILED, - MB_ICONERROR | MB_OK); - return 1; - } - - if (register_ime) - enable_profile(TRUE, hant); - - return 0; -} - -int install(bool hant, bool silent) { - std::wstring ime_src_path; - int retval = 0; - - retval += install_ime_file(ime_src_path, L".dll", hant, silent, - ®ister_text_service); - - // 写注册表 - WCHAR drive[_MAX_DRIVE]; - WCHAR dir[_MAX_DIR]; - _wsplitpath_s(ime_src_path.c_str(), drive, _countof(drive), dir, - _countof(dir), NULL, 0, NULL, 0); - std::wstring rootDir = std::wstring(drive) + dir; - rootDir.pop_back(); - auto ret = SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY, L"WeaselRoot", - rootDir.c_str(), REG_SZ); - if (FAILED(HRESULT_FROM_WIN32(ret))) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRWRITEWEASELROOT, - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - const std::wstring executable = L"WeaselServer.exe"; - ret = SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY, L"ServerExecutable", - executable.c_str(), REG_SZ); - if (FAILED(HRESULT_FROM_WIN32(ret))) { - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRREGIMEWRITESVREXE, - IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); - return 1; - } - - // InstallLayoutOrTip - // https://learn.microsoft.com/zh-cn/windows/win32/tsf/installlayoutortip - // example in ref page not right with "*PTF_ INSTALLLAYOUTORTIP" - // space inside should be removed - HMODULE hInputDLL = LoadLibrary(TEXT("input.dll")); - if (hInputDLL) { - PTF_INSTALLLAYOUTORTIP pfnInstallLayoutOrTip; - pfnInstallLayoutOrTip = - (PTF_INSTALLLAYOUTORTIP)GetProcAddress(hInputDLL, "InstallLayoutOrTip"); - if (pfnInstallLayoutOrTip) { - if (hant) - (*pfnInstallLayoutOrTip)(PSZTITLE_HANT, 0); - else - (*pfnInstallLayoutOrTip)(PSZTITLE_HANS, 0); - } - FreeLibrary(hInputDLL); - } - - // https://learn.microsoft.com/zh-cn/windows/win32/wer/collecting-user-mode-dumps - const std::wstring dmpPathW = WeaselLogPath().wstring(); - // DumpFolder - SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpFolder", - dmpPathW.c_str(), REG_SZ, true); - // dump type 0 - SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpType", 0, REG_DWORD, - true); - // CustomDumpFlags, MiniDumpNormal - SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"CustomDumpFlags", 0, - REG_DWORD, true); - // maximium dump count 10 - SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpCount", 10, - REG_DWORD, true); - - if (retval) - return 1; - - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_INSTALL_SUCCESS_INFO, - IDS_STR_INSTALL_SUCCESS_CAP, - MB_ICONINFORMATION | MB_OK); - return 0; -} - -int uninstall(bool silent) { - // 注销输入法 - int retval = 0; - - const WCHAR KEY[] = L"Software\\Rime\\Weasel"; - HKEY hKey; - LSTATUS ret = RegOpenKey(HKEY_CURRENT_USER, KEY, &hKey); - if (ret == ERROR_SUCCESS) { - DWORD type = 0; - DWORD data = 0; - DWORD len = sizeof(data); - ret = RegQueryValueEx(hKey, L"Hant", NULL, &type, (LPBYTE)&data, &len); - if (ret == ERROR_SUCCESS && type == REG_DWORD) { - HMODULE hInputDLL = LoadLibrary(TEXT("input.dll")); - if (hInputDLL) { - PTF_INSTALLLAYOUTORTIP pfnInstallLayoutOrTip; - pfnInstallLayoutOrTip = (PTF_INSTALLLAYOUTORTIP)GetProcAddress( - hInputDLL, "InstallLayoutOrTip"); - if (pfnInstallLayoutOrTip) { - if (data != 0) - (*pfnInstallLayoutOrTip)(PSZTITLE_HANT, ILOT_UNINSTALL); - else - (*pfnInstallLayoutOrTip)(PSZTITLE_HANS, ILOT_UNINSTALL); - } - FreeLibrary(hInputDLL); - } - } - RegCloseKey(hKey); - } - - // IMM/.ime support removed; only uninstall TSF/.dll - retval += uninstall_ime_file(L".dll", silent, ®ister_text_service); - - // 清除注册信息 - RegDeleteKey(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY); - RegDeleteKey(HKEY_LOCAL_MACHINE, RIME_REG_KEY); - - // delete WER register, - // "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\Windows Error - // Reporting\\LocalDumps\\WeaselServer.exe" no WOW64 redirect - - auto flag_wow64 = is_wow64() ? KEY_WOW64_64KEY : 0; - RegDeleteKeyEx(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, flag_wow64, 0); - if (retval) - return 1; - - MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_UNINSTALL_SUCCESS_INFO, - IDS_STR_UNINSTALL_SUCCESS_CAP, - MB_ICONINFORMATION | MB_OK); - return 0; -} - -bool has_installed() { - WCHAR path[MAX_PATH]; - GetSystemDirectory(path, _countof(path)); - std::wstring sysPath(path); - DWORD attr = GetFileAttributesW((sysPath + L"\\weasel.dll").c_str()); - return (attr != INVALID_FILE_ATTRIBUTES && - !(attr & FILE_ATTRIBUTE_DIRECTORY)); -} +#include "stdafx.h" +#include +#include +#include +#include +#include +#include +#include +#include "InstallOptionsDlg.h" + +// {A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A} +static const GUID c_clsidTextService = { + 0xa3f4cded, + 0xb1e9, + 0x41ee, + {0x9c, 0xa6, 0x7b, 0x4d, 0xd, 0xe6, 0xcb, 0xa}}; + +// {3D02CAB6-2B8E-4781-BA20-1C9267529467} +static const GUID c_guidProfile = { + 0x3d02cab6, + 0x2b8e, + 0x4781, + {0xba, 0x20, 0x1c, 0x92, 0x67, 0x52, 0x94, 0x67}}; + +// if in the future, option hant is extended, maybe a function to generate this +// info is required +#define PSZTITLE_HANS \ + L"0804:{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}{3D02CAB6-2B8E-4781-BA20-" \ + L"1C9267529467}" +#define PSZTITLE_HANT \ + L"0404:{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}{3D02CAB6-2B8E-4781-BA20-" \ + L"1C9267529467}" +#define ILOT_UNINSTALL 0x00000001 +typedef HRESULT(WINAPI* PTF_INSTALLLAYOUTORTIP)(LPCWSTR psz, DWORD dwFlags); + +#define WEASEL_WER_KEY \ + L"SOFTWARE\\Microsoft\\Windows\\Windows Error " \ + L"Reporting\\LocalDumps\\WeaselServer.exe" + +BOOL copy_file(const std::wstring& src, const std::wstring& dest) { + WeaselDebugLog(L"WeaselSetup", L"copy_file src=" + src + L" dest=" + dest); + + DWORD src_attr = GetFileAttributesW(src.c_str()); + if (src_attr == INVALID_FILE_ATTRIBUTES || + (src_attr & FILE_ATTRIBUTE_DIRECTORY)) { + WeaselDebugLog(L"WeaselSetup", L"copy_file source unavailable error=" + + std::to_wstring(GetLastError())); + return FALSE; + } + + std::wstring temp = dest + L".new"; + DeleteFileW(temp.c_str()); + if (!CopyFileW(src.c_str(), temp.c_str(), FALSE)) { + WeaselDebugLog(L"WeaselSetup", L"copy_file temp CopyFile failed error=" + + std::to_wstring(GetLastError())); + return FALSE; + } + + if (MoveFileExW(temp.c_str(), dest.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + WeaselDebugLog(L"WeaselSetup", L"copy_file result=1"); + return TRUE; + } + + DWORD replace_error = GetLastError(); + WeaselDebugLog(L"WeaselSetup", L"copy_file replace failed error=" + + std::to_wstring(replace_error)); + + if (MoveFileExW(temp.c_str(), dest.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_DELAY_UNTIL_REBOOT)) { + WeaselDebugLog(L"WeaselSetup", L"copy_file scheduled delayed replace"); + return TRUE; + } + + DWORD delayed_error = GetLastError(); + DeleteFileW(temp.c_str()); + WeaselDebugLog(L"WeaselSetup", L"copy_file delayed replace failed error=" + + std::to_wstring(delayed_error)); + return FALSE; +} + +BOOL delete_file(const std::wstring& file) { + BOOL ret = DeleteFile(file.c_str()); + if (!ret) { + for (int i = 0; i < 10; ++i) { + std::wstring old = file + L".old." + std::to_wstring(i); + if (MoveFileEx(file.c_str(), old.c_str(), MOVEFILE_REPLACE_EXISTING)) { + MoveFileEx(old.c_str(), NULL, MOVEFILE_DELAY_UNTIL_REBOOT); + return TRUE; + } + } + } + return ret; +} + +typedef BOOL(WINAPI* PISWOW64P2)(HANDLE, USHORT*, USHORT*); +BOOL is_arm64_machine() { + PISWOW64P2 fnIsWow64Process2 = (PISWOW64P2)GetProcAddress( + GetModuleHandle(_T("kernel32.dll")), "IsWow64Process2"); + + if (fnIsWow64Process2 == NULL) { + return FALSE; + } + + USHORT processMachine; + USHORT nativeMachine; + + if (!fnIsWow64Process2(GetCurrentProcess(), &processMachine, + &nativeMachine)) { + return FALSE; + } + return nativeMachine == IMAGE_FILE_MACHINE_ARM64; +} + +typedef HRESULT(WINAPI* PISWOWGMS)(USHORT, BOOL*); +typedef UINT(WINAPI* PGSW64DIR2)(LPWSTR, UINT, WORD); +INT get_wow_arm32_system_dir(LPWSTR lpBuffer, UINT uSize) { + PISWOWGMS fnIsWow64GuestMachineSupported = (PISWOWGMS)GetProcAddress( + GetModuleHandle(_T("kernel32.dll")), "IsWow64GuestMachineSupported"); + PGSW64DIR2 fnGetSystemWow64Directory2W = (PGSW64DIR2)GetProcAddress( + GetModuleHandle(_T("kernelbase.dll")), "GetSystemWow64Directory2W"); + + if (fnIsWow64GuestMachineSupported == NULL || + fnGetSystemWow64Directory2W == NULL) { + return 0; + } + + BOOL supported; + if (fnIsWow64GuestMachineSupported(IMAGE_FILE_MACHINE_ARMNT, &supported) != + S_OK) { + return 0; + } + + if (!supported) { + return 0; + } + + return fnGetSystemWow64Directory2W(lpBuffer, uSize, IMAGE_FILE_MACHINE_ARMNT); +} + +typedef int (*ime_register_func)(const std::wstring& ime_path, + bool register_ime, + bool is_wow64, + bool is_wowarm, + bool hant, + bool silent); + +int install_ime_file(std::wstring& srcPath, + const std::wstring& ext, + bool hant, + bool silent, + ime_register_func func) { + WCHAR path[MAX_PATH]; + GetModuleFileNameW(GetModuleHandle(NULL), path, _countof(path)); + + std::wstring srcFileName = L"weasel"; + + srcFileName += ext; + WCHAR drive[_MAX_DRIVE]; + WCHAR dir[_MAX_DIR]; + _wsplitpath_s(path, drive, _countof(drive), dir, _countof(dir), NULL, 0, NULL, + 0); + srcPath = std::wstring(drive) + dir + srcFileName; + + GetSystemDirectoryW(path, _countof(path)); + std::wstring destPath = std::wstring(path) + L"\\weasel" + ext; + + int retval = 0; + // 复制 .dll/.ime 到系统目录 + if (!copy_file(srcPath, destPath)) { + MSG_NOT_SILENT_ID_CAP(silent, destPath.c_str(), IDS_STR_INSTALL_FAILED, + MB_ICONERROR | MB_OK); + return 1; + } + retval += func(destPath, true, false, false, hant, silent); + if (is_wow64()) { + PVOID OldValue = NULL; + // PW64DW64FR fnWow64DisableWow64FsRedirection = + // (PW64DW64FR)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), + // "Wow64DisableWow64FsRedirection"); PW64RW64FR + // fnWow64RevertWow64FsRedirection = + // (PW64RW64FR)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), + // "Wow64RevertWow64FsRedirection"); + if (Wow64DisableWow64FsRedirection(&OldValue) == FALSE) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRCANCELFSREDIRECT, + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + if (is_arm64_machine()) { + WCHAR sysarm32[MAX_PATH]; + if (get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)) > 0) { + // Install the ARM32 version if ARM32 WOW is supported (lower than + // Windows 11 24H2). + std::wstring srcPathARM32 = srcPath; + ireplace_last(srcPathARM32, ext, L"ARM" + ext); + + std::wstring destPathARM32 = std::wstring(sysarm32) + L"\\weasel" + ext; + if (!copy_file(srcPathARM32, destPathARM32)) { + MSG_NOT_SILENT_ID_CAP(silent, destPathARM32.c_str(), + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + retval += func(destPathARM32, true, true, true, hant, silent); + } + + // Then install the ARM64 (and x64) version. + // On ARM64 weasel.dll(ime) is an ARM64X redirection DLL (weaselARM64X). + // When loaded, it will be redirected to weaselARM64.dll(ime) on ARM64 + // processes, and weaselx64.dll(ime) on x64 processes. So we need a total + // of three files. + + std::wstring srcPathX64 = srcPath; + std::wstring destPathX64 = destPath; + ireplace_last(srcPathX64, ext, L"x64" + ext); + ireplace_last(destPathX64, ext, L"x64" + ext); + if (!copy_file(srcPathX64, destPathX64)) { + MSG_NOT_SILENT_ID_CAP(silent, destPathX64.c_str(), + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + std::wstring srcPathARM64 = srcPath; + std::wstring destPathARM64 = destPath; + ireplace_last(srcPathARM64, ext, L"ARM64" + ext); + ireplace_last(destPathARM64, ext, L"ARM64" + ext); + if (!copy_file(srcPathARM64, destPathARM64)) { + MSG_NOT_SILENT_ID_CAP(silent, destPathARM64.c_str(), + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + // Since weaselARM64X is just a redirector we don't have separate + // HANS and HANT variants. + srcPath = std::wstring(drive) + dir + L"weaselARM64X" + ext; + } else { + ireplace_last(srcPath, ext, L"x64" + ext); + } + + if (!copy_file(srcPath, destPath)) { + MSG_NOT_SILENT_ID_CAP(silent, destPath.c_str(), IDS_STR_INSTALL_FAILED, + MB_ICONERROR | MB_OK); + return 1; + } + retval += func(destPath, true, true, false, hant, silent); + if (Wow64RevertWow64FsRedirection(OldValue) == FALSE) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRRECOVERFSREDIRECT, + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + } + return retval; +} + +int uninstall_ime_file(const std::wstring& ext, + bool silent, + ime_register_func func) { + int retval = 0; + WCHAR path[MAX_PATH]; + GetSystemDirectoryW(path, _countof(path)); + std::wstring imePath(path); + imePath += L"\\weasel" + ext; + retval += func(imePath, false, false, false, false, silent); + delete_file(imePath); + if (is_wow64()) { + retval += func(imePath, false, true, false, false, silent); + PVOID OldValue = NULL; + if (Wow64DisableWow64FsRedirection(&OldValue) == FALSE) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRCANCELFSREDIRECT, + IDS_STR_UNINSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + if (is_arm64_machine()) { + WCHAR sysarm32[MAX_PATH]; + if (get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)) > 0) { + std::wstring imePathARM32 = std::wstring(sysarm32) + L"\\weasel" + ext; + retval += func(imePathARM32, false, true, true, false, silent); + delete_file(imePathARM32); + } + + std::wstring imePathX64 = imePath; + ireplace_last(imePathX64, ext, L"x64" + ext); + delete_file(imePathX64); + + std::wstring imePathARM64 = imePath; + ireplace_last(imePathARM64, ext, L"ARM64" + ext); + delete_file(imePathARM64); + } + + delete_file(imePath); + if (Wow64RevertWow64FsRedirection(OldValue) == FALSE) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRRECOVERFSREDIRECT, + IDS_STR_UNINSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + } + return retval; +} + +// 注册 TSF 文本服务 + +void enable_profile(BOOL fEnable, bool hant) { + HRESULT hr; + ITfInputProcessorProfiles* pProfiles = NULL; + + hr = CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, + CLSCTX_INPROC_SERVER, IID_ITfInputProcessorProfiles, + (LPVOID*)&pProfiles); + + if (SUCCEEDED(hr)) { + LANGID lang_id = hant ? 0x0404 : 0x0804; + if (fEnable) { + pProfiles->EnableLanguageProfile(c_clsidTextService, lang_id, + c_guidProfile, fEnable); + pProfiles->EnableLanguageProfileByDefault(c_clsidTextService, lang_id, + c_guidProfile, fEnable); + } else { + pProfiles->RemoveLanguageProfile(c_clsidTextService, lang_id, + c_guidProfile); + } + + pProfiles->Release(); + } +} + +// 注册TSF输入法 +int register_text_service(const std::wstring& tsf_path, + bool register_ime, + bool is_wow64, + bool is_wowarm32, + bool hant, + bool silent) { + using RegisterServerFunction = HRESULT(STDAPICALLTYPE*)(); + + if (!register_ime) + enable_profile(FALSE, hant); + + std::wstring params = L" \"" + tsf_path + L"\""; + if (!register_ime) { + params = L" /u " + params; // unregister + } + // if (silent) // always silent + { params = L" /s " + params; } + + if (!SetEnvironmentVariable(L"TEXTSERVICE_PROFILE", + hant ? L"hant" : L"hans")) { + throw std::runtime_error("SetEnvironmentVariable failed"); + } + + std::wstring app = L"regsvr32.exe"; + if (is_wowarm32) { + WCHAR sysarm32[MAX_PATH]; + get_wow_arm32_system_dir(sysarm32, _countof(sysarm32)); + + app = std::wstring(sysarm32) + L"\\" + app; + } + + SHELLEXECUTEINFOW shExInfo = {0}; + shExInfo.cbSize = sizeof(shExInfo); + shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + shExInfo.hwnd = 0; + shExInfo.lpVerb = L"open"; // Operation to perform + shExInfo.lpFile = app.c_str(); // Application to start + shExInfo.lpParameters = params.c_str(); // Additional parameters + shExInfo.lpDirectory = 0; + shExInfo.nShow = SW_SHOW; + shExInfo.hInstApp = 0; + if (ShellExecuteExW(&shExInfo)) { + WaitForSingleObject(shExInfo.hProcess, INFINITE); + CloseHandle(shExInfo.hProcess); + } else { + WCHAR msg[100]; + CString str; + str.LoadStringW(IDS_STR_ERRREGTSF); + StringCchPrintfW(msg, _countof(msg), str, params.c_str()); + MSG_NOT_SILENT_ID_CAP(silent, msg, IDS_STR_INORUN_FAILED, + MB_ICONERROR | MB_OK); + return 1; + } + + if (register_ime) + enable_profile(TRUE, hant); + + return 0; +} + +int install(bool hant, bool silent) { + std::wstring ime_src_path; + int retval = 0; + + retval += install_ime_file(ime_src_path, L".dll", hant, silent, + ®ister_text_service); + + // 写注册表 + WCHAR drive[_MAX_DRIVE]; + WCHAR dir[_MAX_DIR]; + _wsplitpath_s(ime_src_path.c_str(), drive, _countof(drive), dir, + _countof(dir), NULL, 0, NULL, 0); + std::wstring rootDir = std::wstring(drive) + dir; + rootDir.pop_back(); + auto ret = SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY, L"WeaselRoot", + rootDir.c_str(), REG_SZ); + if (FAILED(HRESULT_FROM_WIN32(ret))) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRWRITEWEASELROOT, + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + const std::wstring executable = L"WeaselServer.exe"; + ret = SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY, L"ServerExecutable", + executable.c_str(), REG_SZ); + if (FAILED(HRESULT_FROM_WIN32(ret))) { + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_ERRREGIMEWRITESVREXE, + IDS_STR_INSTALL_FAILED, MB_ICONERROR | MB_OK); + return 1; + } + + // InstallLayoutOrTip + // https://learn.microsoft.com/zh-cn/windows/win32/tsf/installlayoutortip + // example in ref page not right with "*PTF_ INSTALLLAYOUTORTIP" + // space inside should be removed + HMODULE hInputDLL = LoadLibrary(TEXT("input.dll")); + if (hInputDLL) { + PTF_INSTALLLAYOUTORTIP pfnInstallLayoutOrTip; + pfnInstallLayoutOrTip = + (PTF_INSTALLLAYOUTORTIP)GetProcAddress(hInputDLL, "InstallLayoutOrTip"); + if (pfnInstallLayoutOrTip) { + if (hant) + (*pfnInstallLayoutOrTip)(PSZTITLE_HANT, 0); + else + (*pfnInstallLayoutOrTip)(PSZTITLE_HANS, 0); + } + FreeLibrary(hInputDLL); + } + + // https://learn.microsoft.com/zh-cn/windows/win32/wer/collecting-user-mode-dumps + const std::wstring dmpPathW = WeaselLogPath().wstring(); + // DumpFolder + SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpFolder", + dmpPathW.c_str(), REG_SZ, true); + // dump type 0 + SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpType", 0, REG_DWORD, + true); + // CustomDumpFlags, MiniDumpNormal + SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"CustomDumpFlags", 0, + REG_DWORD, true); + // maximium dump count 10 + SetRegKeyValue(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, L"DumpCount", 10, + REG_DWORD, true); + + if (retval) + return 1; + + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_INSTALL_SUCCESS_INFO, + IDS_STR_INSTALL_SUCCESS_CAP, + MB_ICONINFORMATION | MB_OK); + return 0; +} + +int uninstall(bool silent) { + // 注销输入法 + int retval = 0; + + const WCHAR KEY[] = L"Software\\Rime\\Weasel"; + HKEY hKey; + LSTATUS ret = RegOpenKey(HKEY_CURRENT_USER, KEY, &hKey); + if (ret == ERROR_SUCCESS) { + DWORD type = 0; + DWORD data = 0; + DWORD len = sizeof(data); + ret = RegQueryValueEx(hKey, L"Hant", NULL, &type, (LPBYTE)&data, &len); + if (ret == ERROR_SUCCESS && type == REG_DWORD) { + HMODULE hInputDLL = LoadLibrary(TEXT("input.dll")); + if (hInputDLL) { + PTF_INSTALLLAYOUTORTIP pfnInstallLayoutOrTip; + pfnInstallLayoutOrTip = (PTF_INSTALLLAYOUTORTIP)GetProcAddress( + hInputDLL, "InstallLayoutOrTip"); + if (pfnInstallLayoutOrTip) { + if (data != 0) + (*pfnInstallLayoutOrTip)(PSZTITLE_HANT, ILOT_UNINSTALL); + else + (*pfnInstallLayoutOrTip)(PSZTITLE_HANS, ILOT_UNINSTALL); + } + FreeLibrary(hInputDLL); + } + } + RegCloseKey(hKey); + } + + // Uninstall TSF text service DLL. + retval += uninstall_ime_file(L".dll", silent, ®ister_text_service); + + // 清除注册信息 + RegDeleteKey(HKEY_LOCAL_MACHINE, WEASEL_REG_KEY); + RegDeleteKey(HKEY_LOCAL_MACHINE, RIME_REG_KEY); + + // delete WER register, + // "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\Windows Error + // Reporting\\LocalDumps\\WeaselServer.exe" no WOW64 redirect + + auto flag_wow64 = is_wow64() ? KEY_WOW64_64KEY : 0; + RegDeleteKeyEx(HKEY_LOCAL_MACHINE, WEASEL_WER_KEY, flag_wow64, 0); + if (retval) + return 1; + + MSG_NOT_SILENT_BY_IDS(silent, IDS_STR_UNINSTALL_SUCCESS_INFO, + IDS_STR_UNINSTALL_SUCCESS_CAP, + MB_ICONINFORMATION | MB_OK); + return 0; +} + +bool has_installed() { + WCHAR path[MAX_PATH]; + GetSystemDirectory(path, _countof(path)); + std::wstring sysPath(path); + DWORD attr = GetFileAttributesW((sysPath + L"\\weasel.dll").c_str()); + return (attr != INVALID_FILE_ATTRIBUTES && + !(attr & FILE_ATTRIBUTE_DIRECTORY)); +} diff --git a/WeaselTSF/Composition.cpp b/WeaselTSF/Composition.cpp index 62852bc7f..abedaa566 100644 --- a/WeaselTSF/Composition.cpp +++ b/WeaselTSF/Composition.cpp @@ -405,6 +405,8 @@ STDAPI WeaselTSF::OnCompositionTerminated(TfEditCookie ecWrite, void WeaselTSF::_AbortComposition(bool clear) { m_client.ClearComposition(); + weasel::MarkLocalCompositionAborted(_status); + _keyEventTestCache.Clear(); if (_IsComposing()) { _EndComposition(_pEditSessionContext, clear); } diff --git a/WeaselTSF/KeyEventSink.cpp b/WeaselTSF/KeyEventSink.cpp index c885dddf9..737b043e1 100644 --- a/WeaselTSF/KeyEventSink.cpp +++ b/WeaselTSF/KeyEventSink.cpp @@ -3,30 +3,119 @@ #include "WeaselTSF.h" #include #include "CandidateList.h" +#include "ResponseParser.h" static weasel::KeyEvent prevKeyEvent; static BOOL prevfEaten = FALSE; static int keyCountToSimulate = 0; +static const DWORD kManualExitKeyCheckIntervalMs = 100; -void WeaselTSF::_ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { +ibus::Keycode TranslateKeycode(UINT vkey, KeyInfo kinfo); + +static std::wstring CurrentHostPath() { + WCHAR path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(NULL, path, _countof(path)); + if (length == 0) + return L""; + return path; +} + +static std::wstring KeyEventTracePrefix(const wchar_t* callback, + WPARAM wParam, + LPARAM lParam) { + KeyInfo info(lParam); + return std::wstring(callback) + L" vk=" + std::to_wstring(wParam) + + L" lparam=" + std::to_wstring(static_cast(lParam)) + + L" identity=" + + std::to_wstring(weasel::KeyEventTestCache::ComparableLParam(lParam)) + + L" repeat=" + std::to_wstring(info.repeatCount) + L" scan=" + + std::to_wstring(info.scanCode) + L" ext=" + + std::to_wstring(info.isExtended) + L" context=" + + std::to_wstring(info.contextCode) + L" prev=" + + std::to_wstring(info.prevKeyState) + L" up=" + + std::to_wstring(info.isKeyUp); +} + +static void TraceKeyEvent(const wchar_t* callback, + WPARAM wParam, + LPARAM lParam, + const std::wstring& message) { + if (!ShouldTraceKeyEvents()) + return; + WeaselDebugLog(L"KeyEvent", KeyEventTracePrefix(callback, wParam, lParam) + + L" " + message); +} + +#define TRACE_KEY_EVENT(callback, wParam, lParam, message) \ + do { \ + if (ShouldTraceKeyEvents()) \ + TraceKeyEvent(callback, wParam, lParam, message); \ + } while (0) + +bool WeaselTSF::_TestKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + *pfEaten = FALSE; + if ((_isToOpenClose && !_IsKeyboardOpen()) || _IsKeyboardDisabled()) { + TRACE_KEY_EVENT(L"Test", wParam, lParam, + L"skip disabled_or_keyboard_closed=1"); + return false; + } + bool service_available = _CanHandleKeyEvent(); + if (!service_available) { + TRACE_KEY_EVENT(L"Test", wParam, lParam, + L"skip manual_exit_or_recovery_disabled=1"); + return false; + } + + KeyInfo key_info(lParam); + bool known_key = + TranslateKeycode(static_cast(wParam), key_info) != ibus::Null || + weasel::IsTextVirtualKey(wParam); + if (!known_key) { + TRACE_KEY_EVENT(L"Test", wParam, lParam, L"known_key=0"); + return false; + } + + BYTE key_state[256] = {0}; + GetKeyboardState(key_state); + bool shortcut_modifier = + (key_state[VK_CONTROL] & 0x80) != 0 || (key_state[VK_MENU] & 0x80) != 0; + bool predicted_eaten = weasel::ShouldEatTestKeyEvent( + service_available, _status.ascii_mode, keyCountToSimulate != 0, + _status.composing, shortcut_modifier, known_key, wParam); + *pfEaten = predicted_eaten ? TRUE : FALSE; + TRACE_KEY_EVENT( + L"Test", wParam, lParam, + L"known_key=1 ascii_mode=" + std::to_wstring(_status.ascii_mode) + + L" composing=" + std::to_wstring(_status.composing) + + L" capslock_simulation=" + std::to_wstring(keyCountToSimulate != 0) + + L" shortcut_modifier=" + std::to_wstring(shortcut_modifier) + + L" predicted_eaten=" + std::to_wstring(*pfEaten)); + return predicted_eaten; +} + +bool WeaselTSF::_ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + *pfEaten = FALSE; // when _IsKeyboardDisabled don't eat the key, // when keyboard closable and keyboard closed, don't eat the key if ((_isToOpenClose && !_IsKeyboardOpen()) || _IsKeyboardDisabled()) { - *pfEaten = FALSE; - return; + TRACE_KEY_EVENT(L"Process", wParam, lParam, + L"skip disabled_or_keyboard_closed=1"); + return false; } - - // if server connection is Not OK, don't eat it. - if (!_EnsureServerConnected()) { - *pfEaten = FALSE; - return; + if (!_CanHandleKeyEvent()) { + TRACE_KEY_EVENT(L"Process", wParam, lParam, + L"skip manual_exit_or_recovery_disabled=1"); + return false; } + weasel::KeyEvent ke; GetKeyboardState(_lpbKeyState); if (!ConvertKeyEvent(static_cast(wParam), lParam, _lpbKeyState, ke)) { /* Unknown key event */ - *pfEaten = FALSE; + TRACE_KEY_EVENT(L"Process", wParam, lParam, L"convert=0"); + return false; } else { + bool processed = false; // cheet key code when vertical auto reverse happened, swap up and down if (_cand->GetIsReposition()) { if (ke.keycode == ibus::Up) @@ -34,8 +123,27 @@ void WeaselTSF::_ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { else if (ke.keycode == ibus::Down) ke.keycode = ibus::Up; } - if (!keyCountToSimulate) - *pfEaten = (BOOL)m_client.ProcessKeyEvent(ke); + if (!keyCountToSimulate) { + bool eaten = false; + if (m_client.ProcessKeyEvent(ke, &eaten)) { + *pfEaten = (BOOL)eaten; + processed = true; + weasel::ResponseParser parser(NULL, NULL, &_status, NULL, + &_cand->style()); + m_client.GetResponseData(std::ref(parser)); + } else { + *pfEaten = FALSE; + _RecoverServerAsync(); + } + TRACE_KEY_EVENT(L"Process", wParam, lParam, + L"convert=1 keycode=" + std::to_wstring(ke.keycode) + + L" mask=" + std::to_wstring(ke.mask) + + L" processed=" + std::to_wstring(processed) + + L" eaten=" + std::to_wstring(*pfEaten)); + } else { + TRACE_KEY_EVENT(L"Process", wParam, lParam, + L"skip capslock_simulation=1"); + } if (ke.keycode == ibus::Caps_Lock) { if (prevKeyEvent.keycode == ibus::Caps_Lock && prevfEaten == TRUE && @@ -59,13 +167,29 @@ void WeaselTSF::_ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { prevfEaten = *pfEaten; prevKeyEvent = ke; + return processed; } } +bool WeaselTSF::_CanHandleKeyEvent() { + DWORD now = GetTickCount(); + if (_manualExitCheckTick == 0 || + now - _manualExitCheckTick >= kManualExitKeyCheckIntervalMs) { + _manualExitMarkedForKeyEvents = weasel::IsServiceManualExitMarked(); + _manualExitCheckTick = now; + } + return !_manualExitMarkedForKeyEvents; +} + +void WeaselTSF::_ResetKeyEventTestCacheIfNeeded() { + _keyEventTestCacheReset.Apply(_keyEventTestCache); +} + STDAPI WeaselTSF::OnSetFocus(BOOL fForeground) { if (fForeground) m_client.FocusIn(); else { + _activeKeyDownGuard.Reset(); m_client.FocusOut(); _AbortComposition(); } @@ -78,24 +202,37 @@ STDAPI WeaselTSF::OnSetFocus(BOOL fForeground) { * Some sends multiple OnTestKeyDown() for a single key event. (MS WORD 2010 * x64) * - * We assume every key event will eventually cause a OnKeyDown() call. - * We use _fTestKeyDownPending to omit multiple OnTestKeyDown() calls, - * and for OnKeyDown() to check if the key has already been sent to the server. + * Test-key callbacks may be the only callback a host reliably sends for + * composition-editing keys. Process them once and cache the result so the + * following OnKey* callback, when present, does not send the same key to Rime + * twice. */ STDAPI WeaselTSF::OnTestKeyDown(ITfContext* pContext, WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + _ResetKeyEventTestCacheIfNeeded(); _fTestKeyUpPending = FALSE; - if (_fTestKeyDownPending) { - *pfEaten = TRUE; + _keyEventTestCache.Remove(true, wParam, lParam); + if (_keyEventTestCache.Matches(false, wParam, lParam)) { + *pfEaten = _keyEventTestCache.Eaten(); + TRACE_KEY_EVENT(L"OnTestKeyDown", wParam, lParam, + L"cache=1 eaten=" + std::to_wstring(*pfEaten)); return S_OK; } - _ProcessKeyEvent(wParam, lParam, pfEaten); - _UpdateComposition(pContext); - if (*pfEaten) - _fTestKeyDownPending = TRUE; + bool processed = _ProcessKeyEvent(wParam, lParam, pfEaten); + if (processed) { + _keyEventTestCache.Store(false, wParam, lParam, *pfEaten); + _UpdateComposition(pContext); + } else { + _keyEventTestCache.Clear(); + } + _fTestKeyDownPending = (processed && *pfEaten) ? TRUE : FALSE; + TRACE_KEY_EVENT(L"OnTestKeyDown", wParam, lParam, + L"processed=" + std::to_wstring(processed) + L" eaten=" + + std::to_wstring(*pfEaten) + L" pending=" + + std::to_wstring(_fTestKeyDownPending)); return S_OK; } @@ -103,14 +240,30 @@ STDAPI WeaselTSF::OnKeyDown(ITfContext* pContext, WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + _ResetKeyEventTestCacheIfNeeded(); _fTestKeyUpPending = FALSE; - if (_fTestKeyDownPending) { - _fTestKeyDownPending = FALSE; + _fTestKeyDownPending = FALSE; + if (_keyEventTestCache.Matches(false, wParam, lParam)) { + *pfEaten = _keyEventTestCache.Eaten(); + _keyEventTestCache.RemoveMatched(); + TRACE_KEY_EVENT(L"OnKeyDown", wParam, lParam, + L"cache=1 eaten=" + std::to_wstring(*pfEaten)); + return S_OK; + } + _keyEventTestCache.Clear(); + if (_activeKeyDownGuard.ShouldSuppress(wParam, lParam)) { *pfEaten = TRUE; - } else { - _ProcessKeyEvent(wParam, lParam, pfEaten); - _UpdateComposition(pContext); + TRACE_KEY_EVENT(L"OnKeyDown", wParam, lParam, + L"active_duplicate=1 suppress=1"); + return S_OK; } + bool processed = _ProcessKeyEvent(wParam, lParam, pfEaten); + if (processed && *pfEaten) + _activeKeyDownGuard.Remember(wParam, lParam); + _UpdateComposition(pContext); + TRACE_KEY_EVENT(L"OnKeyDown", wParam, lParam, + L"processed=" + std::to_wstring(processed) + L" eaten=" + + std::to_wstring(*pfEaten)); return S_OK; } @@ -118,15 +271,27 @@ STDAPI WeaselTSF::OnTestKeyUp(ITfContext* pContext, WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + _ResetKeyEventTestCacheIfNeeded(); _fTestKeyDownPending = FALSE; - if (_fTestKeyUpPending) { - *pfEaten = TRUE; + _keyEventTestCache.Remove(false, wParam, lParam); + if (_keyEventTestCache.Matches(true, wParam, lParam)) { + *pfEaten = _keyEventTestCache.Eaten(); + TRACE_KEY_EVENT(L"OnTestKeyUp", wParam, lParam, + L"cache=1 eaten=" + std::to_wstring(*pfEaten)); return S_OK; } - _ProcessKeyEvent(wParam, lParam, pfEaten); - _UpdateComposition(pContext); - if (*pfEaten) - _fTestKeyUpPending = TRUE; + bool processed = _ProcessKeyEvent(wParam, lParam, pfEaten); + if (processed) { + _keyEventTestCache.Store(true, wParam, lParam, *pfEaten); + _UpdateComposition(pContext); + } else { + _keyEventTestCache.Clear(); + } + _fTestKeyUpPending = (processed && *pfEaten) ? TRUE : FALSE; + TRACE_KEY_EVENT(L"OnTestKeyUp", wParam, lParam, + L"processed=" + std::to_wstring(processed) + L" eaten=" + + std::to_wstring(*pfEaten) + L" pending=" + + std::to_wstring(_fTestKeyUpPending)); return S_OK; } @@ -134,15 +299,24 @@ STDAPI WeaselTSF::OnKeyUp(ITfContext* pContext, WPARAM wParam, LPARAM lParam, BOOL* pfEaten) { + _ResetKeyEventTestCacheIfNeeded(); _fTestKeyDownPending = FALSE; - if (_fTestKeyUpPending) { - _fTestKeyUpPending = FALSE; - *pfEaten = TRUE; - } else { - _ProcessKeyEvent(wParam, lParam, pfEaten); - if (!_async_edit) - _UpdateComposition(pContext); + _fTestKeyUpPending = FALSE; + _activeKeyDownGuard.Release(wParam, lParam); + if (_keyEventTestCache.Matches(true, wParam, lParam)) { + *pfEaten = _keyEventTestCache.Eaten(); + _keyEventTestCache.RemoveMatched(); + TRACE_KEY_EVENT(L"OnKeyUp", wParam, lParam, + L"cache=1 eaten=" + std::to_wstring(*pfEaten)); + return S_OK; } + _keyEventTestCache.Clear(); + bool processed = _ProcessKeyEvent(wParam, lParam, pfEaten); + if (!_async_edit) + _UpdateComposition(pContext); + TRACE_KEY_EVENT(L"OnKeyUp", wParam, lParam, + L"processed=" + std::to_wstring(processed) + L" eaten=" + + std::to_wstring(*pfEaten)); return S_OK; } @@ -162,6 +336,8 @@ BOOL WeaselTSF::_InitKeyEventSink() { hr = pKeystrokeMgr->AdviseKeyEventSink(_tfClientId, (ITfKeyEventSink*)this, TRUE); + WeaselDebugLog(L"KeyEvent", L"InitKeyEventSink hr=" + std::to_wstring(hr) + + L" host=" + CurrentHostPath()); return (hr == S_OK); } diff --git a/WeaselTSF/LanguageBar.cpp b/WeaselTSF/LanguageBar.cpp index 0ab85bec7..8067358fd 100644 --- a/WeaselTSF/LanguageBar.cpp +++ b/WeaselTSF/LanguageBar.cpp @@ -9,6 +9,10 @@ static const DWORD LANGBARITEMSINK_COOKIE = 0x42424242; +static std::wstring HandleValue(const void* handle) { + return std::to_wstring(reinterpret_cast(handle)); +} + static void HMENU2ITfMenu(HMENU hMenu, ITfMenu* pTfMenu) { /* NOTE: Only limited functions are supported */ int N = GetMenuItemCount(hMenu); @@ -34,14 +38,136 @@ static void HMENU2ITfMenu(HMENU hMenu, ITfMenu* pTfMenu) { } } -static LPCWSTR GetWeaselRegName() { - LPCWSTR WEASEL_REG_NAME_; - if (is_wow64()) - WEASEL_REG_NAME_ = L"Software\\WOW6432Node\\Rime\\Weasel"; - else - WEASEL_REG_NAME_ = L"Software\\Rime\\Weasel"; +static bool QueryMachineStringValue(LPCWSTR key_path, + LPCWSTR value_name, + REGSAM view, + std::wstring& value) { + HKEY key = NULL; + LSTATUS status = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_path, 0, KEY_READ | view, &key); + if (status != ERROR_SUCCESS) + return false; + + WCHAR buffer[MAX_PATH] = {0}; + DWORD type = 0; + DWORD bytes = sizeof(buffer); + status = RegQueryValueExW(key, value_name, NULL, &type, + reinterpret_cast(buffer), &bytes); + RegCloseKey(key); + if (status != ERROR_SUCCESS || (type != REG_SZ && type != REG_EXPAND_SZ)) + return false; + + value = buffer; + if (type == REG_EXPAND_SZ) { + WCHAR expanded[MAX_PATH] = {0}; + if (ExpandEnvironmentStringsW(value.c_str(), expanded, _countof(expanded))) + value = expanded; + } + return !value.empty(); +} + +static bool QueryMachineStringValueAllViews(LPCWSTR key_path, + LPCWSTR value_name, + std::wstring& value) { + if (is_wow64() && + QueryMachineStringValue(key_path, value_name, KEY_WOW64_64KEY, value)) + return true; + if (QueryMachineStringValue(key_path, value_name, 0, value)) + return true; + return is_wow64() && + QueryMachineStringValue(key_path, value_name, KEY_WOW64_32KEY, value); +} + +static bool QueryWeaselRoot(std::wstring& dir) { + bool ok = QueryMachineStringValueAllViews(L"Software\\Rime\\Weasel", + L"WeaselRoot", dir); + WeaselDebugLog(L"LanguageBar", + L"QueryWeaselRoot ok=" + std::to_wstring(ok) + L" dir=" + dir); + return ok; +} + +static bool QueryWeaselRootFromRunKey(std::wstring& dir) { + std::wstring command; + if (!QueryMachineStringValueAllViews( + L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", L"WeaselServer", + command)) { + WeaselDebugLog(L"LanguageBar", L"Run key WeaselServer not found"); + return false; + } + bool ok = weasel::ServiceRootFromCommandLine(command, dir); + WeaselDebugLog(L"LanguageBar", L"Run key command=" + command + L" parse_ok=" + + std::to_wstring(ok) + L" dir=" + dir); + return ok; +} + +static bool QueryWeaselRootFromModule(std::wstring& dir) { + dir.clear(); + WCHAR path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(g_hInst, path, _countof(path)); + if (length == 0 || length >= _countof(path)) { + WeaselDebugLog(L"LanguageBar", L"Module root lookup failed"); + return false; + } + + std::wstring module_path = path; + size_t slash = module_path.find_last_of(L"\\/"); + if (slash == std::wstring::npos) { + WeaselDebugLog(L"LanguageBar", L"Module root lookup failed: no directory"); + return false; + } - return WEASEL_REG_NAME_; + std::wstring module_dir = module_path.substr(0, slash); + std::wstring server = weasel::ServiceExecutablePath(module_dir); + bool ok = !server.empty() && fs::exists(server); + WeaselDebugLog(L"LanguageBar", L"Module root lookup module=" + module_path + + L" ok=" + std::to_wstring(ok) + L" dir=" + + module_dir + L" server=" + server); + if (!ok) + return false; + + dir = module_dir; + return true; +} + +static void ShowStartServiceFailure(const std::wstring& server) { + std::wstring message = L"无法启动算法服务"; + if (!server.empty()) + message += L":\n" + server; + MessageBoxW(NULL, message.c_str(), get_weasel_ime_name().c_str(), + MB_ICONERROR | MB_OK); +} + +static void ShowServiceQuitFailure() { + MessageBoxW(NULL, L"无法退出算法服务", get_weasel_ime_name().c_str(), + MB_ICONERROR | MB_OK); +} + +static bool StartWeaselServer(const std::wstring& dir, bool restart) { + std::wstring server = weasel::ServiceExecutablePath(dir); + WeaselDebugLog(L"LanguageBar", L"StartWeaselServer restart=" + + std::to_wstring(restart) + L" dir=" + dir + + L" server=" + server); + if (server.empty()) { + WeaselDebugLog(L"LanguageBar", L"StartWeaselServer failed: empty server"); + ShowStartServiceFailure(server); + return false; + } + SetLastError(ERROR_SUCCESS); + HINSTANCE result = + ShellExecuteW(NULL, L"open", server.c_str(), + restart ? weasel::ServiceManualRestartArgument() : NULL, + dir.c_str(), SW_HIDE); + if ((uintptr_t)result <= 32) { + WeaselDebugLog(L"LanguageBar", L"ShellExecute failed result=" + + std::to_wstring((uintptr_t)result) + + L" last_error=" + + std::to_wstring(GetLastError())); + ShowStartServiceFailure(server); + return false; + } + WeaselDebugLog(L"LanguageBar", L"ShellExecute ok result=" + + std::to_wstring((uintptr_t)result)); + return true; } static bool open(const std::wstring& path) { @@ -64,9 +190,13 @@ CLangBarItemButton::CLangBarItemButton(com_ptr pTextService, _pTextService = pTextService; _guid = guid; ascii_mode = false; + WeaselDebugLog(L"LanguageBar", + L"CLangBarItemButton created this=" + HandleValue(this)); } CLangBarItemButton::~CLangBarItemButton() { + WeaselDebugLog(L"LanguageBar", + L"CLangBarItemButton destroyed this=" + HandleValue(this)); DllRelease(); } @@ -152,9 +282,16 @@ STDAPI CLangBarItemButton::GetTooltipString(BSTR* pbstrToolTip) { STDAPI CLangBarItemButton::OnClick(TfLBIClick click, POINT pt, const RECT* prcArea) { + WeaselDebugLog(L"LanguageBar", L"OnClick click=" + std::to_wstring(click) + + L" pt=(" + std::to_wstring(pt.x) + L"," + + std::to_wstring(pt.y) + L") area_null=" + + std::to_wstring(prcArea == NULL)); if (click == TF_LBI_CLK_LEFT) { - _pTextService->_HandleLangBarMenuSelect( - ascii_mode ? ID_WEASELTRAY_DISABLE_ASCII : ID_WEASELTRAY_ENABLE_ASCII); + UINT wID = + ascii_mode ? ID_WEASELTRAY_DISABLE_ASCII : ID_WEASELTRAY_ENABLE_ASCII; + WeaselDebugLog(L"LanguageBar", + L"OnClick left dispatch wID=" + std::to_wstring(wID)); + _pTextService->_HandleLangBarMenuSelect(wID); ascii_mode = !ascii_mode; if (_pLangBarItemSink) { _pLangBarItemSink->OnUpdate(TF_LBI_STATUS | TF_LBI_ICON); @@ -162,36 +299,74 @@ STDAPI CLangBarItemButton::OnClick(TfLBIClick click, } else if (click == TF_LBI_CLK_RIGHT) { /* Open menu */ HWND hwnd = _pTextService->_GetFocusedContextWindow(); + WeaselDebugLog(L"LanguageBar", + L"OnClick right focused_hwnd=" + HandleValue(hwnd)); if (hwnd != NULL) { LANGID langid = get_language_id(); - HMENU menu; + UINT menu_resource = IDR_MENU_POPUP; if (langid == TEXTSERVICE_LANGID_HANS) { - menu = LoadMenuW(g_hInst, MAKEINTRESOURCE(IDR_MENU_POPUP_HANS)); + menu_resource = IDR_MENU_POPUP_HANS; } else if (langid == TEXTSERVICE_LANGID_HANT) { - menu = LoadMenuW(g_hInst, MAKEINTRESOURCE(IDR_MENU_POPUP_HANT)); - } else { - menu = LoadMenuW(g_hInst, MAKEINTRESOURCE(IDR_MENU_POPUP)); + menu_resource = IDR_MENU_POPUP_HANT; } + SetLastError(ERROR_SUCCESS); + HMENU menu = LoadMenuW(g_hInst, MAKEINTRESOURCE(menu_resource)); + WeaselDebugLog(L"LanguageBar", L"OnClick right LoadMenu resource=" + + std::to_wstring(menu_resource) + + L" menu=" + HandleValue(menu) + + L" last_error=" + + std::to_wstring(GetLastError())); + if (menu == NULL) + return S_OK; HMENU popupMenu = GetSubMenu(menu, 0); + WeaselDebugLog(L"LanguageBar", + L"OnClick right popup=" + HandleValue(popupMenu)); + if (popupMenu == NULL) { + DestroyMenu(menu); + return S_OK; + } UINT wID = TrackPopupMenuEx( popupMenu, TPM_NONOTIFY | TPM_RETURNCMD | TPM_HORPOSANIMATION, pt.x, pt.y, hwnd, NULL); + WeaselDebugLog(L"LanguageBar", L"OnClick right TrackPopupMenuEx wID=" + + std::to_wstring(wID)); DestroyMenu(menu); + if (weasel::IsTrayMenuSelectionCancelled(wID)) { + WeaselDebugLog(L"LanguageBar", + L"OnClick right menu cancelled or dismissed"); + return S_OK; + } _pTextService->_HandleLangBarMenuSelect(wID); + } else { + WeaselDebugLog(L"LanguageBar", + L"OnClick right ignored: no focused context window"); } } return S_OK; } STDAPI CLangBarItemButton::InitMenu(ITfMenu* pMenu) { + WeaselDebugLog(L"LanguageBar", L"InitMenu pMenu=" + HandleValue(pMenu)); + SetLastError(ERROR_SUCCESS); HMENU menu = LoadMenuW(g_hInst, MAKEINTRESOURCE(IDR_MENU_POPUP)); + WeaselDebugLog(L"LanguageBar", L"InitMenu LoadMenu menu=" + + HandleValue(menu) + L" last_error=" + + std::to_wstring(GetLastError())); + if (menu == NULL) + return E_FAIL; HMENU popupMenu = GetSubMenu(menu, 0); + WeaselDebugLog(L"LanguageBar", L"InitMenu popup=" + HandleValue(popupMenu)); + if (popupMenu == NULL) { + DestroyMenu(menu); + return E_FAIL; + } HMENU2ITfMenu(popupMenu, pMenu); DestroyMenu(menu); return S_OK; } STDAPI CLangBarItemButton::OnMenuSelect(UINT wID) { + WeaselDebugLog(L"LanguageBar", L"OnMenuSelect wID=" + std::to_wstring(wID)); _pTextService->_HandleLangBarMenuSelect(wID); return S_OK; } @@ -289,25 +464,41 @@ void CLangBarItemButton::SetLangbarStatus(DWORD dwStatus, BOOL fSet) { std::wstring WeaselTSF::_GetRootDir() { std::wstring dir{}; - RegGetStringValue(HKEY_LOCAL_MACHINE, GetWeaselRegName(), L"WeaselRoot", dir); + if (QueryWeaselRoot(dir)) + return dir; + if (QueryWeaselRootFromRunKey(dir)) + return dir; + QueryWeaselRootFromModule(dir); return dir; } void WeaselTSF::_HandleLangBarMenuSelect(UINT wID) { + WeaselDebugLog(L"LanguageBar", + L"HandleLangBarMenuSelect wID=" + std::to_wstring(wID)); + if (weasel::IsTrayMenuSelectionCancelled(wID)) { + WeaselDebugLog(L"LanguageBar", + L"HandleLangBarMenuSelect ignored cancelled menu"); + return; + } std::wstring dir{}; switch (wID) { case ID_WEASELTRAY_RERUN_SERVICE: case ID_WEASELTRAY_INSTALLDIR: - if (RegGetStringValue(HKEY_LOCAL_MACHINE, GetWeaselRegName(), - L"WeaselRoot", dir) == ERROR_SUCCESS) { + dir = _GetRootDir(); + if (wID == ID_WEASELTRAY_RERUN_SERVICE) { + WeaselDebugLog(L"LanguageBar", + L"Handle rerun service menu root_dir=" + dir); + } + if (!dir.empty()) { if (wID == ID_WEASELTRAY_RERUN_SERVICE) { - std::thread th([dir]() { - ShellExecuteW(NULL, L"open", (dir + L"\\start_service.bat").c_str(), - NULL, dir.c_str(), SW_HIDE); - }); + std::thread th([dir]() { StartWeaselServer(dir, true); }); th.detach(); } else open(dir); + } else if (wID == ID_WEASELTRAY_RERUN_SERVICE) { + WeaselDebugLog(L"LanguageBar", + L"Handle rerun service menu failed: empty root_dir"); + ShowStartServiceFailure(L""); } break; case ID_WEASELTRAY_USERCONFIG: @@ -327,6 +518,10 @@ void WeaselTSF::_HandleLangBarMenuSelect(UINT wID) { case ID_WEASELTRAY_LOGDIR: open(WeaselLogPath().wstring()); break; + case ID_WEASELTRAY_QUIT: + if (!m_client.TrayCommandSync(wID)) + ShowServiceQuitFailure(); + break; case ID_WEASELTRAY_WIKI: open(L"https://rime.im/docs/"); break; @@ -334,6 +529,8 @@ void WeaselTSF::_HandleLangBarMenuSelect(UINT wID) { open(L"https://rime.im/discuss/"); break; default: + WeaselDebugLog(L"LanguageBar", + L"Forward TrayCommand wID=" + std::to_wstring(wID)); m_client.TrayCommand(wID); break; } @@ -369,19 +566,27 @@ BOOL WeaselTSF::_InitLanguageBar() { com_ptr pLangBarItemMgr; BOOL fRet = FALSE; - if (_pThreadMgr->QueryInterface(&pLangBarItemMgr) != S_OK) + WeaselDebugLog(L"LanguageBar", L"InitLanguageBar start"); + HRESULT hr = _pThreadMgr->QueryInterface(&pLangBarItemMgr); + WeaselDebugLog(L"LanguageBar", + L"InitLanguageBar QueryInterface hr=" + std::to_wstring(hr)); + if (hr != S_OK) return FALSE; if ((_pLangBarButton = new CLangBarItemButton(this, GUID_LBI_INPUTMODE, _cand->style())) == NULL) return FALSE; - if (pLangBarItemMgr->AddItem(_pLangBarButton) != S_OK) { + hr = pLangBarItemMgr->AddItem(_pLangBarButton); + WeaselDebugLog(L"LanguageBar", + L"InitLanguageBar AddItem hr=" + std::to_wstring(hr)); + if (hr != S_OK) { _pLangBarButton = NULL; return FALSE; } _pLangBarButton->Show(TRUE); + WeaselDebugLog(L"LanguageBar", L"InitLanguageBar Show(TRUE)"); fRet = TRUE; return fRet; @@ -390,14 +595,19 @@ BOOL WeaselTSF::_InitLanguageBar() { void WeaselTSF::_UninitLanguageBar() { com_ptr pLangBarItemMgr; - if (_pLangBarButton == NULL) + if (_pLangBarButton == NULL) { + WeaselDebugLog(L"LanguageBar", + L"UninitLanguageBar skipped: no langbar button"); return; + } if (_pThreadMgr->QueryInterface(&pLangBarItemMgr) == S_OK) { + WeaselDebugLog(L"LanguageBar", L"UninitLanguageBar RemoveItem"); pLangBarItemMgr->RemoveItem(_pLangBarButton); } _pLangBarButton = NULL; + WeaselDebugLog(L"LanguageBar", L"UninitLanguageBar done"); } void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { diff --git a/WeaselTSF/WeaselTSF.cpp b/WeaselTSF/WeaselTSF.cpp index 2e7dcffd9..5db2a8551 100644 --- a/WeaselTSF/WeaselTSF.cpp +++ b/WeaselTSF/WeaselTSF.cpp @@ -1,281 +1,392 @@ -#include "stdafx.h" - -#include -#include -#include -#include -#include "WeaselTSF.h" -#include "CandidateList.h" -#include "LanguageBar.h" -#include "Compartment.h" -#include "ResponseParser.h" - -static void error_message(const WCHAR* msg) { - static DWORD next_tick = 0; - DWORD now = GetTickCount(); - if (now > next_tick) { - next_tick = now + 10000; // (ms) - MessageBox(NULL, msg, get_weasel_ime_name().c_str(), MB_ICONERROR | MB_OK); - } -} - -WeaselTSF::WeaselTSF() { - _cRef = 1; - - _dwThreadMgrEventSinkCookie = TF_INVALID_COOKIE; - - _dwTextEditSinkCookie = TF_INVALID_COOKIE; - _dwTextLayoutSinkCookie = TF_INVALID_COOKIE; - _dwThreadFocusSinkCookie = TF_INVALID_COOKIE; - _fTestKeyDownPending = FALSE; - _fTestKeyUpPending = FALSE; - - _fCUASWorkaroundTested = _fCUASWorkaroundEnabled = FALSE; - - _cand = new CCandidateList(this); - - DllAddRef(); -} - -WeaselTSF::~WeaselTSF() { - DllRelease(); -} - -STDAPI WeaselTSF::QueryInterface(REFIID riid, void** ppvObject) { - if (ppvObject == NULL) - return E_INVALIDARG; - - *ppvObject = NULL; - - if (IsEqualIID(riid, IID_IUnknown) || - IsEqualIID(riid, IID_ITfTextInputProcessor)) - *ppvObject = (ITfTextInputProcessor*)this; - else if (IsEqualIID(riid, IID_ITfTextInputProcessorEx)) - *ppvObject = (ITfTextInputProcessorEx*)this; - else if (IsEqualIID(riid, IID_ITfThreadMgrEventSink)) - *ppvObject = (ITfThreadMgrEventSink*)this; - else if (IsEqualIID(riid, IID_ITfTextEditSink)) - *ppvObject = (ITfTextEditSink*)this; - else if (IsEqualIID(riid, IID_ITfTextLayoutSink)) - *ppvObject = (ITfTextLayoutSink*)this; - else if (IsEqualIID(riid, IID_ITfKeyEventSink)) - *ppvObject = (ITfKeyEventSink*)this; - else if (IsEqualIID(riid, IID_ITfCompositionSink)) - *ppvObject = (ITfCompositionSink*)this; - else if (IsEqualIID(riid, IID_ITfEditSession)) - *ppvObject = (ITfEditSession*)this; - else if (IsEqualIID(riid, IID_ITfThreadFocusSink)) - *ppvObject = (ITfThreadFocusSink*)this; - else if (IsEqualIID(riid, IID_ITfDisplayAttributeProvider)) - *ppvObject = (ITfDisplayAttributeProvider*)this; - - if (*ppvObject) { - AddRef(); - return S_OK; - } - return E_NOINTERFACE; -} - -STDAPI_(ULONG) WeaselTSF::AddRef() { - return ++_cRef; -} - -STDAPI_(ULONG) WeaselTSF::Release() { - LONG cr = --_cRef; - - assert(_cRef >= 0); - - if (_cRef == 0) - delete this; - - return cr; -} - -STDAPI WeaselTSF::Activate(ITfThreadMgr* pThreadMgr, TfClientId tfClientId) { - return ActivateEx(pThreadMgr, tfClientId, 0U); -} - -STDAPI WeaselTSF::Deactivate() { - m_client.EndSession(); - - _InitTextEditSink(com_ptr()); - - _UninitThreadMgrEventSink(); - - _UninitKeyEventSink(); - _UninitPreservedKey(); - - _UninitLanguageBar(); - - _UninitCompartment(); - - _UninitThreadMgrEventSink(); - - _pThreadMgr = NULL; - - _tfClientId = TF_CLIENTID_NULL; - - _cand->DestroyAll(); - - return S_OK; -} - -STDAPI WeaselTSF::ActivateEx(ITfThreadMgr* pThreadMgr, - TfClientId tfClientId, - DWORD dwFlags) { - com_ptr pDocMgrFocus; - _activateFlags = dwFlags; - - _pThreadMgr = pThreadMgr; - _tfClientId = tfClientId; - - if (!_InitThreadMgrEventSink()) - goto ExitError; - - if ((_pThreadMgr->GetFocus(&pDocMgrFocus) == S_OK) && - (pDocMgrFocus != NULL)) { - _InitTextEditSink(pDocMgrFocus); - } - - if (!_InitKeyEventSink()) - goto ExitError; - - // if (!_InitDisplayAttributeGuidAtom()) - // goto ExitError; - // some app might init failed because it not provide DisplayAttributeInfo, - // like some opengl stuff - _InitDisplayAttributeGuidAtom(); - - if (!_InitPreservedKey()) - goto ExitError; - - if (!_InitLanguageBar()) - goto ExitError; - - if (!_IsKeyboardOpen()) - _SetKeyboardOpen(TRUE); - - if (!_InitCompartment()) - goto ExitError; - if (!_InitThreadFocusSink()) - goto ExitError; - - _EnsureServerConnected(); - - return S_OK; - -ExitError: - Deactivate(); - return E_FAIL; -} - -STDMETHODIMP WeaselTSF::OnSetThreadFocus() { - std::wstring _ToggleImeOnOpenClose{}; - RegGetStringValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", - L"ToggleImeOnOpenClose", _ToggleImeOnOpenClose); - _isToOpenClose = (_ToggleImeOnOpenClose == L"yes"); - if (m_client.Echo()) { - m_client.ProcessKeyEvent(0); - weasel::ResponseParser parser(NULL, NULL, &_status, NULL, &_cand->style()); - bool ok = m_client.GetResponseData(std::ref(parser)); - if (ok) - _UpdateLanguageBar(_status); - } - return S_OK; -} -STDMETHODIMP WeaselTSF::OnKillThreadFocus() { - _AbortComposition(); - return S_OK; -} -BOOL WeaselTSF::_InitThreadFocusSink() { - com_ptr pSource; - if (FAILED(_pThreadMgr->QueryInterface(&pSource))) - return FALSE; - if (FAILED(pSource->AdviseSink(IID_ITfThreadFocusSink, - (ITfThreadFocusSink*)this, - &_dwThreadFocusSinkCookie))) - return FALSE; - return TRUE; -} -void WeaselTSF::_UninitThreadFocusSink() { - com_ptr pSource; - if (FAILED(_pThreadMgr->QueryInterface(&pSource))) - return; - if (FAILED(pSource->UnadviseSink(_dwThreadFocusSinkCookie))) - return; -} - -STDMETHODIMP WeaselTSF::OnActivated(REFCLSID clsid, - REFGUID guidProfile, - BOOL isActivated) { - if (!IsEqualCLSID(clsid, c_clsidTextService)) { - return S_OK; - } - - if (isActivated) { - _ShowLanguageBar(TRUE); - _UpdateLanguageBar(_status); - } else { - _DeleteCandidateList(); - _ShowLanguageBar(FALSE); - } - return S_OK; -} - -void WeaselTSF::_Reconnect() { - m_client.Disconnect(); - m_client.Connect(NULL); - m_client.StartSession(); - weasel::ResponseParser parser(NULL, NULL, &_status, NULL, &_cand->style()); - bool ok = m_client.GetResponseData(std::ref(parser)); - if (ok) { - _UpdateLanguageBar(_status); - } -} - -static unsigned int retry = 0; - -bool WeaselTSF::_EnsureServerConnected() { - if (!m_client.Echo()) { - _Reconnect(); - retry++; - if (retry >= 6) { - HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerExclusiveMutex"); - const auto count_server_process = []() -> int { - int count = 0; - HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snap == INVALID_HANDLE_VALUE) - return 0; - PROCESSENTRY32 pe; - pe.dwSize = sizeof(pe); - if (Process32First(snap, &pe)) { - do { - if (_wcsicmp(pe.szExeFile, L"WeaselServer.exe") == 0) - count++; - } while (Process32Next(snap, &pe)); - } - CloseHandle(snap); - return count; - }; - if (!m_client.Echo() && GetLastError() != ERROR_ALREADY_EXISTS && - !count_server_process()) { - std::wstring dir = _GetRootDir(); - std::thread th([dir, this]() { - ShellExecuteW(NULL, L"open", (dir + L"\\start_service.bat").c_str(), - NULL, dir.c_str(), SW_HIDE); - // wait 500ms, then reconnect - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - _Reconnect(); - }); - th.detach(); - } - if (hMutex) { - CloseHandle(hMutex); - } - retry = 0; - } - return (m_client.Echo() != 0); - } else { - return true; - } -} +#include "stdafx.h" + +#include +#include +#include +#include +#include "WeaselTSF.h" +#include "CandidateList.h" +#include "LanguageBar.h" +#include "Compartment.h" +#include "ResponseParser.h" + +static std::wstring ModuleFileName(HMODULE module) { + WCHAR path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(module, path, _countof(path)); + if (length == 0) + return L""; + return path; +} + +static void error_message(const WCHAR* msg) { + static DWORD next_tick = 0; + DWORD now = GetTickCount(); + if (now > next_tick) { + next_tick = now + 10000; // (ms) + MessageBox(NULL, msg, get_weasel_ime_name().c_str(), MB_ICONERROR | MB_OK); + } +} + +WeaselTSF::WeaselTSF() { + _cRef = 1; + + _dwThreadMgrEventSinkCookie = TF_INVALID_COOKIE; + + _dwTextEditSinkCookie = TF_INVALID_COOKIE; + _dwTextLayoutSinkCookie = TF_INVALID_COOKIE; + _dwThreadFocusSinkCookie = TF_INVALID_COOKIE; + _fTestKeyDownPending = FALSE; + _fTestKeyUpPending = FALSE; + + _fCUASWorkaroundTested = _fCUASWorkaroundEnabled = FALSE; + + _cand = new CCandidateList(this); + + DllAddRef(); + WeaselDebugLog(L"WeaselTSF", L"constructed module=" + + ModuleFileName(g_hInst) + L" host=" + + ModuleFileName(NULL)); +} + +WeaselTSF::~WeaselTSF() { + WeaselDebugLog(L"WeaselTSF", L"destroyed"); + DllRelease(); +} + +STDAPI WeaselTSF::QueryInterface(REFIID riid, void** ppvObject) { + if (ppvObject == NULL) + return E_INVALIDARG; + + *ppvObject = NULL; + + if (IsEqualIID(riid, IID_IUnknown) || + IsEqualIID(riid, IID_ITfTextInputProcessor)) + *ppvObject = (ITfTextInputProcessor*)this; + else if (IsEqualIID(riid, IID_ITfTextInputProcessorEx)) + *ppvObject = (ITfTextInputProcessorEx*)this; + else if (IsEqualIID(riid, IID_ITfThreadMgrEventSink)) + *ppvObject = (ITfThreadMgrEventSink*)this; + else if (IsEqualIID(riid, IID_ITfTextEditSink)) + *ppvObject = (ITfTextEditSink*)this; + else if (IsEqualIID(riid, IID_ITfTextLayoutSink)) + *ppvObject = (ITfTextLayoutSink*)this; + else if (IsEqualIID(riid, IID_ITfKeyEventSink)) + *ppvObject = (ITfKeyEventSink*)this; + else if (IsEqualIID(riid, IID_ITfCompositionSink)) + *ppvObject = (ITfCompositionSink*)this; + else if (IsEqualIID(riid, IID_ITfEditSession)) + *ppvObject = (ITfEditSession*)this; + else if (IsEqualIID(riid, IID_ITfThreadFocusSink)) + *ppvObject = (ITfThreadFocusSink*)this; + else if (IsEqualIID(riid, IID_ITfDisplayAttributeProvider)) + *ppvObject = (ITfDisplayAttributeProvider*)this; + + if (*ppvObject) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; +} + +STDAPI_(ULONG) WeaselTSF::AddRef() { + return InterlockedIncrement(&_cRef); +} + +STDAPI_(ULONG) WeaselTSF::Release() { + LONG cr = InterlockedDecrement(&_cRef); + + assert(cr >= 0); + + if (cr == 0) + delete this; + + return cr; +} + +STDAPI WeaselTSF::Activate(ITfThreadMgr* pThreadMgr, TfClientId tfClientId) { + WeaselDebugLog(L"WeaselTSF", + L"Activate client_id=" + std::to_wstring(tfClientId)); + return ActivateEx(pThreadMgr, tfClientId, 0U); +} + +STDAPI WeaselTSF::Deactivate() { + WeaselDebugLog(L"WeaselTSF", L"Deactivate start"); + m_client.EndSession(); + + _InitTextEditSink(com_ptr()); + + _UninitThreadMgrEventSink(); + + _UninitKeyEventSink(); + _UninitPreservedKey(); + + _UninitLanguageBar(); + + _UninitCompartment(); + + _UninitThreadMgrEventSink(); + + _pThreadMgr = NULL; + + _tfClientId = TF_CLIENTID_NULL; + + _cand->DestroyAll(); + + WeaselDebugLog(L"WeaselTSF", L"Deactivate done"); + return S_OK; +} + +STDAPI WeaselTSF::ActivateEx(ITfThreadMgr* pThreadMgr, + TfClientId tfClientId, + DWORD dwFlags) { + com_ptr pDocMgrFocus; + _activateFlags = dwFlags; + + WeaselDebugLog(L"WeaselTSF", L"ActivateEx start client_id=" + + std::to_wstring(tfClientId) + L" flags=" + + std::to_wstring(dwFlags) + L" module=" + + ModuleFileName(g_hInst) + L" host=" + + ModuleFileName(NULL)); + + _pThreadMgr = pThreadMgr; + _tfClientId = tfClientId; + + if (!_InitThreadMgrEventSink()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitThreadMgrEventSink"); + goto ExitError; + } + + if ((_pThreadMgr->GetFocus(&pDocMgrFocus) == S_OK) && + (pDocMgrFocus != NULL)) { + _InitTextEditSink(pDocMgrFocus); + } + + if (!_InitKeyEventSink()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitKeyEventSink"); + goto ExitError; + } + + // if (!_InitDisplayAttributeGuidAtom()) + // goto ExitError; + // some app might init failed because it not provide DisplayAttributeInfo, + // like some opengl stuff + _InitDisplayAttributeGuidAtom(); + + if (!_InitPreservedKey()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitPreservedKey"); + goto ExitError; + } + + if (!_InitLanguageBar()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitLanguageBar"); + goto ExitError; + } + + if (!_IsKeyboardOpen()) + _SetKeyboardOpen(TRUE); + + if (!_InitCompartment()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitCompartment"); + goto ExitError; + } + if (!_InitThreadFocusSink()) { + WeaselDebugLog(L"WeaselTSF", L"ActivateEx failed: _InitThreadFocusSink"); + goto ExitError; + } + + _EnsureServerConnected(); + + WeaselDebugLog(L"WeaselTSF", L"ActivateEx succeeded"); + return S_OK; + +ExitError: + WeaselDebugLog(L"WeaselTSF", L"ActivateEx returning E_FAIL"); + Deactivate(); + return E_FAIL; +} + +STDMETHODIMP WeaselTSF::OnSetThreadFocus() { + std::wstring _ToggleImeOnOpenClose{}; + RegGetStringValue(HKEY_CURRENT_USER, L"Software\\Rime\\weasel", + L"ToggleImeOnOpenClose", _ToggleImeOnOpenClose); + _isToOpenClose = (_ToggleImeOnOpenClose == L"yes"); + if (_EnsureServerConnected()) { + bool eaten = false; + m_client.ProcessKeyEvent(0, &eaten); + weasel::ResponseParser parser(NULL, NULL, &_status, NULL, &_cand->style()); + bool ok = m_client.GetResponseData(std::ref(parser)); + if (ok) + _UpdateLanguageBar(_status); + } + return S_OK; +} +STDMETHODIMP WeaselTSF::OnKillThreadFocus() { + _AbortComposition(); + return S_OK; +} +BOOL WeaselTSF::_InitThreadFocusSink() { + com_ptr pSource; + if (FAILED(_pThreadMgr->QueryInterface(&pSource))) + return FALSE; + if (FAILED(pSource->AdviseSink(IID_ITfThreadFocusSink, + (ITfThreadFocusSink*)this, + &_dwThreadFocusSinkCookie))) + return FALSE; + return TRUE; +} +void WeaselTSF::_UninitThreadFocusSink() { + com_ptr pSource; + if (FAILED(_pThreadMgr->QueryInterface(&pSource))) + return; + if (FAILED(pSource->UnadviseSink(_dwThreadFocusSinkCookie))) + return; +} + +STDMETHODIMP WeaselTSF::OnActivated(REFCLSID clsid, + REFGUID guidProfile, + BOOL isActivated) { + if (!IsEqualCLSID(clsid, c_clsidTextService)) { + return S_OK; + } + + WeaselDebugLog(L"WeaselTSF", + L"OnActivated isActivated=" + std::to_wstring(isActivated)); + if (isActivated) { + _ShowLanguageBar(TRUE); + _UpdateLanguageBar(_status); + } else { + _DeleteCandidateList(); + _ShowLanguageBar(FALSE); + } + return S_OK; +} + +bool WeaselTSF::_Reconnect(bool update_tsf_status, bool wait_for_pipe) { + if (!m_client.Reconnect(NULL, wait_for_pipe)) + return false; + _keyEventTestCacheReset.Mark(); + if (!update_tsf_status) + return true; + weasel::ResponseParser parser(NULL, NULL, &_status, NULL, &_cand->style()); + bool ok = m_client.GetResponseData(std::ref(parser)); + if (ok) { + _UpdateLanguageBar(_status); + } + return true; +} + +void WeaselTSF::_RecoverServerAsync() { + if (!weasel::ShouldAutoRecoverService()) { + WeaselDebugLog(L"WeaselTSF", L"skip async recovery: manual exit marked"); + return; + } + + if (InterlockedCompareExchange(&_recoveringServer, 1, 0) != 0) + return; + + AddRef(); + try { + std::thread th([this]() { + try { + _EnsureServerConnected(false); + } catch (...) { + } + InterlockedExchange(&_recoveringServer, 0); + Release(); + }); + th.detach(); + } catch (...) { + InterlockedExchange(&_recoveringServer, 0); + Release(); + } +} + +bool WeaselTSF::_EnsureServerConnected(bool update_tsf_status) { + if (!weasel::ShouldAutoRecoverService()) { + WeaselDebugLog(L"WeaselTSF", L"skip recovery: manual exit marked"); + return false; + } + + if (!m_client.Echo()) { + _Reconnect(update_tsf_status, false); + if (++_serverRecoveryRetry >= 6) { + HANDLE hMutex = CreateMutex(NULL, TRUE, L"WeaselDeployerExclusiveMutex"); + DWORD mutex_error = GetLastError(); + const auto count_server_process = []() -> int { + int count = 0; + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) + return 0; + PROCESSENTRY32 pe; + pe.dwSize = sizeof(pe); + if (Process32First(snap, &pe)) { + do { + if (_wcsicmp(pe.szExeFile, L"WeaselServer.exe") == 0) + count++; + } while (Process32Next(snap, &pe)); + } + CloseHandle(snap); + return count; + }; + if (!m_client.Echo() && mutex_error != ERROR_ALREADY_EXISTS && + !count_server_process()) { + if (!weasel::ShouldAutoRecoverService()) { + WeaselDebugLog(L"WeaselTSF", + L"skip launch during recovery: manual exit marked"); + if (hMutex) { + CloseHandle(hMutex); + } + _serverRecoveryRetry.store(0); + return false; + } + std::wstring dir = _GetRootDir(); + auto restart_server = [dir, this]() { + if (!weasel::ShouldAutoRecoverService()) { + WeaselDebugLog(L"WeaselTSF", + L"skip recovery thread launch: manual exit marked"); + return; + } + if (dir.empty()) + return; + std::wstring server = weasel::ServiceExecutablePath(dir); + ShellExecuteW(NULL, L"open", server.c_str(), + weasel::ServiceRecoveryArgument(), dir.c_str(), + SW_HIDE); + for (int i = 0; i < 20; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (_Reconnect(false, false) && m_client.Echo()) { + m_client.NotifyService( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS); + break; + } + } + }; + if (update_tsf_status) { + AddRef(); + try { + std::thread th([restart_server, this]() { + try { + restart_server(); + } catch (...) { + } + Release(); + }); + th.detach(); + } catch (...) { + Release(); + } + } else { + restart_server(); + } + } + if (hMutex) { + CloseHandle(hMutex); + } + _serverRecoveryRetry.store(0); + } + return (m_client.Echo() != 0); + } else { + _serverRecoveryRetry.store(0); + return true; + } +} diff --git a/WeaselTSF/WeaselTSF.h b/WeaselTSF/WeaselTSF.h index 696d3cce4..807ee3b7d 100644 --- a/WeaselTSF/WeaselTSF.h +++ b/WeaselTSF/WeaselTSF.h @@ -1,236 +1,248 @@ -#pragma once - -#include "Globals.h" -#include -#include - -class CCandidateList; -class CLangBarItemButton; -class CCompartmentEventSink; - -class WeaselTSF : public ITfTextInputProcessorEx, - public ITfThreadMgrEventSink, - public ITfTextEditSink, - public ITfTextLayoutSink, - public ITfKeyEventSink, - public ITfCompositionSink, - public ITfThreadFocusSink, - public ITfActiveLanguageProfileNotifySink, - public ITfEditSession, - public ITfDisplayAttributeProvider { - public: - WeaselTSF(); - ~WeaselTSF(); - - /* IUnknown */ - STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject); - STDMETHODIMP_(ULONG) AddRef(); - STDMETHODIMP_(ULONG) Release(); - - /* ITfTextInputProcessor */ - STDMETHODIMP Activate(ITfThreadMgr* pThreadMgr, TfClientId tfClientId); - STDMETHODIMP Deactivate(); - - /* ITfTextInputProcessorEx */ - STDMETHODIMP ActivateEx(ITfThreadMgr* pThreadMgr, - TfClientId tfClientId, - DWORD dwFlags); - - /* ITfThreadMgrEventSink */ - STDMETHODIMP OnInitDocumentMgr(ITfDocumentMgr* pDocMgr); - STDMETHODIMP OnUninitDocumentMgr(ITfDocumentMgr* pDocMgr); - STDMETHODIMP OnSetFocus(ITfDocumentMgr* pDocMgrFocus, - ITfDocumentMgr* pDocMgrPrevFocus); - STDMETHODIMP OnPushContext(ITfContext* pContext); - STDMETHODIMP OnPopContext(ITfContext* pContext); - - /* ITfTextEditSink */ - STDMETHODIMP OnEndEdit(ITfContext* pic, - TfEditCookie ecReadOnly, - ITfEditRecord* pEditRecord); - - /* ITfTextLayoutSink */ - STDMETHODIMP OnLayoutChange(ITfContext* pContext, - TfLayoutCode lcode, - ITfContextView* pContextView); - - /* ITfKeyEventSink */ - STDMETHODIMP OnSetFocus(BOOL fForeground); - STDMETHODIMP OnTestKeyDown(ITfContext* pContext, - WPARAM wParam, - LPARAM lParam, - BOOL* pfEaten); - STDMETHODIMP OnKeyDown(ITfContext* pContext, - WPARAM wParam, - LPARAM lParam, - BOOL* pfEaten); - STDMETHODIMP OnTestKeyUp(ITfContext* pContext, - WPARAM wParam, - LPARAM lParam, - BOOL* pfEaten); - STDMETHODIMP OnKeyUp(ITfContext* pContext, - WPARAM wParam, - LPARAM lParam, - BOOL* pfEaten); - STDMETHODIMP OnPreservedKey(ITfContext* pContext, - REFGUID rguid, - BOOL* pfEaten); - - // ITfThreadFocusSink - STDMETHODIMP OnSetThreadFocus(); - STDMETHODIMP OnKillThreadFocus(); - - /* ITfCompositionSink */ - STDMETHODIMP OnCompositionTerminated(TfEditCookie ecWrite, - ITfComposition* pComposition); - - /* ITfEditSession */ - STDMETHODIMP DoEditSession(TfEditCookie ec); - - /* ITfActiveLanguageProfileNotifySink */ - STDMETHODIMP OnActivated(REFCLSID clsid, - REFGUID guidProfile, - BOOL isActivated); - - // ITfDisplayAttributeProvider - STDMETHODIMP EnumDisplayAttributeInfo( - __RPC__deref_out_opt IEnumTfDisplayAttributeInfo** ppEnum); - STDMETHODIMP GetDisplayAttributeInfo( - __RPC__in REFGUID guidInfo, - __RPC__deref_out_opt ITfDisplayAttributeInfo** ppInfo); - - ///* ITfCompartmentEventSink */ - // STDMETHODIMP OnChange(_In_ REFGUID guid); - - /* Compartments */ - BOOL _IsKeyboardDisabled(); - BOOL _IsKeyboardOpen(); - HRESULT _SetKeyboardOpen(BOOL fOpen); - HRESULT _GetCompartmentDWORD(DWORD& value, const GUID guid); - HRESULT _SetCompartmentDWORD(const DWORD& value, const GUID guid); - - /* Composition */ - void _StartComposition(com_ptr pContext, - BOOL fCUASWorkaroundEnabled); - void _EndComposition(com_ptr pContext, BOOL clear); - BOOL _ShowInlinePreedit(com_ptr pContext, - const std::shared_ptr context); - void _UpdateComposition(com_ptr pContext); - BOOL _IsComposing(); - void _SetComposition(com_ptr pComposition); - void _SetCompositionPosition(const RECT& rc); - BOOL _UpdateCompositionWindow(com_ptr pContext); - void _FinalizeComposition(); - void _AbortComposition(bool clear = true); - - /* Language bar */ - HWND _GetFocusedContextWindow(); - void _HandleLangBarMenuSelect(UINT wID); - - /* IPC */ - bool _EnsureServerConnected(); - - /* UI */ - void _UpdateUI(const weasel::Context& ctx, const weasel::Status& status); - void _StartUI(); - void _EndUI(); - void _ShowUI(); - void _HideUI(); - com_ptr _GetUIContextDocument(); - - /* Display Attribute */ - void _ClearCompositionDisplayAttributes(TfEditCookie ec, - _In_ ITfContext* pContext); - BOOL _SetCompositionDisplayAttributes(TfEditCookie ec, - _In_ ITfContext* pContext, - ITfRange* pRangeComposition); - BOOL _InitDisplayAttributeGuidAtom(); - - com_ptr _GetThreadMgr() { return _pThreadMgr; } - void HandleUICallback(size_t* const sel, - size_t* const hov, - bool* const next, - bool* const scroll_next); - - private: - /* ui callback functions private */ - void _SelectCandidateOnCurrentPage(const size_t index); - void _HandleMouseHoverEvent(const size_t index); - void _HandleMousePageEvent(bool* const nextPage, bool* const scrollNextPage); - /* TSF Related */ - BOOL _InitThreadMgrEventSink(); - void _UninitThreadMgrEventSink(); - // ITfThreadFocusSink - BOOL _InitThreadFocusSink(); - void _UninitThreadFocusSink(); - DWORD _dwThreadFocusSinkCookie; - - BOOL _InitTextEditSink(com_ptr pDocMgr); - - BOOL _InitKeyEventSink(); - void _UninitKeyEventSink(); - void _ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten); - - BOOL _InitPreservedKey(); - void _UninitPreservedKey(); - - BOOL _InitLanguageBar(); - void _UninitLanguageBar(); - void _UpdateLanguageBar(weasel::Status stat); - void _ShowLanguageBar(BOOL show); - void _EnableLanguageBar(BOOL enable); - - BOOL _InsertText(com_ptr pContext, const std::wstring& ext); - - void _DeleteCandidateList(); - - BOOL _InitCompartment(); - void _UninitCompartment(); - HRESULT _HandleCompartment(REFGUID guidCompartment); - - void _Reconnect(); - std::wstring _GetRootDir(); - - bool isImmersive() const { - return (_activateFlags & TF_TMF_IMMERSIVEMODE) != 0; - } - - com_ptr _pThreadMgr; - TfClientId _tfClientId; - DWORD _dwThreadMgrEventSinkCookie; - - com_ptr _pTextEditSinkContext; - DWORD _dwTextEditSinkCookie, _dwTextLayoutSinkCookie; - BYTE _lpbKeyState[256]; - BOOL _fTestKeyDownPending, _fTestKeyUpPending; - - com_ptr _pEditSessionContext; - std::wstring _editSessionText; - - com_ptr _pKeyboardCompartmentSink; - com_ptr _pConvertionCompartmentSink; - - com_ptr _pComposition; - - com_ptr _pLangBarButton; - - com_ptr _cand; - - LONG _cRef; // COM ref count - - /* CUAS Candidate Window Position Workaround */ - BOOL _fCUASWorkaroundTested, _fCUASWorkaroundEnabled; - - /* Weasel Related */ - weasel::Client m_client; - DWORD _activateFlags; - - /* IME status */ - weasel::Status _status; - - // guidatom for the display attibute. - TfGuidAtom _gaDisplayAttributeInput; - BOOL _async_edit = false; - BOOL _committed = false; - BOOL _isToOpenClose = false; -}; +#pragma once + +#include "Globals.h" +#include +#include +#include + +class CCandidateList; +class CLangBarItemButton; +class CCompartmentEventSink; + +class WeaselTSF : public ITfTextInputProcessorEx, + public ITfThreadMgrEventSink, + public ITfTextEditSink, + public ITfTextLayoutSink, + public ITfKeyEventSink, + public ITfCompositionSink, + public ITfThreadFocusSink, + public ITfActiveLanguageProfileNotifySink, + public ITfEditSession, + public ITfDisplayAttributeProvider { + public: + WeaselTSF(); + ~WeaselTSF(); + + /* IUnknown */ + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + /* ITfTextInputProcessor */ + STDMETHODIMP Activate(ITfThreadMgr* pThreadMgr, TfClientId tfClientId); + STDMETHODIMP Deactivate(); + + /* ITfTextInputProcessorEx */ + STDMETHODIMP ActivateEx(ITfThreadMgr* pThreadMgr, + TfClientId tfClientId, + DWORD dwFlags); + + /* ITfThreadMgrEventSink */ + STDMETHODIMP OnInitDocumentMgr(ITfDocumentMgr* pDocMgr); + STDMETHODIMP OnUninitDocumentMgr(ITfDocumentMgr* pDocMgr); + STDMETHODIMP OnSetFocus(ITfDocumentMgr* pDocMgrFocus, + ITfDocumentMgr* pDocMgrPrevFocus); + STDMETHODIMP OnPushContext(ITfContext* pContext); + STDMETHODIMP OnPopContext(ITfContext* pContext); + + /* ITfTextEditSink */ + STDMETHODIMP OnEndEdit(ITfContext* pic, + TfEditCookie ecReadOnly, + ITfEditRecord* pEditRecord); + + /* ITfTextLayoutSink */ + STDMETHODIMP OnLayoutChange(ITfContext* pContext, + TfLayoutCode lcode, + ITfContextView* pContextView); + + /* ITfKeyEventSink */ + STDMETHODIMP OnSetFocus(BOOL fForeground); + STDMETHODIMP OnTestKeyDown(ITfContext* pContext, + WPARAM wParam, + LPARAM lParam, + BOOL* pfEaten); + STDMETHODIMP OnKeyDown(ITfContext* pContext, + WPARAM wParam, + LPARAM lParam, + BOOL* pfEaten); + STDMETHODIMP OnTestKeyUp(ITfContext* pContext, + WPARAM wParam, + LPARAM lParam, + BOOL* pfEaten); + STDMETHODIMP OnKeyUp(ITfContext* pContext, + WPARAM wParam, + LPARAM lParam, + BOOL* pfEaten); + STDMETHODIMP OnPreservedKey(ITfContext* pContext, + REFGUID rguid, + BOOL* pfEaten); + + // ITfThreadFocusSink + STDMETHODIMP OnSetThreadFocus(); + STDMETHODIMP OnKillThreadFocus(); + + /* ITfCompositionSink */ + STDMETHODIMP OnCompositionTerminated(TfEditCookie ecWrite, + ITfComposition* pComposition); + + /* ITfEditSession */ + STDMETHODIMP DoEditSession(TfEditCookie ec); + + /* ITfActiveLanguageProfileNotifySink */ + STDMETHODIMP OnActivated(REFCLSID clsid, + REFGUID guidProfile, + BOOL isActivated); + + // ITfDisplayAttributeProvider + STDMETHODIMP EnumDisplayAttributeInfo( + __RPC__deref_out_opt IEnumTfDisplayAttributeInfo** ppEnum); + STDMETHODIMP GetDisplayAttributeInfo( + __RPC__in REFGUID guidInfo, + __RPC__deref_out_opt ITfDisplayAttributeInfo** ppInfo); + + ///* ITfCompartmentEventSink */ + // STDMETHODIMP OnChange(_In_ REFGUID guid); + + /* Compartments */ + BOOL _IsKeyboardDisabled(); + BOOL _IsKeyboardOpen(); + HRESULT _SetKeyboardOpen(BOOL fOpen); + HRESULT _GetCompartmentDWORD(DWORD& value, const GUID guid); + HRESULT _SetCompartmentDWORD(const DWORD& value, const GUID guid); + + /* Composition */ + void _StartComposition(com_ptr pContext, + BOOL fCUASWorkaroundEnabled); + void _EndComposition(com_ptr pContext, BOOL clear); + BOOL _ShowInlinePreedit(com_ptr pContext, + const std::shared_ptr context); + void _UpdateComposition(com_ptr pContext); + BOOL _IsComposing(); + void _SetComposition(com_ptr pComposition); + void _SetCompositionPosition(const RECT& rc); + BOOL _UpdateCompositionWindow(com_ptr pContext); + void _FinalizeComposition(); + void _AbortComposition(bool clear = true); + + /* Language bar */ + HWND _GetFocusedContextWindow(); + void _HandleLangBarMenuSelect(UINT wID); + + /* IPC */ + bool _EnsureServerConnected(bool update_tsf_status = true); + + /* UI */ + void _UpdateUI(const weasel::Context& ctx, const weasel::Status& status); + void _StartUI(); + void _EndUI(); + void _ShowUI(); + void _HideUI(); + com_ptr _GetUIContextDocument(); + + /* Display Attribute */ + void _ClearCompositionDisplayAttributes(TfEditCookie ec, + _In_ ITfContext* pContext); + BOOL _SetCompositionDisplayAttributes(TfEditCookie ec, + _In_ ITfContext* pContext, + ITfRange* pRangeComposition); + BOOL _InitDisplayAttributeGuidAtom(); + + com_ptr _GetThreadMgr() { return _pThreadMgr; } + void HandleUICallback(size_t* const sel, + size_t* const hov, + bool* const next, + bool* const scroll_next); + + private: + /* ui callback functions private */ + void _SelectCandidateOnCurrentPage(const size_t index); + void _HandleMouseHoverEvent(const size_t index); + void _HandleMousePageEvent(bool* const nextPage, bool* const scrollNextPage); + /* TSF Related */ + BOOL _InitThreadMgrEventSink(); + void _UninitThreadMgrEventSink(); + // ITfThreadFocusSink + BOOL _InitThreadFocusSink(); + void _UninitThreadFocusSink(); + DWORD _dwThreadFocusSinkCookie; + + BOOL _InitTextEditSink(com_ptr pDocMgr); + + BOOL _InitKeyEventSink(); + void _UninitKeyEventSink(); + bool _TestKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten); + bool _ProcessKeyEvent(WPARAM wParam, LPARAM lParam, BOOL* pfEaten); + bool _CanHandleKeyEvent(); + void _ResetKeyEventTestCacheIfNeeded(); + + BOOL _InitPreservedKey(); + void _UninitPreservedKey(); + + BOOL _InitLanguageBar(); + void _UninitLanguageBar(); + void _UpdateLanguageBar(weasel::Status stat); + void _ShowLanguageBar(BOOL show); + void _EnableLanguageBar(BOOL enable); + + BOOL _InsertText(com_ptr pContext, const std::wstring& ext); + + void _DeleteCandidateList(); + + BOOL _InitCompartment(); + void _UninitCompartment(); + HRESULT _HandleCompartment(REFGUID guidCompartment); + + bool _Reconnect(bool update_tsf_status = true, bool wait_for_pipe = false); + void _RecoverServerAsync(); + std::wstring _GetRootDir(); + + bool isImmersive() const { + return (_activateFlags & TF_TMF_IMMERSIVEMODE) != 0; + } + + com_ptr _pThreadMgr; + TfClientId _tfClientId; + DWORD _dwThreadMgrEventSinkCookie; + + com_ptr _pTextEditSinkContext; + DWORD _dwTextEditSinkCookie, _dwTextLayoutSinkCookie; + BYTE _lpbKeyState[256]; + BOOL _fTestKeyDownPending, _fTestKeyUpPending; + weasel::KeyEventTestCache _keyEventTestCache; + weasel::KeyEventTestCacheReset _keyEventTestCacheReset; + weasel::ActiveKeyDownGuard _activeKeyDownGuard; + DWORD _manualExitCheckTick = 0; + bool _manualExitMarkedForKeyEvents = false; + + com_ptr _pEditSessionContext; + std::wstring _editSessionText; + + com_ptr _pKeyboardCompartmentSink; + com_ptr _pConvertionCompartmentSink; + + com_ptr _pComposition; + + com_ptr _pLangBarButton; + + com_ptr _cand; + + LONG _cRef; // COM ref count + + /* CUAS Candidate Window Position Workaround */ + BOOL _fCUASWorkaroundTested, _fCUASWorkaroundEnabled; + + /* Weasel Related */ + weasel::Client m_client; + DWORD _activateFlags; + + /* IME status */ + weasel::Status _status; + + // guidatom for the display attibute. + TfGuidAtom _gaDisplayAttributeInput; + BOOL _async_edit = false; + BOOL _committed = false; + BOOL _isToOpenClose = false; + std::atomic_uint _serverRecoveryRetry{0}; + volatile LONG _recoveringServer = 0; +}; diff --git a/docs/key-event-latency-pr-review.md b/docs/key-event-latency-pr-review.md new file mode 100644 index 000000000..d8b4b422e --- /dev/null +++ b/docs/key-event-latency-pr-review.md @@ -0,0 +1,209 @@ +# 按键卡顿优化 PR 草稿与 Review + +## PR 标题建议 + +perf: 降低按键处理热路径阻塞和重复刷新 + +## 背景 + +用户反馈的问题不是主动切换输入法状态,而是在进程稍多、服务端忙或 IPC 不稳定时,输入法按键处理会出现明显卡顿,甚至表现为“算法掉了、切不回来,只能重启算法”。这类问题不适合靠新增日志等待复现,优先目标是直接减少按键热路径上的同步等待、重复 IPC、重复 UI/托盘刷新和不必要的全局锁竞争。 + +## 改动范围 + +1. TSF 按键热路径改为使用带状态返回的 `ProcessKeyEvent(key, bool* eaten)`,按键处理失败时不再在当前按键路径里阻塞重连,而是触发异步恢复。 +2. IPC 增加 `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS`,结果同时表达“服务端是否处理过”和“按键是否被吃掉”,避免客户端把 IPC 失败误判成正常未吃键。 +3. `PipeChannel` 增加非阻塞的一次性连接/请求路径,按键与输入位置更新避免反复等待管道、避免热路径 `FlushFileBuffers`。 +4. 输入位置更新增加客户端侧去重,相同压缩坐标不再重复发送 IPC。 +5. 托盘图标刷新增加签名缓存,只有输入法状态或图标相关配置实际变化时才刷新,减少每次按键后读取 `client_app`、创建线程或触发回调。 +6. Rime 响应和 UI 更新复用同一次状态快照,减少一次按键内重复 `get_status`。 +7. TSF 的 `OnTestKey*` / `OnKey*` 对同一方向、同一 `wParam/lParam` 的测试结果做缓存,包含 `eaten=false`,避免 Word 等应用重复测试同一个物理按键时反复打到 Rime。 +8. 服务端 UI 更新增加 context/status diff,内容不变时跳过候选窗刷新。 +9. 服务端 IPC 外层锁收窄:Rime/UI 状态相关命令仍受保护,shutdown、输入位置解码、慢托盘命令不再持有整个 API 外层锁;需要接触 Rime 状态的分支保留短锁。 +10. `TestWeaselIPC` 增加单元入口,覆盖按键结果编码、托盘签名、Rime 状态快照、TSF 测试键缓存 helper、输入位置缓存 helper、UI diff 和 IPC 锁策略。 + +## 预期效果 + +按键路径不再因为服务端暂时不可用、管道繁忙、重复 test-key、重复坐标更新、重复托盘/UI 刷新而放大延迟。进程较多或服务端忙时,输入延迟应从“每次按键都可能等待服务端/管道/UI”收敛到“按键只做必要的 Rime 处理,恢复和慢刷新尽量移出热路径”。 + +恢复期间如果后台线程正在重建 IPC/session,候选选择、候选高亮、翻页、commit/clear、focus/tray 等 UI 入口可能短暂无响应;这是用少量操作失败换取 UI 线程不被慢 IPC 卡住。服务重启后 LanguageBar 也不会由后台线程立刻刷新,而是等下一次 UI 线程事件自然同步状态,避免非 UI 线程触碰 TSF apartment-bound 对象。 + +这个改动不试图改变 Shift、CapsLock 等主动切换语义,也不把用户误触组合键当成根因;它处理的是非主动切换场景下服务端/IPC/刷新路径拖慢或失联后恢复困难的问题。 + +## 兼容性与风险点 + +1. 旧的 `ProcessKeyEvent(key)` 入口保留,新增带状态入口供 TSF 热路径使用,降低调用方兼容风险。 +2. 新 IPC 命令只在客户端和服务端同时更新后使用;失败时客户端返回“未处理”,并触发异步恢复。 +3. 跳过 `FlushFileBuffers` 只用于按键/坐标这类热路径非阻塞请求,普通事务仍保留原同步语义。 +4. UI/托盘去重依赖签名和 diff 的字段完整性,后续如果新增影响显示的字段,需要同步纳入签名或比较逻辑。 +5. 服务端锁收窄需要保证所有接触 Rime API 或共享 UI 状态的路径仍被保护。 +6. 异步恢复路径需要避免和按键热路径并发修改同一个客户端会话状态,也需要保住延迟重连线程里的 TSF 对象生命周期。 + +## 待 Review 清单 + +1. 按键 IPC 失败时,客户端是否能明确区分“服务端未处理”和“服务端处理但没吃键”。 +2. `OnTestKey*` 缓存是否只复用同一个物理事件,不跨方向、不跨不同 `lParam`。 +3. 输入位置去重是否只在发送成功后更新缓存,避免失败后吞掉后续重试。 +4. 托盘签名和 UI diff 是否覆盖所有会影响可见状态的字段。 +5. 服务端锁收窄后,Rime API、session map、UI 状态读写是否仍在锁内。 +6. 异步恢复线程是否有并发保护,避免按键失败时创建大量恢复线程、并发改写 `m_client` 或释放后继续使用 `this`。 +7. 测试是否覆盖新增协议、缓存 helper、diff 和锁策略;真实 TSF 事件序列、真实 IPC 响应体和恢复线程并发仍需要人工/集成验证。 + +## Review 结果 + +本地 review 和独立子代理 review 找到 1 个 Critical、2 个 Important: + +1. Critical:`_RecoverServerAsync()` 的 detached 线程会和按键热路径并发访问同一个 `m_client`;`_EnsureServerConnected()` 里达到重试阈值后还会再创建一个捕获裸 `this` 的延迟重连线程,存在数据竞争和生命周期风险。 +2. Important:输入位置缓存只在发送成功后更新是正确的,但新 session、断开、维护模式切换后没有清空缓存;重连后相同坐标可能不会重新发给服务端。 +3. Important:`PipeChannelBase::_EnsureOnce()` 在 `CreateFile` 成功但 `SetNamedPipeHandleState` 失败时没有关闭已打开句柄,后续可能把未设置 message read mode 的句柄当成已连接。 + +处理结果: + +1. `ClientImpl` 增加内部递归互斥,所有 `m_client` 公共操作串行化;按键处理和输入位置更新使用 `try_lock`,恢复线程占用客户端时热路径直接失败并触发异步恢复,不阻塞等待锁。 +2. `_RecoverServerAsync()` 调用 `_EnsureServerConnected(false)`,后台恢复线程自己完成启动服务和延迟重连,不再额外派生裸 `this` 的内层线程;同步路径保留异步重连时增加 `AddRef/Release`。 +3. `WeaselTSF::AddRef/Release` 改成 `InterlockedIncrement/InterlockedDecrement`,并用本次原子递减返回值决定是否 `delete this`,适配恢复线程跨线程持有引用。 +4. 输入位置缓存抽成 `InputPositionCache`,并在 `Disconnect`、`StartSession`、`EndSession`、维护模式切换时 reset;新增单元测试固定 reset 后同坐标必须重新发送。 +5. `_EnsureOnce()` 只在管道句柄成功切换到 message read mode 后写回线程本地句柄;失败时关闭临时句柄。 + +## 第二轮 Review 发现(基于已完成修复后的代码) + +在前一轮 Critical/Important 都已处理的前提下,又扫了一遍 committed + 未提交改动,发现以下问题: + +### Critical:恢复线程上调用 TSF apartment-bound API,且 `_status` 有 data race + +`WeaselTSF/WeaselTSF.cpp` 中 `_Reconnect()` 末尾会调 `_UpdateLanguageBar(_status)`,而 `_UpdateLanguageBar`(`WeaselTSF/LanguageBar.cpp:403`)会调 `_GetCompartmentDWORD` / `_SetCompartmentDWORD` 以及 `_pLangBarButton->UpdateWeaselStatus`。这些都是 STA apartment 绑定的 TSF 对象。 + +PR 前 `_Reconnect()` 只从 `_ProcessKeyEvent` / `OnSetThreadFocus` 调用,全在 TSF UI 线程上;PR 后 `_RecoverServerAsync()` 起 detached background thread → `_EnsureServerConnected(false)` → `_Reconnect()`,把 TSF API 搬到了非 UI 线程,属于 undefined behavior。 + +同时 `_status` 既被恢复线程上的 parser(`ResponseParser` 持 `&_status`)写入,又被 UI 线程的 `KeyEventSink.cpp:48`、`EditSession.cpp:32-35` 读取,没有同步——data race。 + +候选修法: + +1. 恢复线程只跑 IPC 部分(`Echo` / `Disconnect` / `Connect` / `StartSession`),状态解析和 `_UpdateLanguageBar` 通过 `PostMessage` 回 TSF UI 线程做。 +2. 或者恢复线程里跳过 `_UpdateLanguageBar`,让下一个用户事件自然带出最新状态(接受 1 个 key 的状态延迟)。 +3. 给 `_status` 加短锁,所有 TSF 状态写都 marshal 回 UI 线程。 + +### Important:UI 线程在非按键路径上仍会等恢复线程 + +`WeaselIPC/WeaselClientImpl.cpp` 里只有 `ProcessKeyEvent(KeyEvent, bool*)` 和 `UpdateInputPosition` 用 `try_lock`,其余都用 `std::lock_guard`: + +- `FocusIn` / `FocusOut` +- `CommitComposition` / `ClearComposition` +- `SelectCandidateOnCurrentPage` / `HighlightCandidateOnCurrentPage` / `ChangePage` +- `StartSession` / `EndSession` / `Echo` + +当恢复线程在 `_Reconnect()` 内部阻塞(`WaitNamedPipe` 500ms × N、`ShellExecute` + `sleep(500ms)`、`StartSession` 全程持锁)时,UI 线程上这些入口会同样阻塞——焦点切换、点选候选、commit 上屏都会卡。 + +要么扩大 try_lock 覆盖到这些方法,要么显式声明短时阻塞可接受并加一个上限(如 `try_lock_for(50ms)`)。 + +### Important:`_keyEventTestCache` 在 TestKey 交错时可能让 Rime 重复收到同一个 key + +`include/KeyEvent.h` 的 `KeyEventTestCache` 只存一项。若发生 `OnTestKeyDown(A)` → `OnTestKeyDown(B)` 这种交错(B 覆盖 A),随后 `OnKeyDown(A)` `Matches` 不命中 → 走 `_ProcessKeyEvent(A)` 第二次。 + +这是把原来 `_fTestKeyDownPending`(单态布尔)的行为继承下来,并不是回归。但 review doc 第 2 项点名"OnTestKey\* 缓存是否只复用同一个物理事件"——当前结论是"不会错配,但不防止重复打 Rime"。 + +如果确认 TSF 不会交错 TestKeyDown,需要在 `KeyEventTestCache` 上加注释说明前提;否则扩成 `(wParam,lParam)→eaten` 的小 map。 + +### Minor + +1. `include/RimeWithWeasel.h::RimeUiNeedsUpdate` 按值收 `Context`/`Status`,每次按键深拷贝 4 份(preedit / cinfo / labels / comments)。改成 `const&` 更合适,但前提是把 `WeaselIPCData.h` 里 `Context::operator==/!=` 和 `Status::operator==`(目前签名 `const Status status` 且非 const 成员函数)补成 const。 +2. `WeaselClientImpl.cpp::StartSession` 里 `input_position_cache.Reset()` 调了两次(`_SendMessage` 前后)。`_SendMessage` 不会回调进 cache,只 reset 一次就够,多写一次会让读者怀疑漏了什么。 +3. `RimeWithWeasel.cpp::_UpdateUI` 把 `Status& weasel_status = m_ui->status();` 改成 `Status weasel_status = m_ui->status();`(值拷贝),并新增了 `RimeUiNeedsUpdate` 短路:同 ctx/status 不再 `m_ui->Hide()` + `m_ui->Update()`。如果有依赖每次 Hide 重绘的路径需要手动 smoke。 +4. `WeaselTSF.cpp` 的 `static std::atomic_uint retry` 跨实例共享,多 TSF 实例下失败计数会互相累加。pre-existing,不阻塞此 PR,但顺手改成成员更安全。 +5. `PipeChannel.cpp::_FinalizePipe` 对 client-side handle 调 `DisconnectNamedPipe` 一定失败(仅 server 端合法)。无害——紧接着 `CloseHandle`——但本 PR 在 `_EnsureOnce` 失败路径上新增了一次这种用法,留作日后清理。 + +### 不属于本 PR 范围的观察 + +`_EnsureServerConnected` 的同步 restart_server 分支只在 `reconnect_after_launch_async=false`(恢复线程)时走。若 Critical 按"恢复线程上跳过 TSF UI 更新"修,restart_server 的同步路径需要相应避开 `_UpdateLanguageBar`。 + +## 第二轮 Review 处理结果 + +1. `_Reconnect` 增加 `update_tsf_status` 参数:后台恢复线程只做 IPC 重连和 session 重建,不再解析响应写 `_status`,也不再调用 `_UpdateLanguageBar` / TSF compartment / language bar API。 +2. `_EnsureServerConnected(false)` 用于后台恢复路径;延迟启动服务后的后台 `_Reconnect` 固定走 `update_tsf_status=false`,避免非 UI 线程触碰 TSF apartment-bound 对象。 +3. `ClientImpl` 中按键热路径之外的焦点、候选选择、翻页、commit/clear、旧 `ProcessKeyEvent`、`Echo` 改为 `try_lock`,恢复线程持有客户端锁时这些 UI 入口直接失败返回,不等待恢复锁。 +4. `KeyEventTestCache` 从单槽改为 4 槽固定缓存,并新增 `RemoveMatched()`:交错 `OnTestKeyDown(A)` / `OnTestKeyDown(B)` 后,A/B 都可分别命中,不会错配,也减少重复打到 Rime。 +5. `RimeUiNeedsUpdate` 改为按 `const&` 比较;同步把 `WeaselIPCData` 里的相关比较操作补成 `const`,避免每次按键做多份 context/status 深拷贝。 +6. `StartSession` 去掉重复 `input_position_cache.Reset()`;恢复重试计数从静态全局改成 TSF 实例成员,并在连接恢复成功时清零。 + +## 第三轮自查与补充处理 + +第二轮处理后又复查了 `StartSession`、`EndSession`、`GetResponseData`、`TrayCommand` 等非按键路径,发现还有一个更深的阻塞来源:`Echo()` 虽然已经改为 `try_lock`,但拿到锁后仍走阻塞 `Transact`;同时 `_EnsureServerConnected()` 在 echo 失败后会进入 `_Reconnect()`,旧的重连序列会拆成 `Disconnect` / `Connect` / `StartSession` 三个公开调用,其中 `Disconnect()` 仍是阻塞锁,`Connect()` 也可能等待管道。 + +补充处理: + +1. `PipeChannel` 增加 `TryConnect()`,复用 `_EnsureOnce()` 做一次性连接;`TryTransact()` 在一次性连接失败时主动清空写缓冲,避免 `StartSession` 这种带 body 的请求失败后把旧 body 留到下一次发送。 +2. `ClientImpl` 增加 `Reconnect(launcher, wait_for_pipe)`,用一次 `try_lock` 包住断开、清 session、连接和重建 session;UI 线程传 `wait_for_pipe=false`,只尝试一次管道连接,后台恢复传 `true`,允许在后台等待忙碌管道。 +3. `_Reconnect(update_tsf_status=true)` 改为 UI 侧非阻塞重连尝试;后台恢复仍然不解析响应、不写 `_status`、不调用 `_UpdateLanguageBar`。 +4. `Echo()` 改为 `_TrySendMessage(WEASEL_IPC_ECHO)`,不再通过失败路径触发阻塞重连。 +5. 旧 `ProcessKeyEvent(key)`、commit/clear、候选选择/高亮/翻页、焦点进出、托盘命令、`StartSession`、`EndSession` 改为 `TryTransact` 路径;恢复线程持锁时先快速返回,拿到锁后也尽量不走 `WaitNamedPipe` 和 `FlushFileBuffers`。 +6. `StartMaintenance` / `EndMaintenance` / `ShutdownServer` 仍保留同步发送语义,因为这些不是 TSF 按键/UI 热路径,并且部署/退出流程需要明确完成。 + +## 第四轮 Review 发现(在第三轮补丁基础上再扫一遍) + +第二轮 + 第三轮处理已经把 Critical、所有 Important 都修了;这一轮没有发现新的 Critical 或 Important,只剩下 Minor / design / doc 级别的问题: + +### Minor:恢复成功后 `_keyEventTestCache` 没清空,可能返回陈旧 `eaten` + +`WeaselTSF::_Reconnect(...)` 重连成功路径上没有 `_keyEventTestCache.Clear()`。如果服务挂掉前 cache 里残留了"上一次 successful processed"的 entry(例如 OnTestKeyDown 已 Store 但 OnKeyDown 还没消费就断连),重启后 Rime 状态可能变(例如 ascii_mode 切换),下一次 `OnTestKeyDown(same wParam, lParam)` 命中残留 entry 直接 `return cache.Eaten()`,不再打到 Rime。 + +实际残留窗口很小(一对 TestKey/KeyDown 之间),但严格起见值得在 `_Reconnect` 成功分支末尾加一行 `_keyEventTestCache.Clear()`。 + +### Minor:`KeyEventTestCache::Matches` 是 const 但写 `mutable matched_entry`,有调用顺序耦合 + +`Matches(...) const` 写入 `mutable int matched_entry`,`Eaten()` / `RemoveMatched()` 都依赖上一次 `Matches` 留下的下标。隐含约束:"`Matches` 之后必须紧接 `Eaten` / `RemoveMatched`,中间不允许 `Store` / `Clear`"。`KeyEventSink.cpp` 当前用法满足这个约束,但接口本身没强制,后续改动容易踩。 + +建议(择一,非阻塞): +- `Matches` 返回 `std::optional`,调用方持有 index 后再问 `Eaten/Remove`; +- 或者合并成 `bool TakeEaten(bool is_key_up, WPARAM, LPARAM, BOOL& out_eaten)`,一步完成查找 + 取值 + 移除;TestKey 路径单独走只查询不移除的 `PeekEaten`。 + +### Minor:UI 线程和恢复线程并发 `++_serverRecoveryRetry`,可能更早触发 `restart_server` + +`OnSetThreadFocus` → `_EnsureServerConnected(true)`(UI 线程)和 `_RecoverServerAsync` → `_EnsureServerConnected(false)`(恢复线程)都会 `++_serverRecoveryRetry`。两条线并发 ++ 会让 retry 比预期更早到 6、提前触发 `restart_server`。`restart_server` 内有 `WeaselDeployerExclusiveMutex` 防重复 ShellExecute,所以不致命,但破坏了"连续失败 6 次才重启"的语义。 + +如要保留语义,可以把 retry++ 限制在恢复线程独占:UI 侧的 `_EnsureServerConnected(true)` 只做 Echo + 非阻塞 Reconnect,不计数;计数由 `_RecoverServerAsync` 持有的恢复线程独自完成。 + +### Minor / nit:`Connect()` 没加锁,与其他公共方法不一致 + +`ClientImpl::Connect(...)` 直接 `return channel.Connect();`,没 `lock_guard`。实际只在 TSF 初始化路径调,没有并发问题;但与 `Disconnect` / `Reconnect` / `ShutdownServer` 都加锁的约定不一致。一致性 nit。 + +### Doc:恢复期间候选窗操作也会"无响应" + +恢复线程持锁的几百 ms 到数秒内,`SelectCandidateOnCurrentPage` / `HighlightCandidateOnCurrentPage` / `ChangePage` / `CommitComposition` 等 UI 入口都会 try_lock 失败立即返回 false。用户感知 = "字符以英文落地 + 候选窗点了没反应、翻页失败、commit 不上屏"。 + +这是用"切英文 + 候选无响应"换"UI 线程不卡"的预期 trade-off;建议在 PR doc 的"预期效果"里写明,避免后续被当成新 bug 反馈。 + +### Doc:`restart_server` 永远走 `_Reconnect(false)`,重启后 LanguageBar 不会立刻刷 + +`restart_server` lambda 内硬编码 `_Reconnect(false)`,无论从 UI 线程派生的子线程还是恢复线程触发都不会更新 `_status` / `_UpdateLanguageBar`。这是 Critical 修复的延伸(restart 永远在非 UI 线程跑)。结果:服务重启后 LanguageBar 状态要等下一次按键触发的 `_UpdateComposition` 才更新一次。 + +属于已知 trade-off;如果觉得 LanguageBar 滞后让人困惑,可以在恢复完成后用 `PostMessage` 把一个 refresh 请求 marshal 回 TSF UI 线程做。 + +### Nit:`Reconnect` 失败路径已 `channel.Disconnect()`,但语义没在头注释里写明 + +`ClientImpl::Reconnect()` 在 `channel.Disconnect()` 之后才尝试 `Connect/TryConnect`,失败返回 false 时 channel 保持 disconnected——这是预期的(下次按键 / `OnSetThreadFocus` 自然触发新一轮),但 `WeaselIPC.h::Client::Reconnect` 头注释没说"失败后客户端不可用直到下次主动触发"。加一行头注释可读性更好。 + +## 第四轮 Review 处理结果 + +1. 已处理 `_keyEventTestCache` 的陈旧结果风险,但没有在后台恢复线程直接清 cache。实现为 `KeyEventTestCacheReset` 原子 pending 标记:`_Reconnect` 成功后只 `Mark()`,下一次 `OnTestKeyDown` / `OnKeyDown` / `OnTestKeyUp` / `OnKeyUp` 进入 TSF UI 线程时再 `Apply()` 清空 cache,避免把 UI 线程状态带回后台恢复线程。 +2. 新增单元测试覆盖 reset helper:标记 pending 后会清空已有 test-key cache,重复 apply 不会误报。 +3. 明确保留恢复期间 UI 入口 `try_lock` 快速失败的 trade-off:恢复线程持锁期间,候选选择、候选高亮、翻页、commit/clear、focus/tray 等操作可能短暂无响应;这是用少量操作丢失换取 UI 线程不被慢 IPC 卡住。 +4. 明确 `restart_server` 后 LanguageBar 不会由后台线程立刻刷新;后台恢复只重建 IPC/session,语言栏状态等下一次 UI 线程事件自然刷新,避免再次触碰 TSF apartment-bound 对象。 + +其余 nit(`Matches` 接口形态、retry 计数语义、`Connect` 一致性、`Reconnect` 注释)暂不处理,不阻塞当前卡顿优化 PR。 + +## 本轮状态 + +当前只准备本地 Markdown、修复 review 发现的问题并复验;不推送,不创建 PR。第二轮 review 发现的 Critical 已处理;第三轮补掉了 `Echo` / 重连序列 / 非按键 UI IPC 的剩余阻塞风险;第四轮最接近真实行为问题的 test-key cache 陈旧风险已处理。剩余主要风险是真实 TSF apartment、真实 IPC 慢响应和候选窗 smoke 仍需要人工验证。 + +## 验证命令 + +第四轮补充处理后已重新复验: + +```powershell +MSBuild test\TestWeaselIPC\TestWeaselIPC.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 +.\x64\Release\TestWeaselIPC.exe /unit +MSBuild WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 +MSBuild WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 +git diff --check +``` + +其中 `InputPositionCache` 的测试先以缺少 helper 编译失败,再实现 helper 和 reset 逻辑后通过。 diff --git a/docs/key-event-latency-pr.md b/docs/key-event-latency-pr.md new file mode 100644 index 000000000..7126becbd --- /dev/null +++ b/docs/key-event-latency-pr.md @@ -0,0 +1,175 @@ +# perf: 降低按键延迟并修复服务状态通知 + +## 背景 + +这批改动针对两个用户可感知的问题: + +1. 输入法在服务端忙、pipe 不稳定、进程较多时出现按键卡顿,严重时表现为“算法掉了、切不回来,只能重启算法”。 +2. 部署、退出算法服务、重启算法服务等状态变化缺少可靠的托盘提示,用户无法判断操作是否完成。 + +这里不把 Shift、CapsLock 等主动切换行为当成根因,也不改变它们的既有语义。本轮没有保留 JetBrains/IDEA/Rider 专用分支,也没有引入 120ms/15ms 之类按键延迟 hack。 + +## 主要改动 + +### 1. 按键 IPC 语义更明确 + +- 新增 `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS`。 +- 服务端返回值同时表达: + - 服务端是否实际处理过该按键。 + - Rime 是否吃掉该按键。 +- 吃键语义补充 composing 前后状态:如果当前键让 Rime 从 composing 变成非 composing,即使没有 commit、`handled=false`,也视为输入法处理过,避免 Backspace 清掉最后一个编码字符时被宿主误处理。 +- TSF 按键热路径改用 `ProcessKeyEvent(key, bool* eaten)`,避免把 IPC 失败误判成“服务端处理了但没吃键”。 +- IPC 失败时当前按键路径快速返回,并触发后台恢复,不在当前输入线程同步重连。 +- 旧 `WEASEL_IPC_PROCESS_KEY_EVENT` 也补了 session 守卫,和新入口语义保持一致。 + +### 2. 降低按键热路径阻塞 + +- `PipeChannel` 增加一次性连接/请求路径: + - `TryConnect()` 只尝试一次连接,不进入 `WaitNamedPipe` 循环。 + - `TryTransact()` 跳过热路径 `FlushFileBuffers`。 + - 一次性连接失败或发送异常时清空写缓冲,避免带 body 的请求污染下一次发送。 +- `ClientImpl` 增加内部互斥保护 `session_id`、pipe channel、输入位置缓存等共享状态。 +- 按键、输入位置、候选、焦点、托盘等 UI 路径尽量使用 `try_lock` 和 `TryTransact`,恢复线程持锁时快速失败,不等待恢复锁。 +- 服务端 key IPC 不再走阻塞 `g_api_mutex`,改为 try-lock;锁忙时立即返回未处理,让客户端把当前键交回宿主。 +- `Echo()` 改走非阻塞 `_TrySendMessage(WEASEL_IPC_ECHO)`,避免 echo 失败路径自己造成阻塞重连。 + +### 3. 后台恢复不触碰 TSF apartment 对象 + +- `_Reconnect(update_tsf_status, wait_for_pipe)` 支持区分 UI 线程和后台恢复线程。 +- 后台恢复固定不解析响应、不写 `_status`、不调用 `_UpdateLanguageBar`,避免跨线程访问 TSF compartment 或 language bar 对象。 +- `AddRef/Release` 改为 `InterlockedIncrement/InterlockedDecrement`,恢复线程持有 TSF 对象引用时使用原子引用计数。 +- 恢复重试计数从静态全局改为 TSF 实例成员,避免多个实例互相影响。 + +### 4. 避免按键响应丢失和 Backspace 状态滞后 + +- `ClientImpl::GetResponseData()` 不再抢 `client_mutex`;response buffer 是 `PipeChannel` 的 thread-local 数据,不应被后台恢复线程阻塞。 +- `_ProcessKeyEvent()` 在 `ProcessKeyEvent` 成功后立即解析一次 status-only 响应,提前刷新 `_status.composing`。 +- 本地 `_AbortComposition()` 会同步把 `_status.composing` 置为 false,并清理 TestKey cache,避免 TSF 本地状态残留影响下一次按键预测。 +- 这样可以缩小 TestKey/Backspace 使用陈旧 composing 状态的窗口,降低“刚进入组合后 Backspace 删到宿主文本”或“首个编码字符 Backspace 删除不掉”的风险。 + +### 5. TestKey 单次处理缓存,重复 keydown 防护更保守 + +- 某些宿主可能只稳定触发 `OnTestKeyDown` / `OnTestKeyUp`,不保证后续一定按预期触发正式 `OnKeyDown` / `OnKeyUp`。 +- TestKey 回调现在可以真正处理一次 Rime 按键,并缓存结果;如果随后正式 Key 回调到来,直接复用缓存结果,不再把同一个物理按键送给 Rime 第二次。 +- TestKey cache 匹配时忽略 repeat count / previous-key-state 等 lParam 差异,适配部分宿主 TestKey 与 Key 回调参数不完全一致的情况。 +- TestKeyDown 会清理上一轮残留的 KeyUp cache,TestKeyUp 会清理同一物理键的 KeyDown cache;即使宿主没有发送正式 KeyUp,也不会让下一次 Shift 切换命中旧缓存而跳过 Rime。 +- 手动退出算法服务后,TestKey 和正式 Key 路径都透明放行,不再因为中文模式预测吃字母而导致退出后无法输入英文字母。 +- `ActiveKeyDownGuard` 改为只判断是否应 suppress,不再在判断时自动记录。 +- 正式 `OnKeyDown` 只有在 `_ProcessKeyEvent()` 成功且 `pfEaten=TRUE` 后才记录 active key,避免 disabled、unknown、pass-through key 的第二次回调被误吞。 + +### 6. 输入位置和 UI/托盘刷新去重 + +- 新增 `InputPositionCache`,相同压缩坐标不重复发送 `WEASEL_IPC_UPDATE_INPUT_POS`。 +- 只有发送成功后才更新坐标缓存;失败后允许下一次重试。 +- `Disconnect`、`StartSession`、`EndSession`、维护模式切换后 reset 缓存,避免新 session 漏发首个相同坐标。 +- 托盘刷新增加 `RimeTrayIconSignature`,只有影响托盘状态的字段实际变化时才刷新。 +- `_Respond` 和 `_UpdateUI` 复用 Rime status/context 快照,减少一次按键内重复 `get_status` / `get_context`。 +- UI 更新增加 context/status diff,内容不变时跳过候选窗 `Hide/Update`。 +- 抑制 `ascii_mode` / `!ascii_mode` 的候选窗内联图标提示;Shift 中英切换不再在输入位置弹出只含图标的状态窗,部署、方案、全半角等其他通知不受影响。 + +### 7. 服务通知和托盘提示补齐 + +- 部署完成/失败通过 `EndMaintenance(result)` 回传给 server,并显示托盘气泡。 +- `EndMaintenance(result)` 的部署结果通知不再被 `m_disabled` 门控;即使维护状态没有成功进入,也不会吞掉部署结果提示。 +- `SyncUserData()` 失败早退前会尝试 `EndMaintenance()`,避免 server 停留在 disabled/维护状态。 +- 退出算法服务、正在重启算法服务、重启成功、重启失败都映射到托盘提示。 +- `WEASEL_IPC_NOTIFY_SERVICE` 和 shutdown notification 不再从 pipe worker 线程直接操作托盘,改为 `PostMessage(WM_WEASEL_SERVICE_NOTIFY)` 收口到 server 消息线程。 +- `OnServiceNotifyMessage` 内部持 `g_api_mutex` 读取 handler,避免和系统结束会话时的 finalize/置空并发。 + +### 8. 退出、重启、恢复语义修正 + +- 手动退出算法服务会写 manual-exit flag,并注销系统应用恢复。 +- `/recover` 在 manual-exit flag 存在时不启动服务,避免用户手动退出后被自动恢复拉起。 +- `/restart-manual` 和 `/startup` 会清除 manual-exit flag,用于明确的手动恢复。 +- 旧 `/restart` 只在明确来自交互入口时兼容成手动重启;父进程判断从黑名单改为白名单。 +- 新增非手动 `/stop` / `/stop-service`,安装器、卸载器和 stop 脚本使用 `/stop`,避免安装/卸载流程污染 manual-exit flag。 +- SCM `WeaselService::Stop()` 改用 `WEASEL_IPC_SHUTDOWN_REASON_STOP`,不再写 manual-exit flag。 +- LanguageBar 退出不再先写 manual-exit flag;成功退出由 server 端 EXIT 路径统一写。 +- 控制命令路径对退出/重启增加有界连接重试,pipe 瞬时不可连时不再只试一次就静默丢命令。 + +### 9. 安装和 IPC 稳定性修复 + +- `copy_file()` 改为安全替换: + - 先验证源文件存在。 + - 先复制到 `dest.new`。 + - 即时替换失败时只安排 delayed replace。 + - 任意失败都保留原目标文件,不再先移动旧 DLL。 +- `FindSession()` 改为 `map.find()` 无副作用查询,不再通过 `operator[]` 插入 stale session。 +- `_UpdateUI(0)` 不再创建 session 0,也不再对 session 0 调 Rime option。 +- `PipeChannel::_Send()` 重连后使用新 pipe handle 重发,不再拿旧 handle 重发。 +- `PipeChannelBase::_Connect()` 在 `SetNamedPipeHandleState` 失败时关闭已打开 pipe,避免句柄泄漏。 +- `PipeServer::Listen()` 创建 per-connection worker 后显式 `detach()`,避免局部 `boost::thread` 析构时留下 joinable 线程风险。 + +### 10. 热路径日志和清理 + +- `ShouldTraceKeyEvents()` 提到 `WeaselUtility.h` 并缓存环境变量结果。 +- TSF key trace 调用改为宏门控,`WEASEL_TRACE_KEY_EVENTS` 关闭时不再构造每键 trace 字符串。 +- Rime `!handled && has_commit` 的每键 debug log 改为受 `WEASEL_TRACE_KEY_EVENTS` 门控。 +- 移除临时 `idea-keytrace-20260521` 调试标记。 +- 删除 `output/install.bat` 里 IMM 时代遗留的 `regsvr32` 注释。 +- `imesetup.cpp` 的 IMM 注释改成当前 TSF-only 行为。 +- `include/resource.h` 的 `_APS_NEXT_COMMAND_VALUE` 与 `WeaselServer/resource.h` 对齐。 + +### 11. 测试补充 + +`TestWeaselIPC /unit` 增加或更新覆盖: + +- 按键 IPC 结果编码和 `ShouldEatKeyEvent(TRUE, TRUE)`。 +- `ShouldEatKeyEvent` 覆盖 composing 被当前键结束时仍应吃键。 +- TestKey 预测、单次处理 cache、lParam 容错匹配和 cache reset。 +- TestKey cache 按物理键清理上下沿残留,覆盖没有正式 Key 回调消费缓存时的下一轮 Shift。 +- 本地 composition abort 后 `_status.composing` 清理。 +- 重复 keydown guard 的显式 `Remember()` / suppress 行为。 +- 输入位置缓存 reset。 +- 托盘签名、服务通知、维护结果映射。 +- Rime status 快照转换。 +- UI diff 判断。 +- `ascii_mode` / `!ascii_mode` 内联提示抑制,确认不会误伤部署、方案、全半角通知。 +- 服务端 key IPC 不走阻塞 outer lock。 +- `/restart` 父进程白名单、`/stop`、manual-exit flag 语义。 +- 缺失 server 时 `TryConnect()` 快速失败。 + +## 预期效果 + +- 服务端 busy、pipe 瞬时不可用、后台恢复进行中时,TSF 当前按键线程不再被同步重连、阻塞 outer lock 或恢复锁拖住。 +- TestKey 和正式 Key 回调不会重复推进 Rime 状态,减少重复回调导致的双击、吞键和组合状态错乱。 +- 宿主只发 TestKey、不发正式 KeyUp 时,下一次 Shift 仍会进入 Rime 处理,不会被上一轮缓存吞掉。 +- Backspace/Delete/Enter 等编辑键在组合状态下更容易拿到及时的 composing 状态;部分宿主里“输入第一个编码字符后 Backspace 删除不掉”的场景应被修复。 +- 手动退出算法服务后,选中小狼毫时普通字母应交回宿主输入,不再被 TSF 预测路径吞掉。 +- 部署完成/失败、退出服务、重启服务、重启成功/失败都有托盘提示。 +- Shift 切换中英只改变输入状态,不再额外弹出输入位置附近的中英状态图标。 +- 手动退出后不会被自动恢复误拉起;安装/卸载/SCM stop 不会污染手动退出标记。 +- 安装复制失败不会先破坏现有 DLL。 + +## 行为取舍和残余风险 + +- 客户端 pipe 事务已有连接时仍是同步 `WriteFile/ReadFile` round-trip。本轮通过服务端 key path try-lock 处理最直接的 `g_api_mutex` 阻塞源;如果 Rime 在真正处理某个 key 时长时间卡住,仍可能等待该次响应。彻底解决需要 overlapped I/O + timeout/cancel 的更大 IPC 改造。 +- `_UpdateUICallback` / tray refresh 的数据流还没有整体迁到 UI 线程快照模式。本轮只修了 `NotifyService(DWORD)` 这条跨线程托盘气泡路径;`Refresh()` 涉及 `UIStyle`/`Status` 中的字符串引用,不能只靠 `PostMessage` 简单搬线程。 +- `WeaselSetup::CustomInstall` 仍用后台线程串联 `/stop`、`/restart-manual`、`/deploy`,保留固定 sleep。要彻底消除 sleep,需要新增“等待新 server ready 后再 deploy”的专用命令或 IPC handshake。 +- 真机 TSF smoke 仍然必要:首个编码字符 Backspace 删除、Backspace 删除 preedit、IDEA/Rider 不再双击、托盘退出后手动重启、部署完成提示都需要用打包产物验证。 + +## 验证 + +已执行: + +```powershell +MSBuild .\test\TestWeaselIPC\TestWeaselIPC.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +.\x64\Release\TestWeaselIPC.exe /unit + +MSBuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselSetup\WeaselSetup.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselDeployer\WeaselDeployer.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselDeployer\WeaselDeployer.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal + +git diff --check +``` + +结果: + +- `TestWeaselIPC.exe /unit` 输出 `No errors detected.`。 +- `WeaselServer` x64/Win32、`WeaselTSF` x64/Win32、`WeaselSetup` Win32、`WeaselDeployer` x64/Win32 均构建成功。 +- 构建仍有既有类型转换 warning,未出现新的 error。 +- `git diff --check` 无 whitespace error,只输出当前工作区 LF/CRLF 归一化提示。 diff --git a/docs/weasel-change-review-claude.md b/docs/weasel-change-review-claude.md new file mode 100644 index 000000000..938492cf1 --- /dev/null +++ b/docs/weasel-change-review-claude.md @@ -0,0 +1,365 @@ +# Weasel `fix-key-event-latency` Review(Claude) + +本文档是基于 `docs/weasel-change-review-reference.md` 给出的检查路线,对当前 `fix-key-event-latency` 分支(含未提交工作区改动)做的全面 review 结论。 + +- 工作区 diff:32 文件,+2521 / -741 +- 已提交 commits:6 个 perf 提交(master..HEAD) +- review 维度:reuse / quality / efficiency / correctness / doc-criteria +- review 分工:热路径&线程、IPC 协议&UI 去重、服务生命周期&托盘、安装/打包/测试 +- 已交叉对照 `docs/weasel-change-review-codex.md`,新增/修正项见末尾"与 Codex review 交叉对照"段。 + +## 通过标准对照 + +引用自 reference 文档"Review 通过标准"。 + +| 标准 | 状态 | 说明 | +|---|---|---| +| 按键热路径没有新的同步等待、阻塞重连或无界锁等待 | ⚠️ | 客户端侧改进到位(try_lock + TryTransact + _EnsureOnce + buffer 清理);但服务端 `OnKeyEventWithStatus` 仍在 `g_api_mutex` 内,慢任务会卡。详见 C6。 | +| TestKey 不再推进 Rime 状态 | ✅ | `OnTestKeyDown`/`OnTestKeyUp` 只读 `_status` 和调 `ShouldEatTestKeyEvent`,不调 `process_key`,不改 composition。 | +| Backspace 回归测试存在且语义正确 | ⚠️ | 覆盖了无组合/有组合/CapsLock/Ctrl-shortcut/cache/repeat-guard/restart 命令行 7 大类,差一个 `ShouldEatKeyEvent(processed=TRUE, commit=TRUE)` 断言。 | +| 服务手动退出、自动恢复、手动重启三者语义不互相打架 | ⚠️ | 存在两处真实风险,见 H1、H2。 | +| 托盘提示覆盖部署完成/失败、退出、重启中、重启成功、重启失败 | ✅ | 五条都映射到正确文案和图标类型,server 端 1200ms 延迟保留 balloon 显示时间,三语种字符串资源齐全。 | +| 单元测试通过,TSF/Server/Setup 目标可构建 | — | 未运行验证命令;review 仅基于源码。 | +| 未出现 TSF apartment 跨线程、IPC buffer 污染、manual-exit flag 竞态 | ✅ | 这三类高风险未发现。后台恢复线程严格 `_Reconnect(false, false)` 不触 `_status` 和 `_UpdateLanguageBar`;`AddRef/Release` 用 `InterlockedIncrement/Decrement`。 | + +整体判断(初版):没有 blocker,若干高优先项建议合并前处理。 + +**复评(结合 Codex review 后)**:Codex 暴露的 C1(安装破坏 DLL)、C2(响应丢失)、C3(session map 污染)、C6(服务端 outer lock 仍卡按键)属于硬问题,本文末尾"与 Codex review 交叉对照"段已收编并给出建议处理顺序。建议把 C1、C6 当成 blocker 候选 —— +- C1:让用户在升级失败时丢失现网 IME,等同回滚不了的故障。 +- C6:分支名是 `fix-key-event-latency`,服务端阻塞这一段不修则未达到主目标。 + +## 高优先(建议合并前修) + +### H1 — `IsScriptLikeRestartParent` 方向反了 + +- 位置:`include/WeaselIPC.h:294-311` +- 严重度:high +- 维度:correctness / doc-criteria +- 现状:用黑名单枚举 `cmd.exe / powershell.exe / pwsh.exe / conhost.exe / windowsterminal.exe / wt.exe / weaselserver.exe`,其余一律算"交互父进程",会清掉 manual-exit flag。 +- 问题:reference 文档要求"旧 `/restart` 只在明确来自交互父进程时兼容成手动重启;脚本/自动恢复场景不能误清 flag"。当前实现意味着任何不在黑名单里的父进程(`notepad.exe`、第三方 launcher、调度任务、未知 service host……)都会被当成手动重启。 +- 唯一兜底:`parent_path` 为空时被当 SCM 恢复,不清 flag。 +- 建议:反转为白名单(`explorer.exe` / `taskmgr.exe` / `WeaselDeployer.exe` / `WeaselSetup.exe` 等已知交互入口)。在 `TestWeaselIPC` 加一个 `notepad.exe` 父进程的 case 锁住意图。 + +### H2 — `WeaselService::Stop` 无条件写 manual-exit flag + +- 位置:`WeaselServer/WeaselService.cpp:90` +- 严重度:high +- 维度:correctness / doc-criteria +- 现状:所有走 SCM `Stop()` 的路径都会 `MarkServiceManualExit()`。 +- 问题:reference 文档只要求"用户手动退出会写 manual-exit flag",而 SCM `Stop` 在管理员命令、卸载、机器重启等场景下都会被调,写入 flag 后下次 `/recover` 会被压制,需要用户手动 `/restart-manual` 才能恢复。 +- 建议:拆 shutdown reason,只在托盘 quit / IPC EXIT 路径里写 flag。SCM `Stop` 走一个不写 flag 的 reason(或者根本不调用这个 helper)。 + +### H3 — `WEASEL_IPC_NOTIFY_SERVICE` 在非 UI 线程触发 Shell_NotifyIcon + +- 位置:`WeaselIPCServer/WeaselServerImpl.cpp:188-198, 275-307` +- 严重度:high +- 维度:correctness + +#### 现状 + +- `ServerImpl` 自己是 `CWindowImpl`,HWND 在 `ServerImpl::Run` 跑的 `CMessageLoop` 线程上创建。 +- pipe 收发跑在另一条 `boost::thread pipeThread` 上。 +- `PipeMessageNeedsOuterApiLock` 把 `WEASEL_IPC_NOTIFY_SERVICE` 和 `WEASEL_IPC_SHUTDOWN_SERVER` 都判成不需要 outer lock。 +- 这两条命令最终都会调 `m_pRequestHandler->NotifyService(notification)`,server 端注册的回调是 `[this](DWORD n){ tray_icon.ShowServiceNotification(n); }`(`WeaselServerApp.cpp:45-48`)。 +- 结果:`Shell_NotifyIcon` 在 pipe worker 线程被调用,且跟拿 `g_api_mutex` 的 UI 路径可以并发。`Shell_NotifyIcon` 文档要求由建图标的 window owner 线程调,违反这一条。 + +#### 修法:PostMessage 收口到 server message loop 线程 + +`ServerImpl` 已经是 `CWindowImpl`,直接在 message map 加一条自定义消息: + +```cpp +// WeaselServerImpl.h +#define WM_SERVER_NOTIFY_SERVICE (WM_APP + 1) + +BEGIN_MSG_MAP(WEASEL_IPC_WINDOW) + ... + MESSAGE_HANDLER(WM_COMMAND, OnCommand) + MESSAGE_HANDLER(WM_SERVER_NOTIFY_SERVICE, OnServiceNotifyMessage) +END_MSG_MAP() + +LRESULT OnServiceNotifyMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, + BOOL& bHandled); +``` + +```cpp +// WeaselServerImpl.cpp +DWORD ServerImpl::OnServiceNotification(WEASEL_IPC_COMMAND uMsg, + DWORD wParam, + DWORD lParam) { + PostMessage(WM_SERVER_NOTIFY_SERVICE, static_cast(wParam), 0); + return 0; +} + +LRESULT ServerImpl::OnServiceNotifyMessage(UINT, WPARAM wParam, LPARAM, + BOOL&) { + if (m_pRequestHandler) + m_pRequestHandler->NotifyService(static_cast(wParam)); + return 0; +} +``` + +`OnShutdownServer` 里原本同步调的 `m_pRequestHandler->NotifyService(notification)` 同样改成 `PostMessage(WM_SERVER_NOTIFY_SERVICE, ...)`。后面 detached thread `Sleep(1200) → Stop()` 期间 message loop 会把通知处理掉,足够 balloon 显示。 + +#### 为什么 Post 不 Send + +- pipe worker 跑 `SendMessage` 会同步等 UI 线程,把延迟拉回来,跟"非阻塞"基调拧着。 +- PostMessage 立刻返回;NotifyService 通知幂等,丢一条不影响正确性。 +- HWND 在 `WM_DESTROY` 后 Post 失败:通知丢了用户最多少看一个 balloon,可接受;想严谨可在 `OnDestroy` 翻一个 `std::atomic_bool m_running = false`,Post 前判一下。 + +#### 为什么 `tray_icon.Refresh()` 不在这次范围里 + +评估过,**风险显著大于 NotifyService,不打包进 H3。** + +- `NotifyService` 跨线程只传 `DWORD` —— atomic、值传递、无共享状态。 +- `Refresh()` 读的是 `m_style`/`m_status` 引用,包含 `current_zhung_icon` / `current_ascii_icon` 等 `std::wstring`。写方是 pipe worker 在 `g_api_mutex` 内写。 +- 只 PostMessage `Refresh()` 而不改数据流:UI 线程读 wstring 期间 worker 改写 = 真正的 UB,不是 DWORD 那种 benign torn read。 +- 安全迁移路径需要先**在 PostMessage 前快照所需字段**,把快照塞进消息或 staging 结构。这要改 `RimeWithWeaselHandler` 的 update 路径和 `WeaselTrayIcon` 的状态读取接口,scope 远大于 H3。 +- `SendMessage` 同步等可绕开 wstring race,但语义上把 UI 线程拉回来等 worker,违背非阻塞目标。 + +挂为 follow-up:`_UpdateUICallback` 路径上的 tray 操作也应该用同样的 PostMessage 模式收口到 UI 线程,但要配合数据快照一并改。 + +#### 副作用 / 不打架 + +- `_UpdateUI` 现在的跨线程行为不被此修改改变。 +- `WM_SERVER_NOTIFY_SERVICE = WM_APP + 1`,`WM_WEASEL_TRAY_NOTIFY = WEASEL_IPC_LAST_COMMAND + 100`,值域不重叠。 +- 改动量约 10 行;不动 `RimeWithWeasel`,不动 `WeaselTrayIcon`。 + +### H4 — Rime 热路径有未门控的 debug log + +- 位置:`RimeWithWeasel/RimeWithWeasel.cpp:296-302` +- 严重度:high +- 维度:efficiency / doc-criteria +- 现状:`!handled && has_commit` 这条很常见的分支(标点上屏、空格选词等)每键 `WeaselDebugLog` 一行。 +- 问题:reference 文档明确"`WEASEL_TRACE_KEY_EVENTS` 关闭时不输出每键 trace,避免默认日志过重"。当前实现违反该承诺。 +- 建议:用 `ShouldTraceKeyEvents()` 门控(或本文件早已有的 trace 谓词),关闭时不进 log 路径。 + +### H5 — TSF `TraceKeyEvent` 调用点未先门控就构造字符串 + +- 位置:`WeaselTSF/KeyEventSink.cpp:81-89, 127-131, 199-202, 222-225, 238-241, 257-259` +- 严重度:high +- 维度:efficiency +- 现状:每个 Test/Key Down/Up 都进 `TraceKeyEvent`,函数内部检查环境变量并 early-return;但在调用点之前已经构造好 6-8 个 `std::wstring`、跑了多次 `std::to_wstring`,并且 `ShouldTraceKeyEvents()` 每次都查环境变量。 +- 问题:这是按键热路径,doc 关心默认日志开销。 +- 建议:把 `ShouldTraceKeyEvents()` 缓存成 `static bool g_trace_keys`(`std::call_once`),调用点改成 `if (g_trace_keys) TraceKeyEvent(...)`,关闭时不构造任何字符串。 + +### H6 — 旧 `OnKeyEvent` 缺 `FindSession` 守卫 + +- 位置:`WeaselIPCServer/WeaselServerImpl.cpp:248-259` +- 严重度:high +- 维度:correctness +- 现状:新 `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS` 先 `FindSession(lParam)` 守卫再 `ProcessKeyEvent`;旧 `WEASEL_IPC_PROCESS_KEY_EVENT` 直接走 `m_pRequestHandler->ProcessKeyEvent`,session miss 会让 `session_id == 0` 落到 Rime。 +- 建议:旧分支也加同样的 `FindSession` 守卫,保持两个变体对外语义一致。 + +## 中优先(可同 PR 修;也可挂 follow-up) + +### M1 — 重启等待循环硬编码在 `_tWinMain` 里 25 行 + +- 位置:`WeaselServer/WeaselServer.cpp:231-259` +- 维度:quality / reuse +- 现状:50 × 100ms 的 poll loop,`retry == 24` 时再次 nudge shutdown,超时 `NotifyService(RESTART_FAILURE)`,全部 inline。 +- 建议:抽成 `weasel::WaitForExistingServiceShutdown()`,返回枚举 `{kStopped, kTimedOut}`,5s 预算、mid-wait re-shutdown、failure-bubble 都用命名常量;挂 TestWeaselIPC 单测。 + +### M2 — `ClearServiceManualExit()` 在 restart 路径冗余 + +- 位置:`WeaselServer/WeaselServer.cpp:229` +- 维度:quality +- 现状:合法清除点已有两处(`line 177-180` 的 `ShouldClearServiceManualExit` 守卫 + 服务端 `OnShutdownServer` `WeaselServerImpl.cpp:283-284`),这里再无条件清一次。 +- 建议:删 `line 229`。"脚本/自动恢复路径不能误清"这条不变式由 `ShouldClearServiceManualExit` 单点保证。 + +### M3 — `MarkServiceManualExit` 在 TSF 和 server 各调一次 + +- 位置:`WeaselTSF/LanguageBar.cpp:535` + `WeaselIPCServer/WeaselServerImpl.cpp:282` +- 维度:quality +- 现状:TSF 在发 IPC 前写一次 flag,server 收到 EXIT 后又写一次。 +- 建议:统一到 server 端写,TSF 只发命令。这样 `WeaselService::Stop` 等非 TSF 调用方也能复用同一段逻辑(与 H2 一起重构)。 + +### M4 — `OnMaintenanceResult` 等死壳 + +- 位置:`include/RimeWithWeasel.h:140-141`、`WeaselServer/WeaselTrayIcon.h:33-39`、`WeaselServer/WeaselTrayIcon.cpp:95-97` +- 维度:quality +- 现状:`OnMaintenanceResult`、`ShowMaintenanceResult`、`MaintenanceDeployResultStringId`、`MaintenanceDeployResultBalloonIcon` 都只是转发到对应的 `ServiceNotification*`。无生产调用方。 +- 建议:删除这些 wrapper,单测改 call `ServiceNotificationStringId/...BalloonIcon`,减少双类型概念。 + +### M5 — `MAINTENANCE_RESULT_NONE` 与 `SERVICE_NOTIFICATION_NONE` 共享 wire 值 0 + +- 位置:`include/WeaselIPC.h:46-58` +- 维度:quality +- 现状:两组常量值范围(1-2 vs 3-6)手工分段,靠"我没让它碰撞"维持互斥;都通过 `NotifyService(DWORD)` 一条整数通道传。 +- 建议:合并成单个 `WEASEL_IPC_NOTIFICATION` 枚举,或彻底分流到两个独立 IPC 命令。 + +### M6 — `Configurator` 同步用户数据失败不发 `EndMaintenance` + +- 位置:`WeaselDeployer/Configurator.cpp:226-230`(早 return)、对照 `:155-158`(成功路径) +- 维度:correctness / doc-criteria +- 现状:`SyncUserData` 失败时 mutex 关掉就 return,从不调 `EndMaintenance`,server 保持 disabled 到下个 UI 事件才同步。`DictManagement` 仍传默认 `MAINTENANCE_RESULT_NONE`。 +- 建议:所有维护路径走 RAII guard,`EndMaintenance(success ? DEPLOY_SUCCESS : DEPLOY_FAILURE)` 不漏发。 + +### M7 — `CustomInstall` 三个 `Sleep(500)` + +- 位置:`WeaselSetup/WeaselSetup.cpp:134-143` +- 维度:efficiency +- 现状:`/q` → sleep → `/restart-manual` → sleep → 启动 deployer → sleep。 +- 现状:server 端已经等 pipe-disconnect + single-instance mutex;客户端硬延迟是 belt-and-suspenders。 +- 建议:删 sleeps 或在注释里写清楚为什么每一段都必要。 + +### M8 — `copy_file` 10 槽位 `.old.N` 全占时静默失败 + +- 位置:`WeaselSetup/imesetup.cpp:51-69` +- 维度:correctness +- 现状:循环 i=0..9,找到一个未占用的 `.old.N` 重命名旧 DLL 并 `MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT)`;若 10 个槽位都被先前安装失败留下的占满,循环走完没人成功,落到一次 `CopyFile` 重试,也会失败。原始 DLL 保留,但用户看到的是"安装失败"而不是"清掉 `.old.N` 再试"。 +- 建议:循环结束没成功就 early-return + `MSG_NOT_SILENT_*` 显式提示用户清掉残留。 + +## 低 / nit(清理项) + +- `WeaselTSF/WeaselTSF.cpp:47` 的 `marker=idea-keytrace-20260521` token 看起来像未清理的调试标记;doc 又明确反对 JetBrains/时间相关 hack。即便只是日志字符串也应该删。 +- `TestWeaselIPC/TestWeaselIPC.cpp` 少一行 `BOOST_TEST(weasel::ShouldEatKeyEvent(TRUE, TRUE));` 把"process_key+commit"边界 case 锁住。 +- `output/install.bat:36, 41-42` 三行 `rem regsvr32.exe ...` 是 IMM 时代残骸,删。 +- `WeaselSetup/imesetup.cpp:486, 296` 的"IMM/.ime support removed; only uninstall TSF/.dll"是变更注释,应该改成描述当前行为或删掉。 +- `include/WeaselUtility.h:42-44, 70-74` 的 `WeaselDebugLog` 每行调用都跑 `ExpandEnvironmentStringsW` + `fs::exists` + 可能的 `fs::create_directories`,再 `CreateFileW/Write/Close`。缓存路径一次,可选:缓存 handle。 +- 三处重复 `GetModuleFileName` helper:`WeaselTSF/KeyEventSink.cpp:13-19`、`WeaselTSF/WeaselTSF.cpp:13-19`、`WeaselTSF/LanguageBar.cpp:108`。抽到 `include/WeaselUtility.h`。 +- `WeaselSetup/imesetup.cpp:135-140, 323-328` 的 `register_text_service` 6 个 bool/path 参数,`register_ime` 现在等同于 do_register;rename 或打包成 struct。 +- `WeaselIPC/WeaselClientImpl.cpp:30-46` 的 `_InitializeClientInfo` 手卷字符串而非 `std::filesystem`。 +- `WeaselServer/WeaselTrayIcon.cpp:99-113` 的 `ShowServiceNotification` 没有 debounce,连续相同通知会连发 Shell_NotifyIcon。 +- `WeaselIPC.h:93-98` + `WeaselServer/WeaselServer.cpp:286`:干净启动后 `/restart-manual` 也会显示"重启成功"(没有实际 restart 任何东西)。 +- `WeaselServer/resource.h:42` (`_APS_NEXT_COMMAND_VALUE=40003`) 与 `include/resource.h:42` (`=40001`) 不一致;下次资源编辑器保存会出问题。 +- `WeaselIPCServer/WeaselServerImpl.cpp:294-297` 的 `Sleep(1200)` 和 `WeaselServer/WeaselServerApp.cpp:73-76` 的 `Sleep(1200)` 应该共享同一个命名常量。 +- `WeaselIPCServer/WeaselServerImpl.cpp:261-273` 的 0 返回值("server not processed")与 `MakeKeyEventResult(FALSE)` (= 0x01) 不冲突仅靠当前 bit 布局;加一行注释说明。 +- `output/install.nsi`:卸载路径未清 HKCU manual-exit flag;下次安装可能继承上次手动退出态。 +- `WeaselTrayIcon::Refresh` 里 `m_disabled = false` 那段在 `m_mode != DISABLED` 分支里,看起来不可达,需确认是死代码。 +- `RimeUiNeedsUpdate` / `RimeTrayIconSignature` 当前覆盖足够;建议在 `RimeTrayIconSignature::From` 附近加注释列出"影响 tray 渲染的字段集",让未来加字段时不漏更新。 + +## 复核记录 + +四个 review 维度各自结论: + +| 维度 | 范围 | 通过标准对照结论 | +|---|---|---| +| 热路径 & 线程 | `WeaselTSF/*`、`WeaselIPC/WeaselClientImpl.*`、`WeaselIPC/PipeChannel.cpp`、`include/KeyEvent.h`、`include/PipeChannel.h` | 通过;TestKey 副作用、repeat-key、UI 单次重连、后台恢复隔离均符合。 | +| IPC 协议 & UI 去重 | `include/WeaselIPC.h`、`include/WeaselIPCData.h`、`WeaselIPCServer/*`、`RimeWithWeasel/*`、`include/RimeWithWeasel.h` | 通过;`InputPositionCache` reset 覆盖齐全,`RimeUiNeedsUpdate`/`RimeTrayIconSignature` 当前充分。 | +| 服务生命周期 & 托盘 | `WeaselServer/*`、`WeaselTSF/LanguageBar.cpp`、`WeaselDeployer/Configurator.cpp`、`output/start_service.bat` | 通过但有 H1/H2 真实风险;五种托盘通知映射齐全;重启等待同时等 pipe + mutex;shutdown reason 全部显式。 | +| 安装 / 打包 / 测试 | `WeaselSetup/*`、`output/install.*`、`output/start_service.bat`、`test/TestWeaselIPC/TestWeaselIPC.cpp`、`include/WeaselUtility.h`、`resource.h` | 通过;Run-key 用 `/startup`,脚本和 langbar 都用 `/restart-manual`;建议加一个 `(TRUE, TRUE)` 测试。 | + +## 与 Codex review 交叉对照 + +读完 `docs/weasel-change-review-codex.md` 并逐项核对代码后,Codex 抓到了我这轮分工的 4 个 agent 漏掉的几个实质问题,也跟我的部分发现交叉。下面给出核对结果、严重度复评和处置建议。 + +### 新增的硬问题(应补入合并前修单) + +#### C1 — `copy_file` 任何失败都先把旧 DLL 移走(升 Critical) + +- 位置:`WeaselSetup/imesetup.cpp:40-74` +- 核对:`CopyFile(src, dest, FALSE)` 失败后**无条件**进入 `MoveFileEx(dest, .old.N, MOVEFILE_REPLACE_EXISTING)` + `MOVEFILE_DELAY_UNTIL_REBOOT` 流程,再做一次 `CopyFile`。不区分失败原因。 +- 风险:src 缺失/不可读/权限/磁盘满任一原因都会先把现网 DLL 移走,第二次 CopyFile 仍失败 → 安装被破坏。 +- 我之前的 M8 只覆盖"10 槽位全占"的子集,主问题漏。 +- 修法: + 1. 先验证 `src` 可读、size 合理。 + 2. 优先 `CopyFile` 到同目录 `dest + L".new"` 临时文件。 + 3. 临时文件就绪后用 `MoveFileEx(temp, dest, MOVEFILE_REPLACE_EXISTING)`;只有这步失败("目标被占用")才走 `.old.N` + `MOVEFILE_DELAY_UNTIL_REBOOT`。 + 4. 任意阶段失败一律保留原 `dest`。 + +#### C2 — ProcessKeyEvent 与 GetResponseData 之间存在恢复线程抢锁丢响应窗口 + +- 位置:`WeaselIPC/WeaselClientImpl.cpp:340-349`(GetResponseData)、`WeaselTSF/KeyEventSink.cpp:120-126`(ProcessKeyEvent → eaten → 后续 ASYNCDONTCARE edit session) +- 核对:两者都用 `client_mutex` 的 `try_to_lock`。ProcessKeyEvent 成功(eaten=true)后释放锁;TSF 调度 `_UpdateComposition`,最终 `DoEditSession` 在另一时间点调 `GetResponseData`。若此期间后台恢复线程拿到 `client_mutex` 并执行 `_Reconnect`,`GetResponseData` 的 try_lock 失败 → 直接返回 false。结果:宿主已被告知 `eaten=true`(不会再处理原始键),但 commit/preedit 没解析 → 吞字/候选不更新。 +- 唯一兜底:恢复线程只在前一次 IPC 失败时被 kick 起来,正常路径上抢锁窗口偏窄。但 window 客观存在。 +- 修法(两选一): + - 把当前线程 response buffer 的解析从 `client_mutex` 里拆出来(单独 buffer mutex)。 + - 或保证 `ProcessKeyEvent` 与对应 `GetResponseData` 在同一 UI 线程上连续完成,期间不允许恢复线程抢锁(在 ProcessKeyEvent 成功后设个 `pending_response = true` 标志,恢复线程检测到时短退)。 + +#### C3 — `FindSession` 用 `operator[]` 写入污染 session map + +- 位置:`include/RimeWithWeasel.h:174-176`(`to_session_id`);`RimeWithWeasel/RimeWithWeasel.cpp:159-166`(FindSession);`WeaselIPCServer/WeaselServerImpl.cpp:264`(调用方) +- 核对:`to_session_id(ipc_id)` 实现是 `m_session_status_map[ipc_id].session_id`,`operator[]` 在 key 不存在时**插入默认 `SessionStatus`**。`FindSession` 内部调用它做探测,等于"每次探测一个不存在的 ipc_id,map 就长一条 stale 项"。 +- 影响范围:`OnKeyEventWithStatus` 用 `FindSession` 做守卫;如果客户端传入的 session_id 已失效,会反复写入 stale 默认项。 +- 修法:`FindSession` 改成 `auto it = m_session_status_map.find(ipc_id); if (it == end) return 0;` 后再用 `it->second.session_id` 调 `rime_api->find_session`。`to_session_id`/`get_session_status` 不能用在探测路径。 + +#### C4 — `EndMaintenance(result)` 被 `m_disabled` 门控,可能压制通知 + +- 位置:`RimeWithWeasel/RimeWithWeasel.cpp:496-508` +- 核对:`NotifyService(result)` 只在 `if (m_disabled)` 分支里调。`StartMaintenance` 通过 `Finalize()` 把 `m_disabled` 设为 true(正常路径下成立),但如果 `StartMaintenance` IPC 丢失、server 在部署后才启动、或者 server 因某些原因没进入 disabled 态,结果通知就被吃掉。 +- 修法:拆开 "重新初始化 + UI 更新"(保留在 `m_disabled` 分支内)和 "通知部署结果"(无条件做)。让 deploy result balloon 独立于服务 disabled 状态。 + +#### C5 — `/q`、`/restart*` 只做一次 `TryConnect`,pipe 瞬时不可连即丢命令 + +- 位置:`WeaselServer/WeaselServer.cpp:203-263` +- 核对:进入控制命令分支前只 `client.TryConnect()` 一次。pipe 刚启动/退出/listener 间隙等瞬态不可连场景下,`/q` 走 "quit requested but no server" 直接退出;`/restart-manual` 则当作"无旧服务,直接启动",可能与还在收尾的旧 server 撞 mutex。 +- 修法:控制命令路径用**有界重试 + 兼看 single-instance mutex**,mutex 仍在但 pipe 不通时继续等或明确失败上报,不能静默丢。 + +#### C6 — 服务端 KEY_EVENT 仍在 `g_api_mutex` 内,慢任务会卡按键 + +- 位置:`WeaselIPCServer/WeaselServerImpl.cpp:188-198`(`PipeMessageNeedsOuterApiLock`)+ `OnKeyEventWithStatus` +- 核对:bypass list 只放了 `SHUTDOWN_SERVER` / `NOTIFY_SERVICE` / `UPDATE_INPUT_POS` / `TRAY_COMMAND`,KEY_EVENT 系列仍走 outer lock。Rime 卡、维护/初始化持锁、deploy 中等场景下,服务端的 KEY_EVENT 处理会被 outer lock 卡住 → 客户端 WriteFile/ReadFile 即便走 try_lock 取到了 `client_mutex`,本身的 pipe 同步 round-trip 仍要等服务端响应。 +- 我之前判"非阻塞已达标"是偏乐观的。本分支客户端侧做了改进(try_lock、recovery 异步、buffer 清理),但**服务端 outer lock 这一段仍然是按键热路径上的真实阻塞源**。 +- 修法(两选一): + - 服务端 key path 对 `g_api_mutex` 用 try-lock,忙时直接返回 `MakeKeyEventResult(FALSE)` + force-eat 走 commit 兜底。 + - 客户端走 overlapped I/O + 短超时 + cancel,超时即按"未处理"回宿主。 + +### 重新评估 / 修正我的发现 + +#### H6 → 反转:先修 `FindSession`,再讨论是否给旧 `OnKeyEvent` 加守卫 + +- 我此前 H6 建议给旧 `OnKeyEvent` 也加 `FindSession` 守卫"保持两个 IPC 变体一致"。但 Codex 揭示 `FindSession` 本身写副作用(C3)。 +- 修订建议:**优先做 C3 的 `find()` 改造**,确认无副作用后再考虑给旧 `OnKeyEvent` 加守卫。在 C3 修好之前,给旧 `OnKeyEvent` 加守卫等于把 bug 复制一份到第二个调用点。 + +#### H3 补充:handler 生命周期 + +- 我之前 H3 只处理"NotifyService 跨线程触发 `Shell_NotifyIcon`"。Codex 指出同一段还有第二层问题:`m_pRequestHandler` 的读取、`OnEndSystemSession` 里 `Finalize()` 然后置空之间无统一 happens-before。 +- 修订建议:在 H3 的 PostMessage 收口方案之上,再补 `m_pRequestHandler` 的生命周期保护——要么走同一把锁,要么 `shared_ptr` + 在 dispatcher 取 `weak_ptr.lock()`。 + +#### M3 补充:LanguageBar 的"先写 flag 再发 IPC"在 IPC 失败时不回滚 + +- 位置:`WeaselTSF/LanguageBar.cpp:533-538` +- 我之前 M3 只说"flag 在 TSF 和 server 各写一次,重复"。Codex 更精确:写在 IPC **之前**,IPC 失败时只显示退出失败提示,flag 不回滚。 +- 修订建议:合并到 M3 的修法里 —— + - 由 server(接收 EXIT IPC 一侧)单点写 flag; + - TSF 不再预写; + - 或者保留 TSF 预写,但 IPC 失败时必须 `ClearServiceManualExit()` 回滚。 + +#### M7 复核:500ms vs 1200ms 真实风险有限 + +- 我之前 M7 评级是"sleeps 冗余"。Codex 升级为竞态。 +- 核对后维持原评级:`/restart-manual` 子进程会走 `WeaselServer.cpp:235-247` 的 50×100ms 等待循环检查 pipe + mutex 都清空,足够覆盖旧 server 的 1200ms 退出延迟。两段 500ms sleep 确实冗余、可读性差,但**不会让 deploy 真的撞上将退出的旧 server**。 +- 修订建议:M7 维持"冗余可清理"评级,不升 Important。 + +#### Codex Critical 1 "按键热路径仍同步阻塞 IPC" 与我的 H4/H5 关系 + +- 我的 H4(Rime 热路径 debug log)和 H5(TraceKeyEvent 字符串无门控)讲的是"客户端 / Rime 自身在热路径上的开销"。 +- Codex Critical 1 讲的是"服务端 outer lock 阻塞按键"(=本文 C6)。 +- 两者**不是同一件事**,建议都修。H4/H5 减少正常路径每键开销;C6 解决慢任务并发时的按键卡顿。 + +### 双方都有的发现(共识) + +| 主题 | 我编号 | Codex 编号 | 共识 | +|---|---|---|---| +| NotifyService 绕 lock + 跨线程触发 tray | H3 | Important 1 | 同;H3 PostMessage 方案适用,需补 handler 生命周期保护 | +| LanguageBar manual-exit flag 时序与重复写 | M3 | Important 8 | 同;以 server 为单点 | +| install 流程依赖固定 sleep | M7 | Important 6 | 同;评级我维持冗余、Codex 升竞态,按实际等待循环判断我维持原评 | +| copy_file 失败路径不安全 | M8(局部) | Critical 3 | 升 C1 | + +### 我有但 Codex 没提的关键项 + +- H1(`IsScriptLikeRestartParent` 黑名单方向) +- H2(`WeaselService::Stop` 无条件写 flag) +- H4(Rime `ProcessKeyEvent` 每键 debug log) +- H5(`TraceKeyEvent` 调用点未先门控) +- M2(重复 `ClearServiceManualExit`) +- M4(`OnMaintenanceResult` 等死壳) +- M5(`MAINTENANCE_RESULT_NONE` 与 `SERVICE_NOTIFICATION_NONE` wire 值同 0) +- 各类 nits(idea-keytrace 标记、缺少 `ShouldEatKeyEvent(TRUE, TRUE)` 测试、`rem regsvr32` 残骸等) + +这些仍按本文上面的 H/M/nit 处理。 + +### 合并修单建议顺序 + +按实际改动风险 + 用户感知顺序: + +1. **先做 C1**:安装 DLL 复制安全(影响升级用户保留 IME),改动局部。 +2. **再做 C6 + H4 + H5**:按键热路径阻塞与噪声,影响日常输入手感。 +3. **再做 C2 + C3**:响应丢失窗口 + session map 污染,长跑稳定性。 +4. **接着 H1 + H2 + M3(含 codex 失败回滚补强)**:手动退出/恢复语义。 +5. **C4**:deploy 通知独立于 m_disabled。 +6. **C5**:控制命令有界重试。 +7. **H3(含 handler 生命周期)**:tray 跨线程收口。 +8. **M2 / M4 / M5 / nit**:清理。 + +C1 / C2 / C3 / C6 在我的初版结论里漏了;以本节为准。 diff --git a/docs/weasel-change-review-codex.md b/docs/weasel-change-review-codex.md new file mode 100644 index 000000000..a0e273dc7 --- /dev/null +++ b/docs/weasel-change-review-codex.md @@ -0,0 +1,412 @@ +# Codex Review 结果 + +本轮 review 基于 `docs/weasel-change-review-reference.md` 的检查路线,对当前本地未提交改动做静态审查。重点看按键热路径、IPC 恢复、服务退出/重启、托盘通知、部署安装流程。 + +本轮只读代码做 review,没有跑构建或单元测试。 + +## Findings + +### Critical:按键热路径仍会同步阻塞 IPC + +位置: + +- `WeaselTSF/KeyEventSink.cpp:120` +- `include/PipeChannel.h:130` +- `WeaselIPC/PipeChannel.cpp:94` +- `WeaselIPC/PipeChannel.cpp:112` +- `WeaselIPCServer/WeaselServerImpl.cpp:485` + +`OnKeyDown` 通过 `m_client.ProcessKeyEvent(ke, &eaten)` 进入 `TryTransact()`。当前 `TryTransact()` 只在没有 pipe handle 时用 `_EnsureOnce()` 避免等待 pipe;一旦已有连接,仍然同步执行 `WriteFile` 和 `ReadFile` 等待服务端响应。 + +服务端 key event 默认仍进入 `g_api_mutex`,因此 server 忙、Rime 卡、维护/初始化持锁、pipe peer 接收但迟迟不响应时,TSF 当前按键线程仍可能卡住。这和“按键热路径非阻塞”的目标不一致。 + +建议: + +- 客户端热路径改成 overlapped I/O + 短超时 + cancel。 +- 或服务端 key path 对 `g_api_mutex` 使用 try-lock,忙时立即返回未处理。 +- 不应把可能阻塞 `ReadFile` 的同步事务命名和使用成 `TryTransact`。 + +### Critical:按键被吃掉后,commit/preedit 响应可能丢失 + +位置: + +- `WeaselTSF/KeyEventSink.cpp:220` +- `WeaselTSF/Composition.cpp:389` +- `WeaselTSF/EditSession.cpp:14` +- `WeaselIPC/WeaselClientImpl.cpp:340` +- `WeaselTSF/WeaselTSF.cpp:362` + +`OnKeyDown` 先调用 `ProcessKeyEvent` 拿到 `eaten`,再通过 `_UpdateComposition()` 请求 edit session。真正解析 commit/preedit 的地方在 `DoEditSession()` 里调用 `m_client.GetResponseData()`。 + +问题是 `GetResponseData()` 当前也用 `std::try_to_lock`。如果后台恢复线程在 keydown 和 edit session 之间抢到同一个 `m_client` 锁,`GetResponseData()` 会直接返回 false。此时按键可能已经对宿主返回 `eaten=true`,宿主不会再处理原始键,但 Rime 返回的 commit/preedit 没有被解析,可能表现为吞字、候选状态不更新或组合状态错乱。 + +建议: + +- 把当前线程 response buffer 的解析从 `client_mutex` 里拆出来。 +- 或保证 `ProcessKeyEvent` 与对应 `GetResponseData` 在同一 UI 线程上成对完成,中间不允许恢复线程抢锁。 + +### Critical:安装复制失败会先破坏现有 DLL + +位置: + +- `WeaselSetup/imesetup.cpp:44` +- `WeaselSetup/imesetup.cpp:51` +- `WeaselSetup/imesetup.cpp:64` + +`copy_file()` 在任何 `CopyFile(src, dest, FALSE)` 失败后,都会先把当前目标文件移动到 `.old.*`,并安排重启删除,然后再重试复制。 + +这没有区分失败原因。如果失败是源文件缺失、权限、磁盘满、路径错误等,旧 DLL 会先被移走,新 DLL 仍复制失败,安装会被破坏。 + +建议: + +- 先验证源文件存在且可读。 +- 优先复制到同目录临时文件。 +- 只有确认替换文件可用,并且失败原因确认为目标被占用时,才安排延迟替换/删除。 +- 失败时保留原目标文件。 + +### Important:服务通知从 pipe worker 线程直接操作托盘,并绕过 handler 生命周期锁 + +位置: + +- `WeaselIPCServer/WeaselServerImpl.cpp:188` +- `WeaselIPCServer/WeaselServerImpl.cpp:288` +- `WeaselIPCServer/WeaselServerImpl.cpp:304` +- `WeaselIPCServer/WeaselServerImpl.cpp:106` +- `WeaselServer/WeaselServerApp.cpp:45` +- `WeaselServer/WeaselTrayIcon.cpp:99` + +`WEASEL_IPC_SHUTDOWN_SERVER` 和 `WEASEL_IPC_NOTIFY_SERVICE` 被排除在 `g_api_mutex` 外,但它们仍直接访问 `m_pRequestHandler` 并触发 `NotifyService()`。同时 `OnEndSystemSession()` 会在持锁状态下 `Finalize()` 并把 `m_pRequestHandler` 置空。 + +这存在两个风险: + +- `m_pRequestHandler` 的读取、finalize、置空之间没有统一 happens-before,可能出现数据竞争。 +- `NotifyService()` 的回调最终直接调用 `tray_icon.ShowServiceNotification()`,这发生在 pipe worker 线程上,跨线程操作托盘 UI 对象。 + +建议: + +- handler 生命周期仍用同一把锁保护,或改成 `shared_ptr/weak_ptr` 管理。 +- 托盘气泡通知通过 `PostMessage` marshal 回 server 主消息线程显示。 + +### Important:Backspace/Delete/Enter 的 TestKey 预测可能使用陈旧 `_status.composing` + +位置: + +- `WeaselTSF/KeyEventSink.cpp:77` +- `WeaselTSF/EditSession.cpp:11` +- `WeaselTSF/Composition.cpp:389` + +`_TestKeyEvent()` 用 `_status.composing` 判断 Backspace/Delete/Enter/方向键等编辑键是否应该交给 Rime。但 `_status` 是后续 edit session 里解析服务端响应才更新,而 `_UpdateComposition()` 使用 `TF_ES_ASYNCDONTCARE`,可能异步返回。 + +因此刚输入进入组合状态后,下一次 `OnTestKeyDown(Backspace)` 仍可能看到旧的 `composing=false`,把 Backspace 放给宿主,导致删除宿主文本而不是 Rime preedit。 + +建议: + +- 正式 `OnKeyDown` 收到 IPC 响应后,同步维护一个只用于 TestKey 预测的 composing/status 快照。 +- 或先解析必要 status,再调度异步 edit session。 + +### Important:`/q`、`/restart*` 只做一次 `TryConnect()`,pipe 瞬时不可连就当作没有服务 + +位置: + +- `WeaselServer/WeaselServer.cpp:205` +- `WeaselServer/WeaselServer.cpp:260` + +服务命令启动时只调用一次 `client.TryConnect()`。如果 pipe 正忙、server 刚启动/退出、listener 处于间隙,`TryConnect()` 返回 false 后,`/q` 会直接走 “quit requested but no server” 并返回。 + +这会导致退出/重启命令被静默丢掉,调用方误以为服务已经停止。该现象也和之前日志里的 `/q ... connected=0 ... no server` 一致。 + +建议: + +- 退出/重启命令使用有界重试。 +- 同时检查单实例 mutex;如果 mutex 仍存在但 pipe 暂不可连,应继续等待或明确失败。 +- 不要对 stop/restart 这类控制命令只做一次非阻塞连接。 + +### Important:修改安装后的后台流程和 1200ms 延迟退出存在竞态 + +位置: + +- `WeaselSetup/WeaselSetup.cpp:134` +- `WeaselSetup/WeaselSetup.cpp:137` +- `WeaselSetup/WeaselSetup.cpp:140` +- `WeaselIPCServer/WeaselServerImpl.cpp:294` + +修改安装完成后,后台线程按固定顺序执行: + +1. `WeaselServer.exe /q` +2. `Sleep(500)` +3. `WeaselServer.exe /restart-manual` +4. `Sleep(500)` +5. `WeaselDeployer.exe /deploy` + +但服务端收到 shutdown 后会延迟 1200ms 才 `Stop()`。因此 500ms 后旧服务通常仍在退出过程中,新服务和 deploy 可能与旧服务重叠,deploy 也可能连到即将退出的旧 server,造成部署通知丢失或服务状态不稳定。 + +建议: + +- 不要用固定 sleep 串联。 +- 等待旧服务 pipe 断开且单实例 mutex 释放后,再启动新服务。 +- 确认新服务 pipe/mutex 就绪后再 deploy。 + +### Important:部署完成/失败通知被 `m_disabled` 状态门控 + +位置: + +- `WeaselDeployer/Configurator.cpp:136` +- `WeaselDeployer/Configurator.cpp:153` +- `RimeWithWeasel/RimeWithWeasel.cpp:496` + +部署器只有在 `client.TryConnect()` 成功时发送 `StartMaintenance()` 和 `EndMaintenance(result)`。server 端 `EndMaintenance(result)` 只有在 `m_disabled == true` 时才会 `_SetDeployMessage(result)` 并 `NotifyService(result)`。 + +如果 `StartMaintenance()` 消息丢失、server 在部署后才启动、或安装流程竞态导致 `m_disabled == false`,部署结果通知会被跳过。 + +建议: + +- 部署结果通知应独立于 `m_disabled` 状态处理。 +- 只把重新初始化和 UI 更新放在 `m_disabled` 分支内。 +- `StartMaintenance()` / `EndMaintenance()` 最好返回可检查的 IPC 结果,让 deployer 能知道通知是否真的送达。 + +### Important:LanguageBar 退出先写 manual-exit flag,再确认 IPC 是否成功 + +位置: + +- `WeaselTSF/LanguageBar.cpp:533` +- `WeaselTSF/LanguageBar.cpp:536` +- `WeaselServer/WeaselServerApp.cpp:67` + +LanguageBar 退出菜单先调用 `MarkServiceManualExit()`,然后才 `TrayCommandSync(ID_WEASELTRAY_QUIT)`。如果 IPC 失败,代码只显示退出失败提示,不会回滚 manual-exit flag。 + +结果是服务可能仍在运行或未成功退出,但后续自动恢复已经被 manual-exit flag 抑制。server 托盘退出路径本身也会写 manual-exit flag,因此这里存在重复且提前写入。 + +建议: + +- IPC 确认成功后再写 manual-exit flag。 +- 或失败时清除 flag。 +- 最好由真正执行退出的一端统一负责写 flag。 + +### Important:旧 `/restart` 的父进程判断过宽,未知父进程会误清 manual-exit flag + +位置: + +- `include/WeaselIPC.h:294` +- `include/WeaselIPC.h:302` + +`ShouldTreatLegacyRestartAsManual()` 通过 `IsScriptLikeRestartParent()` 做黑名单判断:`cmd.exe`、`powershell.exe`、`pwsh.exe`、`conhost.exe`、`windowsterminal.exe`、`wt.exe`、`weaselserver.exe` 被视为脚本/非交互,其余父进程只要 `parent_path` 不为空,就会被当成“交互父进程”,从而清掉 manual-exit flag。 + +这和 review 目标里的“旧 `/restart` 只在明确来自交互父进程时兼容成手动重启;脚本/自动恢复场景不能误清 flag”不一致。未知 launcher、调度任务、第三方服务外壳等都可能被误判成交互重启。 + +建议: + +- 改成白名单,只允许明确的交互入口清 flag。 +- 至少为未知父进程加测试,例如 `notepad.exe` 或第三方 launcher 不应被当成手动重启。 + +### Important:SCM `Stop()` 无条件写 manual-exit flag + +位置: + +- `WeaselServer/WeaselService.cpp:90` + +`WeaselService::Stop()` 在所有 SCM stop 路径上都会 `MarkServiceManualExit()`,然后再尝试通过 IPC shutdown server。 + +SCM stop 不等同于用户手动退出。管理员停服务、卸载、系统关机、服务管理器操作等路径都会走这里。如果写入 manual-exit flag,下次 `/recover` 会被抑制,需要用户手动重启算法服务才能恢复。 + +建议: + +- 只在托盘退出或明确 IPC EXIT 路径写 manual-exit flag。 +- SCM stop 使用不写 manual-exit flag 的 shutdown reason,或单独的 stop helper。 + +### Important:Rime 热路径 debug log 没有按 `WEASEL_TRACE_KEY_EVENTS` 门控 + +位置: + +- `RimeWithWeasel/RimeWithWeasel.cpp:296` + +`ProcessKeyEvent()` 在 `!handled && has_commit` 时每次都会写 `WeaselDebugLog`。这类分支在标点上屏、空格选词等场景可能很常见,属于按键热路径。 + +文档目标里明确要求 `WEASEL_TRACE_KEY_EVENTS` 关闭时不输出每键 trace,避免默认日志过重。这里没有门控,会让默认路径仍然产生按键级日志和字符串构造开销。 + +建议: + +- 用 `ShouldTraceKeyEvents()` 或同等全局开关门控。 +- 关闭 trace 时不要进入日志字符串构造路径。 + +### Important:TSF `TraceKeyEvent` 调用点先构造字符串,关闭 trace 仍有热路径开销 + +位置: + +- `WeaselTSF/KeyEventSink.cpp:21` +- `WeaselTSF/KeyEventSink.cpp:45` +- `WeaselTSF/KeyEventSink.cpp:81` +- `WeaselTSF/KeyEventSink.cpp:127` +- `WeaselTSF/KeyEventSink.cpp:199` +- `WeaselTSF/KeyEventSink.cpp:222` +- `WeaselTSF/KeyEventSink.cpp:238` +- `WeaselTSF/KeyEventSink.cpp:257` + +`TraceKeyEvent()` 内部会先检查 `ShouldTraceKeyEvents()`,但多个调用点在调用前已经构造了 `std::wstring`,包含多次 `std::to_wstring` 和字符串拼接。`ShouldTraceKeyEvents()` 本身每次也查环境变量。 + +因此 trace 关闭时,默认按键路径仍然承担字符串构造和环境变量查询开销。 + +建议: + +- 把 trace 开关缓存起来,例如 `std::call_once` 初始化。 +- 调用点先 `if (trace_enabled)`,关闭时不构造任何 trace 字符串。 + +### Important:旧 `WEASEL_IPC_PROCESS_KEY_EVENT` 缺少 session 守卫 + +位置: + +- `WeaselIPCServer/WeaselServerImpl.cpp:248` +- `WeaselIPCServer/WeaselServerImpl.cpp:261` + +新增的 `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS` 会先检查 `FindSession(lParam)`,无效 session 直接返回 0。旧的 `WEASEL_IPC_PROCESS_KEY_EVENT` 只检查 `m_pRequestHandler`,然后直接调用 `ProcessKeyEvent()`。 + +这让两个 key event 入口的语义不一致。旧客户端或旧调用路径传入 stale session 时,可能继续把 session 0 或无效 session 送进 Rime 处理。 + +建议: + +- 旧 `OnKeyEvent()` 也加同样的 `FindSession(lParam)` 守卫。 +- 同时处理本文档前面提到的 `FindSession()` 无副作用查询问题。 + +### Important:同步用户数据失败会漏掉 `EndMaintenance()` + +位置: + +- `WeaselDeployer/Configurator.cpp:218` +- `WeaselDeployer/Configurator.cpp:226` +- `WeaselDeployer/Configurator.cpp:236` + +`SyncUserData()` 成功路径会先 `StartMaintenance()`,结束后 `EndMaintenance()`。但 `rime->sync_user_data()` 失败时直接 `CloseHandle(hMutex)` 并 `return 1`,没有通知 server 退出维护模式。 + +如果 `StartMaintenance()` 已经送达,失败早退会让 server 保持 disabled,直到后续其他路径重新初始化或用户手动重启。 + +建议: + +- 维护模式用 RAII guard,保证所有 return 路径都会尝试 `EndMaintenance()`。 +- 失败路径可以传部署/维护失败通知,至少不能静默留在 disabled 状态。 + +### Minor:`FindSession()` 会污染 session map + +位置: + +- `WeaselIPCServer/WeaselServerImpl.cpp:264` +- `RimeWithWeasel/RimeWithWeasel.cpp:159` +- `include/RimeWithWeasel.h:174` + +`OnKeyEventWithStatus()` 用 `FindSession(lParam)` 检查 session 是否存在。但 `FindSession()` 内部调用 `to_session_id(ipc_id)`,而 `to_session_id()` 使用 `m_session_status_map[ipc_id]`。对于不存在的 stale session,这会插入一个默认 `SessionStatus`,即使最终返回 0。 + +建议: + +- `FindSession()` 改为使用 `m_session_status_map.find(ipc_id)` 做无副作用查询。 +- `to_session_id()` / `get_session_status()` 不应用于探测路径。 + +## Review 结论 + +这轮改动方向是对的:目标是减少按键路径阻塞、补齐服务通知、修正退出/重启语义。但当前实现仍有几个会真实影响体验和稳定性的硬问题: + +- 按键路径还没有真正摆脱同步 IPC 阻塞。 +- keydown 和 edit session 响应解析之间存在恢复线程抢锁导致响应丢失的窗口。 +- 退出/重启/部署流程仍有一次性连接和固定 sleep 竞态。 +- 托盘通知线程模型和 handler 生命周期还不稳。 +- 安装复制失败路径有破坏现有安装的风险。 +- Claude review 中的 H1/H2/H4/H5/H6/M6 已吸收到本文档;H3/M3/M7 与本文档已有项重合。 + +建议先处理 Critical,再处理服务退出/部署通知相关 Important,最后再清理 Minor。 + +## 修复后复核(2026-05-21) + +本节记录根据 Claude review 与 Codex review 合并清单完成的一轮修复结果。已重新跑单元目标和主构建,见下方“验证”。 + +### 已处理 + +1. 按键服务端外层锁阻塞: + - `WEASEL_IPC_PROCESS_KEY_EVENT` 和 `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS` 不再走阻塞 `g_api_mutex`。 + - 服务端 key path 改为 try-lock;锁忙时直接返回 0,让客户端按“未处理”快速返回宿主。 + - `TestWeaselIPC` 增加断言,锁定 key IPC 不走阻塞 outer lock。 + +2. keydown 与 edit session 响应解析之间的抢锁窗口: + - `ClientImpl::GetResponseData()` 不再抢 `client_mutex`;response buffer 本来就是 `PipeChannel` 的 thread-local 数据,后台恢复线程不应阻塞当前 UI 线程解析响应。 + - `OnKeyDown` 在 `ProcessKeyEvent` 成功后立即解析一次 status-only 响应,提前刷新 `_status.composing`,降低 Backspace/TestKey 使用陈旧 composing 状态的窗口。 + +3. 安装复制安全: + - `copy_file()` 不再在任意 `CopyFile` 失败后先移动旧 DLL。 + - 新流程先验证源文件,再复制到 `dest.new`,即时替换失败时只安排 delayed replace;失败时保留原目标文件。 + +4. 托盘通知跨线程: + - `WEASEL_IPC_NOTIFY_SERVICE` 和 shutdown notification 不再从 pipe worker 线程直接调用 `tray_icon.ShowServiceNotification()`。 + - 新增 `WM_WEASEL_SERVICE_NOTIFY`,通过 `PostMessage` 收口到 server message loop 线程,再调用 handler 通知托盘。 + - `OnServiceNotifyMessage` 内部持 `g_api_mutex` 读取 handler,避免和 `OnEndSystemSession()` 的 finalize/置空并发。 + +5. manual-exit 语义: + - 旧 `/restart` 父进程判断从黑名单改为白名单:只有 `explorer.exe`、`taskmgr.exe`、`WeaselDeployer.exe`、`WeaselSetup.exe` 等明确交互入口才兼容成手动重启。 + - 新增非手动 `/stop` / `/stop-service`,安装器和 stop 脚本改用 `/stop`,避免安装/卸载把用户 manual-exit flag 写脏。 + - SCM `WeaselService::Stop()` 改用 `WEASEL_IPC_SHUTDOWN_REASON_STOP`,不再写 manual-exit flag。 + - LanguageBar 退出不再先写 manual-exit flag;成功退出由 server 端 EXIT 路径统一写。 + - restart 路径去掉冗余 `ClearServiceManualExit()`,清 flag 收敛到命令语义入口和 server restart reason。 + +6. 部署/维护通知: + - `EndMaintenance(result)` 的部署成功/失败通知不再被 `m_disabled` 门控;只把重新初始化和 UI 更新保留在 disabled 分支内。 + - `SyncUserData()` 失败早退前会尝试 `EndMaintenance()`,避免 server 停留在维护/disabled 状态。 + +7. session 与 IPC 细节: + - `FindSession()` 改为 `map.find()` 无副作用查询,不再通过 `operator[]` 插入 stale session。 + - `_UpdateUI(0)` 不再通过 `to_session_id(0)` / `get_session_status(0)` 创建 session 0,也不再对 session 0 调 Rime option。 + - 旧 `WEASEL_IPC_PROCESS_KEY_EVENT` 加上和新 status 入口一致的 `FindSession` 守卫。 + - `PipeChannel::_Send()` 重连后改用新 pipe handle 重发,不再拿旧 handle 重发。 + - `PipeChannelBase::_Connect()` 在 `SetNamedPipeHandleState` 失败时关闭已打开 pipe,避免句柄泄漏。 + - `PipeServer::Listen()` 创建 per-connection worker 后显式 `detach()`,避免局部 `boost::thread` 析构时留下 joinable 线程风险。 + +8. 热路径日志: + - `ShouldTraceKeyEvents()` 提到 `WeaselUtility.h` 并缓存环境变量结果。 + - TSF key trace 调用改为宏门控,trace 关闭时不再构造每键 trace 字符串。 + - Rime `!handled && has_commit` 的每键 debug log 改为受 `WEASEL_TRACE_KEY_EVENTS` 门控。 + - 移除 `idea-keytrace-20260521` 调试标记。 + +9. 重复 keydown guard: + - `ActiveKeyDownGuard` 改为只判断是否应 suppress,不再在判断时自动记录。 + - `OnKeyDown` 只有在 `_ProcessKeyEvent()` 成功且 `pfEaten=TRUE` 后才记录 active key,避免 disabled/unknown/pass-through key 的第二次回调被误吞。 + - 单元测试同步改为显式 `Remember()` 后再验证 suppress。 + +10. 低风险清理: + - `output/install.bat` 删除 IMM 时代遗留的 `regsvr32` 注释。 + - `imesetup.cpp` 的 IMM 注释改成当前 TSF-only 行为。 + - `include/resource.h` 的 `_APS_NEXT_COMMAND_VALUE` 与 `WeaselServer/resource.h` 对齐。 + +### 仍需真实 smoke 的点 + +1. 客户端 pipe 事务仍是同步 `WriteFile/ReadFile`。本轮通过服务端 key path try-lock 处理了 review 中最直接的 `g_api_mutex` 阻塞源,但没有把客户端 IPC 改成 overlapped I/O + timeout。若服务端拿到 key lock 后 Rime 自身长时间卡住,仍可能等待该次 round-trip。这个需要更大的 IPC 改造和真实延迟测试。 + +2. `_UpdateUICallback` / tray refresh 的数据流仍没有整体迁到 UI 线程快照模式。本轮只修了 `NotifyService(DWORD)` 这条跨线程托盘气泡路径;`Refresh()` 涉及 `UIStyle`/`Status` 中的字符串引用,不能只靠 `PostMessage` 简单搬线程,否则会引入 wstring 并发读写风险。 + +3. `WeaselSetup::CustomInstall` 仍然用后台线程串联 `/stop`、`/restart-manual`、`/deploy`,保留固定 sleep。由于 `/restart-manual` 进程最终会成为长驻 server,不能简单等待进程退出;这块要彻底消除 sleep,需要设计一个“等待新 server ready 后再 deploy”的专用命令或 IPC handshake。 + +4. 真机 TSF smoke 仍是必要项:Backspace 删除 preedit、JetBrains/Rider 重复输入、托盘退出后手动重启、部署完成通知都需要打包安装后验证。 + +### 验证 + +已执行: + +```powershell +MSBuild .\test\TestWeaselIPC\TestWeaselIPC.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +.\x64\Release\TestWeaselIPC.exe /unit +``` + +结果:构建成功;`TestWeaselIPC.exe /unit` 输出 `No errors detected.` + +已执行: + +```powershell +MSBuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +MSBuild .\WeaselSetup\WeaselSetup.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir='F:\IdeaProjects\参考项目\weasel\' /m:1 /v:minimal +``` + +结果:全部构建成功;仍有既有类型转换 warning,未出现新的 error。 + +已执行: + +```powershell +git diff --check +``` + +结果:无 whitespace error;只输出当前工作区 LF/CRLF 归一化提示。 diff --git a/docs/weasel-change-review-reference.md b/docs/weasel-change-review-reference.md new file mode 100644 index 000000000..d0dead5ab --- /dev/null +++ b/docs/weasel-change-review-reference.md @@ -0,0 +1,253 @@ +# Weasel 本地改动 Review 参照 + +本文档用于 review 当前本地未提交改动。它不是 PR 描述正文,而是给 reviewer 的检查路线图:说明每组改动的目标、关键设计、需要重点质疑的地方、以及建议验证方式。 + +## Review 范围 + +当前 tracked diff 涉及这些主要模块: + +- TSF 按键路径:`WeaselTSF/KeyEventSink.cpp`、`WeaselTSF/WeaselTSF.cpp`、`WeaselTSF/WeaselTSF.h`、`include/KeyEvent.h` +- IPC 客户端和管道:`WeaselIPC/WeaselClientImpl.*`、`WeaselIPC/PipeChannel.cpp`、`include/PipeChannel.h`、`include/WeaselIPC.h` +- IPC 服务端:`WeaselIPCServer/WeaselServerImpl.*` +- Rime/UI 桥接:`RimeWithWeasel/RimeWithWeasel.cpp`、`include/RimeWithWeasel.h`、`include/WeaselIPCData.h` +- 服务进程和托盘:`WeaselServer/WeaselServer.cpp`、`WeaselServer/WeaselServerApp.*`、`WeaselServer/WeaselTrayIcon.*`、`WeaselServer/WeaselService.cpp` +- 部署和安装:`WeaselDeployer/Configurator.cpp`、`WeaselSetup/*`、`output/install.*`、`output/start_service.bat` +- 测试:`test/TestWeaselIPC/TestWeaselIPC.cpp` +- 文案资源:`WeaselServer/WeaselServer.rc`、`WeaselServer/resource.h`、`include/resource.h` + +未跟踪的 `x64/`、`test/TestWeaselIPC/x64/` 属于构建产物,不应纳入代码 review。未跟踪的 `output/data/` 方案、lua、皮肤预览等数据是否纳入提交需要单独确认。 + +## 改动目标 + +这批改动主要解决三类问题: + +1. 按键卡顿:服务端忙、pipe 不可用、恢复过程或重复 UI 刷新不应该阻塞 TSF 当前按键线程。 +2. 服务状态可见性:部署完成/失败、退出算法服务、重启算法服务、重启成功/失败需要有托盘提示。 +3. 服务退出/恢复语义:用户手动退出算法服务后,自动恢复不能立刻把服务重新拉起;手动重启应能清除退出标记并启动服务。 + +后续追加的回归修复: + +- `OnTestKeyDown` 预测逻辑不能无条件吃 Backspace。无组合状态下 Backspace/Delete/Enter/方向键等编辑键应交给宿主;正在组合时再交给 Rime。 +- 不保留 JetBrains/IDEA/Rider 专用分支,也不使用 120ms 这类输入延迟 hack。 + +## 建议 Review 顺序 + +1. 先读 `include/WeaselIPC.h` + - 确认新增 IPC 命令、维护结果、服务通知、shutdown reason、命令行参数语义是否清晰。 + - 重点看 `/restart`、`/restart-manual`、`/recover`、`/startup`、`/q` 的分工。 + +2. 再读 IPC 热路径 + - `include/PipeChannel.h` + - `WeaselIPC/PipeChannel.cpp` + - `WeaselIPC/WeaselClientImpl.cpp` + - 重点确认按键、输入位置、焦点、候选、托盘等 UI 热路径没有引入新的同步等待。 + +3. 再读 TSF 按键路径 + - `include/KeyEvent.h` + - `WeaselTSF/KeyEventSink.cpp` + - `WeaselTSF/WeaselTSF.cpp` + - 重点确认 TestKey 无副作用、正式 KeyDown 才进 Rime、Backspace 预测正确、重复 keydown 防护不影响长按和正常快捷键。 + +4. 再读服务端和托盘 + - `WeaselIPCServer/WeaselServerImpl.cpp` + - `WeaselServer/WeaselServer.cpp` + - `WeaselServer/WeaselServerApp.cpp` + - `WeaselServer/WeaselTrayIcon.*` + - 重点确认退出/重启通知能发出,且不会在通知刚出现时立刻移除托盘图标。 + +5. 最后读 Rime/UI 刷新和部署安装 + - `RimeWithWeasel/RimeWithWeasel.cpp` + - `include/RimeWithWeasel.h` + - `WeaselDeployer/Configurator.cpp` + - `WeaselSetup/*` + - 重点确认部署结果能传回 server,UI/托盘 diff 不会漏掉可见状态变化。 + +## 重点 Review 清单 + +### 1. 按键 IPC 语义 + +需要确认: + +- `WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS` 的返回值同时表达“服务端处理过”和“Rime 吃键”。 +- IPC 失败时客户端返回未处理,不应被误解成“服务端处理了但没吃键”。 +- Rime 产生 commit 时,即使 `process_key` 返回未处理,也应吃掉当前键,避免宿主再插入原始字符。 + +重点文件: + +- `include/WeaselIPC.h` +- `WeaselIPC/WeaselClientImpl.cpp` +- `WeaselIPCServer/WeaselServerImpl.cpp` +- `RimeWithWeasel/RimeWithWeasel.cpp` + +### 2. TSF TestKey 无副作用 + +需要确认: + +- `OnTestKeyDown` / `OnTestKeyUp` 不调用 Rime `process_key`,不更新 composition。 +- TestKey 只做预测,正式 `OnKeyDown` / `OnKeyUp` 才改变 Rime 状态。 +- `ShouldEatTestKeyEvent` 的规则符合输入法语义: + - 非已知键不吃。 + - CapsLock 模拟流程可吃。 + - Shift/CapsLock/Ctrl 等模式切换键可吃。 + - Ctrl/Alt 快捷键在无组合时不吃。 + - 正在组合时编辑键交给 Rime。 + - 无组合时 Backspace/Delete/Enter/方向键不吃。 + - 非 ascii 模式下普通文本键吃。 + +重点文件: + +- `include/KeyEvent.h` +- `WeaselTSF/KeyEventSink.cpp` +- `test/TestWeaselIPC/TestWeaselIPC.cpp` + +### 3. 重复按键防护 + +需要确认: + +- `ActiveKeyDownGuard` 只抑制同一物理 keydown 在 keyup 前重复到达的情况。 +- `prevKeyState=1` 的系统 repeat 不应被误杀。 +- `Release` 在 keyup 时清理状态,焦点丢失时 reset。 +- 没有应用名白名单,没有 JetBrains 特判,没有固定时间延迟。 + +重点文件: + +- `include/KeyEvent.h` +- `WeaselTSF/KeyEventSink.cpp` + +### 4. IPC 非阻塞和恢复 + +需要确认: + +- UI/按键热路径使用 `try_lock` 和 `TryTransact`,恢复线程持锁时快速失败,不等待慢恢复。 +- `TryTransact` 连接失败时清空发送 buffer,避免带 body 的请求污染下一次请求。 +- `Reconnect(..., wait_for_pipe=false)` 在 UI 线程上只做一次尝试;后台恢复才允许等待 pipe。 +- 后台恢复线程不解析响应写 `_status`,不调用 `_UpdateLanguageBar`,不触碰 TSF apartment-bound 对象。 +- `AddRef/Release` 的原子化足够支撑后台恢复线程持有 TSF 对象。 + +重点文件: + +- `WeaselIPC/WeaselClientImpl.cpp` +- `WeaselIPC/PipeChannel.cpp` +- `include/PipeChannel.h` +- `WeaselTSF/WeaselTSF.cpp` + +### 5. 输入位置和 UI/托盘去重 + +需要确认: + +- `InputPositionCache` 只在发送成功后记录坐标。 +- Disconnect、StartSession、EndSession、维护模式切换后 reset,避免新 session 漏发首个相同坐标。 +- `RimeUiNeedsUpdate` 比较字段足够覆盖候选窗可见状态。 +- `RimeTrayIconSignature` 包含所有影响托盘图标状态的字段;后续新增字段时需要同步更新签名。 + +重点文件: + +- `WeaselIPC/WeaselClientImpl.h` +- `WeaselIPC/WeaselClientImpl.cpp` +- `include/RimeWithWeasel.h` +- `RimeWithWeasel/RimeWithWeasel.cpp` +- `include/WeaselIPCData.h` + +### 6. 退出、重启、恢复语义 + +需要确认: + +- 手动退出会写 manual-exit flag,并注销系统应用恢复。 +- `/recover` 在 manual-exit flag 存在时不会启动服务。 +- `/restart-manual` 会清除 manual-exit flag 并启动服务。 +- 旧 `/restart` 只在明确来自交互父进程时兼容成手动重启;脚本/自动恢复场景不能误清 flag。 +- 重启旧服务时同时等待 pipe 断开和单实例 mutex 释放。 +- 退出服务和重启服务的 shutdown reason 传递明确,不再依赖默认值。 + +重点文件: + +- `include/WeaselIPC.h` +- `WeaselServer/WeaselServer.cpp` +- `WeaselIPCServer/WeaselServerImpl.cpp` +- `WeaselServer/WeaselService.cpp` +- `WeaselTSF/LanguageBar.cpp` +- `output/start_service.bat` + +### 7. 托盘提示 + +需要确认: + +- 部署完成/失败通过 `EndMaintenance(result)` 传到 server,再转成托盘气泡。 +- 退出算法服务、正在重启算法服务、重启成功、重启失败都映射到正确文案和图标类型。 +- 服务即将退出时延迟停止,给托盘气泡留出显示时间。 +- 无活跃输入会话时仍能看到部署完成/失败提示。 + +重点文件: + +- `WeaselDeployer/Configurator.cpp` +- `RimeWithWeasel/RimeWithWeasel.cpp` +- `WeaselServer/WeaselTrayIcon.*` +- `WeaselServer/WeaselServerApp.cpp` +- `WeaselServer/WeaselServer.rc` + +### 8. 安装和打包脚本 + +需要确认: + +- Run key 使用 `WeaselServer.exe /startup`。 +- `start_service.bat` 使用 `/restart-manual`,用于手动重启。 +- 安装器复制 TSF DLL 失败时,重命名旧文件和安排重启删除不会破坏正常升级。 +- `WeaselSetup` 里手动重启服务参数和新语义一致。 + +重点文件: + +- `WeaselSetup/WeaselSetup.cpp` +- `WeaselSetup/imesetup.cpp` +- `output/install.bat` +- `output/install.nsi` +- `output/start_service.bat` + +## 已知取舍 + +- 恢复线程持锁期间,候选选择、候选高亮、翻页、commit/clear、focus/tray 等 UI 入口可能短暂失败返回。这是用少量操作失败换取 UI/按键线程不被慢 IPC 卡住。 +- 后台恢复成功后不会立刻刷新 LanguageBar;状态会等下一次 UI 线程事件自然同步,避免后台线程触碰 TSF apartment-bound 对象。 +- `WEASEL_TRACE_KEY_EVENTS` 关闭时不会输出每个按键的详细 trace,避免默认日志过重;需要排查真实宿主按键序列时再开启。 +- 当前单元测试覆盖 helper、协议和策略,不替代真实 TSF 宿主 smoke。 + +## 建议 Smoke 验证 + +建议在打包安装后按下面顺序验证: + +1. 普通编辑器输入中文、英文、数字、标点。 +2. 无组合状态按 Backspace,应删除宿主文本。 +3. 有组合状态按 Backspace,应删除 Rime preedit。 +4. Ctrl/Alt 快捷键在无组合状态不应被输入法吞掉。 +5. Shift、CapsLock 仍保持原有切换语义。 +6. JetBrains IDEA/Rider 中输入不应双击成两个字符。 +7. 托盘菜单“退出算法服务”:服务退出,不应自动恢复。 +8. 退出后语言栏/脚本“重启算法服务”:服务能重新启动,并显示重启成功提示。 +9. 重新部署:先看到维护中,结束后看到部署成功或失败提示。 +10. 服务端进程被手动杀掉后,输入法应尝试后台恢复;恢复期间 UI 不应长时间卡死。 + +## 建议自动验证命令 + +```powershell +$msbuild='C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe' +$solutionDir='F:\IdeaProjects\参考项目\weasel\' + +& $msbuild .\test\TestWeaselIPC\TestWeaselIPC.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir=$solutionDir /m /v:minimal +.\x64\Release\TestWeaselIPC.exe /unit + +& $msbuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir=$solutionDir /m /v:minimal +& $msbuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=x64 /p:SolutionDir=$solutionDir /m /v:minimal +& $msbuild .\WeaselServer\WeaselServer.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir=$solutionDir /m /v:minimal +& $msbuild .\WeaselTSF\WeaselTSF.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir=$solutionDir /m /v:minimal +& $msbuild .\WeaselSetup\WeaselSetup.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir=$solutionDir /m /v:minimal +``` + +## Review 通过标准 + +这批改动可以认为通过 review 的最低标准: + +- 按键热路径没有新的同步等待、阻塞重连或无界锁等待。 +- TestKey 不再推进 Rime 状态。 +- Backspace 回归测试存在且语义正确。 +- 服务手动退出、自动恢复、手动重启三者语义不互相打架。 +- 托盘提示覆盖部署完成/失败、退出、重启中、重启成功、重启失败。 +- 单元测试通过,TSF/Server/Setup 目标可构建。 +- reviewer 没有发现 TSF apartment 跨线程访问、IPC buffer 污染、manual-exit flag 竞态这三类高风险问题。 diff --git a/include/KeyEvent.h b/include/KeyEvent.h index 6fe8adef6..dc476cf7a 100644 --- a/include/KeyEvent.h +++ b/include/KeyEvent.h @@ -1,247 +1,480 @@ -#pragma once - -struct KeyInfo { - UINT repeatCount : 16; - UINT scanCode : 8; - UINT isExtended : 1; - UINT reserved : 4; - UINT contextCode : 1; - UINT prevKeyState : 1; - UINT isKeyUp : 1; - - KeyInfo(LPARAM lparam) { *this = *reinterpret_cast(&lparam); } - - operator UINT32() { return *reinterpret_cast(this); } -}; - -namespace weasel { -struct KeyEvent { - UINT keycode : 16; - UINT mask : 16; - KeyEvent() : keycode(0), mask(0) {} - KeyEvent(UINT _keycode, UINT _mask) : keycode(_keycode), mask(_mask) {} - KeyEvent(UINT x) { *reinterpret_cast(this) = x; } - operator UINT32 const() const { - return *reinterpret_cast(this); - } -}; -} // namespace weasel - -bool ConvertKeyEvent(UINT vkey, - KeyInfo kinfo, - const LPBYTE keyState, - weasel::KeyEvent& result); - -namespace ibus { -// keycodes - -enum Keycode { - VoidSymbol = 0xFFFFFF, - space = 0x020, - grave = 0x060, - BackSpace = 0xFF08, - Tab = 0xFF09, - Linefeed = 0xFF0A, - Clear = 0xFF0B, - Return = 0xFF0D, - Pause = 0xFF13, - Scroll_Lock = 0xFF14, - Sys_Req = 0xFF15, - Escape = 0xFF1B, - Delete = 0xFFFF, - Multi_key = 0xFF20, - Codeinput = 0xFF37, - SingleCandidate = 0xFF3C, - MultipleCandidate = 0xFF3D, - PreviousCandidate = 0xFF3E, - Kanji = 0xFF21, - Muhenkan = 0xFF22, - Henkan_Mode = 0xFF23, - Henkan = 0xFF23, - Romaji = 0xFF24, - Hiragana = 0xFF25, - Katakana = 0xFF26, - Hiragana_Katakana = 0xFF27, - Zenkaku = 0xFF28, - Hankaku = 0xFF29, - Zenkaku_Hankaku = 0xFF2A, - Touroku = 0xFF2B, - Massyo = 0xFF2C, - Kana_Lock = 0xFF2D, - Kana_Shift = 0xFF2E, - Eisu_Shift = 0xFF2F, - Eisu_toggle = 0xFF30, - Kanji_Bangou = 0xFF37, - Zen_Koho = 0xFF3D, - Mae_Koho = 0xFF3E, - Home = 0xFF50, - Left = 0xFF51, - Up = 0xFF52, - Right = 0xFF53, - Down = 0xFF54, - Prior = 0xFF55, - Page_Up = 0xFF55, - Next = 0xFF56, - Page_Down = 0xFF56, - End = 0xFF57, - Begin = 0xFF58, - Select = 0xFF60, - Print = 0xFF61, - Execute = 0xFF62, - Insert = 0xFF63, - Undo = 0xFF65, - Redo = 0xFF66, - Menu = 0xFF67, - Find = 0xFF68, - Cancel = 0xFF69, - Help = 0xFF6A, - Break = 0xFF6B, - Mode_switch = 0xFF7E, - script_switch = 0xFF7E, - Num_Lock = 0xFF7F, - KP_Space = 0xFF80, - KP_Tab = 0xFF89, - KP_Enter = 0xFF8D, - KP_F1 = 0xFF91, - KP_F2 = 0xFF92, - KP_F3 = 0xFF93, - KP_F4 = 0xFF94, - KP_Home = 0xFF95, - KP_Left = 0xFF96, - KP_Up = 0xFF97, - KP_Right = 0xFF98, - KP_Down = 0xFF99, - KP_Prior = 0xFF9A, - KP_Page_Up = 0xFF9A, - KP_Next = 0xFF9B, - KP_Page_Down = 0xFF9B, - KP_End = 0xFF9C, - KP_Begin = 0xFF9D, - KP_Insert = 0xFF9E, - KP_Delete = 0xFF9F, - KP_Equal = 0xFFBD, - KP_Multiply = 0xFFAA, - KP_Add = 0xFFAB, - KP_Separator = 0xFFAC, - KP_Subtract = 0xFFAD, - KP_Decimal = 0xFFAE, - KP_Divide = 0xFFAF, - KP_0 = 0xFFB0, - KP_1 = 0xFFB1, - KP_2 = 0xFFB2, - KP_3 = 0xFFB3, - KP_4 = 0xFFB4, - KP_5 = 0xFFB5, - KP_6 = 0xFFB6, - KP_7 = 0xFFB7, - KP_8 = 0xFFB8, - KP_9 = 0xFFB9, - F1 = 0xFFBE, - F2 = 0xFFBF, - F3 = 0xFFC0, - F4 = 0xFFC1, - F5 = 0xFFC2, - F6 = 0xFFC3, - F7 = 0xFFC4, - F8 = 0xFFC5, - F9 = 0xFFC6, - F10 = 0xFFC7, - F11 = 0xFFC8, - L1 = 0xFFC8, - F12 = 0xFFC9, - L2 = 0xFFC9, - F13 = 0xFFCA, - L3 = 0xFFCA, - F14 = 0xFFCB, - L4 = 0xFFCB, - F15 = 0xFFCC, - L5 = 0xFFCC, - F16 = 0xFFCD, - L6 = 0xFFCD, - F17 = 0xFFCE, - L7 = 0xFFCE, - F18 = 0xFFCF, - L8 = 0xFFCF, - F19 = 0xFFD0, - L9 = 0xFFD0, - F20 = 0xFFD1, - L10 = 0xFFD1, - F21 = 0xFFD2, - R1 = 0xFFD2, - F22 = 0xFFD3, - R2 = 0xFFD3, - F23 = 0xFFD4, - R3 = 0xFFD4, - F24 = 0xFFD5, - R4 = 0xFFD5, - F25 = 0xFFD6, - R5 = 0xFFD6, - F26 = 0xFFD7, - R6 = 0xFFD7, - F27 = 0xFFD8, - R7 = 0xFFD8, - F28 = 0xFFD9, - R8 = 0xFFD9, - F29 = 0xFFDA, - R9 = 0xFFDA, - F30 = 0xFFDB, - R10 = 0xFFDB, - F31 = 0xFFDC, - R11 = 0xFFDC, - F32 = 0xFFDD, - R12 = 0xFFDD, - F33 = 0xFFDE, - R13 = 0xFFDE, - F34 = 0xFFDF, - R14 = 0xFFDF, - F35 = 0xFFE0, - R15 = 0xFFE0, - Shift_L = 0xFFE1, - Shift_R = 0xFFE2, - Control_L = 0xFFE3, - Control_R = 0xFFE4, - Caps_Lock = 0xFFE5, - Shift_Lock = 0xFFE6, - Meta_L = 0xFFE7, - Meta_R = 0xFFE8, - Alt_L = 0xFFE9, - Alt_R = 0xFFEA, - Super_L = 0xFFEB, - Super_R = 0xFFEC, - Hyper_L = 0xFFED, - Hyper_R = 0xFFEE, - XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ - XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ - XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ - Null = 0 -}; - -// modifiers, modified to fit a UINT16 - -enum Modifier { - NULL_MASK = 0, - - SHIFT_MASK = 1 << 0, - LOCK_MASK = 1 << 1, - CONTROL_MASK = 1 << 2, - ALT_MASK = 1 << 3, - MOD1_MASK = 1 << 3, - MOD2_MASK = 1 << 4, - MOD3_MASK = 1 << 5, - MOD4_MASK = 1 << 6, - MOD5_MASK = 1 << 7, - - HANDLED_MASK = 1 << 8, // 24 - IGNORED_MASK = 1 << 9, // 25 - FORWARD_MASK = 1 << 9, // 25 - - SUPER_MASK = 1 << 10, // 26 - HYPER_MASK = 1 << 11, // 27 - META_MASK = 1 << 12, // 28 - - RELEASE_MASK = 1 << 14, // 30 - - MODIFIER_MASK = 0x2fff -}; - -} // namespace ibus +#pragma once +#include + +struct KeyInfo { + UINT repeatCount : 16; + UINT scanCode : 8; + UINT isExtended : 1; + UINT reserved : 4; + UINT contextCode : 1; + UINT prevKeyState : 1; + UINT isKeyUp : 1; + + KeyInfo(LPARAM lparam) { *this = *reinterpret_cast(&lparam); } + + operator UINT32() { return *reinterpret_cast(this); } +}; + +namespace weasel { +struct KeyEvent { + UINT keycode : 16; + UINT mask : 16; + KeyEvent() : keycode(0), mask(0) {} + KeyEvent(UINT _keycode, UINT _mask) : keycode(_keycode), mask(_mask) {} + KeyEvent(UINT x) { *reinterpret_cast(this) = x; } + operator UINT32 const() const { + return *reinterpret_cast(this); + } +}; + +inline bool IsTextVirtualKey(WPARAM wParam) { + return (wParam >= L'0' && wParam <= L'9') || + (wParam >= L'A' && wParam <= L'Z') || + (wParam >= VK_NUMPAD0 && wParam <= VK_DIVIDE) || + (wParam >= VK_OEM_1 && wParam <= VK_OEM_102) || wParam == VK_PACKET || + wParam == VK_SPACE; +} + +inline bool IsModeSwitchVirtualKey(WPARAM wParam) { + return wParam == VK_SHIFT || wParam == VK_LSHIFT || wParam == VK_RSHIFT || + wParam == VK_CONTROL || wParam == VK_LCONTROL || + wParam == VK_RCONTROL || wParam == VK_CAPITAL; +} + +inline bool ShouldEatTestKeyEvent(bool service_available, + bool ascii_mode, + bool capslock_simulation, + bool composing, + bool shortcut_modifier, + bool known_key, + WPARAM wParam) { + if (!service_available) + return false; + if (!known_key) + return false; + if (capslock_simulation) + return true; + if (IsModeSwitchVirtualKey(wParam)) + return true; + if (shortcut_modifier && !composing) + return false; + if (composing) + return true; + return !ascii_mode && IsTextVirtualKey(wParam); +} + +inline bool ShouldEatTestKeyEvent(bool ascii_mode, + bool capslock_simulation, + bool composing, + bool shortcut_modifier, + bool known_key, + WPARAM wParam) { + return ShouldEatTestKeyEvent(true, ascii_mode, capslock_simulation, composing, + shortcut_modifier, known_key, wParam); +} + +struct KeyEventTestCache { + KeyEventTestCache() : entries(), next_entry(0), matched_entry(-1) {} + + bool Matches(bool is_key_up, WPARAM wParam, LPARAM lParam) const { + matched_entry = -1; + UINT32 key_identity = ComparableLParam(lParam); + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (entries[i].valid && entries[i].key_up == is_key_up && + entries[i].w_param == wParam && + ComparableLParam(entries[i].l_param) == key_identity) { + matched_entry = static_cast(i); + return true; + } + } + return false; + } + + void Store(bool is_key_up, WPARAM wParam, LPARAM lParam, BOOL is_eaten) { + size_t entry = next_entry; + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (!entries[i].valid || + (entries[i].key_up == is_key_up && entries[i].w_param == wParam && + entries[i].l_param == lParam)) { + entry = i; + break; + } + } + entries[entry].valid = true; + entries[entry].key_up = is_key_up; + entries[entry].w_param = wParam; + entries[entry].l_param = lParam; + entries[entry].eaten = is_eaten ? TRUE : FALSE; + matched_entry = static_cast(entry); + next_entry = (entry + 1) % ENTRY_COUNT; + } + + BOOL Eaten() const { + if (matched_entry < 0) + return FALSE; + return entries[matched_entry].eaten; + } + + void Clear() { + for (size_t i = 0; i < ENTRY_COUNT; ++i) + entries[i] = Entry(); + next_entry = 0; + matched_entry = -1; + } + + void RemoveMatched() { + if (matched_entry >= 0) + entries[matched_entry].valid = false; + matched_entry = -1; + } + + void Remove(bool is_key_up, WPARAM wParam, LPARAM lParam) { + UINT32 key_identity = PhysicalKeyIdentity(lParam); + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (entries[i].valid && entries[i].key_up == is_key_up && + entries[i].w_param == wParam && + PhysicalKeyIdentity(entries[i].l_param) == key_identity) { + entries[i].valid = false; + } + } + matched_entry = -1; + } + + static UINT32 ComparableLParam(LPARAM lParam) { + // Repeat count and previous-key-state may differ between TestKey and Key + // callbacks for the same physical key in some hosts. + return static_cast(lParam) & 0xA1FF0000; + } + + static UINT32 PhysicalKeyIdentity(LPARAM lParam) { + return ComparableLParam(lParam) & ~0x80000000u; + } + + struct Entry { + Entry() + : valid(false), key_up(false), w_param(0), l_param(0), eaten(FALSE) {} + + bool valid; + bool key_up; + WPARAM w_param; + LPARAM l_param; + BOOL eaten; + }; + + enum { ENTRY_COUNT = 4 }; + Entry entries[ENTRY_COUNT]; + size_t next_entry; + mutable int matched_entry; +}; + +struct ActiveKeyDownGuard { + ActiveKeyDownGuard() : entries(), next_entry(0) {} + + bool ShouldSuppress(WPARAM wParam, LPARAM lParam) { + KeyInfo info(lParam); + if (info.isKeyUp || info.prevKeyState) + return false; + + UINT32 identity = KeyEventTestCache::PhysicalKeyIdentity(lParam); + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (entries[i].valid && entries[i].w_param == wParam && + entries[i].key_identity == identity) { + return true; + } + } + + return false; + } + + void Remember(WPARAM wParam, LPARAM lParam) { + KeyInfo info(lParam); + if (info.isKeyUp || info.prevKeyState) + return; + + UINT32 identity = KeyEventTestCache::PhysicalKeyIdentity(lParam); + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (entries[i].valid && entries[i].w_param == wParam && + entries[i].key_identity == identity) { + return; + } + } + + size_t entry = next_entry; + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (!entries[i].valid) { + entry = i; + break; + } + } + entries[entry].valid = true; + entries[entry].w_param = wParam; + entries[entry].key_identity = identity; + next_entry = (entry + 1) % ENTRY_COUNT; + } + + void Release(WPARAM wParam, LPARAM lParam) { + UINT32 identity = KeyEventTestCache::PhysicalKeyIdentity(lParam); + for (size_t i = 0; i < ENTRY_COUNT; ++i) { + if (entries[i].valid && entries[i].w_param == wParam && + entries[i].key_identity == identity) { + entries[i].valid = false; + } + } + } + + void Reset() { + for (size_t i = 0; i < ENTRY_COUNT; ++i) + entries[i] = Entry(); + next_entry = 0; + } + + struct Entry { + Entry() : valid(false), w_param(0), key_identity(0) {} + + bool valid; + WPARAM w_param; + UINT32 key_identity; + }; + + enum { ENTRY_COUNT = 8 }; + Entry entries[ENTRY_COUNT]; + size_t next_entry; +}; + +struct KeyEventTestCacheReset { + KeyEventTestCacheReset() : pending(false) {} + + void Mark() { pending.store(true); } + + bool Pending() const { return pending.load(); } + + bool Apply(KeyEventTestCache& cache) { + if (!pending.exchange(false)) + return false; + cache.Clear(); + return true; + } + + std::atomic_bool pending; +}; + +} // namespace weasel + +bool ConvertKeyEvent(UINT vkey, + KeyInfo kinfo, + const LPBYTE keyState, + weasel::KeyEvent& result); + +namespace ibus { +// keycodes + +enum Keycode { + VoidSymbol = 0xFFFFFF, + space = 0x020, + grave = 0x060, + BackSpace = 0xFF08, + Tab = 0xFF09, + Linefeed = 0xFF0A, + Clear = 0xFF0B, + Return = 0xFF0D, + Pause = 0xFF13, + Scroll_Lock = 0xFF14, + Sys_Req = 0xFF15, + Escape = 0xFF1B, + Delete = 0xFFFF, + Multi_key = 0xFF20, + Codeinput = 0xFF37, + SingleCandidate = 0xFF3C, + MultipleCandidate = 0xFF3D, + PreviousCandidate = 0xFF3E, + Kanji = 0xFF21, + Muhenkan = 0xFF22, + Henkan_Mode = 0xFF23, + Henkan = 0xFF23, + Romaji = 0xFF24, + Hiragana = 0xFF25, + Katakana = 0xFF26, + Hiragana_Katakana = 0xFF27, + Zenkaku = 0xFF28, + Hankaku = 0xFF29, + Zenkaku_Hankaku = 0xFF2A, + Touroku = 0xFF2B, + Massyo = 0xFF2C, + Kana_Lock = 0xFF2D, + Kana_Shift = 0xFF2E, + Eisu_Shift = 0xFF2F, + Eisu_toggle = 0xFF30, + Kanji_Bangou = 0xFF37, + Zen_Koho = 0xFF3D, + Mae_Koho = 0xFF3E, + Home = 0xFF50, + Left = 0xFF51, + Up = 0xFF52, + Right = 0xFF53, + Down = 0xFF54, + Prior = 0xFF55, + Page_Up = 0xFF55, + Next = 0xFF56, + Page_Down = 0xFF56, + End = 0xFF57, + Begin = 0xFF58, + Select = 0xFF60, + Print = 0xFF61, + Execute = 0xFF62, + Insert = 0xFF63, + Undo = 0xFF65, + Redo = 0xFF66, + Menu = 0xFF67, + Find = 0xFF68, + Cancel = 0xFF69, + Help = 0xFF6A, + Break = 0xFF6B, + Mode_switch = 0xFF7E, + script_switch = 0xFF7E, + Num_Lock = 0xFF7F, + KP_Space = 0xFF80, + KP_Tab = 0xFF89, + KP_Enter = 0xFF8D, + KP_F1 = 0xFF91, + KP_F2 = 0xFF92, + KP_F3 = 0xFF93, + KP_F4 = 0xFF94, + KP_Home = 0xFF95, + KP_Left = 0xFF96, + KP_Up = 0xFF97, + KP_Right = 0xFF98, + KP_Down = 0xFF99, + KP_Prior = 0xFF9A, + KP_Page_Up = 0xFF9A, + KP_Next = 0xFF9B, + KP_Page_Down = 0xFF9B, + KP_End = 0xFF9C, + KP_Begin = 0xFF9D, + KP_Insert = 0xFF9E, + KP_Delete = 0xFF9F, + KP_Equal = 0xFFBD, + KP_Multiply = 0xFFAA, + KP_Add = 0xFFAB, + KP_Separator = 0xFFAC, + KP_Subtract = 0xFFAD, + KP_Decimal = 0xFFAE, + KP_Divide = 0xFFAF, + KP_0 = 0xFFB0, + KP_1 = 0xFFB1, + KP_2 = 0xFFB2, + KP_3 = 0xFFB3, + KP_4 = 0xFFB4, + KP_5 = 0xFFB5, + KP_6 = 0xFFB6, + KP_7 = 0xFFB7, + KP_8 = 0xFFB8, + KP_9 = 0xFFB9, + F1 = 0xFFBE, + F2 = 0xFFBF, + F3 = 0xFFC0, + F4 = 0xFFC1, + F5 = 0xFFC2, + F6 = 0xFFC3, + F7 = 0xFFC4, + F8 = 0xFFC5, + F9 = 0xFFC6, + F10 = 0xFFC7, + F11 = 0xFFC8, + L1 = 0xFFC8, + F12 = 0xFFC9, + L2 = 0xFFC9, + F13 = 0xFFCA, + L3 = 0xFFCA, + F14 = 0xFFCB, + L4 = 0xFFCB, + F15 = 0xFFCC, + L5 = 0xFFCC, + F16 = 0xFFCD, + L6 = 0xFFCD, + F17 = 0xFFCE, + L7 = 0xFFCE, + F18 = 0xFFCF, + L8 = 0xFFCF, + F19 = 0xFFD0, + L9 = 0xFFD0, + F20 = 0xFFD1, + L10 = 0xFFD1, + F21 = 0xFFD2, + R1 = 0xFFD2, + F22 = 0xFFD3, + R2 = 0xFFD3, + F23 = 0xFFD4, + R3 = 0xFFD4, + F24 = 0xFFD5, + R4 = 0xFFD5, + F25 = 0xFFD6, + R5 = 0xFFD6, + F26 = 0xFFD7, + R6 = 0xFFD7, + F27 = 0xFFD8, + R7 = 0xFFD8, + F28 = 0xFFD9, + R8 = 0xFFD9, + F29 = 0xFFDA, + R9 = 0xFFDA, + F30 = 0xFFDB, + R10 = 0xFFDB, + F31 = 0xFFDC, + R11 = 0xFFDC, + F32 = 0xFFDD, + R12 = 0xFFDD, + F33 = 0xFFDE, + R13 = 0xFFDE, + F34 = 0xFFDF, + R14 = 0xFFDF, + F35 = 0xFFE0, + R15 = 0xFFE0, + Shift_L = 0xFFE1, + Shift_R = 0xFFE2, + Control_L = 0xFFE3, + Control_R = 0xFFE4, + Caps_Lock = 0xFFE5, + Shift_Lock = 0xFFE6, + Meta_L = 0xFFE7, + Meta_R = 0xFFE8, + Alt_L = 0xFFE9, + Alt_R = 0xFFEA, + Super_L = 0xFFEB, + Super_R = 0xFFEC, + Hyper_L = 0xFFED, + Hyper_R = 0xFFEE, + XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ + XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ + XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ + Null = 0 +}; + +// modifiers, modified to fit a UINT16 + +enum Modifier { + NULL_MASK = 0, + + SHIFT_MASK = 1 << 0, + LOCK_MASK = 1 << 1, + CONTROL_MASK = 1 << 2, + ALT_MASK = 1 << 3, + MOD1_MASK = 1 << 3, + MOD2_MASK = 1 << 4, + MOD3_MASK = 1 << 5, + MOD4_MASK = 1 << 6, + MOD5_MASK = 1 << 7, + + HANDLED_MASK = 1 << 8, // 24 + IGNORED_MASK = 1 << 9, // 25 + FORWARD_MASK = 1 << 9, // 25 + + SUPER_MASK = 1 << 10, // 26 + HYPER_MASK = 1 << 11, // 27 + META_MASK = 1 << 12, // 28 + + RELEASE_MASK = 1 << 14, // 30 + + MODIFIER_MASK = 0x2fff +}; + +} // namespace ibus diff --git a/include/PipeChannel.h b/include/PipeChannel.h index b747a49e6..2cf725369 100644 --- a/include/PipeChannel.h +++ b/include/PipeChannel.h @@ -27,13 +27,15 @@ class PipeChannelBase { protected: /* To ensure connection before operation */ bool _Ensure(); + /* Try to connect once without waiting for a pipe instance. */ + bool _EnsureOnce(); /* Connect pipe as client */ HANDLE _Connect(const wchar_t* name); /* To reconnect message pipe */ void _Reconnect(); /* Try to connect for one time */ HANDLE _TryConnect(); - size_t _WritePipe(HANDLE p, size_t s, char* b); + size_t _WritePipe(HANDLE p, size_t s, char* b, bool flush = true); void _FinalizePipe(HANDLE& p); void _Receive(HANDLE pipe, LPVOID msg, size_t rec_len); /* Try to get a connection from client */ @@ -93,6 +95,7 @@ class PipeChannel : public PipeChannelBase { /* Common pipe operations */ bool Connect() { return _Ensure(); } + bool TryConnect() { return _EnsureOnce(); } bool Connected() const { HANDLE* phandle = _GetPipeHandle(); return !_Invalid(*phandle); @@ -124,6 +127,25 @@ class PipeChannel : public PipeChannelBase { return _ReceiveResponse(); } + bool TryTransact(Msg& msg, _TyRes* result) { + HANDLE* phandle = _GetPipeHandle(); + if (!result) + return false; + if (_Invalid(*phandle) && !_EnsureOnce()) { + ClearBufferStream(); + return false; + } + try { + _SendOnce(*phandle, msg, false); + *result = _ReceiveResponse(); + return true; + } catch (...) { + _FinalizePipe(*phandle); + ClearBufferStream(); + return false; + } + } + void ClearBufferStream() { auto ctx = _GetContext(); ctx->has_body = false; @@ -149,6 +171,16 @@ class PipeChannel : public PipeChannelBase { protected: void _Send(HANDLE pipe, Msg& msg) { + try { + _SendOnce(pipe, msg); + } catch (...) { + _Reconnect(); + HANDLE* phandle = _GetPipeHandle(); + _SendOnce(*phandle, msg); + } + } + + void _SendOnce(HANDLE pipe, Msg& msg, bool flush = true) { auto ctx = _GetContext(); char* pbuff = ctx->buffer.get(); DWORD lwritten = 0; @@ -165,12 +197,7 @@ class PipeChannel : public PipeChannelBase { if (data_sz > buff_size) data_sz = buff_size; - try { - _WritePipe(pipe, data_sz, pbuff); - } catch (...) { - _Reconnect(); - _WritePipe(pipe, data_sz, pbuff); - } + _WritePipe(pipe, data_sz, pbuff, flush); ClearBufferStream(); } diff --git a/include/RimeWithWeasel.h b/include/RimeWithWeasel.h index ab20d8d76..7e4e82f7d 100644 --- a/include/RimeWithWeasel.h +++ b/include/RimeWithWeasel.h @@ -1,123 +1,217 @@ -#pragma once -#include -#include -#include -#include -#include - -#include - -struct CaseInsensitiveCompare { - bool operator()(const std::string& str1, const std::string& str2) const { - std::string str1Lower, str2Lower; - std::transform(str1.begin(), str1.end(), std::back_inserter(str1Lower), - [](char c) { return std::tolower(c); }); - std::transform(str2.begin(), str2.end(), std::back_inserter(str2Lower), - [](char c) { return std::tolower(c); }); - return str1Lower < str2Lower; - } -}; - -typedef std::map AppOptions; -typedef std::map - AppOptionsByAppName; - -struct SessionStatus { - SessionStatus() : style(weasel::UIStyle()), __synced(false), session_id(0) { - RIME_STRUCT(RimeStatus, status); - } - weasel::UIStyle style; - RimeStatus status; - bool __synced; - RimeSessionId session_id; -}; -typedef std::map SessionStatusMap; -typedef DWORD WeaselSessionId; -class RimeWithWeaselHandler : public weasel::RequestHandler { - public: - RimeWithWeaselHandler(weasel::UI* ui); - virtual ~RimeWithWeaselHandler(); - virtual void Initialize(); - virtual void Finalize(); - virtual DWORD FindSession(WeaselSessionId ipc_id); - virtual DWORD AddSession(LPWSTR buffer, EatLine eat = 0); - virtual DWORD RemoveSession(WeaselSessionId ipc_id); - virtual BOOL ProcessKeyEvent(weasel::KeyEvent keyEvent, - WeaselSessionId ipc_id, - EatLine eat); - virtual void CommitComposition(WeaselSessionId ipc_id); - virtual void ClearComposition(WeaselSessionId ipc_id); - virtual void SelectCandidateOnCurrentPage(size_t index, - WeaselSessionId ipc_id); - virtual bool HighlightCandidateOnCurrentPage(size_t index, - WeaselSessionId ipc_id, - EatLine eat); - virtual bool ChangePage(bool backward, WeaselSessionId ipc_id, EatLine eat); - virtual void FocusIn(DWORD param, WeaselSessionId ipc_id); - virtual void FocusOut(DWORD param, WeaselSessionId ipc_id); - virtual void UpdateInputPosition(RECT const& rc, WeaselSessionId ipc_id); - virtual void StartMaintenance(); - virtual void EndMaintenance(); - virtual void SetOption(WeaselSessionId ipc_id, - const std::string& opt, - bool val); - virtual void UpdateColorTheme(BOOL darkMode); - - void OnUpdateUI(std::function const& cb); - - private: - void _Setup(); - bool _IsDeployerRunning(); - void _UpdateUI(WeaselSessionId ipc_id); - void _LoadSchemaSpecificSettings(WeaselSessionId ipc_id, - const std::string& schema_id); - void _LoadAppInlinePreeditSet(WeaselSessionId ipc_id, - bool ignore_app_name = false); - bool _ShowMessage(weasel::Context& ctx, weasel::Status& status); - bool _Respond(WeaselSessionId ipc_id, EatLine eat); - void _ReadClientInfo(WeaselSessionId ipc_id, LPWSTR buffer); - void _GetCandidateInfo(weasel::CandidateInfo& cinfo, RimeContext& ctx); - void _GetStatus(weasel::Status& stat, - WeaselSessionId ipc_id, - weasel::Context& ctx); - void _GetContext(weasel::Context& ctx, RimeSessionId session_id); - void _UpdateShowNotifications(RimeConfig* config, bool initialize = false); - - void _UpdateInlinePreeditStatus(WeaselSessionId ipc_id); - - RimeSessionId to_session_id(WeaselSessionId ipc_id) { - return m_session_status_map[ipc_id].session_id; - } - SessionStatus& get_session_status(WeaselSessionId ipc_id) { - return m_session_status_map[ipc_id]; - } - SessionStatus& new_session_status(WeaselSessionId ipc_id) { - return m_session_status_map[ipc_id] = SessionStatus(); - } - - AppOptionsByAppName m_app_options; - weasel::UI* m_ui; // reference - DWORD m_active_session; - bool m_disabled; - std::string m_last_schema_id; - std::string m_last_app_name; - weasel::UIStyle m_base_style; - std::map m_show_notifications; - std::map m_show_notifications_base; - std::function _UpdateUICallback; - - static void OnNotify(void* context_object, - uintptr_t session_id, - const char* message_type, - const char* message_value); - static std::string m_message_type; - static std::string m_message_value; - static std::string m_message_label; - static std::string m_option_name; - static std::mutex m_notifier_mutex; - SessionStatusMap m_session_status_map; - bool m_current_dark_mode; - bool m_global_ascii_mode; - int m_show_notifications_time; - DWORD m_pid; -}; +#pragma once +#include +#include +#include +#include +#include + +#include + +struct CaseInsensitiveCompare { + bool operator()(const std::string& str1, const std::string& str2) const { + std::string str1Lower, str2Lower; + std::transform(str1.begin(), str1.end(), std::back_inserter(str1Lower), + [](char c) { return std::tolower(c); }); + std::transform(str2.begin(), str2.end(), std::back_inserter(str2Lower), + [](char c) { return std::tolower(c); }); + return str1Lower < str2Lower; + } +}; + +typedef std::map AppOptions; +typedef std::map + AppOptionsByAppName; + +struct SessionStatus { + SessionStatus() : style(weasel::UIStyle()), __synced(false), session_id(0) { + RIME_STRUCT(RimeStatus, status); + } + weasel::UIStyle style; + RimeStatus status; + bool __synced; + RimeSessionId session_id; +}; +typedef std::map SessionStatusMap; +typedef DWORD WeaselSessionId; + +struct RimeTrayIconSignature { + RimeTrayIconSignature() + : valid(false), + display_tray_icon(false), + disabled(false), + ascii_mode(false) {} + + static RimeTrayIconSignature From(const weasel::UIStyle& style, + const weasel::Status& status) { + RimeTrayIconSignature signature; + signature.valid = true; + signature.display_tray_icon = style.display_tray_icon; + signature.disabled = status.disabled; + signature.ascii_mode = status.ascii_mode; + signature.current_zhung_icon = style.current_zhung_icon; + signature.current_ascii_icon = style.current_ascii_icon; + return signature; + } + + bool operator==(const RimeTrayIconSignature& rhs) const { + return valid == rhs.valid && display_tray_icon == rhs.display_tray_icon && + disabled == rhs.disabled && ascii_mode == rhs.ascii_mode && + current_zhung_icon == rhs.current_zhung_icon && + current_ascii_icon == rhs.current_ascii_icon; + } + + bool operator!=(const RimeTrayIconSignature& rhs) const { + return !(*this == rhs); + } + + bool valid; + bool display_tray_icon; + bool disabled; + bool ascii_mode; + std::wstring current_zhung_icon; + std::wstring current_ascii_icon; +}; + +struct RimeUiStatusSnapshot { + RimeUiStatusSnapshot() : has_status(false) {} + + static RimeUiStatusSnapshot From(const RimeStatus& rime_status) { + RimeUiStatusSnapshot snapshot; + const char* schema_id = rime_status.schema_id ? rime_status.schema_id : ""; + const char* schema_name = + rime_status.schema_name ? rime_status.schema_name : ""; + snapshot.has_status = true; + snapshot.schema_id = schema_id; + snapshot.status.schema_name = u8tow(schema_name); + snapshot.status.schema_id = u8tow(schema_id); + snapshot.status.ascii_mode = !!rime_status.is_ascii_mode; + snapshot.status.composing = !!rime_status.is_composing; + snapshot.status.disabled = !!rime_status.is_disabled; + snapshot.status.full_shape = !!rime_status.is_full_shape; + return snapshot; + } + + bool has_status; + std::string schema_id; + weasel::Status status; +}; + +inline bool RimeUiNeedsUpdate(const weasel::Context& current_context, + const weasel::Status& current_status, + const weasel::Context& next_context, + const weasel::Status& next_status) { + return current_context != next_context || !(current_status == next_status); +} + +inline bool ShouldSuppressInlineOptionNotification( + const std::string& message_type, + const std::string& message_value) { + return message_type == "option" && + (message_value == "ascii_mode" || message_value == "!ascii_mode"); +} + +class RimeWithWeaselHandler : public weasel::RequestHandler { + public: + RimeWithWeaselHandler(weasel::UI* ui); + virtual ~RimeWithWeaselHandler(); + virtual void Initialize(); + virtual void Finalize(); + virtual DWORD FindSession(WeaselSessionId ipc_id); + virtual DWORD AddSession(LPWSTR buffer, EatLine eat = 0); + virtual DWORD RemoveSession(WeaselSessionId ipc_id); + virtual BOOL ProcessKeyEvent(weasel::KeyEvent keyEvent, + WeaselSessionId ipc_id, + EatLine eat); + virtual void CommitComposition(WeaselSessionId ipc_id); + virtual void ClearComposition(WeaselSessionId ipc_id); + virtual void SelectCandidateOnCurrentPage(size_t index, + WeaselSessionId ipc_id); + virtual bool HighlightCandidateOnCurrentPage(size_t index, + WeaselSessionId ipc_id, + EatLine eat); + virtual bool ChangePage(bool backward, WeaselSessionId ipc_id, EatLine eat); + virtual void FocusIn(DWORD param, WeaselSessionId ipc_id); + virtual void FocusOut(DWORD param, WeaselSessionId ipc_id); + virtual void UpdateInputPosition(RECT const& rc, WeaselSessionId ipc_id); + virtual void StartMaintenance(); + virtual void EndMaintenance( + DWORD result = weasel::WEASEL_IPC_MAINTENANCE_RESULT_NONE); + virtual void NotifyService(DWORD notification); + virtual void SetOption(WeaselSessionId ipc_id, + const std::string& opt, + bool val); + virtual void UpdateColorTheme(BOOL darkMode); + + void OnUpdateUI(std::function const& cb); + void OnMaintenanceResult(std::function const& cb); + void OnServiceNotification(std::function const& cb); + + private: + void _Setup(); + bool _IsDeployerRunning(); + void _UpdateUI(WeaselSessionId ipc_id, + const RimeUiStatusSnapshot* status_snapshot = nullptr); + void _LoadSchemaSpecificSettings(WeaselSessionId ipc_id, + const std::string& schema_id); + void _LoadAppInlinePreeditSet(WeaselSessionId ipc_id, + bool ignore_app_name = false); + bool _ShowMessage(weasel::Context& ctx, weasel::Status& status); + bool _Respond(WeaselSessionId ipc_id, + EatLine eat, + RimeUiStatusSnapshot* status_snapshot = nullptr, + bool* has_commit = nullptr); + void _ReadClientInfo(WeaselSessionId ipc_id, LPWSTR buffer); + void _GetCandidateInfo(weasel::CandidateInfo& cinfo, RimeContext& ctx); + void _GetStatus(weasel::Status& stat, + WeaselSessionId ipc_id, + weasel::Context& ctx); + void _ApplyStatusSnapshot(weasel::Status& stat, + WeaselSessionId ipc_id, + weasel::Context& ctx, + const RimeUiStatusSnapshot& snapshot); + void _GetContext(weasel::Context& ctx, RimeSessionId session_id); + void _UpdateShowNotifications(RimeConfig* config, bool initialize = false); + + void _UpdateInlinePreeditStatus(WeaselSessionId ipc_id); + void _RefreshTrayIconIfNeeded(RimeSessionId session_id); + void _InvalidateTrayIconSignature(); + void _SetDeployMessage(DWORD result); + + RimeSessionId to_session_id(WeaselSessionId ipc_id) { + return m_session_status_map[ipc_id].session_id; + } + SessionStatus& get_session_status(WeaselSessionId ipc_id) { + return m_session_status_map[ipc_id]; + } + SessionStatus& new_session_status(WeaselSessionId ipc_id) { + return m_session_status_map[ipc_id] = SessionStatus(); + } + + AppOptionsByAppName m_app_options; + weasel::UI* m_ui; // reference + DWORD m_active_session; + bool m_disabled; + std::string m_last_schema_id; + std::string m_last_app_name; + weasel::UIStyle m_base_style; + std::map m_show_notifications; + std::map m_show_notifications_base; + std::function _UpdateUICallback; + std::function _ServiceNotificationCallback; + + static void OnNotify(void* context_object, + uintptr_t session_id, + const char* message_type, + const char* message_value); + static std::string m_message_type; + static std::string m_message_value; + static std::string m_message_label; + static std::string m_option_name; + static std::mutex m_notifier_mutex; + SessionStatusMap m_session_status_map; + bool m_current_dark_mode; + bool m_global_ascii_mode; + int m_show_notifications_time; + DWORD m_pid; + RimeTrayIconSignature m_tray_icon_signature; +}; diff --git a/include/WeaselIPC.h b/include/WeaselIPC.h index d260ac2b7..738974a9b 100644 --- a/include/WeaselIPC.h +++ b/include/WeaselIPC.h @@ -1,178 +1,514 @@ -#pragma once -#include -#include -#include -#include -#include -#include - -#define WEASEL_IPC_WINDOW L"WeaselIPCWindow_1.0" -#define WEASEL_IPC_PIPE_NAME L"WeaselNamedPipe" - -#define WEASEL_IPC_METADATA_SIZE 1024 -#define WEASEL_IPC_BUFFER_SIZE (4 * 1024) -#define WEASEL_IPC_BUFFER_LENGTH (WEASEL_IPC_BUFFER_SIZE / sizeof(WCHAR)) -#define WEASEL_IPC_SHARED_MEMORY_SIZE \ - (sizeof(PipeMessage) + WEASEL_IPC_BUFFER_SIZE) - -enum WEASEL_IPC_COMMAND { - WEASEL_IPC_ECHO = (WM_APP + 1), - WEASEL_IPC_START_SESSION, - WEASEL_IPC_END_SESSION, - WEASEL_IPC_PROCESS_KEY_EVENT, - WEASEL_IPC_SHUTDOWN_SERVER, - WEASEL_IPC_FOCUS_IN, - WEASEL_IPC_FOCUS_OUT, - WEASEL_IPC_UPDATE_INPUT_POS, - WEASEL_IPC_START_MAINTENANCE, - WEASEL_IPC_END_MAINTENANCE, - WEASEL_IPC_COMMIT_COMPOSITION, - WEASEL_IPC_CLEAR_COMPOSITION, - WEASEL_IPC_TRAY_COMMAND, - WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, - WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, - WEASEL_IPC_CHANGE_PAGE, - WEASEL_IPC_LAST_COMMAND -}; - -namespace weasel { -struct PipeMessage { - WEASEL_IPC_COMMAND Msg; - DWORD wParam; - DWORD lParam; -}; - -struct IPCMetadata { - enum { WINDOW_CLASS_LENGTH = 64 }; - UINT32 server_hwnd; - WCHAR server_window_class[WINDOW_CLASS_LENGTH]; -}; - -// 處理請求之物件 -struct RequestHandler { - using EatLine = std::function; - RequestHandler() {} - virtual ~RequestHandler() {} - virtual void Initialize() {} - virtual void Finalize() {} - virtual DWORD FindSession(DWORD session_id) { return 0; } - virtual DWORD AddSession(LPWSTR buffer, EatLine eat = 0) { return 0; } - virtual DWORD RemoveSession(DWORD session_id) { return 0; } - virtual BOOL ProcessKeyEvent(KeyEvent keyEvent, - DWORD session_id, - EatLine eat) { - return FALSE; - } - virtual void CommitComposition(DWORD session_id) {} - virtual void ClearComposition(DWORD session_id) {} - virtual void SelectCandidateOnCurrentPage(size_t index, DWORD session_id) {} - virtual bool HighlightCandidateOnCurrentPage(size_t index, - DWORD session_id, - EatLine eat) { - return false; - } - virtual bool ChangePage(bool backward, DWORD session_id, EatLine eat) { - return false; - } - virtual void FocusIn(DWORD param, DWORD session_id) {} - virtual void FocusOut(DWORD param, DWORD session_id) {} - virtual void UpdateInputPosition(RECT const& rc, DWORD session_id) {} - virtual void StartMaintenance() {} - virtual void EndMaintenance() {} - virtual void SetOption(DWORD session_id, const std::string& opt, bool val) {} - virtual void UpdateColorTheme(BOOL darkMode) {} -}; - -// 處理server端回應之物件 -typedef std::function ResponseHandler; - -// 事件處理函數 -typedef std::function CommandHandler; - -// 啟動服務進程之物件 -typedef CommandHandler ServerLauncher; - -// IPC實現類聲明 - -class ClientImpl; -class ServerImpl; - -// IPC接口類 - -class Client { - public: - Client(); - virtual ~Client(); - - // 连接到服务,必要时启动服务进程 - bool Connect(ServerLauncher launcher = 0); - // 断开连接 - void Disconnect(); - // 终止服务 - void ShutdownServer(); - // 發起會話 - void StartSession(); - // 結束會話 - void EndSession(); - // 進入維護模式 - void StartMaintenance(); - // 退出維護模式 - void EndMaintenance(); - // 测试连接 - bool Echo(); - // 请求服务处理按键消息 - bool ProcessKeyEvent(KeyEvent const& keyEvent); - // 上屏正在編輯的文字 - bool CommitComposition(); - // 清除正在編輯的文字 - bool ClearComposition(); - // 选择当前页面编号为index的候选 - bool SelectCandidateOnCurrentPage(size_t index); - // 高亮当前页面编号为index的候选 - bool HighlightCandidateOnCurrentPage(size_t index); - // 翻页,backward = true 向前翻,false向后翻 - bool ChangePage(bool backward); - // 更新输入位置 - void UpdateInputPosition(RECT const& rc); - // 输入窗口获得焦点 - void FocusIn(); - // 输入窗口失去焦点 - void FocusOut(); - // 托盤菜單 - void TrayCommand(UINT menuId); - // 读取server返回的数据 - bool GetResponseData(ResponseHandler handler); - - private: - ClientImpl* m_pImpl; -}; - -class Server { - public: - Server(); - virtual ~Server(); - - // 初始化服务 - HWND Start(); - // 结束服务 - int Stop(); - // 消息循环 - int Run(); - - void SetRequestHandler(RequestHandler* pHandler); - void AddMenuHandler(UINT uID, CommandHandler handler); - HWND GetHWnd(); - - private: - ServerImpl* m_pImpl; -}; - -inline std::wstring GetPipeName() { - std::wstring pipe_name; - pipe_name += L"\\\\.\\pipe\\"; - pipe_name += getUsername(); - pipe_name += L"\\"; - pipe_name += WEASEL_IPC_PIPE_NAME; - return pipe_name; -} -} // namespace weasel +#pragma once +#include +#include +#include +#include +#include +#include + +#define WEASEL_IPC_WINDOW L"WeaselIPCWindow_1.0" +#define WEASEL_IPC_PIPE_NAME L"WeaselNamedPipe" + +#define WEASEL_IPC_METADATA_SIZE 1024 +#define WEASEL_IPC_BUFFER_SIZE (4 * 1024) +#define WEASEL_IPC_BUFFER_LENGTH (WEASEL_IPC_BUFFER_SIZE / sizeof(WCHAR)) +#define WEASEL_IPC_SHARED_MEMORY_SIZE \ + (sizeof(PipeMessage) + WEASEL_IPC_BUFFER_SIZE) + +enum WEASEL_IPC_COMMAND { + WEASEL_IPC_ECHO = (WM_APP + 1), + WEASEL_IPC_START_SESSION, + WEASEL_IPC_END_SESSION, + WEASEL_IPC_PROCESS_KEY_EVENT, + WEASEL_IPC_SHUTDOWN_SERVER, + WEASEL_IPC_FOCUS_IN, + WEASEL_IPC_FOCUS_OUT, + WEASEL_IPC_UPDATE_INPUT_POS, + WEASEL_IPC_START_MAINTENANCE, + WEASEL_IPC_END_MAINTENANCE, + WEASEL_IPC_COMMIT_COMPOSITION, + WEASEL_IPC_CLEAR_COMPOSITION, + WEASEL_IPC_TRAY_COMMAND, + WEASEL_IPC_SELECT_CANDIDATE_ON_CURRENT_PAGE, + WEASEL_IPC_HIGHLIGHT_CANDIDATE_ON_CURRENT_PAGE, + WEASEL_IPC_CHANGE_PAGE, + WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS, + WEASEL_IPC_NOTIFY_SERVICE, + WEASEL_IPC_LAST_COMMAND +}; + +namespace weasel { +enum WEASEL_IPC_KEY_EVENT_RESULT { + WEASEL_IPC_KEY_EVENT_PROCESSED = 0x01, + WEASEL_IPC_KEY_EVENT_EATEN = 0x02 +}; + +enum WEASEL_IPC_MAINTENANCE_RESULT { + WEASEL_IPC_MAINTENANCE_RESULT_NONE = 0, + WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS = 1, + WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE = 2 +}; + +enum WEASEL_IPC_SERVICE_NOTIFICATION { + WEASEL_IPC_SERVICE_NOTIFICATION_NONE = 0, + WEASEL_IPC_SERVICE_NOTIFICATION_EXITING = 3, + WEASEL_IPC_SERVICE_NOTIFICATION_RESTARTING = 4, + WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS = 5, + WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE = 6 +}; + +enum WEASEL_IPC_SHUTDOWN_REASON { + WEASEL_IPC_SHUTDOWN_REASON_NONE = 0, + WEASEL_IPC_SHUTDOWN_REASON_EXIT = 1, + WEASEL_IPC_SHUTDOWN_REASON_RESTART = 2, + WEASEL_IPC_SHUTDOWN_REASON_STOP = 3 +}; + +inline DWORD MakeKeyEventResult(BOOL eaten) { + return WEASEL_IPC_KEY_EVENT_PROCESSED | + (eaten ? WEASEL_IPC_KEY_EVENT_EATEN : 0); +} + +inline bool IsKeyEventResultProcessed(DWORD result) { + return (result & WEASEL_IPC_KEY_EVENT_PROCESSED) != 0; +} + +inline bool IsKeyEventResultEaten(DWORD result) { + return (result & WEASEL_IPC_KEY_EVENT_EATEN) != 0; +} + +inline bool ShouldEatKeyEvent(BOOL handled, BOOL has_commit) { + return handled || has_commit; +} + +inline bool ShouldEatKeyEvent(BOOL handled, + BOOL has_commit, + BOOL was_composing, + BOOL is_composing) { + return ShouldEatKeyEvent(handled, has_commit) || + (was_composing && !is_composing); +} + +inline bool IsMaintenanceDeployResult(DWORD result) { + return result == WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS || + result == WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE; +} + +inline const char* MaintenanceDeployMessageValue(DWORD result) { + return result == WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE ? "failure" + : "success"; +} + +inline DWORD ServiceStartupNotification(bool restart_requested, + bool restarted_existing_server) { + return restart_requested || restarted_existing_server + ? WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS + : WEASEL_IPC_SERVICE_NOTIFICATION_NONE; +} + +inline fs::path ServiceManualExitFlagPath() { + return WeaselLogPath() / L"weasel-service-manual-exit.flag"; +} + +inline void MarkServiceManualExit() { + try { + fs::path path = ServiceManualExitFlagPath(); + HANDLE file = + CreateFileW(path.c_str(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (file != INVALID_HANDLE_VALUE) { + CloseHandle(file); + WeaselDebugLog(L"WeaselIPC", + L"manual exit marked path=" + path.wstring()); + } else { + WeaselDebugLog(L"WeaselIPC", L"manual exit mark failed path=" + + path.wstring() + L" error=" + + std::to_wstring(GetLastError())); + } + } catch (...) { + } +} + +inline void ClearServiceManualExit() { + try { + fs::path path = ServiceManualExitFlagPath(); + BOOL deleted = DeleteFileW(path.c_str()); + DWORD error = deleted ? ERROR_SUCCESS : GetLastError(); + WeaselDebugLog(L"WeaselIPC", L"manual exit cleared path=" + path.wstring() + + L" deleted=" + std::to_wstring(deleted) + + L" error=" + std::to_wstring(error)); + } catch (...) { + } +} + +inline bool IsServiceManualExitMarked() { + try { + return fs::exists(ServiceManualExitFlagPath()); + } catch (...) { + return false; + } +} + +inline bool ShouldAutoRecoverService() { + return !IsServiceManualExitMarked(); +} + +inline LPCWSTR ServiceExecutableName() { + return L"WeaselServer.exe"; +} + +inline std::wstring ServiceInstanceMutexName() { + std::wstring name = L"(WEASEL)Furandōru-Sukāretto-"; + name += getUsername(); + return name; +} + +inline bool IsServiceInstanceMutexPresent() { + std::wstring name = ServiceInstanceMutexName(); + HANDLE mutex = OpenMutexW(SYNCHRONIZE, FALSE, name.c_str()); + if (!mutex) + return GetLastError() == ERROR_ACCESS_DENIED; + CloseHandle(mutex); + return true; +} + +inline LPCWSTR ServiceRestartArgument() { + return L"/restart"; +} + +inline LPCWSTR ServiceManualRestartArgument() { + return L"/restart-manual"; +} + +inline LPCWSTR ServiceRecoveryArgument() { + return L"/recover"; +} + +inline LPCWSTR ServiceStartupArgument() { + return L"/startup"; +} + +inline LPCWSTR ServiceStopArgument() { + return L"/stop"; +} + +inline std::wstring NormalizeServiceCommandLineArgument(LPCWSTR command_line) { + if (!command_line) + return L""; + + std::wstring argument = command_line; + while (!argument.empty() && iswspace(argument.front())) + argument.erase(0, 1); + while (!argument.empty() && iswspace(argument.back())) + argument.pop_back(); + if (argument.size() >= 2 && argument.front() == L'"' && + argument.back() == L'"') { + argument.erase(0, 1); + argument.pop_back(); + } + for (auto& ch : argument) + ch = static_cast(towlower(ch)); + return argument; +} + +inline std::wstring ServiceExecutablePath(const std::wstring& root_dir) { + if (root_dir.empty()) + return L""; + std::wstring path = root_dir; + wchar_t last = path[path.size() - 1]; + if (last != L'\\' && last != L'/') + path += L"\\"; + path += ServiceExecutableName(); + return path; +} + +inline bool IsServiceRestartCommandLine(LPCWSTR command_line) { + std::wstring argument = NormalizeServiceCommandLineArgument(command_line); + return argument == ServiceRestartArgument() || + argument == ServiceManualRestartArgument() || argument == L"/rerun"; +} + +inline bool IsServiceLegacyRestartCommandLine(LPCWSTR command_line) { + std::wstring argument = NormalizeServiceCommandLineArgument(command_line); + return argument == ServiceRestartArgument() || argument == L"/rerun"; +} + +inline bool IsServiceManualRestartCommandLine(LPCWSTR command_line) { + return NormalizeServiceCommandLineArgument(command_line) == + ServiceManualRestartArgument(); +} + +inline bool IsServiceRecoveryCommandLine(LPCWSTR command_line) { + return NormalizeServiceCommandLineArgument(command_line) == + ServiceRecoveryArgument(); +} + +inline bool IsServiceStartupCommandLine(LPCWSTR command_line) { + std::wstring argument = NormalizeServiceCommandLineArgument(command_line); + return argument == ServiceStartupArgument() || argument == L"/start"; +} + +inline bool IsImplicitServiceStartCommandLine(LPCWSTR command_line) { + return NormalizeServiceCommandLineArgument(command_line).empty(); +} + +inline bool IsServiceQuitCommandLine(LPCWSTR command_line) { + std::wstring argument = NormalizeServiceCommandLineArgument(command_line); + return argument == L"/q" || argument == L"/quit"; +} + +inline bool IsServiceStopCommandLine(LPCWSTR command_line) { + std::wstring argument = NormalizeServiceCommandLineArgument(command_line); + return argument == ServiceStopArgument() || argument == L"/stop-service"; +} + +inline bool IsServiceUpdateCommandLine(LPCWSTR command_line) { + return NormalizeServiceCommandLineArgument(command_line) == L"/update"; +} + +inline bool ShouldSuppressServiceRecoveryAfterManualExit(LPCWSTR command_line) { + return IsServiceRecoveryCommandLine(command_line) && + IsServiceManualExitMarked(); +} + +inline bool ShouldClearServiceManualExit(LPCWSTR command_line) { + return IsServiceManualRestartCommandLine(command_line) || + IsServiceStartupCommandLine(command_line); +} + +inline bool ShouldShutdownExistingService(LPCWSTR command_line) { + return IsServiceQuitCommandLine(command_line) || + IsServiceStopCommandLine(command_line) || + IsServiceRestartCommandLine(command_line); +} + +inline bool ShouldSuppressServiceStartAfterManualExit(LPCWSTR command_line) { + if (!IsServiceManualExitMarked()) + return false; + if (IsServiceManualRestartCommandLine(command_line) || + IsServiceStartupCommandLine(command_line) || + IsServiceQuitCommandLine(command_line) || + IsServiceStopCommandLine(command_line)) + return false; + if (IsServiceRestartCommandLine(command_line)) + return true; + return IsImplicitServiceStartCommandLine(command_line) || + IsServiceRecoveryCommandLine(command_line); +} + +inline bool IsTrayMenuSelectionCancelled(UINT menu_id) { + return menu_id == 0; +} + +inline std::wstring ProcessBaseName(std::wstring path) { + size_t slash = path.find_last_of(L"\\/"); + if (slash != std::wstring::npos) + path = path.substr(slash + 1); + for (auto& ch : path) + ch = static_cast(towlower(ch)); + return path; +} + +inline bool IsInteractiveLegacyRestartParent(const std::wstring& parent_name) { + std::wstring name = ProcessBaseName(parent_name); + return name == L"explorer.exe" || name == L"taskmgr.exe" || + name == L"weaseldeployer.exe" || name == L"weaselsetup.exe"; +} + +inline bool ShouldTreatLegacyRestartAsManual(LPCWSTR command_line, + const std::wstring& parent_name, + const std::wstring& parent_path) { + if (!IsServiceLegacyRestartCommandLine(command_line)) + return false; + if (parent_path.empty()) + return false; + return IsInteractiveLegacyRestartParent(parent_name.empty() ? parent_path + : parent_name); +} + +inline bool ServiceRootFromCommandLine(const std::wstring& command_line, + std::wstring& root_dir) { + root_dir.clear(); + if (command_line.empty()) + return false; + + std::wstring exe_name = ServiceExecutableName(); + size_t exe_pos = std::wstring::npos; + for (size_t i = 0; i + exe_name.size() <= command_line.size(); ++i) { + if (!_wcsnicmp(command_line.c_str() + i, exe_name.c_str(), + exe_name.size())) { + exe_pos = i; + break; + } + } + if (exe_pos == std::wstring::npos) + return false; + + std::wstring exe_path = command_line.substr(0, exe_pos + exe_name.size()); + while (!exe_path.empty() && iswspace(exe_path[0])) + exe_path.erase(0, 1); + if (!exe_path.empty() && exe_path[0] == L'"') + exe_path.erase(0, 1); + while (!exe_path.empty() && (iswspace(exe_path[exe_path.size() - 1]) || + exe_path[exe_path.size() - 1] == L'"')) { + exe_path.erase(exe_path.size() - 1); + } + + size_t slash_pos = exe_path.find_last_of(L"\\/"); + if (slash_pos == std::wstring::npos) + return false; + root_dir = exe_path.substr(0, slash_pos); + return !root_dir.empty(); +} + +struct PipeMessage { + WEASEL_IPC_COMMAND Msg; + DWORD wParam; + DWORD lParam; +}; + +struct IPCMetadata { + enum { WINDOW_CLASS_LENGTH = 64 }; + UINT32 server_hwnd; + WCHAR server_window_class[WINDOW_CLASS_LENGTH]; +}; + +// 處理請求之物件 +struct RequestHandler { + using EatLine = std::function; + RequestHandler() {} + virtual ~RequestHandler() {} + virtual void Initialize() {} + virtual void Finalize() {} + virtual DWORD FindSession(DWORD session_id) { return 0; } + virtual DWORD AddSession(LPWSTR buffer, EatLine eat = 0) { return 0; } + virtual DWORD RemoveSession(DWORD session_id) { return 0; } + virtual BOOL ProcessKeyEvent(KeyEvent keyEvent, + DWORD session_id, + EatLine eat) { + return FALSE; + } + virtual void CommitComposition(DWORD session_id) {} + virtual void ClearComposition(DWORD session_id) {} + virtual void SelectCandidateOnCurrentPage(size_t index, DWORD session_id) {} + virtual bool HighlightCandidateOnCurrentPage(size_t index, + DWORD session_id, + EatLine eat) { + return false; + } + virtual bool ChangePage(bool backward, DWORD session_id, EatLine eat) { + return false; + } + virtual void FocusIn(DWORD param, DWORD session_id) {} + virtual void FocusOut(DWORD param, DWORD session_id) {} + virtual void UpdateInputPosition(RECT const& rc, DWORD session_id) {} + virtual void StartMaintenance() {} + virtual void EndMaintenance( + DWORD result = WEASEL_IPC_MAINTENANCE_RESULT_NONE) {} + virtual void NotifyService(DWORD notification) {} + virtual void SetOption(DWORD session_id, const std::string& opt, bool val) {} + virtual void UpdateColorTheme(BOOL darkMode) {} +}; + +// 處理server端回應之物件 +typedef std::function ResponseHandler; + +// 事件處理函數 +typedef std::function CommandHandler; + +// 啟動服務進程之物件 +typedef CommandHandler ServerLauncher; + +// IPC實現類聲明 + +class ClientImpl; +class ServerImpl; + +// IPC接口類 + +class Client { + public: + Client(); + virtual ~Client(); + + // 连接到服务,必要时启动服务进程 + bool Connect(ServerLauncher launcher = 0); + // 仅尝试连接已运行的服务;服务不存在时立即返回 + bool TryConnect(); + // 重新建立会话 + bool Reconnect(ServerLauncher launcher = 0, bool wait_for_pipe = true); + // 断开连接 + void Disconnect(); + // 终止服务 + void ShutdownServer(DWORD reason); + // 显示服务状态通知 + void NotifyService(DWORD notification); + // 發起會話 + void StartSession(); + // 結束會話 + void EndSession(); + // 進入維護模式 + void StartMaintenance(); + // 退出維護模式 + void EndMaintenance(DWORD result = WEASEL_IPC_MAINTENANCE_RESULT_NONE); + // 测试连接 + bool Echo(); + // 请求服务处理按键消息 + bool ProcessKeyEvent(KeyEvent const& keyEvent); + bool ProcessKeyEvent(KeyEvent const& keyEvent, bool* eaten); + // 上屏正在編輯的文字 + bool CommitComposition(); + // 清除正在編輯的文字 + bool ClearComposition(); + // 选择当前页面编号为index的候选 + bool SelectCandidateOnCurrentPage(size_t index); + // 高亮当前页面编号为index的候选 + bool HighlightCandidateOnCurrentPage(size_t index); + // 翻页,backward = true 向前翻,false向后翻 + bool ChangePage(bool backward); + // 更新输入位置 + void UpdateInputPosition(RECT const& rc); + // 输入窗口获得焦点 + void FocusIn(); + // 输入窗口失去焦点 + void FocusOut(); + // 托盤菜單 + void TrayCommand(UINT menuId); + bool TrayCommandSync(UINT menuId); + // 读取server返回的数据 + bool GetResponseData(ResponseHandler handler); + + private: + ClientImpl* m_pImpl; +}; + +class Server { + public: + Server(); + virtual ~Server(); + + // 初始化服务 + HWND Start(); + // 结束服务 + int Stop(); + // 消息循环 + int Run(); + + void SetRequestHandler(RequestHandler* pHandler); + void AddMenuHandler(UINT uID, CommandHandler handler); + HWND GetHWnd(); + + private: + ServerImpl* m_pImpl; +}; + +inline std::wstring GetPipeName() { + std::wstring pipe_name; + pipe_name += L"\\\\.\\pipe\\"; + pipe_name += getUsername(); + pipe_name += L"\\"; + pipe_name += WEASEL_IPC_PIPE_NAME; + return pipe_name; +} +} // namespace weasel diff --git a/include/WeaselIPCData.h b/include/WeaselIPCData.h index 09f22f91c..c6a1072f3 100644 --- a/include/WeaselIPCData.h +++ b/include/WeaselIPCData.h @@ -1,536 +1,541 @@ -#pragma once - -#include -#include -#include -#include - -namespace weasel { - -enum TextAttributeType { NONE = 0, HIGHLIGHTED, LAST_TYPE }; - -struct TextRange { - TextRange() : start(0), end(0), cursor(-1) {} - TextRange(int _start, int _end, int _cursor) - : start(_start), end(_end), cursor(_cursor) {} - bool operator==(const TextRange& tr) { - return (start == tr.start && end == tr.end && cursor == tr.cursor); - } - bool operator!=(const TextRange& tr) { - return (start != tr.start || end != tr.end || cursor != tr.cursor); - } - int start; - int end; - int cursor; -}; - -struct TextAttribute { - TextAttribute() : type(NONE) {} - TextAttribute(int _start, int _end, TextAttributeType _type) - : range(_start, _end, -1), type(_type) {} - bool operator==(const TextAttribute& ta) { - return (range == ta.range && type == ta.type); - } - bool operator!=(const TextAttribute& ta) { - return (range != ta.range || type != ta.type); - } - TextRange range; - TextAttributeType type; -}; - -struct Text { - Text() : str(L"") {} - Text(std::wstring const& _str) : str(_str) {} - void clear() { - str.clear(); - attributes.clear(); - } - bool empty() const { return str.empty(); } - bool operator==(const Text& txt) { - if (str != txt.str || (attributes.size() != txt.attributes.size())) - return false; - for (size_t i = 0; i < attributes.size(); i++) { - if ((attributes[i] != txt.attributes[i])) - return false; - } - return true; - } - bool operator!=(const Text& txt) { - if (str != txt.str || (attributes.size() != txt.attributes.size())) - return true; - for (size_t i = 0; i < attributes.size(); i++) { - if ((attributes[i] != txt.attributes[i])) - return true; - } - return false; - } - std::wstring str; - std::vector attributes; -}; - -struct CandidateInfo { - CandidateInfo() { - currentPage = 0; - totalPages = 0; - highlighted = 0; - is_last_page = false; - } - void clear() { - currentPage = 0; - totalPages = 0; - highlighted = 0; - is_last_page = false; - candies.clear(); - labels.clear(); - } - bool empty() const { return candies.empty(); } - bool operator==(const CandidateInfo& ci) { - if (currentPage != ci.currentPage || totalPages != ci.totalPages || - highlighted != ci.highlighted || is_last_page != ci.is_last_page || - notequal(candies, ci.candies) || notequal(comments, ci.comments) || - notequal(labels, ci.labels)) - return false; - return true; - } - bool operator!=(const CandidateInfo& ci) { - if (currentPage != ci.currentPage || totalPages != ci.totalPages || - highlighted != ci.highlighted || is_last_page != ci.is_last_page || - notequal(candies, ci.candies) || notequal(comments, ci.comments) || - notequal(labels, ci.labels)) - return true; - return false; - } - bool notequal(std::vector txtSrc, std::vector txtDst) { - if (txtSrc.size() != txtDst.size()) - return true; - for (size_t i = 0; i < txtSrc.size(); i++) { - if (txtSrc[i] != txtDst[i]) - return true; - } - return false; - } - int currentPage; - bool is_last_page; - int totalPages; - int highlighted; - std::vector candies; - std::vector comments; - std::vector labels; -}; - -struct Context { - Context() {} - void clear() { - preedit.clear(); - aux.clear(); - cinfo.clear(); - } - bool empty() const { return preedit.empty() && aux.empty() && cinfo.empty(); } - bool operator==(const Context& ctx) { - if (preedit == ctx.preedit && aux == ctx.aux && cinfo == ctx.cinfo) - return true; - return false; - } - bool operator!=(const Context& ctx) { return !(operator==(ctx)); } - - bool operator!() { - if (preedit.str.empty() && aux.str.empty() && cinfo.candies.empty() && - cinfo.labels.empty() && cinfo.comments.empty()) - return true; - else - return false; - } - Text preedit; - Text aux; - CandidateInfo cinfo; -}; -// for icon type in tip -enum IconType { SCHEMA, FULL_SHAPE }; -// 由ime管理 -struct Status { - Status() - : type(SCHEMA), - ascii_mode(false), - composing(false), - disabled(false), - full_shape(false) {} - void reset() { - schema_name.clear(); - schema_id.clear(); - ascii_mode = false; - composing = false; - disabled = false; - full_shape = false; - type = SCHEMA; - } - bool operator==(const Status status) { - return (status.schema_name == schema_name && - status.schema_id == schema_id && status.ascii_mode == ascii_mode && - status.composing == composing && status.disabled == disabled && - status.full_shape == full_shape && status.type == type); - } - // 輸入方案 - std::wstring schema_name; - // 輸入方案 id - std::wstring schema_id; - // 轉換開關 - bool ascii_mode; - // 寫作狀態 - bool composing; - // 維護模式(暫停輸入功能) - bool disabled; - // 全角状态 - bool full_shape; - // 图标类型, schema/full_shape - IconType type; -}; - -// 用於向前端告知設置信息 -struct Config { - Config() : inline_preedit(false) {} - void reset() { inline_preedit = false; } - bool inline_preedit; -}; - -struct UIStyle { - enum AntiAliasMode { - DEFAULT = 0, - CLEARTYPE = 1, - GRAYSCALE = 2, - ALIASED = 3, - FORCE_DWORD = 0xffffffff - }; - - enum PreeditType { COMPOSITION, PREVIEW, PREVIEW_ALL }; - enum HoverType { NONE, SEMI_HILITE, HILITE }; - enum LayoutType { - LAYOUT_VERTICAL = 0, - LAYOUT_HORIZONTAL, - LAYOUT_VERTICAL_TEXT, - LAYOUT_VERTICAL_FULLSCREEN, - LAYOUT_HORIZONTAL_FULLSCREEN, - LAYOUT_TYPE_LAST - }; - - enum LayoutAlignType { ALIGN_BOTTOM = 0, ALIGN_CENTER, ALIGN_TOP }; - - // font face and font point settings - std::wstring font_face; - std::wstring label_font_face; - std::wstring comment_font_face; - int font_point; - int label_font_point; - int comment_font_point; - int candidate_abbreviate_length; - - bool inline_preedit; - bool display_tray_icon; - bool ascii_tip_follow_cursor; - bool paging_on_scroll; - bool enhanced_position; - bool click_to_capture; - HoverType hover_type; - AntiAliasMode antialias_mode; - PreeditType preedit_type; - // custom icon settings - std::wstring current_zhung_icon; - std::wstring current_ascii_icon; - std::wstring current_half_icon; - std::wstring current_full_icon; - // label format and mark_text - std::wstring label_text_format; - std::wstring mark_text; - // layout relative parameters - LayoutType layout_type; - LayoutAlignType align_type; - bool vertical_text_left_to_right; - bool vertical_text_with_wrap; - // layout, with key name like style/layout/... - int min_width; - int max_width; - int min_height; - int max_height; - int border; - int margin_x; - int margin_y; - int spacing; - int candidate_spacing; - int hilite_spacing; - int hilite_padding_x; - int hilite_padding_y; - int round_corner; - int round_corner_ex; - int shadow_radius; - int shadow_offset_x; - int shadow_offset_y; - bool vertical_auto_reverse; - // color scheme - int text_color; - int candidate_text_color; - int candidate_back_color; - int candidate_shadow_color; - int candidate_border_color; - int label_text_color; - int comment_text_color; - int back_color; - int shadow_color; - int border_color; - int hilited_text_color; - int hilited_back_color; - int hilited_shadow_color; - int hilited_candidate_text_color; - int hilited_candidate_back_color; - int hilited_candidate_shadow_color; - int hilited_candidate_border_color; - int hilited_label_text_color; - int hilited_comment_text_color; - int hilited_mark_color; - int prevpage_color; - int nextpage_color; - // per client - int client_caps; - int baseline; - int linespacing; - - UIStyle() - : font_face(), - label_font_face(), - comment_font_face(), - font_point(0), - label_font_point(0), - comment_font_point(0), - candidate_abbreviate_length(0), - inline_preedit(false), - display_tray_icon(false), - ascii_tip_follow_cursor(false), - paging_on_scroll(false), - enhanced_position(false), - click_to_capture(false), - hover_type(NONE), - antialias_mode(DEFAULT), - preedit_type(COMPOSITION), - current_zhung_icon(), - current_ascii_icon(), - current_half_icon(), - current_full_icon(), - label_text_format(L"%s."), - mark_text(), - layout_type(LAYOUT_VERTICAL), - align_type(ALIGN_BOTTOM), - vertical_text_left_to_right(false), - vertical_text_with_wrap(false), - min_width(0), - max_width(0), - min_height(0), - max_height(0), - border(0), - margin_x(0), - margin_y(0), - spacing(0), - candidate_spacing(0), - hilite_spacing(0), - hilite_padding_x(0), - hilite_padding_y(0), - round_corner(0), - round_corner_ex(0), - shadow_radius(0), - shadow_offset_x(0), - shadow_offset_y(0), - vertical_auto_reverse(false), - text_color(0), - candidate_text_color(0), - candidate_back_color(0), - candidate_shadow_color(0), - candidate_border_color(0), - label_text_color(0), - comment_text_color(0), - back_color(0), - shadow_color(0), - border_color(0), - hilited_text_color(0), - hilited_back_color(0), - hilited_shadow_color(0), - hilited_candidate_text_color(0), - hilited_candidate_back_color(0), - hilited_candidate_shadow_color(0), - hilited_candidate_border_color(0), - hilited_label_text_color(0), - hilited_comment_text_color(0), - hilited_mark_color(0), - prevpage_color(0), - nextpage_color(0), - baseline(0), - linespacing(0), - client_caps(0) {} - bool operator!=(const UIStyle& st) { - return ( - align_type != st.align_type || antialias_mode != st.antialias_mode || - preedit_type != st.preedit_type || layout_type != st.layout_type || - vertical_text_left_to_right != st.vertical_text_left_to_right || - vertical_text_with_wrap != st.vertical_text_with_wrap || - paging_on_scroll != st.paging_on_scroll || font_face != st.font_face || - label_font_face != st.label_font_face || - comment_font_face != st.comment_font_face || - hover_type != st.hover_type || font_point != st.font_point || - label_font_point != st.label_font_point || - comment_font_point != st.comment_font_point || - candidate_abbreviate_length != st.candidate_abbreviate_length || - inline_preedit != st.inline_preedit || mark_text != st.mark_text || - display_tray_icon != st.display_tray_icon || - ascii_tip_follow_cursor != st.ascii_tip_follow_cursor || - current_zhung_icon != st.current_zhung_icon || - current_ascii_icon != st.current_ascii_icon || - current_half_icon != st.current_half_icon || - current_full_icon != st.current_full_icon || - enhanced_position != st.enhanced_position || - click_to_capture != st.click_to_capture || - label_text_format != st.label_text_format || - min_width != st.min_width || max_width != st.max_width || - min_height != st.min_height || max_height != st.max_height || - border != st.border || margin_x != st.margin_x || - margin_y != st.margin_y || spacing != st.spacing || - candidate_spacing != st.candidate_spacing || - hilite_spacing != st.hilite_spacing || - hilite_padding_x != st.hilite_padding_x || - hilite_padding_y != st.hilite_padding_y || - round_corner != st.round_corner || - round_corner_ex != st.round_corner_ex || - shadow_radius != st.shadow_radius || - shadow_offset_x != st.shadow_offset_x || - shadow_offset_y != st.shadow_offset_y || - vertical_auto_reverse != st.vertical_auto_reverse || - baseline != st.baseline || linespacing != st.linespacing || - text_color != st.text_color || - candidate_text_color != st.candidate_text_color || - candidate_back_color != st.candidate_back_color || - candidate_shadow_color != st.candidate_shadow_color || - candidate_border_color != st.candidate_border_color || - hilited_candidate_border_color != st.hilited_candidate_border_color || - label_text_color != st.label_text_color || - comment_text_color != st.comment_text_color || - back_color != st.back_color || shadow_color != st.shadow_color || - border_color != st.border_color || - hilited_text_color != st.hilited_text_color || - hilited_back_color != st.hilited_back_color || - hilited_shadow_color != st.hilited_shadow_color || - hilited_candidate_text_color != st.hilited_candidate_text_color || - hilited_candidate_back_color != st.hilited_candidate_back_color || - hilited_candidate_shadow_color != st.hilited_candidate_shadow_color || - hilited_label_text_color != st.hilited_label_text_color || - hilited_comment_text_color != st.hilited_comment_text_color || - hilited_mark_color != st.hilited_mark_color || - prevpage_color != st.prevpage_color || - nextpage_color != st.nextpage_color); - } -}; -} // namespace weasel -namespace boost { -namespace serialization { -template -void serialize(Archive& ar, weasel::UIStyle& s, const unsigned int version) { - ar & s.font_face; - ar & s.label_font_face; - ar & s.comment_font_face; - ar & s.hover_type; - ar & s.font_point; - ar & s.label_font_point; - ar & s.comment_font_point; - ar & s.candidate_abbreviate_length; - ar & s.inline_preedit; - ar & s.align_type; - ar & s.antialias_mode; - ar & s.mark_text; - ar & s.preedit_type; - ar & s.display_tray_icon; - ar & s.ascii_tip_follow_cursor; - ar & s.current_zhung_icon; - ar & s.current_ascii_icon; - ar & s.current_half_icon; - ar & s.current_full_icon; - ar & s.enhanced_position; - ar & s.click_to_capture; - ar & s.label_text_format; - // layout - ar & s.layout_type; - ar & s.vertical_text_left_to_right; - ar & s.vertical_text_with_wrap; - ar & s.paging_on_scroll; - ar & s.min_width; - ar & s.max_width; - ar & s.min_height; - ar & s.max_height; - ar & s.border; - ar & s.margin_x; - ar & s.margin_y; - ar & s.spacing; - ar & s.candidate_spacing; - ar & s.hilite_spacing; - ar & s.hilite_padding_x; - ar & s.hilite_padding_y; - ar & s.round_corner; - ar & s.round_corner_ex; - ar & s.shadow_radius; - ar & s.shadow_offset_x; - ar & s.shadow_offset_y; - ar & s.vertical_auto_reverse; - // color scheme - ar & s.text_color; - ar & s.candidate_text_color; - ar & s.candidate_back_color; - ar & s.candidate_shadow_color; - ar & s.candidate_border_color; - ar & s.label_text_color; - ar & s.comment_text_color; - ar & s.back_color; - ar & s.shadow_color; - ar & s.border_color; - ar & s.hilited_text_color; - ar & s.hilited_back_color; - ar & s.hilited_shadow_color; - ar & s.hilited_candidate_text_color; - ar & s.hilited_candidate_back_color; - ar & s.hilited_candidate_shadow_color; - ar & s.hilited_candidate_border_color; - ar & s.hilited_label_text_color; - ar & s.hilited_comment_text_color; - ar & s.hilited_mark_color; - ar & s.prevpage_color; - ar & s.nextpage_color; - // per client - ar & s.client_caps; - ar & s.baseline; - ar & s.linespacing; -} - -template -void serialize(Archive& ar, - weasel::CandidateInfo& s, - const unsigned int version) { - ar & s.currentPage; - ar & s.totalPages; - ar & s.highlighted; - ar & s.is_last_page; - ar & s.candies; - ar & s.comments; - ar & s.labels; -} -template -void serialize(Archive& ar, weasel::Text& s, const unsigned int version) { - ar & s.str; - ar & s.attributes; -} -template -void serialize(Archive& ar, - weasel::TextAttribute& s, - const unsigned int version) { - ar & s.range; - ar & s.type; -} -template -void serialize(Archive& ar, weasel::TextRange& s, const unsigned int version) { - ar & s.start; - ar & s.end; - ar & s.cursor; -} -} // namespace serialization -} // namespace boost +#pragma once + +#include +#include +#include +#include + +namespace weasel { + +enum TextAttributeType { NONE = 0, HIGHLIGHTED, LAST_TYPE }; + +struct TextRange { + TextRange() : start(0), end(0), cursor(-1) {} + TextRange(int _start, int _end, int _cursor) + : start(_start), end(_end), cursor(_cursor) {} + bool operator==(const TextRange& tr) const { + return (start == tr.start && end == tr.end && cursor == tr.cursor); + } + bool operator!=(const TextRange& tr) const { + return (start != tr.start || end != tr.end || cursor != tr.cursor); + } + int start; + int end; + int cursor; +}; + +struct TextAttribute { + TextAttribute() : type(NONE) {} + TextAttribute(int _start, int _end, TextAttributeType _type) + : range(_start, _end, -1), type(_type) {} + bool operator==(const TextAttribute& ta) const { + return (range == ta.range && type == ta.type); + } + bool operator!=(const TextAttribute& ta) const { + return (range != ta.range || type != ta.type); + } + TextRange range; + TextAttributeType type; +}; + +struct Text { + Text() : str(L"") {} + Text(std::wstring const& _str) : str(_str) {} + void clear() { + str.clear(); + attributes.clear(); + } + bool empty() const { return str.empty(); } + bool operator==(const Text& txt) const { + if (str != txt.str || (attributes.size() != txt.attributes.size())) + return false; + for (size_t i = 0; i < attributes.size(); i++) { + if ((attributes[i] != txt.attributes[i])) + return false; + } + return true; + } + bool operator!=(const Text& txt) const { + if (str != txt.str || (attributes.size() != txt.attributes.size())) + return true; + for (size_t i = 0; i < attributes.size(); i++) { + if ((attributes[i] != txt.attributes[i])) + return true; + } + return false; + } + std::wstring str; + std::vector attributes; +}; + +struct CandidateInfo { + CandidateInfo() { + currentPage = 0; + totalPages = 0; + highlighted = 0; + is_last_page = false; + } + void clear() { + currentPage = 0; + totalPages = 0; + highlighted = 0; + is_last_page = false; + candies.clear(); + labels.clear(); + } + bool empty() const { return candies.empty(); } + bool operator==(const CandidateInfo& ci) const { + if (currentPage != ci.currentPage || totalPages != ci.totalPages || + highlighted != ci.highlighted || is_last_page != ci.is_last_page || + notequal(candies, ci.candies) || notequal(comments, ci.comments) || + notequal(labels, ci.labels)) + return false; + return true; + } + bool operator!=(const CandidateInfo& ci) const { + if (currentPage != ci.currentPage || totalPages != ci.totalPages || + highlighted != ci.highlighted || is_last_page != ci.is_last_page || + notequal(candies, ci.candies) || notequal(comments, ci.comments) || + notequal(labels, ci.labels)) + return true; + return false; + } + bool notequal(const std::vector& txtSrc, + const std::vector& txtDst) const { + if (txtSrc.size() != txtDst.size()) + return true; + for (size_t i = 0; i < txtSrc.size(); i++) { + if (txtSrc[i] != txtDst[i]) + return true; + } + return false; + } + int currentPage; + bool is_last_page; + int totalPages; + int highlighted; + std::vector candies; + std::vector comments; + std::vector labels; +}; + +struct Context { + Context() {} + void clear() { + preedit.clear(); + aux.clear(); + cinfo.clear(); + } + bool empty() const { return preedit.empty() && aux.empty() && cinfo.empty(); } + bool operator==(const Context& ctx) const { + if (preedit == ctx.preedit && aux == ctx.aux && cinfo == ctx.cinfo) + return true; + return false; + } + bool operator!=(const Context& ctx) const { return !(operator==(ctx)); } + + bool operator!() const { + if (preedit.str.empty() && aux.str.empty() && cinfo.candies.empty() && + cinfo.labels.empty() && cinfo.comments.empty()) + return true; + else + return false; + } + Text preedit; + Text aux; + CandidateInfo cinfo; +}; +// for icon type in tip +enum IconType { SCHEMA, FULL_SHAPE }; +// 由ime管理 +struct Status { + Status() + : type(SCHEMA), + ascii_mode(false), + composing(false), + disabled(false), + full_shape(false) {} + void reset() { + schema_name.clear(); + schema_id.clear(); + ascii_mode = false; + composing = false; + disabled = false; + full_shape = false; + type = SCHEMA; + } + bool operator==(const Status& status) const { + return (status.schema_name == schema_name && + status.schema_id == schema_id && status.ascii_mode == ascii_mode && + status.composing == composing && status.disabled == disabled && + status.full_shape == full_shape && status.type == type); + } + // 輸入方案 + std::wstring schema_name; + // 輸入方案 id + std::wstring schema_id; + // 轉換開關 + bool ascii_mode; + // 寫作狀態 + bool composing; + // 維護模式(暫停輸入功能) + bool disabled; + // 全角状态 + bool full_shape; + // 图标类型, schema/full_shape + IconType type; +}; + +inline void MarkLocalCompositionAborted(Status& status) { + status.composing = false; +} + +// 用於向前端告知設置信息 +struct Config { + Config() : inline_preedit(false) {} + void reset() { inline_preedit = false; } + bool inline_preedit; +}; + +struct UIStyle { + enum AntiAliasMode { + DEFAULT = 0, + CLEARTYPE = 1, + GRAYSCALE = 2, + ALIASED = 3, + FORCE_DWORD = 0xffffffff + }; + + enum PreeditType { COMPOSITION, PREVIEW, PREVIEW_ALL }; + enum HoverType { NONE, SEMI_HILITE, HILITE }; + enum LayoutType { + LAYOUT_VERTICAL = 0, + LAYOUT_HORIZONTAL, + LAYOUT_VERTICAL_TEXT, + LAYOUT_VERTICAL_FULLSCREEN, + LAYOUT_HORIZONTAL_FULLSCREEN, + LAYOUT_TYPE_LAST + }; + + enum LayoutAlignType { ALIGN_BOTTOM = 0, ALIGN_CENTER, ALIGN_TOP }; + + // font face and font point settings + std::wstring font_face; + std::wstring label_font_face; + std::wstring comment_font_face; + int font_point; + int label_font_point; + int comment_font_point; + int candidate_abbreviate_length; + + bool inline_preedit; + bool display_tray_icon; + bool ascii_tip_follow_cursor; + bool paging_on_scroll; + bool enhanced_position; + bool click_to_capture; + HoverType hover_type; + AntiAliasMode antialias_mode; + PreeditType preedit_type; + // custom icon settings + std::wstring current_zhung_icon; + std::wstring current_ascii_icon; + std::wstring current_half_icon; + std::wstring current_full_icon; + // label format and mark_text + std::wstring label_text_format; + std::wstring mark_text; + // layout relative parameters + LayoutType layout_type; + LayoutAlignType align_type; + bool vertical_text_left_to_right; + bool vertical_text_with_wrap; + // layout, with key name like style/layout/... + int min_width; + int max_width; + int min_height; + int max_height; + int border; + int margin_x; + int margin_y; + int spacing; + int candidate_spacing; + int hilite_spacing; + int hilite_padding_x; + int hilite_padding_y; + int round_corner; + int round_corner_ex; + int shadow_radius; + int shadow_offset_x; + int shadow_offset_y; + bool vertical_auto_reverse; + // color scheme + int text_color; + int candidate_text_color; + int candidate_back_color; + int candidate_shadow_color; + int candidate_border_color; + int label_text_color; + int comment_text_color; + int back_color; + int shadow_color; + int border_color; + int hilited_text_color; + int hilited_back_color; + int hilited_shadow_color; + int hilited_candidate_text_color; + int hilited_candidate_back_color; + int hilited_candidate_shadow_color; + int hilited_candidate_border_color; + int hilited_label_text_color; + int hilited_comment_text_color; + int hilited_mark_color; + int prevpage_color; + int nextpage_color; + // per client + int client_caps; + int baseline; + int linespacing; + + UIStyle() + : font_face(), + label_font_face(), + comment_font_face(), + font_point(0), + label_font_point(0), + comment_font_point(0), + candidate_abbreviate_length(0), + inline_preedit(false), + display_tray_icon(false), + ascii_tip_follow_cursor(false), + paging_on_scroll(false), + enhanced_position(false), + click_to_capture(false), + hover_type(NONE), + antialias_mode(DEFAULT), + preedit_type(COMPOSITION), + current_zhung_icon(), + current_ascii_icon(), + current_half_icon(), + current_full_icon(), + label_text_format(L"%s."), + mark_text(), + layout_type(LAYOUT_VERTICAL), + align_type(ALIGN_BOTTOM), + vertical_text_left_to_right(false), + vertical_text_with_wrap(false), + min_width(0), + max_width(0), + min_height(0), + max_height(0), + border(0), + margin_x(0), + margin_y(0), + spacing(0), + candidate_spacing(0), + hilite_spacing(0), + hilite_padding_x(0), + hilite_padding_y(0), + round_corner(0), + round_corner_ex(0), + shadow_radius(0), + shadow_offset_x(0), + shadow_offset_y(0), + vertical_auto_reverse(false), + text_color(0), + candidate_text_color(0), + candidate_back_color(0), + candidate_shadow_color(0), + candidate_border_color(0), + label_text_color(0), + comment_text_color(0), + back_color(0), + shadow_color(0), + border_color(0), + hilited_text_color(0), + hilited_back_color(0), + hilited_shadow_color(0), + hilited_candidate_text_color(0), + hilited_candidate_back_color(0), + hilited_candidate_shadow_color(0), + hilited_candidate_border_color(0), + hilited_label_text_color(0), + hilited_comment_text_color(0), + hilited_mark_color(0), + prevpage_color(0), + nextpage_color(0), + baseline(0), + linespacing(0), + client_caps(0) {} + bool operator!=(const UIStyle& st) { + return ( + align_type != st.align_type || antialias_mode != st.antialias_mode || + preedit_type != st.preedit_type || layout_type != st.layout_type || + vertical_text_left_to_right != st.vertical_text_left_to_right || + vertical_text_with_wrap != st.vertical_text_with_wrap || + paging_on_scroll != st.paging_on_scroll || font_face != st.font_face || + label_font_face != st.label_font_face || + comment_font_face != st.comment_font_face || + hover_type != st.hover_type || font_point != st.font_point || + label_font_point != st.label_font_point || + comment_font_point != st.comment_font_point || + candidate_abbreviate_length != st.candidate_abbreviate_length || + inline_preedit != st.inline_preedit || mark_text != st.mark_text || + display_tray_icon != st.display_tray_icon || + ascii_tip_follow_cursor != st.ascii_tip_follow_cursor || + current_zhung_icon != st.current_zhung_icon || + current_ascii_icon != st.current_ascii_icon || + current_half_icon != st.current_half_icon || + current_full_icon != st.current_full_icon || + enhanced_position != st.enhanced_position || + click_to_capture != st.click_to_capture || + label_text_format != st.label_text_format || + min_width != st.min_width || max_width != st.max_width || + min_height != st.min_height || max_height != st.max_height || + border != st.border || margin_x != st.margin_x || + margin_y != st.margin_y || spacing != st.spacing || + candidate_spacing != st.candidate_spacing || + hilite_spacing != st.hilite_spacing || + hilite_padding_x != st.hilite_padding_x || + hilite_padding_y != st.hilite_padding_y || + round_corner != st.round_corner || + round_corner_ex != st.round_corner_ex || + shadow_radius != st.shadow_radius || + shadow_offset_x != st.shadow_offset_x || + shadow_offset_y != st.shadow_offset_y || + vertical_auto_reverse != st.vertical_auto_reverse || + baseline != st.baseline || linespacing != st.linespacing || + text_color != st.text_color || + candidate_text_color != st.candidate_text_color || + candidate_back_color != st.candidate_back_color || + candidate_shadow_color != st.candidate_shadow_color || + candidate_border_color != st.candidate_border_color || + hilited_candidate_border_color != st.hilited_candidate_border_color || + label_text_color != st.label_text_color || + comment_text_color != st.comment_text_color || + back_color != st.back_color || shadow_color != st.shadow_color || + border_color != st.border_color || + hilited_text_color != st.hilited_text_color || + hilited_back_color != st.hilited_back_color || + hilited_shadow_color != st.hilited_shadow_color || + hilited_candidate_text_color != st.hilited_candidate_text_color || + hilited_candidate_back_color != st.hilited_candidate_back_color || + hilited_candidate_shadow_color != st.hilited_candidate_shadow_color || + hilited_label_text_color != st.hilited_label_text_color || + hilited_comment_text_color != st.hilited_comment_text_color || + hilited_mark_color != st.hilited_mark_color || + prevpage_color != st.prevpage_color || + nextpage_color != st.nextpage_color); + } +}; +} // namespace weasel +namespace boost { +namespace serialization { +template +void serialize(Archive& ar, weasel::UIStyle& s, const unsigned int version) { + ar & s.font_face; + ar & s.label_font_face; + ar & s.comment_font_face; + ar & s.hover_type; + ar & s.font_point; + ar & s.label_font_point; + ar & s.comment_font_point; + ar & s.candidate_abbreviate_length; + ar & s.inline_preedit; + ar & s.align_type; + ar & s.antialias_mode; + ar & s.mark_text; + ar & s.preedit_type; + ar & s.display_tray_icon; + ar & s.ascii_tip_follow_cursor; + ar & s.current_zhung_icon; + ar & s.current_ascii_icon; + ar & s.current_half_icon; + ar & s.current_full_icon; + ar & s.enhanced_position; + ar & s.click_to_capture; + ar & s.label_text_format; + // layout + ar & s.layout_type; + ar & s.vertical_text_left_to_right; + ar & s.vertical_text_with_wrap; + ar & s.paging_on_scroll; + ar & s.min_width; + ar & s.max_width; + ar & s.min_height; + ar & s.max_height; + ar & s.border; + ar & s.margin_x; + ar & s.margin_y; + ar & s.spacing; + ar & s.candidate_spacing; + ar & s.hilite_spacing; + ar & s.hilite_padding_x; + ar & s.hilite_padding_y; + ar & s.round_corner; + ar & s.round_corner_ex; + ar & s.shadow_radius; + ar & s.shadow_offset_x; + ar & s.shadow_offset_y; + ar & s.vertical_auto_reverse; + // color scheme + ar & s.text_color; + ar & s.candidate_text_color; + ar & s.candidate_back_color; + ar & s.candidate_shadow_color; + ar & s.candidate_border_color; + ar & s.label_text_color; + ar & s.comment_text_color; + ar & s.back_color; + ar & s.shadow_color; + ar & s.border_color; + ar & s.hilited_text_color; + ar & s.hilited_back_color; + ar & s.hilited_shadow_color; + ar & s.hilited_candidate_text_color; + ar & s.hilited_candidate_back_color; + ar & s.hilited_candidate_shadow_color; + ar & s.hilited_candidate_border_color; + ar & s.hilited_label_text_color; + ar & s.hilited_comment_text_color; + ar & s.hilited_mark_color; + ar & s.prevpage_color; + ar & s.nextpage_color; + // per client + ar & s.client_caps; + ar & s.baseline; + ar & s.linespacing; +} + +template +void serialize(Archive& ar, + weasel::CandidateInfo& s, + const unsigned int version) { + ar & s.currentPage; + ar & s.totalPages; + ar & s.highlighted; + ar & s.is_last_page; + ar & s.candies; + ar & s.comments; + ar & s.labels; +} +template +void serialize(Archive& ar, weasel::Text& s, const unsigned int version) { + ar & s.str; + ar & s.attributes; +} +template +void serialize(Archive& ar, + weasel::TextAttribute& s, + const unsigned int version) { + ar & s.range; + ar & s.type; +} +template +void serialize(Archive& ar, weasel::TextRange& s, const unsigned int version) { + ar & s.start; + ar & s.end; + ar & s.cursor; +} +} // namespace serialization +} // namespace boost diff --git a/include/WeaselUtility.h b/include/WeaselUtility.h index 742f15744..ee122d00a 100755 --- a/include/WeaselUtility.h +++ b/include/WeaselUtility.h @@ -1,321 +1,367 @@ -#pragma once -#include -#include -#include -#include -using namespace Microsoft::WRL; - -namespace fs = std::filesystem; - -inline int utf8towcslen(const char* utf8_str, int utf8_len) { - return MultiByteToWideChar(CP_UTF8, 0, utf8_str, utf8_len, NULL, 0); -} - -inline std::wstring getUsername() { - DWORD len = 0; - GetUserName(NULL, &len); - - if (len <= 0) { - return L""; - } - - wchar_t* username = new wchar_t[len + 1]; - - GetUserName(username, &len); - if (len <= 0) { - delete[] username; - return L""; - } - auto res = std::wstring(username); - delete[] username; - return res; -} - -// data directories -std::filesystem::path WeaselSharedDataPath(); -std::filesystem::path WeaselUserDataPath(); -inline fs::path WeaselLogPath() { - WCHAR _path[MAX_PATH] = {0}; - // default location - ExpandEnvironmentStringsW(L"%TEMP%\\rime.weasel", _path, _countof(_path)); - fs::path path = fs::path(_path); - if (!fs::exists(path)) { - fs::create_directories(path); - } - return path; -} - -inline BOOL IsUserDarkMode() { - constexpr const LPCWSTR key = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; - constexpr const LPCWSTR value = L"AppsUseLightTheme"; - - DWORD type; - DWORD data; - DWORD size = sizeof(DWORD); - LSTATUS st = RegGetValue(HKEY_CURRENT_USER, key, value, RRF_RT_REG_DWORD, - &type, &data, &size); - - if (st == ERROR_SUCCESS && type == REG_DWORD) - return data == 0; - return false; -} - -inline std::wstring string_to_wstring(const std::string& str, - int code_page = CP_ACP) { - // support CP_ACP and CP_UTF8 only - if (code_page != 0 && code_page != CP_UTF8) - return L""; - // calc len - int len = - MultiByteToWideChar(code_page, 0, str.c_str(), (int)str.size(), NULL, 0); - if (len <= 0) - return L""; - std::wstring res; - TCHAR* buffer = new TCHAR[len + 1]; - MultiByteToWideChar(code_page, 0, str.c_str(), (int)str.size(), buffer, len); - buffer[len] = '\0'; - res.append(buffer); - delete[] buffer; - return res; -} - -inline std::string wstring_to_string(const std::wstring& wstr, - int code_page = CP_ACP) { - // support CP_ACP and CP_UTF8 only - if (code_page != 0 && code_page != CP_UTF8) - return ""; - int len = WideCharToMultiByte(code_page, 0, wstr.c_str(), (int)wstr.size(), - NULL, 0, NULL, NULL); - if (len <= 0) - return ""; - std::string res; - char* buffer = new char[len + 1]; - WideCharToMultiByte(code_page, 0, wstr.c_str(), (int)wstr.size(), buffer, len, - NULL, NULL); - buffer[len] = '\0'; - res.append(buffer); - delete[] buffer; - return res; -} - -inline BOOL is_wow64() { - DWORD errorCode; - if (GetSystemWow64DirectoryW(NULL, 0) == 0) - if ((errorCode = GetLastError()) == ERROR_CALL_NOT_IMPLEMENTED) - return FALSE; - else - ExitProcess((UINT)errorCode); - else - return TRUE; -} - -template -struct EscapeChar { - static const CharT escape; - static const CharT linefeed; - static const CharT tab; - static const CharT linefeed_escape; - static const CharT tab_escape; -}; - -template <> -const char EscapeChar::escape = '\\'; -template <> -const char EscapeChar::linefeed = '\n'; -template <> -const char EscapeChar::tab = '\t'; -template <> -const char EscapeChar::linefeed_escape = 'n'; -template <> -const char EscapeChar::tab_escape = 't'; - -template <> -const wchar_t EscapeChar::escape = L'\\'; -template <> -const wchar_t EscapeChar::linefeed = L'\n'; -template <> -const wchar_t EscapeChar::tab = L'\t'; -template <> -const wchar_t EscapeChar::linefeed_escape = L'n'; -template <> -const wchar_t EscapeChar::tab_escape = L't'; - -template -inline std::basic_string escape_string( - const std::basic_string& input) { - using Esc = EscapeChar; - std::basic_stringstream res; - for (auto p = input.begin(); p != input.end(); ++p) { - if (*p == Esc::escape) { - res << Esc::escape << Esc::escape; - } else if (*p == Esc::linefeed) { - res << Esc::escape << Esc::linefeed_escape; - } else if (*p == Esc::tab) { - res << Esc::escape << Esc::tab_escape; - } else { - res << *p; - } - } - return res.str(); -} - -template -inline std::basic_string unescape_string( - const std::basic_string& input) { - using Esc = EscapeChar; - std::basic_stringstream res; - for (auto p = input.begin(); p != input.end(); ++p) { - if (*p == Esc::escape) { - if (++p == input.end()) { - break; - } else if (*p == Esc::linefeed_escape) { - res << Esc::linefeed; - } else if (*p == Esc::tab_escape) { - res << Esc::tab; - } else { // \a => a - res << *p; - } - } else { - res << *p; - } - } - return res.str(); -} - -// resource -std::string GetCustomResource(const char* name, const char* type); - -inline std::wstring get_weasel_ime_name() { - LANGID langId = GetUserDefaultUILanguage(); - - if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_HONGKONG) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_MACAU)) { - return L"小狼毫"; - } else { - return L"Weasel"; - } -} - -inline LONG RegGetStringValue(HKEY key, - LPCWSTR lpSubKey, - LPCWSTR lpValue, - std::wstring& value) { - TCHAR szValue[MAX_PATH]; - DWORD dwBufLen = MAX_PATH; - - LONG lRes = RegGetValue(key, lpSubKey, lpValue, RRF_RT_REG_SZ, NULL, szValue, - &dwBufLen); - if (lRes == ERROR_SUCCESS) { - value = std::wstring(szValue); - } - return lRes; -} - -inline LANGID get_language_id() { - std::wstring lang{}; - if (RegGetStringValue(HKEY_CURRENT_USER, L"Software\\Rime\\Weasel", - L"Language", lang) == ERROR_SUCCESS) { - if (lang == L"chs") - return MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); - else if (lang == L"cht") - return MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); - else if (lang == L"eng") - return MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); - } - LANGID langId = GetUserDefaultUILanguage(); - if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE)) { - langId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); - } else if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_HONGKONG) || - langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_MACAU)) { - langId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); - } else { - langId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); - } - return langId; -} - -#define wtou8(x) wstring_to_string(x, CP_UTF8) -#define wtoacp(x) wstring_to_string(x, CP_ACP) -#define u8tow(x) string_to_wstring(x, CP_UTF8) -#define acptow(x) string_to_wstring(x, CP_ACP) -#define u8toacp(x) wtoacp(u8tow(x)) - -class DebugStream { - public: - DebugStream() = default; - ~DebugStream() { OutputDebugString(ss.str().c_str()); } - template - DebugStream& operator<<(const T& value) { - ss << value; - return *this; - } - DebugStream& operator<<(const char* value) { - if (value) { - std::wstring wvalue(u8tow(value)); // utf-8 - ss << wvalue; - } - return *this; - } - DebugStream& operator<<(const std::string value) { - std::wstring wvalue(acptow(value)); // utf-8 - ss << wvalue; - return *this; - } - - private: - std::wstringstream ss; -}; -inline std::string current_time() { - using namespace std::chrono; - auto now = system_clock::now(); - auto time_point = system_clock::to_time_t(now); - auto ns = duration_cast(now.time_since_epoch()); - std::tm tm; - localtime_s(&tm, &time_point); - std::ostringstream oss; - oss << std::put_time(&tm, "%Y%m%d %H:%M:%S"); - oss << "." << std::setw(6) << std::setfill('0') << ns.count() % 1000000; - return oss.str(); -} - -#define DEBUG \ - (DebugStream() << "[" << current_time() << " " << __FILE__ << ":" \ - << __LINE__ << "] ") - -using wstring = std::wstring; -using string = std::string; -template -using vector = std::vector; - -inline string HRESULTToString(HRESULT hr) { - if (SUCCEEDED(hr)) - return "Success"; - char buffer[512]; - DWORD dwFlags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; - DWORD dwSize = - FormatMessageA(dwFlags, nullptr, hr, 0, buffer, sizeof(buffer), nullptr); - if (dwSize == 0) - return "Unknown HRESULT error"; - return string(buffer); -} - -struct ComException { - HRESULT result; - ComException(HRESULT const value) : result(value) {} -}; - -#define HR(result) HR_Impl(result, __FILE__, __LINE__) - -inline void HR_Impl(HRESULT const result, const char* file, int line) { - if (S_OK != result) { - DebugStream() << "[" << current_time() << " " << file << ":" << line << "] " - << HRESULTToString(result); - throw ComException(result); - } -} +#pragma once +#include +#include +#include +#include +using namespace Microsoft::WRL; + +namespace fs = std::filesystem; + +inline int utf8towcslen(const char* utf8_str, int utf8_len) { + return MultiByteToWideChar(CP_UTF8, 0, utf8_str, utf8_len, NULL, 0); +} + +inline std::wstring getUsername() { + DWORD len = 0; + GetUserName(NULL, &len); + + if (len <= 0) { + return L""; + } + + wchar_t* username = new wchar_t[len + 1]; + + GetUserName(username, &len); + if (len <= 0) { + delete[] username; + return L""; + } + auto res = std::wstring(username); + delete[] username; + return res; +} + +// data directories +std::filesystem::path WeaselSharedDataPath(); +std::filesystem::path WeaselUserDataPath(); +inline fs::path WeaselLogPath() { + WCHAR _path[MAX_PATH] = {0}; + // default location + ExpandEnvironmentStringsW(L"%TEMP%\\rime.weasel", _path, _countof(_path)); + fs::path path = fs::path(_path); + if (!fs::exists(path)) { + fs::create_directories(path); + } + return path; +} + +inline bool ShouldTraceKeyEvents() { + static bool enabled = []() { + WCHAR value[8] = {0}; + DWORD length = GetEnvironmentVariableW(L"WEASEL_TRACE_KEY_EVENTS", value, + static_cast(_countof(value))); + return length > 0 && value[0] != L'0'; + }(); + return enabled; +} + +inline void WeaselDebugLog(const std::wstring& component, + const std::wstring& message) { + try { + SYSTEMTIME st; + GetLocalTime(&st); + WCHAR prefix[256] = {0}; + swprintf_s(prefix, _countof(prefix), + L"%04d-%02d-%02d %02d:%02d:%02d.%03d pid=%lu tid=%lu [%s] ", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, + st.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId(), + component.c_str()); + std::wstring line = std::wstring(prefix) + message + L"\r\n"; + int bytes = + WideCharToMultiByte(CP_UTF8, 0, line.c_str(), + static_cast(line.size()), NULL, 0, NULL, NULL); + if (bytes <= 0) + return; + std::string utf8(bytes, '\0'); + WideCharToMultiByte(CP_UTF8, 0, line.c_str(), static_cast(line.size()), + utf8.data(), bytes, NULL, NULL); + + fs::path log_file = WeaselLogPath() / L"weasel-debug.log"; + HANDLE file = + CreateFileW(log_file.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) + return; + DWORD written = 0; + WriteFile(file, utf8.data(), static_cast(utf8.size()), &written, + NULL); + CloseHandle(file); + } catch (...) { + } +} + +inline BOOL IsUserDarkMode() { + constexpr const LPCWSTR key = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + constexpr const LPCWSTR value = L"AppsUseLightTheme"; + + DWORD type; + DWORD data; + DWORD size = sizeof(DWORD); + LSTATUS st = RegGetValue(HKEY_CURRENT_USER, key, value, RRF_RT_REG_DWORD, + &type, &data, &size); + + if (st == ERROR_SUCCESS && type == REG_DWORD) + return data == 0; + return false; +} + +inline std::wstring string_to_wstring(const std::string& str, + int code_page = CP_ACP) { + // support CP_ACP and CP_UTF8 only + if (code_page != 0 && code_page != CP_UTF8) + return L""; + // calc len + int len = + MultiByteToWideChar(code_page, 0, str.c_str(), (int)str.size(), NULL, 0); + if (len <= 0) + return L""; + std::wstring res; + TCHAR* buffer = new TCHAR[len + 1]; + MultiByteToWideChar(code_page, 0, str.c_str(), (int)str.size(), buffer, len); + buffer[len] = '\0'; + res.append(buffer); + delete[] buffer; + return res; +} + +inline std::string wstring_to_string(const std::wstring& wstr, + int code_page = CP_ACP) { + // support CP_ACP and CP_UTF8 only + if (code_page != 0 && code_page != CP_UTF8) + return ""; + int len = WideCharToMultiByte(code_page, 0, wstr.c_str(), (int)wstr.size(), + NULL, 0, NULL, NULL); + if (len <= 0) + return ""; + std::string res; + char* buffer = new char[len + 1]; + WideCharToMultiByte(code_page, 0, wstr.c_str(), (int)wstr.size(), buffer, len, + NULL, NULL); + buffer[len] = '\0'; + res.append(buffer); + delete[] buffer; + return res; +} + +inline BOOL is_wow64() { + DWORD errorCode; + if (GetSystemWow64DirectoryW(NULL, 0) == 0) + if ((errorCode = GetLastError()) == ERROR_CALL_NOT_IMPLEMENTED) + return FALSE; + else + ExitProcess((UINT)errorCode); + else + return TRUE; +} + +template +struct EscapeChar { + static const CharT escape; + static const CharT linefeed; + static const CharT tab; + static const CharT linefeed_escape; + static const CharT tab_escape; +}; + +template <> +const char EscapeChar::escape = '\\'; +template <> +const char EscapeChar::linefeed = '\n'; +template <> +const char EscapeChar::tab = '\t'; +template <> +const char EscapeChar::linefeed_escape = 'n'; +template <> +const char EscapeChar::tab_escape = 't'; + +template <> +const wchar_t EscapeChar::escape = L'\\'; +template <> +const wchar_t EscapeChar::linefeed = L'\n'; +template <> +const wchar_t EscapeChar::tab = L'\t'; +template <> +const wchar_t EscapeChar::linefeed_escape = L'n'; +template <> +const wchar_t EscapeChar::tab_escape = L't'; + +template +inline std::basic_string escape_string( + const std::basic_string& input) { + using Esc = EscapeChar; + std::basic_stringstream res; + for (auto p = input.begin(); p != input.end(); ++p) { + if (*p == Esc::escape) { + res << Esc::escape << Esc::escape; + } else if (*p == Esc::linefeed) { + res << Esc::escape << Esc::linefeed_escape; + } else if (*p == Esc::tab) { + res << Esc::escape << Esc::tab_escape; + } else { + res << *p; + } + } + return res.str(); +} + +template +inline std::basic_string unescape_string( + const std::basic_string& input) { + using Esc = EscapeChar; + std::basic_stringstream res; + for (auto p = input.begin(); p != input.end(); ++p) { + if (*p == Esc::escape) { + if (++p == input.end()) { + break; + } else if (*p == Esc::linefeed_escape) { + res << Esc::linefeed; + } else if (*p == Esc::tab_escape) { + res << Esc::tab; + } else { // \a => a + res << *p; + } + } else { + res << *p; + } + } + return res.str(); +} + +// resource +std::string GetCustomResource(const char* name, const char* type); + +inline std::wstring get_weasel_ime_name() { + LANGID langId = GetUserDefaultUILanguage(); + + if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_HONGKONG) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_MACAU)) { + return L"小狼毫"; + } else { + return L"Weasel"; + } +} + +inline LONG RegGetStringValue(HKEY key, + LPCWSTR lpSubKey, + LPCWSTR lpValue, + std::wstring& value) { + TCHAR szValue[MAX_PATH]; + DWORD dwBufLen = MAX_PATH; + + LONG lRes = RegGetValue(key, lpSubKey, lpValue, RRF_RT_REG_SZ, NULL, szValue, + &dwBufLen); + if (lRes == ERROR_SUCCESS) { + value = std::wstring(szValue); + } + return lRes; +} + +inline LANGID get_language_id() { + std::wstring lang{}; + if (RegGetStringValue(HKEY_CURRENT_USER, L"Software\\Rime\\Weasel", + L"Language", lang) == ERROR_SUCCESS) { + if (lang == L"chs") + return MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); + else if (lang == L"cht") + return MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); + else if (lang == L"eng") + return MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + } + LANGID langId = GetUserDefaultUILanguage(); + if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE)) { + langId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); + } else if (langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_HONGKONG) || + langId == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_MACAU)) { + langId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); + } else { + langId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + } + return langId; +} + +#define wtou8(x) wstring_to_string(x, CP_UTF8) +#define wtoacp(x) wstring_to_string(x, CP_ACP) +#define u8tow(x) string_to_wstring(x, CP_UTF8) +#define acptow(x) string_to_wstring(x, CP_ACP) +#define u8toacp(x) wtoacp(u8tow(x)) + +class DebugStream { + public: + DebugStream() = default; + ~DebugStream() { OutputDebugString(ss.str().c_str()); } + template + DebugStream& operator<<(const T& value) { + ss << value; + return *this; + } + DebugStream& operator<<(const char* value) { + if (value) { + std::wstring wvalue(u8tow(value)); // utf-8 + ss << wvalue; + } + return *this; + } + DebugStream& operator<<(const std::string value) { + std::wstring wvalue(acptow(value)); // utf-8 + ss << wvalue; + return *this; + } + + private: + std::wstringstream ss; +}; +inline std::string current_time() { + using namespace std::chrono; + auto now = system_clock::now(); + auto time_point = system_clock::to_time_t(now); + auto ns = duration_cast(now.time_since_epoch()); + std::tm tm; + localtime_s(&tm, &time_point); + std::ostringstream oss; + oss << std::put_time(&tm, "%Y%m%d %H:%M:%S"); + oss << "." << std::setw(6) << std::setfill('0') << ns.count() % 1000000; + return oss.str(); +} + +#define DEBUG \ + (DebugStream() << "[" << current_time() << " " << __FILE__ << ":" \ + << __LINE__ << "] ") + +using wstring = std::wstring; +using string = std::string; +template +using vector = std::vector; + +inline string HRESULTToString(HRESULT hr) { + if (SUCCEEDED(hr)) + return "Success"; + char buffer[512]; + DWORD dwFlags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + DWORD dwSize = + FormatMessageA(dwFlags, nullptr, hr, 0, buffer, sizeof(buffer), nullptr); + if (dwSize == 0) + return "Unknown HRESULT error"; + return string(buffer); +} + +struct ComException { + HRESULT result; + ComException(HRESULT const value) : result(value) {} +}; + +#define HR(result) HR_Impl(result, __FILE__, __LINE__) + +inline void HR_Impl(HRESULT const result, const char* file, int line) { + if (S_OK != result) { + DebugStream() << "[" << current_time() << " " << file << ":" << line << "] " + << HRESULTToString(result); + throw ComException(result); + } +} diff --git a/include/resource.h b/include/resource.h index 3a40f51c7..d4ef50443 100644 --- a/include/resource.h +++ b/include/resource.h @@ -9,10 +9,16 @@ #define IDR_MENU_POPUP 105 #define IDI_FULL_SHAPE 106 #define IDI_HALF_SHAPE 107 -#define IDR_MENU_POPUP_HANT 108 -#define IDR_MENU_POPUP_HANS 109 -#define IDS_STR_UNDER_MAINTENANCE 302 -#define ID_WEASELTRAY_QUIT 40001 +#define IDR_MENU_POPUP_HANT 108 +#define IDR_MENU_POPUP_HANS 109 +#define IDS_STR_UNDER_MAINTENANCE 302 +#define IDS_STR_DEPLOY_SUCCESS 303 +#define IDS_STR_DEPLOY_FAILURE 304 +#define IDS_STR_SERVICE_EXITING 305 +#define IDS_STR_SERVICE_RESTARTING 306 +#define IDS_STR_SERVICE_RESTART_SUCCESS 307 +#define IDS_STR_SERVICE_RESTART_FAILURE 308 +#define ID_WEASELTRAY_QUIT 40001 #define ID_WEASELTRAY_DEPLOY 40002 #define ID_WEASELTRAY_CHECKUPDATE 40003 #define ID_WEASELTRAY_FORUM 40004 @@ -33,7 +39,7 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 104 -#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_COMMAND_VALUE 40003 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif diff --git a/output/install.bat b/output/install.bat index 0c50c52b4..1e37864a2 100644 --- a/output/install.bat +++ b/output/install.bat @@ -31,26 +31,23 @@ cscript check_windows_version.js if errorlevel 2 goto win7_x64_install if errorlevel 1 goto xp_install -:win7_install -WeaselSetup.exe %install_option% -rem regsvr32.exe /s "%CD%\weasel.dll" -goto next - -:win7_x64_install -WeaselSetupx64.exe %install_option% -rem regsvr32.exe /s "%CD%\weasel.dll" -rem regsvr32.exe /s "%CD%\weaselx64.dll" -goto next +:win7_install +WeaselSetup.exe %install_option% +goto next + +:win7_x64_install +WeaselSetupx64.exe %install_option% +goto next :xp_install WeaselSetup.exe %install_option% goto next :next -reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run" /v WeaselServer /t REG_SZ /d "%CD%\WeaselServer.exe" /f - -:done -start WeaselServer.exe +reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run" /v WeaselServer /t REG_SZ /d "\"%CD%\WeaselServer.exe\" /startup" /f + +:done +start "" WeaselServer.exe /startup if /i "%2" == "/register" pause echo installed. diff --git a/output/install.nsi b/output/install.nsi index 45e32cbf6..a20eebcb1 100644 --- a/output/install.nsi +++ b/output/install.nsi @@ -161,7 +161,7 @@ uninst: CopyFiles $R1\data\*.* $TEMP\weasel-backup call_uninstaller: - ExecWait '"$R1\WeaselServer.exe" /quit' + ExecWait '"$R1\WeaselServer.exe" /stop' ExecWait '"$R1\WeaselSetup.exe" /u' ; Remove registry keys DeleteRegKey HKLM SOFTWARE\Rime @@ -212,7 +212,7 @@ Section "Weasel" StrCpy $INSTDIR "${WEASEL_ROOT}" IfFileExists "$INSTDIR\WeaselServer.exe" 0 +2 - ExecWait '"$INSTDIR\WeaselServer.exe" /quit' + ExecWait '"$INSTDIR\WeaselServer.exe" /stop' SetOverwrite try ; Set output path to the installation directory. @@ -305,7 +305,10 @@ program_files: IfErrors +2 0 StrCpy $R2 "/t" - ExecWait '"$INSTDIR\WeaselSetup.exe" $R2' + ExecWait '"$INSTDIR\WeaselSetup.exe" $R2' $R3 + ${If} $R3 != 0 + Abort + ${Endif} ; Write the uninstall keys for Windows WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "$(DISPLAYNAME)" @@ -321,11 +324,11 @@ program_files: ; run as user... IfSilent deploy_silently - ExecWait "$INSTDIR\WeaselDeployer.exe /install" + ExecWait '"$INSTDIR\WeaselDeployer.exe" /install' GoTo deploy_done deploy_silently: - ExecWait "$INSTDIR\WeaselDeployer.exe /deploy" + ExecWait '"$INSTDIR\WeaselDeployer.exe" /deploy' deploy_done: ; don't redirect on 64 bit system for auto run setting @@ -335,9 +338,9 @@ program_files: SetRegView 64 ${Endif} ; Write autorun key - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Run" "WeaselServer" "$INSTDIR\WeaselServer.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Run" "WeaselServer" '"$INSTDIR\WeaselServer.exe" /startup' ; Start WeaselServer - Exec "$INSTDIR\WeaselServer.exe" + Exec '"$INSTDIR\WeaselServer.exe" /startup' ; option CheckForUpdates IfSilent DisableAutoCheckUpdate @@ -364,7 +367,7 @@ Section "Start Menu Shortcuts" CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORDICT).lnk" "$INSTDIR\WeaselDeployer.exe" "/dict" "$SYSDIR\shell32.dll" 6 CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORSYNC).lnk" "$INSTDIR\WeaselDeployer.exe" "/sync" "$SYSDIR\shell32.dll" 26 CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORDEPLOY).lnk" "$INSTDIR\WeaselDeployer.exe" "/deploy" "$SYSDIR\shell32.dll" 144 - CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORSERVER).lnk" "$INSTDIR\WeaselServer.exe" "" "$INSTDIR\WeaselServer.exe" 0 + CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORSERVER).lnk" "$INSTDIR\WeaselServer.exe" "/startup" "$INSTDIR\WeaselServer.exe" 0 CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORUSERFOLDER).lnk" "$INSTDIR\WeaselServer.exe" "/userdir" "$SYSDIR\shell32.dll" 126 CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORAPPFOLDER).lnk" "$INSTDIR\WeaselServer.exe" "/weaseldir" "$SYSDIR\shell32.dll" 19 CreateShortCut "$SMPROGRAMS\$(DISPLAYNAME)\$(LNKFORUPDATER).lnk" "$INSTDIR\WeaselServer.exe" "/update" "$SYSDIR\shell32.dll" 13 @@ -379,7 +382,7 @@ SectionEnd Section "Uninstall" - ExecWait '"$INSTDIR\WeaselServer.exe" /quit' + ExecWait '"$INSTDIR\WeaselServer.exe" /stop' ExecWait '"$INSTDIR\WeaselSetup.exe" /u' diff --git a/output/start_service.bat b/output/start_service.bat index 1ae47b02f..bfae2a930 100644 --- a/output/start_service.bat +++ b/output/start_service.bat @@ -1,2 +1,2 @@ cd "%~dp0" -start "" WeaselServer.exe \ No newline at end of file +start "" WeaselServer.exe /restart-manual diff --git a/output/stop_service.bat b/output/stop_service.bat index 243ad0d3a..c267c59ce 100644 --- a/output/stop_service.bat +++ b/output/stop_service.bat @@ -1,2 +1,2 @@ cd "%~dp0" -weaselserver.exe /q +weaselserver.exe /stop diff --git a/test/TestWeaselIPC/TestWeaselIPC.cpp b/test/TestWeaselIPC/TestWeaselIPC.cpp index efa98c7e6..51c44713c 100644 --- a/test/TestWeaselIPC/TestWeaselIPC.cpp +++ b/test/TestWeaselIPC/TestWeaselIPC.cpp @@ -1,182 +1,566 @@ -// TestWeaselIPC.cpp : Defines the entry point for the console application. -// - -#include "stdafx.h" -#include -#include - -#include -using namespace boost::interprocess; - -#include -#include - -CAppModule _Module; - -int console_main(); -int client_main(); -int server_main(); - -// usage: TestWeaselIPC.exe [/start | /stop | /console] - -int _tmain(int argc, _TCHAR* argv[]) { - if (argc == 1) // no args - { - return client_main(); - } else if (argc > 1 && !wcscmp(L"/start", argv[1])) { - return server_main(); - } else if (argc > 1 && !wcscmp(L"/stop", argv[1])) { - weasel::Client client; - if (!client.Connect()) { - std::cerr << "server not running." << std::endl; - return 0; - } - client.ShutdownServer(); - return 0; - } else if (argc > 1 && !wcscmp(L"/console", argv[1])) { - return console_main(); - return 0; - } - - return -1; -} - -bool launch_server() { - int ret = (int)ShellExecute(NULL, L"open", L"TestWeaselIPC.exe", L"/start", - NULL, SW_NORMAL); - if (ret <= 32) { - std::cerr << "failed to launch server." << std::endl; - return false; - } - return true; -} - -bool read_buffer(LPWSTR buffer, UINT length, LPWSTR dest) { - wbufferstream bs(buffer, length); - bs.read(dest, WEASEL_IPC_BUFFER_LENGTH); - return bs.good(); -} - -const char* wcstomb(const wchar_t* wcs) { - const int buffer_len = 8192; - static char buffer[buffer_len]; - WideCharToMultiByte(CP_OEMCP, NULL, wcs, -1, buffer, buffer_len, NULL, FALSE); - return buffer; -} - -int console_main() { - weasel::Client client; - if (!client.Connect()) { - std::cerr << "failed to connect to server." << std::endl; - return -2; - } - client.StartSession(); - if (!client.Echo()) { - std::cerr << "failed to start session." << std::endl; - return -3; - } - - while (std::cin.good()) { - int ch = std::cin.get(); - if (!std::cin.good()) - break; - bool eaten = client.ProcessKeyEvent(weasel::KeyEvent(ch, 0)); - std::cout << "server replies: " << eaten << std::endl; - if (eaten) { - WCHAR response[WEASEL_IPC_BUFFER_LENGTH]; - bool ret = client.GetResponseData( - std::bind(read_buffer, std::placeholders::_1, - std::placeholders::_2, std::ref(response))); - std::cout << "get response data: " << ret << std::endl; - std::cout << "buffer reads: " << std::endl - << wcstomb(response) << std::endl; - } - } - - client.EndSession(); - - return 0; -} - -int client_main() { - // launch_server(); - Sleep(1000); - weasel::Client client; - if (!client.Connect()) { - std::cerr << "failed to connect to server." << std::endl; - return -2; - } - client.StartSession(); - if (!client.Echo()) { - std::cerr << "failed to login." << std::endl; - return -3; - } - bool eaten = client.ProcessKeyEvent(weasel::KeyEvent(L'a', 0)); - std::cout << "server replies: " << eaten << std::endl; - if (eaten) { - WCHAR response[WEASEL_IPC_BUFFER_LENGTH]; - bool ret = client.GetResponseData( - std::bind(read_buffer, std::placeholders::_1, - std::placeholders::_2, std::ref(response))); - std::cout << "get response data: " << ret << std::endl; - std::cout << "buffer reads: " << std::endl - << wcstomb(response) << std::endl; - } - client.EndSession(); - - system("pause"); - return 0; -} - -class TestRequestHandler : public weasel::RequestHandler { - public: - TestRequestHandler() : m_counter(0) { - std::cerr << "handler ctor." << std::endl; - } - virtual ~TestRequestHandler() { - std::cerr << "handler dtor: " << m_counter << std::endl; - } - virtual UINT FindSession(UINT session_id) { - std::cerr << "FindSession: " << session_id << std::endl; - return (session_id <= m_counter ? session_id : 0); - } - virtual UINT AddSession(LPWSTR buffer) { - std::cerr << "AddSession: " << m_counter + 1 << std::endl; - return ++m_counter; - } - virtual UINT RemoveSession(UINT session_id) { - std::cerr << "RemoveClient: " << session_id << std::endl; - return 0; - } - virtual BOOL ProcessKeyEvent(weasel::KeyEvent keyEvent, - UINT session_id, - EatLine eat) { - std::cerr << "ProcessKeyEvent: " << session_id - << " keycode: " << keyEvent.keycode << " mask: " << keyEvent.mask - << std::endl; - eat(std::wstring(L"Greeting=Hello, 小狼毫.\n")); - return TRUE; - } - - private: - unsigned int m_counter; -}; - -int server_main() { - HRESULT hRes = _Module.Init(NULL, GetModuleHandle(NULL)); - ATLASSERT(SUCCEEDED(hRes)); - - weasel::Server server; - // weasel::UI ui; - // const std::unique_ptr handler(new - // RimeWithWeaselHandler(&ui)); - const std::unique_ptr handler(new TestRequestHandler); - - server.SetRequestHandler(handler.get()); - if (!server.Start()) - return -4; - std::cerr << "server running." << std::endl; - int ret = server.Run(); - std::cerr << "server quitting." << std::endl; - return ret; -} +// TestWeaselIPC.cpp : Defines the entry point for the console application. +// + +#include "stdafx.h" +#include +#include +#include +#include "../../WeaselIPC/WeaselClientImpl.h" +#include "../../WeaselIPCServer/WeaselServerImpl.h" +#include "../../WeaselServer/WeaselTrayIcon.h" + +#include +#include +using namespace boost::interprocess; + +#include +#include + +CAppModule _Module; + +int console_main(); +int client_main(); +int server_main(); +int unit_main(); + +// usage: TestWeaselIPC.exe [/start | /stop | /console] + +int _tmain(int argc, _TCHAR* argv[]) { + if (argc == 1) // no args + { + return client_main(); + } else if (argc > 1 && !wcscmp(L"/start", argv[1])) { + return server_main(); + } else if (argc > 1 && !wcscmp(L"/stop", argv[1])) { + weasel::Client client; + if (!client.TryConnect()) { + std::cerr << "server not running." << std::endl; + return 0; + } + client.ShutdownServer(weasel::WEASEL_IPC_SHUTDOWN_REASON_STOP); + return 0; + } else if (argc > 1 && !wcscmp(L"/console", argv[1])) { + return console_main(); + return 0; + } else if (argc > 1 && !wcscmp(L"/unit", argv[1])) { + return unit_main(); + } + + return -1; +} + +int unit_main() { + BOOST_TEST(!weasel::IsKeyEventResultProcessed(0)); + BOOST_TEST(!weasel::IsKeyEventResultEaten(0)); + + DWORD not_eaten = weasel::MakeKeyEventResult(FALSE); + BOOST_TEST(weasel::IsKeyEventResultProcessed(not_eaten)); + BOOST_TEST(!weasel::IsKeyEventResultEaten(not_eaten)); + + DWORD eaten = weasel::MakeKeyEventResult(TRUE); + BOOST_TEST(weasel::IsKeyEventResultProcessed(eaten)); + BOOST_TEST(weasel::IsKeyEventResultEaten(eaten)); + BOOST_TEST(!weasel::ShouldEatKeyEvent(FALSE, FALSE)); + BOOST_TEST(weasel::ShouldEatKeyEvent(TRUE, FALSE)); + BOOST_TEST(weasel::ShouldEatKeyEvent(FALSE, TRUE)); + BOOST_TEST(weasel::ShouldEatKeyEvent(TRUE, TRUE)); + BOOST_TEST(weasel::ShouldEatKeyEvent(FALSE, FALSE, TRUE, FALSE)); + BOOST_TEST(!weasel::ShouldEatKeyEvent(FALSE, FALSE, FALSE, FALSE)); + BOOST_TEST(!weasel::ShouldEatKeyEvent(FALSE, FALSE, TRUE, TRUE)); + BOOST_TEST( + !weasel::ShouldEatTestKeyEvent(false, false, false, false, false, 'A')); + BOOST_TEST(!weasel::ShouldEatTestKeyEvent(false, false, false, false, false, + true, 'A')); + BOOST_TEST( + weasel::ShouldEatTestKeyEvent(false, false, false, false, true, 'A')); + BOOST_TEST( + !weasel::ShouldEatTestKeyEvent(true, false, false, false, true, 'A')); + BOOST_TEST(!weasel::ShouldEatTestKeyEvent(true, false, false, false, true, + VK_SPACE)); + BOOST_TEST( + weasel::ShouldEatTestKeyEvent(true, false, false, false, true, VK_SHIFT)); + BOOST_TEST( + weasel::ShouldEatTestKeyEvent(true, true, false, false, true, 'A')); + BOOST_TEST(!weasel::ShouldEatTestKeyEvent(false, false, false, false, true, + VK_BACK)); + BOOST_TEST( + weasel::ShouldEatTestKeyEvent(false, false, true, false, true, VK_BACK)); + BOOST_TEST( + !weasel::ShouldEatTestKeyEvent(false, false, false, true, true, 'C')); + BOOST_TEST( + weasel::ShouldEatTestKeyEvent(false, false, true, true, true, 'C')); + BOOST_TEST(weasel::IsTextVirtualKey('A')); + BOOST_TEST(weasel::IsTextVirtualKey(VK_SPACE)); + BOOST_TEST(!weasel::IsTextVirtualKey(VK_SHIFT)); + BOOST_TEST(weasel::IsModeSwitchVirtualKey(VK_SHIFT)); + BOOST_TEST(weasel::IsModeSwitchVirtualKey(VK_CONTROL)); + BOOST_TEST(weasel::IsModeSwitchVirtualKey(VK_CAPITAL)); + BOOST_TEST(!weasel::IsModeSwitchVirtualKey(VK_BACK)); + + BOOST_TEST(weasel::IsMaintenanceDeployResult( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS)); + BOOST_TEST(weasel::IsMaintenanceDeployResult( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE)); + BOOST_TEST(!weasel::IsMaintenanceDeployResult( + weasel::WEASEL_IPC_MAINTENANCE_RESULT_NONE)); + BOOST_TEST(!strcmp(weasel::MaintenanceDeployMessageValue( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS), + "success")); + BOOST_TEST(!strcmp(weasel::MaintenanceDeployMessageValue( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE), + "failure")); + BOOST_TEST(MaintenanceDeployResultStringId( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS) == + IDS_STR_DEPLOY_SUCCESS); + BOOST_TEST(MaintenanceDeployResultStringId( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE) == + IDS_STR_DEPLOY_FAILURE); + BOOST_TEST(MaintenanceDeployResultBalloonIcon( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_SUCCESS) == NIIF_INFO); + BOOST_TEST(MaintenanceDeployResultBalloonIcon( + weasel::WEASEL_IPC_MAINTENANCE_DEPLOY_FAILURE) == NIIF_ERROR); + BOOST_TEST(ServiceNotificationStringId( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_EXITING) == + IDS_STR_SERVICE_EXITING); + BOOST_TEST(ServiceNotificationStringId( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTARTING) == + IDS_STR_SERVICE_RESTARTING); + BOOST_TEST(ServiceNotificationStringId( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS) == + IDS_STR_SERVICE_RESTART_SUCCESS); + BOOST_TEST(ServiceNotificationStringId( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE) == + IDS_STR_SERVICE_RESTART_FAILURE); + BOOST_TEST(ServiceNotificationBalloonIcon( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_FAILURE) == + NIIF_ERROR); + BOOST_TEST(ServiceNotificationBalloonIcon( + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS) == + NIIF_INFO); + BOOST_TEST(weasel::ServiceStartupNotification(false, false) == + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_NONE); + BOOST_TEST(weasel::ServiceStartupNotification(true, false) == + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS); + BOOST_TEST(weasel::ServiceStartupNotification(false, true) == + weasel::WEASEL_IPC_SERVICE_NOTIFICATION_RESTART_SUCCESS); + BOOST_TEST(!wcscmp(weasel::ServiceExecutableName(), L"WeaselServer.exe")); + BOOST_TEST(!wcscmp(weasel::ServiceRestartArgument(), L"/restart")); + BOOST_TEST( + !wcscmp(weasel::ServiceManualRestartArgument(), L"/restart-manual")); + BOOST_TEST(!wcscmp(weasel::ServiceRecoveryArgument(), L"/recover")); + BOOST_TEST(!wcscmp(weasel::ServiceStartupArgument(), L"/startup")); + BOOST_TEST(!wcscmp(weasel::ServiceStopArgument(), L"/stop")); + BOOST_TEST(weasel::IsServiceRestartCommandLine(L"/restart")); + BOOST_TEST(weasel::IsServiceRestartCommandLine(L"/restart-manual")); + BOOST_TEST(weasel::IsServiceLegacyRestartCommandLine(L"/restart")); + BOOST_TEST(!weasel::IsServiceLegacyRestartCommandLine(L"/restart-manual")); + BOOST_TEST(weasel::IsServiceManualRestartCommandLine(L"/restart-manual")); + BOOST_TEST(!weasel::IsServiceManualRestartCommandLine(L"/restart")); + BOOST_TEST(weasel::IsServiceRestartCommandLine(L"/rerun")); + BOOST_TEST(weasel::IsServiceRestartCommandLine(L" /ReStart ")); + BOOST_TEST(weasel::IsServiceManualRestartCommandLine(L" /ReStart-Manual ")); + BOOST_TEST(weasel::IsServiceRestartCommandLine(L"/RERUN ")); + BOOST_TEST(!weasel::IsServiceRestartCommandLine(L"/recover")); + BOOST_TEST(weasel::IsServiceRecoveryCommandLine(L"/recover")); + BOOST_TEST(weasel::IsServiceRecoveryCommandLine(L" /Recover ")); + BOOST_TEST(!weasel::IsServiceRecoveryCommandLine(L"/restart")); + BOOST_TEST(weasel::IsServiceStartupCommandLine(L"/startup")); + BOOST_TEST(weasel::IsServiceStartupCommandLine(L" /START ")); + BOOST_TEST(!weasel::IsServiceStartupCommandLine(L"/restart")); + BOOST_TEST(weasel::IsImplicitServiceStartCommandLine(L"")); + BOOST_TEST(weasel::IsImplicitServiceStartCommandLine(NULL)); + BOOST_TEST(!weasel::IsImplicitServiceStartCommandLine(L"/startup")); + BOOST_TEST(weasel::IsServiceQuitCommandLine(L"/q")); + BOOST_TEST(weasel::IsServiceQuitCommandLine(L" /QUIT ")); + BOOST_TEST(!weasel::IsServiceQuitCommandLine(L"/restart")); + BOOST_TEST(weasel::IsServiceStopCommandLine(L"/stop")); + BOOST_TEST(weasel::IsServiceStopCommandLine(L" /STOP-SERVICE ")); + BOOST_TEST(!weasel::IsServiceStopCommandLine(L"/q")); + BOOST_TEST(!weasel::IsServiceRestartCommandLine(L"")); + BOOST_TEST(!weasel::IsServiceRestartCommandLine(NULL)); + std::wstring service_root = L"C:\\Program Files\\Rime\\weasel"; + BOOST_TEST(weasel::ServiceExecutablePath(service_root) == + L"C:\\Program Files\\Rime\\weasel\\WeaselServer.exe"); + std::wstring parsed_root; + BOOST_TEST(weasel::ServiceRootFromCommandLine( + L"C:\\Program Files\\Rime\\weasel\\WeaselServer.exe", parsed_root)); + BOOST_TEST(parsed_root == service_root); + parsed_root.clear(); + BOOST_TEST(weasel::ServiceRootFromCommandLine( + L"\"C:\\Program Files\\Rime\\weasel\\WeaselServer.exe\" /restart", + parsed_root)); + BOOST_TEST(parsed_root == service_root); + BOOST_TEST(!weasel::ServiceRootFromCommandLine(L"", parsed_root)); + BOOST_TEST(!weasel::ShouldTreatLegacyRestartAsManual( + L"/restart", L"notepad.exe", L"C:\\Windows\\System32\\notepad.exe")); + BOOST_TEST(weasel::ShouldTreatLegacyRestartAsManual( + L"/rerun", L"explorer.exe", L"C:\\Windows\\explorer.exe")); + BOOST_TEST(weasel::ShouldTreatLegacyRestartAsManual( + L"/restart", L"WeaselDeployer.exe", + L"C:\\Program Files\\Rime\\weasel\\WeaselDeployer.exe")); + BOOST_TEST(weasel::ShouldTreatLegacyRestartAsManual( + L"/restart", L"WeaselSetup.exe", + L"C:\\Program Files\\Rime\\weasel\\WeaselSetup.exe")); + BOOST_TEST(!weasel::ShouldTreatLegacyRestartAsManual( + L"/restart", L"cmd.exe", L"C:\\Windows\\System32\\cmd.exe")); + BOOST_TEST(!weasel::ShouldTreatLegacyRestartAsManual(L"/restart", L"", L"")); + BOOST_TEST(!weasel::ShouldTreatLegacyRestartAsManual( + L"/restart-manual", L"notepad.exe", + L"C:\\Windows\\System32\\notepad.exe")); + + struct ManualExitFlagGuard { + ManualExitFlagGuard() : was_marked(weasel::IsServiceManualExitMarked()) {} + ~ManualExitFlagGuard() { + if (was_marked) + weasel::MarkServiceManualExit(); + else + weasel::ClearServiceManualExit(); + } + bool was_marked; + } manual_exit_guard; + + { + weasel::ClearServiceManualExit(); + BOOST_TEST(!weasel::IsServiceManualExitMarked()); + BOOST_TEST(weasel::ShouldAutoRecoverService()); + weasel::MarkServiceManualExit(); + BOOST_TEST(weasel::IsServiceManualExitMarked()); + BOOST_TEST(!weasel::ShouldAutoRecoverService()); + BOOST_TEST( + !weasel::ShouldSuppressServiceRecoveryAfterManualExit(L"/restart")); + BOOST_TEST( + weasel::ShouldSuppressServiceRecoveryAfterManualExit(L"/recover")); + BOOST_TEST(!weasel::ShouldSuppressServiceRecoveryAfterManualExit(L"")); + BOOST_TEST(!weasel::ShouldSuppressServiceRecoveryAfterManualExit(L"/q")); + BOOST_TEST(weasel::ShouldSuppressServiceStartAfterManualExit(L"")); + BOOST_TEST(weasel::ShouldSuppressServiceStartAfterManualExit(NULL)); + BOOST_TEST(weasel::ShouldSuppressServiceStartAfterManualExit(L"/recover")); + BOOST_TEST(weasel::ShouldSuppressServiceStartAfterManualExit(L"/restart")); + BOOST_TEST( + !weasel::ShouldSuppressServiceStartAfterManualExit(L"/restart-manual")); + BOOST_TEST(!weasel::ShouldSuppressServiceStartAfterManualExit(L"/startup")); + BOOST_TEST(!weasel::ShouldSuppressServiceStartAfterManualExit(L"/q")); + BOOST_TEST(!weasel::ShouldSuppressServiceStartAfterManualExit(L"/stop")); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(L"")); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(NULL)); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(L"/recover")); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(L"/update")); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(L"/unknown")); + BOOST_TEST(!weasel::ShouldClearServiceManualExit(L"/restart")); + BOOST_TEST(weasel::ShouldClearServiceManualExit(L"/restart-manual")); + BOOST_TEST(weasel::ShouldClearServiceManualExit(L"/startup")); + BOOST_TEST(weasel::ShouldShutdownExistingService(L"/q")); + BOOST_TEST(weasel::ShouldShutdownExistingService(L"/stop")); + BOOST_TEST(weasel::ShouldShutdownExistingService(L"/restart")); + BOOST_TEST(!weasel::ShouldShutdownExistingService(L"")); + BOOST_TEST(!weasel::ShouldShutdownExistingService(NULL)); + BOOST_TEST(!weasel::ShouldShutdownExistingService(L"/startup")); + BOOST_TEST(!weasel::ShouldShutdownExistingService(L"/recover")); + BOOST_TEST(!weasel::ShouldShutdownExistingService(L"/update")); + weasel::ClearServiceManualExit(); + BOOST_TEST(!weasel::IsServiceManualExitMarked()); + BOOST_TEST(weasel::ShouldAutoRecoverService()); + BOOST_TEST( + !weasel::ShouldSuppressServiceRecoveryAfterManualExit(L"/recover")); + BOOST_TEST(!weasel::ShouldSuppressServiceStartAfterManualExit(L"")); + } + + BOOST_TEST(weasel::IsTrayMenuSelectionCancelled(0)); + BOOST_TEST(!weasel::IsTrayMenuSelectionCancelled(ID_WEASELTRAY_QUIT)); + BOOST_TEST( + !weasel::IsTrayMenuSelectionCancelled(ID_WEASELTRAY_RERUN_SERVICE)); + + weasel::UIStyle style; + weasel::Status status; + status.composing = true; + weasel::MarkLocalCompositionAborted(status); + BOOST_TEST(!status.composing); + status.reset(); + + auto signature = RimeTrayIconSignature::From(style, status); + BOOST_TEST(signature == RimeTrayIconSignature::From(style, status)); + + weasel::Status composing_status = status; + composing_status.composing = true; + BOOST_TEST(signature == RimeTrayIconSignature::From(style, composing_status)); + + weasel::Status ascii_status = status; + ascii_status.ascii_mode = true; + BOOST_TEST(signature != RimeTrayIconSignature::From(style, ascii_status)); + + weasel::UIStyle icon_style = style; + icon_style.current_zhung_icon = L"zh.ico"; + BOOST_TEST(signature != RimeTrayIconSignature::From(icon_style, status)); + + RIME_STRUCT(RimeStatus, rime_status); + rime_status.schema_id = "luna_pinyin"; + rime_status.schema_name = "Luna Pinyin"; + rime_status.is_ascii_mode = True; + rime_status.is_composing = True; + rime_status.is_disabled = False; + rime_status.is_full_shape = True; + + auto status_snapshot = RimeUiStatusSnapshot::From(rime_status); + BOOST_TEST(status_snapshot.has_status); + BOOST_TEST(status_snapshot.schema_id == "luna_pinyin"); + BOOST_TEST(status_snapshot.status.schema_id == L"luna_pinyin"); + BOOST_TEST(status_snapshot.status.schema_name == L"Luna Pinyin"); + BOOST_TEST(status_snapshot.status.ascii_mode); + BOOST_TEST(status_snapshot.status.composing); + BOOST_TEST(!status_snapshot.status.disabled); + BOOST_TEST(status_snapshot.status.full_shape); + + weasel::KeyEventTestCache key_cache; + BOOST_TEST(!key_cache.Matches(false, 'A', 0x1e0001)); + key_cache.Store(false, 'A', 0x1e0001, FALSE); + BOOST_TEST(key_cache.Matches(false, 'A', 0x1e0001)); + BOOST_TEST(!key_cache.Eaten()); + BOOST_TEST(key_cache.Matches(false, 'A', 0x401e0002)); + BOOST_TEST(!key_cache.Eaten()); + BOOST_TEST(!key_cache.Matches(true, 'A', 0x1e0001)); + BOOST_TEST(!key_cache.Matches(false, 'B', 0x1e0001)); + BOOST_TEST(!key_cache.Matches(false, 'A', 0x1f0001)); + key_cache.Store(true, 'A', 0x9e0001, TRUE); + BOOST_TEST(key_cache.Matches(true, 'A', 0x9e0001)); + BOOST_TEST(key_cache.Eaten()); + key_cache.Clear(); + BOOST_TEST(!key_cache.Matches(true, 'A', 0x9e0001)); + + key_cache.Store(false, VK_SHIFT, 0x002a0001, FALSE); + key_cache.Remove(false, VK_SHIFT, 0x802a0001); + BOOST_TEST(!key_cache.Matches(false, VK_SHIFT, 0x002a0001)); + key_cache.Store(true, VK_SHIFT, 0x802a0001, TRUE); + key_cache.Remove(true, VK_SHIFT, 0x002a0001); + BOOST_TEST(!key_cache.Matches(true, VK_SHIFT, 0x802a0001)); + + key_cache.Store(false, 'A', 0x1e0001, FALSE); + key_cache.Store(false, 'B', 0x300001, TRUE); + BOOST_TEST(key_cache.Matches(false, 'A', 0x1e0001)); + BOOST_TEST(!key_cache.Eaten()); + BOOST_TEST(key_cache.Matches(false, 'B', 0x300001)); + BOOST_TEST(key_cache.Eaten()); + BOOST_TEST(key_cache.Matches(false, 'A', 0x1e0001)); + key_cache.RemoveMatched(); + BOOST_TEST(!key_cache.Matches(false, 'A', 0x1e0001)); + BOOST_TEST(key_cache.Matches(false, 'B', 0x300001)); + key_cache.RemoveMatched(); + BOOST_TEST(!key_cache.Matches(false, 'B', 0x300001)); + + weasel::KeyEventTestCacheReset key_cache_reset; + key_cache.Store(false, 'C', 0x2e0001, TRUE); + key_cache_reset.Mark(); + BOOST_TEST(key_cache_reset.Pending()); + BOOST_TEST(key_cache_reset.Apply(key_cache)); + BOOST_TEST(!key_cache_reset.Pending()); + BOOST_TEST(!key_cache.Matches(false, 'C', 0x2e0001)); + BOOST_TEST(!key_cache_reset.Apply(key_cache)); + + weasel::ActiveKeyDownGuard active_key_guard; + BOOST_TEST(!active_key_guard.ShouldSuppress('A', 0x001e0001)); + active_key_guard.Remember('A', 0x001e0001); + BOOST_TEST(active_key_guard.ShouldSuppress('A', 0x001e0001)); + BOOST_TEST(!active_key_guard.ShouldSuppress('A', 0x401e0001)); + active_key_guard.Release('A', 0x801e0001); + BOOST_TEST(!active_key_guard.ShouldSuppress('A', 0x001e0001)); + BOOST_TEST(!active_key_guard.ShouldSuppress('C', 0x002e0001)); + active_key_guard.Remember('C', 0x402e0001); + BOOST_TEST(!active_key_guard.ShouldSuppress('C', 0x402e0001)); + active_key_guard.Remember('C', 0x002e0001); + BOOST_TEST(active_key_guard.ShouldSuppress('C', 0x002e0001)); + active_key_guard.Release('C', 0x802e0001); + BOOST_TEST(!active_key_guard.ShouldSuppress('C', 0x002e0001)); + BOOST_TEST(!active_key_guard.ShouldSuppress('B', 0x00300001)); + active_key_guard.Remember('B', 0x00300001); + BOOST_TEST(active_key_guard.ShouldSuppress('B', 0x00300001)); + active_key_guard.Reset(); + BOOST_TEST(!active_key_guard.ShouldSuppress('B', 0x00300001)); + + weasel::InputPositionCache position_cache; + BOOST_TEST(position_cache.ShouldSend(0x12345678)); + position_cache.MarkSent(0x12345678); + BOOST_TEST(!position_cache.ShouldSend(0x12345678)); + BOOST_TEST(position_cache.ShouldSend(0x12345679)); + position_cache.Reset(); + BOOST_TEST(position_cache.ShouldSend(0x12345678)); + + weasel::Context current_context; + weasel::Status current_status; + BOOST_TEST(!RimeUiNeedsUpdate(current_context, current_status, + current_context, current_status)); + + weasel::Context next_context = current_context; + next_context.aux.str = L"tip"; + BOOST_TEST(RimeUiNeedsUpdate(current_context, current_status, next_context, + current_status)); + + weasel::Status next_status = current_status; + next_status.ascii_mode = true; + BOOST_TEST(RimeUiNeedsUpdate(current_context, current_status, current_context, + next_status)); + + BOOST_TEST(ShouldSuppressInlineOptionNotification("option", "ascii_mode")); + BOOST_TEST(ShouldSuppressInlineOptionNotification("option", "!ascii_mode")); + BOOST_TEST(!ShouldSuppressInlineOptionNotification("option", "full_shape")); + BOOST_TEST(!ShouldSuppressInlineOptionNotification("option", "!full_shape")); + BOOST_TEST(!ShouldSuppressInlineOptionNotification("deploy", "success")); + BOOST_TEST(!ShouldSuppressInlineOptionNotification("schema", "luna_pinyin")); + + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_PROCESS_KEY_EVENT_WITH_STATUS, 0, 0})); + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_PROCESS_KEY_EVENT, 0, 0})); + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_SHUTDOWN_SERVER, 0, 0})); + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_UPDATE_INPUT_POS, 0, 0})); + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_TRAY_COMMAND, ID_WEASELTRAY_DEPLOY, 0})); + BOOST_TEST(!weasel::ServerImpl::PipeMessageNeedsOuterApiLock( + {WEASEL_IPC_TRAY_COMMAND, ID_WEASELTRAY_ENABLE_ASCII, 0})); + + weasel::ClientImpl missing_server_client( + L"\\\\.\\pipe\\WeaselTestMissingServerPipe"); + BOOST_TEST(!missing_server_client.TryConnect()); + + return boost::report_errors(); +} + +bool launch_server() { + int ret = (int)ShellExecute(NULL, L"open", L"TestWeaselIPC.exe", L"/start", + NULL, SW_NORMAL); + if (ret <= 32) { + std::cerr << "failed to launch server." << std::endl; + return false; + } + return true; +} + +bool read_buffer(LPWSTR buffer, UINT length, LPWSTR dest) { + wbufferstream bs(buffer, length); + bs.read(dest, WEASEL_IPC_BUFFER_LENGTH); + return bs.good(); +} + +const char* wcstomb(const wchar_t* wcs) { + const int buffer_len = 8192; + static char buffer[buffer_len]; + WideCharToMultiByte(CP_OEMCP, NULL, wcs, -1, buffer, buffer_len, NULL, FALSE); + return buffer; +} + +int console_main() { + weasel::Client client; + if (!client.Connect()) { + std::cerr << "failed to connect to server." << std::endl; + return -2; + } + client.StartSession(); + if (!client.Echo()) { + std::cerr << "failed to start session." << std::endl; + return -3; + } + + while (std::cin.good()) { + int ch = std::cin.get(); + if (!std::cin.good()) + break; + bool eaten = client.ProcessKeyEvent(weasel::KeyEvent(ch, 0)); + std::cout << "server replies: " << eaten << std::endl; + if (eaten) { + WCHAR response[WEASEL_IPC_BUFFER_LENGTH]; + bool ret = client.GetResponseData( + std::bind(read_buffer, std::placeholders::_1, + std::placeholders::_2, std::ref(response))); + std::cout << "get response data: " << ret << std::endl; + std::cout << "buffer reads: " << std::endl + << wcstomb(response) << std::endl; + } + } + + client.EndSession(); + + return 0; +} + +int client_main() { + // launch_server(); + Sleep(1000); + weasel::Client client; + if (!client.Connect()) { + std::cerr << "failed to connect to server." << std::endl; + return -2; + } + client.StartSession(); + if (!client.Echo()) { + std::cerr << "failed to login." << std::endl; + return -3; + } + bool eaten = client.ProcessKeyEvent(weasel::KeyEvent(L'a', 0)); + std::cout << "server replies: " << eaten << std::endl; + if (eaten) { + WCHAR response[WEASEL_IPC_BUFFER_LENGTH]; + bool ret = client.GetResponseData( + std::bind(read_buffer, std::placeholders::_1, + std::placeholders::_2, std::ref(response))); + std::cout << "get response data: " << ret << std::endl; + std::cout << "buffer reads: " << std::endl + << wcstomb(response) << std::endl; + } + client.EndSession(); + + system("pause"); + return 0; +} + +class TestRequestHandler : public weasel::RequestHandler { + public: + TestRequestHandler() : m_counter(0) { + std::cerr << "handler ctor." << std::endl; + } + virtual ~TestRequestHandler() { + std::cerr << "handler dtor: " << m_counter << std::endl; + } + virtual UINT FindSession(UINT session_id) { + std::cerr << "FindSession: " << session_id << std::endl; + return (session_id <= m_counter ? session_id : 0); + } + virtual UINT AddSession(LPWSTR buffer) { + std::cerr << "AddSession: " << m_counter + 1 << std::endl; + return ++m_counter; + } + virtual UINT RemoveSession(UINT session_id) { + std::cerr << "RemoveClient: " << session_id << std::endl; + return 0; + } + virtual BOOL ProcessKeyEvent(weasel::KeyEvent keyEvent, + UINT session_id, + EatLine eat) { + std::cerr << "ProcessKeyEvent: " << session_id + << " keycode: " << keyEvent.keycode << " mask: " << keyEvent.mask + << std::endl; + eat(std::wstring(L"Greeting=Hello, 小狼毫.\n")); + return TRUE; + } + + private: + unsigned int m_counter; +}; + +int server_main() { + HRESULT hRes = _Module.Init(NULL, GetModuleHandle(NULL)); + ATLASSERT(SUCCEEDED(hRes)); + + weasel::Server server; + // weasel::UI ui; + // const std::unique_ptr handler(new + // RimeWithWeaselHandler(&ui)); + const std::unique_ptr handler(new TestRequestHandler); + + server.SetRequestHandler(handler.get()); + if (!server.Start()) + return -4; + std::cerr << "server running." << std::endl; + int ret = server.Run(); + std::cerr << "server quitting." << std::endl; + return ret; +}