#include #include #include #include "zero-pinyin-service.h" #include "zero-pinyin-service-generated.h" #include "../sqlite3_util.h" #include #include static const int MAX_PHRASE_LEN = 16; static const char *SQLITE3_MEMORY_DB = ":memory:"; typedef struct { GApplication *app; guint owner_id; ZeroPinyinService *interface; sqlite3 *db; gchar **env; } AppData; static gboolean on_handle_get_candidates_v2(ZeroPinyinService *object, GDBusMethodInvocation *invocation, const gchar *preedit_str, guint fetch_size, guint fuzzy_flag, AppData *appdata) { if (preedit_str == NULL || fetch_size == 0) { g_dbus_method_invocation_return_dbus_error( invocation, "org.gtk.GDBus.Failed", "Bad param"); return TRUE; } g_info("get_candidates for preedit_str=%s fetch_size=%u", preedit_str, fetch_size); GVariant *result = NULL; GVariantBuilder *candidates_builder = NULL; GVariantBuilder *matched_lengths_builder = NULL; GVariantBuilder *candidates_pinyin_indices = NULL; /* test data */ /* get_candidates_test(preedit_str, fetch_size, candidates_builder, matched_lengths_builder); */ 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, fuzzy_flag, candidates_builder, matched_lengths_builder, candidates_pinyin_indices); result = g_variant_new("(asauaa(ii))", candidates_builder, matched_lengths_builder, candidates_pinyin_indices); g_assert_nonnull(result); /* result is a GVarient tuple of two dbus arrays */ g_dbus_method_invocation_return_value(invocation, result); g_variant_builder_unref(candidates_builder); g_variant_builder_unref(matched_lengths_builder); g_variant_builder_unref(candidates_pinyin_indices); return TRUE; } static gboolean on_handle_get_candidates(ZeroPinyinService *object, GDBusMethodInvocation *invocation, const gchar *preedit_str, guint fetch_size, AppData *appdata) { return on_handle_get_candidates_v2(object, invocation, preedit_str, fetch_size, 0, appdata); } static gboolean on_handle_commit_candidate(ZeroPinyinService *object, GDBusMethodInvocation *invocation, const gchar *candidate, GVariant *candidate_pinyin_indices, AppData *appdata) { commit_candidate(appdata->db, candidate, candidate_pinyin_indices); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static gboolean on_handle_delete_candidate(ZeroPinyinService *object, GDBusMethodInvocation *invocation, const char *candidate, AppData *appdata) { if (! candidate) { g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } guint len = g_utf8_strlen(candidate, -1); if (len == 1) { g_debug("delete single character %s is a no-op", candidate); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } g_message("delete candidate %s", candidate); /* insert phrase to userdb.not_phrase table. */ char *sql = NULL; gboolean rb = FALSE; sql = sqlite3_mprintf("INSERT INTO userdb.not_phrase (phrase) VALUES (%Q);", candidate); rb = sqlite3_exec_simple(appdata->db, sql); if (! rb) { g_warning("insert phrase to not_phrase table failed"); } sqlite3_free(sql); /* delete phrase from userdb.py_phrase_x table. */ guint table_suffix = len - 1; sql = sqlite3_mprintf("DELETE FROM userdb.py_phrase_%u WHERE phrase = %Q;", table_suffix, candidate); rb = sqlite3_exec_simple(appdata->db, sql); if (! rb) { g_warning("delete phrase from py_phrase_%u table failed", table_suffix); } sqlite3_free(sql); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static gboolean on_handle_quit(ZeroPinyinService *object, GDBusMethodInvocation *invocation, AppData *appdata) { g_application_quit(appdata->app); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static void on_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { AppData *appdata = (AppData *) user_data; GError *err = NULL; g_message("on_bus_acquired() name=%s", name); appdata->interface = zero_pinyin_service_skeleton_new(); g_signal_connect(appdata->interface, "handle-get-candidates-v2", G_CALLBACK(on_handle_get_candidates_v2), appdata); 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), connection, ZERO_PINYIN_OBJECT_PATH, &err); if (err) { 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)); return; } g_message("interface exported at %s", ZERO_PINYIN_OBJECT_PATH); } static void on_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { g_message("on_name_acquired() name=%s", name); } static void on_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data) { 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. */ g_message("on_name_lost() name=%s exiting", name); g_application_quit(G_APPLICATION(appdata->app)); } static void config_dbus_service(AppData *appdata) { 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); } /** * handle SIGTERM gracefully. */ static gboolean on_sigterm_received(gpointer user_data) { AppData *appdata = (AppData *) user_data; g_application_quit(appdata->app); 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 */ static void config_db(AppData *appdata) { gint ri = 0; gboolean rb = FALSE; sqlite3 *db = NULL; gchar *sql = NULL; const gchar *home_dir; gchar *maindb_file = NULL; gchar *userdb_file = NULL; home_dir = get_home_dir(appdata); ri = sqlite3_open(SQLITE3_MEMORY_DB, &db); if (ri != SQLITE_OK) { g_warning("sqlite3_open :memory: db failed, query will not work."); goto db_fail; } 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); rb = sqlite3_exec_simple(db, sql); if (! rb) { g_warning("attach maindb failed, query will not work."); goto attach_fail; } sqlite3_free(sql); userdb_file = get_userdb_file_create(home_dir); sql = sqlite3_mprintf("ATTACH %Q AS userdb", userdb_file); g_free(userdb_file); rb = sqlite3_exec_simple(db, sql); if (! rb) { g_warning("attach userdb failed, query will not work."); goto attach_fail; } sqlite3_free(sql); init_userdb(db, "userdb", appdata); appdata->db = db; return; attach_fail: sqlite3_free(sql); sqlite3_close(db); db_fail: appdata->db = NULL; } /** * allow graceful shutdown by Ctrl-C and SIGTERM. */ static void setup_sigint_sigterm_handler(AppData *appdata) { GSource *source = NULL; 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); } static void on_startup(GApplication *app, AppData *appdata) { g_message("zero-pinyin-service startup()"); appdata->env = g_get_environ(); config_db(appdata); config_dbus_service(appdata); setup_sigint_sigterm_handler(appdata); g_application_hold(app); } static void on_activate(GApplication *app, AppData *appdata) { g_message("zero-pinyin-service activate()"); } static void on_shutdown(GApplication *app, AppData *appdata) { g_message("zero-pinyin-service shutdown()"); if (appdata->owner_id > 0) { g_bus_unown_name(appdata->owner_id); appdata->owner_id = 0; } if (appdata->db != NULL) { sqlite3_close(appdata->db); appdata->db = NULL; } if (appdata->env != NULL) { g_strfreev(appdata->env); } } /** * provides zero-pinyin-service dbus service. * it's a console app (GApplication) based on glib and gio. */ int main(int argc, char *argv[]) { static AppData appdata = {0}; GApplication *app = NULL; int status = 0; setlocale(LC_ALL, ""); app = g_application_new("com.emacsos.zero.ZeroPinyinServiceApp", G_APPLICATION_FLAGS_NONE); g_assert_nonnull(app); appdata.app = app; 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); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }