92fdb09959
A few follow-on fixes found while testing the ibus reconnect changes in a live session: - xrdp_input_unicode_init() failed outright if ibus had no default global engine yet at connect time, which is a normal state on a fresh session (nothing has chosen one yet), not an error. Since nothing else used the return value, this permanently killed unicode input for the whole session over a spurious check. - ibus can disable our engine instance (e.g. on a focus change) while leaving the global engine name as "XrdpIme", since those are tracked separately. xrdp_input_enable()'s fast path only checked the name, so it could skip re-asserting and leave xrdp_input_send_unicode() committing text through a disabled g_engine. Clear g_engine on disable and require it to be set for the fast path to apply. - ibus_engine_commit_text() was being called from chansrv's own thread, not the thread pumping the glib main loop that owns the engine's D-Bus connection (xrdp_input_main_loop). Marshal the actual commit through g_main_context_invoke() onto the correct thread. None of these are the full fix for intermittent dropped commits during real pinyin input testing - that's still open, with the current lead being ibus Reset calls interleaved with commit bursts, likely from the FocusOut/FocusIn churn caused by passing every raw keystroke through engine_process_key_event_cb. To be continued. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
383 lines
12 KiB
C
383 lines
12 KiB
C
/**
|
|
* xrdp: A Remote Desktop Protocol server.
|
|
*
|
|
* Copyright (C) Jay Sorg 2009-2013
|
|
* Copyright (C) Laxmikant Rashinkar 2009-2012
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
#if defined(HAVE_CONFIG_H)
|
|
#include <config_ac.h>
|
|
#endif
|
|
|
|
#include <glib.h>
|
|
#include <glib-object.h>
|
|
#include <glib/gstdio.h>
|
|
#include <ibus.h>
|
|
#include "input.h"
|
|
#include "thread_calls.h"
|
|
|
|
static IBusBus *bus;
|
|
static IBusEngine *g_engine;
|
|
/* This is the engine name enabled before unicode engine enabled */
|
|
static gchar *last_input_name;
|
|
static int id = 0;
|
|
|
|
static int
|
|
xrdp_input_enable(void)
|
|
{
|
|
IBusEngineDesc *desc;
|
|
const gchar *name;
|
|
|
|
if (!bus)
|
|
{
|
|
LOG(LOG_LEVEL_ERROR, "xrdp_ibus_init: input method switched failed, ibus not connected");
|
|
return 1;
|
|
}
|
|
|
|
/* Re-check the current global engine on every call rather than
|
|
* trusting a one-time flag: ibus (particularly with
|
|
* use_global_engine disabled, which is common) can silently swap
|
|
* the active engine back to the user's own IME between calls, and
|
|
* we need to notice that and reassert XrdpIme rather than keep
|
|
* committing text to an engine that's no longer active. */
|
|
desc = ibus_bus_get_global_engine(bus);
|
|
name = desc ? ibus_engine_desc_get_name(desc) : NULL;
|
|
if (name && !g_ascii_strcasecmp(name, "XrdpIme") && g_engine)
|
|
{
|
|
/* XrdpIme is the global engine AND our cached engine instance is
|
|
* still the enabled one - genuinely nothing to do. If g_engine
|
|
* is NULL here, ibus disabled our instance (see
|
|
* xrdp_input_ibus_engine_disable) without changing the global
|
|
* engine name, so fall through and reassert below to get a
|
|
* fresh, enabled instance. */
|
|
g_object_unref(desc);
|
|
return 0;
|
|
}
|
|
|
|
if (!last_input_name && name)
|
|
{
|
|
/* remember user's original input method (first time only), will
|
|
* switch back when disconnected. Copy the name out since it's
|
|
* owned by desc, which we're about to unref. */
|
|
last_input_name = g_strdup(name);
|
|
}
|
|
|
|
if (desc)
|
|
{
|
|
g_object_unref(desc);
|
|
}
|
|
|
|
if (!ibus_bus_set_global_engine(bus, "XrdpIme"))
|
|
{
|
|
LOG(LOG_LEVEL_ERROR, "xrdp_input_enable: input method enable failed");
|
|
return 1;
|
|
}
|
|
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_init: input method switched sucessfully, old input name: %s", last_input_name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static gboolean
|
|
xrdp_input_commit_text_cb(gpointer data)
|
|
{
|
|
gunichar chr = GPOINTER_TO_UINT(data);
|
|
|
|
/* Runs on the same thread/main context as xrdp_input_main_loop()
|
|
* (see the g_main_context_invoke() call below), so this re-check of
|
|
* g_engine is race-free against xrdp_input_ibus_engine_enable() /
|
|
* xrdp_input_ibus_engine_disable(), which are dispatched on that
|
|
* same thread.
|
|
*
|
|
* NOTE: engine->has_focus was tried here as a gate (skip/retry the
|
|
* commit until the engine reports focus) on the theory that ibus
|
|
* drops commits sent while unfocused. That made things strictly
|
|
* worse: has_focus read FALSE on every single attempt, even ones
|
|
* that would otherwise have succeeded, because IBusEngine does not
|
|
* appear to auto-maintain that field from the FocusIn/FocusOut
|
|
* D-Bus calls in this setup - nothing in this file ever writes to
|
|
* it, so it's not a trustworthy signal without our own focus-in/
|
|
* focus-out handlers actually tracking it. Do not reintroduce a
|
|
* has_focus check without first confirming it actually flips to
|
|
* TRUE (e.g. by logging it from a real focus-in signal handler). */
|
|
if (g_engine)
|
|
{
|
|
ibus_engine_commit_text(g_engine, ibus_text_new_from_unichar(chr));
|
|
}
|
|
else
|
|
{
|
|
LOG(LOG_LEVEL_ERROR, "xrdp_input_send_unicode: no active ibus engine to commit to");
|
|
}
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
int
|
|
xrdp_input_send_unicode(char32_t unicode)
|
|
{
|
|
LOG(LOG_LEVEL_DEBUG, "xrdp_input_send_unicode: received unicode input %i", unicode);
|
|
|
|
if (xrdp_input_enable())
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
/* ibus_engine_commit_text() must run on the same thread that's
|
|
* pumping the glib main loop the engine's D-Bus connection belongs
|
|
* to (the xrdp_input_main_loop thread) - that's also the thread
|
|
* that processes FocusIn/FocusOut for the engine. Calling it
|
|
* directly from here (chansrv's own thread) lets the commit race
|
|
* against that focus churn with no ordering guarantee, so ibus can
|
|
* silently drop it. g_main_context_invoke() marshals the actual
|
|
* commit onto the correct thread instead. */
|
|
g_main_context_invoke(NULL, xrdp_input_commit_text_cb,
|
|
GUINT_TO_POINTER((guint)unicode));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
xrdp_input_ibus_engine_enable(IBusEngine *engine)
|
|
{
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_engine_enable: IM enabled");
|
|
g_engine = engine;
|
|
}
|
|
|
|
static void
|
|
xrdp_input_ibus_engine_disable(IBusEngine *engine)
|
|
{
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_engine_disable: IM disabled");
|
|
|
|
/* ibus can disable our engine instance (e.g. it loses focus) while
|
|
* still reporting "XrdpIme" as the global engine name, since that's
|
|
* a separate piece of bookkeeping. Clear g_engine so xrdp_input_enable()
|
|
* notices the mismatch instead of committing text to a disabled
|
|
* engine on the next keypress. */
|
|
if (engine == g_engine)
|
|
{
|
|
g_engine = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
xrdp_input_ibus_disconnect(IBusEngine *engine)
|
|
{
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_engine_disable: IM disabled");
|
|
if (g_engine)
|
|
{
|
|
g_object_unref(g_engine);
|
|
g_engine = NULL;
|
|
}
|
|
if (bus)
|
|
{
|
|
g_object_unref(bus);
|
|
bus = NULL;
|
|
}
|
|
g_free(last_input_name);
|
|
last_input_name = NULL;
|
|
|
|
/* ibus_main()/ibus_quit() operate on a single loop shared by the
|
|
* whole process, not one per IBusBus. Without this, the
|
|
* xrdp_input_main_loop thread for this (now dead) connection stays
|
|
* blocked in ibus_main() forever, and a fresh thread + loop gets
|
|
* started on the next reconnect - leaking a thread per reconnect. */
|
|
ibus_quit();
|
|
}
|
|
|
|
static gboolean
|
|
engine_process_key_event_cb(IBusEngine *engine,
|
|
guint keyval,
|
|
guint keycode,
|
|
guint state)
|
|
{
|
|
/* Pass the keyboard event to system */
|
|
return FALSE;
|
|
}
|
|
|
|
static IBusEngine *
|
|
xrdp_input_ibus_create_engine(IBusFactory *factory,
|
|
gchar *engine_name,
|
|
gpointer user_data)
|
|
{
|
|
IBusEngine *engine;
|
|
gchar *path = g_strdup_printf("/org/freedesktop/IBus/Engine/%i", ++id);
|
|
engine = ibus_engine_new(engine_name,
|
|
path,
|
|
ibus_bus_get_connection(bus));
|
|
|
|
LOG(LOG_LEVEL_DEBUG, "xrdp_input_ibus_create_engine: Creating IM Engine with name:%s and id:%d\n", engine_name, id);
|
|
g_free(path);
|
|
|
|
g_signal_connect(engine, "process-key-event", G_CALLBACK(engine_process_key_event_cb), NULL);
|
|
g_signal_connect(engine, "enable", G_CALLBACK(xrdp_input_ibus_engine_enable), NULL);
|
|
g_signal_connect(engine, "disable", G_CALLBACK(xrdp_input_ibus_engine_disable), NULL);
|
|
|
|
return engine;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
static THREAD_RV THREAD_CC
|
|
xrdp_input_main_loop(void *in_val)
|
|
{
|
|
IBusFactory *factory;
|
|
IBusComponent *component;
|
|
IBusEngineDesc *desc;
|
|
THREAD_RV rv = 0;
|
|
|
|
LOG(LOG_LEVEL_DEBUG, "xrdp_input_main_loop: Entering ibus loop");
|
|
|
|
g_signal_connect(bus, "disconnected", G_CALLBACK(xrdp_input_ibus_disconnect), NULL);
|
|
|
|
factory = ibus_factory_new(ibus_bus_get_connection(bus));
|
|
g_object_ref_sink(factory);
|
|
g_signal_connect(factory, "create-engine", G_CALLBACK(xrdp_input_ibus_create_engine), NULL);
|
|
|
|
ibus_factory_add_engine(factory, "XrdpIme", IBUS_TYPE_ENGINE);
|
|
|
|
component = ibus_component_new("org.freedesktop.IBus.XrdpIme", /* name */
|
|
"Xrdp input method", /* description */
|
|
"1.1", /* version */
|
|
"MIT", /* license */
|
|
"seflerZ", /* author */
|
|
"default", /* homepage */
|
|
"default", /* cmd */
|
|
"xrdpime"); /* text domain */
|
|
|
|
desc = ibus_engine_desc_new("XrdpIme",
|
|
"unicode input method for xrdp",
|
|
"unicode input method for xrdp",
|
|
"unicode",
|
|
"MIT",
|
|
"seflerZ",
|
|
"default", /* icon */
|
|
"default"); /* layout */
|
|
|
|
ibus_component_add_engine(component, desc);
|
|
ibus_bus_register_component(bus, component);
|
|
|
|
ibus_main();
|
|
|
|
g_object_unref(desc);
|
|
g_object_unref(component);
|
|
g_object_unref(factory);
|
|
|
|
return rv;
|
|
}
|
|
|
|
int
|
|
xrdp_input_unicode_destroy(void)
|
|
{
|
|
LOG(LOG_LEVEL_DEBUG, "xrdp_input_unicode_destory: ibus input is under destory");
|
|
if (last_input_name && bus)
|
|
{
|
|
LOG(LOG_LEVEL_INFO, "xrdp_input_unicode_destory: ibus engine rolling back to origin: %s", last_input_name);
|
|
ibus_bus_set_global_engine(bus, last_input_name);
|
|
}
|
|
|
|
if (g_engine)
|
|
{
|
|
g_object_unref(g_engine);
|
|
}
|
|
if (bus)
|
|
{
|
|
g_object_unref(bus);
|
|
}
|
|
|
|
g_free(last_input_name);
|
|
last_input_name = NULL;
|
|
bus = NULL;
|
|
g_engine = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
xrdp_input_unicode_init(void)
|
|
{
|
|
if (bus)
|
|
{
|
|
if (ibus_bus_is_connected(bus))
|
|
{
|
|
/* Already initialized, just re-enable it */
|
|
xrdp_input_enable();
|
|
return 0;
|
|
}
|
|
|
|
/* The bus is stale (e.g. the ibus daemon restarted and left a
|
|
* dead connection behind). Tear it down so we reconnect below
|
|
* instead of operating on a dead connection. */
|
|
LOG(LOG_LEVEL_WARNING,
|
|
"xrdp_ibus_init: existing iBus connection is stale, reconnecting");
|
|
if (g_engine)
|
|
{
|
|
g_object_unref(g_engine);
|
|
g_engine = NULL;
|
|
}
|
|
g_object_unref(bus);
|
|
bus = NULL;
|
|
g_free(last_input_name);
|
|
last_input_name = NULL;
|
|
|
|
/* Belt-and-suspenders: normally the "disconnected" signal handler
|
|
* (xrdp_input_ibus_disconnect) already called this when the bus
|
|
* dropped. But a stale connection can be detected here without
|
|
* that signal ever having fired, and ibus_main()/ibus_quit() are
|
|
* process-global rather than per-bus, so make sure the old
|
|
* xrdp_input_main_loop thread isn't left blocked in ibus_main()
|
|
* before we start a new one below. */
|
|
ibus_quit();
|
|
}
|
|
|
|
/* Wait because the ibus daemon may not be ready on first login */
|
|
const char *addr = ibus_get_address();
|
|
unsigned int cnt = 0;
|
|
while (!addr && cnt < 10)
|
|
{
|
|
usleep(500 * 1000); // half a second
|
|
addr = ibus_get_address();
|
|
++cnt;
|
|
}
|
|
if (!addr)
|
|
{
|
|
LOG(LOG_LEVEL_ERROR,
|
|
"xrdp_ibus_init: Timed out waiting for iBus daemon");
|
|
return 1;
|
|
}
|
|
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_init: Initializing the iBus engine");
|
|
ibus_init();
|
|
bus = ibus_bus_new();
|
|
g_object_ref_sink(bus);
|
|
|
|
if (!ibus_bus_is_connected(bus))
|
|
{
|
|
LOG(LOG_LEVEL_ERROR, "xrdp_ibus_init: Connect to iBus failed");
|
|
g_object_unref(bus);
|
|
bus = NULL;
|
|
return 1;
|
|
}
|
|
|
|
LOG(LOG_LEVEL_INFO, "xrdp_ibus_init: iBus connected");
|
|
|
|
tc_thread_create(xrdp_input_main_loop, NULL);
|
|
|
|
/* No global engine may be set yet at this point (e.g. ibus hasn't
|
|
* picked a default on this fresh session, or the user simply never
|
|
* had one configured) - that's a normal state, not a failure. There
|
|
* is nothing to remember here; xrdp_input_enable() already handles
|
|
* a NULL/absent current engine correctly when it's actually needed. */
|
|
|
|
return 0;
|
|
}
|