Files
xrdp/sesman/chansrv/input_ibus.c
T
Liyi Meng 92fdb09959 chansrv: Fix stale engine handling and cross-thread ibus commit
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>
2026-08-17 20:38:20 +00:00

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;
}