From 2a77f5f3e8acfefb09003e77bcdaf12cd9243292 Mon Sep 17 00:00:00 2001 From: Liyi Meng Date: Mon, 17 Aug 2026 22:10:23 +0000 Subject: [PATCH] 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 --- sesman/chansrv/input_ibus.c | 921 ++++++++++++++++++++++++------------ 1 file changed, 630 insertions(+), 291 deletions(-) diff --git a/sesman/chansrv/input_ibus.c b/sesman/chansrv/input_ibus.c index 9b604945..e7e051cf 100644 --- a/sesman/chansrv/input_ibus.c +++ b/sesman/chansrv/input_ibus.c @@ -16,6 +16,34 @@ * 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 #endif @@ -24,359 +52,670 @@ #include #include #include + #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) +typedef struct { - IBusEngineDesc *desc; - const gchar *name; + gunichar unicode; +} XrdpUnicodeEvent; - if (!bus) - { - LOG(LOG_LEVEL_ERROR, "xrdp_ibus_init: input method switched failed, ibus not connected"); - return 1; - } +/* 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; - /* 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; -} +/* 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"); - g_engine = engine; -} + LOG(LOG_LEVEL_INFO, + "xrdp_ibus_engine_enable: IM enabled, engine=%p", 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) + if (g_engine != NULL && g_engine != 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(); + 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) { - /* Pass the keyboard event to system */ + /* 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) + 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, + 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 with name:%s and id:%d\n", engine_name, id); + 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); + 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; - IBusComponent *component; - IBusEngineDesc *desc; - THREAD_RV rv = 0; + IBusFactory *factory = NULL; + IBusComponent *component = NULL; + IBusEngineDesc *desc = NULL; + gboolean thread_default_pushed = FALSE; - LOG(LOG_LEVEL_DEBUG, "xrdp_input_main_loop: Entering ibus loop"); + LOG(LOG_LEVEL_DEBUG, "xrdp_input_main_loop: starting IBus thread"); - g_signal_connect(bus, "disconnected", G_CALLBACK(xrdp_input_ibus_disconnect), NULL); + ibus_init(); - 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) + /* 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_ibus_init: Timed out waiting for iBus daemon"); - return 1; + "xrdp_input_main_loop: failed to create GMainContext"); + goto thread_exit; } - LOG(LOG_LEVEL_INFO, "xrdp_ibus_init: Initializing the iBus engine"); - ibus_init(); + 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_ibus_init: Connect to iBus failed"); + 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; } - LOG(LOG_LEVEL_INFO, "xrdp_ibus_init: iBus connected"); + 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; + } - tc_thread_create(xrdp_input_main_loop, NULL); + event->unicode = (gunichar)unicode; - /* 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. */ + /* 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; }