/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2006-2007 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. * * Author: David Zeuthen */ #include #include #include #include #include #include #include #include #include "ggphoto2volumemonitor.h" #include "ggphoto2volume.h" #ifdef HAVE_GUDEV #include #else #include "hal-pool.h" #endif G_LOCK_DEFINE_STATIC(hal_vm); static GGPhoto2VolumeMonitor *the_volume_monitor = NULL; #ifndef HAVE_GUDEV static HalPool *pool = NULL; #endif struct _GGPhoto2VolumeMonitor { GNativeVolumeMonitor parent; GUnixMountMonitor *mount_monitor; #ifdef HAVE_GUDEV GUdevClient *gudev_client; #else HalPool *pool; #endif GList *last_camera_devices; GList *camera_volumes; }; #ifdef HAVE_GUDEV static void on_uevent (GUdevClient *client, gchar *action, GUdevDevice *device, gpointer user_data); #else static void hal_changed (HalPool *pool, HalDevice *device, gpointer user_data); static void update_all (GGPhoto2VolumeMonitor *monitor, gboolean emit_changes); static void update_cameras (GGPhoto2VolumeMonitor *monitor, GList **added_volumes, GList **removed_volumes); #endif static GList* get_stores_for_camera (const char *bus_num, const char *device_num); G_DEFINE_TYPE (GGPhoto2VolumeMonitor, g_gphoto2_volume_monitor, G_TYPE_VOLUME_MONITOR) static void list_free (GList *objects) { g_list_foreach (objects, (GFunc)g_object_unref, NULL); g_list_free (objects); } #ifndef HAVE_GUDEV static HalPool * get_hal_pool (void) { char *cap_only[] = {"camera", "portable_audio_player", "usb_device", NULL}; if (pool == NULL) pool = hal_pool_new (cap_only); return pool; } #endif static void g_gphoto2_volume_monitor_dispose (GObject *object) { G_LOCK (hal_vm); the_volume_monitor = NULL; G_UNLOCK (hal_vm); if (G_OBJECT_CLASS (g_gphoto2_volume_monitor_parent_class)->dispose) (*G_OBJECT_CLASS (g_gphoto2_volume_monitor_parent_class)->dispose) (object); } static void g_gphoto2_volume_monitor_finalize (GObject *object) { GGPhoto2VolumeMonitor *monitor; monitor = G_GPHOTO2_VOLUME_MONITOR (object); #ifdef HAVE_GUDEV g_signal_handlers_disconnect_by_func (monitor->gudev_client, on_uevent, monitor); g_object_unref (monitor->gudev_client); #else g_signal_handlers_disconnect_by_func (monitor->pool, hal_changed, monitor); g_object_unref (monitor->pool); #endif list_free (monitor->last_camera_devices); list_free (monitor->camera_volumes); if (G_OBJECT_CLASS (g_gphoto2_volume_monitor_parent_class)->finalize) (*G_OBJECT_CLASS (g_gphoto2_volume_monitor_parent_class)->finalize) (object); } static GList * get_mounts (GVolumeMonitor *volume_monitor) { return NULL; } static GList * get_volumes (GVolumeMonitor *volume_monitor) { GGPhoto2VolumeMonitor *monitor; GList *l; monitor = G_GPHOTO2_VOLUME_MONITOR (volume_monitor); G_LOCK (hal_vm); l = g_list_copy (monitor->camera_volumes); g_list_foreach (l, (GFunc)g_object_ref, NULL); G_UNLOCK (hal_vm); return l; } static GList * get_connected_drives (GVolumeMonitor *volume_monitor) { return NULL; } static GVolume * get_volume_for_uuid (GVolumeMonitor *volume_monitor, const char *uuid) { return NULL; } static GMount * get_mount_for_uuid (GVolumeMonitor *volume_monitor, const char *uuid) { return NULL; } #ifdef HAVE_GUDEV static void gudev_add_camera (GGPhoto2VolumeMonitor *monitor, GUdevDevice *device, gboolean do_emit) { GGPhoto2Volume *volume; GList *store_heads, *l; guint num_store_heads; const char *usb_bus_num, *usb_device_num; /* For iPhones and iPod Touches, don't mount gphoto mounts, * we already have access through AFC */ #ifdef HAVE_AFC if (g_udev_device_get_property_as_boolean (device, "USBMUX_SUPPORTED")) { /* g_debug ("ignoring device, is AFC"); */ return; } #endif /* HAVE_AFC */ usb_bus_num = g_udev_device_get_property (device, "BUSNUM"); if (usb_bus_num == NULL) { g_warning("device %s has no BUSNUM property, ignoring", g_udev_device_get_device_file (device)); return; } usb_device_num = g_udev_device_get_property (device, "DEVNUM"); if (usb_device_num == NULL) { g_warning("device %s has no DEVNUM property, ignoring", g_udev_device_get_device_file (device)); return; } /* g_debug ("gudev_add_camera: camera device %s (bus: %i, device: %i)", g_udev_device_get_device_file (device), usb_bus_num, usb_device_num); */ store_heads = get_stores_for_camera (usb_bus_num, usb_device_num); num_store_heads = g_list_length (store_heads); for (l = store_heads ; l != NULL; l = l->next) { char *store_path = (char *) l->data; GFile *activation_mount_root; gchar *uri; /* If we only have a single store, don't use the store name at all. The backend automatically * prepend the storename; this is to work around bugs with devices (like the iPhone) for which * the store name changes every time the camera is initialized (e.g. mounted). */ if (num_store_heads == 1) { uri = g_strdup_printf ("gphoto2://[usb:%s,%s]", usb_bus_num, usb_device_num); } else { uri = g_strdup_printf ("gphoto2://[usb:%s,%s]/%s", usb_bus_num, usb_device_num, store_path[0] == '/' ? store_path + 1 : store_path); } /* g_debug ("gudev_add_camera: ... adding URI for storage head: %s", uri); */ activation_mount_root = g_file_new_for_uri (uri); g_free (uri); volume = g_gphoto2_volume_new (G_VOLUME_MONITOR (monitor), device, monitor->gudev_client, activation_mount_root); if (volume != NULL) { monitor->camera_volumes = g_list_prepend (monitor->camera_volumes, volume); if (do_emit) g_signal_emit_by_name (monitor, "volume_added", volume); } if (activation_mount_root != NULL) g_object_unref (activation_mount_root); } g_list_foreach (store_heads, (GFunc) g_free, NULL); g_list_free (store_heads); } static void gudev_remove_camera (GGPhoto2VolumeMonitor *monitor, GUdevDevice *device) { GList *l, *ll; const gchar* sysfs_path; sysfs_path = g_udev_device_get_sysfs_path (device); /* g_debug ("gudev_remove_camera: %s", g_udev_device_get_device_file (device)); */ for (l = monitor->camera_volumes; l != NULL; l = ll) { GGPhoto2Volume *volume = G_GPHOTO2_VOLUME (l->data); ll = l->next; if (g_gphoto2_volume_has_path (volume, sysfs_path)) { /* g_debug ("gudev_remove_camera: found volume %s, deleting", sysfs_path); */ g_signal_emit_by_name (monitor, "volume_removed", volume); g_signal_emit_by_name (volume, "removed"); g_gphoto2_volume_removed (volume); monitor->camera_volumes = g_list_remove (monitor->camera_volumes, volume); g_object_unref (volume); } } } static void on_uevent (GUdevClient *client, gchar *action, GUdevDevice *device, gpointer user_data) { GGPhoto2VolumeMonitor *monitor = G_GPHOTO2_VOLUME_MONITOR (user_data); /* g_debug ("on_uevent: action=%s, device=%s", action, g_udev_device_get_device_file(device)); */ /* filter out uninteresting events */ if (!g_udev_device_has_property (device, "ID_GPHOTO2")) { /* g_debug ("on_uevent: discarding, not ID_GPHOTO2"); */ return; } if (strcmp (action, "add") == 0) gudev_add_camera (monitor, device, TRUE); else if (strcmp (action, "remove") == 0) gudev_remove_camera (monitor, device); } /* Find all attached gphoto supported cameras; this is called on startup * (coldplugging). */ static void gudev_coldplug_cameras (GGPhoto2VolumeMonitor *monitor) { GList *usb_devices, *l; usb_devices = g_udev_client_query_by_subsystem (monitor->gudev_client, "usb"); for (l = usb_devices; l != NULL; l = l->next) { GUdevDevice *d = l->data; if (g_udev_device_has_property (d, "ID_GPHOTO2")) gudev_add_camera (monitor, d, FALSE); } } #else static void hal_changed (HalPool *pool, HalDevice *device, gpointer user_data) { GGPhoto2VolumeMonitor *monitor = G_GPHOTO2_VOLUME_MONITOR (user_data); /*g_warning ("hal changed");*/ update_all (monitor, TRUE); } #endif static GObject * g_gphoto2_volume_monitor_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties) { GObject *object; GGPhoto2VolumeMonitor *monitor; GGPhoto2VolumeMonitorClass *klass; GObjectClass *parent_class; G_LOCK (hal_vm); if (the_volume_monitor != NULL) { object = g_object_ref (the_volume_monitor); G_UNLOCK (hal_vm); return object; } G_UNLOCK (hal_vm); /*g_warning ("creating hal vm");*/ object = NULL; /* Invoke parent constructor. */ klass = G_GPHOTO2_VOLUME_MONITOR_CLASS (g_type_class_peek (G_TYPE_GPHOTO2_VOLUME_MONITOR)); parent_class = G_OBJECT_CLASS (g_type_class_peek_parent (klass)); object = parent_class->constructor (type, n_construct_properties, construct_properties); monitor = G_GPHOTO2_VOLUME_MONITOR (object); #ifdef HAVE_GUDEV const char *subsystems[] = {"usb", NULL}; monitor->gudev_client = g_udev_client_new (subsystems); g_signal_connect (monitor->gudev_client, "uevent", G_CALLBACK (on_uevent), monitor); gudev_coldplug_cameras (monitor); #else monitor->pool = g_object_ref (get_hal_pool ()); g_signal_connect (monitor->pool, "device_added", G_CALLBACK (hal_changed), monitor); g_signal_connect (monitor->pool, "device_removed", G_CALLBACK (hal_changed), monitor); update_all (monitor, FALSE); #endif G_LOCK (hal_vm); the_volume_monitor = monitor; G_UNLOCK (hal_vm); return object; } static void g_gphoto2_volume_monitor_init (GGPhoto2VolumeMonitor *monitor) { } static gboolean is_supported (void) { #ifdef HAVE_GUDEV /* Today's Linux desktops pretty much need udev to have anything working, so * assume it's there */ return TRUE; #else return get_hal_pool() != NULL; #endif } static void g_gphoto2_volume_monitor_class_init (GGPhoto2VolumeMonitorClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GVolumeMonitorClass *monitor_class = G_VOLUME_MONITOR_CLASS (klass); gobject_class->constructor = g_gphoto2_volume_monitor_constructor; gobject_class->finalize = g_gphoto2_volume_monitor_finalize; gobject_class->dispose = g_gphoto2_volume_monitor_dispose; monitor_class->get_mounts = get_mounts; monitor_class->get_volumes = get_volumes; monitor_class->get_connected_drives = get_connected_drives; monitor_class->get_volume_for_uuid = get_volume_for_uuid; monitor_class->get_mount_for_uuid = get_mount_for_uuid; monitor_class->is_supported = is_supported; } /** * g_gphoto2_volume_monitor_new: * * Returns: a new #GVolumeMonitor. **/ GVolumeMonitor * g_gphoto2_volume_monitor_new (void) { GGPhoto2VolumeMonitor *monitor; monitor = g_object_new (G_TYPE_GPHOTO2_VOLUME_MONITOR, NULL); return G_VOLUME_MONITOR (monitor); } #ifndef HAVE_GUDEV static void diff_sorted_lists (GList *list1, GList *list2, GCompareFunc compare, GList **added, GList **removed) { int order; *added = *removed = NULL; while (list1 != NULL && list2 != NULL) { order = (*compare) (list1->data, list2->data); if (order < 0) { *removed = g_list_prepend (*removed, list1->data); list1 = list1->next; } else if (order > 0) { *added = g_list_prepend (*added, list2->data); list2 = list2->next; } else { /* same item */ list1 = list1->next; list2 = list2->next; } } while (list1 != NULL) { *removed = g_list_prepend (*removed, list1->data); list1 = list1->next; } while (list2 != NULL) { *added = g_list_prepend (*added, list2->data); list2 = list2->next; } } static GGPhoto2Volume * find_camera_volume_by_udi (GGPhoto2VolumeMonitor *monitor, const char *udi) { GList *l; for (l = monitor->camera_volumes; l != NULL; l = l->next) { GGPhoto2Volume *volume = l->data; if (g_gphoto2_volume_has_udi (volume, udi)) return volume; } return NULL; } static gint hal_device_compare (HalDevice *a, HalDevice *b) { return strcmp (hal_device_get_udi (a), hal_device_get_udi (b)); } static void list_emit (GGPhoto2VolumeMonitor *monitor, const char *monitor_signal, const char *object_signal, GList *objects) { GList *l; for (l = objects; l != NULL; l = l->next) { g_signal_emit_by_name (monitor, monitor_signal, l->data); if (object_signal) g_signal_emit_by_name (l->data, object_signal); } } typedef struct { GGPhoto2VolumeMonitor *monitor; GList *added_volumes, *removed_volumes; } ChangedLists; static gboolean emit_lists_in_idle (gpointer data) { ChangedLists *lists = data; list_emit (lists->monitor, "volume_removed", "removed", lists->removed_volumes); list_emit (lists->monitor, "volume_added", NULL, lists->added_volumes); list_free (lists->removed_volumes); list_free (lists->added_volumes); g_object_unref (lists->monitor); g_free (lists); return FALSE; } /* Must be called from idle if emit_changes, with no locks held */ static void update_all (GGPhoto2VolumeMonitor *monitor, gboolean emit_changes) { ChangedLists *lists; GList *added_volumes, *removed_volumes; added_volumes = NULL; removed_volumes = NULL; G_LOCK (hal_vm); update_cameras (monitor, &added_volumes, &removed_volumes); G_UNLOCK (hal_vm); if (emit_changes) { lists = g_new0 (ChangedLists, 1); lists->monitor = g_object_ref (monitor); lists->added_volumes = added_volumes; lists->removed_volumes = removed_volumes; g_idle_add (emit_lists_in_idle, lists); } else { list_free (removed_volumes); list_free (added_volumes); } } #endif static GList * get_stores_for_camera (const char *bus_num, const char *device_num) { GList *l; CameraStorageInformation *storage_info; GPContext *context; GPPortInfo info; GPPortInfoList *il; int num_storage_info, n, rc; Camera *camera; char *port; guint i; il = NULL; camera = NULL; context = NULL; l = NULL; port = g_strdup_printf ("usb:%s,%s", bus_num, device_num); /* Connect to the camera */ context = gp_context_new (); if (gp_camera_new (&camera) != 0) goto out; if (gp_port_info_list_new (&il) != 0) goto out; if (gp_port_info_list_load (il) != 0) goto out; n = gp_port_info_list_lookup_path (il, port); if (n == GP_ERROR_UNKNOWN_PORT) goto out; if (gp_port_info_list_get_info (il, n, &info) != 0) goto out; if (gp_camera_set_port_info (camera, info) != 0) goto out; gp_port_info_list_free (il); il = NULL; if (gp_camera_init (camera, context) != 0) goto out; /* Get information about the storage heads */ rc = gp_camera_get_storageinfo (camera, &storage_info, &num_storage_info, context); if (rc != 0) { /* Not all gphoto drivers implement get storage info (drivers for proprietary protocols often don't) */ if (rc == GP_ERROR_NOT_SUPPORTED) l = g_list_prepend (l, g_strdup ("/")); goto out; } /* Append the data to the list */ for (i = 0; i < num_storage_info; i++) { const gchar *basedir; /* Ignore storage with no capacity (see bug 570888) */ if ((storage_info[i].fields & GP_STORAGEINFO_MAXCAPACITY) && storage_info[i].capacitykbytes == 0) continue; /* Some cameras, such as the Canon 5D, won't report the basedir */ if (storage_info[i].fields & GP_STORAGEINFO_BASE) basedir = storage_info[i].basedir; else basedir = "/"; /* g_debug ("capacitykbytes[%d] = %d", i, (gint) storage_info[i].capacitykbytes); */ l = g_list_prepend (l, g_strdup (basedir)); } out: /* Clean up */ if (il != NULL) gp_port_info_list_free (il); if (context != NULL) gp_context_unref (context); if (camera != NULL) gp_camera_unref (camera); g_free (port); return l; } #ifndef HAVE_GUDEV static GList * get_stores_for_camera_int (int bus_num, int device_num) { GList *ret; char *bus_num_str, *device_num_str; bus_num_str = g_strdup_printf ("%d", bus_num); device_num_str = g_strdup_printf ("%d", device_num); ret = get_stores_for_camera (bus_num_str, device_num_str); g_free (bus_num_str); g_free (device_num_str); return ret; } static void update_cameras (GGPhoto2VolumeMonitor *monitor, GList **added_volumes, GList **removed_volumes) { GList *new_camera_devices; GList *new_mtp_devices; GList *removed, *added; GList *l, *ll; GGPhoto2Volume *volume; const char *udi; new_mtp_devices = hal_pool_find_by_capability (monitor->pool, "portable_audio_player"); for (l = new_mtp_devices; l != NULL; l = ll) { HalDevice *d = l->data; ll = l->next; if (! hal_device_get_property_bool (d, "camera.libgphoto2.support")) { /*g_warning ("ignoring %s", hal_device_get_udi (d));*/ /* filter out everything that isn't supported by libgphoto2 */ new_mtp_devices = g_list_delete_link (new_mtp_devices, l); } } new_camera_devices = hal_pool_find_by_capability (monitor->pool, "camera"); new_camera_devices = g_list_concat (new_camera_devices, new_mtp_devices); for (l = new_camera_devices; l != NULL; l = ll) { HalDevice *d = l->data; ll = l->next; /*g_warning ("got %s", hal_device_get_udi (d));*/ if (! hal_device_get_property_bool (d, "camera.libgphoto2.support")) { /*g_warning ("ignoring %s", hal_device_get_udi (d));*/ /* filter out everything that isn't supported by libgphoto2 */ new_camera_devices = g_list_delete_link (new_camera_devices, l); } } g_list_foreach (new_camera_devices, (GFunc) g_object_ref, NULL); new_camera_devices = g_list_sort (new_camera_devices, (GCompareFunc) hal_device_compare); diff_sorted_lists (monitor->last_camera_devices, new_camera_devices, (GCompareFunc) hal_device_compare, &added, &removed); for (l = removed; l != NULL; l = l->next) { HalDevice *d = l->data; udi = hal_device_get_udi (d); /*g_warning ("camera removing %s", udi);*/ volume = find_camera_volume_by_udi (monitor, udi); if (volume != NULL) { g_gphoto2_volume_removed (volume); monitor->camera_volumes = g_list_remove (monitor->camera_volumes, volume); *removed_volumes = g_list_prepend (*removed_volumes, volume); } } for (l = added; l != NULL; l = l->next) { HalDevice *d = l->data; int usb_bus_num; int usb_device_num; gboolean found; GList *store_heads, *l; guint num_store_heads; /* Look for the device in the added volumes, so as * not to add devices that are both audio players, and cameras */ found = FALSE; for (ll = *added_volumes; ll; ll = ll->next) { if (g_gphoto2_volume_has_udi (ll->data, hal_device_get_udi (d)) != FALSE) { found = TRUE; break; } } if (found) continue; usb_bus_num = hal_device_get_property_int (d, "usb.bus_number"); #if defined(__linux__) usb_device_num = hal_device_get_property_int (d, "usb.linux.device_number"); #elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) usb_device_num = hal_device_get_property_int (d, "freebsd.unit"); #else # error "Need OS specific tweaks" #endif store_heads = get_stores_for_camera_int (usb_bus_num, usb_device_num); num_store_heads = g_list_length (store_heads); for (l = store_heads ; l != NULL; l = l->next) { char *store_path = (char *) l->data; GFile *activation_mount_root; gchar *uri; /* If we only have a single store, don't use the store name at all. The backend automatically * prepend the storename; this is to work around bugs with devices (like the iPhone) for which * the store name changes every time the camera is initialized (e.g. mounted). */ if (num_store_heads == 1) { uri = g_strdup_printf ("gphoto2://[usb:%03d,%03d]", usb_bus_num, usb_device_num); } else { uri = g_strdup_printf ("gphoto2://[usb:%03d,%03d]/%s", usb_bus_num, usb_device_num, store_path[0] == '/' ? store_path + 1 : store_path); } activation_mount_root = g_file_new_for_uri (uri); g_free (uri); udi = hal_device_get_udi (d); volume = g_gphoto2_volume_new (G_VOLUME_MONITOR (monitor), d, monitor->pool, activation_mount_root); if (volume != NULL) { monitor->camera_volumes = g_list_prepend (monitor->camera_volumes, volume); *added_volumes = g_list_prepend (*added_volumes, g_object_ref (volume)); } if (activation_mount_root != NULL) g_object_unref (activation_mount_root); } g_list_foreach (store_heads, (GFunc) g_free, NULL); g_list_free (store_heads); } g_list_free (added); g_list_free (removed); list_free (monitor->last_camera_devices); monitor->last_camera_devices = new_camera_devices; } #endif