#include "common.h" #include #include #include #include #include #include #include #include "customcellrendererstate.h" #include "customcellrendererprogress.h" #include "customcellrendererrate.h" #include "customcellrendererleft.h" #ifndef G_VALUE_INIT # define G_VALUE_INIT { 0 } #endif static const guint UPDATE_INTERVAL_MS = 3500; typedef enum { COLUMN_STATE, COLUMN_TITLE, COLUMN_DOWNLOADED, COLUMN_SEEDED, COLUMN_UP, COLUMN_DOWN, COLUMN_LEFT, COLUMN_PROGRESSSORT, COLUMN_HASH, } column_t; typedef struct { const gchar* hash; state_t state; gchar* title; gfloat downloaded; /* percent */ gfloat seeded; /* ratio */ gfloat down, up; /* kilo (1000) byte per second */ guint64 size; /* bytes */ gint64 left; /* seconds left until done, < 0 means unknown */ } torrent_data_t; typedef struct { GtkListStore* store; GtkTreeIter iter; } torrent_t; static void torrent_update(torrent_t* torrent, const torrent_data_t* data); static void torrent_data_init(torrent_data_t* data, const gchar* hash); static void torrent_data_free(torrent_data_t* data); typedef struct { GAsyncQueue* queue; gboolean connected; GtkListStore* liststore; GtkTreeSelection* listselection; GtkWidget* top; GtkAction* connectaction, * disconnectaction; GtkAction* startaction, * stopaction, * rehashaction; GtkCheckMenuItem* viewtoolbaritem; GtkWidget* torrentmenu; GtkToolbar* toolbar; GtkWidget* connectdlg; GtkWidget* connectdlg_url; GtkWidget* connectdlg_user; GtkWidget* connectdlg_pwd; GtkWidget* statusbar; guint status_contextid; GtkWidget* aboutdlg; GKeyFile* config; gchar* configfile; guint config_timer; guint config_try; GHashTable* torrents; guint sync_timeout; const gchar* item_lock; } master_t; typedef struct { GAsyncQueue* queue; master_t* master; guint64 ratio_min, ratio_max, ratio_upload; } worker_data_t; typedef struct { int major; int minor; int patch; } version_t; typedef enum { /* Tell worker to connect. Will generate a MSG_CONNECTRESULT as response. */ MSG_CONNECT, /* Worker responding to an MSG_CONNECT */ MSG_CONNECTRESULT, /* Tell worker to disconnect. No response. */ MSG_DISCONNECT, /* Tell worker to QUIT. No response. */ MSG_QUIT, /* Worker reporting a fatal error to main. Main should just quit. */ MSG_ERROR, /* Worker reporting a status message to main. Can be generated at any time * for any reason */ MSG_STATUS, /* Tell worker to update the list of torrents. * Will generate a MSG_SYNCLIST response */ MSG_UPDATELIST, /* Worker responding to a MSG_UPDATELIST with added and removed torrents * and torrent data for all (except removed) */ MSG_SYNCLIST, /* Worker responding to a MSG_START/MSG_STOP/MSG_REHASH with uptodate * torrent state */ MSG_SYNCSTATE, /* Worker responding to a MSG_START/MSG_STOP/MSG_REHASH that a torrent * is removed */ MSG_SYNCREMOVE, /* Tell worker to send a start command for torrent. * Generates a MSG_SYNCSTATE as response if no error */ MSG_START, /* Tell worker to send a stop command for torrent. * Generates a MSG_SYNCSTATE or MSG_SYNCREMOVE as response if no error */ MSG_STOP, /* Tell worker to send a rehash command for torrent. * Generates a MSG_SYNCSTATE as response if no error */ MSG_REHASH, } msg_type_t; typedef struct { msg_type_t type; master_t* master; } msg_t; typedef struct { msg_t base; gchar* url; gchar* auth_username, * auth_password; } msg_connect_t; typedef struct { msg_t base; gboolean success; version_t version; gchar* errmsg; } msg_connectresult_t; typedef struct { msg_t base; gchar* msg; } msg_error_t; typedef struct { msg_t base; gchar* msg; } msg_status_t; typedef struct { msg_t base; } msg_quit_t; typedef struct { msg_t base; } msg_disconnect_t; typedef struct { msg_t base; } msg_updatelist_t; typedef struct { msg_t base; const torrent_data_t* data; /* .hash NULL terminated list */ const torrent_data_t* added; /* .hash NULL terminated list */ const gchar** removed; /* NULL terminated list */ } msg_synclist_t; typedef struct { msg_t base; const gchar* hash; state_t state; } msg_syncstate_t; typedef struct { msg_t base; const gchar* hash; } msg_syncremove_t; typedef struct { msg_t base; gchar* hash; } msg_start_t; typedef struct { msg_t base; gchar* hash; } msg_stop_t; typedef struct { msg_t base; gchar* hash; } msg_rehash_t; typedef struct _hashlist_t { torrent_data_t* data; gboolean* mark; gsize size, fill; torrent_data_t* added; const gchar** removed; gsize removed_size; } hashlist_t; static void msg_free(msg_t* msg); static msg_t* msg_connect(const gchar* url, const gchar* username, const gchar* password); static msg_t* msg_connectresult_success(version_t version); static msg_t* msg_connectresult_failed(const gchar* errfmt, ...); static msg_t* msg_error(const gchar* format, ...); static msg_t* msg_status(const gchar* format, ...); static msg_t* msg_disconnect(void); static msg_t* msg_quit(void); static msg_t* msg_updatelist(void); static msg_t* msg_synclist(torrent_data_t* data, torrent_data_t* added, const gchar** removed); static msg_t* msg_syncstate(const gchar* hash, state_t state); static msg_t* msg_syncremove(const gchar* hash); static msg_t* msg_start(const gchar* hash); static msg_t* msg_stop(const gchar* hash); static msg_t* msg_rehash(const gchar* hash); static void do_connect(GtkAction* action, gpointer data); static void do_disconnect(GtkAction* action, gpointer data); static void do_start(GtkAction* action, gpointer data); static void do_stop(GtkAction* action, gpointer data); static void do_rehash(GtkAction* action, gpointer data); static gboolean do_popupmenu(GtkWidget* treeview, gpointer data); static gboolean do_buttonpressed(GtkWidget *treeview, GdkEventButton *event, gpointer data); static void do_viewtoolbaritem(GtkCheckMenuItem* checkmenuitem, gpointer data); static void do_about(GtkMenuItem* menuitem, gpointer data); static gpointer worker_main(gpointer data); static gboolean incoming_msg(gpointer data); static gboolean sync_config(gpointer data); static void save_config(master_t* master); static void status(master_t* master, const char* format, ...); static void noop_destroy(gpointer value); static void torrent_destroy(gpointer value); static void listselection_changed(GtkTreeSelection* selection, gpointer user_data); static void liststore_sort_column_changed(GtkTreeSortable* sortable, gpointer user_data); static gint liststore_default_compare_func(GtkTreeModel* model, GtkTreeIter* a, GtkTreeIter* b, gpointer user_data); int main(int argc, char** argv) { gchar* gladefile; GtkBuilder* builder; GError* error = NULL; guint ret; GtkWidget* pwddlg; GtkCellRenderer* cell; GtkTreeViewColumn* column; GtkWidget* treeview; GThread* worker; worker_data_t worker_data; master_t master; memset(&master, 0, sizeof(master)); memset(&worker_data, 0, sizeof(worker_data)); #ifndef GLIB_VERSION_2_32 g_thread_init(NULL); #endif gtk_init(&argc, &argv); worker_data.queue = g_async_queue_new(); master.queue = worker_data.queue; worker_data.master = &master; #ifdef GLIB_VERSION_2_32 worker = g_thread_new("worker", worker_main, &worker_data); #else worker = g_thread_create(worker_main, &worker_data, TRUE, NULL); #endif if (worker == NULL) { fprintf(stderr, "Unable to create thread\n"); return EXIT_FAILURE; } builder = gtk_builder_new(); gladefile = g_build_filename(DATAROOTDIR, "viewtorrents.glade", NULL); ret = gtk_builder_add_from_file(builder, gladefile, &error); #ifdef DEBUG if (ret == 0) { g_clear_error(&error); gladefile = g_strdup("gui/viewtorrents.glade"); ret = gtk_builder_add_from_file(builder, gladefile, &error); } #endif if (ret == 0) { fprintf(stderr, "Unable to load %s: %s\n", gladefile, error->message); g_free(gladefile); g_clear_error(&error); return EXIT_FAILURE; } g_free(gladefile); master.torrents = g_hash_table_new_full(g_direct_hash, g_direct_equal, noop_destroy, torrent_destroy); master.top = GTK_WIDGET(gtk_builder_get_object(builder, "main")); g_signal_connect(master.top, "delete-event", G_CALLBACK(gtk_main_quit), NULL); g_signal_connect(gtk_builder_get_object(builder, "quitmenuitem"), "activate", G_CALLBACK(gtk_main_quit), NULL); master.aboutdlg = GTK_WIDGET(gtk_builder_get_object(builder, "aboutdialog")); g_signal_connect_swapped(master.aboutdlg, "response", G_CALLBACK(gtk_widget_hide), master.aboutdlg); g_signal_connect(gtk_builder_get_object(builder, "aboutmenuitem"), "activate", G_CALLBACK(do_about), &master); master.connectaction = GTK_ACTION(gtk_builder_get_object(builder, "connectaction")); g_signal_connect(master.connectaction, "activate", G_CALLBACK(do_connect), &master); master.disconnectaction = GTK_ACTION(gtk_builder_get_object(builder, "disconnectaction")); g_signal_connect(master.disconnectaction, "activate", G_CALLBACK(do_disconnect), &master); master.startaction = GTK_ACTION(gtk_builder_get_object(builder, "startaction")); g_signal_connect(master.startaction, "activate", G_CALLBACK(do_start), &master); master.stopaction = GTK_ACTION(gtk_builder_get_object(builder, "stopaction")); g_signal_connect(master.stopaction, "activate", G_CALLBACK(do_stop), &master); master.rehashaction = GTK_ACTION(gtk_builder_get_object(builder, "rehashaction")); g_signal_connect(master.rehashaction, "activate", G_CALLBACK(do_rehash), &master); gtk_action_set_sensitive(master.connectaction, TRUE); gtk_action_set_sensitive(master.disconnectaction, FALSE); gtk_action_set_sensitive(master.startaction, FALSE); gtk_action_set_sensitive(master.stopaction, FALSE); gtk_action_set_sensitive(master.rehashaction, FALSE); master.toolbar = GTK_TOOLBAR(gtk_builder_get_object(builder, "toolbar")); master.viewtoolbaritem = GTK_CHECK_MENU_ITEM(gtk_builder_get_object(builder, "viewtoolbaritem")); g_signal_connect(master.viewtoolbaritem, "toggled", G_CALLBACK(do_viewtoolbaritem), &master); pwddlg = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg")); gtk_dialog_set_default_response(GTK_DIALOG(pwddlg), GTK_RESPONSE_OK); master.liststore = GTK_LIST_STORE(gtk_builder_get_object(builder, "liststore")); g_signal_connect(master.liststore, "sort-column-changed", G_CALLBACK(liststore_sort_column_changed), &master); gtk_tree_sortable_set_default_sort_func(GTK_TREE_SORTABLE(master.liststore), liststore_default_compare_func, &master, NULL); master.torrentmenu = GTK_WIDGET(gtk_builder_get_object(builder, "torrentmenu")); treeview = GTK_WIDGET(gtk_builder_get_object(builder, "treeview")); master.listselection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); gtk_tree_selection_set_mode(master.listselection, GTK_SELECTION_SINGLE); g_signal_connect(master.listselection, "changed", G_CALLBACK(listselection_changed), &master); g_signal_connect(treeview, "button-press-event", G_CALLBACK(do_buttonpressed), &master); g_signal_connect(treeview, "popup-menu", G_CALLBACK(do_popupmenu), &master); cell = custom_cell_renderer_state_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "statecolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "state", COLUMN_STATE); cell = custom_cell_renderer_progress_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "progresscolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "downloaded", COLUMN_DOWNLOADED); gtk_tree_view_column_add_attribute(column, cell, "seeded", COLUMN_SEEDED); cell = custom_cell_renderer_rate_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder, "upcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "rate", COLUMN_UP); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder,"downcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "rate", COLUMN_DOWN); cell = custom_cell_renderer_left_new(); column = GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(builder,"leftcolumn")); gtk_tree_view_column_pack_start(column, cell, TRUE); gtk_tree_view_column_add_attribute(column, cell, "left", COLUMN_LEFT); master.statusbar = GTK_WIDGET(gtk_builder_get_object(builder, "statusbar")); master.status_contextid = gtk_statusbar_get_context_id(GTK_STATUSBAR(master.statusbar), "main"); master.config = g_key_file_new(); master.configfile = g_build_filename(g_get_user_config_dir(), "viewtorrent.ini", NULL); g_key_file_load_from_file(master.config, master.configfile, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, NULL); { gint column = g_key_file_get_integer(master.config, "view", "sort_column", NULL); gboolean desc = g_key_file_get_boolean(master.config, "view", "sort_desc", NULL); if (column > 0) { gtk_tree_sortable_set_sort_column_id( GTK_TREE_SORTABLE(master.liststore), column, desc ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING); } else { gtk_tree_sortable_set_sort_column_id( GTK_TREE_SORTABLE(master.liststore), GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, GTK_SORT_ASCENDING); } } gtk_widget_show_all(master.top); { GError* error = NULL; gboolean view = g_key_file_get_boolean(master.config, "view", "toolbar", &error); if (!view && error) { g_clear_error(&error); view = TRUE; /* default */ } gtk_check_menu_item_set_active(master.viewtoolbaritem, view); if (!view) { gtk_widget_hide(GTK_WIDGET(master.toolbar)); } } master.connectdlg = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg")); gtk_dialog_set_default_response(GTK_DIALOG(master.connectdlg), GTK_RESPONSE_OK); master.connectdlg_url = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_url")); master.connectdlg_user = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_user")); master.connectdlg_pwd = GTK_WIDGET(gtk_builder_get_object(builder, "connectdlg_pwd")); if (g_key_file_get_boolean(master.config, "connect", "auto", NULL)) { gchar* url = g_key_file_get_string(master.config, "connect", "url", NULL); gchar* user = g_key_file_get_string(master.config, "connect", "username", NULL); gchar* pass = NULL; if (url != NULL && user != NULL) { GtkWidget* label, * pwd; gchar* format, * text; label = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg_label")); pwd = GTK_WIDGET(gtk_builder_get_object(builder, "pwddlg_pwd")); format = g_strdup(gtk_label_get_text(GTK_LABEL(label))); text = g_strdup_printf(format, url); g_free(format); gtk_label_set_text(GTK_LABEL(label), text); g_free(text); switch (gtk_dialog_run(GTK_DIALOG(pwddlg))) { case GTK_RESPONSE_OK: pass = g_strdup(gtk_entry_get_text(GTK_ENTRY(pwd))); break; case GTK_RESPONSE_CANCEL: default: g_free(url); url = NULL; } gtk_widget_hide(pwddlg); } if (url != NULL) { g_async_queue_push(worker_data.queue, msg_connect(url, user, pass)); status(&master, "Autoconnecting to %s...", url); } g_free(url); g_free(user); g_free(pass); } gtk_main(); if (master.sync_timeout != 0) { g_source_remove(master.sync_timeout); master.sync_timeout = 0; } if (master.config_timer != 0) { g_source_remove(master.config_timer); master.config_timer = 0; sync_config(&master); } g_async_queue_push(worker_data.queue, msg_quit()); g_async_queue_unref(worker_data.queue); g_thread_join(worker); g_hash_table_destroy(master.torrents); gtk_widget_destroy(master.top); gtk_widget_destroy(pwddlg); gtk_widget_destroy(master.connectdlg); gtk_widget_destroy(master.torrentmenu); g_object_unref(builder); g_key_file_free(master.config); g_free(master.configfile); return EXIT_SUCCESS; } gboolean sync_config(gpointer _data) { master_t* master = (master_t*)_data; gsize length; gchar *data = g_key_file_to_data(master->config, &length, NULL); GError* err = NULL; if (!g_file_set_contents(master->configfile, data, length, &err)) { g_free(data); status(master, "Error writing configfile: %s", err->message); g_error_free(err); master->config_try++; if (master->config_try == 2) { /* Wait longer */ master->config_timer = g_timeout_add_seconds(5 * 60, sync_config, master); return FALSE; } if (master->config_try == 10) { /* Just give up */ return FALSE; } /* Try again */ return TRUE; } g_free(data); master->config_timer = 0; master->config_try = 0; return FALSE; } static gboolean update_list(gpointer _master) { master_t* master = _master; g_async_queue_push(master->queue, msg_updatelist()); master->sync_timeout = 0; return FALSE; } gboolean incoming_msg(gpointer data) { msg_t* msg = data; switch (msg->type) { case MSG_CONNECTRESULT: { msg_connectresult_t* m = (msg_connectresult_t*)msg; if (m->success) { msg->master->connected = TRUE; gtk_action_set_sensitive(msg->master->connectaction, FALSE); gtk_action_set_sensitive(msg->master->disconnectaction, TRUE); listselection_changed(msg->master->listselection, msg->master); g_key_file_set_boolean(msg->master->config, "connect", "auto", TRUE); save_config(msg->master); status(msg->master, "Connected to rTorrent %d.%d.%d.", m->version.major, m->version.minor, m->version.patch); g_async_queue_push(msg->master->queue, msg_updatelist()); } else { GtkWidget* dlg; msg->master->connected = FALSE; gtk_action_set_sensitive(msg->master->connectaction, TRUE); gtk_action_set_sensitive(msg->master->disconnectaction, FALSE); gtk_list_store_clear(msg->master->liststore); listselection_changed(msg->master->listselection, msg->master); dlg = gtk_message_dialog_new(GTK_WINDOW(msg->master->top), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to connect: %s", m->errmsg); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); } break; } case MSG_ERROR: { msg_error_t* m = (msg_error_t*)msg; GtkWidget* dlg; dlg = gtk_message_dialog_new(GTK_WINDOW(msg->master->top), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Fatal error: %s", m->msg); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); gtk_main_quit(); break; } case MSG_STATUS: { msg_status_t* m = (msg_status_t*)msg; status(msg->master, m->msg); break; } case MSG_SYNCLIST: { msg_synclist_t* m = (msg_synclist_t*)msg; const torrent_data_t* data; const gchar** del = m->removed; for (; *del != NULL; ++del) { if (msg->master->item_lock == *del) { msg->master->item_lock = NULL; listselection_changed(msg->master->listselection, msg->master); } g_hash_table_remove(msg->master->torrents, *del); } data = m->data; for (; data->hash != NULL; ++data) { torrent_t* t = g_hash_table_lookup(msg->master->torrents, data->hash); if (t != NULL) { torrent_update(t, data); } if (msg->master->item_lock == data->hash) { msg->master->item_lock = NULL; listselection_changed(msg->master->listselection, msg->master); } } data = m->added; for (; data->hash != NULL; ++data) { torrent_t* t = g_slice_new(torrent_t); t->store = msg->master->liststore; gtk_list_store_append(t->store, &t->iter); g_hash_table_insert(msg->master->torrents, (gpointer)data->hash, t); torrent_update(t, data); } msg->master->sync_timeout = g_timeout_add(UPDATE_INTERVAL_MS, update_list, msg->master); break; } case MSG_SYNCSTATE: { msg_syncstate_t* m = (msg_syncstate_t*)msg; torrent_t* t = g_hash_table_lookup(msg->master->torrents, m->hash); if (t != NULL) { gtk_list_store_set(t->store, &t->iter, COLUMN_STATE, m->state, -1); } if (msg->master->item_lock == m->hash) { msg->master->item_lock = NULL; listselection_changed(msg->master->listselection, msg->master); } break; } case MSG_SYNCREMOVE: { msg_syncremove_t* m = (msg_syncremove_t*)msg; if (msg->master->item_lock == m->hash) { msg->master->item_lock = NULL; listselection_changed(msg->master->listselection, msg->master); } g_hash_table_remove(msg->master->torrents, m->hash); break; } case MSG_CONNECT: case MSG_DISCONNECT: case MSG_QUIT: case MSG_UPDATELIST: case MSG_START: case MSG_STOP: case MSG_REHASH: g_assert(FALSE); } msg_free(msg); return FALSE; } void do_connect(GtkAction* action, gpointer data) { master_t* master = data; gchar* url, * user, * pass = NULL; if (master->connected) { return; } url = g_key_file_get_string(master->config, "connect", "url", NULL); user = g_key_file_get_string(master->config, "connect", "username", NULL); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_url), url ? url : ""); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_user),user ? user : ""); gtk_entry_set_text(GTK_ENTRY(master->connectdlg_pwd), ""); switch (gtk_dialog_run(GTK_DIALOG(master->connectdlg))) { case GTK_RESPONSE_OK: g_free(url); g_free(user); url = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_url))); user = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_user))); pass = g_strdup(gtk_entry_get_text(GTK_ENTRY(master->connectdlg_pwd))); break; case GTK_RESPONSE_CANCEL: default: g_free(url); g_free(user); url = NULL; } gtk_widget_hide(master->connectdlg); if (url == NULL) { return; } if (strlen(user) == 0 && strlen(pass) == 0) { g_free(user); g_free(pass); user = NULL; pass = NULL; } gtk_action_set_sensitive(action, FALSE); status(master, "Connecting to %s...", url); g_key_file_set_string(master->config, "connect", "url", url); if (user != NULL) { g_key_file_set_string(master->config, "connect", "username", user); } else { g_key_file_remove_key(master->config, "connect", "username", NULL); } g_key_file_remove_key(master->config, "connect", "auto", NULL); save_config(master); g_async_queue_push(master->queue, msg_connect(url, user, pass)); g_free(url); g_free(user); g_free(pass); } void do_disconnect(GtkAction* action, gpointer data) { master_t* master = data; if (!master->connected) { return; } if (master->sync_timeout != 0) { g_source_remove(master->sync_timeout); master->sync_timeout = 0; } gtk_action_set_sensitive(action, FALSE); status(master, "Disconnecting..."); g_hash_table_remove_all(master->torrents); g_async_queue_push(master->queue, msg_disconnect()); master->connected = FALSE; listselection_changed(master->listselection, master); } static const gchar* get_selected_torrent(master_t* master) { GValue value = G_VALUE_INIT; GtkTreeModel* model; GtkTreeIter iter; const gchar* hash; if (!master->connected || master->item_lock) { return FALSE; } if (!gtk_tree_selection_get_selected(master->listselection, &model, &iter)) { return FALSE; } gtk_tree_model_get_value(model, &iter, COLUMN_HASH, &value); hash = g_value_get_pointer(&value); g_value_unset(&value); return hash; } void do_start(GtkAction* action, gpointer data) { master_t* master = data; const gchar* hash; if ((hash = get_selected_torrent(master)) == NULL) { return; } g_async_queue_push(master->queue, msg_start(hash)); master->item_lock = hash; gtk_action_set_sensitive(master->startaction, FALSE); gtk_action_set_sensitive(master->stopaction, FALSE); gtk_action_set_sensitive(master->rehashaction, FALSE); } void do_stop(GtkAction* action, gpointer data) { master_t* master = data; const gchar* hash; if ((hash = get_selected_torrent(master)) == NULL) { return; } g_async_queue_push(master->queue, msg_stop(hash)); master->item_lock = hash; gtk_action_set_sensitive(master->startaction, FALSE); gtk_action_set_sensitive(master->stopaction, FALSE); gtk_action_set_sensitive(master->rehashaction, FALSE); } void do_rehash(GtkAction* action, gpointer data) { master_t* master = data; const gchar* hash; if ((hash = get_selected_torrent(master)) == NULL) { return; } g_async_queue_push(master->queue, msg_rehash(hash)); master->item_lock = hash; gtk_action_set_sensitive(master->startaction, FALSE); gtk_action_set_sensitive(master->stopaction, FALSE); gtk_action_set_sensitive(master->rehashaction, FALSE); } void save_config(master_t* master) { if (master->config_timer == 0) { master->config_timer = g_timeout_add_seconds(5, sync_config, master); } } void status(master_t* master, const char* format, ...) { gchar* tmp; va_list args; gtk_statusbar_pop(GTK_STATUSBAR(master->statusbar), master->status_contextid); va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); gtk_statusbar_push(GTK_STATUSBAR(master->statusbar), master->status_contextid, tmp); g_free(tmp); } void torrent_update(torrent_t* torrent, const torrent_data_t* data) { gtk_list_store_set(torrent->store, &torrent->iter, COLUMN_STATE, data->state, COLUMN_TITLE, data->title, COLUMN_DOWNLOADED, data->downloaded, COLUMN_SEEDED, data->seeded, COLUMN_UP, data->up, COLUMN_DOWN, data->down, COLUMN_LEFT, data->left, COLUMN_PROGRESSSORT, data->downloaded < 100.0f ? data->downloaded : 1000.0f + data->seeded, COLUMN_HASH, data->hash, -1); } static void worker_respond(worker_data_t* data, msg_t* msg); static gint64 get_i64_xmlrpc(xmlrpc_env* env, xmlrpc_value* value); static gboolean get_bool_xmlrpc(xmlrpc_env* env, xmlrpc_value* value); static void sync_torrent_state(worker_data_t* data, xmlrpc_env* env, xmlrpc_client* client, xmlrpc_server_info* server, const gchar* hash); static void hashlist_init(hashlist_t* hlist); static void hashlist_clear(hashlist_t* hlist); static void hashlist_free(hashlist_t* hlist); static gboolean hashlist_sync(hashlist_t* hlist, worker_data_t* data, xmlrpc_env * const env, const xmlrpc_value * const result, gsize count); gpointer worker_main(gpointer _data) { worker_data_t* data = _data; xmlrpc_env env; xmlrpc_client* client; xmlrpc_server_info* server = NULL; gboolean quit = FALSE; version_t version; hashlist_t hashlist; hashlist_init(&hashlist); data->queue = g_async_queue_ref(data->queue); xmlrpc_env_init(&env); xmlrpc_client_setup_global_const(&env); xmlrpc_client_create(&env, XMLRPC_CLIENT_NO_FLAGS, "viewtorrents", "0.2", NULL, 0, &client); if (env.fault_occurred) { worker_respond(data, msg_error("Unable to create XMLRPC-C client (%d): %s", env.fault_code, env.fault_string)); quit = TRUE; } while (!quit) { msg_t* msg = g_async_queue_pop(data->queue); switch (msg->type) { case MSG_CONNECT: { const gchar* tmp; xmlrpc_value* params, * result; msg_connect_t* m = (msg_connect_t*)msg; char* end; long value; server = xmlrpc_server_info_new(&env, m->url); if (m->auth_username != NULL) { xmlrpc_server_info_set_user(&env, server, m->auth_username, m->auth_password); xmlrpc_server_info_allow_auth_basic(&env, server); } params = xmlrpc_array_new(&env); xmlrpc_client_call2(&env, client, server, "system.client_version", params, &result); xmlrpc_DECREF(params); if (env.fault_occurred) { xmlrpc_server_info_free(server); msg_free(msg); server = NULL; worker_respond(data, msg_connectresult_failed( "Unable to get client version (%d): %s", env.fault_code, env.fault_string)); env.fault_occurred = FALSE; continue; } xmlrpc_read_string(&env, result, &tmp); xmlrpc_DECREF(result); if (env.fault_occurred) { xmlrpc_server_info_free(server); msg_free(msg); server = NULL; worker_respond(data, msg_connectresult_failed( "Unable to get client version (%d): %s", env.fault_code, env.fault_string)); env.fault_occurred = FALSE; continue; } memset(&version, 0, sizeof(version)); errno = 0; value = strtol(tmp, &end, 10); if (errno == 0 && value >= 0 && value < INT_MAX) { version.major = (int)value; if (end && *end == '.') { char* last = end + 1; value = strtol(last, &end, 10); if (errno == 0 && value >= 0 && value < INT_MAX) { version.minor = (int)value; if (end && *end == '.') { last = end + 1; value = strtol(last, &end, 10); if (errno == 0 && value >= 0 && value < INT_MAX) { version.patch = (int)value; } } } } } free((void*)tmp); worker_respond(data, msg_connectresult_success(version)); hashlist_clear(&hashlist); msg_free(msg); break; } case MSG_DISCONNECT: { xmlrpc_server_info_free(server); server = NULL; worker_respond(data, msg_status("Disconnected.")); break; } case MSG_UPDATELIST: { xmlrpc_value* params, * item, * result = NULL; int count; params = xmlrpc_array_new(&env); item = xmlrpc_string_new(&env, ""); /* default view */ xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.hash="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.is_active="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.hashing_failed="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.is_hash_checking="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.name="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.size_bytes="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.bytes_done="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.ratio="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.down.rate="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); item = xmlrpc_string_new(&env, "d.up.rate="); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); xmlrpc_client_call2(&env, client, server, "d.multicall", params, &result); xmlrpc_DECREF(params); if (!env.fault_occurred) { count = xmlrpc_array_size(&env, result); } if (env.fault_occurred) { if (result) xmlrpc_DECREF(result); worker_respond(data, msg_status("Failed to update list (%d): %s", env.fault_code, env.fault_string)); env.fault_occurred = FALSE; msg_free(msg); continue; } if (!hashlist_sync(&hashlist, data, &env, result, count > 0 ? (gsize)count : 0)) { xmlrpc_DECREF(result); if (env.fault_occurred) { worker_respond(data, msg_status("Failed to update list (%d): %s", env.fault_code, env.fault_string)); } else { worker_respond(data, msg_status("Failed to update list: Out of memory")); } env.fault_occurred = FALSE; msg_free(msg); continue; } xmlrpc_DECREF(result); worker_respond(data, msg_synclist(hashlist.data, hashlist.added, hashlist.removed)); params = xmlrpc_array_new(&env); xmlrpc_client_call2(&env, client, server, "ratio.min", params, &result); data->ratio_min = get_i64_xmlrpc(&env, result) * 10; if (env.fault_occurred) { env.fault_occurred = FALSE; } xmlrpc_client_call2(&env, client, server, "ratio.max", params, &result); data->ratio_max = get_i64_xmlrpc(&env, result) * 10; if (env.fault_occurred) { env.fault_occurred = FALSE; } xmlrpc_client_call2(&env, client, server, "ratio.upload", params, &result); data->ratio_upload = get_i64_xmlrpc(&env, result); if (env.fault_occurred) { env.fault_occurred = FALSE; } xmlrpc_DECREF(params); msg_free(msg); break; } case MSG_QUIT: quit = TRUE; msg_free(msg); break; case MSG_START: { msg_start_t* m = (msg_start_t*)msg; xmlrpc_value* params, * item, * result = NULL; xmlrpc_int ret; params = xmlrpc_array_new(&env); item = xmlrpc_string_new(&env, m->hash); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); xmlrpc_client_call2(&env, client, server, "d.resume", params, &result); xmlrpc_DECREF(params); xmlrpc_read_int(&env, result, &ret); if (result) xmlrpc_DECREF(result); if (env.fault_occurred || ret != 0) { worker_respond(data, msg_status("Failed to resume torrent %s (%d): %s", m->hash, env.fault_code, env.fault_string)); env.fault_occurred = FALSE; } else { sync_torrent_state(data, &env, client, server, m->hash); } msg_free(msg); break; } case MSG_STOP: { msg_stop_t* m = (msg_stop_t*)msg; xmlrpc_value* params, * item, * result = NULL; gboolean active; params = xmlrpc_array_new(&env); item = xmlrpc_string_new(&env, m->hash); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); xmlrpc_client_call2(&env, client, server, "d.is_active", params, &result); active = get_bool_xmlrpc(&env, result); if (env.fault_occurred) { worker_respond(data, msg_status("Failed to get torrent status %s (%d): %s", m->hash, env.fault_code, env.fault_string)); env.fault_occurred = FALSE; } else { xmlrpc_int ret; xmlrpc_client_call2(&env, client, server, active ? "d.pause" : "d.erase", params, &result); xmlrpc_DECREF(params); xmlrpc_read_int(&env, result, &ret); if (result) xmlrpc_DECREF(result); if (env.fault_occurred || ret != 0) { worker_respond(data, msg_status("Failed to %s torrent %s (%d): %s", active ? "pause" : "remove", m->hash, env.fault_code, env.fault_string)); env.fault_occurred = FALSE; } else { if (active) { sync_torrent_state(data, &env, client, server, m->hash); } else { worker_respond(data, msg_syncremove(m->hash)); } } } msg_free(msg); break; } case MSG_REHASH: { msg_rehash_t* m = (msg_rehash_t*)msg; xmlrpc_value* params, * item, * result = NULL; xmlrpc_int ret; params = xmlrpc_array_new(&env); item = xmlrpc_string_new(&env, m->hash); xmlrpc_array_append_item(&env, params, item); xmlrpc_DECREF(item); xmlrpc_client_call2(&env, client, server, "d.check_hash", params, &result); xmlrpc_DECREF(params); xmlrpc_read_int(&env, result, &ret); if (result) xmlrpc_DECREF(result); if (env.fault_occurred || ret != 0) { worker_respond(data, msg_status("Failed to rehash torrent %s (%d): %s", m->hash, env.fault_code, env.fault_string)); env.fault_occurred = FALSE; } else { sync_torrent_state(data, &env, client, server, m->hash); } msg_free(msg); break; } case MSG_CONNECTRESULT: case MSG_ERROR: case MSG_STATUS: case MSG_SYNCLIST: case MSG_SYNCSTATE: case MSG_SYNCREMOVE: g_assert(FALSE); break; } } hashlist_free(&hashlist); if (server != NULL) { xmlrpc_server_info_free(server); } xmlrpc_client_destroy(client); xmlrpc_client_teardown_global_const(); g_async_queue_unref(data->queue); return NULL; } void worker_respond(worker_data_t* data, msg_t* msg) { msg->master = data->master; g_main_context_invoke(NULL, incoming_msg, msg); } void msg_free(msg_t* msg) { switch (msg->type) { case MSG_CONNECT: { msg_connect_t* m = (msg_connect_t*)msg; g_free(m->url); g_free(m->auth_username); g_free(m->auth_password); g_slice_free(msg_connect_t, m); break; } case MSG_CONNECTRESULT: { msg_connectresult_t* m = (msg_connectresult_t*)msg; if (!m->success) { g_free(m->errmsg); } g_slice_free(msg_connectresult_t, m); break; } case MSG_ERROR: { msg_error_t* m = (msg_error_t*)msg; g_free(m->msg); g_slice_free(msg_error_t, m); break; } case MSG_STATUS: { msg_status_t* m = (msg_status_t*)msg; g_free(m->msg); g_slice_free(msg_status_t, m); break; } case MSG_DISCONNECT: { msg_disconnect_t* m = (msg_disconnect_t*)msg; g_slice_free(msg_disconnect_t, m); break; } case MSG_QUIT: { msg_quit_t* m = (msg_quit_t*)msg; g_slice_free(msg_quit_t, m); break; } case MSG_UPDATELIST: { msg_updatelist_t* m = (msg_updatelist_t*)msg; g_slice_free(msg_updatelist_t, m); break; } case MSG_SYNCLIST: { msg_synclist_t* m = (msg_synclist_t*)msg; g_slice_free(msg_synclist_t, m); break; } case MSG_SYNCSTATE: { msg_syncstate_t* m = (msg_syncstate_t*)msg; g_slice_free(msg_syncstate_t, m); break; } case MSG_SYNCREMOVE: { msg_syncremove_t* m = (msg_syncremove_t*)msg; g_slice_free(msg_syncremove_t, m); break; } case MSG_START: { msg_start_t* m = (msg_start_t*)msg; g_free(m->hash); g_slice_free(msg_start_t, m); break; } case MSG_STOP: { msg_stop_t* m = (msg_stop_t*)msg; g_free(m->hash); g_slice_free(msg_stop_t, m); break; } case MSG_REHASH: { msg_rehash_t* m = (msg_rehash_t*)msg; g_free(m->hash); g_slice_free(msg_rehash_t, m); break; } } } msg_t* msg_connect(const gchar* url, const gchar* username, const gchar* password) { msg_connect_t* msg = g_slice_new(msg_connect_t); msg->base.type = MSG_CONNECT; msg->url = strdup(url); msg->auth_username = g_strdup(username); msg->auth_password = g_strdup(password); return &msg->base; } msg_t* msg_connectresult_success(version_t version) { msg_connectresult_t* msg = g_slice_new(msg_connectresult_t); msg->base.type = MSG_CONNECTRESULT; msg->success = TRUE; msg->version = version; return &msg->base; } msg_t* msg_connectresult_failed(const gchar* errfmt, ...) { msg_connectresult_t* msg = g_slice_new(msg_connectresult_t); va_list args; msg->base.type = MSG_CONNECTRESULT; msg->success = FALSE; va_start(args, errfmt); msg->errmsg = g_strdup_vprintf(errfmt, args); va_end(args); return &msg->base; } msg_t* msg_error(const gchar* format, ...) { msg_error_t* msg = g_slice_new(msg_error_t); va_list args; msg->base.type = MSG_ERROR; va_start(args, format); msg->msg = g_strdup_vprintf(format, args); va_end(args); return &msg->base; } msg_t* msg_status(const gchar* format, ...) { msg_status_t* msg = g_slice_new(msg_status_t); va_list args; msg->base.type = MSG_STATUS; va_start(args, format); msg->msg = g_strdup_vprintf(format, args); va_end(args); return &msg->base; } msg_t* msg_disconnect(void) { msg_disconnect_t* msg = g_slice_new(msg_disconnect_t); msg->base.type = MSG_DISCONNECT; return &msg->base; } msg_t* msg_quit(void) { msg_quit_t* msg = g_slice_new(msg_quit_t); msg->base.type = MSG_QUIT; return &msg->base; } msg_t* msg_updatelist(void) { msg_updatelist_t* msg = g_slice_new(msg_updatelist_t); msg->base.type = MSG_UPDATELIST; return &msg->base; } msg_t* msg_synclist(torrent_data_t* data, torrent_data_t* added, const gchar** removed) { msg_synclist_t* msg = g_slice_new(msg_synclist_t); msg->base.type = MSG_SYNCLIST; msg->data = data; msg->added = added; msg->removed = removed; return &msg->base; } msg_t* msg_syncstate(const gchar* hash, state_t state) { msg_syncstate_t* msg = g_slice_new(msg_syncstate_t); msg->base.type = MSG_SYNCSTATE; msg->hash = hash; msg->state = state; return &msg->base; } msg_t* msg_syncremove(const gchar* hash) { msg_syncstate_t* msg = g_slice_new(msg_syncstate_t); msg->base.type = MSG_SYNCREMOVE; msg->hash = hash; return &msg->base; } msg_t* msg_start(const gchar* hash) { msg_start_t* msg = g_slice_new(msg_start_t); msg->base.type = MSG_START; msg->hash = g_strdup(hash); return &msg->base; } msg_t* msg_stop(const gchar* hash) { msg_stop_t* msg = g_slice_new(msg_stop_t); msg->base.type = MSG_STOP; msg->hash = g_strdup(hash); return &msg->base; } msg_t* msg_rehash(const gchar* hash) { msg_rehash_t* msg = g_slice_new(msg_rehash_t); msg->base.type = MSG_REHASH; msg->hash = g_strdup(hash); return &msg->base; } void hashlist_init(hashlist_t* hlist) { hlist->fill = 0; hlist->size = 10; hlist->data = g_new0(torrent_data_t, hlist->size); hlist->mark = g_new0(gboolean, hlist->size); hlist->added = hlist->data; hlist->removed_size = 5; hlist->removed = g_new0(const gchar*, hlist->removed_size); } void hashlist_clear(hashlist_t* hlist) { torrent_data_t* d = hlist->data; const gchar** r; for (; d->hash != NULL; d++) { g_free((gchar*)d->hash); torrent_data_free(d); } for (r = hlist->removed; *r != NULL; r++) { g_free((gchar*)*r); } hlist->data[0].hash = NULL; hlist->fill = 0; hlist->added = hlist->data; hlist->removed[0] = NULL; } void hashlist_free(hashlist_t* hlist) { hashlist_clear(hlist); g_free(hlist->data); g_free(hlist->mark); g_free(hlist->removed); } static gboolean hashlist_data_resize(hashlist_t* hlist) { gsize ns = hlist->size * 2; torrent_data_t* tmp1; gboolean* tmp2; if (ns < 10) ns = 10; tmp1 = realloc(hlist->data, ns * sizeof(torrent_data_t)); tmp2 = realloc(hlist->mark, ns * sizeof(gboolean)); if (tmp1 == NULL || tmp2 == NULL) { if (tmp1) hlist->data = tmp1; if (tmp2) hlist->mark = tmp2; hlist->added = hlist->data + hlist->fill; return FALSE; } hlist->data = tmp1; hlist->mark = tmp2; hlist->size = ns; return TRUE; } static gboolean hashlist_removed_resize(hashlist_t* hlist, gsize need) { gsize ns; const gchar** tmp; if (hlist->removed_size > need) { return TRUE; } ns = hlist->removed_size * 2; if (ns < need + 1) ns = need + 1; tmp = realloc(hlist->data, ns * sizeof(const gchar*)); if (tmp == NULL) { hlist->added = hlist->data + hlist->fill; return FALSE; } hlist->removed = tmp; hlist->removed_size = ns; return TRUE; } static gboolean hashlist_item_sync(worker_data_t* data, xmlrpc_env * const env, const xmlrpc_value * const line, torrent_data_t* torrent_data); gboolean hashlist_sync(hashlist_t* hlist, worker_data_t* data, xmlrpc_env * const env, const xmlrpc_value * const result, gsize count) { gsize i, j, a, r; gsize found = 0; g_assert(hlist->data + hlist->fill == hlist->added); for (a = hlist->added - hlist->data; hlist->data[a].hash; ++a); hlist->fill = a; hlist->added = hlist->data + a; for (r = 0; hlist->removed[r]; ++r) { g_free((gchar*)hlist->removed[r]); } hlist->removed[0] = NULL; memset(hlist->mark, 0, hlist->fill * sizeof(gboolean)); for (i = 0; i < count; ++i) { xmlrpc_value* line, *item; const gchar* hash; xmlrpc_array_read_item(env, result, i, &line); xmlrpc_array_read_item(env, line, 0, &item); xmlrpc_read_string(env, item, &hash); if (item) xmlrpc_DECREF(item); if (env->fault_occurred) { hlist->added = hlist->data + hlist->fill; if (line) xmlrpc_DECREF(line); return FALSE; } if (found == hlist->fill) { if (a + 1 >= hlist->size && !hashlist_data_resize(hlist)) { free((void*)hash); xmlrpc_DECREF(line); return FALSE; } torrent_data_init(hlist->data + a, hash); if (!hashlist_item_sync(data, env, line, hlist->data + a++)) { hlist->added = hlist->data + hlist->fill; hlist->data[a].hash = NULL; xmlrpc_DECREF(line); return FALSE; } if (a < hlist->size) hlist->data[a].hash = NULL; xmlrpc_DECREF(line); continue; } /* Quick path */ if (i < hlist->fill && !hlist->mark[i] && strcmp(hlist->data[i].hash, hash) == 0) { if (!hashlist_item_sync(data, env, line, hlist->data + i)) { hlist->added = hlist->data + hlist->fill; xmlrpc_DECREF(line); return FALSE; } hlist->mark[i] = TRUE; found++; free((void*)hash); xmlrpc_DECREF(line); continue; } for (j = i + 1; j != i; ++j) { if (j >= hlist->fill) { j = 0; if (i == 0) { break; } } if (hlist->mark[j]) { continue; } if (strcmp(hlist->data[j].hash, hash) == 0) { break; } } if (j != i) { if (!hashlist_item_sync(data, env, line, hlist->data + j)) { hlist->added = hlist->data + hlist->fill; xmlrpc_DECREF(line); return FALSE; } hlist->mark[j] = TRUE; found++; free((void*)hash); } else { /* New */ if (a + 1 >= hlist->size && !hashlist_data_resize(hlist)) { free((void*)hash); xmlrpc_DECREF(line); return FALSE; } torrent_data_init(hlist->data + a, hash); if (!hashlist_item_sync(data, env, line, hlist->data + a++)) { hlist->data[a].hash = NULL; hlist->added = hlist->data + hlist->fill; xmlrpc_DECREF(line); return FALSE; } if (a < hlist->size) hlist->data[a].hash = NULL; } xmlrpc_DECREF(line); } if (found < hlist->fill) { gsize n = hlist->fill - found; if (!hashlist_removed_resize(hlist, n)) { hlist->added = hlist->data + hlist->fill; return FALSE; } r = 0; for (j = hlist->fill - 1; r < n; --j) { if (!hlist->mark[j]) { hlist->removed[r++] = hlist->data[j].hash; torrent_data_free(hlist->data + j); a--; hlist->fill--; memmove(hlist->data + j, hlist->data + j + 1, (a - j) * sizeof(torrent_data_t)); hlist->data[a].hash = NULL; } } hlist->removed[r] = NULL; } hlist->added = hlist->data + hlist->fill; return TRUE; } void noop_destroy(G_GNUC_UNUSED gpointer key) { } void torrent_destroy(gpointer value) { torrent_t* torrent = value; gtk_list_store_remove(torrent->store, &torrent->iter); g_slice_free(torrent_t, torrent); } void torrent_data_init(torrent_data_t* data, const gchar* hash) { memset(data, 0, sizeof(torrent_data_t)); data->hash = hash; data->left = -1; } void torrent_data_free(torrent_data_t* data) { g_free(data->title); } gint64 get_i64_xmlrpc(xmlrpc_env* env, xmlrpc_value* value) { if (env->fault_occurred || !value) { return 0; } switch (xmlrpc_value_type(value)) { #if XMLRPC_HAVE_I8 case XMLRPC_TYPE_I8: { xmlrpc_int64 tmp; xmlrpc_read_i8(env, value, &tmp); xmlrpc_DECREF(value); return tmp; } #endif case XMLRPC_TYPE_INT: default: { xmlrpc_int tmp; xmlrpc_read_int(env, value, &tmp); xmlrpc_DECREF(value); return tmp; } } } gboolean get_bool_xmlrpc(xmlrpc_env* env, xmlrpc_value* value) { if (env->fault_occurred || !value) { return 0; } switch (xmlrpc_value_type(value)) { #if XMLRPC_HAVE_I8 case XMLRPC_TYPE_I8: { xmlrpc_int64 tmp; xmlrpc_read_i8(env, value, &tmp); xmlrpc_DECREF(value); return tmp != 0; } #endif case XMLRPC_TYPE_INT: { xmlrpc_int tmp; xmlrpc_read_int(env, value, &tmp); xmlrpc_DECREF(value); return tmp != 0; } case XMLRPC_TYPE_BOOL: default: { xmlrpc_bool tmp; xmlrpc_read_bool(env, value, &tmp); xmlrpc_DECREF(value); return tmp; } } } void liststore_sort_column_changed(GtkTreeSortable* sortable, gpointer user_data) { master_t* master = user_data; gint column; GtkSortType order; if (gtk_tree_sortable_get_sort_column_id(sortable, &column, &order)) { g_key_file_set_integer(master->config, "view", "sort_column", column + 1); g_key_file_set_boolean(master->config, "view", "sort_desc", order == GTK_SORT_DESCENDING); } else { if (!g_key_file_remove_key(master->config, "view", "sort_column", NULL) && !g_key_file_remove_key(master->config, "view", "sort_desc", NULL)) { return; } } save_config(master); } gint liststore_default_compare_func(GtkTreeModel* model, GtkTreeIter* a, GtkTreeIter* b, G_GNUC_UNUSED gpointer user_data) { GValue value = G_VALUE_INIT; gfloat af, bf; gint ai, bi; const gchar* as, * bs; gtk_tree_model_get_value(model, a, COLUMN_DOWN, &value); af = g_value_get_float(&value); g_value_unset(&value); gtk_tree_model_get_value(model, b, COLUMN_DOWN, &value); bf = g_value_get_float(&value); g_value_unset(&value); if (fabs(af - bf) > 1e-4) { return af > bf ? -1 : 1; } gtk_tree_model_get_value(model, a, COLUMN_UP, &value); af = g_value_get_float(&value); g_value_unset(&value); gtk_tree_model_get_value(model, b, COLUMN_UP, &value); bf = g_value_get_float(&value); g_value_unset(&value); if (fabs(af - bf) > 1e-4) { return af > bf ? -1 : 1; } gtk_tree_model_get_value(model, a, COLUMN_STATE, &value); ai = g_value_get_int(&value); g_value_unset(&value); gtk_tree_model_get_value(model, b, COLUMN_STATE, &value); bi = g_value_get_int(&value); g_value_unset(&value); if (ai != bi) { return ai > bi ? -1 : 1; } gtk_tree_model_get_value(model, a, COLUMN_PROGRESSSORT, &value); af = g_value_get_float(&value); g_value_unset(&value); gtk_tree_model_get_value(model, b, COLUMN_PROGRESSSORT, &value); bf = g_value_get_float(&value); g_value_unset(&value); if (fabs(af - bf) > 1e-3) { return af < bf ? -1 : 1; } gtk_tree_model_get_value(model, a, COLUMN_TITLE, &value); as = g_value_get_string(&value); { GValue value2 = G_VALUE_INIT; gint ret; gtk_tree_model_get_value(model, b, COLUMN_TITLE, &value2); bs = g_value_get_string(&value2); ret = strcmp(as, bs); g_value_unset(&value); g_value_unset(&value2); return ret; } } static gchar* safe_str(gchar* str); gboolean hashlist_item_sync(worker_data_t* data, xmlrpc_env * const env, const xmlrpc_value * const line, torrent_data_t* torrent_data) { do { xmlrpc_value* item; const gchar* tmpstr; gint64 tmpi64; gboolean tmpb; guint64 done, up_rate, down_rate, ratio; g_assert(torrent_data->hash != NULL); /* d.active */ xmlrpc_array_read_item(env, line, 1, &item); tmpb = get_bool_xmlrpc(env, item); if (env->fault_occurred) { break; } if (!tmpb) { /* d.hashing_failed */ xmlrpc_array_read_item(env, line, 2, &item); tmpb = get_bool_xmlrpc(env, item); if (env->fault_occurred) { break; } if (tmpb) { torrent_data->state = STATE_HASHFAILED; } else { torrent_data->state = STATE_DEAD; } } else { /* d.is_hash_checking */ xmlrpc_array_read_item(env, line, 3, &item); tmpb = get_bool_xmlrpc(env, item); if (env->fault_occurred) { break; } if (tmpb) { torrent_data->state = STATE_REHASH; } else { torrent_data->state = STATE_ACTIVE; } } /* d.name */ xmlrpc_array_read_item(env, line, 4, &item); if (!env->fault_occurred) { xmlrpc_read_string(env, item, &tmpstr); } if (item) xmlrpc_DECREF(item); if (env->fault_occurred) { break; } g_free(torrent_data->title); torrent_data->title = (gchar*)tmpstr; torrent_data->title = safe_str(torrent_data->title); /* d.size_bytes */ xmlrpc_array_read_item(env, line, 5, &item); tmpi64 = get_i64_xmlrpc(env, item); if (env->fault_occurred) { break; } torrent_data->size = (guint64)tmpi64; /* d.bytes_done */ xmlrpc_array_read_item(env, line, 6, &item); tmpi64 = get_i64_xmlrpc(env, item); if (env->fault_occurred) { break; } done = (guint64)tmpi64; if (done >= torrent_data->size) { torrent_data->downloaded = 100.0f; } else if (torrent_data->size > 0) { torrent_data->downloaded = ((gfloat)done * 100.0f) / (gfloat)torrent_data->size; } else { torrent_data->downloaded = 0.0f; } /* d.ratio */ xmlrpc_array_read_item(env, line, 7, &item); tmpi64 = get_i64_xmlrpc(env, item); if (env->fault_occurred) { break; } ratio = (guint64)tmpi64; torrent_data->seeded = (gfloat)ratio / 1000.0f; /* d.down.rate */ xmlrpc_array_read_item(env, line, 8, &item); tmpi64 = get_i64_xmlrpc(env, item); if (env->fault_occurred) { break; } down_rate = (guint64)tmpi64; torrent_data->down = (gfloat)down_rate / 1000.0f; /* d.up.rate */ xmlrpc_array_read_item(env, line, 9, &item); tmpi64 = get_i64_xmlrpc(env, item); if (env->fault_occurred) { break; } up_rate = (guint64)tmpi64; torrent_data->up = (gfloat)up_rate / 1000.0f; torrent_data->left = -1; if (torrent_data->downloaded < 100.0f) { if (down_rate >= 512) { torrent_data->left = (torrent_data->size - done) / (down_rate & ~(guint32)511); } } else { gint64 upload_left = -1; if (data->ratio_max > 0) { if (ratio < data->ratio_max) { upload_left = (torrent_data->size * (data->ratio_max - ratio)) / 1000; } else { upload_left = 0; } } if (data->ratio_min > 0) { /* Inexact but good enough */ guint64 uploaded_bytes = (torrent_data->size * ratio) / 1000; if (uploaded_bytes >= data->ratio_upload) { if (ratio < data->ratio_min) { gint64 tmp = (torrent_data->size * (data->ratio_min - ratio)) / 1000; if (upload_left < 0 || tmp < upload_left) { upload_left = tmp; } } else { upload_left = 0; } } else { gint64 tmp = data->ratio_upload - uploaded_bytes; if (upload_left < 0 || tmp < upload_left) { upload_left = tmp; } } } if (upload_left >= 0) { if (up_rate >= 512) { torrent_data->left = (guint64)upload_left / (up_rate & ~(guint32)511); } } } } while (FALSE); if (env->fault_occurred) { worker_respond(data, msg_status("Failed to sync torrent %s (%d): %s", torrent_data->hash, env->fault_code, env->fault_string)); env->fault_occurred = FALSE; torrent_data->state = STATE_DEAD; return FALSE; } return TRUE; } void sync_torrent_state(worker_data_t* data, xmlrpc_env* env, xmlrpc_client* client, xmlrpc_server_info* server, const gchar* hash) { xmlrpc_value* params; state_t state = STATE_DEAD; do { xmlrpc_value* result = NULL, * item; gboolean tmpb; params = xmlrpc_array_new(env); item = xmlrpc_string_new(env, hash); xmlrpc_array_append_item(env, params, item); xmlrpc_DECREF(item); xmlrpc_client_call2(env, client, server, "d.is_active", params, &result); tmpb = get_bool_xmlrpc(env, result); if (env->fault_occurred) { break; } if (!tmpb) { xmlrpc_client_call2(env, client, server, "d.hashing_failed", params, &result); tmpb = get_bool_xmlrpc(env, result); if (env->fault_occurred) { break; } if (tmpb) { state = STATE_HASHFAILED; } else { state = STATE_DEAD; } } else { xmlrpc_client_call2(env, client, server, "d.is_hash_checking", params, &result); tmpb = get_bool_xmlrpc(env, result); if (env->fault_occurred) { break; } if (tmpb) { state = STATE_REHASH; } else { state = STATE_ACTIVE; } } } while (FALSE); xmlrpc_DECREF(params); if (env->fault_occurred) { worker_respond(data, msg_status("Failed to sync torrent %s (%d): %s", hash, env->fault_code, env->fault_string)); env->fault_occurred = FALSE; state = STATE_DEAD; } worker_respond(data, msg_syncstate(hash, state)); } void listselection_changed(GtkTreeSelection* selection, gpointer user_data) { master_t* master = user_data; GtkTreeIter iter; GtkTreeModel* model; if (master->item_lock) { if (!master->connected) { master->item_lock = NULL; } } else if (master->connected && gtk_tree_selection_get_selected(selection, &model, &iter)) { GValue value = G_VALUE_INIT; state_t state; gtk_tree_model_get_value(model, &iter, COLUMN_STATE, &value); state = (state_t)g_value_get_int(&value); g_value_unset(&value); gtk_action_set_sensitive(master->startaction, state == STATE_DEAD || state == STATE_HASHFAILED); gtk_action_set_sensitive(master->stopaction, state != STATE_REHASH); gtk_action_set_sensitive(master->rehashaction, state != STATE_REHASH); return; } gtk_action_set_sensitive(master->startaction, FALSE); gtk_action_set_sensitive(master->stopaction, FALSE); gtk_action_set_sensitive(master->rehashaction, FALSE); } void do_viewtoolbaritem(GtkCheckMenuItem* checkmenuitem, gpointer data) { master_t* master = data; gboolean active = gtk_check_menu_item_get_active(checkmenuitem); if (active != gtk_widget_get_visible(GTK_WIDGET(master->toolbar))) { if (active) { gtk_widget_show_all(GTK_WIDGET(master->toolbar)); } else { gtk_widget_hide(GTK_WIDGET(master->toolbar)); } g_key_file_set_boolean(master->config, "view", "toolbar", active); save_config(master); } } void do_about(GtkMenuItem* G_GNUC_UNUSED menuitem, gpointer data) { master_t* master = data; gtk_widget_show_all(master->aboutdlg); } static void view_contextmenu(GtkWidget* treeview, GdkEventButton* event, gpointer data) { master_t* master = data; gtk_widget_show_all(master->torrentmenu); gtk_menu_popup(GTK_MENU(master->torrentmenu), NULL, NULL, NULL, NULL, event ? event->button : 0, gdk_event_get_time((GdkEvent*)event)); } gboolean do_popupmenu(GtkWidget* treeview, gpointer data) { view_contextmenu(treeview, NULL, data); return TRUE; } gboolean do_buttonpressed(GtkWidget *treeview, GdkEventButton *event, gpointer data) { if (event->type == GDK_BUTTON_PRESS && event->button == 3) { master_t* master = data; GtkTreePath *path; if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview), (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL)) { gtk_tree_selection_unselect_all(master->listselection); gtk_tree_selection_select_path(master->listselection, path); gtk_tree_path_free(path); } view_contextmenu(treeview, event, data); return TRUE; } return FALSE; } gchar* safe_str(gchar* str) { gchar* pos; gchar* ret = NULL; gsize size = 0, alloc = 0; pos = strchr(str, '&'); if (pos == NULL) { return str; } size = strlen(str); alloc = size + 10; ret = g_malloc(alloc); if (ret == NULL) { return str; } memcpy(ret, str, size + 1); pos = ret + (pos - str); g_free(str); for (;;) { gchar* last; if (size + 5 >= alloc) { size_t p = pos - ret; size_t na = alloc * 2; gchar* tmp; if (na < size + 10) { na = size + 10; } tmp = g_realloc(ret, na); if (tmp == NULL) { break; } ret = tmp; alloc = na; pos = ret + p; } last = pos + 5; memmove(last, pos + 1, size - (pos - ret)); memcpy(pos + 1, "amp;", 4); size += 4; pos = strchr(last, '&'); if (pos == NULL) { break; } } return ret; }