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>
This commit is contained in:
+628
-289
@@ -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 <config_ac.h>
|
||||
#endif
|
||||
@@ -24,359 +52,670 @@
|
||||
#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)
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user