====== Job Framework ====== ===== Differences In ThunarVFS And GIO ===== ThunarVFS has a job framework modelled around ''ThunarVfsJob''. While this is very convenient, it differs quite a lot from how GIO handles complex, possibly blocking tasks. Copying a file or directory using ''ThunarVFS'' is done with thunar_vfs_copy_file (source_path, target_path, &error) which calls ''thunar_vfs_copy_files()'' which in turn returns a ''ThunarVfsTransferJob''. The job is launched in a separate thread and reports back to the application using signals. In GIO, copying a file is done with g_file_copy_async (source_file, destination_file, G_FILE_COPY_NONE, 0, cancellable, progress_callback, progress_user_data, finish_callback, finish_user_data) which uses the ''GIOScheduler'' job framework internally and reports back to the application using the progress and finish callbacks. (Unfortunately this function doesn't do recursive copies, so we need something on top of it). ===== How To Design Jobs In Thunar ===== Let's take ''ThunarVfsDeepCountJob'' as an example here. ==== GIO-style Jobs ==== I've written an experimental implementation of it based on ''GIOScheduler'' and ''GSimpleAsyncResult'': {{:preparation:deepcount.c|}}, {{:preparation:Makefile.txt}}. This turns the job into a simple function call: static void g_file_deep_count_async (GFile *file, int io_priority, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, GAsyncReadyCallback callback, gpointer callback_data) The ''progress_callback'' is executed in the GUI thread to avoids threading issues. This makes it easy to hook into jobs e.g. in order to ask the user whether he wants to overwrite a file. All other jobs can be implemented in the same way. However, it would be nice to reduce the amount of boilerplate code involved for similar job types. This kind of job API is also not very object-oriented and doesn't involve objects and signals the way we're used to. Should we ever decided to move away from GIO this design might become a problem. ==== Abstraction Layer On Top Of GIO ==== So what we really want is an abstraction layer on top of GIO-style jobs, implemented using GObject and GTypeInterface. We don't want to pass dozens of callbacks to complex jobs. Instead, we want jobs to have signals and we want to connect to these. Internally, we can still use GIO-style jobs based on asynchronous functions, so we don't have to worry about threading and scheduling. Similar to ''ThunarVfsJob'' and ''ThunarVfsDeepCountJob'' we want: === ThunarJob (Draft) === A base class: ''ThunarJob'' with common signals like these "error", "finished", "status-message", "ask", "ask-replace", "progress" and "new-files". To create jobs, there should be a number of functions like ThunarJob *thunar_job_new_deep_count_new (GFile *file) ThunarJob *thunar_job_new_copy_file (GFile *source, GFile *destination) ThunarJob *thunar_job_new_copy_files (GList *source_files, GList *destination_files) Internally, these could look like this: ThunarJob * thunar_job_new_deep_count (GFile *file) { ThunarJob *job; GList *source_files = NULL; g_return_val_if_fail (G_IS_FILE (file), NULL); source_files = g_list_append (source_files, file); job = g_object_new (THUNAR_TYPE_JOB, "source-files", source_files, "job-function", thunar_job_deep_count, NULL); g_list_free (source_files); return job; } where ''thunar_job_deep_count'' is a ''ThunarJobFunc'': typedef gboolean (*ThunarJobFunc) (ThunarJobData *data); and ''ThunarJobData'' is a special ''struct'' for jobs: typedef struct { GIOSchedulerJob *gio_job; GCancellable *cancellable; ThunarJob *job; GError **error; GList *source_files; GList *target_files; } ThunarJobData; ''ThunarJob'' can create and run all jobs in the same fashion. The public API is reduced to the very minimum: void thunar_job_launch (ThunarJob *job); void thunar_job_cancel (ThunarJob *job); void thunar_job_emit_status_message (ThunarJobData *data const gchar *message); ThunarJobResponse thunar_job_emit_ask (ThunarJobData *job, const gchar *message, ThunarJobResponse choices); ... I've written a simplified sample implementation in {{:preparation:thunar-job.c|}} and {{:preparation:thunar-job.h|}}. === Specific Job Implementation === Here's a quick and dirty implementation of the job created with ''thunar_job_new_deep_count()''. static gboolean thunar_job_real_deep_count (ThunarJobData *data, GFile *file, goffset *num_files, goffset *num_bytes) { GFileEnumerator *enumerator; GFileInfo *info; GFileInfo *child_info; GFile *child; gboolean success = TRUE; gchar *message; g_return_val_if_fail (G_IS_FILE (file), FALSE); info = g_file_query_info (file, "standard::*", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, data->cancellable, data->error); if (g_cancellable_is_cancelled (data->cancellable)) return FALSE; if (info == NULL) return FALSE; *num_files += 1; *num_bytes += g_file_info_get_size (info); message = g_strdup_printf ("%lld files, %.2f MB", *num_files, *num_bytes / 1024.0 / 1024.0); thunar_job_emit_status_message (data, message); g_free (message); if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { enumerator = g_file_enumerate_children (file, "standard::*", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, data->cancellable, data->error); if (!g_cancellable_is_cancelled (data->cancellable)) { if (enumerator != NULL) { while (!g_cancellable_is_cancelled (data->cancellable) && success) { child_info = g_file_enumerator_next_file (enumerator, data->cancellable, data->error); if (g_cancellable_is_cancelled (data->cancellable)) break; if (child_info == NULL) { if (*(data->error) != NULL) success = FALSE; break; } child = g_file_resolve_relative_path (file, g_file_info_get_name (child_info)); success = success && thunar_job_real_deep_count (data, child, num_files, num_bytes); g_object_unref (child); g_object_unref (child_info); } g_object_unref (enumerator); } } } g_object_unref (info); return !g_cancellable_is_cancelled (data->cancellable) && success; } static gboolean thunar_job_deep_count (ThunarJobData *data) { GFile *file; gboolean success; goffset num_files = 0; goffset num_bytes = 0; g_return_val_if_fail (data != NULL, FALSE); g_return_val_if_fail (THUNAR_IS_JOB (data->job), FALSE); g_return_val_if_fail (g_list_length (data->source_files) == 1, FALSE); if (g_cancellable_set_error_if_cancelled (data->cancellable, data->error)) return FALSE; file = g_list_first (data->source_files)->data; g_assert (G_IS_FILE (file)); success = thunar_job_real_deep_count (data, file, &num_files, &num_bytes); if (*(data->error) != NULL && g_cancellable_is_cancelled (data->cancellable)) { g_error_free (*(data->error)); *(data->error) = NULL; } if (g_cancellable_set_error_if_cancelled (data->cancellable, data->error)) return FALSE; else return success; } ThunarJob * thunar_job_new_deep_count (GFile *file) { ThunarJob *job; GList *source_files = NULL; g_return_val_if_fail (G_IS_FILE (file), NULL); source_files = g_list_append (source_files, file); job = g_object_new (THUNAR_TYPE_JOB, "source-files", source_files, "job-function", thunar_job_deep_count, NULL); g_list_free (source_files); return job; } And here's another dirty non-recursive implementation of ''g_file_copy()'' based on ''ThunarJob'' (as ''thunar_job_new_copy_file()''). It uses a simplified version of the "ask" signal to demonstrate that all kinds of signals work even from within asynchronous jobs: static void thunar_job_copy_file_progress (goffset current_num_bytes, goffset total_num_bytes, ThunarJobData *data) { gchar *message; message = g_strdup_printf ("%lld / %lld bytes", current_num_bytes, total_num_bytes); thunar_job_emit_status_message (data, message); g_free (message); } static gboolean thunar_job_copy_file (ThunarJobData *data) { GFile *source_file; GFile *target_file; gboolean success; source_file = g_list_first (data->source_files)->data; target_file = g_list_first (data->target_files)->data; if (!g_file_copy (source_file, target_file, G_FILE_COPY_NONE, data->cancellable, (GFileProgressCallback) thunar_job_copy_file_progress, data, data->error)) { if ((*(data->error))->code != G_IO_ERROR_EXISTS) return FALSE; if (!thunar_job_emit_ask (data)) return FALSE; g_error_free (*(data->error)); *(data->error) = NULL; return g_file_copy (source_file, target_file, G_FILE_COPY_OVERWRITE, data->cancellable, (GFileProgressCallback) thunar_job_copy_file_progress, data, data->error); } else return TRUE; } ThunarJob * thunar_job_new_copy_file (GFile *source_file, GFile *target_file) { ThunarJob *job; GList *source_files = NULL; GList *target_files = NULL; source_files = g_list_append (source_files, source_file); target_files = g_list_append (target_files, target_file); job = g_object_new (THUNAR_TYPE_JOB, "source-files", source_files, "target-files", target_files, "job-function", thunar_job_copy_file, NULL); g_list_free (source_files); g_list_free (target_files); return job; } ==== Job Classes ==== Of course ''ThunarJob'' can be subclassed for special purposes. All in all, we need the following classes to cover the functionality present in ThunarVFS: * ''ThunarJob'' (base class) * ''ThunarSimpleJob'' (derived from ''ThunarJob'', making one-function jobs easy to create) * ''ThunarDeepCountJob'' (derived from ''ThunarJob'' with an additional "status-ready" signal) * ''ThunarTransferJob'' (derived from ''ThunarJob'' for copying/moving files)