/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Author : * Peter Williams (peterw@helixcode.com) * * Copyright 2000, Helix Code, Inc. (http://www.helixcode.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA. * */ #include #include #include #include #include #include "folder-browser-factory.h" #include "camel/camel-object.h" #include "mail.h" #include "mail-threads.h" #define DEBUG(p) g_print p /** * A function and its userdata **/ typedef struct closure_s { gpointer in_data; gboolean free_in_data; gpointer op_data; const mail_operation_spec *spec; CamelException *ex; gchar *infinitive; gchar *gerund; } closure_t; /** * A command issued through the compipe **/ typedef struct com_msg_s { enum com_msg_type_e { STARTING, #if 0 PERCENTAGE, HIDE_PBAR, SHOW_PBAR, #endif MESSAGE, PASSWORD, ERROR, FORWARD_EVENT, FINISHED } type; gfloat percentage; gchar *message; closure_t *clur; /* Password stuff */ gchar **reply; gboolean secret; gboolean *success; /* Event stuff */ CamelObjectEventHookFunc event_hook; CamelObject *event_obj; gpointer event_event_data; gpointer event_user_data; } com_msg_t; /** * Stuff needed for blocking **/ typedef struct block_info_s { GMutex *mutex; GCond *cond; gboolean val; } block_info_t; #define BLOCK_INFO_INIT { NULL, NULL, FALSE } /** * @dispatch_thread_started: gboolean that tells us whether * the dispatch thread has been launched. **/ static gboolean dispatch_thread_started = FALSE; /** * @queue_len : the number of operations pending * and being executed. * * Because camel is not thread-safe we work * with the restriction that more than one mailbox * cannot be accessed at once. Thus we cannot * concurrently check mail and move messages, etc. **/ static gint queue_len = 0; /** * @main_compipe: The pipe through which the dispatcher communicates * with the main thread for GTK+ calls * * @chan_reader: the GIOChannel that reads our pipe * * @MAIN_READER: the fd in our main pipe that.... reads! * @MAIN_WRITER: the fd in our main pipe that.... writes! */ #define MAIN_READER main_compipe[0] #define MAIN_WRITER main_compipe[1] #define DISPATCH_READER dispatch_compipe[0] #define DISPATCH_WRITER dispatch_compipe[1] static int main_compipe[2] = { -1, -1 }; static int dispatch_compipe[2] = { -1, -1 }; GIOChannel *chan_reader = NULL; /** * @modal_block: a condition maintained so that the * calling thread (the dispatch thread) blocks correctly * until the user has responded to some kind of modal * dialog boxy thing. */ static block_info_t modal_block = BLOCK_INFO_INIT; /** * @finish_block: A condition so that the dispatch thread * blocks until the main thread has finished the cleanup. **/ static block_info_t finish_block = BLOCK_INFO_INIT; /** * @current_message: The current message for the status bar. * @busy_status: Whether we are currently busy doing some async operation, * for status bar purposes. */ static char *current_message = NULL; static gboolean busy = FALSE; /** * Static prototypes **/ static void ui_set_busy (void); static void ui_unset_busy (void); static void ui_set_message (const char *message); static void ui_unset_message (void); static void block_prepare (block_info_t *info); static void block_wait (block_info_t *info); static void block_hold (block_info_t *info); static void block_release (block_info_t *info); static void *dispatch (void * data); static void check_dispatcher (void); static void check_compipes (void); static gboolean read_msg (GIOChannel * source, GIOCondition condition, gpointer userdata); static void show_error (com_msg_t * msg); static void get_password (com_msg_t * msg); static void get_password_cb (gchar * string, gpointer data); static void cleanup_op (com_msg_t * msg); static closure_t *new_closure (const mail_operation_spec * spec, gpointer input, gboolean free_in_data); static void free_closure (closure_t *clur); /* Pthread code */ /* FIXME: support other thread types!!!! */ #ifdef G_THREADS_IMPL_POSIX #include /** * @dispatch_thread: the pthread_t (when using pthreads, of * course) representing our dispatcher routine. Never used * except to make pthread_create happy **/ static pthread_t dispatch_thread; /* FIXME: do we need to set any attributes for our thread? * If so, we need to create a pthread_attr structure and * fill it in somewhere. But the defaults should be good * enough. */ #elif defined( G_THREADS_IMPL_SOLARIS ) #include static thread_t dispatch_thread; #else /* no supported thread impl */ void f (void) { Error_No_supported_thread_implementation_recognized (); choke on this; } #endif static int pipe_write (int fd, const void *buf, size_t count) { size_t res; do { res = write (fd, buf, count); } while (res == -1 && errno == EINTR); return res; } static size_t pipe_read (int fd, void *buf, size_t count) { size_t res; do { res = read (fd, buf, count); } while (res == -1 && errno == EINTR); return res; } /** * mail_operation_queue: * @spec: describes the operation to be performed * @input: input data for the operation. * * Runs a mail operation asynchronously. If no other operation is running, * we start another thread and call the callback in that thread. The function * can then use the mail_op_ functions to perform limited UI returns, while * the main UI is completely unlocked. * * If an async operation is going on when this function is called again, * it waits for the currently executing operation to finish, then * executes the callback function in another thread. * * Returns TRUE on success, FALSE on some sort of queueing error. **/ gboolean mail_operation_queue (const mail_operation_spec * spec, gpointer input, gboolean free_in_data) { closure_t *clur; g_assert (spec); clur = new_closure (spec, input, free_in_data); if (spec->setup) (spec->setup) (clur->in_data, clur->op_data, clur->ex); if (camel_exception_is_set (clur->ex)) { if (clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) { GtkWidget *err_dialog; gchar *msg; msg = g_strdup_printf (_("Error while preparing to %s:\n" "%s"), clur->infinitive, camel_exception_get_description (clur->ex)); err_dialog = gnome_error_dialog (msg); g_free (msg); gnome_dialog_set_close (GNOME_DIALOG (err_dialog), TRUE); gnome_dialog_run_and_close (GNOME_DIALOG (err_dialog)); g_warning ("Setup failed for `%s': %s", clur->infinitive, camel_exception_get_description (clur-> ex)); } free_closure (clur); return FALSE; } if (queue_len == 0) { check_compipes (); check_dispatcher (); } /* else add self to queue */ pipe_write (DISPATCH_WRITER, clur, sizeof (closure_t)); /* dispatch allocates a separate buffer * to hold the closure; it's in the pipe and * can safely be freed */ g_free (clur); queue_len++; return TRUE; } #if 0 /** * mail_op_set_percentage: * @percentage: the percentage that will be displayed in the progress bar * * Set the percentage of the progress bar for the currently executing operation. * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ void mail_op_set_percentage (gfloat percentage) { com_msg_t msg; msg.type = PERCENTAGE; msg.percentage = percentage; pipe_write (MAIN_WRITER, &msg, sizeof (msg)); } /** * mail_op_hide_progressbar: * * Hide the progress bar in the status box * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ void mail_op_hide_progressbar (void) { com_msg_t msg; msg.type = HIDE_PBAR; pipe_write (MAIN_WRITER, &msg, sizeof (msg)); } /** * mail_op_show_progressbar: * * Show the progress bar in the status box * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ void mail_op_show_progressbar (void) { com_msg_t msg; msg.type = SHOW_PBAR; pipe_write (MAIN_WRITER, &msg, sizeof (msg)); } #endif /** * mail_op_set_message: * @str: message * * Set the message displayed above the progress bar for the currently * executing operation. * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ static void set_message (gchar *str) { com_msg_t msg; msg.type = MESSAGE; msg.message = str; pipe_write (MAIN_WRITER, &msg, sizeof (msg)); } void mail_op_set_message_plain (const gchar *str) { set_message (g_strdup (str)); } /** * mail_op_set_message: * @fmt: printf-style format string for the message * @...: arguments to the format string * * Set the message displayed above the progress bar for the currently * executing operation. * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ void mail_op_set_message (const gchar *fmt, ...) { gchar *str; va_list val; va_start (val, fmt); str = g_strdup_vprintf (fmt, val); va_end (val); set_message (str); } /** * mail_op_get_password: * @prompt: the question put to the user * @secret: whether the dialog box shold print stars when the user types * @dest: where to store the reply * * Asks the user for a password (or string entry in general). Waits for * the user's response. On success, returns TRUE and @dest contains the * response. On failure, returns FALSE and @dest contains the error * message. **/ gboolean mail_op_get_password (gchar * prompt, gboolean secret, gchar ** dest) { com_msg_t msg; gboolean result; msg.type = PASSWORD; msg.secret = secret; msg.message = prompt; msg.reply = dest; msg.success = &result; (*dest) = NULL; block_prepare (&modal_block); pipe_write (MAIN_WRITER, &msg, sizeof (msg)); block_wait (&modal_block); return result; } /** * mail_op_error: * @fmt: printf-style format string for the error * @...: arguments to the format string * * Opens an error dialog for the currently executing operation. * Threadsafe for, nay, intended to be called by, the dispatching thread. **/ void mail_op_error (gchar * fmt, ...) { com_msg_t msg; va_list val; va_start (val, fmt); msg.type = ERROR; msg.message = g_strdup_vprintf (fmt, val); va_end (val); block_prepare (&modal_block); pipe_write (MAIN_WRITER, &msg, sizeof (msg)); block_wait (&modal_block); } /** * mail_op_forward_event: * * Communicate a camel event over to the main thread. **/ void mail_op_forward_event (CamelObjectEventHookFunc func, CamelObject *o, gpointer event_data, gpointer user_data) { com_msg_t msg; msg.type = FORWARD_EVENT; msg.event_hook = func; msg.event_obj = o; msg.event_event_data = event_data; msg.event_user_data = user_data; pipe_write (MAIN_WRITER, &msg, sizeof (msg)); } /** * mail_operation_wait_for_finish: * * Waits for the currently executing async operations * to finish executing */ void mail_operation_wait_for_finish (void) { while (queue_len) gtk_main_iteration (); /* Sigh. Otherwise we deadlock upon exit. */ GDK_THREADS_LEAVE (); } /** * mail_operations_are_executing: * * Returns TRUE if operations are being executed asynchronously * when called, FALSE if not. **/ gboolean mail_operations_are_executing (void) { return (queue_len > 0); } /** * mail_operations_terminate: * * Let the operations finish then terminate the dispatch thread **/ void mail_operations_terminate (void) { closure_t clur; mail_operation_wait_for_finish(); memset (&clur, 0, sizeof (closure_t)); clur.spec = NULL; pipe_write (DISPATCH_WRITER, &clur, sizeof (closure_t)); close (DISPATCH_WRITER); close (MAIN_READER); } void mail_operations_get_status (int *busy_return, const char **message_return) { *busy_return = busy; *message_return = current_message; } /* ** Static functions **************************************************** */ static void check_dispatcher (void) { int res; if (dispatch_thread_started) return; #if defined( G_THREADS_IMPL_POSIX ) res = pthread_create (&dispatch_thread, NULL, (void *) &dispatch, NULL); #elif defined( G_THREADS_IMPL_SOLARIS ) res = thr_create (NULL, 0, (void *) &dispatch, NULL, 0, &dispatch_thread); #else /* no known impl */ Error_No_thread_create_implementation (); choke on this; #endif if (res != 0) { g_warning ("Error launching dispatch thread!"); /* FIXME: more error handling */ } else dispatch_thread_started = TRUE; } /** * check_compipes: * * Check and see if our pipe has been opened and open * it if necessary. **/ static void check_compipes (void) { if (MAIN_READER < 0) { if (pipe (main_compipe) < 0) { g_warning ("Call to pipe(2) failed!"); /* FIXME: better error handling. How do we react? */ return; } chan_reader = g_io_channel_unix_new (MAIN_READER); g_io_add_watch (chan_reader, G_IO_IN, read_msg, NULL); } if (DISPATCH_READER < 0) { if (pipe (dispatch_compipe) < 0) { g_warning ("Call to pipe(2) failed!"); /* FIXME: better error handling. How do we react? */ return; } } } /** * dispatch: * @clur: The operation to execute and its parameters * * Start a thread that executes the closure and exit * it when done. */ static void * dispatch (void *unused) { size_t len; closure_t *clur; com_msg_t msg; /* Let the compipes be created */ sleep (1); while (1) { clur = g_new (closure_t, 1); len = pipe_read (DISPATCH_READER, clur, sizeof (closure_t)); if (len <= 0) break; if (len != sizeof (closure_t)) { g_warning ("dispatcher: Didn't read full message!"); continue; } if (clur->spec == NULL) break; msg.type = STARTING; msg.message = g_strdup (clur->gerund); pipe_write (MAIN_WRITER, &msg, sizeof (msg)); (clur->spec->callback) (clur->in_data, clur->op_data, clur->ex); if (camel_exception_is_set (clur->ex)) { if (clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) { g_warning ("Callback failed for `%s': %s", clur->infinitive, camel_exception_get_description (clur-> ex)); mail_op_error (_("Error while `%s':\n%s"), clur->gerund, camel_exception_get_description (clur-> ex)); } } msg.type = FINISHED; msg.clur = clur; /* Wait for the cleanup to finish before starting our next op */ block_prepare (&finish_block); pipe_write (MAIN_WRITER, &msg, sizeof (msg)); block_wait (&finish_block); } close (DISPATCH_READER); close (MAIN_WRITER); #ifdef G_THREADS_IMPL_POSIX pthread_exit (0); #elif defined( G_THREADS_IMPL_SOLARIS ) thr_exit (NULL); #else /* no known impl */ Error_No_thread_exit_implemented (); choke on this; #endif return NULL; /*NOTREACHED*/ } /** * read_msg: * @source: the channel that has data to read * @condition: the reason we were called * @userdata: unused * * A message has been recieved on our pipe; perform the appropriate * action. **/ static gboolean read_msg (GIOChannel * source, GIOCondition condition, gpointer userdata) { com_msg_t *msg; guint size; msg = g_new0 (com_msg_t, 1); g_io_channel_read (source, (gchar *) msg, sizeof (com_msg_t) / sizeof (gchar), &size); if (size != sizeof (com_msg_t)) { g_warning (_("Incomplete message written on pipe!")); msg->type = ERROR; msg->message = g_strdup (_ ("Error reading commands from dispatching thread.")); } /* This is very important, though I'm not quite sure why * it is as we are in the main thread right now. */ /*g_message ("DLG: IN: read_msg");*/ switch (msg->type) { case STARTING: DEBUG (("*** Message -- STARTING %s\n", msg->message)); ui_set_message (msg->message); ui_set_busy (); g_free (msg->message); break; #if 0 case PERCENTAGE: DEBUG (("*** Message -- PERCENTAGE\n")); g_warning ("PERCENTAGE operation unsupported"); break; case HIDE_PBAR: DEBUG (("*** Message -- HIDE_PBAR\n")); g_warning ("HIDE_PBAR operation unsupported"); break; case SHOW_PBAR: DEBUG (("*** Message -- SHOW_PBAR\n")); g_warning ("HIDE_PBAR operation unsupported"); break; #endif case MESSAGE: DEBUG (("*** Message -- MESSAGE\n")); ui_set_message (msg->message); g_free (msg->message); break; case PASSWORD: DEBUG (("*** Message -- PASSWORD\n")); g_assert (msg->reply); g_assert (msg->success); get_password (msg); break; case ERROR: DEBUG (("*** Message -- ERROR\n")); show_error (msg); break; /* Don't fall through; dispatch_func does the FINISHED * call for us */ case FORWARD_EVENT: DEBUG (("*** Message -- FORWARD_EVENT %p\n", msg->event_hook)); g_assert (msg->event_hook); (msg->event_hook) (msg->event_obj, msg->event_event_data, msg->event_user_data); break; case FINISHED: DEBUG (("*** Message -- FINISH %s\n", msg->clur->gerund)); cleanup_op (msg); break; default: g_warning (_("Corrupted message from dispatching thread?")); break; } /*g_message ("DLG: OUT: read_msg");*/ g_free (msg); return TRUE; } /** * cleanup_op: * * Cleanup after a finished operation **/ static void cleanup_op (com_msg_t * msg) { block_hold (&finish_block); /* Run the cleanup */ if (msg->clur->spec->cleanup) (msg->clur->spec->cleanup) (msg->clur->in_data, msg->clur->op_data, msg->clur->ex); /* Tell the dispatch thread that it can start * the next operation */ block_release (&finish_block); /* Print an exception if the cleanup caused one */ if (camel_exception_is_set (msg->clur->ex) && msg->clur->ex->id != CAMEL_EXCEPTION_USER_CANCEL) { g_warning ("Error on cleanup of `%s': %s", msg->clur->infinitive, camel_exception_get_description (msg->clur->ex)); } free_closure (msg->clur); queue_len--; ui_unset_busy (); ui_unset_message (); } /** * show_error: * * Show the error dialog and wait for user OK **/ static void show_error (com_msg_t * msg) { GtkWidget *err_dialog; /* Create the dialog */ err_dialog = gnome_error_dialog (msg->message); g_free (msg->message); /* Stop the other thread until the user reacts */ ui_unset_busy (); block_hold (&modal_block); /* Show the dialog. */ /* Do not GDK_THREADS_ENTER; we're inside the read_msg * handler which takes care of this for us. Oh, if * only GDK_THREADS_ENTER were recursive... */ gnome_dialog_run_and_close (GNOME_DIALOG (err_dialog)); /* Allow the other thread to proceed */ block_release (&modal_block); ui_set_busy (); } static void focus_on_entry(GtkWidget *widget, gpointer user_data) { if (GTK_IS_ENTRY(widget)) { gtk_widget_grab_focus(widget); } } /** * get_password: * * Ask for a password and put the answer in *(msg->reply) **/ static void get_password (com_msg_t * msg) { GtkWidget *dialog; int button; /* Create the dialog */ dialog = gnome_request_dialog (msg->secret, msg->message, NULL, 0, get_password_cb, msg, NULL); /* Stop the other thread */ ui_unset_busy (); block_hold (&modal_block); /* Show the dialog (or report an error) */ if (dialog == NULL) { *(msg->success) = FALSE; *(msg->reply) = g_strdup (_("Could not create dialog box.")); button = -1; } else { e_container_foreach_leaf (GTK_CONTAINER (dialog), focus_on_entry, NULL); *(msg->reply) = NULL; button = gnome_dialog_run_and_close (GNOME_DIALOG (dialog)); } if (button == 1 || *(msg->reply) == NULL) { *(msg->success) = FALSE; *(msg->reply) = g_strdup (_("User cancelled query.")); } else if (button >= 0) { *(msg->success) = TRUE; } /* Allow the other thread to proceed */ block_release (&modal_block); ui_set_busy (); } static void get_password_cb (gchar * string, gpointer data) { com_msg_t *msg = (com_msg_t *) data; if (string) *(msg->reply) = g_strdup (string); else *(msg->reply) = NULL; } static closure_t * new_closure (const mail_operation_spec * spec, gpointer input, gboolean free_in_data) { closure_t *clur; clur = g_new0 (closure_t, 1); clur->spec = spec; clur->in_data = input; clur->free_in_data = free_in_data; clur->ex = camel_exception_new (); clur->op_data = g_malloc (spec->datasize); camel_exception_init (clur->ex); clur->infinitive = (spec->describe) (input, FALSE); clur->gerund = (spec->describe) (input, TRUE); return clur; } static void free_closure (closure_t *clur) { clur->spec = NULL; if (clur->free_in_data) g_free (clur->in_data); clur->in_data = NULL; g_free (clur->op_data); clur->op_data = NULL; camel_exception_free (clur->ex); clur->ex = NULL; g_free (clur->infinitive); g_free (clur->gerund); g_free (clur); } /* ******************** */ /** * * Thread A calls block_prepare * Thread A causes thread B to do something * Thread A calls block_wait * Thread A continues when thread B calls block_release * * Thread B gets thread A's message * Thread B calls block_hold * Thread B does something * Thread B calls block_release * **/ static void block_prepare (block_info_t *info) { if (info->cond == NULL) { info->cond = g_cond_new (); info->mutex = g_mutex_new (); } g_mutex_lock (info->mutex); info->val = FALSE; } static void block_wait (block_info_t *info) { g_assert (info->cond); while (info->val == FALSE) g_cond_wait (info->cond, info->mutex); g_mutex_unlock (info->mutex); } static void block_hold (block_info_t *info) { g_assert (info->cond); g_mutex_lock (info->mutex); info->val = FALSE; } static void block_release (block_info_t *info) { g_assert (info->cond); info->val = TRUE; g_cond_signal (info->cond); g_mutex_unlock (info->mutex); } /* ******************** */ /* FIXME FIXME FIXME This is a totally evil hack. */ static GNOME_Evolution_ShellView retrieve_shell_view_interface_from_control (BonoboControl *control) { Bonobo_ControlFrame control_frame; GNOME_Evolution_ShellView shell_view_interface; CORBA_Environment ev; control_frame = bonobo_control_get_control_frame (control); if (control_frame == NULL) return CORBA_OBJECT_NIL; CORBA_exception_init (&ev); shell_view_interface = Bonobo_Unknown_queryInterface (control_frame, "IDL:GNOME/Evolution/ShellView:1.0", &ev); CORBA_exception_free (&ev); if (shell_view_interface != CORBA_OBJECT_NIL) gtk_object_set_data (GTK_OBJECT (control), "mail_threads_shell_view_interface", shell_view_interface); else g_warning ("Control frame doesn't have Evolution/ShellView."); return shell_view_interface; } static void update_active_views (void) { EList *controls; EIterator *it; controls = folder_browser_factory_get_control_list (); for (it = e_list_get_iterator (controls); e_iterator_is_valid (it); e_iterator_next (it)) { BonoboControl *control; GNOME_Evolution_ShellView shell_view_interface; CORBA_Environment ev; control = BONOBO_CONTROL (e_iterator_get (it)); shell_view_interface = gtk_object_get_data (GTK_OBJECT (control), "mail_threads_shell_view_interface"); if (shell_view_interface == CORBA_OBJECT_NIL) shell_view_interface = retrieve_shell_view_interface_from_control (control); CORBA_exception_init (&ev); if (shell_view_interface != CORBA_OBJECT_NIL) { if (current_message == NULL && ! busy) { GNOME_Evolution_ShellView_unsetMessage (shell_view_interface, &ev); } else { if (current_message == NULL) GNOME_Evolution_ShellView_setMessage (shell_view_interface, "", busy, &ev); else GNOME_Evolution_ShellView_setMessage (shell_view_interface, current_message, busy, &ev); } } CORBA_exception_free (&ev); } } static void ui_set_busy (void) { busy = TRUE; update_active_views (); } static void ui_unset_busy (void) { busy = FALSE; update_active_views (); } static void ui_set_message (const char *message) { g_free (current_message); current_message = g_strdup (message); update_active_views (); } static void ui_unset_message (void) { g_free (current_message); current_message = NULL; update_active_views (); }