1 /* Required for strdup, snprintf, getopt */
2 #define _XOPEN_SOURCE 500
13 #include <sys/select.h>
14 #include <sys/inotify.h>
16 #include <gdk/gdkkeysyms.h>
18 typedef struct sProperty
24 typedef struct sDevice
34 typedef struct sGuiContext
42 char *post_mount_command;
45 GIOChannel *inotify_channel;
50 char **pmount_argv = NULL;
53 Parses a string of the form name=value and places the components in a Property
54 structure. Returns 0 on success, or -1 if the string wasn't a valid property.
56 int parse_property(char *str, int size, Property *prop)
61 for(i=0; (equals<0 && i<size); ++i)
68 prop->name = malloc(equals+1);
69 strncpy(prop->name, str, equals);
70 prop->name[equals] = 0;
72 prop->value = malloc(size-equals);
73 strncpy(prop->value, str+equals+1, size-equals-1);
74 prop->value[size-equals-1] = 0;
80 Retrieves all properties associated with a /dev node. The returned array is
81 terminated with an entry containing NULL values. Use free_properties to free
84 Property *get_device_properties(char *node)
86 static const char *udevadm_path = NULL;
93 Property *props = NULL;
98 udevadm_path = "/bin/udevadm";
99 if(access(udevadm_path, X_OK)<0)
101 udevadm_path = "/sbin/udevadm";
102 if(access(udevadm_path, X_OK)<0)
105 perror("Unable to find udevadm");
118 printf("Running udevadm info -q property -n \"%s\"\n", node);
123 if(execl(udevadm_path, "udevadm", "info", "-q", "property", "-n", node, NULL)<0)
124 fprintf(stderr, "Unable to execute %s: %s\n", udevadm_path, strerror(errno));
140 buf = (char *)malloc(bufsize);
152 len = read(pipe_fd[0], buf+pos, bufsize-pos);
161 for(i=0; (newline<0 && i<pos); ++i)
170 /* There was no newline in the buffer but there is more output to
171 be read. Try again with a larger buffer. */
173 buf = (char *)realloc(buf, bufsize);
177 if(parse_property(buf, newline, &prop)==0)
179 /* Reserve space for a sentinel value as well. */
180 props = (Property *)realloc(props, (n_props+2)*sizeof(Property));
181 props[n_props] = prop;
184 memmove(buf, buf+newline+1, pos-newline-1);
195 /* Terminate the array with NULL pointers. */
196 props[n_props].name = NULL;
197 props[n_props].value = NULL;
200 waitpid(pid, NULL, 0);
207 Looks for a property in an array of properties and returns its value. Returns
208 NULL if the property was not found.
210 char *get_property_value(Property *props, char *name)
213 for(i=0; props[i].name; ++i)
214 if(strcmp(props[i].name, name)==0)
215 return props[i].value;
220 Checks if a property has a specific value. A NULL value is matched if the
221 property does not exist.
223 int match_property_value(Property *props, char *name, char *value)
225 char *v = get_property_value(props, name);
228 return strcmp(v, value)==0;
232 Frees an array of properties and all strings contained in it.
234 void free_properties(Property *props)
239 for(i=0; props[i].name; ++i)
242 free(props[i].value);
248 Returns an array of user-mountable devices listed in fstab.
250 char **get_fstab_devices(void)
254 char **devices = NULL;
257 file = setmntent("/etc/fstab", "r");
261 while((me = getmntent(file)))
262 if(hasmntopt(me, "user")!=NULL)
264 devices = (char **)realloc(devices, (n_devices+2)*sizeof(char *));
265 devices[n_devices] = strdup(me->mnt_fsname);
271 devices[n_devices] = NULL;
277 Checks if an array of strings contains the specified string.
279 int is_in_array(char **array, char *str)
284 for(i=0; array[i]; ++i)
285 if(!strcmp(str, array[i]))
291 Frees an array of strings.
293 void free_string_array(char **array)
298 for(i=0; array[i]; ++i)
304 Checks if a partition identified by a sysfs path is on a removable device.
306 int is_removable(char *devpath)
313 len = snprintf(fnbuf, sizeof(fnbuf), "/sys%s", devpath);
314 /* Default to not removable if the path was too long. */
315 if(len+10>=(int)sizeof(fnbuf))
318 /* We got a partition as a parameter, but the removable property is on the
319 disk. Replace the last component with "removable". */
320 for(ptr=fnbuf+len; (ptr>fnbuf && *ptr!='/'); --ptr) ;
321 strcpy(ptr, "/removable");
323 fd = open(fnbuf, O_RDONLY);
332 printf(" Removable\n");
336 printf(" Not removable\n");
343 Checks if a partition's disk or any of its parent devices are connected to any
344 of a set of buses. The device is identified by a sysfs path. The bus array
345 must be terminated with a NULL entry.
347 int check_buses(char *devpath, char **buses)
353 len = snprintf(fnbuf, sizeof(fnbuf), "/sys%s", devpath);
354 /* Default to no match if the path was too long. */
355 if(len+10>=(int)sizeof(fnbuf))
358 for(ptr=fnbuf+len; ptr>fnbuf+12; --ptr)
362 /* Replace the last component with "subsystem". */
363 strcpy(ptr, "/subsystem");
364 len = readlink(fnbuf, linkbuf, sizeof(linkbuf)-1);
369 /* Extract the last component of the subsystem symlink. */
370 for(; (len>0 && linkbuf[len-1]!='/'); --len) ;
375 printf(" Subsystem of %s is %s\n", fnbuf, linkbuf+len);
378 if(is_in_array(buses, linkbuf+len))
387 Check if an array of properties describes a device that can be mounted. An
388 array of explicitly allowed devices can be passed in as well. Both arrays must
389 be terminated by a NULL entry.
391 int can_mount(Property *props, char **allowed)
393 static char *removable_buses[] = { "usb", "firewire", 0 };
398 devname = get_property_value(props, "DEVNAME");
399 if(is_in_array(allowed, devname))
402 /* Special case for CD devices, since they are not partitions. Only allow
403 mounting if media is inserted. */
404 if(match_property_value(props, "ID_TYPE", "cd") && match_property_value(props, "ID_CDROM_MEDIA", "1"))
407 /* Only allow mounting partitions. */
408 if(!match_property_value(props, "DEVTYPE", "partition"))
411 devpath = get_property_value(props, "DEVPATH");
412 if(is_removable(devpath))
415 /* Certain buses are removable by nature, but devices only advertise
416 themselves as removable if they support removable media, e.g. memory card
418 bus = get_property_value(props, "ID_BUS");
419 if(is_in_array(removable_buses, bus))
422 return check_buses(devpath, removable_buses);
426 Returns an array of all device nodes in a directory. Symbolic links are
429 char **get_device_nodes(char *dirname)
438 char **checked = NULL;
442 dir = opendir(dirname);
446 while((de = readdir(dir)))
451 /* Ignore . and .. entries. */
452 if(de->d_name[0]=='.' && (de->d_name[1]==0 || (de->d_name[1]=='.' && de->d_name[2]==0)))
455 snprintf(fnbuf, sizeof(fnbuf), "%s/%s", dirname, de->d_name);
459 if(S_ISLNK(st.st_mode))
462 len = readlink(fnbuf, linkbuf, sizeof(linkbuf)-1);
470 /* There may be multiple symlinks to the same device. Only include each
471 device once in the returned array. */
474 for(i=0; (!duplicate && i<n_checked); ++i)
475 if(strcmp(node, checked[i])==0)
481 printf("Device %s is a duplicate\n", fnbuf);
485 checked = (char **)realloc(checked, (n_checked+1)*sizeof(char *));
486 checked[n_checked] = strdup(node);
489 nodes = (char **)realloc(nodes, (n_nodes+2)*sizeof(char *));
490 nodes[n_nodes] = strdup(fnbuf);
497 for(i=0; i<n_checked; ++i)
503 nodes[n_nodes] = NULL;
509 Reads the list of mounted devices from /etc/mtab and records the mount points.
511 void check_mounts(Device *devices)
516 file = setmntent("/etc/mtab", "r");
520 while((me = getmntent(file)))
524 for(i=0; devices[i].node; ++i)
525 if(!strcmp(devices[i].devname, me->mnt_fsname))
527 devices[i].mount_point = strdup(me->mnt_dir);
530 printf("Device %s is mounted on %s\n", devices[i].node, devices[i].mount_point);
538 Returns an array of all mountable devices.
540 Device *get_devices(void)
543 Device *devices = NULL;
548 nodes = get_device_nodes("/dev/disk/by-id");
549 fstab = get_fstab_devices();
551 for(i=0; nodes[i]; ++i)
556 printf("Examining device %s\n", nodes[i]);
558 props = get_device_properties(nodes[i]);
562 printf(" No properties\n");
570 for(j=0; props[j].name; ++j)
571 printf(" %s = %s\n", props[j].name, props[j].value);
574 if(can_mount(props, fstab))
585 printf(" Using device\n");
587 devname = get_property_value(props, "DEVNAME");
589 /* Get a human-readable label for the device. Use filesystem label,
590 filesystem UUID or device node name in order of preference. */
591 label = get_property_value(props, "ID_FS_LABEL");
593 label = get_property_value(props, "ID_FS_UUID");
599 for(ptr=label; *ptr; ++ptr)
604 vendor = get_property_value(props, "ID_VENDOR");
605 model = get_property_value(props, "ID_MODEL");
607 pos = snprintf(buf, sizeof(buf), "%s", label);
609 pos += snprintf(buf+pos, sizeof(buf)-pos, " (%s %s)", vendor, model);
613 /* Reserve space for a sentinel entry. */
614 devices = (Device *)realloc(devices, (n_devices+2)*sizeof(Device));
615 devices[n_devices].node = nodes[i];
616 devices[n_devices].devname = strdup(devname);
617 devices[n_devices].label = strdup(label);
618 devices[n_devices].description = strdup(buf);
619 devices[n_devices].mount_point = NULL;
620 devices[n_devices].time = st.st_mtime;
625 free_properties(props);
629 free_string_array(fstab);
633 /* Terminate the array with NULL pointers. */
634 devices[n_devices].node = NULL;
635 devices[n_devices].devname = NULL;
636 devices[n_devices].label = NULL;
637 devices[n_devices].description = NULL;
638 devices[n_devices].mount_point = NULL;
640 check_mounts(devices);
647 Frees an array of devices and all strings contained in it.
649 void free_devices(Device *devices)
654 for(i=0; devices[i].node; ++i)
656 free(devices[i].node);
657 free(devices[i].devname);
658 free(devices[i].label);
659 free(devices[i].description);
660 if(devices[i].mount_point)
661 free(devices[i].mount_point);
667 Mounts a device if it was not mounted, or unmounts if it was.
669 int toggle_device(Device *device, char *out_buf, int out_size)
671 int umount = !!device->mount_point;
672 char mount_point[1024];
676 struct timeval timeout;
683 /* Find a mount point that does not exist yet. */
688 len = snprintf(mount_point, sizeof(mount_point), "/media/%s", device->label);
689 if(len+2>=(int)sizeof(mount_point))
693 len += snprintf(mount_point+len, sizeof(mount_point)-len, "_%c", suffix);
695 if(access(mount_point, F_OK)<0 && errno==ENOENT)
713 /* Complete construction of pmount call */
716 pmount_argv[0] = "pmount";
719 while(pmount_argv[++last]);
721 pmount_argv[last] = device->node;
722 pmount_argv[last+1] = mount_point+7;
723 pmount_argv[last+2] = NULL;
728 printf("Running pumount %s\n", device->node);
732 printf("Running pmount");
733 while(pmount_argv[++i])
734 printf(" %s", pmount_argv[i]);
744 execl("/usr/bin/pumount", "pumount", device->node, NULL);
746 execvp("/usr/bin/pmount", pmount_argv);
756 FD_SET(pipe_fd[0], &fds);
758 timeout.tv_usec = 200000;
762 /* The write fd for the pipe may be inherited by a fuse server
763 process and stay open indefinitely. */
764 if(select(pipe_fd[0]+1, &fds, NULL, NULL, &timeout))
768 len = read(pipe_fd[0], out_buf+pos, out_size-pos-1);
773 else if(waitpid(pid, &status, 0))
782 waitpid(pid, &status, 0);
788 if(WIFEXITED(status))
790 if(WEXITSTATUS(status))
791 printf("Command exited successfully\n");
793 printf("Command exited with status %d\n", WEXITSTATUS(status));
795 else if(WIFSIGNALED(status))
796 printf("Command terminated with signal %d\n", WTERMSIG(status));
798 printf("Command exited with unknown result %04X\n", status);
801 if(!WIFEXITED(status) || WEXITSTATUS(status))
806 free(device->mount_point);
807 device->mount_point = NULL;
810 device->mount_point = strdup(mount_point);
816 Refreshes both the internal devices array and the GUI list.
818 int refresh_devices(GuiContext *context, int umount)
821 GtkTreeSelection *selection;
827 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(context->list)));
828 gtk_list_store_clear(store);
830 free_devices(context->devices);
832 context->devices = get_devices();
833 if(!context->devices)
836 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context->list));
838 /* Populate the list with devices in appropriate state. */
841 for(i=0; context->devices[i].node; ++i)
845 dev = &context->devices[i];
846 if(umount<0 || !dev->mount_point==!umount)
848 gtk_list_store_append(store, &iter);
849 gtk_list_store_set(store, &iter,
852 2, !!dev->mount_point,
857 /* Pre-select the device that appeared on the system most recently. */
859 gtk_tree_selection_select_iter(selection, &iter);
870 Handles an automatic refresh of the device list in response to inotify events.
872 gboolean refresh_devices_idle(gpointer data)
874 GuiContext *context = (GuiContext *)data;
877 n_listed = refresh_devices(context, -1);
878 context->refresh_pending = 0;
880 if(context->autohide)
883 gtk_widget_show_all(context->window);
885 gtk_widget_hide(context->window);
892 Callback for selection in the device list changing. Updates the action button
893 label according to device status.
895 void selection_changed(GtkTreeSelection *selection, gpointer user_data)
897 GuiContext *context = (GuiContext *)user_data;
902 if(!gtk_tree_selection_get_selected(selection, &model, &iter))
905 gtk_tree_model_get(model, &iter, 1, &device, -1);
906 gtk_button_set_label(GTK_BUTTON(context->button), (device->mount_point ? "Unmount" : "Mount"));
910 Callback for activating a row in the device list. Mounts or unmounts the
911 device depending on operating mode.
913 void row_activated(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
915 GuiContext *context = (GuiContext *)user_data;
923 model = gtk_tree_view_get_model(list);
925 if(!gtk_tree_model_get_iter(model, &iter, path))
928 gtk_tree_model_get(model, &iter, 1, &device, -1);
929 ret = toggle_device(device, output, sizeof(output));
934 /* Pmount terminated with nonzero status or a signal. Display an
935 error to the user. */
936 dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", output);
938 g_signal_connect(dialog, "response", G_CALLBACK(>k_widget_destroy), dialog);
940 g_signal_connect(dialog, "response", G_CALLBACK(>k_main_quit), NULL);
941 gtk_widget_show_all(dialog);
947 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
948 2, !!device->mount_point,
949 3, device->mount_point,
952 gtk_button_set_label(GTK_BUTTON(context->button), (device->mount_point ? "Unmount" : "Mount"));
957 if(context->post_mount_command && device->mount_point)
960 printf("Running %s in %s\n", context->post_mount_command, device->mount_point);
965 chdir(device->mount_point);
966 execlp(context->post_mount_command, context->post_mount_command, NULL);
976 Callback for the mount/unmount button. Causes the selected row in the device
977 list to be activated.
979 void button_clicked(GtkButton *button, gpointer user_data)
981 GuiContext *context = (GuiContext *)user_data;
982 GtkTreeSelection *selection;
986 GtkTreeViewColumn *column;
988 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context->list));
989 gtk_tree_selection_get_selected(selection, &model, &iter);
990 path = gtk_tree_model_get_path(model, &iter);
991 column = gtk_tree_view_get_column(GTK_TREE_VIEW(context->list), 0);
992 gtk_tree_view_row_activated(GTK_TREE_VIEW(context->list), path, column);
993 gtk_tree_path_free(path);
999 Global key press callback for the window.
1001 gboolean key_press(GtkWidget *widget, GdkEvent *event, gpointer user_data)
1003 if(event->key.keyval==GDK_KEY_Escape)
1016 Callback for inotify events.
1018 gboolean inotify_event_available(GIOChannel *source, GIOCondition condition, gpointer user_data)
1020 GuiContext *context = (GuiContext *)user_data;
1022 char eventbuf[sizeof(struct inotify_event)+NAME_MAX+1];
1025 fd = g_io_channel_unix_get_fd(source);
1026 len = read(fd, eventbuf, sizeof(eventbuf));
1027 if(len>=(int)sizeof(struct inotify_event))
1029 if(!context->refresh_pending)
1031 g_timeout_add(500, &refresh_devices_idle, context);
1032 context->refresh_pending = 1;
1041 void show_help(void)
1043 printf("pmount-gui\n"
1044 "Copyright (c) 2011-2016 Mikko Rasa, Mikkosoft Productions\n\n"
1045 "Usage: pmount-gui [-v] [-u] [-r <command>] [-m|-M] [-h] [-- <pmount options>]\n\n"
1047 " -v Increase verbosity\n"
1048 " -u Unmount a device (default is mount)\n"
1049 " -r Run a command after mounting\n"
1050 " -m Start a persistent mount manager\n"
1051 " -M Like -m, but hide the window if there are no devices\n"
1052 " -h Display this help\n");
1055 int main(int argc, char **argv)
1059 GtkWidget *viewport;
1060 GtkListStore *store;
1061 GtkTreeSelection *selection;
1062 GtkCellRenderer *mounted_toggle;
1068 context.manager = 0;
1069 context.autohide = 0;
1070 context.devices = NULL;
1071 context.post_mount_command = NULL;
1072 context.inotify_fd = -1;
1073 context.dev_wd = -1;
1074 context.inotify_channel = NULL;
1075 context.refresh_pending = 0;
1077 gtk_init(&argc, &argv);
1079 while((opt = getopt(argc, argv, "vur:mMh"))!=-1) switch(opt)
1088 context.post_mount_command = optarg;
1091 context.manager = 1;
1092 context.autohide = 0;
1095 context.manager = 1;
1096 context.autohide = 1;
1103 /* argc - optind equals to number of options after "--" */
1104 /* One empty element at the beginning is for program name for execvp */
1105 /* Three empty elements at the end are required options for pmount + NULL */
1107 pmount_argv = malloc(sizeof(char *)*(argc-optind+4));
1109 for(i=0; i<(argc-optind); ++i)
1110 pmount_argv[i+1] = argv[optind+i];
1111 pmount_argv[argc-optind+1] = NULL;
1113 context.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1114 gtk_container_set_border_width(GTK_CONTAINER(context.window), 5);
1115 g_signal_connect(context.window, "destroy", G_CALLBACK(>k_main_quit), NULL);
1117 box = gtk_vbox_new(FALSE, 5);
1118 gtk_container_add(GTK_CONTAINER(context.window), box);
1120 viewport = gtk_viewport_new(NULL, NULL);
1121 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_IN);
1122 gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
1124 context.list = gtk_tree_view_new();
1125 gtk_container_add(GTK_CONTAINER(viewport), context.list);
1126 g_signal_connect(context.list, "row-activated", G_CALLBACK(&row_activated), &context);
1128 store = gtk_list_store_new(4, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_BOOLEAN, G_TYPE_STRING);
1129 gtk_tree_view_set_model(GTK_TREE_VIEW(context.list), GTK_TREE_MODEL(store));
1130 gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(context.list),
1131 -1, "Device", gtk_cell_renderer_text_new(), "text", 0, NULL);
1135 GtkTreeViewColumn *mounted_column;
1136 GtkCellRenderer *mount_point_renderer;
1138 mounted_column = gtk_tree_view_column_new();
1139 gtk_tree_view_column_set_title(mounted_column, "Mounted");
1141 mounted_toggle = gtk_cell_renderer_toggle_new();
1142 gtk_tree_view_column_pack_start(mounted_column, mounted_toggle, FALSE);
1143 gtk_tree_view_column_add_attribute(mounted_column, mounted_toggle, "active", 2);
1145 mount_point_renderer = gtk_cell_renderer_text_new();
1146 gtk_tree_view_column_pack_start(mounted_column, mount_point_renderer, TRUE);
1147 gtk_tree_view_column_add_attribute(mounted_column, mount_point_renderer, "text", 3);
1149 gtk_tree_view_insert_column(GTK_TREE_VIEW(context.list), mounted_column, -1);
1151 context.inotify_fd = inotify_init();
1152 if(context.inotify_fd>=0)
1154 context.dev_wd = inotify_add_watch(context.inotify_fd, "/dev/disk/by-id", IN_CREATE|IN_DELETE);
1156 context.inotify_channel = g_io_channel_unix_new(context.inotify_fd);
1157 g_io_add_watch(context.inotify_channel, G_IO_IN, &inotify_event_available, &context);
1160 printf("Warning: Unable to initialize inotify\n");
1165 g_signal_connect(context.window, "key-press-event", G_CALLBACK(&key_press), NULL);
1167 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(context.list));
1168 g_signal_connect(selection, "changed", G_CALLBACK(&selection_changed), &context);
1170 context.button = gtk_button_new_with_label(umount ? "Unmount" : "Mount");
1171 g_signal_connect(context.button, "clicked", G_CALLBACK(&button_clicked), &context);
1172 gtk_box_pack_start(GTK_BOX(box), context.button, FALSE, TRUE, 0);
1174 n_listed = refresh_devices(&context, umount);
1176 if(n_listed || (context.manager && !context.autohide))
1177 gtk_widget_show_all(context.window);
1178 else if(!context.manager)
1182 /* Don't show the main window if no devices were listed. */
1183 dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
1184 "No devices to %s", (umount ? "unmount" : "mount"));
1185 g_signal_connect(dialog, "response", G_CALLBACK(>k_main_quit), NULL);
1186 gtk_widget_show_all(dialog);
1191 free_devices(context.devices);
1192 if(context.inotify_fd>=0)
1193 close(context.inotify_fd);