/** * 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 #endif #include #include #include #include #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 chansrv's own thread, NOT the IBus thread - confirmed by * hitting the deadlock this avoids. ibus_bus_set_global_engine() blocks * synchronously waiting for ibus-daemon's reply, but ibus-daemon can't * reply until it finishes instantiating the engine, which means calling * back into *our own* IBusFactory's "create-engine" over the same * connection. If this function runs on the IBus thread, that thread is * stuck blocked inside this call and can't service that nested * callback - ibus-daemon times out waiting and set_global_engine fails * with "Set global engine failed: Timeout was reached". Calling this * from chansrv's thread instead leaves the IBus thread free to answer * the nested call while this blocks. g_dbus_connection sync calls are * documented thread-safe to issue from any thread, so that part is * fine despite `bus` otherwise being owned by the IBus thread - but * the `bus` pointer itself is not: the IBus thread can null it out * (disconnect, teardown) at any time. Take our own reference under * state_mutex before touching it, so a concurrent teardown drops the * shared pointer without pulling the object out from under us. */ static gboolean xrdp_input_enable(void) { IBusEngineDesc *desc; const gchar *name; IBusBus *local_bus; gboolean result; g_mutex_lock(&state_mutex); local_bus = (bus != NULL) ? g_object_ref(bus) : NULL; g_mutex_unlock(&state_mutex); if (local_bus == NULL || !ibus_bus_is_connected(local_bus)) { LOG(LOG_LEVEL_ERROR, "xrdp_input_enable: IBus is not connected"); g_clear_object(&local_bus); return FALSE; } desc = ibus_bus_get_global_engine(local_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); g_object_unref(local_bus); 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); } result = ibus_bus_set_global_engine(local_bus, "XrdpIme"); if (!result) { LOG(LOG_LEVEL_ERROR, "xrdp_input_enable: failed to switch global engine to XrdpIme"); } g_object_unref(local_bus); return result; } /*****************************************************************************/ 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; { IBusBus *new_bus = ibus_bus_new(); if (new_bus == NULL) { LOG(LOG_LEVEL_ERROR, "xrdp_input_main_loop: ibus_bus_new failed"); goto thread_cleanup; } g_object_ref_sink(new_bus); /* Published under lock so xrdp_input_enable(), running on * chansrv's thread, never observes a partially-constructed * `bus`. */ g_mutex_lock(&state_mutex); bus = new_bus; g_mutex_unlock(&state_mutex); } 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; } { /* Clear the shared pointer under lock before dropping the * reference, so a concurrent xrdp_input_enable() ref (taken * under the same lock) always sees either the live object or * NULL, never a dangling one. */ IBusBus *old_bus; g_mutex_lock(&state_mutex); old_bus = bus; bus = NULL; g_mutex_unlock(&state_mutex); if (old_bus != NULL) { g_object_unref(old_bus); } } 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; } /*****************************************************************************/ 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; } /* Called directly from here (chansrv's own thread), not marshaled * onto the IBus thread - see the comment on xrdp_input_enable() * for why running it there deadlocks against ibus-daemon's nested * "create-engine" callback. */ if (!xrdp_input_enable()) { return 1; } 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 wakeup call below, once we drop the * mutex. The drain source's prepare/check gate on g_engine != NULL, * so if engine creation (triggered by xrdp_input_enable() above) is * still in flight, this wakeup is a no-op and the "enable" signal * handler's own wakeup picks the queued character up once the * engine is actually ready - it isn't lost. */ context = g_main_context_ref(ibus_context); g_async_queue_push(unicode_queue, event); g_main_context_wakeup(context); 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) { /* ibus_ready normally tracks connection liveness exactly, * since the "disconnected" handler and thread_exit update it * together - but there's a window between the socket actually * dying and that signal being dispatched on the IBus thread. * Double-check here rather than trust a possibly-stale flag, * so a client that reconnects right into that window gets a * clean failure (and a real retry next time) instead of a * false "unicode input is supported" advertisement it can * never actually use. */ IBusBus *check_bus = (bus != NULL) ? g_object_ref(bus) : NULL; gboolean connected = (check_bus != NULL) && ibus_bus_is_connected(check_bus); g_clear_object(&check_bus); if (connected) { g_mutex_unlock(&state_mutex); return 0; } /* Stale: the IBus thread hasn't noticed its connection is dead * yet. Ask it to shut down and wait for it to fully exit * before starting a new one below - starting a second thread * while the first is still alive would race both of them over * bus/g_engine. */ LOG(LOG_LEVEL_WARNING, "xrdp_input_unicode_init: stale IBus connection, " "restarting IBus thread"); if (ibus_loop != NULL) { g_main_loop_quit(ibus_loop); } while (!ibus_thread_exited) { g_cond_wait(&state_cond, &state_mutex); } } 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; }