a3934cde9a
Three correctness gaps found in code review of the private-loop rewrite, all in how chansrv's thread and the IBus thread interact around `bus`: - xrdp_input_enable() read the shared `bus` pointer with no synchronization at all, despite the IBus thread being free to null it out (disconnect, teardown) at any time - a real use-after-free risk, not just a formality, since this function actively calls methods on it rather than just checking it's non-NULL. Fixed by taking a ref under state_mutex before touching it, and using that local reference for the rest of the call; bus's creation and teardown in xrdp_input_main_loop() now publish/clear the shared pointer under the same lock so the ref-under-lock pattern actually synchronizes against something. - xrdp_input_unicode_init()'s fast path trusted ibus_ready without re-checking the connection. ibus_ready and connection liveness normally change together, but there's a window between the underlying socket actually dying and the "disconnected" signal being dispatched on the IBus thread. A client that reconnects into that window would get a false "unicode input is supported" advertisement it could never actually use. Now double-checks ibus_bus_is_connected() before taking the fast path. - That staleness check can't just flip ibus_ready and fall through to spawning a new thread - the old one may still be alive and about to touch bus/g_engine itself, racing the new thread's own setup. Now asks the stale thread to shut down (g_main_loop_quit, safe to call cross-thread) and waits on the exit condvar before proceeding, so there's never more than one IBus thread alive at a time. Verified: 20 back-to-back xrdp_input_unicode_init() calls on a healthy connection all take the fast path correctly (no spurious thread restarts); 5 kill/restart cycles still show no thread leak; an 18-char rapid-fire send against a real focused GTK entry still lands every character in order with zero drops after these changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
799 lines
24 KiB
C
799 lines
24 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 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;
|
|
}
|