diff --git a/wdmtg.c b/wdmtg.c index d794c8b..9483ed2 100644 --- a/wdmtg.c +++ b/wdmtg.c @@ -86,10 +86,12 @@ event_free(struct event *self) struct { GAsyncQueue *queue; // Async queue of `struct event` + sqlite3 *db; // Event database } g; struct { Display *dpy; // X display handle + GThread *thread; // Worker thread Atom net_active_window; // _NET_ACTIVE_WINDOW Atom net_wm_name; // _NET_WM_NAME @@ -170,7 +172,16 @@ on_x_error(Display *dpy, XErrorEvent *ee) return g_default_x_error_handler(dpy, ee); } -// --- Application ------------------------------------------------------------- +// --- Generator --------------------------------------------------------------- + +static void +push_event(void) { + struct event *event = g_slice_new0(struct event); + event->timestamp = g_get_real_time(); + event->title = g_strdup(gen.current_title); + event->idle = gen.current_idle; + g_async_queue_push(g.queue, event); +} static char * x_window_title(Window window) @@ -192,15 +203,6 @@ update_window_title(char *new_title) return changed; } -static void -push_event(void) { - struct event *event = g_slice_new0(struct event); - event->timestamp = g_get_real_time(); - event->title = g_strdup(gen.current_title); - event->idle = gen.current_idle; - g_async_queue_push(g.queue, event); -} - static void update_current_window(void) { @@ -235,6 +237,12 @@ update_current_window(void) static void on_x_property_notify(XPropertyEvent *ev) { + // TODO: also consider watch WM_CLASS + // - the first one is an "Instance name", the second one a "Class name" + // - don't know which one to pick, though probably the second one, + // since the instance name is Navigator/Mail for Firefox/Thunderbird + // - perhaps only when the active window has changed, so u_c_window() + // This is from the EWMH specification, set by the window manager if (ev->atom == gen.net_active_window) { update_current_window(); @@ -306,7 +314,7 @@ on_x_ready(G_GNUC_UNUSED gpointer user_data) } static void -generate_events(void) +generator_thread(void) { GIOChannel *channel = g_io_channel_unix_new(ConnectionNumber(gen.dpy)); GSource *watch = g_io_create_watch(channel, G_IO_IN); @@ -318,31 +326,9 @@ generate_events(void) g_main_loop_run(loop); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int -main(int argc, char *argv[]) +static void +generator_init(void) { - gboolean show_version = false; - const GOptionEntry options[] = { - {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, - &show_version, "output version information and exit", NULL}, - {}, - }; - - GError *error = NULL; - if (!gtk_init_with_args(&argc, &argv, " - activity tracker", - options, NULL, &error)) - exit_fatal("%s", error->message); - if (show_version) { - printf(PROJECT_NAME " " PROJECT_VERSION "\n"); - return 0; - } - - g.queue = g_async_queue_new_full((GDestroyNotify) event_free); - - if (!setlocale(LC_CTYPE, "")) - exit_fatal("cannot set locale"); if (!XSupportsLocale()) exit_fatal("locale not supported by Xlib"); @@ -392,56 +378,29 @@ main(int argc, char *argv[]) update_current_window(); set_idle_alarm(&gen.idle_alarm_inactive, XSyncPositiveComparison, gen.idle_timeout); +} - gchar *data_path = - g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL); - g_mkdir_with_parents(data_path, 0755); +static void +generator_launch(void) +{ + gen.thread = + g_thread_new("generator", (GThreadFunc) generator_thread, NULL); +} - // TODO: try exclusivity/invocation either via DBus directly, - // or via GApplication or GtkApplication: - // - GtkApplication calls Gtk.init automatically during "startup" signal, - // Gtk.init doesn't get command line args - // - "inhibiting" makes no sense, it can't be used for mere delays - // - actually, the "query-end" signal - // - should check whether it tries to exit cleanly - // - what is the session manager, do I have it? - // - "register-session" looks useful - // - GTK+ keeps the application running as long as it has windows, - // though I want to keep it running forever - // - g_application_hold(), perhaps - // - so maybe just use GApplication, that will provide more control +static void +generator_cleanup(void) +{ + g_thread_join(gen.thread); + free(gen.current_title); + XCloseDisplay(gen.dpy); +} - // Bind to a control socket, also ensuring only one instance is running - gchar *socket_path = g_build_filename(data_path, "socket", NULL); - - struct flock lock = - { - .l_type = F_WRLCK, - .l_start = 0, - .l_whence = SEEK_SET, - .l_len = 0, - }; - - gchar *lock_path = g_strdup_printf("%s.lock", socket_path); - int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644); - if (fcntl(lock_fd, F_SETLK, &lock)) - exit_fatal("failed to acquire lock: %s", strerror(errno)); - unlink(socket_path); - - int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (socket_fd < 0) - exit_fatal("%s: %s", socket_path, strerror(errno)); - - struct sockaddr_un sun; - sun.sun_family = AF_UNIX; - strncpy(sun.sun_path, socket_path, sizeof sun.sun_path); - if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun)) - exit_fatal("%s: %s", socket_path, strerror(errno)); - if (listen(socket_fd, 10)) - exit_fatal("%s: %s", socket_path, strerror(errno)); +// --- Main -------------------------------------------------------------------- +static sqlite3 * +database_init(const gchar *db_path) +{ sqlite3 *db = NULL; - gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL); int rc = sqlite3_open(db_path, &db); if (rc != SQLITE_OK) exit_fatal("%s: %s", db_path, sqlite3_errmsg(db)); @@ -464,6 +423,9 @@ main(int argc, char *argv[]) exit_fatal("%s: %s", db_path, "cannot retrieve user version"); int user_version = sqlite3_column_int(stmt, 0); + if ((rc = sqlite3_finalize(stmt))) + exit_fatal("%s: %s", db_path, "cannot retrieve user version"); + if (user_version == 0) { if ((rc = sqlite3_exec(db, "CREATE TABLE events (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -479,14 +441,95 @@ main(int argc, char *argv[]) } if ((rc = sqlite3_exec(db, "COMMIT", NULL, NULL, &errmsg))) exit_fatal("%s: %s", db_path, errmsg); + return db; +} - g_free(db_path); +static int +socket_init(const gchar *socket_path) +{ + // TODO: try exclusivity/invocation either via DBus directly, + // or via GApplication or GtkApplication: + // - GtkApplication calls Gtk.init automatically during "startup" signal, + // Gtk.init doesn't get command line args + // - "inhibiting" makes no sense, it can't be used for mere delays + // - actually, the "query-end" signal + // - should check whether it tries to exit cleanly + // - what is the session manager, do I have it? + // - "register-session" looks useful + // - GTK+ keeps the application running as long as it has windows, + // though I want to keep it running forever + // - g_application_hold(), perhaps + // - so maybe just use GApplication, that will provide more control + + struct flock lock = + { + .l_type = F_WRLCK, + .l_start = 0, + .l_whence = SEEK_SET, + .l_len = 0, + }; + + gchar *lock_path = g_strdup_printf("%s.lock", socket_path); + int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644); + if (fcntl(lock_fd, F_SETLK, &lock)) + exit_fatal("failed to acquire lock: %s", strerror(errno)); + unlink(socket_path); g_free(lock_path); - g_free(socket_path); - g_free(data_path); - GThread *generator = - g_thread_new("generator", (GThreadFunc) generate_events, NULL); + int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0) + exit_fatal("%s: %s", socket_path, strerror(errno)); + + struct sockaddr_un sun; + sun.sun_family = AF_UNIX; + strncpy(sun.sun_path, socket_path, sizeof sun.sun_path); + if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun)) + exit_fatal("%s: %s", socket_path, strerror(errno)); + if (listen(socket_fd, 10)) + exit_fatal("%s: %s", socket_path, strerror(errno)); + + // lock_fd stays open as long as the application is running--leak + return socket_fd; +} + +int +main(int argc, char *argv[]) +{ + if (!setlocale(LC_CTYPE, "")) + exit_fatal("cannot set locale"); + + gboolean show_version = false; + const GOptionEntry options[] = { + {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, + &show_version, "output version information and exit", NULL}, + {}, + }; + + GError *error = NULL; + if (!gtk_init_with_args(&argc, &argv, " - activity tracker", + options, NULL, &error)) + exit_fatal("%s", error->message); + if (show_version) { + printf(PROJECT_NAME " " PROJECT_VERSION "\n"); + return 0; + } + + g.queue = g_async_queue_new_full((GDestroyNotify) event_free); + generator_init(); + + gchar *data_path = + g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL); + g_mkdir_with_parents(data_path, 0755); + + // Bind to a control socket, also ensuring only one instance is running + gchar *socket_path = g_build_filename(data_path, "socket", NULL); + int socket_fd = socket_init(socket_path); + g_free(socket_path); + + gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL); + g_free(data_path); + g.db = database_init(db_path); + g_free(db_path); // TODO: somehow read events from the async queue // TODO: how in the name of fuck would our custom source wake up a sleeping @@ -495,12 +538,13 @@ main(int argc, char *argv[]) // TODO: listen for connections on the control socket - WdmtgWindow *window = wdmtg_window_new_with_db(db); + WdmtgWindow *window = wdmtg_window_new_with_db(g.db); + generator_launch(); gtk_main(); - g_thread_join(generator); + generator_cleanup(); - free(gen.current_title); - XCloseDisplay(gen.dpy); + sqlite3_close(g.db); + close(socket_fd); return 0; }