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: deepcount.c, 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 thunar-job.c and 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)
 
preparation/job-framework.txt · Last modified: 2009/04/08 17:49 by jannis
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki