Files
xrdp/sesman/chansrv/input_ibus.c
T
Liyi Meng 2a77f5f3e8 chansrv: Rewrite ibus unicode input around a private main loop
Replace the shared-global-context design with one where chansrv's
thread never touches libibus directly. All IBus objects (bus, engine,
factory, component) are now owned exclusively by a private
GMainContext/GMainLoop created inside the IBus thread; chansrv hands
off unicode codepoints through a thread-safe GAsyncQueue and wakes the
loop, instead of calling ibus_engine_commit_text() from the wrong
thread or relying on ibus_main()/ibus_quit(), which turned out to
operate on a single loop shared by the whole process rather than one
per connection.

Fixes along the way, found by testing against a live ibus-daemon and
a real focused GTK app rather than just reading the diff:

- xrdp_engine_enable()/disable() now take a real reference on g_engine
  (g_object_ref/unref) instead of caching a bare pointer. The old code
  unreffed g_engine on teardown without ever having reffed it - IBusEngine
  derives from GInitiallyUnowned, so that was releasing a reference it
  never owned.
- A custom GSource drains the unicode queue, gated on the engine
  actually existing (engine creation is async once XrdpIme is
  selected), so a character queued before the engine is ready isn't
  lost - it commits as soon as the "enable" signal fires, instead of
  racing a one-shot commit against that async setup.
