Skip to content
main.c 14.2 KiB
Newer Older
#include <locale.h>
Yuanle Song's avatar
Yuanle Song committed
#include <gio/gio.h>
#include <glib-unix.h>
#include "zero-pinyin-service.h"
#include "zero-pinyin-service-generated.h"
#include "../sqlite3_util.h"
#include <sqlite3.h>
#include <uuid.h>

static const int MAX_PHRASE_LEN = 16;
static const char *SQLITE3_MEMORY_DB = ":memory:";
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
	GApplication *app;
	guint owner_id;
	ZeroPinyinService *interface;
Yuanle Song's avatar
Yuanle Song committed

static gboolean
Yuanle Song's avatar
Yuanle Song committed
on_handle_get_candidates(ZeroPinyinService *object,
			 GDBusMethodInvocation *invocation,
			 const gchar *preedit_str,
			 guint fetch_size,
			 AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
{
	if (preedit_str == NULL || fetch_size == 0) {
Yuanle Song's avatar
Yuanle Song committed
		g_dbus_method_invocation_return_dbus_error(
			invocation,
			"org.gtk.GDBus.Failed",
			"Bad param");
Yuanle Song's avatar
Yuanle Song committed
	g_info("get_candidates for preedit_str=%s fetch_size=%u",
	       preedit_str, fetch_size);
Yuanle Song's avatar
Yuanle Song committed

	GVariant *result = NULL;
	GVariantBuilder *candidates_builder = NULL;
	GVariantBuilder *matched_lengths_builder = NULL;
	GVariantBuilder *candidates_pinyin_indices = NULL;
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
	/* test data */
	/* get_candidates_test(preedit_str, fetch_size, candidates_builder, matched_lengths_builder); */
Yuanle Song's avatar
Yuanle Song committed
	candidates_builder = g_variant_builder_new(G_VARIANT_TYPE("as"));
	matched_lengths_builder = g_variant_builder_new(G_VARIANT_TYPE("au"));
	candidates_pinyin_indices = g_variant_builder_new(G_VARIANT_TYPE("aa(ii)"));
	get_candidates(appdata->db, preedit_str, fetch_size, zero_pinyin_service_get_fuzzy_flag(object), candidates_builder, matched_lengths_builder, candidates_pinyin_indices);
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
	result = g_variant_new("(asauaa(ii))", candidates_builder, matched_lengths_builder, candidates_pinyin_indices);
	g_assert_nonnull(result);
Yuanle Song's avatar
Yuanle Song committed

	/* result is a GVarient tuple of two dbus arrays */
Yuanle Song's avatar
Yuanle Song committed
	g_dbus_method_invocation_return_value(invocation, result);
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
	g_variant_builder_unref(candidates_builder);
	g_variant_builder_unref(matched_lengths_builder);
	g_variant_builder_unref(candidates_pinyin_indices);
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
}

static gboolean
Yuanle Song's avatar
Yuanle Song committed
on_handle_commit_candidate(ZeroPinyinService *object,
			   GDBusMethodInvocation *invocation,
			   const gchar *candidate,
			   GVariant *candidate_pinyin_indices,
			   AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
	commit_candidate(appdata->db, candidate, candidate_pinyin_indices);
	g_dbus_method_invocation_return_value(invocation, NULL);
	return TRUE;
}

static gboolean
Yuanle Song's avatar
Yuanle Song committed
on_handle_delete_candidate(ZeroPinyinService *object,
			   GDBusMethodInvocation *invocation,
			   const char *candidate,
			   AppData *appdata)
{
	if (! candidate) {
Yuanle Song's avatar
Yuanle Song committed
		g_dbus_method_invocation_return_value(invocation, NULL);
		return TRUE;
	}
Yuanle Song's avatar
Yuanle Song committed
	guint len = g_utf8_strlen(candidate, -1);
	if (len == 1) {
Yuanle Song's avatar
Yuanle Song committed
		g_debug("delete single character %s is a no-op", candidate);
Yuanle Song's avatar
Yuanle Song committed
		g_dbus_method_invocation_return_value(invocation, NULL);
		return TRUE;
	}
Yuanle Song's avatar
Yuanle Song committed
	g_message("delete candidate %s", candidate);

	/* insert phrase to userdb.not_phrase table. */
	char *sql = NULL;
	gboolean rb = FALSE;
Yuanle Song's avatar
Yuanle Song committed
	sql = sqlite3_mprintf("INSERT INTO userdb.not_phrase (phrase) VALUES (%Q);", candidate);
	rb = sqlite3_exec_simple(appdata->db, sql);
	if (! rb) {
Yuanle Song's avatar
Yuanle Song committed
		g_warning("insert phrase to not_phrase table failed");
Yuanle Song's avatar
Yuanle Song committed
	sqlite3_free(sql);

	/* delete phrase from userdb.py_phrase_x table. */
	guint table_suffix = len - 1;
Yuanle Song's avatar
Yuanle Song committed
	sql = sqlite3_mprintf("DELETE FROM userdb.py_phrase_%u WHERE phrase = %Q;", table_suffix, candidate);
	rb = sqlite3_exec_simple(appdata->db, sql);
	if (! rb) {
Yuanle Song's avatar
Yuanle Song committed
		g_warning("delete phrase from py_phrase_%u table failed", table_suffix);
Yuanle Song's avatar
Yuanle Song committed
	sqlite3_free(sql);
Yuanle Song's avatar
Yuanle Song committed
	g_dbus_method_invocation_return_value(invocation, NULL);
Yuanle Song's avatar
Yuanle Song committed
static gboolean
Yuanle Song's avatar
Yuanle Song committed
on_handle_quit(ZeroPinyinService *object,
	       GDBusMethodInvocation *invocation,
	       AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	g_application_quit(appdata->app);
	g_dbus_method_invocation_return_value(invocation, NULL);
Yuanle Song's avatar
Yuanle Song committed
	return TRUE;
}

static void
Yuanle Song's avatar
Yuanle Song committed
on_bus_acquired(GDBusConnection *connection,
		const gchar *name,
		gpointer user_data)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	AppData *appdata = (AppData *) user_data;
Yuanle Song's avatar
Yuanle Song committed
	GError *err = NULL;

Yuanle Song's avatar
Yuanle Song committed
	g_message("on_bus_acquired() name=%s", name);

	appdata->interface = zero_pinyin_service_skeleton_new();
	g_signal_connect(appdata->interface,
			 "handle-get-candidates",
			 G_CALLBACK(on_handle_get_candidates),
			 appdata);
	g_signal_connect(appdata->interface,
			 "handle-commit-candidate",
			 G_CALLBACK(on_handle_commit_candidate),
			 appdata);
	g_signal_connect(appdata->interface,
			 "handle-delete-candidate",
			 G_CALLBACK(on_handle_delete_candidate),
			 appdata);
	g_signal_connect(appdata->interface,
			 "handle-quit",
			 G_CALLBACK(on_handle_quit),
			 appdata);
	g_dbus_interface_skeleton_export(
		G_DBUS_INTERFACE_SKELETON(appdata->interface),
Yuanle Song's avatar
Yuanle Song committed
		connection,
		ZERO_PINYIN_OBJECT_PATH,
		&err);
	if (err) {
Yuanle Song's avatar
Yuanle Song committed
		g_warning("export interface at %s failed: %s",
			  ZERO_PINYIN_OBJECT_PATH, err->message);
		g_error_free(err);
		g_application_quit(G_APPLICATION(appdata->app));
Yuanle Song's avatar
Yuanle Song committed
		return;
	}
Yuanle Song's avatar
Yuanle Song committed
	g_message("interface exported at %s", ZERO_PINYIN_OBJECT_PATH);
Yuanle Song's avatar
Yuanle Song committed
}

static void
Yuanle Song's avatar
Yuanle Song committed
on_name_acquired(GDBusConnection *connection,
		 const gchar     *name,
		 gpointer         user_data)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	g_message("on_name_acquired() name=%s", name);
Yuanle Song's avatar
Yuanle Song committed
}

static void
Yuanle Song's avatar
Yuanle Song committed
on_name_lost(GDBusConnection *connection,
	     const gchar     *name,
	     gpointer         user_data)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	AppData *appdata = (AppData *) user_data;
	/* this won't happen if this is the only app that tries to take the
	 * name, because GApplication already have primary instance
	 * concept. None primary instance will just send 'activate' signal to
	 * primary instance and exit. They will not try to register ibus at
	 * all. */
Yuanle Song's avatar
Yuanle Song committed
	g_message("on_name_lost() name=%s exiting", name);
	g_application_quit(G_APPLICATION(appdata->app));
Yuanle Song's avatar
Yuanle Song committed
}

static void
Yuanle Song's avatar
Yuanle Song committed
config_dbus_service(AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	appdata->owner_id = g_bus_own_name(
				    G_BUS_TYPE_SESSION,
				    ZERO_PINYIN_WELL_KNOWN_NAME,
				    G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE,
				    on_bus_acquired,
				    on_name_acquired,
				    on_name_lost,
				    appdata,
				    NULL);
	g_assert_cmpint(appdata->owner_id, >, 0);
Yuanle Song's avatar
Yuanle Song committed
}

/**
 * handle SIGTERM gracefully.
 */
static gboolean
Yuanle Song's avatar
Yuanle Song committed
on_sigterm_received(gpointer user_data)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	AppData *appdata = (AppData *) user_data;
	g_application_quit(appdata->app);
Yuanle Song's avatar
Yuanle Song committed
	return G_SOURCE_REMOVE;
}

/**
 * return HOME dir, get value from HOME env variable.
 */
static const gchar *
get_home_dir(AppData *appdata)
{
	const gchar *result;
	result = g_environ_getenv(appdata->env, "HOME");
	return result;
}

/**
 * return TRUE if file exists
 */
static gboolean
file_exists(const char *filename)
{
	return g_file_test(filename, G_FILE_TEST_EXISTS);
}

/**
 * Return the file path of the main db file. main db is the main word/phrase
 * dababase in libpyzy db format. Without main db, char/phrase query will not
 * work at all.
 *
 * if not main db found, return NULL.
 *
 * returned gchar* should be freed with g_free().
 */
static gchar *
get_maindb_file(const gchar *home_dir)
{
	/* TODO make db path configurable */
	const gchar *user_main_db = ".cache/ibus/pinyin/main.db";
	const gchar *open_phrase_db = "/usr/share/pyzy/db/open-phrase.db";
	const gchar *android_db = "/usr/share/pyzy/db/android.db";
	gchar *home_dir_maindb = NULL;

	home_dir_maindb = g_strconcat(home_dir, "/", user_main_db, NULL);
	if (file_exists(home_dir_maindb)) {
		return home_dir_maindb;
	}
	if (file_exists(open_phrase_db)) {
		return g_strdup(open_phrase_db);
	}
	if (file_exists(android_db)) {
		return g_strdup(android_db);
	}
	return NULL;
}

/**
 * return userdb file path.
 * "~/.cache/ibus/pinyin/user-1.0.db"
 *
 * returned gchar* should be freed with g_free().
 */
static gchar *
get_userdb_file(const gchar *home_dir)
{
	/* TODO make db path configurable */
	return g_strconcat(home_dir, "/.cache/ibus/pinyin/user-1.0.db", NULL);
}

/**
 * return a usable userdb file, it's either the path returned by
 * `get_userdb_file()' or ':memory:' if that file doesn't exist and can't be
 * created.
 *
 * returned gchar* should be freed with g_free().
 */
static gchar *
get_userdb_file_create(const gchar *home_dir)
{
	gchar *userdb_file = NULL;
	userdb_file = get_userdb_file(home_dir);
	if (file_exists(userdb_file)) {
		return userdb_file;
	}

	/* try create the file */
	gchar *parent_dir = NULL;
	parent_dir = g_path_get_dirname(userdb_file);
	gint r = g_mkdir_with_parents(parent_dir, 0750);
	if (r != 0) {
		g_warning("create dir %s failed: %d (%s)",
			  parent_dir, r, g_strerror(r));
		return g_strdup(SQLITE3_MEMORY_DB);
	}

	sqlite3 *userdb = NULL;
	g_message("creating user db at %s", userdb_file);
	r = sqlite3_open(userdb_file, &userdb);
	if (r != SQLITE_OK) {
		sqlite3_close(userdb);
		return g_strdup(SQLITE3_MEMORY_DB);
	}
	sqlite3_close(userdb);
	return userdb_file;
}

/**
 * initialize user db.
 * create tables, index and populate data into desc table.
 *
 * Returns: true on success, false otherwise.
 */
static gboolean
init_userdb(sqlite3 *userdb, const char *schema, AppData *appdata)
{
	gboolean rb;
	GString *sql;
	gchar uuid_str[37];
	uuid_t uuid;
	gchar *snippet;

	/* original libpyzy user db schema */
	uuid_generate_random(uuid);
	uuid_unparse_lower(uuid, uuid_str);
	/* uuid = g_uuid_string_random(); */
	sql = g_string_sized_new(200);

	snippet = sqlite3_mprintf(
			  "BEGIN TRANSACTION;\n"
			  "CREATE TABLE IF NOT EXISTS %s.desc (name PRIMARY KEY, value TEXT);\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('version', '1.2.0');\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('uuid', %Q);\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('hostname', %Q);\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('username', %Q);\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('create-time', datetime());\n"
			  "INSERT OR IGNORE INTO %s.desc VALUES ('attach-time', datetime());\n", schema, schema, schema, uuid_str, schema, g_environ_getenv(appdata->env, "HOSTNAME"), schema, g_environ_getenv(appdata->env, "USER"), schema, schema);
	sql = g_string_append(sql, snippet);
	sqlite3_free(snippet);

	/* create phrase tables */
	for (gint i = 0; i < MAX_PHRASE_LEN; i++) {
		g_string_append_printf(sql, "CREATE TABLE IF NOT EXISTS %s.py_phrase_%d (user_freq, phrase TEXT, freq INTEGER", schema, i);
		for (gint j = 0; j <= i; j++)
			g_string_append_printf(sql, ", s%d INTEGER, y%d INTEGER", j, j);
		sql = g_string_append(sql, ");\n");
	}

	/* create index */
	g_string_append_printf(
		sql,
		"CREATE UNIQUE INDEX IF NOT EXISTS %s.index_0_0 ON py_phrase_0(s0,y0,phrase);\n"
		"CREATE UNIQUE INDEX IF NOT EXISTS %s.index_1_0 ON py_phrase_1(s0,y0,s1,y1,phrase);\n"
		"CREATE INDEX IF NOT EXISTS %s.index_1_1 ON py_phrase_1(s0,s1,y1);\n", schema, schema, schema);
	for (gint i = 2; i < MAX_PHRASE_LEN; i++) {
		g_string_append_printf(sql, "CREATE UNIQUE INDEX IF NOT EXISTS %s.index_%d_0 ON py_phrase_%d(s0,y0", schema, i, i);
		for (gint j = 1; j <= i; j++)
			g_string_append_printf(sql, ",s%d,y%d", j, j);
		sql = g_string_append(sql, ",phrase);\n");
		g_string_append_printf(sql, "CREATE INDEX IF NOT EXISTS %s.index_%d_1 ON py_phrase_%d(s0,s1,s2,y2);\n", schema, i, i);
	}

	/* zero-pinyin-service addition */
	g_string_append_printf(sql, "CREATE TABLE IF NOT EXISTS %s.not_phrase (phrase TEXT UNIQUE);\n", schema);

	sql = g_string_append(sql, "COMMIT;");

	rb = sqlite3_exec_simple(userdb, sql->str);
	g_string_free(sql, TRUE);
	if (! rb) {
		g_warning("init userdb failed, query will not work.");
		return FALSE;
	}
	return TRUE;
}

/**
 * init appdata->db
 */
Yuanle Song's avatar
Yuanle Song committed
static void
Yuanle Song's avatar
Yuanle Song committed
config_db(AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
{
Yuanle Song's avatar
Yuanle Song committed
	sqlite3 *db = NULL;
	gchar *sql = NULL;
	const gchar *home_dir;
	gchar *maindb_file = NULL;
	gchar *userdb_file = NULL;

	home_dir = get_home_dir(appdata);

Yuanle Song's avatar
Yuanle Song committed
	ri = sqlite3_open(SQLITE3_MEMORY_DB, &db);
Yuanle Song's avatar
Yuanle Song committed
		g_warning("sqlite3_open :memory: db failed, query will not work.");
Yuanle Song's avatar
Yuanle Song committed
	g_assert_nonnull(db);
	maindb_file = get_maindb_file(home_dir);
	g_info("using maindb file: %s", maindb_file);
	sql = sqlite3_mprintf("ATTACH %Q AS maindb", maindb_file);
	g_free(maindb_file);
Yuanle Song's avatar
Yuanle Song committed
	rb = sqlite3_exec_simple(db, sql);
Yuanle Song's avatar
Yuanle Song committed
		g_warning("attach maindb failed, query will not work.");
Yuanle Song's avatar
Yuanle Song committed
	sqlite3_free(sql);
	userdb_file = get_userdb_file_create(home_dir);
	sql = sqlite3_mprintf("ATTACH %Q AS userdb", userdb_file);
	g_free(userdb_file);
Yuanle Song's avatar
Yuanle Song committed
	rb = sqlite3_exec_simple(db, sql);
Yuanle Song's avatar
Yuanle Song committed
		g_warning("attach userdb failed, query will not work.");
Yuanle Song's avatar
Yuanle Song committed
	sqlite3_free(sql);
	init_userdb(db, "userdb", appdata);
Yuanle Song's avatar
Yuanle Song committed
	sqlite3_free(sql);
	sqlite3_close(db);
db_fail:
	appdata->db = NULL;
Yuanle Song's avatar
Yuanle Song committed
}

/**
 * allow graceful shutdown by Ctrl-C and SIGTERM.
 */
static void
Yuanle Song's avatar
Yuanle Song committed
setup_sigint_sigterm_handler(AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
{
	GSource *source = NULL;
Yuanle Song's avatar
Yuanle Song committed
	source = g_unix_signal_source_new(SIGTERM);
	g_source_set_callback(source, on_sigterm_received, appdata, NULL);
	g_source_attach(source, NULL);
	g_source_unref(source);

	source = g_unix_signal_source_new(SIGINT);
	g_source_set_callback(source, on_sigterm_received, appdata, NULL);
	g_source_attach(source, NULL);
	g_source_unref(source);
Yuanle Song's avatar
Yuanle Song committed
}

Yuanle Song's avatar
Yuanle Song committed
on_startup(GApplication *app,
	   AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
	g_message("zero-pinyin-service startup()");
	appdata->env = g_get_environ();
Yuanle Song's avatar
Yuanle Song committed
	config_db(appdata);
	config_dbus_service(appdata);
	setup_sigint_sigterm_handler(appdata);
	g_application_hold(app);
Yuanle Song's avatar
Yuanle Song committed
on_activate(GApplication *app,
	    AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
	g_message("zero-pinyin-service activate()");
Yuanle Song's avatar
Yuanle Song committed
on_shutdown(GApplication *app,
	    AppData *appdata)
Yuanle Song's avatar
Yuanle Song committed
	g_message("zero-pinyin-service shutdown()");
	if (appdata->owner_id > 0) {
Yuanle Song's avatar
Yuanle Song committed
		g_bus_unown_name(appdata->owner_id);
		appdata->owner_id = 0;
	}
	if (appdata->db != NULL) {
Yuanle Song's avatar
Yuanle Song committed
		sqlite3_close(appdata->db);
	if (appdata->env != NULL) {
		g_strfreev(appdata->env);
	}
Yuanle Song's avatar
Yuanle Song committed
/**
 * provides zero-pinyin-service dbus service.
 * it's a console app (GApplication) based on glib and gio.
Yuanle Song's avatar
Yuanle Song committed
 */
int
Yuanle Song's avatar
Yuanle Song committed
main(int argc, char *argv[])
Yuanle Song's avatar
Yuanle Song committed
{
	static AppData appdata = {0};
Yuanle Song's avatar
Yuanle Song committed
	GApplication *app = NULL;
	int status = 0;

Yuanle Song's avatar
Yuanle Song committed
	setlocale(LC_ALL, "");
Yuanle Song's avatar
Yuanle Song committed
	app = g_application_new("com.emacsos.zero.ZeroPinyinServiceApp",
				G_APPLICATION_FLAGS_NONE);
	g_assert_nonnull(app);
Yuanle Song's avatar
Yuanle Song committed
	appdata.app = app;

Yuanle Song's avatar
Yuanle Song committed
	g_signal_connect(app, "startup", G_CALLBACK(on_startup), &appdata);
	g_signal_connect(app, "activate", G_CALLBACK(on_activate), &appdata);
	g_signal_connect(app, "shutdown", G_CALLBACK(on_shutdown), &appdata);
Yuanle Song's avatar
Yuanle Song committed

Yuanle Song's avatar
Yuanle Song committed
	status = g_application_run(G_APPLICATION(app), argc, argv);
	g_object_unref(app);
Yuanle Song's avatar
Yuanle Song committed
	return status;
}