- ibus_engine_commit_text() releases its IBusText argument itself
  (it's floating, documented behavior for that specific call) - an
  earlier draft of this rewrite added an extra g_object_unref() after
  it, which would have been a double-release.
- bus (and anything else that opens a GDBusConnection) must be created
  after the private GMainContext is pushed as thread-default, not
  before - otherwise its async I/O silently binds to the global
  default context, which nothing here iterates, and signals like
  "disconnected" simply never fire.
- xrdp_input_unicode_init() now blocks on a condvar until the IBus
  thread actually signals ready (or exits), instead of returning
  immediately and hoping the engine shows up in time.
- unicode_init()/destroy() no longer touch bus/g_engine from chansrv's
  own thread while the IBus thread may still be running against them;
  destroy() schedules real teardown on the IBus thread and joins it via
  a condvar before returning.

Known limitation, confirmed empirically rather than assumed: this does
NOT make reconnect-after-daemon-restart (#3230) actually work.
ibus_bus_new() returns a process-wide singleton in this libibus
version - two calls in the same process with no disconnect involved
return the identical pointer - so once its connection dies, every
later call just hands back the same dead object; ibus_bus_is_connected()
on it stays permanently false, confirmed not to be a transient state
via repeated retries with delay. The "disconnected" handler still
tears the thread down cleanly so a later xrdp_input_unicode_init()
fails fast instead of hanging or operating on stale state, but a real
fix would mean bypassing IBusBus for a raw GDBusConnection to
ibus-daemon, which isn't justified given how rarely the daemon
actually restarts mid-session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 22:10:23 +00:00

722 lines
21 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.
*/
/*
* IBus Unicode input support for chansrv.
*
* All IBus objects (bus, engine, factory, component, and the private
* GMainContext/GMainLoop that services them) are owned and only ever
* touched by the IBus thread (xrdp_input_main_loop). chansrv's own
* thread never calls into libibus directly - it hands off unicode
* codepoints via a thread-safe queue and wakes the IBus thread's loop:
*
* chansrv thread IBus thread (private main loop)
* | |
* |-- queue('你') |
* |-- queue('好') --> XrdpIme not yet enabled?
* | | (create-engine is async)
* | v
* | wait for "enable" signal
* | |
* | v
* | drain queue, commit each char
* | |
* | commit('你'), commit('好')
*
* This means a character queued before XrdpIme has finished being
* created/enabled isn't lost - it's committed as soon as the engine
* becomes ready, instead of racing a one-shot commit against
* asynchronous engine setup.
*/
#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"
typedef struct
{
gunichar unicode;
} XrdpUnicodeEvent;
/* These are owned and accessed by the IBus thread, except where noted. */
static IBusBus *bus = NULL;
static IBusEngine *g_engine = NULL;
static gchar *last_input_name = NULL;
static GMainContext *ibus_context = NULL;
static GMainLoop *ibus_loop = NULL;
/* Shared between chansrv and the IBus thread. */
static GAsyncQueue *unicode_queue = NULL;
static GMutex state_mutex;
static GCond state_cond;
static gboolean ibus_ready = FALSE;
static gboolean ibus_thread_exited = TRUE;
static gboolean ibus_shutting_down = FALSE;
static int engine_id = 0;
/*****************************************************************************/
static void
xrdp_input_ibus_engine_enable(IBusEngine *engine)
{
LOG(LOG_LEVEL_INFO,
"xrdp_ibus_engine_enable: IM enabled, engine=%p", engine);
if (g_engine != NULL && g_engine != engine)
{
g_object_unref(g_engine);
g_engine = NULL;
}
if (g_engine == NULL)
{
g_engine = g_object_ref(engine);
}
if (ibus_context != NULL)
{
g_main_context_wakeup(ibus_context);
}
}
/*****************************************************************************/
static void
xrdp_input_ibus_engine_disable(IBusEngine *engine)
{
LOG(LOG_LEVEL_INFO,
"xrdp_ibus_engine_disable: IM disabled, engine=%p", engine);
if (g_engine == engine)
{
g_object_unref(g_engine);
g_engine = NULL;
}
}
/*****************************************************************************/
static gboolean
engine_process_key_event_cb(IBusEngine *engine,
guint keyval,
guint keycode,
guint state)
{
/* XrdpIme is a Unicode commit bridge, not a keyboard-event IME. */
return FALSE;
}
/*****************************************************************************/
static IBusEngine *
xrdp_input_ibus_create_engine(IBusFactory *factory,
gchar *engine_name,
gpointer user_data)
{
IBusEngine *engine;
gchar *path;
path = g_strdup_printf("/org/freedesktop/IBus/Engine/%d", ++engine_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 name=%s id=%d",
engine_name, engine_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;
}
/*****************************************************************************/
/* Must run on the IBus thread. */
static gboolean
xrdp_input_enable(void)
{
IBusEngineDesc *desc;
const gchar *name;
if (bus == NULL || !ibus_bus_is_connected(bus))
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_enable: IBus is not connected");
return FALSE;
}
desc = ibus_bus_get_global_engine(bus);
name = desc != NULL ? ibus_engine_desc_get_name(desc) : NULL;
if (name != NULL && g_ascii_strcasecmp(name, "XrdpIme") == 0 &&
g_engine != NULL)
{
g_object_unref(desc);
return TRUE;
}
/* Remember the user's engine only the first time we replace it. */
if (last_input_name == NULL && name != NULL)
{
last_input_name = g_strdup(name);
LOG(LOG_LEVEL_INFO,
"xrdp_input_enable: saving original IBus engine: %s",
last_input_name);
}
if (desc != NULL)
{
g_object_unref(desc);
}
if (!ibus_bus_set_global_engine(bus, "XrdpIme"))
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_enable: failed to switch global engine to XrdpIme");
return FALSE;
}
return TRUE;
}
/*****************************************************************************/
static gboolean
xrdp_input_process_unicode_queue(gpointer data)
{
XrdpUnicodeEvent *event;
/* This callback is always executed by the private IBus main loop. */
while (g_engine != NULL &&
(event = g_async_queue_try_pop(unicode_queue)) != NULL)
{
IBusText *text = ibus_text_new_from_unichar(event->unicode);
if (text != NULL)
{
/* ibus_engine_commit_text() sinks and releases `text` itself
* since it's still floating (IBusText derives from
* GInitiallyUnowned) - do not unref it again here, that
* would release an already-finalized object. */
ibus_engine_commit_text(g_engine, text);
}
g_free(event);
}
return G_SOURCE_CONTINUE;
}
/*****************************************************************************/
static gboolean
xrdp_input_queue_source_prepare(GSource *source, gint *timeout)
{
*timeout = -1;
return g_engine != NULL && g_async_queue_length(unicode_queue) > 0;
}
/*****************************************************************************/
static gboolean
xrdp_input_queue_source_check(GSource *source)
{
return g_engine != NULL && g_async_queue_length(unicode_queue) > 0;
}
/*****************************************************************************/
static gboolean
xrdp_input_queue_source_dispatch(GSource *source,
GSourceFunc callback,
gpointer user_data)
{
return callback != NULL ? callback(user_data) : G_SOURCE_CONTINUE;
}
static GSourceFuncs xrdp_input_queue_source_funcs =
{
xrdp_input_queue_source_prepare,
xrdp_input_queue_source_check,
xrdp_input_queue_source_dispatch,
NULL,
NULL,
NULL
};
/*****************************************************************************/
static void
xrdp_input_install_queue_source(void)
{
GSource *source;
source = g_source_new(&xrdp_input_queue_source_funcs, sizeof(GSource));
g_source_set_name(source, "xrdp-ibus-unicode-queue");
g_source_set_callback(source, xrdp_input_process_unicode_queue, NULL, NULL);
g_source_attach(source, ibus_context);
g_source_unref(source);
}
/*****************************************************************************/
static gboolean
xrdp_input_shutdown_cb(gpointer data)
{
XrdpUnicodeEvent *event;
LOG(LOG_LEVEL_INFO, "xrdp_input: shutting down IBus");
ibus_shutting_down = TRUE;
if (last_input_name != NULL && bus != NULL &&
ibus_bus_is_connected(bus))
{
LOG(LOG_LEVEL_INFO,
"xrdp_input: restoring original IBus engine: %s",
last_input_name);
if (!ibus_bus_set_global_engine(bus, last_input_name))
{
LOG(LOG_LEVEL_WARNING,
"xrdp_input: failed to restore original IBus engine");
}
}
g_clear_pointer(&last_input_name, g_free);
while ((event = g_async_queue_try_pop(unicode_queue)) != NULL)
{
g_free(event);
}
if (ibus_loop != NULL)
{
g_main_loop_quit(ibus_loop);
}
return G_SOURCE_REMOVE;
}
/*****************************************************************************/
/* If the ibus daemon restarts or the socket otherwise goes away
* mid-session, this fires on the IBus thread. This is expected to be a
* rare event in practice (daemon crash, manual "ibus-restart", or a
* package upgrade touching ibus mid-session).
*
* NOTE: this does NOT achieve a working reconnect (see #3230), only a
* clean failure. ibus_bus_new() returns a process-wide singleton in
* this libibus version - confirmed directly: two ibus_bus_new() calls
* in the same process, no disconnect involved, return the identical
* pointer. Once that singleton's connection dies, every later
* ibus_bus_new() call - including from a brand new thread here - just
* hands back the same dead object; ibus_bus_is_connected() on it stays
* FALSE permanently, not a transient state that clears with a retry.
* A real reconnect would mean bypassing IBusBus and talking to
* ibus-daemon over a raw GDBusConnection instead, which is a
* significant undertaking not justified by how rarely this fires.
*
* What quitting the loop here does still buy us: xrdp_input_main_loop()
* falls through to thread_cleanup and tears everything down cleanly,
* thread_exit clears ibus_ready, and the next xrdp_input_unicode_init()
* call sees ibus_ready == FALSE, so it correctly spawns a new thread
* and fails fast (is_connected() on the singleton is false) instead of
* either hanging or silently taking the "already ready" fast path
* against a connection that's actually dead. Unicode input stays
* broken until the session is fully reconnected, but nothing hangs,
* leaks, or corrupts state in the meantime. */
static void
xrdp_input_ibus_bus_disconnected(IBusBus *ibus_bus, gpointer user_data)
{
LOG(LOG_LEVEL_WARNING,
"xrdp_input_ibus_bus_disconnected: IBus connection lost - "
"unicode input will stay unavailable until the session "
"reconnects (see #3230 note above this function)");
if (ibus_loop != NULL)
{
g_main_loop_quit(ibus_loop);
}
}
/*****************************************************************************/
static THREAD_RV THREAD_CC
xrdp_input_main_loop(void *in_val)
{
IBusFactory *factory = NULL;
IBusComponent *component = NULL;
IBusEngineDesc *desc = NULL;
gboolean thread_default_pushed = FALSE;
LOG(LOG_LEVEL_DEBUG, "xrdp_input_main_loop: starting IBus thread");
ibus_init();
/* Create and push our private context as thread-default *before*
* creating bus (or anything else that opens a GDBusConnection).
* GDBusConnection binds its async I/O to whatever is thread-default
* at the moment it's constructed - if bus were created first, its
* connection would silently bind to the global default context
* instead, which nothing here ever iterates. Symptom: things that
* need sync round-trips still appear to work, but signals like
* "disconnected" simply never fire, since nothing is polling that
* connection's fd at all. */
ibus_context = g_main_context_new();
if (ibus_context == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_main_loop: failed to create GMainContext");
goto thread_exit;
}
ibus_loop = g_main_loop_new(ibus_context, FALSE);
if (ibus_loop == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_main_loop: failed to create GMainLoop");
goto thread_cleanup;
}
g_main_context_push_thread_default(ibus_context);
thread_default_pushed = TRUE;
bus = ibus_bus_new();
if (bus == NULL)
{
LOG(LOG_LEVEL_ERROR, "xrdp_input_main_loop: ibus_bus_new failed");
goto thread_cleanup;
}
g_object_ref_sink(bus);
if (!ibus_bus_is_connected(bus))
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_main_loop: IBus connection failed");
goto thread_cleanup;
}
g_signal_connect(bus, "disconnected",
G_CALLBACK(xrdp_input_ibus_bus_disconnected), NULL);
factory = ibus_factory_new(ibus_bus_get_connection(bus));
if (factory == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_main_loop: ibus_factory_new failed");
goto thread_cleanup;
}
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",
"Xrdp input method",
"1.1",
"MIT",
"xrdp",
"default",
"default",
"xrdpime");
desc = ibus_engine_desc_new(
"XrdpIme",
"unicode input method for xrdp",
"unicode input method for xrdp",
"unicode",
"MIT",
"xrdp",
"default",
"default");
if (component == NULL || desc == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_main_loop: failed to create IBus component");
goto thread_cleanup;
}
ibus_component_add_engine(component, desc);
if (!ibus_bus_register_component(bus, component))
{
LOG(LOG_LEVEL_WARNING,
"xrdp_input_main_loop: IBus component registration failed");
}
xrdp_input_install_queue_source();
g_mutex_lock(&state_mutex);
ibus_ready = TRUE;
ibus_thread_exited = FALSE;
g_cond_broadcast(&state_cond);
g_mutex_unlock(&state_mutex);
/* All IBus operations and callbacks now run on this private context. */
g_main_loop_run(ibus_loop);
thread_cleanup:
if (thread_default_pushed)
{
g_main_context_pop_thread_default(ibus_context);
thread_default_pushed = FALSE;
}
if (g_engine != NULL)
{
g_object_unref(g_engine);
g_engine = NULL;
}
g_clear_pointer(&last_input_name, g_free);
if (desc != NULL)
{
g_object_unref(desc);
desc = NULL;
}
if (component != NULL)
{
g_object_unref(component);
component = NULL;
}
if (factory != NULL)
{
g_object_unref(factory);
factory = NULL;
}
if (bus != NULL)
{
g_object_unref(bus);
bus = NULL;
}
if (ibus_loop != NULL)
{
g_main_loop_unref(ibus_loop);
ibus_loop = NULL;
}
if (ibus_context != NULL)
{
g_main_context_unref(ibus_context);
ibus_context = NULL;
}
thread_exit:
g_mutex_lock(&state_mutex);
ibus_ready = FALSE;
ibus_thread_exited = TRUE;
g_cond_broadcast(&state_cond);
g_mutex_unlock(&state_mutex);
LOG(LOG_LEVEL_DEBUG, "xrdp_input_main_loop: IBus thread exited");
return 0;
}
/*****************************************************************************/
/* One-shot trampoline so xrdp_input_enable() (which must run on the IBus
* thread - see its own comment) gets invoked there instead of from
* chansrv's thread. The drain source's prepare/check deliberately still
* gate on g_engine != NULL so the main loop can block normally instead
* of spinning while engine creation is in flight; this is what actually
* gets that engine created/re-asserted in the first place. Harmless to
* run repeatedly - xrdp_input_enable() no-ops once XrdpIme is already
* the enabled global engine. */
static gboolean
xrdp_input_enable_invoke_cb(gpointer data)
{
xrdp_input_enable();
return G_SOURCE_REMOVE;
}
/*****************************************************************************/
int
xrdp_input_send_unicode(char32_t unicode)
{
XrdpUnicodeEvent *event;
GMainContext *context;
LOG(LOG_LEVEL_DEBUG,
"xrdp_input_send_unicode: received U+%04X",
(unsigned int)unicode);
if (unicode == 0)
{
return 0;
}
g_mutex_lock(&state_mutex);
if (!ibus_ready || ibus_shutting_down ||
unicode_queue == NULL || ibus_context == NULL)
{
g_mutex_unlock(&state_mutex);
LOG(LOG_LEVEL_WARNING,
"xrdp_input_send_unicode: IBus input is not ready");
return 1;
}
event = g_new0(XrdpUnicodeEvent, 1);
if (event == NULL)
{
g_mutex_unlock(&state_mutex);
LOG(LOG_LEVEL_ERROR,
"xrdp_input_send_unicode: allocation failed");
return 1;
}
event->unicode = (gunichar)unicode;
/* Take our own ref so the context can't be torn down by the IBus
* thread between here and the calls below, once we drop the mutex. */
context = g_main_context_ref(ibus_context);
g_async_queue_push(unicode_queue, event);
g_main_context_wakeup(context);
g_main_context_invoke(context, xrdp_input_enable_invoke_cb, NULL);
g_mutex_unlock(&state_mutex);
g_main_context_unref(context);
return 0;
}
/*****************************************************************************/
int
xrdp_input_unicode_init(void)
{
const char *addr;
unsigned int cnt = 0;
gboolean ready;
if (unicode_queue == NULL)
{
unicode_queue = g_async_queue_new();
if (unicode_queue == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_unicode_init: failed to create queue");
return 1;
}
}
g_mutex_lock(&state_mutex);
if (ibus_ready)
{
g_mutex_unlock(&state_mutex);
return 0;
}
ibus_shutting_down = FALSE;
ibus_thread_exited = FALSE;
g_mutex_unlock(&state_mutex);
addr = ibus_get_address();
while (addr == NULL && cnt < 10)
{
usleep(500 * 1000);
addr = ibus_get_address();
++cnt;
}
if (addr == NULL)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_unicode_init: timed out waiting for IBus daemon");
return 1;
}
LOG(LOG_LEVEL_INFO, "xrdp_input_unicode_init: starting IBus thread");
if (tc_thread_create(xrdp_input_main_loop, NULL) != 0)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_unicode_init: failed to create IBus thread");
g_mutex_lock(&state_mutex);
ibus_thread_exited = TRUE;
g_mutex_unlock(&state_mutex);
return 1;
}
g_mutex_lock(&state_mutex);
while (!ibus_ready && !ibus_thread_exited)
{
g_cond_wait(&state_cond, &state_mutex);
}
ready = ibus_ready;
g_mutex_unlock(&state_mutex);
if (!ready)
{
LOG(LOG_LEVEL_ERROR,
"xrdp_input_unicode_init: IBus thread failed to initialize");
return 1;
}
return 0;
}
/*****************************************************************************/
int
xrdp_input_unicode_destroy(void)
{
GMainContext *context;
LOG(LOG_LEVEL_DEBUG,
"xrdp_input_unicode_destroy: destroying IBus input");
g_mutex_lock(&state_mutex);
ibus_shutting_down = TRUE;
context = ibus_context;
g_mutex_unlock(&state_mutex);
if (context != NULL)
{
g_main_context_invoke_full(
context,
G_PRIORITY_HIGH,
xrdp_input_shutdown_cb,
NULL,
NULL);
g_main_context_wakeup(context);
}
g_mutex_lock(&state_mutex);
while (!ibus_thread_exited)
{
g_cond_wait(&state_cond, &state_mutex);
}
g_mutex_unlock(&state_mutex);
if (unicode_queue != NULL)
{
XrdpUnicodeEvent *event;
while ((event = g_async_queue_try_pop(unicode_queue)) != NULL)
{
g_free(event);
}
g_async_queue_unref(unicode_queue);
unicode_queue = NULL;
}
return 0;
}